Compare commits

..

49 Commits

Author SHA1 Message Date
f4ac815d07 hopefully finished with RCon changes.
added more tests.
fixed issues from event changes (there's most definitely still issues related to that)
2018-10-02 12:39:08 -05:00
7fa0b52543 more rcon tweaks, and starting on unit tests for commands bleh 2018-09-29 21:49:12 -05:00
d45729d7e1 clean up rcon, fix a bunch of little things 2018-09-29 14:52:22 -05:00
5d93e7ac57 a ton of stuff and fix migations 2018-09-23 19:45:54 -05:00
0f9d2e92e1 fix for issue #50 2018-09-16 17:51:11 -05:00
4a46abc46d add index to time sent in EFCLientMessage, so we can retrieve faster in context view
set the maximum height of the
add link to profile on client chat
move change history into a seperate service
move around AC penalty processing
2018-09-16 15:34:16 -05:00
7c708f06f3 adds two day ban to drop down on for issue #47 remove IW5 (Pluto) from supported client until it's rewritten 2018-09-13 20:00:41 -05:00
98adfb12d2 update map names for IW4 (issue #48)
only check shared GUID for IW4
optimized get privileged clients query
fine-tuned the version printout to include revision numbers
2018-09-13 14:34:42 -05:00
a786541484 re-implemented auto-upload on publish
fixed the max length migration for MySQL
configure the python projects to be able to be published from command line
optimize find active pentalties query
add feature for issue #38
testing fix for concurrent dict access (in stats plugin)
2018-09-12 19:53:11 -05:00
b9086fd145 fix parsing view angles in exponential form
update RestEase  and CodePages dependencies
optimized the find by name query
add index to name
2018-09-11 14:28:37 -05:00
3d8108f339 fixed profanity bug
fix the shared GUID connect
fix linux  log issue
2018-09-08 20:20:11 -05:00
39596db56e fixed rating and kill streak bug, but uncommenting something I forgot I commented out
Added SharedGUIDKick plugin to kick people with shared GUID
2018-09-08 17:29:30 -05:00
ba5b1e19a6 update readme
add vision average to client stats
other stuff
2018-09-07 22:29:42 -05:00
385879618d add game log server 2018-09-06 13:25:58 -05:00
0c90d02e44 update libraries to pre release
fix remaining issue for issue #32
adds overall ranking to profile page for issue #24
2018-09-04 21:07:34 -05:00
cfbacabb4a 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
2018-09-04 12:40:29 -05:00
672d45df7c fix for issue #45 and #37 2018-09-02 22:09:25 -05:00
20d4ab27d3 add warn event
add alert to IW4ScriptCommands
2018-09-02 21:25:09 -05:00
e77ef69ee8 Added additional properties method to allow easier extension to client properties
updated VPN plugin to use WebClient
message is sent to client trying to execute commands before they are authenticated
fixed rare issue with ToAdmins failing
record bullet distance fraction for client kills (_customcallbacks)
change client level/permissions through webfront
ability to tempban through webfront
2018-09-02 16:59:27 -05:00
cc7628d058 fixed broken broadcast events
events don't get out of order when a invalid event line throws exception
handle the stats history update with no change throwing DBConcurrencyException
2018-08-31 22:35:51 -05:00
46bdc2ac33 moved event API stuff around
finally fixed threading issue (which actually had to do with IW4x log outputs being out of sync (not an issue with my code). What a lot of headache over something that wasn't my fault.
2018-08-30 20:53:00 -05:00
bbefd53db4 think I finished reworking the event system
added http log reading support for debugging remotely
started working on unit test framework
2018-08-28 16:32:59 -05:00
56cb8c50e7 reworked event management (again)
almost finished
2018-08-27 17:07:54 -05:00
0538d9f479 started update for readme
start update for version changes
hopefully fixed pesky stat bug
move vpn detection into script plugin
2018-08-26 19:20:47 -05:00
1343d4959e more support for javascript plugins 2018-08-23 16:16:30 -05:00
ac64d8d3c1 fixed unicode crap stuff in webhook
enable preview of tiered compiliation (faster startup)
ban events are sent to the API properly now
add vpn except id configuration
begin work on javascript plugin support
2018-08-22 20:25:34 -05:00
b5939bbdaf add more event types to discord webhook 2018-08-08 15:36:42 -05:00
a0fafe5797 cleaned up some namespace discrepancies
fixed the coloring for custom groups translation
add reserved slots
add webhook project to show notifications in discord
2018-08-07 13:43:09 -05:00
bbade07646 add localized level names
intellisense suggestion junk
2018-08-03 21:11:58 -05:00
3c0e101f14 appeal website is always show on kick
previously banned for recursion fixed
2018-08-03 19:06:47 -05:00
d0be08629d add page list to manager so we can inject pages into the layout view 2018-08-03 17:10:20 -05:00
9d00d5a16a removed event controller, and added status to api controller
get time passed returns weeks after 90 days
and months after 365
2018-08-02 20:52:35 -05:00
396e5c9215 confirmed working for linux
fixed the database access issue
2018-08-01 21:09:22 -05:00
4ec16d3aa2 increased max events for event api to 100
added GameInfo to EventInfo class
make sure score gets updated properly after authentication
2018-07-30 19:31:00 -05:00
f40bcce44f update to .NET Core 2.1
fix bower repo deprecation
2018-07-29 14:43:42 -05:00
6071ad8653 fix bug with AC failing to ban because of EF issue. 2018-07-05 21:04:34 -05:00
16d7ccd590 fix parsing of certain chat messages
print out the correct exception message when a server is not responding.
prevent log reader from reading before the servers have initialized
2018-07-04 21:09:42 -05:00
87541c4a5a more changes to the event management.
bots ip adjusted
2018-07-01 19:30:38 -05:00
af6361144e moved validate command into shared library.
reworked connection system to read from log file for join/quits and authenticate later with polling
2018-06-30 20:55:16 -05:00
454238a192 finer version numbers work correctly.
fix bug with level being reset.
add {{ADMINS}} to message tokens
modified offset threshold calculation
2018-06-26 20:17:24 -05:00
e7c7145da1 Show time passed since ban instead of "forever"
reworked event api to include all events (sans unknown)
2018-06-16 21:11:25 -05:00
5be6b75ccf [webfront] search by ip and name
[application] levels set properly with multiple GUIDs
[stats] require 3 hours of playtime for top stats recognition
[application] configurable rcon polling rate
2018-06-07 21:19:12 -05:00
e60f612f95 [application] added chat context to profile page
[iw4script] reworked balance to balance based on performance rating
[stats] log penalty context to database
2018-06-05 16:31:36 -05:00
ba023ceeb5 [application] added next map command and token 2018-06-02 21:21:01 -05:00
e3dba96d72 [sharedlibrary] add client meta
[application] add gravatar command
2018-06-01 23:48:10 -05:00
6d0f859a93 more updates to top stats page 2018-06-01 19:55:26 -05:00
696e2d12c9 change table design for rating history 2018-05-31 19:17:52 -05:00
bf68e5672f Add automated ban offense for anti-cheat
add EFClientStatHistory and EFClientAverageStatHistory for tracking change of stats over time
2018-05-30 20:50:20 -05:00
2204686b08 added top player stats
fix for some commands returning multiple matches found when target not required
2018-05-28 20:30:31 -05:00
405 changed files with 25012 additions and 2626 deletions

9
.gitignore vendored
View File

@ -220,7 +220,12 @@ Thumbs.db
DEPLOY
global.min.css
global.min.js
bootstrap-custom.css
bootstrap-custom.min.css
**/Master/static
**/Master/dev_env
**/Master/dev_env
/WebfrontCore/Views/Plugins/Stats
/WebfrontCore/wwwroot/**/dds
/DiscordWebhook/env
/DiscordWebhook/config.dev.json
/GameLogServer/env

View File

@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
namespace IW4MAdmin.Application.API
{
class EventApi : IEventApi
{
Queue<EventInfo> Events = new Queue<EventInfo>();
DateTime LastFlagEvent;
static string[] FlaggedMessageContains =
{
" wh ",
"hax",
"cheat",
" hack ",
"aim",
"wall",
"cheto",
"hak",
"bot"
};
int FlaggedMessageCount;
public Queue<EventInfo> GetEvents() => Events;
public void OnServerEvent(object sender, GameEvent E)
{
if (E.Type == GameEvent.EventType.Say && E.Origin.Level < Player.Permission.Trusted)
{
bool flaggedMessage = false;
foreach (string msg in FlaggedMessageContains)
flaggedMessage = flaggedMessage ? flaggedMessage : E.Data.ToLower().Contains(msg);
if (flaggedMessage)
FlaggedMessageCount++;
if (FlaggedMessageCount > 3)
{
if (Events.Count > 20)
Events.Dequeue();
FlaggedMessageCount = 0;
E.Owner.Broadcast(Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_REPORT"]).Wait(5000);
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
"Chat indicates there may be a cheater",
"Alert",
E.Owner.Hostname, ""));
}
if ((DateTime.UtcNow - LastFlagEvent).Minutes >= 3)
{
FlaggedMessageCount = 0;
LastFlagEvent = DateTime.Now;
}
}
if (E.Type == GameEvent.EventType.Report)
{
Events.Enqueue(new EventInfo(
EventInfo.EventType.ALERT,
EventInfo.EventVersion.IW4MAdmin,
$"**{E.Origin.Name}** has reported **{E.Target.Name}** for: {E.Data.Trim()}",
E.Target.Name, E.Origin.Name, ""));
}
}
}
}

View File

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using RestEase;
namespace IW4MAdmin.Application.API.GameLogServer
{
[Header("User-Agent", "IW4MAdmin-RestEase")]
public interface IGameLogServer
{
[Get("log/{path}")]
Task<LogInfo> Log([Path] string path);
}
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application.API.GameLogServer
{
public class LogInfo
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("length")]
public int Length { get; set; }
[JsonProperty("data")]
public string Data { get; set; }
}
}

View File

@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.API.Master
public class Endpoint
{
#if !DEBUG
private static IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
private static readonly IMasterApi api = RestClient.For<IMasterApi>("http://api.raidmax.org:5000");
#else
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
#endif

View File

@ -2,10 +2,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
<Version>2.1.0</Version>
<Version>2.1.9.2</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Product>IW4MAdmin</Product>
@ -23,12 +23,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RestEase" Version="1.4.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
<PackageReference Include="RestEase" Version="1.4.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
</ItemGroup>
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<TieredCompilation>true</TieredCompilation>
</PropertyGroup>
<ItemGroup>
@ -75,19 +76,21 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
<PackageReference Update="Microsoft.NETCore.App" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="call $(ProjectDir)BuildScripts\PreBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="CurrentAssembly" />
</GetAssemblyIdentity>
<Exec Command="call $(ProjectDir)BuildScripts\PostBuild.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir) %(CurrentAssembly.Version)" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(SolutionDir) $(ProjectDir) $(TargetDir) $(OutDir)" />
<Exec Command="call $(ProjectDir)BuildScripts\PostPublish.bat $(ProjectDir)..\ $(ProjectDir) $(TargetDir) $(OutDir)" />
</Target>
</Project>

View File

@ -2,6 +2,9 @@ set SolutionDir=%1
set ProjectDir=%2
set TargetDir=%3
set OutDir=%4
set Version=%5
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
echo Copying dependency configs
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
@ -17,4 +20,15 @@ xcopy /y "%SolutionDir%Build\Plugins" "%TargetDir%Plugins\"
echo Copying plugins for publish
del %SolutionDir%BUILD\Plugins\Tests.dll
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\WindowsPrerelease\Plugins\"
echo Copying script plugins for publish
xcopy /Y "%SolutionDir%Plugins\ScriptPlugins" "%SolutionDir%Publish\Windows\Plugins\"
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\"

View File

@ -54,6 +54,9 @@ del "%SolutionDir%Publish\Windows\*pdb"
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
echo making start script
echo making start scripts
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.cmd"
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\Windows\StartIW4MAdmin.cmd"
@(echo #!/bin/bash && echo dotnet IW4MAdmin.dll) > "%SolutionDir%Publish\WindowsPrerelease\StartIW4MAdmin.sh"
@(echo #!/bin/bash && echo dotnet IW4MAdmin.dll) > "%SolutionDir%Publish\Windows\StartIW4MAdmin.sh"

View File

@ -0,0 +1,82 @@
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace IW4MAdmin.Application.Core
{
class ClientAuthentication : IClientAuthentication
{
private Queue<Player> ClientAuthenticationQueue;
private Dictionary<long, Player> AuthenticatedClients;
public ClientAuthentication()
{
ClientAuthenticationQueue = new Queue<Player>();
AuthenticatedClients = new Dictionary<long, Player>();
}
public void AuthenticateClients(IList<Player> clients)
{
// we need to un-auth all the clients that have disconnected
var clientNetworkIds = clients.Select(c => c.NetworkId);
var clientsToRemove = AuthenticatedClients.Keys.Where(c => !clientNetworkIds.Contains(c));
// remove them
foreach (long Id in clientsToRemove.ToList())
{
AuthenticatedClients.Remove(Id);
}
// loop through the polled clients to see if they've been authenticated yet
foreach (var client in clients)
{
// they've not been authenticated
if (!AuthenticatedClients.TryGetValue(client.NetworkId, out Player value))
{
// authenticate them
client.State = Player.ClientState.Authenticated;
AuthenticatedClients.Add(client.NetworkId, client);
}
else
{
// this update their ping
// todo: this seems kinda hacky
value.Ping = client.Ping;
value.Score = client.Score;
}
}
// empty out the queue of clients detected through log
while (ClientAuthenticationQueue.Count > 0)
{
// grab each client that's connected via log
var clientToAuthenticate = ClientAuthenticationQueue.Dequeue();
// if they're not already authed, auth them
if (!AuthenticatedClients.TryGetValue(clientToAuthenticate.NetworkId, out Player value))
{
// authenticate them
clientToAuthenticate.State = Player.ClientState.Authenticated;
AuthenticatedClients.Add(clientToAuthenticate.NetworkId, clientToAuthenticate);
}
}
}
public IList<Player> GetAuthenticatedClients()
{
if (AuthenticatedClients.Values.Count > 18)
{
Program.ServerManager.GetLogger().WriteWarning($"auth client count is {AuthenticatedClients.Values.Count}, this is bad");
return AuthenticatedClients.Values.Take(18).ToList();
}
return AuthenticatedClients.Values.ToList();
}
public void RequestClientAuthentication(Player client)
{
ClientAuthenticationQueue.Enqueue(client);
}
}
}

View File

@ -171,7 +171,7 @@
},
{
"Alias": "IW4 Credits",
"Alias": "Test map",
"Name": "iw4_credits"
},
@ -190,6 +190,11 @@
"Name": "mp_cargoship_sh"
},
{
"Alias": "Cargoship",
"Name": "mp_cargoship"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
@ -216,23 +221,43 @@
},
{
"Alias": "Favela - Tropical",
"Alias": "Tropical Favela",
"Name": "mp_fav_tropical"
},
{
"Alias": "Estate - Tropical",
"Alias": "Tropical Estate",
"Name": "mp_estate_tropical"
},
{
"Alias": "Crash - Tropical",
"Alias": "Tropical Crash",
"Name": "mp_crash_tropical"
},
{
"Alias": "Forgotten City",
"Name": "mp_bloc_sh"
},
{
"Alias": "Crossfire",
"Name": "mp_cross_fire"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Oilrig",
"Name": "oilrig"
},
{
"Name": "Village",
"Alias": "co_hunted"
}
]
},

View File

@ -10,92 +10,176 @@ namespace IW4MAdmin.Application.EventParsers
{
class IW4EventParser : IEventParser
{
private const string SayRegex = @"(say|sayteam);(.{1,32});([0-9]+)(.*);(.*)";
public virtual GameEvent GetEvent(Server server, string logLine)
{
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
string[] lineSplit = logLine.Split(';');
string cleanedEventLine = Regex.Replace(lineSplit[0], @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
string eventType = lineSplit[0];
if (cleanedEventLine[0] == 'K')
if (eventType == "JoinTeam")
{
if (!server.CustomCallback)
var origin = server.GetPlayersAsList().FirstOrDefault(c => c.NetworkId == lineSplit[1].ConvertLong());
return new GameEvent()
{
return new GameEvent()
{
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server
};
}
Type = GameEvent.EventType.JoinTeam,
Data = eventType,
Origin = origin,
Owner = server
};
}
if (cleanedEventLine == "say" || cleanedEventLine == "sayteam")
if (eventType == "say" || eventType == "sayteam")
{
string message = lineSplit[4].Replace("\x15", "");
var matchResult = Regex.Match(logLine, SayRegex);
if (message[0] == '!' || message[0] == '@')
if (matchResult.Success)
{
string message = matchResult.Groups[5].ToString()
.Replace("\x15", "")
.Trim();
var origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2));
if (message[0] == '!' || message[0] == '@')
{
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Data = message,
Origin = origin,
Owner = server,
Message = message
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Command,
Type = GameEvent.EventType.Say,
Data = message,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Origin = origin,
Owner = server,
Message = message
};
}
return new GameEvent()
{
Type = GameEvent.EventType.Say,
Data = message,
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
Owner = server,
Message = message
};
}
if (cleanedEventLine.Contains("ScriptKill"))
if (eventType == "K")
{
return new GameEvent()
if (!server.CustomCallback)
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
Owner = server
};
}
var origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6));
var target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2));
if (cleanedEventLine.Contains("ScriptDamage"))
{
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
Owner = server
};
}
if (cleanedEventLine[0] == 'D')
{
if (Regex.Match(cleanedEventLine, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = cleanedEventLine,
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()),
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
Type = GameEvent.EventType.Kill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
}
if (cleanedEventLine.Contains("ExitLevel"))
if (eventType == "ScriptKill")
{
var origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong());
var target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong());
return new GameEvent()
{
Type = GameEvent.EventType.ScriptKill,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
if (eventType == "ScriptDamage")
{
var origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong());
var target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong());
return new GameEvent()
{
Type = GameEvent.EventType.ScriptDamage,
Data = logLine,
Origin = origin,
Target = target,
Owner = server
};
}
// damage
if (eventType == "D")
{
if (!server.CustomCallback)
{
if (Regex.Match(eventType, @"^(D);((?:bot[0-9]+)|(?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[A-Z]|[0-9])+);([0-9]+);(axis|allies);(.+);((?:[0-9]+|[a-z]+|_)+);([0-9]+);((?:[A-Z]|_)+);((?:[a-z]|_)+)$").Success)
{
var origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong());
var target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong());
return new GameEvent()
{
Type = GameEvent.EventType.Damage,
Data = eventType,
Origin = origin,
Target = target,
Owner = server
};
}
}
}
// join
if (eventType == "J")
{
var regexMatch = Regex.Match(logLine, @"^(J;)(.{1,32});([0-9]+);(.*)$");
if (regexMatch.Success)
{
return new GameEvent()
{
Type = GameEvent.EventType.Join,
Data = logLine,
Owner = server,
Origin = new Player()
{
Name = regexMatch.Groups[4].ToString().StripColors(),
NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
State = Player.ClientState.Connecting,
CurrentServer = server
}
};
}
}
//if (eventType == "Q")
//{
// var regexMatch = Regex.Match(logLine, @"^(Q;)(.{1,32});([0-9]+);(.*)$");
// if (regexMatch.Success)
// {
// return new GameEvent()
// {
// Type = GameEvent.EventType.Quit,
// Data = logLine,
// Owner = server,
// Origin = new Player()
// {
// Name = regexMatch.Groups[4].ToString().StripColors(),
// NetworkId = regexMatch.Groups[2].ToString().ConvertLong(),
// ClientNumber = Convert.ToInt32(regexMatch.Groups[3].ToString()),
// State = Player.ClientState.Connecting
// }
// };
// }
//}
if (eventType.Contains("ExitLevel"))
{
return new GameEvent()
{
@ -113,9 +197,9 @@ namespace IW4MAdmin.Application.EventParsers
};
}
if (cleanedEventLine.Contains("InitGame"))
if (eventType.Contains("InitGame"))
{
string dump = cleanedEventLine.Replace("InitGame: ", "");
string dump = eventType.Replace("InitGame: ", "");
return new GameEvent()
{

View File

@ -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";
}
}

View File

@ -1,105 +1,23 @@
using SharedLibraryCore;
using SharedLibraryCore.Events;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace IW4MAdmin.Application
{
class GameEventHandler : IEventHandler
{
private ConcurrentQueue<GameEvent> EventQueue;
private Queue<GameEvent> StatusSensitiveQueue;
private IManager Manager;
readonly IManager Manager;
public GameEventHandler(IManager mgr)
{
EventQueue = new ConcurrentQueue<GameEvent>();
StatusSensitiveQueue = new Queue<GameEvent>();
Manager = mgr;
}
public void AddEvent(GameEvent gameEvent)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Got new event of type {gameEvent.Type} for {gameEvent.Owner}");
#endif
// we need this to keep accurate track of the score
if (gameEvent.Type == GameEvent.EventType.Kill ||
gameEvent.Type == GameEvent.EventType.Damage ||
gameEvent.Type == GameEvent.EventType.ScriptDamage ||
gameEvent.Type == GameEvent.EventType.ScriptKill ||
gameEvent.Type == GameEvent.EventType.MapChange)
{
#if DEBUG
Manager.GetLogger().WriteDebug($"Added sensitive event to queue");
#endif
lock (StatusSensitiveQueue)
{
StatusSensitiveQueue.Enqueue(gameEvent);
}
return;
}
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 GetNextSensitiveEvent()
{
if (StatusSensitiveQueue.Count > 0)
{
lock (StatusSensitiveQueue)
{
if (!StatusSensitiveQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue time sensitive event for processing");
}
else
{
return newEvent;
}
}
}
return null;
}
public GameEvent GetNextEvent()
{
if (EventQueue.Count > 0)
{
#if DEBUG
Manager.GetLogger().WriteDebug("Getting next event to be processed");
#endif
if (!EventQueue.TryDequeue(out GameEvent newEvent))
{
Manager.GetLogger().WriteWarning("Could not dequeue event for processing");
}
else
{
return newEvent;
}
}
return null;
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
}
}
}

View File

@ -1,78 +0,0 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.IO;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEvent
{
Server Server;
long PreviousFileSize;
GameLogReader Reader;
string GameLogFile;
class EventState
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEvent(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
Reader = new GameLogReader(gameLogPath, server.EventParser);
Server = server;
Task.Run(async () =>
{
while (!server.Manager.ShutdownRequested())
{
OnEvent(new EventState()
{
Log = server.Manager.GetLogger(),
ServerId = server.ToString()
});
await Task.Delay(100);
}
});
}
private void OnEvent(object state)
{
long newLength = new FileInfo(GameLogFile).Length;
try
{
UpdateLogEvents(newLength);
}
catch (Exception e)
{
((EventState)state).Log.WriteWarning($"Failed to update log event for {((EventState)state).ServerId}");
((EventState)state).Log.WriteDebug($"Exception: {e.Message}");
((EventState)state).Log.WriteDebug($"StackTrace: {e.StackTrace}");
}
}
private void UpdateLogEvents(long fileSize)
{
if (PreviousFileSize == 0)
PreviousFileSize = fileSize;
long fileDiff = fileSize - PreviousFileSize;
if (fileDiff < 1)
return;
PreviousFileSize = fileSize;
var events = Reader.EventsFromLog(Server, fileDiff, 0);
foreach (var ev in events)
Server.Manager.GetEventHandler().AddEvent(ev);
PreviousFileSize = fileSize;
}
}
}

View File

@ -0,0 +1,87 @@
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace IW4MAdmin.Application.IO
{
class GameLogEventDetection
{
Server Server;
long PreviousFileSize;
IGameLogReader Reader;
readonly string GameLogFile;
class EventState
{
public ILogger Log { get; set; }
public string ServerId { get; set; }
}
public GameLogEventDetection(Server server, string gameLogPath, string gameLogName)
{
GameLogFile = gameLogPath;
// todo: abtract this more
if (gameLogPath.StartsWith("http"))
{
Reader = new GameLogReaderHttp(gameLogPath, server.EventParser);
}
else
{
Reader = new GameLogReader(gameLogPath, server.EventParser);
}
Server = server;
}
public void PollForChanges()
{
while (!Server.Manager.ShutdownRequested())
{
if ((Server.Manager as ApplicationManager).IsInitialized)
{
try
{
UpdateLogEvents();
}
catch (Exception e)
{
Server.Logger.WriteWarning($"Failed to update log event for {Server.GetHashCode()}");
Server.Logger.WriteDebug($"Exception: {e.Message}");
Server.Logger.WriteDebug($"StackTrace: {e.StackTrace}");
}
}
Thread.Sleep(Reader.UpdateInterval);
}
}
private void UpdateLogEvents()
{
long fileSize = Reader.Length;
if (PreviousFileSize == 0)
PreviousFileSize = fileSize;
long fileDiff = fileSize - PreviousFileSize;
// this makes the http log get pulled
if (fileDiff < 1 && fileSize != -1)
return;
PreviousFileSize = fileSize;
var events = Reader.ReadEventsFromLog(Server, fileDiff, 0);
foreach (var ev in events)
{
Server.Manager.GetEventHandler().AddEvent(ev);
}
PreviousFileSize = fileSize;
}
}
}

View File

@ -7,10 +7,14 @@ using System.Text;
namespace IW4MAdmin.Application.IO
{
class GameLogReader
class GameLogReader : IGameLogReader
{
IEventParser Parser;
string LogFile;
readonly string LogFile;
public long Length => new FileInfo(LogFile).Length;
public int UpdateInterval => 300;
public GameLogReader(string logFile, IEventParser parser)
{
@ -18,7 +22,7 @@ namespace IW4MAdmin.Application.IO
Parser = parser;
}
public ICollection<GameEvent> EventsFromLog(Server server, long fileSizeDiff, long startPosition)
public ICollection<GameEvent> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
// allocate the bytes for the new log lines
List<string> logLines = new List<string>();

View File

@ -0,0 +1,72 @@
using IW4MAdmin.Application.API.GameLogServer;
using RestEase;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Net.Http;
using static SharedLibraryCore.Utilities;
namespace IW4MAdmin.Application.IO
{
/// <summary>
/// provides capibility of reading log files over HTTP
/// </summary>
class GameLogReaderHttp : IGameLogReader
{
readonly IEventParser Parser;
readonly IGameLogServer Api;
readonly string LogFile;
public GameLogReaderHttp(string logFile, IEventParser parser)
{
LogFile = logFile;
Parser = parser;
Api = RestClient.For<IGameLogServer>(logFile);
}
public long Length => -1;
public int UpdateInterval => 1000;
public ICollection<GameEvent> ReadEventsFromLog(Server server, long fileSizeDiff, long startPosition)
{
#if DEBUG == true
server.Logger.WriteDebug($"Begin reading {fileSizeDiff} from http log");
#endif
var events = new List<GameEvent>();
string b64Path = server.LogPath.ToBase64UrlSafeString();
var response = Api.Log(b64Path).Result;
if (!response.Success)
{
server.Logger.WriteError($"Could not get log server info of {LogFile}/{b64Path} ({server.LogPath})");
}
// parse each line
foreach (string eventLine in response.Data.Split(Environment.NewLine))
{
if (eventLine.Length > 0)
{
try
{
var e = Parser.GetEvent(server, eventLine);
#if DEBUG == true
server.Logger.WriteDebug($"Parsed event with id {e.Id} from http");
#endif
events.Add(e);
}
catch (Exception e)
{
Program.ServerManager.GetLogger().WriteWarning("Could not properly parse event line");
Program.ServerManager.GetLogger().WriteDebug(e.Message);
Program.ServerManager.GetLogger().WriteDebug(eventLine);
}
}
}
return events;
}
}
}

View File

@ -114,11 +114,11 @@
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
"COMMANDS_WHO_DESC": "give information about yourself",
"GLOBAL_DAYS": "days",
"GLOBAL_TIME_DAYS": "days",
"GLOBAL_ERROR": "Error",
"GLOBAL_HOURS": "hours",
"GLOBAL_TIME_HOURS": "hours",
"GLOBAL_INFO": "Info",
"GLOBAL_MINUTES": "minutes",
"GLOBAL_TIME_MINUTES": "minutes",
"GLOBAL_REPORT": "If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command",
"GLOBAL_VERBOSE": "Verbose",
"GLOBAL_WARNING": "Warning",

View File

@ -114,11 +114,11 @@
"COMMANDS_WARNCLEAR_DESC": "eliminar todas las advertencias de un cliente",
"COMMANDS_WARNCLEAR_SUCCESS": "Todas las advertencias borradas para",
"COMMANDS_WHO_DESC": "da información sobre ti",
"GLOBAL_DAYS": "días",
"GLOBAL_TIME_DAYS": "días",
"GLOBAL_ERROR": "Error",
"GLOBAL_HOURS": "horas",
"GLOBAL_TIME_HOURS": "horas",
"GLOBAL_INFO": "Información",
"GLOBAL_MINUTES": "minutos",
"GLOBAL_TIME_MINUTES": "minutos",
"GLOBAL_REPORT": "Si sospechas que alguien ^5usa cheats ^7usa el comando ^5!report",
"GLOBAL_VERBOSE": "Detallado",
"GLOBAL_WARNING": "Advertencia",

View File

@ -114,11 +114,11 @@
"COMMANDS_WARNCLEAR_DESC": "remove todos os avisos para um cliente",
"COMMANDS_WARNCLEAR_SUCCESS": "Todos as advertências foram apagados para",
"COMMANDS_WHO_DESC": "dá informações sobre você",
"GLOBAL_DAYS": "dias",
"GLOBAL_TIME_DAYS": "dias",
"GLOBAL_ERROR": "Erro",
"GLOBAL_HOURS": "horas",
"GLOBAL_TIME_HOURS": "horas",
"GLOBAL_INFO": "Informação",
"GLOBAL_MINUTES": "minutos",
"GLOBAL_TIME_MINUTES": "minutos",
"GLOBAL_REPORT": "Se você está suspeitando alguém de alguma ^5TRAPAÇA ^7use o comando ^5!report",
"GLOBAL_VERBOSE": "Detalhe",
"GLOBAL_WARNING": "AVISO",

View File

@ -114,11 +114,11 @@
"COMMANDS_WARNCLEAR_DESC": "удалить все предупреждения у игрока",
"COMMANDS_WARNCLEAR_SUCCESS": "Все предупреждения очищены у",
"COMMANDS_WHO_DESC": "предоставить информацию о себе",
"GLOBAL_DAYS": "дней",
"GLOBAL_TIME_DAYS": "дней",
"GLOBAL_ERROR": "Ошибка",
"GLOBAL_HOURS": "часов",
"GLOBAL_TIME_HOURS": "часов",
"GLOBAL_INFO": "Информация",
"GLOBAL_MINUTES": "минут",
"GLOBAL_TIME_MINUTES": "минут",
"GLOBAL_REPORT": "Если вы подозреваете кого-то в ^5ЧИТЕРСТВЕ^7, используйте команду ^5!report",
"GLOBAL_VERBOSE": "Подробно",
"GLOBAL_WARNING": "Предупреждение",

View File

@ -17,8 +17,8 @@ namespace IW4MAdmin.Application
Assert
}
string FileName;
object ThreadLock;
readonly string FileName;
readonly object ThreadLock;
public Logger(string fn)
{
@ -39,7 +39,7 @@ namespace IW4MAdmin.Application
catch (Exception) { }
string LogLine = $"[{DateTime.Now.ToString("HH:mm:ss")}] - {stringType}: {msg}";
string LogLine = $"[{DateTime.Now.ToString("MM.dd.yyy HH:mm:ss.fff")}] - {stringType}: {msg}";
lock (ThreadLock)
{
#if DEBUG

View File

@ -23,18 +23,16 @@ namespace IW4MAdmin.Application
public static void Main(string[] args)
{
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = ConsoleColor.Gray;
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
Version = Math.Round(Version, 2);
Version = Utilities.GetVersionAsDouble();
Console.WriteLine("=====================================================");
Console.WriteLine(" IW4M ADMIN");
Console.WriteLine(" by RaidMax ");
Console.WriteLine($" Version {Version.ToString("0.0")}");
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Index loc = null;
@ -48,8 +46,7 @@ namespace IW4MAdmin.Application
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
loc = Utilities.CurrentLocalization.LocalizationIndex;
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
new ContextSeed(db).Seed().Wait();
ServerManager.Logger.WriteInfo($"Version is {Version}");
var api = API.Master.Endpoint.Get();
@ -107,10 +104,10 @@ namespace IW4MAdmin.Application
ServerManager.Init().Wait();
var consoleTask = Task.Run(() =>
var consoleTask = Task.Run(async () =>
{
String userInput;
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
Player Origin = Utilities.IW4MAdminClient;
do
{
@ -137,7 +134,7 @@ namespace IW4MAdmin.Application
};
ServerManager.GetEventHandler().AddEvent(E);
E.OnProcessed.Wait(5000);
await E.WaitAsync(30 * 1000);
}
Console.Write('>');
@ -164,7 +161,7 @@ namespace IW4MAdmin.Application
}
OnShutdownComplete.Reset();
ServerManager.Start().Wait();
ServerManager.Start();
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
OnShutdownComplete.Set();
}

View File

@ -2,8 +2,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.IO;
using System.Threading.Tasks;
using System.Text;
using System.Reflection;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
@ -12,13 +13,10 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Objects;
using SharedLibraryCore.Services;
using IW4MAdmin.Application.API;
using Microsoft.Extensions.Configuration;
using WebfrontCore;
using SharedLibraryCore.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;
using SharedLibraryCore.Database;
using SharedLibraryCore.Events;
using IW4MAdmin.Application.API.Master;
namespace IW4MAdmin.Application
@ -30,20 +28,26 @@ namespace IW4MAdmin.Application
public Dictionary<int, Player> PrivilegedClients { get; set; }
public ILogger Logger { get; private set; }
public bool Running { get; private set; }
public EventHandler<GameEvent> ServerEventOccurred { get; private set; }
public bool IsInitialized { 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; set; }
public DateTime StartTime { get; private set; }
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
static ApplicationManager Instance;
List<AsyncStatus> TaskStatuses;
readonly List<AsyncStatus> TaskStatuses;
List<Command> Commands;
List<MessageToken> MessageTokens;
readonly List<MessageToken> MessageTokens;
ClientService ClientSvc;
AliasService AliasSvc;
PenaltyService PenaltySvc;
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
EventApi Api;
readonly AliasService AliasSvc;
readonly PenaltyService PenaltySvc;
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
GameEventHandler Handler;
ManualResetEventSlim OnEvent;
ManualResetEventSlim OnQuit;
readonly IPageList PageList;
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
private ApplicationManager()
{
@ -56,11 +60,141 @@ namespace IW4MAdmin.Application
AliasSvc = new AliasService();
PenaltySvc = new PenaltyService();
PrivilegedClients = new Dictionary<int, Player>();
Api = new EventApi();
ServerEventOccurred += Api.OnServerEvent;
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
StartTime = DateTime.UtcNow;
OnEvent = new ManualResetEventSlim();
OnQuit = new ManualResetEventSlim();
PageList = new PageList();
OnServerEvent += OnGameEvent;
OnServerEvent += EventApi.OnGameEvent;
}
private async void OnGameEvent(object sender, GameEventArgs args)
{
#if DEBUG == true
Logger.WriteDebug($"Entering event process for {args.Event.Id}");
#endif
var newEvent = args.Event;
// the event has failed already
if (newEvent.Failed)
{
goto skip;
}
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");
if (newEvent.Type == GameEvent.EventType.Command)
{
newEvent.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["SERVER_DELAYED_EVENT_WAIT"]);
}
// offload it to the player to keep
newEvent.Origin.DelayedEvents.Enqueue(newEvent);
}
// if the target client is not in an authorized state (detected by RCon) don't execute the event
else 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);
}
else
{
await newEvent.Owner.ExecuteEvent(newEvent);
// save the event info to the database
var changeHistorySvc = new ChangeHistoryService();
await changeHistorySvc.Add(args.Event);
// todo: this is a hacky mess
if (newEvent.Origin?.DelayedEvents.Count > 0 &&
(//newEvent.Origin?.State == Player.ClientState.Connected ||
newEvent.Type == GameEvent.EventType.Connect))
{
var events = newEvent.Origin.DelayedEvents;
// add the delayed event to the queue
while (events.Count > 0)
{
var oldEvent = events.Dequeue();
var e = new GameEvent()
{
Type = oldEvent.Type,
Origin = newEvent.Origin,
Data = oldEvent.Data,
Extra = oldEvent.Extra,
Owner = oldEvent.Owner,
Message = oldEvent.Message,
Target = oldEvent.Target,
Remote = oldEvent.Remote
};
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 ignored because the target has left");
// hack: don't do anything with the event because the target is invalid
e.Type = GameEvent.EventType.Unknown;
}
}
Logger.WriteDebug($"Adding delayed event of type {e.Type} for {e.Origin} back for processing");
this.GetEventHandler().AddEvent(e);
}
}
}
#if DEBUG
Logger.WriteDebug($"Processed event with id {newEvent.Id}");
#endif
}
// this happens if a plugin requires login
catch (AuthorizationException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Permission;
newEvent.Origin.Tell($"{Utilities.CurrentLocalization.LocalizationIndex["COMMAND_NOTAUTHORIZED"]} - {ex.Message}");
}
catch (NetworkException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteError(ex.Message);
Logger.WriteDebug(ex.GetExceptionInfo());
}
catch (ServerException ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteWarning(ex.Message);
}
catch (Exception ex)
{
newEvent.FailReason = GameEvent.EventFailReason.Exception;
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {newEvent.Owner}");
Logger.WriteDebug(ex.GetExceptionInfo());
}
skip:
// tell anyone waiting for the output that we're done
newEvent.OnProcessed.Set();
}
public IList<Server> GetServers()
@ -78,16 +212,39 @@ namespace IW4MAdmin.Application
return Instance ?? (Instance = new ApplicationManager());
}
public async Task UpdateStatus(object state)
public async Task UpdateServerStates()
{
var taskList = new List<Task>();
// store the server hash code and task for it
var runningUpdateTasks = new Dictionary<int, Task>();
while (Running)
{
taskList.Clear();
foreach (var server in Servers)
// select the server ids that have completed the update task
var serverTasksToRemove = runningUpdateTasks
.Where(ut => ut.Value.Status == TaskStatus.RanToCompletion ||
ut.Value.Status == TaskStatus.Canceled ||
ut.Value.Status == TaskStatus.Faulted)
.Select(ut => ut.Key)
.ToList();
// this is to prevent the log reader from starting before the initial
// query of players on the server
if (serverTasksToRemove.Count > 0)
{
taskList.Add(Task.Run(async () =>
IsInitialized = true;
}
// remove the update tasks as they have completd
foreach (int serverId in serverTasksToRemove)
{
runningUpdateTasks.Remove(serverId);
}
// select the servers where the tasks have completed
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 () =>
{
try
{
@ -97,49 +254,25 @@ namespace IW4MAdmin.Application
catch (Exception e)
{
Logger.WriteWarning($"Failed to update status for {server}");
Logger.WriteDebug($"Exception: {e.Message}");
Logger.WriteDebug($"StackTrace: {e.StackTrace}");
Logger.WriteDebug(e.GetExceptionInfo());
}
}));
}
#if DEBUG
Logger.WriteDebug($"{taskList.Count} servers queued for stats updates");
Logger.WriteDebug($"{runningUpdateTasks.Count} servers queued for stats updates");
ThreadPool.GetMaxThreads(out int workerThreads, out int n);
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
#endif
await Task.WhenAll(taskList.ToArray());
GameEvent sensitiveEvent;
while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null)
{
try
{
await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent);
#if DEBUG
Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}");
await Task.Delay(10000);
#else
await Task.Delay(ConfigHandler.Configuration().RConPollRate);
#endif
}
catch (NetworkException e)
{
Logger.WriteError(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
Logger.WriteDebug(e.Message);
}
catch (Exception E)
{
Logger.WriteError($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_EXCEPTION"]} {sensitiveEvent.Owner}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
}
sensitiveEvent.OnProcessed.Set();
}
await Task.Delay(2500);
}
// trigger the event processing loop to end
SetHasEvent();
}
public async Task Init()
@ -147,6 +280,12 @@ namespace IW4MAdmin.Application
Running = true;
#region DATABASE
using (var db = new DatabaseContext(GetApplicationSettings().Configuration()?.ConnectionString, GetApplicationSettings().Configuration()?.DatabaseProvider))
{
await new ContextSeed(db).Seed();
}
// todo: optimize this (or replace it)
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
.Select(c => new
{
@ -218,7 +357,7 @@ namespace IW4MAdmin.Application
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
{
config.WebfrontBindUrl = "http://127.0.0.1:1624";
config.WebfrontBindUrl = "http://0.0.0.0:1624";
await ConfigHandler.Save();
}
}
@ -288,6 +427,8 @@ namespace IW4MAdmin.Application
Commands.Add(new CKillServer());
Commands.Add(new CSetPassword());
Commands.Add(new CPing());
Commands.Add(new CSetGravatar());
Commands.Add(new CNextMap());
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
Commands.Add(C);
@ -310,7 +451,15 @@ namespace IW4MAdmin.Application
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
// add the start event for this server
Handler.AddEvent(new GameEvent(GameEvent.EventType.Start, "Server started", null, null, ServerInstance));
var e = new GameEvent()
{
Type = GameEvent.EventType.Start,
Data = $"{ServerInstance.GameName} started",
Owner = ServerInstance
};
Handler.AddEvent(e);
}
catch (ServerException e)
@ -397,84 +546,23 @@ 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
#if !DEBUG
// start heartbeat
Task.Run(() => SendHeartbeat(new HeartbeatState()));
#endif
Task.Run(() => UpdateStatus(null));
#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<Task>();
async Task processEvent(GameEvent newEvent)
{
try
{
await newEvent.Owner.ExecuteEvent(newEvent);
#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(Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]);
Logger.WriteDebug(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;
var _ = Task.Run(() => SendHeartbeat(new HeartbeatState()));
_ = Task.Run(() => UpdateServerStates());
while (Running)
{
// wait for new event to be added
OnEvent.Wait();
// todo: sequencially or parallelize?
while ((queuedEvent = Handler.GetNextEvent()) != null)
{
await processEvent(queuedEvent);
}
// this should allow parallel processing of events
// await Task.WhenAll(eventList);
// signal that all events have been processed
OnEvent.Reset();
OnQuit.Wait();
OnQuit.Reset();
}
#if !DEBUG
foreach (var S in _servers)
await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]);
#endif
_servers.Clear();
}
public void Stop()
{
Running = false;
// trigger the event processing loop to end
SetHasEvent();
}
public ILogger GetLogger()
@ -487,28 +575,23 @@ namespace IW4MAdmin.Application
return MessageTokens;
}
public IList<Player> GetActiveClients()
{
var ActiveClients = new List<Player>();
foreach (var server in _servers)
ActiveClients.AddRange(server.Players.Where(p => p != null));
return ActiveClients;
}
public IList<Player> GetActiveClients() => _servers.SelectMany(s => s.Players).Where(p => p != null).ToList();
public ClientService GetClientService() => ClientSvc;
public AliasService GetAliasService() => AliasSvc;
public PenaltyService GetPenaltyService() => PenaltySvc;
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
public IEventApi GetEventApi() => Api;
public bool ShutdownRequested() => !Running;
public IEventHandler GetEventHandler() => Handler;
public void SetHasEvent()
{
OnEvent.Set();
OnQuit.Set();
}
public IList<Assembly> GetPluginAssemblies() => SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
public IPageList GetPageList() => PageList;
}
}

View File

@ -1,42 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Application.Misc
{
public class VPNCheck
{
public static async Task<bool> UsingVPN(string ip, string apiKey)
{
#if DEBUG
return await Task.FromResult(false);
#else
try
{
using (var RequestClient = new System.Net.Http.HttpClient())
{
RequestClient.DefaultRequestHeaders.Add("X-Key", apiKey);
string response = await RequestClient.GetStringAsync($"http://v2.api.iphub.info/ip/{ip}");
var responseJson = JsonConvert.DeserializeObject<JObject>(response);
int blockType = Convert.ToInt32(responseJson["block"]);
/*if (responseJson.ContainsKey("isp"))
{
if (responseJson["isp"].ToString() == "TSF-IP-CORE")
return true;
}*/
return blockType == 1;
}
}
catch (Exception)
{
return false;
}
#endif
}
}
}

26
Application/PageList.cs Normal file
View File

@ -0,0 +1,26 @@
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Application
{
/// <summary>
/// implementatin of IPageList that supports basic
/// pages title and page location for webfront
/// </summary>
class PageList : IPageList
{
/// <summary>
/// Pages dictionary
/// Key = page name
/// Value = page location (url)
/// </summary>
public IDictionary<string, string> Pages { get; set; }
public PageList()
{
Pages = new Dictionary<string, string>();
}
}
}

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<TargetFramework>netcoreapp2.0</TargetFramework>
<PublishDir>C:\Projects\IW4M-Admin\Publish\Windows</PublishDir>
</PropertyGroup>
</Project>

View File

@ -1,14 +1,13 @@
using Application.RconParsers;
using SharedLibraryCore.RCon;
using SharedLibraryCore.RCon;
using System;
using System.Collections.Generic;
using System.Text;
namespace Application.RconParsers
namespace IW4MAdmin.Application.RconParsers
{
class IW3RConParser : IW4RConParser
{
private static CommandPrefix Prefixes = new CommandPrefix()
private static readonly CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",

View File

@ -10,11 +10,11 @@ using SharedLibraryCore;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
namespace Application.RconParsers
namespace IW4MAdmin.Application.RconParsers
{
class IW4RConParser : IRConParser
{
private static CommandPrefix Prefixes = new CommandPrefix()
private static readonly CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tellraw {0} {1}",
Say = "sayraw {0}",
@ -23,11 +23,11 @@ namespace Application.RconParsers
TempBan = "tempbanclient {0} \"{1}\""
};
private static string StatusRegex = @"^( *[0-9]+) +-*([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16}|(?:[a-z]|[0-9]){32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
private static readonly string StatusRegex = @"^( *[0-9]+) +-*([0-9]+) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){16}|(?:[a-z]|[0-9]){32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +([0-9]+) +(\d+\.\d+\.\d+.\d+\:-*\d{1,5}|0+.0+:-*\d{1,5}|loopback) +(-*[0-9]+) +([0-9]+) *$";
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
}
@ -111,17 +111,12 @@ namespace Application.RconParsers
Name = name,
NetworkId = networkId,
ClientNumber = clientNumber,
IPAddress = ip,
IPAddress = ip == 0 ? int.MinValue : ip,
Ping = ping,
Score = score,
IsBot = ip == 0
IsBot = ip == 0,
State = Player.ClientState.Connecting
};
if (P.IsBot)
{
P.IPAddress = P.ClientNumber + 1;
}
StatusPlayers.Add(P);
}
}

View File

@ -2,21 +2,19 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Text;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Objects;
using SharedLibraryCore.RCon;
using SharedLibraryCore.Exceptions;
using System.Text;
using System.Linq;
using System.Net.Http;
namespace Application.RconParsers
namespace IW4MAdmin.Application.RconParsers
{
public class IW5MRConParser : IRConParser
{
private static CommandPrefix Prefixes = new CommandPrefix()
private static readonly CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
@ -155,7 +153,8 @@ namespace Application.RconParsers
IPAddress = ipAddress,
Ping = Ping,
Score = score,
IsBot = false
IsBot = false,
State = Player.ClientState.Connecting
};
StatusPlayers.Add(p);

View File

@ -12,47 +12,11 @@ using System.Text;
using System.Linq;
using System.Net.Http;
namespace Application.RconParsers
namespace IW4MAdmin.Application.RconParsers
{
public class T6MRConParser : IRConParser
{
class T6MResponse
{
public class SInfo
{
public short Com_maxclients { get; set; }
public string Game { get; set; }
public string Gametype { get; set; }
public string Mapname { get; set; }
public short NumBots { get; set; }
public short NumClients { get; set; }
public short Round { get; set; }
public string Sv_hostname { get; set; }
}
public class PInfo
{
public short Assists { get; set; }
public string Clan { get; set; }
public short Deaths { get; set; }
public short Downs { get; set; }
public short Headshots { get; set; }
public short Id { get; set; }
public bool IsBot { get; set; }
public short Kills { get; set; }
public string Name { get; set; }
public short Ping { get; set; }
public short Revives { get; set; }
public int Score { get; set; }
public long Xuid { get; set; }
public string Ip { get; set; }
}
public SInfo Info { get; set; }
public PInfo[] Players { get; set; }
}
private static CommandPrefix Prefixes = new CommandPrefix()
private static readonly CommandPrefix Prefixes = new CommandPrefix()
{
Tell = "tell {0} {1}",
Say = "say {0}",
@ -73,7 +37,6 @@ namespace Application.RconParsers
{
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
if (LineSplit.Length < 2)
{
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
@ -103,8 +66,6 @@ namespace Application.RconParsers
{
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
return ClientsFromStatus(response);
//return ClientsFromResponse(connection);
}
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
@ -114,41 +75,6 @@ namespace Application.RconParsers
return true;
}
private async Task<List<Player>> ClientsFromResponse(Connection conn)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri($"http://{conn.Endpoint.Address}:{conn.Endpoint.Port}/");
try
{
var parameters = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("rcon_password", conn.RConPassword)
});
var serverResponse = await client.PostAsync("/info", parameters);
var serverResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject<T6MResponse>(await serverResponse.Content.ReadAsStringAsync());
return serverResponseObject.Players.Select(p => new Player()
{
Name = p.Name,
NetworkId = p.Xuid,
ClientNumber = p.Id,
IPAddress = p.Ip.Split(':')[0].ConvertToIP(),
Ping = p.Ping,
Score = p.Score,
IsBot = p.IsBot,
}).ToList();
}
catch (HttpRequestException e)
{
throw new NetworkException(e.Message);
}
}
}
private List<Player> ClientsFromStatus(string[] status)
{
List<Player> StatusPlayers = new List<Player>();
@ -174,9 +100,6 @@ namespace Application.RconParsers
#endif
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
int score = 0;
// todo: fix this when T6M score is valid ;)
//int score = Int32.Parse(playerInfo[1]);
var p = new Player()
{
Name = name,
@ -184,7 +107,8 @@ namespace Application.RconParsers
ClientNumber = clientId,
IPAddress = ipAddress,
Ping = Ping,
Score = score,
Score = 0,
State = Player.ClientState.Connecting,
IsBot = networkId == 0
};

View File

@ -14,21 +14,22 @@ using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Exceptions;
using SharedLibraryCore.Localization;
using Application.Misc;
using Application.RconParsers;
using IW4MAdmin.Application.RconParsers;
using IW4MAdmin.Application.EventParsers;
using IW4MAdmin.Application.IO;
using SharedLibraryCore.Localization;
namespace IW4MAdmin
{
public class IW4MServer : Server
{
private static Index loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEvent LogEvent;
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
private GameLogEventDetection LogEvent;
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg) { }
public IW4MServer(IManager mgr, ServerConfiguration cfg) : base(mgr, cfg)
{
}
public override int GetHashCode()
{
@ -49,24 +50,38 @@ namespace IW4MAdmin
return id;
}
public async Task OnPlayerJoined(Player logClient)
{
var existingClient = Players[logClient.ClientNumber];
if (existingClient == null ||
(existingClient.NetworkId != logClient.NetworkId &&
existingClient.State != Player.ClientState.Connected))
{
Logger.WriteDebug($"Log detected {logClient} joining");
Players[logClient.ClientNumber] = logClient;
}
await Task.CompletedTask;
}
override public async Task<bool> AddPlayer(Player polledPlayer)
{
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
polledPlayer.Ping < 1 ||
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)
// set this when they are waiting for authentication
if (Players[polledPlayer.ClientNumber] == null &&
polledPlayer.State == Player.ClientState.Connecting)
{
// update their ping & score
Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping;
Players[polledPlayer.ClientNumber].Score = polledPlayer.Score;
return true;
Players[polledPlayer.ClientNumber] = polledPlayer;
return false;
}
#if !DEBUG
if (polledPlayer.Name.Length < 3)
{
@ -76,7 +91,7 @@ namespace IW4MAdmin
return false;
}
if (Players.FirstOrDefault(p => p != null && p.Name == polledPlayer.Name) != null)
if (Players.FirstOrDefault(p => p != null && p.Name == polledPlayer.Name && p.NetworkId != polledPlayer.NetworkId) != null)
{
Logger.WriteDebug($"Kicking {polledPlayer} because their name is already in use");
string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, polledPlayer.ClientNumber, loc["SERVER_KICK_NAME_INUSE"]);
@ -137,85 +152,108 @@ namespace IW4MAdmin
// we need to update their new ip and name to the virtual property
client.Name = polledPlayer.Name;
client.IPAddress = polledPlayer.IPAddress;
await Manager.GetClientService().Update(client);
}
else if (existingAlias.Name == polledPlayer.Name)
else
{
client.CurrentAlias = existingAlias;
client.CurrentAliasId = existingAlias.AliasId;
await Manager.GetClientService().Update(client);
client.Name = existingAlias.Name;
client.IPAddress = existingAlias.IPAddress;
}
await Manager.GetClientService().Update(client);
player = client.AsPlayer();
}
// reserved slots stuff
if ((MaxClients - ClientNum) < ServerConfig.ReservedSlotNumber &&
!player.IsPrivileged())
{
Logger.WriteDebug($"Kicking {polledPlayer} their spot is reserved");
string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, polledPlayer.ClientNumber, loc["SERVER_KICK_SLOT_IS_RESERVED"]);
await this.ExecuteCommandAsync(formattedKick);
return false;
}
Logger.WriteInfo($"Client {player} connected...");
// Do the player specific stuff
player.ClientNumber = polledPlayer.ClientNumber;
player.IsBot = polledPlayer.IsBot;
player.Score = polledPlayer.Score;
player.CurrentServer = this;
player.DelayedEvents = (Players[player.ClientNumber]?.DelayedEvents) ?? new Queue<GameEvent>();
Players[player.ClientNumber] = player;
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress);
var currentBan = activePenalties.FirstOrDefault(b => b.Expires > DateTime.UtcNow);
var currentAutoFlag = activePenalties.Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1)
.Where(p => p.Active)
.OrderByDescending(p => p.When)
.FirstOrDefault();
// remove their auto flag status after a week
if (currentAutoFlag != null && (DateTime.Now - currentAutoFlag.When).TotalDays > 7)
if (player.Level == Player.Permission.Flagged &&
currentAutoFlag != null &&
(DateTime.Now - currentAutoFlag.When).TotalDays > 7)
{
player.Level = Player.Permission.User;
}
#if DEBUG == false
if (currentBan != null)
{
Logger.WriteInfo($"Banned client {player} trying to connect...");
var autoKickClient = (await Manager.GetClientService().Get(1)).AsPlayer();
var autoKickClient = Utilities.IW4MAdminClient;
autoKickClient.CurrentServer = this;
if (currentBan.Type == Penalty.PenaltyType.TempBan)
// the player is permanently banned
if (currentBan.Type == Penalty.PenaltyType.Ban)
{
string formattedKick = String.Format(RconParser.GetCommandPrefixes().Kick, polledPlayer.ClientNumber, $"{loc["SERVER_TB_REMAIN"]} ({(currentBan.Expires - DateTime.UtcNow).TimeSpanText()} left)");
// don't store the kick message
string formattedKick = String.Format(
RconParser.GetCommandPrefixes().Kick,
polledPlayer.ClientNumber,
$"{loc["SERVER_BAN_PREV"]} {currentBan.Offense} ({loc["SERVER_BAN_APPEAL"]} {Website})");
await this.ExecuteCommandAsync(formattedKick);
}
else
await player.Kick($"{loc["SERVER_BAN_PREV"]} {currentBan.Offense}", autoKickClient);
else
{
string formattedKick = String.Format(
RconParser.GetCommandPrefixes().Kick,
polledPlayer.ClientNumber,
$"{loc["SERVER_TB_REMAIN"]} ({(currentBan.Expires - DateTime.UtcNow).TimeSpanText()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})");
await this.ExecuteCommandAsync(formattedKick);
}
// reban the "evading" guid
if (player.Level != Player.Permission.Banned && currentBan.Type == Penalty.PenaltyType.Ban)
await player.Ban($"{loc["SERVER_BAN_PREV"]} {currentBan.Offense}", autoKickClient);
{
// hack: re apply the automated offense to the reban
if (currentBan.AutomatedOffense != null)
{
autoKickClient.AdministeredPenalties.Add(new EFPenalty() { AutomatedOffense = currentBan.AutomatedOffense });
}
player.Ban($"{currentBan.Offense}", autoKickClient);
}
// they didn't fully connect so empty their slot
Players[player.ClientNumber] = null;
return true;
return false;
}
#endif
Logger.WriteInfo($"Client {player} connecting...");
if (!Manager.GetApplicationSettings().Configuration().EnableClientVPNs &&
await VPNCheck.UsingVPN(player.IPAddressString, Manager.GetApplicationSettings().Configuration().IPHubAPIKey))
{
await player.Kick(Utilities.CurrentLocalization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], new Player() { ClientId = 1 });
return true;
}
var e = new GameEvent()
{
Type = GameEvent.EventType.Connect,
Origin = player,
Owner = this
};
Manager.GetEventHandler().AddEvent(e);
player.State = Player.ClientState.Connected;
return true;
}
catch (Exception E)
catch (Exception ex)
{
Manager.GetLogger().WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {polledPlayer.Name}::{polledPlayer.NetworkId}");
Manager.GetLogger().WriteDebug(E.StackTrace);
Manager.GetLogger().WriteDebug(ex.Message);
Manager.GetLogger().WriteDebug(ex.StackTrace);
return false;
}
}
@ -226,166 +264,40 @@ namespace IW4MAdmin
if (cNum >= 0 && Players[cNum] != null)
{
Player Leaving = Players[cNum];
Logger.WriteInfo($"Client {Leaving} disconnecting...");
var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this);
Manager.GetEventHandler().AddEvent(e);
// wait until the disconnect event is complete
e.OnProcessed.Wait();
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
Leaving.LastConnection = DateTime.UtcNow;
await Manager.GetClientService().Update(Leaving);
Players[cNum] = null;
}
}
//Process requested command correlating to an event
// todo: this needs to be removed out of here
override public async Task<Command> ValidateCommand(GameEvent E)
{
string CommandString = E.Data.Substring(1, E.Data.Length - 1).Split(' ')[0];
E.Message = E.Data;
Command C = null;
foreach (Command cmd in Manager.GetCommands())
{
if (cmd.Name == CommandString.ToLower() || cmd.Alias == CommandString.ToLower())
C = cmd;
}
if (C == null)
{
await E.Origin.Tell(loc["COMMAND_UNKNOWN"]);
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
}
E.Data = E.Data.RemoveWords(1);
String[] Args = E.Data.Trim().Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (E.Origin.Level < C.Permission)
{
await E.Origin.Tell(loc["COMMAND_NOACCESS"]);
throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\"");
}
if (Args.Length < (C.RequiredArgumentCount))
{
await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
await E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
}
if (C.RequiresTarget || Args.Length > 0)
{
if (!Int32.TryParse(Args[0], out int cNum))
cNum = -1;
if (Args[0][0] == '@') // user specifying target by database ID
// occurs when the player disconnects via log before being authenticated by RCon
if (Leaving.State != Player.ClientState.Connected)
{
int dbID = -1;
int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out dbID);
var found = await Manager.GetClientService().Get(dbID);
if (found != null)
{
E.Target = found.AsPlayer();
E.Target.CurrentServer = this as IW4MServer;
E.Owner = this as IW4MServer;
E.Data = String.Join(" ", Args.Skip(1));
}
Players[cNum] = null;
}
else if (Args[0].Length < 3 && cNum > -1 && cNum < MaxClients) // user specifying target by client num
else
{
if (Players[cNum] != null)
{
E.Target = Players[cNum];
E.Data = String.Join(" ", Args.Skip(1));
}
}
List<Player> matchingPlayers;
if (E.Target == null) // Find active player including quotes (multiple words)
{
matchingPlayers = GetClientByName(E.Data.Trim());
if (matchingPlayers.Count > 1)
{
await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
}
else if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
string escapedName = Regex.Escape(E.Target.Name);
var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
if (E.Data.Length == 0 && C.RequiredArgumentCount > 1)
{
await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
await E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
}
}
}
if (E.Target == null) // Find active player as single word
{
matchingPlayers = GetClientByName(Args[0]);
if (matchingPlayers.Count > 1)
{
await E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
foreach (var p in matchingPlayers)
await E.Origin.Tell($"[^3{p.ClientNumber}^7] {p.Name}");
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
}
else if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
string escapedName = Regex.Escape(E.Target.Name);
string escapedArg = Regex.Escape(Args[0]);
var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
if ((E.Data.Trim() == E.Target.Name.ToLower().Trim() ||
E.Data == String.Empty) &&
C.RequiresTarget)
{
await E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
await E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
}
}
}
if (E.Target == null && C.RequiresTarget)
{
await E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\"");
Logger.WriteInfo($"Client {Leaving} [{Leaving.State.ToString().ToLower()}] disconnecting...");
Leaving.State = Player.ClientState.Disconnecting;
Leaving.TotalConnectionTime += Leaving.ConnectionLength;
Leaving.LastConnection = DateTime.UtcNow;
await Manager.GetClientService().Update(Leaving);
Players[cNum] = null;
}
}
E.Data = E.Data.Trim();
return C;
}
public override async Task ExecuteEvent(GameEvent E)
{
bool canExecuteCommand = true;
await ProcessEvent(E);
Manager.GetEventApi().OnServerEvent(this, E);
if (!await ProcessEvent(E))
{
return;
}
Command C = null;
if (E.Type == GameEvent.EventType.Command)
{
try
{
C = await ValidateCommand(E);
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E);
}
catch (CommandException e)
@ -399,47 +311,31 @@ namespace IW4MAdmin
}
}
// this allows us to catch exceptions but still run it parallel
async Task pluginHandlingAsync(Task onEvent, string pluginName)
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
{
try
{
await onEvent;
await plugin.OnEventAsync(E, this);
}
// this happens if a plugin (login) wants to stop commands from executing
catch (AuthorizationException e)
{
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
canExecuteCommand = false;
}
catch (Exception Except)
{
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
while (Except.InnerException != null)
{
Except = Except.InnerException;
Logger.WriteDebug($"Inner exception: {Except.Message}");
}
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
Logger.WriteDebug(Except.GetExceptionInfo());
}
}
var pluginTasks = SharedLibraryCore.Plugins.PluginImporter.ActivePlugins.
Select(p => pluginHandlingAsync(p.OnEventAsync(E, this), p.Name));
// execute all the plugin updates simultaneously
await Task.WhenAll(pluginTasks);
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command &&
E.Extra != null &&
(canExecuteCommand ||
E.Origin?.Level == Player.Permission.Console))
{
await (((Command)E.Extra).ExecuteAsync(E));
var _ = (((Command)E.Extra).ExecuteAsync(E));
}
}
@ -448,51 +344,136 @@ namespace IW4MAdmin
/// </summary>
/// <param name="E"></param>
/// <returns></returns>
override protected async Task ProcessEvent(GameEvent E)
override protected async Task<bool> ProcessEvent(GameEvent E)
{
if (E.Type == GameEvent.EventType.Connect)
{
// this may be a fix for a hard to reproduce null exception error
lock (ChatHistory)
E.Origin.State = Player.ClientState.Authenticated;
// add them to the server
if (!await AddPlayer(E.Origin))
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin?.Name ?? "ERROR!",
Message = "CONNECTED",
Time = DateTime.UtcNow
});
E.Origin.State = Player.ClientState.Connecting;
Logger.WriteDebug("client didn't pass authentication, so we are discontinuing event");
return false;
}
// hack: makes the event propgate with the correct info
E.Origin = Players[E.Origin.ClientNumber];
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin?.Name ?? "ERROR!",
Message = "CONNECTED",
Time = DateTime.UtcNow
});
if (E.Origin.Level > Player.Permission.Moderator)
await E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
{
E.Origin.Tell(string.Format(loc["SERVER_REPORT_COUNT"], E.Owner.Reports.Count));
}
}
else if (E.Type == GameEvent.EventType.Join)
{
// special case for IW5 when connect is from the log
if (E.Extra != null && GameName == Game.IW5)
{
var logClient = (Player)E.Extra;
var client = (await this.GetStatusAsync())
.Single(c => c.ClientNumber == logClient.ClientNumber &&
c.Name == logClient.Name);
client.NetworkId = logClient.NetworkId;
await OnPlayerJoined(E.Origin);
}
await AddPlayer(client);
else if (E.Type == GameEvent.EventType.Flag)
{
Penalty newPenalty = new Penalty()
{
Type = Penalty.PenaltyType.Flag,
Expires = DateTime.UtcNow,
Offender = E.Target,
Offense = E.Data,
Punisher = E.Origin,
Active = true,
When = DateTime.UtcNow,
Link = E.Target.AliasLink
};
var addedPenalty = await Manager.GetPenaltyService().Create(newPenalty);
E.Target.ReceivedPenalties.Add(addedPenalty);
await Manager.GetClientService().Update(E.Target);
}
else if (E.Type == GameEvent.EventType.Unflag)
{
await Manager.GetClientService().Update(E.Target);
}
else if (E.Type == GameEvent.EventType.Report)
{
this.Reports.Add(new Report()
{
Origin = E.Origin,
Target = E.Target,
Reason = E.Data
});
}
else if (E.Type == GameEvent.EventType.TempBan)
{
await TempBan(E.Data, (TimeSpan)E.Extra, E.Target, E.Origin); ;
}
else if (E.Type == GameEvent.EventType.Ban)
{
await Ban(E.Data, E.Target, E.Origin);
}
else if (E.Type == GameEvent.EventType.Unban)
{
await Unban(E.Data, E.Target, E.Origin);
}
else if (E.Type == GameEvent.EventType.Kick)
{
await Kick(E.Data, E.Target, E.Origin);
}
else if (E.Type == GameEvent.EventType.Quit)
{
var origin = Players.FirstOrDefault(p => p != null && p.NetworkId == E.Origin.NetworkId);
if (origin != null &&
// we only want to forward the event if they are connected.
origin.State == Player.ClientState.Connected &&
// make sure we don't get the disconnect event from every time the game ends
origin.ConnectionLength < Manager.GetApplicationSettings().Configuration().RConPollRate)
{
var e = new GameEvent()
{
Type = GameEvent.EventType.Disconnect,
Origin = origin,
Owner = this
};
Manager.GetEventHandler().AddEvent(e);
}
else if (origin != null &&
origin.State != Player.ClientState.Connected)
{
await RemovePlayer(origin.ClientNumber);
}
}
else if (E.Type == GameEvent.EventType.Disconnect)
{
// this may be a fix for a hard to reproduce null exception error
lock (ChatHistory)
ChatHistory.Add(new ChatInfo()
{
ChatHistory.Add(new ChatInfo()
{
Name = E.Origin.Name,
Message = "DISCONNECTED",
Time = DateTime.UtcNow
});
Name = E.Origin.Name,
Message = "DISCONNECTED",
Time = DateTime.UtcNow
});
var currentState = E.Origin.State;
await RemovePlayer(E.Origin.ClientNumber);
if (currentState != Player.ClientState.Connected)
{
throw new ServerException("Disconnecting player was not in a connected state");
}
}
@ -567,7 +548,10 @@ namespace IW4MAdmin
if (E.Type == GameEvent.EventType.Broadcast)
{
// this is a little ugly but I don't want to change the abstract class
await E.Owner.ExecuteCommandAsync(E.Message);
if (E.Data != null)
{
await E.Owner.ExecuteCommandAsync(E.Data);
}
}
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
@ -577,60 +561,45 @@ namespace IW4MAdmin
// so there will still be at least 1 client left
if (ClientNum < 2)
ChatHistory.Clear();
return true;
}
async Task<int> PollPlayersAsync()
/// <summary>
/// lists the connecting and disconnecting clients via RCon response
/// array index 0 = connecting clients
/// array index 1 = disconnecting clients
/// </summary>
/// <returns></returns>
async Task<IList<Player>[]> PollPlayersAsync()
{
#if DEBUG
var now = DateTime.Now;
List<Player> 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)
{
if (GameName == Game.IW5)
{
if (!CurrentPlayers.Select(c => c.ClientNumber).Contains(client.ClientNumber))
await RemovePlayer(client.ClientNumber);
}
else
{
if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId))
await RemovePlayer(client.ClientNumber);
}
// todo: move out somehwere
var existingClient = Players[client.ClientNumber] ?? client;
existingClient.Ping = client.Ping;
existingClient.Score = client.Score;
}
for (int i = 0; i < CurrentPlayers.Count; i++)
{
// todo: wait til GUID is included in status to fix this
if (GameName != Game.IW5)
await AddPlayer(CurrentPlayers[i]);
}
var disconnectingClients = currentClients.Except(polledClients);
var connectingClients = polledClients.Except(currentClients.Where(c => c.State == Player.ClientState.Connected));
return CurrentPlayers.Count;
return new List<Player>[] { connectingClients.ToList(), disconnectingClients.ToList() };
}
DateTime start = DateTime.Now;
DateTime playerCountStart = DateTime.Now;
DateTime lastCount = DateTime.Now;
DateTime tickTime = DateTime.Now;
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
{
@ -638,20 +607,69 @@ namespace IW4MAdmin
{
if (Manager.ShutdownRequested())
{
for (int i = 0; i < Players.Count; i++)
await RemovePlayer(i);
// todo: fix up disconnect
//for (int i = 0; i < Players.Count; i++)
// await RemovePlayer(i);
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
await plugin.OnUnloadAsync();
}
// only check every 2 minutes if the server doesn't seem to be responding
if ((DateTime.Now - LastPoll).TotalMinutes < 2 && ConnectionErrors >= 1)
return true;
/* if ((DateTime.Now - LastPoll).TotalMinutes < 0.5 && ConnectionErrors >= 1)
return true;*/
try
{
int polledPlayerCount = await PollPlayersAsync();
var polledClients = await PollPlayersAsync();
var waiterList = new List<GameEvent>();
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);
}
// wait for all the disconnect tasks to finish
await Task.WhenAll(waiterList.Select(e => e.WaitAsync()));
waiterList.Clear();
// this are our new connecting clients
foreach (var client in polledClients[0])
{
// this prevents duplicate events from being sent to the event api
if (GetPlayersAsList().Count(c => c.NetworkId == client.NetworkId &&
c.State == Player.ClientState.Connected) != 0)
{
continue;
}
var e = new GameEvent()
{
Type = GameEvent.EventType.Connect,
Origin = client,
Owner = this
};
Manager.GetEventHandler().AddEvent(e);
waiterList.Add(e);
}
// wait for all the connect tasks to finish
await Task.WhenAll(waiterList.Select(e => e.WaitAsync()));
if (ConnectionErrors > 0)
{
@ -708,7 +726,9 @@ namespace IW4MAdmin
string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine);
foreach (string message in messages)
await Broadcast(message);
{
Broadcast(message);
}
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
start = DateTime.Now;
@ -723,6 +743,7 @@ namespace IW4MAdmin
if (e is NetworkException)
{
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
Logger.WriteDebug(e.GetExceptionInfo());
}
return false;
@ -731,15 +752,17 @@ namespace IW4MAdmin
catch (Exception E)
{
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}");
Logger.WriteDebug("Error Message: " + E.Message);
Logger.WriteDebug("Error Trace: " + E.StackTrace);
Logger.WriteDebug(E.GetExceptionInfo());
return false;
}
}
public async Task Initialize()
{
RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW3RConParser();
RconParser = ServerConfig.UseT6MParser ?
(IRConParser)new T6MRConParser() :
new IW3RConParser();
if (ServerConfig.UseIW5MParser)
RconParser = new IW5MRConParser();
@ -806,9 +829,6 @@ namespace IW4MAdmin
this.MaxClients = maxplayers;
this.FSGame = game;
this.Gametype = gametype;
//wait this.SetDvarAsync("sv_kickbantime", 60);
if (logsync.Value == 0 || logfile.Value == string.Empty)
{
// this DVAR isn't set until the a map is loaded
@ -823,41 +843,48 @@ namespace IW4MAdmin
CustomCallback = await ScriptLoaded();
string mainPath = EventParser.GetGameDir();
#if DEBUG
basepath.Value = @"\\192.168.88.253\mw2";
// basepath.Value = @"D:\";
#endif
string logPath;
if (GameName == Game.IW5)
string logPath = string.Empty;
LogPath = game == string.Empty ?
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" :
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
if (GameName == Game.IW5 || ServerConfig.ManualLogPath?.Length > 0)
{
logPath = ServerConfig.ManualLogPath;
}
else
{
logPath = game == string.Empty ?
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{mainPath}{Path.DirectorySeparatorChar}{logfile.Value}" :
$"{basepath.Value.Replace('\\', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{game.Replace('/', Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{logfile.Value}";
logPath = LogPath;
}
// hopefully fix wine drive name mangling
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
logPath = Regex.Replace(logPath, @"[A-Z]:", "");
logPath = Regex.Replace($"{Path.DirectorySeparatorChar}{LogPath}", @"[A-Z]:/", "");
}
if (!File.Exists(logPath))
if (!File.Exists(logPath) && !logPath.StartsWith("http"))
{
Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}");
#if !DEBUG
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
//#else
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
#endif
}
else
{
LogEvent = new GameLogEvent(this, logPath, logfile.Value);
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
}
Logger.WriteInfo($"Log file is {logPath}");
_ = Task.Run(() => LogEvent.PollForChanges());
#if !DEBUG
await Broadcast(loc["BROADCAST_ONLINE"]);
Broadcast(loc["BROADCAST_ONLINE"]);
#endif
}
@ -880,13 +907,12 @@ namespace IW4MAdmin
{
if (Target.Warnings >= 4)
{
await Target.Kick(loc["SERVER_WARNLIMT_REACHED"], (await Manager.GetClientService().Get(1)).AsPlayer());
Target.Kick(loc["SERVER_WARNLIMT_REACHED"], Utilities.IW4MAdminClient);
return;
}
Target.Warnings++;
String Message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
await Target.CurrentServer.Broadcast(Message);
String message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
Target.CurrentServer.Broadcast(message);
}
Penalty newPenalty = new Penalty()
@ -904,7 +930,7 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty);
}
public override async Task Kick(String Reason, Player Target, Player Origin)
protected override async Task Kick(String Reason, Player Target, Player Origin)
{
// ensure player gets kicked if command not performed on them in game
if (Target.ClientNumber < 0)
@ -945,7 +971,7 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty);
}
public override async Task TempBan(String Reason, TimeSpan length, Player Target, Player Origin)
protected override async Task TempBan(String Reason, TimeSpan length, Player Target, Player Origin)
{
// ensure player gets banned if command not performed on them in game
if (Target.ClientNumber < 0)
@ -984,7 +1010,7 @@ namespace IW4MAdmin
await Manager.GetPenaltyService().Create(newPenalty);
}
override public async Task Ban(String Message, Player Target, Player Origin)
override protected async Task Ban(String Message, Player Target, Player Origin)
{
// ensure player gets banned if command not performed on them in game
if (Target.ClientNumber < 0)
@ -1007,6 +1033,7 @@ namespace IW4MAdmin
{
// this is set only because they're still in the server.
Target.Level = Player.Permission.Banned;
#if !DEBUG
string formattedString = String.Format(RconParser.GetCommandPrefixes().Kick, Target.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{Message} ^7({loc["SERVER_BAN_APPEAL"]} {Website})^7");
await Target.CurrentServer.ExecuteCommandAsync(formattedString);
@ -1024,7 +1051,8 @@ namespace IW4MAdmin
Punisher = Origin,
Active = true,
When = DateTime.UtcNow,
Link = Target.AliasLink
Link = Target.AliasLink,
AutomatedOffense = Origin.AdministeredPenalties.FirstOrDefault()?.AutomatedOffense
};
await Manager.GetPenaltyService().Create(newPenalty);
@ -1054,6 +1082,8 @@ namespace IW4MAdmin
{
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("TOTALPLAYERS", (Server s) => Manager.GetClientService().GetTotalClientsAsync().Result.ToString()));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("VERSION", (Server s) => Application.Program.Version.ToString()));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("NEXTMAP", (Server s) => SharedLibraryCore.Commands.CNextMap.GetNextMap(s).Result));
Manager.GetMessageTokens().Add(new SharedLibraryCore.Helpers.MessageToken("ADMINS", (Server s) => SharedLibraryCore.Commands.CListAdmins.OnlineAdmins(s)));
}
}
}

View File

@ -0,0 +1,214 @@
import requests
import time
import json
import collections
import os
# the following classes model the discord webhook api parameters
class WebhookAuthor():
def __init__(self, name=None, url=None, icon_url=None):
if name:
self.name = name
if url:
self.url = url
if icon_url:
self.icon_url = icon_url
class WebhookField():
def __init__(self, name=None, value=None, inline=False):
if name:
self.name = name
if value:
self.value = value
if inline:
self.inline = inline
class WebhookEmbed():
def __init__(self):
self.author = ''
self.title = ''
self.url = ''
self.description = ''
self.color = 0
self.fields = []
self.thumbnail = {}
class WebhookParams():
def __init__(self, username=None, avatar_url=None, content=None):
self.username = ''
self.avatar_url = ''
self.content = ''
self.embeds = []
# quick way to convert all the objects to a nice json object
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
# gets the relative link to a user's profile
def get_client_profile(profile_id):
return u'{}/Client/ProfileAsync/{}'.format(base_url, profile_id)
def get_client_profile_markdown(client_name, profile_id):
return u'[{}]({})'.format(client_name, get_client_profile(profile_id))
#todo: exception handling for opening the file
if os.getenv("DEBUG"):
config_file_name = 'config.dev.json'
else:
config_file_name = 'config.json'
with open(config_file_name) as json_config_file:
json_config = json.load(json_config_file)
# this should be an URL to an IP or FQN to an IW4MAdmin instance
# ie http://127.0.0.1 or http://IW4MAdmin.com
base_url = json_config['IW4MAdminUrl']
end_point = '/api/event'
request_url = base_url + end_point
# this should be the full discord webhook url
# ie https://discordapp.com/api/webhooks/<id>/<token>
discord_webhook_notification_url = json_config['DiscordWebhookNotificationUrl']
discord_webhook_information_url = json_config['DiscordWebhookInformationUrl']
# this should be the numerical id of the discord group
# 12345678912345678
notify_role_ids = json_config['NotifyRoleIds']
def get_new_events():
events = []
response = requests.get(request_url)
data = response.json()
should_notify = False
for event in data:
# commonly used event info items
event_type = event['eventType']['name']
server_name = event['ownerEntity']['name']
if event['originEntity']:
origin_client_name = event['originEntity']['name']
origin_client_id = int(event['originEntity']['id'])
if event['targetEntity']:
target_client_name = event['targetEntity']['name'] or ''
target_client_id = int(event['targetEntity']['id']) or 0
webhook_item = WebhookParams()
webhook_item_embed = WebhookEmbed()
#todo: the following don't need to be generated every time, as it says the same
webhook_item.username = 'IW4MAdmin'
webhook_item.avatar_url = 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png'
webhook_item_embed.color = 31436
webhook_item_embed.url = base_url
webhook_item_embed.thumbnail = { 'url' : 'https://raidmax.org/IW4MAdmin/img/iw4adminicon-3.png' }
webhook_item.embeds.append(webhook_item_embed)
# the server should be visible on all event types
server_field = WebhookField('Server', server_name)
webhook_item_embed.fields.append(server_field)
role_ids_string = ''
for id in notify_role_ids:
role_ids_string += '\r\n<@&{}>\r\n'.format(id)
if event_type == 'Report':
report_reason = event['extraInfo']
report_reason_field = WebhookField('Reason', report_reason)
reported_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
reported_field = WebhookField('Reported Player',get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Reported'
webhook_item_embed.fields.append(reported_field)
webhook_item_embed.fields.append(reported_by_field)
webhook_item_embed.fields.append(report_reason_field)
should_notify = True
elif event_type == 'Ban':
ban_reason = event['extraInfo']
ban_reason_field = WebhookField('Reason', ban_reason)
banned_by_field = WebhookField('By', get_client_profile_markdown(origin_client_name, origin_client_id))
banned_field = WebhookField('Banned Player', get_client_profile_markdown(target_client_name, target_client_id))
# add each fields to the embed
webhook_item_embed.title = 'Player Banned'
webhook_item_embed.fields.append(banned_field)
webhook_item_embed.fields.append(banned_by_field)
webhook_item_embed.fields.append(ban_reason_field)
should_notify = True
elif event_type == 'Connect':
connected_field = WebhookField('Connected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Connected'
webhook_item_embed.fields.append(connected_field)
elif event_type == 'Disconnect':
disconnected_field = WebhookField('Disconnected Player', get_client_profile_markdown(origin_client_name, origin_client_id))
webhook_item_embed.title = 'Player Disconnected'
webhook_item_embed.fields.append(disconnected_field)
elif event_type == 'Say':
say_client_field = WebhookField('Player', get_client_profile_markdown(origin_client_name, origin_client_id))
message_field = WebhookField('Message', event['extraInfo'])
webhook_item_embed.title = 'Message From Player'
webhook_item_embed.fields.append(say_client_field)
webhook_item_embed.fields.append(message_field)
#if event_type == 'ScriptKill' or event_type == 'Kill':
# kill_str = '{} killed {}'.format(get_client_profile_markdown(origin_client_name, origin_client_id),
# get_client_profile_markdown(target_client_name, target_client_id))
# killed_field = WebhookField('Kill Information', kill_str)
# webhook_item_embed.title = 'Player Killed'
# webhook_item_embed.fields.append(killed_field)
#todo: handle other events
else:
continue
#make sure there's at least one group to notify
if len(notify_role_ids) > 0:
# unfortunately only the content can be used to to notify members in groups
#embed content shows the role but doesn't notify
webhook_item.content = role_ids_string
events.append({'item' : webhook_item, 'notify' : should_notify})
return events
# sends the data to the webhook location
def execute_webhook(data):
for event in data:
event_json = event['item'].to_json()
url = None
if event['notify']:
url = discord_webhook_notification_url
else:
if len(discord_webhook_information_url) > 0:
url = discord_webhook_information_url
if url :
response = requests.post(url,
data=event_json,
headers={'Content-type' : 'application/json'})
# grabs new events and executes the webhook fo each valid event
def run():
failed_count = 1
print('starting polling for events')
while True:
try:
new_events = get_new_events()
execute_webhook(new_events)
except Exception as e:
print('failed to get new events ({})'.format(failed_count))
print(e)
failed_count += 1
time.sleep(5)
if __name__ == "__main__":
run()

View File

@ -0,0 +1,95 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>15a81d6e-7502-46ce-8530-0647a380b5f4</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>DiscordWebhook.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>DiscordWebhook</Name>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<RootNamespace>DiscordWebhook</RootNamespace>
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
<IsWindowsApplication>False</IsWindowsApplication>
<LaunchProvider>Standard Python launcher</LaunchProvider>
<EnableNativeCodeDebugging>False</EnableNativeCodeDebugging>
<Environment>DEBUG=True</Environment>
<PublishUrl>C:\Projects\IW4M-Admin\Publish\WindowsPrerelease\DiscordWebhook</PublishUrl>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<ItemGroup>
<Compile Include="DiscordWebhook.py" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<ItemGroup>
<Content Include="config.dev.json" />
<Content Include="config.json">
<Publish>True</Publish>
</Content>
<Content Include="requirements.txt">
<Publish>True</Publish>
</Content>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -0,0 +1,6 @@
{
"IW4MAdminUrl": "",
"DiscordWebhookNotificationUrl": "",
"DiscordWebhookInformationUrl": "",
"NotifyRoleIds": []
}

View File

@ -0,0 +1,7 @@
certifi>=2018.4.16
chardet>=3.0.4
idna>=2.7
pip>=18.0
requests>=2.19.1
setuptools>=39.0.1
urllib3>=1.23

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>42efda12-10d3-4c40-a210-9483520116bc</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectTypeGuids>{789894c7-04a9-4a11-a6b5-3f4435165112};{1b580a1a-fdb3-4b32-83e1-6407eb2722e6};{349c5851-65df-11da-9384-00065b846f21};{888888a0-9f3d-457c-b088-3a5042f75d52}</ProjectTypeGuids>
<StartupFile>runserver.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<LaunchProvider>Web launcher</LaunchProvider>
<WebBrowserUrl>http://localhost</WebBrowserUrl>
<OutputPath>.</OutputPath>
<SuppressCollectPythonCloudServiceFiles>true</SuppressCollectPythonCloudServiceFiles>
<Name>GameLogServer</Name>
<RootNamespace>GameLogServer</RootNamespace>
<InterpreterId>MSBuild|env|$(MSBuildProjectFullPath)</InterpreterId>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameLogServer\log_reader.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="GameLogServer\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="runserver.py" />
<Compile Include="GameLogServer\__init__.py" />
<Compile Include="GameLogServer\log_resource.py" />
</ItemGroup>
<ItemGroup>
<Folder Include="GameLogServer\" />
</ItemGroup>
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="requirements.txt" />
</ItemGroup>
<ItemGroup>
<Interpreter Include="env\">
<Id>env</Id>
<Version>3.6</Version>
<Description>env (Python 3.6 (64-bit))</Description>
<InterpreterPath>Scripts\python.exe</InterpreterPath>
<WindowsInterpreterPath>Scripts\pythonw.exe</WindowsInterpreterPath>
<PathEnvironmentVariable>PYTHONPATH</PathEnvironmentVariable>
<Architecture>X64</Architecture>
</Interpreter>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.Web.targets" />
<!-- Specify pre- and post-build commands in the BeforeBuild and
AfterBuild targets below. -->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<AutoAssignPort>True</AutoAssignPort>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -0,0 +1,9 @@
"""
The flask application package.
"""
from flask import Flask
from flask_restful import Api
app = Flask(__name__)
api = Api(app)

View File

@ -0,0 +1,75 @@
import re
import os
import time
class LogReader(object):
def __init__(self):
self.log_file_sizes = {}
# (if the file changes more than this, ignore ) - 1 MB
self.max_file_size_change = 1000000
# (if the time between checks is greater, ignore ) - 5 minutes
self.max_file_time_change = 1000
def read_file(self, path):
# prevent traversing directories
if re.search('r^.+\.\.\\.+$', path):
return False
# must be a valid log path and log file
if not re.search(r'^.+[\\|\/](userraw|mods)[\\|\/].+.log$', path):
return False
# set the initialze size to the current file size
file_size = 0
if path not in self.log_file_sizes:
self.log_file_sizes[path] = {
'length' : self.file_length(path),
'read': time.time()
}
return ''
# grab the previous values
last_length = self.log_file_sizes[path]['length']
last_read = self.log_file_sizes[path]['read']
# the file is being tracked already
new_file_size = self.file_length(path)
# the log size was unable to be read (probably the wrong path)
if new_file_size < 0:
return False
now = time.time()
file_size_difference = new_file_size - last_length
time_difference = now - last_read
# update the new size and actually read the data
self.log_file_sizes[path] = {
'length': new_file_size,
'read': now
}
# if it's been too long since we read and the amount changed is too great, discard it
# todo: do we really want old events? maybe make this an "or"
if file_size_difference > self.max_file_size_change and time_difference > self.max_file_time_change:
return ''
new_log_info = self.get_file_lines(path, file_size_difference)
return new_log_info
def get_file_lines(self, path, length):
try:
file_handle = open(path, 'rb')
file_handle.seek(-length, 2)
file_data = file_handle.read(length)
file_handle.close()
return file_data.decode('utf-8')
except:
return False
def file_length(self, path):
try:
return os.stat(path).st_size
except:
return -1
reader = LogReader()

View File

@ -0,0 +1,17 @@
from flask_restful import Resource
from GameLogServer.log_reader import reader
from base64 import urlsafe_b64decode
class LogResource(Resource):
def get(self, path):
path = urlsafe_b64decode(path).decode('utf-8')
log_info = reader.read_file(path)
if log_info is False:
print('could not read log file ' + path)
return {
'success' : log_info is not False,
'length': -1 if log_info is False else len(log_info),
'data': log_info
}

View File

@ -0,0 +1,9 @@
from flask import Flask
from flask_restful import Api
from .log_resource import LogResource
app = Flask(__name__)
def init():
api = Api(app)
api.add_resource(LogResource, '/log/<string:path>')

View File

@ -0,0 +1,12 @@
Flask==1.0.2
aniso8601==3.0.2
click==6.7
Flask-RESTful==0.3.6
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
pip==9.0.3
pytz==2018.5
setuptools==39.0.1
six==1.11.0
Werkzeug==0.14.1

View File

@ -0,0 +1,15 @@
"""
This script runs the GameLogServer application using a development server.
"""
from os import environ
from GameLogServer.server import app, init
if __name__ == '__main__':
HOST = environ.get('SERVER_HOST', '0.0.0.0')
try:
PORT = int(environ.get('SERVER_PORT', '1625'))
except ValueError:
PORT = 5555
init()
app.run(HOST, PORT, debug=True)

View File

@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
_commands.gsc = _commands.gsc
_customcallbacks.gsc = _customcallbacks.gsc
README.md = README.md
RunPublishPre.cmd = RunPublishPre.cmd
version.txt = version.txt
EndProjectSection
EndProject
@ -31,7 +32,17 @@ Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IW4ScriptCommands", "Plugins\IW4ScriptCommands\IW4ScriptCommands.csproj", "{6C706CE5-A206-4E46-8712-F8C48D526091}"
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DiscordWebhook", "DiscordWebhook\DiscordWebhook.pyproj", "{15A81D6E-7502-46CE-8530-0647A380B5F4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlugins", "{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA}"
ProjectSection(SolutionItems) = preProject
Plugins\ScriptPlugins\SharedGUIDKick.js = Plugins\ScriptPlugins\SharedGUIDKick.js
Plugins\ScriptPlugins\VPNDetection.js = Plugins\ScriptPlugins\VPNDetection.js
EndProjectSection
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "GameLogServer", "GameLogServer\GameLogServer.pyproj", "{42EFDA12-10D3-4C40-A210-9483520116BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -287,6 +298,42 @@ Global
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x64.Build.0 = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = Release|Any CPU
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.Build.0 = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x64.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Debug|x86.ActiveCfg = Debug|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Any CPU.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Prerelease|x86.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x64.ActiveCfg = Release|Any CPU
{15A81D6E-7502-46CE-8530-0647A380B5F4}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x64.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.ActiveCfg = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Debug|x86.Build.0 = Debug|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Prerelease|x86.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Any CPU.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x64.Build.0 = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.ActiveCfg = Release|Any CPU
{42EFDA12-10D3-4C40-A210-9483520116BC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -298,6 +345,7 @@ Global
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
{6C706CE5-A206-4E46-8712-F8C48D526091} = {26E8B310-269E-46D4-A612-24601F16065F}
{3F9ACC27-26DB-49FA-BCD2-50C54A49C9FA} = {26E8B310-269E-46D4-A612-24601F16065F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}

View File

@ -73,6 +73,9 @@
<Compile Include="master\resources\null.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="Master\resources\server.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="master\resources\version.py">
<SubType>Code</SubType>
</Compile>
@ -110,6 +113,7 @@
<ItemGroup>
<None Include="FolderProfile.pubxml" />
<Content Include="master\config\master.json" />
<Content Include="master\templates\serverlist.html" />
<Content Include="requirements.txt" />
<Content Include="master\templates\index.html" />
<Content Include="master\templates\layout.html" />

View File

@ -15,9 +15,9 @@ class Base():
self.scheduler.start()
self.scheduler.add_job(
func=self._remove_staleinstances,
trigger=IntervalTrigger(seconds=120),
trigger=IntervalTrigger(seconds=60),
id='stale_instance_remover',
name='Remove stale instances if no heartbeat in 120 seconds',
name='Remove stale instances if no heartbeat in 60 seconds',
replace_existing=True
)
self.scheduler.add_job(
@ -41,7 +41,7 @@ class Base():
def _remove_staleinstances(self):
for key, value in list(self.instance_list.items()):
if int(time.time()) - value.last_heartbeat > 120:
if int(time.time()) - value.last_heartbeat > 60:
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
del self.instance_list[key]
del self.token_list[key]

View File

@ -1,6 +1,6 @@
class ServerModel(object):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype):
def __init__(self, id, port, game, hostname, clientnum, maxclientnum, map, gametype, ip):
self.id = id
self.port = port
self.game = game
@ -9,6 +9,7 @@ class ServerModel(object):
self.maxclientnum = maxclientnum
self.map = map
self.gametype = gametype
self.ip = ip
def __repr__(self):
return '<ServerModel(id={id})>'.format(id=self.id)

View File

@ -8,10 +8,11 @@ import datetime
class Authenticate(Resource):
def post(self):
instance_id = request.json['id']
if ctx.get_token(instance_id) is not False:
return { 'message' : 'that id already has a token'}, 401
else:
expires = datetime.timedelta(days=1)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200
#todo: see why this is failing
#if ctx.get_token(instance_id) is not False:
# return { 'message' : 'that id already has a token'}, 401
#else:
expires = datetime.timedelta(days=30)
token = create_access_token(instance_id, expires_delta=expires)
ctx.add_token(instance_id, token)
return { 'access_token' : token }, 200

View File

@ -4,6 +4,7 @@ from flask_jwt_extended import jwt_required
from marshmallow import ValidationError
from master.schema.instanceschema import InstanceSchema
from master import ctx
import json
class Instance(Resource):
def get(self, id=None):
@ -21,6 +22,8 @@ class Instance(Resource):
@jwt_required
def put(self, id):
try:
for server in request.json['servers']:
server['ip'] = request.remote_addr
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400
@ -30,6 +33,8 @@ class Instance(Resource):
@jwt_required
def post(self):
try:
for server in request.json['servers']:
server['ip'] = request.remote_addr
instance = InstanceSchema().load(request.json)
except ValidationError as err:
return {'message' : err.messages }, 400

View File

@ -0,0 +1,6 @@
from flask_restful import Resource
class Server(Resource):
"""description of class"""

View File

@ -6,10 +6,12 @@ from master.resources.authenticate import Authenticate
from master.resources.version import Version
from master.resources.history_graph import HistoryGraph
from master.resources.localization import Localization
from master.resources.server import Server
api.add_resource(Null, '/null')
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
api.add_resource(Version, '/version')
api.add_resource(Authenticate, '/authenticate')
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
api.add_resource(Localization, '/localization/', '/localization/<string:language_tag>')
api.add_resource(Localization, '/localization/', '/localization/<string:language_tag>')
api.add_resource(Server, '/server')

View File

@ -6,17 +6,20 @@ class ServerSchema(Schema):
required=True,
validate=validate.Range(1, 2147483647, 'invalid id')
)
ip = fields.Str(
required=True
)
port = fields.Int(
required=True,
validate=validate.Range(1, 665535, 'invalid port')
validate=validate.Range(1, 65535, 'invalid port')
)
game = fields.String(
required=True,
validate=validate.Length(1, 8, 'invalid game name')
validate=validate.Length(1, 5, 'invalid game name')
)
hostname = fields.String(
required=True,
validate=validate.Length(1, 48, 'invalid hostname')
validate=validate.Length(1, 64, 'invalid hostname')
)
clientnum = fields.Int(
required=True,

View File

@ -39,6 +39,7 @@
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,113 @@
{% extends "layout.html" %}
{% block content %}
<!-- todo: move this! -->
<style>
.server-row {
cursor: pointer;
}
.modal-content, .nav-item {
background-color: #212529;
color: #fff;
}
.modal-header, .modal-footer {
border-color: #32383e !important;
}
.modal-dark button.close, a.nav-link {
color: #fff;
}
</style>
<div class="modal modal-dark" id="serverModalCenter" tabindex="-1" role="dialog" aria-labelledby="serverModalCenterTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="serverModalTitle">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="h5" id="server_socket"></div>
</div>
<div class="modal-footer">
<button id="connect_button" type="button" class="btn btn-dark">Connect</button>
<button type="button" class="btn btn-dark" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<nav>
<div class="nav nav-tabs" id="server_game_tabs" role="tablist">
{% for game in games %}
<a class="nav-item nav-link {{'active' if loop.first else ''}}" id="{{game}}_servers_tab" data-toggle="tab" href="#{{game}}_servers" role="tab" aria-controls="{{game}}_servers" aria-selected="{{'true' if loop.first else 'false' }}">{{game}}</a>
{% endfor %}
</div>
</nav>
<div class="tab-content" id="server_game_tabs_content">
{% for game, servers in games.items() %}
<div class="tab-pane {{'show active' if loop.first else ''}}" id="{{game}}_servers" role="tabpanel" aria-labelledby="{{game}}_servers_tab">
<table class="table table-dark table-striped table-hover table-responsive-lg">
<thead>
<tr>
<th>Server Name</th>
<th>Map Name</th>
<th>Players</th>
<th>Mode</th>
<th class="text-center">Connect</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr class="server-row" data-toggle="modal" data-target="#serverModalCenter"
data-ip="{{server.ip}}" data-port="{{server.port}}">
<td data-hostname="{{server.hostname}}" class="server-hostname">{{server.hostname}}</td>
<td data-map="{{server.map}} " class="server-map">{{server.map}}</td>
<td data-clientnum="{{server.clientnum}}" data-maxclientnum="{{server.maxclientnum}}"
class="server-clientnum">
{{server.clientnum}}/{{server.maxclientnum}}
</td>
<td data-gametype="{{server.gametype}}" class="server-gametype">{{server.gametype}}</td>
<td class="text-center"><span class="oi oi-play-circle"></span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
<div class="w-100 small text-right text-muted">
<span>Developed by RaidMax</span><br />
<span>PRERELEASE</span>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(() => {
$('.server-row').off('click');
$('.server-row').on('click', function (e) {
$('#serverModalTitle').text($(this).find('.server-hostname').text());
$('#server_socket').text(`/connect ${$(this).data('ip')}:${$(this).data('port')}`);
});
$('#connect_button').off('click');
$('#connect_button').on('click', (e) => alert('soon...'));
});
</script>
{% endblock %}

View File

@ -4,8 +4,9 @@ Routes and views for the flask application.
from datetime import datetime
from flask import render_template
from master import app
from master import app, ctx
from master.resources.history_graph import HistoryGraph
from collections import defaultdict
@app.route('/')
def home():
@ -19,3 +20,16 @@ def home():
client_count = _history_graph[0]['client_count'],
server_count = _history_graph[0]['server_count']
)
@app.route('/servers')
def servers():
servers = defaultdict(list)
if len(ctx.instance_list.values()) > 0:
ungrouped_servers = [server for instance in ctx.instance_list.values() for server in instance.servers]
for server in ungrouped_servers:
servers[server.game].append(server)
return render_template(
'serverlist.html',
title = 'Server List',
games = servers
)

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4ScriptCommands
{
class CommandInfo
{
public string Command { get; set; }
public int ClientNumber { get; set; }
public List<string> CommandArguments { get; set; } = new List<string>();
public override string ToString() => $"{Command};{ClientNumber},{string.Join(',', CommandArguments)}";
}
}

View File

@ -1,19 +1,197 @@
using SharedLibraryCore;
using SharedLibraryCore.Objects;
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
{
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)
{
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
await E.Origin.Tell("Balance command sent");
}
}
}
// public override async Task ExecuteAsync(GameEvent E)
// {
// string teamsString = (await E.Owner.GetDvarAsync<string>("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();
// // 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<string> teamAssignments = new List<string>();
// 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 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;
// 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
// {
// alliesTeam.Add(activeClients.Last());
// activeClients.RemoveAt(activeClients.Count - 1);
// }
// }
// }
// alliesTeam = alliesTeam.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;
// if (teamSizeDifference > 0)
// {
// if (performanceDisparity > 0)
// {
// axisTeam.Add(alliesTeam.First());
// alliesTeam.RemoveAt(0);
// }
// else
// {
// axisTeam.Add(alliesTeam.Last());
// alliesTeam.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);
// }
// }
// }
// 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 (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");
// }
// }
//}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
</PropertyGroup>
@ -13,10 +13,11 @@
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\Stats\Stats.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
<PackageReference Update="Microsoft.NETCore.App"/>
</ItemGroup>
</Project>

View File

@ -15,7 +15,35 @@ namespace IW4ScriptCommands
public string Author => "RaidMax";
public Task OnEventAsync(GameEvent E, Server S) => Task.CompletedTask;
public Task OnEventAsync(GameEvent E, Server S)
{
//if (E.Type == GameEvent.EventType.JoinTeam || E.Type == GameEvent.EventType.Disconnect)
//{
// E.Origin = new SharedLibraryCore.Objects.Player()
// {
// ClientId = 1,
// CurrentServer = E.Owner
// };
// return new Commands.Balance().ExecuteAsync(E);
//}
if (E.Type == GameEvent.EventType.Warn)
{
return S.SetDvarAsync("sv_iw4madmin_command", new CommandInfo()
{
ClientNumber = E.Target.ClientNumber,
Command = "alert",
CommandArguments = new List<string>()
{
"Warning",
"ui_mp_nukebomb_timer",
E.Data
}
}.ToString());
}
return Task.CompletedTask;
}
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;

View File

@ -15,7 +15,8 @@ namespace IW4MAdmin.Plugins.Login.Commands
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true
}
}){ }
})
{ }
public override async Task ExecuteAsync(GameEvent E)
{
@ -25,12 +26,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
if (hashedPassword[0] == client.Password)
{
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]);
}
else
{
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
}
}
}

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
<PackageReference Update="Microsoft.NETCore.App"/>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,9 +16,9 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
{
OffensiveWords = new List<string>()
{
"nigger",
"nigga",
"fuck"
@"\s*n+.*i+.*g+.*e+.*r+\s*",
@"\s*n+.*i+.*g+.*a+\s*",
@"\s*f+u+.*c+.*k+.*\s*"
};
var loc = Utilities.CurrentLocalization.LocalizationIndex;

View File

@ -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;
@ -21,10 +22,10 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
ConcurrentDictionary<int, Tracking> ProfanityCounts;
IManager Manager;
public async Task OnEventAsync(GameEvent E, Server S)
public Task OnEventAsync(GameEvent E, Server S)
{
if (!Settings.Configuration().EnableProfanityDeterment)
return;
return Task.CompletedTask; ;
if (E.Type == GameEvent.EventType.Connect)
{
@ -36,9 +37,18 @@ 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, RegexOptions.IgnoreCase);
}
}
if (containsObjectionalWord)
{
await E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
{
ClientId = 1
});
@ -56,14 +66,25 @@ 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.Data.ToLower(), word, RegexOptions.IgnoreCase);
// break out early because there's at least one objectional word
if (containsObjectionalWord)
{
break;
}
}
if (containsObjectionalWord)
{
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
{
await clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
clientProfanity.Client.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
{
ClientId = 1
});
@ -73,13 +94,14 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
{
clientProfanity.Infringements++;
await clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
{
ClientId = 1
});
}
}
}
return Task.CompletedTask;
}
public async Task OnLoadAsync(IManager manager)

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
<PackageReference Update="Microsoft.NETCore.App"/>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -0,0 +1,30 @@
var plugin = {
author: 'RaidMax',
version: 1.0,
name: 'Shared GUID Kicker Plugin',
onEventAsync: function (gameEvent, server) {
// make sure we only check for IW4(x)
if (server.GameName !== 2) {
return false;
}
// connect or join event
if (gameEvent.Type === 3 ||
gameEvent.Type === 4) {
// this GUID seems to have been packed in a IW4 torrent and results in an unreasonable amount of people using the same GUID
if (gameEvent.Origin.NetworkId === -805366929435212061) {
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
}
}
},
onLoadAsync: function (manager) {
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -0,0 +1,66 @@
var plugin = {
author: 'RaidMax',
version: 1.0,
name: 'VPN Detection Plugin',
manager: null,
logger: null,
vpnExceptionIds: [],
checkForVpn: function (origin) {
var exempt = false;
// prevent players that are exempt from being kicked
this.vpnExceptionIds.forEach(function (id) {
if (id === origin.ClientId) {
exempt = true;
return false;
}
});
if (exempt) {
return;
}
var usingVPN = false;
try {
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 + ')');
var library = importNamespace('SharedLibraryCore');
var kickOrigin = new library.Objects.Player();
kickOrigin.ClientId = 1;
origin.Kick(_localization.LocalizationIndex["SERVER_KICK_VPNS_NOTALLOWED"], kickOrigin);
}
},
onEventAsync: function (gameEvent, server) {
// connect event
if (gameEvent.Type === 3) {
this.checkForVpn(gameEvent.Origin);
}
},
onLoadAsync: function (manager) {
this.manager = manager;
this.logger = manager.GetLogger();
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -19,16 +19,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
Strain
};
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
int Kills;
int HitCount;
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
ChangeTracking Tracker;
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
DateTime LastHit;
long LastOffset;
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
public Detection(ILogger log, EFClientStatistics clientStats)
{
@ -38,7 +40,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
ClientStats = clientStats;
Strain = new Strain();
Tracker = new ChangeTracking();
Tracker = new ChangeTracking<EFACSnapshot>();
}
/// <summary>
@ -51,7 +53,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
kill.HitLoc == IW4Info.HitLocation.none)
kill.HitLoc == IW4Info.HitLocation.none || kill.TimeOffset - LastOffset < 0)
return new DetectionPenaltyResult()
{
ClientPenalty = Penalty.PenaltyType.Any,
@ -83,7 +85,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
hitLoc.HitOffsetAverage = (float)newAverage;
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset &&
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset(hitLoc.HitCount) &&
hitLoc.HitCount > 100)
{
Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
@ -105,7 +107,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
AngleDifferenceAverage = sessAverage;
if (sessAverage > Thresholds.MaxOffset &&
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
HitCount > 30)
{
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
@ -129,7 +131,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}
double currentStrain = Strain.GetStrain(isDamage, kill.Damage, kill.Distance / 0.0254, kill.ViewAngles, Math.Max(50, kill.TimeOffset - LastOffset));
//double currentWeightedStrain = (currentStrain * ClientStats.SPM) / 170.0;
LastOffset = kill.TimeOffset;
if (currentStrain > ClientStats.MaxStrain)
@ -138,7 +139,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}
// flag
if (currentStrain > Thresholds.MaxStrainFlag)
if (currentStrain > Thresholds.MaxStrainFlag &&
HitCount >= 10)
{
result = new DetectionPenaltyResult()
{
@ -150,7 +152,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}
// ban
if (currentStrain > Thresholds.MaxStrainBan)
if (currentStrain > Thresholds.MaxStrainBan &&
HitCount >= 15)
{
result = new DetectionPenaltyResult()
{
@ -174,7 +177,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
// determine what the max headshot percentage can be for current number of kills
double lerpAmount = Math.Min(1.0, (HitCount - Thresholds.LowSampleMinKills) / (double)(/*Thresholds.HighSampleMinKills*/ 60 - Thresholds.LowSampleMinKills));
double maxHeadshotLerpValueForFlag = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(2.0), Thresholds.HeadshotRatioThresholdHighSample(2.0), lerpAmount) + marginOfError;
double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.0), Thresholds.HeadshotRatioThresholdHighSample(3.0), lerpAmount) + marginOfError;
double maxHeadshotLerpValueForBan = Thresholds.Lerp(Thresholds.HeadshotRatioThresholdLowSample(3.5), Thresholds.HeadshotRatioThresholdHighSample(3.5), lerpAmount) + marginOfError;
// determine what the max bone percentage can be for current number of kills
double maxBoneRatioLerpValueForFlag = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(2.25), Thresholds.BoneRatioThresholdHighSample(2.25), lerpAmount) + marginOfError;
double maxBoneRatioLerpValueForBan = Thresholds.Lerp(Thresholds.BoneRatioThresholdLowSample(3.25), Thresholds.BoneRatioThresholdHighSample(3.25), lerpAmount) + marginOfError;
@ -191,7 +194,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
{
// ban on headshot
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
{
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
Log.WriteDebug($"ClientId: {kill.AttackerId}");
@ -351,16 +354,34 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
#endregion
#endregion
Tracker.OnChange(new DetectionTracking(ClientStats, kill, Strain));
if (result != null)
Tracker.OnChange(new EFACSnapshot()
{
foreach (string change in Tracker.GetChanges())
{
Log.WriteDebug(change);
Log.WriteDebug("--------------SNAPSHOT END-----------");
}
}
When = kill.When,
ClientId = ClientStats.ClientId,
SessionAngleOffset = AngleDifferenceAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalSeconds,
CurrentStrain = currentStrain,
CurrentViewAngle = kill.ViewAngles,
Hits = HitCount,
Kills = Kills,
Deaths = ClientStats.SessionDeaths,
HitDestinationId = kill.DeathOrigin.Vector3Id,
HitDestination = kill.DeathOrigin,
HitOriginId = kill.KillOrigin.Vector3Id,
HitOrigin = kill.KillOrigin,
EloRating = ClientStats.EloRating,
HitLocation = kill.HitLoc,
LastStrainAngle = Strain.LastAngle,
PredictedViewAngles = kill.AnglesList,
// this is in "meters"
Distance = kill.Distance,
SessionScore = ClientStats.SessionScore,
HitType = kill.DeathType,
SessionSPM = ClientStats.SessionSPM,
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = kill.Weapon
});
return result ?? new DetectionPenaltyResult()
{

View File

@ -1,9 +1,4 @@
using SharedLibraryCore.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IW4MAdmin.Plugins.Stats.Cheat
{

View File

@ -1,57 +0,0 @@
using IW4MAdmin.Plugins.Stats.Cheat;
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class DetectionTracking : ITrackable
{
EFClientStatistics Stats;
EFClientKill Hit;
Strain Strain;
public DetectionTracking(EFClientStatistics stats, EFClientKill hit, Strain strain)
{
Stats = stats;
Hit = hit;
Strain = strain;
}
public string GetTrackableValue()
{
var sb = new StringBuilder();
sb.AppendLine($"SPM = {Stats.SPM}");
sb.AppendLine($"KDR = {Stats.KDR}");
sb.AppendLine($"Kills = {Stats.Kills}");
sb.AppendLine($"Session Score = {Stats.SessionScore}");
sb.AppendLine($"Elo = {Stats.EloRating}");
sb.AppendLine($"Max Sess Strain = {Stats.MaxSessionStrain}");
sb.AppendLine($"MaxStrain = {Stats.MaxStrain}");
sb.AppendLine($"Avg Offset = {Stats.AverageHitOffset}");
sb.AppendLine($"TimePlayed, {Stats.TimePlayed}");
sb.AppendLine($"HitDamage = {Hit.Damage}");
sb.AppendLine($"HitOrigin = {Hit.KillOrigin}");
sb.AppendLine($"DeathOrigin = {Hit.DeathOrigin}");
sb.AppendLine($"ViewAngles = {Hit.ViewAngles}");
sb.AppendLine($"WeaponId = {Hit.Weapon.ToString()}");
sb.AppendLine($"Timeoffset = {Hit.TimeOffset}");
sb.AppendLine($"HitLocation = {Hit.HitLoc.ToString()}");
sb.AppendLine($"Distance = {Hit.Distance / 0.0254}");
sb.AppendLine($"HitType = {Hit.DeathType.ToString()}");
int i = 0;
foreach (var predictedAngle in Hit.AnglesList)
{
sb.AppendLine($"Predicted Angle [{i}] {predictedAngle}");
i++;
}
sb.AppendLine(Strain.GetTrackableValue());
sb.AppendLine($"VictimId = {Hit.VictimId}");
sb.AppendLine($"AttackerId = {Hit.AttackerId}");
return sb.ToString();
}
}
}

View File

@ -6,13 +6,13 @@ using System.Text;
namespace IW4MAdmin.Plugins.Stats.Cheat
{
class Strain : ITrackable
class Strain
{
private const double StrainDecayBase = 0.9;
private const double StrainDecayBase = 0.9;
private double CurrentStrain;
private Vector3 LastAngle;
private double LastDeltaTime;
private double LastDistance;
public double LastDistance { get; private set; }
public Vector3 LastAngle { get; private set; }
public double LastDeltaTime { get; private set; }
public int TimesReachedMaxStrain { get; private set; }
@ -53,11 +53,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
return CurrentStrain;
}
public string GetTrackableValue()
{
return $"Strain = {CurrentStrain}\r\n, Angle = {LastAngle}\r\n, Delta Time = {LastDeltaTime}\r\n, Angle Between = {LastDistance}";
}
private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, Math.Pow(2.0, deltaTime / 250.0) / 1000.0);
}
}

View File

@ -27,8 +27,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
public const int HighSampleMinKills = 100;
public const double KillTimeThreshold = 0.2;
public const double MaxStrainBan = 0.4;
public const double MaxOffset = 1.2;
public const double MaxStrainBan = 1.12;
//=exp((MAX(-3.07+(-3.07/sqrt(J20)),-3.07-(-3.07/sqrt(J20))))+(4*(0.869)))
public static double MaxOffset(int sampleSize) => Math.Exp(Math.Max(-3.07 + (-3.07 / Math.Sqrt(sampleSize)), -3.07 - (-3.07 / Math.Sqrt(sampleSize))) + 4 * (0.869));
public const double MaxStrainFlag = 0.36;
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);

View File

@ -63,12 +63,16 @@ namespace IW4MAdmin.Plugins.Stats.Commands
if (!E.Message.IsBroadcastCommand())
{
foreach (var stat in topStats)
await E.Origin.Tell(stat);
{
E.Origin.Tell(stat);
}
}
else
{
foreach (var stat in topStats)
await E.Owner.Broadcast(stat);
{
E.Owner.Broadcast(stat);
}
}
}
}

View File

@ -34,12 +34,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
// fixme: this doesn't work properly when another context exists
await svc.SaveChangesAsync();
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_SUCCESS"]);
}
else
{
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
}
}
}

View File

@ -23,12 +23,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
};
using (var db = new DatabaseContext())
using (var db = new DatabaseContext(true))
{
db.ChangeTracker.AutoDetectChangesEnabled = false;
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
var iqStats = (from stats in db.Set<EFClientStatistics>()
join client in db.Clients
@ -36,9 +33,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
join alias in db.Aliases
on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId
where stats.TimePlayed >= 3600
where stats.TimePlayed >= Plugin.Config.Configuration().TopPlayersMinPlayTime
where client.Level != Player.Permission.Banned
where client.LastConnection >= thirtyDaysAgo
where client.LastConnection >= fifteenDaysAgo
orderby stats.Performance descending
select new
{
@ -48,6 +45,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
})
.Take(5);
#if DEBUG == true
var statsSql = iqStats.ToSql();
#endif
var statsList = (await iqStats.ToListAsync())
.Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}");
@ -74,12 +75,17 @@ namespace IW4MAdmin.Plugins.Stats.Commands
if (!E.Message.IsBroadcastCommand())
{
foreach (var stat in topStats)
await E.Origin.Tell(stat);
{
E.Origin.Tell(stat);
}
}
else
{
foreach (var stat in topStats)
await E.Owner.Broadcast(stat);
{
E.Owner.Broadcast(stat);
}
}
}
}

View File

@ -26,25 +26,17 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
/*if (E.Target?.ClientNumber < 0)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME"]);
return;
}
if (E.Origin.ClientNumber < 0 && E.Target == null)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL_INGAME_SELF"]);
return;
}*/
String statLine;
EFClientStatistics pStats;
if (E.Data.Length > 0 && E.Target == null)
{
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
return;
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault();
if (E.Target == null)
{
E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
}
}
var clientStats = new GenericRepository<EFClientStatistics>();
@ -52,28 +44,31 @@ namespace IW4MAdmin.Plugins.Stats.Commands
if (E.Target != null)
{
pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId).First();
pStats = (await clientStats.FindAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId)).First();
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()}";
}
else
{
pStats = pStats = clientStats.Find(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId).First();
pStats = (await clientStats.FindAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId)).First();
statLine = $"^5{pStats.Kills} ^7{loc["PLUGINS_STATS_TEXT_KILLS"]} | ^5{pStats.Deaths} ^7{loc["PLUGINS_STATS_TEXT_DEATHS"]} | ^5{pStats.KDR} ^7KDR | ^5{pStats.Performance} ^7{loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"].ToUpper()}";
}
if (E.Message.IsBroadcastCommand())
{
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
await E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
await E.Owner.Broadcast(statLine);
E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
E.Owner.Broadcast(statLine);
}
else
{
if (E.Target != null)
await E.Origin.Tell($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{E.Target.Name}^7");
await E.Origin.Tell(statLine);
{
E.Origin.Tell($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{E.Target.Name}^7");
}
E.Origin.Tell(statLine);
}
}
}

View File

@ -4,11 +4,13 @@ using System.Collections.Generic;
namespace IW4MAdmin.Plugins.Stats.Config
{
class StatsConfiguration : IBaseConfiguration
public class StatsConfiguration : IBaseConfiguration
{
public bool EnableAntiCheat { get; set; }
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
public int TopPlayersMinPlayTime { get; set; }
public bool StoreClientKills { get; set; }
public string Name() => "Stats";
public IBaseConfiguration Generate()
{
@ -47,6 +49,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
},
};
TopPlayersMinPlayTime = 3600 * 3;
StoreClientKills = false;
return this;
}
}

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public static double[] AngleStuff(Vector3 a, Vector3 b)
{
double deltaX = 180.0 -Math.Abs(Math.Abs(a.X - b.X) - 180.0);
double deltaX = 180.0 - Math.Abs(Math.Abs(a.X - b.X) - 180.0);
double deltaY = 180.0 - Math.Abs(Math.Abs(a.Y - b.Y) - 180.0);
return new[] { deltaX, deltaY };

View File

@ -23,7 +23,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public int TeamCount(IW4Info.Team teamName)
{
if (PlayerStats.Count(p => p.Value.Team == IW4Info.Team.Spectator) / (double)PlayerStats.Count <= 0.25)
if (PlayerStats.Count(p => p.Value.Team == IW4Info.Team.None) / (double)PlayerStats.Count <= 0.25)
{
return IsTeamBased ? Math.Max(PlayerStats.Count(p => p.Value.Team == teamName), 1) : Math.Max(PlayerStats.Count - 1, 1);
}

View File

@ -11,6 +11,13 @@ using SharedLibraryCore.Objects;
using SharedLibraryCore.Commands;
using IW4MAdmin.Plugins.Stats.Models;
using System.Text.RegularExpressions;
using IW4MAdmin.Plugins.Stats.Web.Dtos;
using SharedLibraryCore.Database;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Services;
using System.Linq.Expressions;
using System.Threading;
namespace IW4MAdmin.Plugins.Stats.Helpers
{
@ -21,19 +28,158 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private ILogger Log;
private IManager Manager;
private readonly SemaphoreSlim OnProcessingPenalty;
public StatManager(IManager mgr)
{
Servers = new ConcurrentDictionary<int, ServerStats>();
ContextThreads = new ConcurrentDictionary<int, ThreadSafeStatsService>();
Log = mgr.GetLogger();
Manager = mgr;
OnProcessingPenalty = new SemaphoreSlim(1, 1);
}
~StatManager()
public EFClientStatistics GetClientStats(int clientId, int serverId) => Servers[serverId].PlayerStats[clientId];
public static Expression<Func<EFRating, bool>> GetRankingFunc(int? serverId = null)
{
Servers.Clear();
Log = null;
Servers = null;
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return (r) => r.ServerId == serverId &&
r.When > fifteenDaysAgo &&
r.RatingHistory.Client.Level != Player.Permission.Banned &&
r.Newest &&
r.ActivityAmount >= Plugin.Config.Configuration().TopPlayersMinPlayTime;
}
/// <summary>
/// gets a ranking across all servers for given client id
/// </summary>
/// <param name="clientId">client id of the player</param>
/// <returns></returns>
public async Task<int> GetClientOverallRanking(int clientId)
{
using (var context = new DatabaseContext(true))
{
var clientPerformance = await context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId == clientId)
.Where(r => r.ServerId == null)
.Where(r => r.Newest)
.Select(r => r.Performance)
.FirstOrDefaultAsync();
if (clientPerformance != 0)
{
var iqClientRanking = context.Set<EFRating>()
.Where(r => r.RatingHistory.ClientId != clientId)
.Where(r => r.Performance > clientPerformance)
.Where(GetRankingFunc());
return await iqClientRanking.CountAsync() + 1;
}
return 0;
}
}
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count)
{
using (var context = new DatabaseContext(true))
{
// setup the query for the clients within the given rating range
var iqClientRatings = (from rating in context.Set<EFRating>()
.Where(GetRankingFunc())
select new
{
rating.RatingHistory.ClientId,
rating.RatingHistory.Client.CurrentAlias.Name,
rating.RatingHistory.Client.LastConnection,
rating.Performance,
})
.OrderByDescending(c => c.Performance)
.Skip(start)
.Take(count);
#if DEBUG == true
var clientRatingsSql = iqClientRatings.ToSql();
#endif
// materialized list
var clientRatings = await iqClientRatings.ToListAsync();
// get all the unique client ids that are in the top stats
var clientIds = clientRatings
.GroupBy(r => r.ClientId)
.Select(r => r.First().ClientId)
.ToList();
var iqRatingInfo = from rating in context.Set<EFRating>()
where clientIds.Contains(rating.RatingHistory.ClientId)
where rating.ServerId == null
select new
{
rating.Ranking,
rating.Performance,
rating.RatingHistory.ClientId,
rating.When
};
#if DEBUG == true
var ratingQuery = iqRatingInfo.ToSql();
#endif
var ratingInfo = (await iqRatingInfo.ToListAsync())
.GroupBy(r => r.ClientId)
.Select(grp => new
{
grp.Key,
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
});
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
where clientIds.Contains(stat.ClientId)
group stat by stat.ClientId into s
select new
{
ClientId = s.Key,
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed)
});
#if DEBUG == true
var statsInfoSql = iqStatsInfo.ToSql();
#endif
var topPlayers = await iqStatsInfo.ToListAsync();
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
var finished = topPlayers.Select(s => new TopStatsInfo()
{
ClientId = s.ClientId,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = Utilities.GetTimePassed(clientRatingsDict[s.ClientId].LastConnection, false),
Name = clientRatingsDict[s.ClientId].Name,
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ?
ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() :
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
})
.OrderByDescending(r => r.Performance)
.ToList();
// set the ranking numerically
int i = start + 1;
foreach (var stat in finished)
{
stat.Ranking = i;
i++;
}
return finished;
}
}
/// <summary>
@ -97,8 +243,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
var playerStats = Servers[serverId].PlayerStats;
var statsSvc = ContextThreads[serverId];
var detectionStats = Servers[serverId].PlayerDetections;
var statsSvc = ContextThreads[serverId];
if (playerStats.ContainsKey(pl.ClientId))
{
@ -128,8 +274,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Active = true,
HitCount = 0,
Location = hl
})
.ToList()
}).ToList()
};
// insert if they've not been added
@ -147,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Location = hl
})
.ToList();
//await statsSvc.ClientStatSvc.SaveChangesAsync();
await statsSvc.ClientStatSvc.SaveChangesAsync();
}
// for stats before rating
@ -165,6 +310,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.LastActive = DateTime.UtcNow;
clientStats.LastStatCalculation = DateTime.UtcNow;
clientStats.SessionScore = pl.Score;
clientStats.LastScore = pl.Score;
Log.WriteInfo($"Adding {pl} to stats");
@ -225,7 +371,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (match.Success)
{
// this gives us what time the player is on
// this gives us what team the player is on
var attackerStats = Servers[serverId].PlayerStats[attackerClientId];
var victimStats = Servers[serverId].PlayerStats[victimClientId];
IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString());
@ -240,7 +386,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// </summary>
/// <returns></returns>
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 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;
@ -278,7 +425,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return;
}
var kill = new EFClientKill()
var hit = new EFClientKill()
{
Active = true,
AttackerId = attacker.ClientId,
@ -296,11 +443,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
When = time,
IsKillstreakKill = isKillstreakKill[0] != '0',
AdsPercent = float.Parse(Ads),
Fraction = double.Parse(fraction),
VisibilityPercentage = double.Parse(visibilityPercentage),
IsKill = !isDamage,
AnglesList = snapshotAngles
};
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
kill.Damage == 100000)
if ((hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
hit.Damage == 100000) || hit.HitLoc == IW4Info.HitLocation.shield)
{
// suicide by switching teams so let's not count it against them
return;
@ -311,7 +461,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await AddStandardKill(attacker, victim);
}
if (kill.IsKillstreakKill)
if (hit.IsKillstreakKill)
{
return;
}
@ -322,98 +472,187 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStatsSvc.Update(clientStats);
// increment their hit count
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
kill.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
kill.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
{
clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
//statsSvc.ClientStatSvc.Update(clientStats);
// await statsSvc.ClientStatSvc.SaveChangesAsync();
clientStats.HitLocations.Single(hl => hl.Location == hit.HitLoc).HitCount += 1;
}
//statsSvc.KillStatsSvc.Insert(kill);
//await statsSvc.KillStatsSvc.SaveChangesAsync();
if (Plugin.Config.Configuration().EnableAntiCheat)
using (var ctx = new DatabaseContext())
{
async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
await OnProcessingPenalty.WaitAsync();
try
{
// prevent multiple bans from occuring
if (attacker.Level == Player.Permission.Banned)
if (Plugin.Config.Configuration().StoreClientKills)
{
return;
ctx.Set<EFClientKill>().Add(hit);
}
switch (penalty.ClientPenalty)
if (Plugin.Config.Configuration().EnableAntiCheat)
{
case Penalty.PenaltyType.Ban:
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
{
ClientId = 1
});
break;
case Penalty.PenaltyType.Flag:
if (attacker.Level != Player.Permission.User)
break;
var e = new GameEvent()
{
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
Origin = new Player()
{
ClientId = 1,
Level = Player.Permission.Console,
ClientNumber = -1,
CurrentServer = attacker.CurrentServer
},
Target = attacker,
Owner = attacker.CurrentServer,
Type = GameEvent.EventType.Flag
};
await new CFlag().ExecuteAsync(e);
break;
await ApplyPenalty(clientDetection.ProcessKill(hit, isDamage), clientDetection, attacker, ctx);
await ApplyPenalty(clientDetection.ProcessTotalRatio(clientStats), clientDetection, attacker, ctx);
}
await clientStatsSvc.SaveChangesAsync();
await ctx.SaveChangesAsync();
}
await executePenalty(clientDetection.ProcessKill(kill, isDamage));
await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
catch (Exception ex)
{
Log.WriteError("AC ERROR");
Log.WriteDebug(ex.GetExceptionInfo());
}
await clientStatsSvc.SaveChangesAsync();
OnProcessingPenalty.Release(1);
}
}
async Task ApplyPenalty(Cheat.DetectionPenaltyResult penalty, Cheat.Detection clientDetection, Player attacker, DatabaseContext ctx)
{
switch (penalty.ClientPenalty)
{
case Penalty.PenaltyType.Ban:
if (attacker.Level == Player.Permission.Banned)
{
break;
}
attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
{
ClientId = 1,
AdministeredPenalties = new List<EFPenalty>()
{
new EFPenalty()
{
AutomatedOffense = penalty.Type == Cheat.Detection.DetectionType.Bone ?
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
}
}
});
if (clientDetection.Tracker.HasChanges)
{
SaveTrackedSnapshots(clientDetection, ctx);
}
break;
case Penalty.PenaltyType.Flag:
if (attacker.Level != Player.Permission.User)
{
break;
}
var e = new GameEvent()
{
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
Origin = new Player()
{
ClientId = 1,
Level = Player.Permission.Console,
ClientNumber = -1,
CurrentServer = attacker.CurrentServer
},
Target = attacker,
Owner = attacker.CurrentServer,
Type = GameEvent.EventType.Flag
};
// because we created an event it must be processed by the manager
// even if it didn't really do anything
Manager.GetEventHandler().AddEvent(e);
await new CFlag().ExecuteAsync(e);
if (clientDetection.Tracker.HasChanges)
{
SaveTrackedSnapshots(clientDetection, ctx);
}
break;
}
}
void SaveTrackedSnapshots(Cheat.Detection clientDetection, DatabaseContext ctx)
{
// todo: why does this cause duplicate primary key
var change = clientDetection.Tracker.GetNextChange();
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
{
if (change.HitOrigin.Vector3Id > 0)
{
change.HitOriginId = change.HitOrigin.Vector3Id;
ctx.Attach(change.HitOrigin);
}
else if (change.HitOrigin.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.HitDestination.Vector3Id > 0)
{
change.HitDestinationId = change.HitDestination.Vector3Id;
ctx.Attach(change.HitDestination);
}
else if (change.HitDestination.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.CurrentViewAngle.Vector3Id > 0)
{
change.CurrentViewAngleId = change.CurrentViewAngle.Vector3Id;
ctx.Attach(change.CurrentViewAngle);
}
else if (change.CurrentViewAngle.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
if (change.LastStrainAngle.Vector3Id > 0)
{
change.LastStrainAngleId = change.LastStrainAngle.Vector3Id;
ctx.Attach(change.LastStrainAngle);
}
else if (change.LastStrainAngle.Vector3Id == 0)
{
ctx.Add(change.HitOrigin);
}
ctx.Add(change);
}
}
public async Task AddStandardKill(Player attacker, Player victim)
{
int serverId = attacker.CurrentServer.GetHashCode();
EFClientStatistics attackerStats = null;
try
if (!Servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
{
attackerStats = await AddPlayer(attacker);
}
else
{
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
}
catch (KeyNotFoundException)
EFClientStatistics victimStats = null;
if (!Servers[serverId].PlayerStats.ContainsKey(victim.ClientId))
{
// happens when the client has disconnected before the last status update
Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
return;
victimStats = await AddPlayer(victim);
}
EFClientStatistics victimStats = null;
try
else
{
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
}
catch (KeyNotFoundException)
{
Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
return;
}
#if DEBUG
Log.WriteDebug("Calculating standard kill");
Log.WriteDebug("Calculating standard kill");
#endif
// update the total stats
@ -442,7 +681,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
StreakMessage.MessageOnStreak(-1, -1);
if (streakMessage != string.Empty)
await attacker.Tell(streakMessage);
{
attacker.Tell(streakMessage);
}
// fixme: why?
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
@ -459,6 +700,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
attackerStats.Skill = 0.0;
}
// update their performance
#if !DEBUG
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 2.5)
#else
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 0.1)
#endif
{
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
await UpdateStatHistory(attacker, attackerStats);
}
// todo: do we want to save this immediately?
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
clientStatsSvc.Update(attackerStats);
@ -466,6 +718,182 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await clientStatsSvc.SaveChangesAsync();
}
/// <summary>
/// Update the invidual and average stat history for a client
/// </summary>
/// <param name="client">client to update</param>
/// <param name="clientStats">stats of client that is being updated</param>
/// <returns></returns>
private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats)
{
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
// don't update their stat history if they haven't played long
#if DEBUG == false
if (currentSessionTime < 60)
{
return;
}
#endif
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
using (var ctx = new DatabaseContext())
{
// select the rating history for client
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
.Include(h => h.Ratings)
where history.ClientId == client.ClientId
select history;
// get the client ratings
var clientHistory = await iqHistoryLink
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
{
Active = true,
ClientId = client.ClientId,
Ratings = new List<EFRating>()
};
// it's the first time they've played
if (clientHistory.RatingHistoryId == 0)
{
ctx.Add(clientHistory);
}
#region INDIVIDUAL_SERVER_PERFORMANCE
// get the client ranking for the current server
int individualClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc(clientStats.ServerId))
// ignore themselves in the query
.Where(c => c.RatingHistory.ClientId != client.ClientId)
.Where(c => c.Performance > clientStats.Performance)
.CountAsync() + 1;
// limit max history per server to 40
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
{
// select the oldest one
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous newest to false
var ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == clientStats.ServerId)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
var newServerRating = new EFRating()
{
Performance = clientStats.Performance,
Ranking = individualClientRanking,
Active = true,
Newest = true,
ServerId = clientStats.ServerId,
RatingHistoryId = clientHistory.RatingHistoryId,
ActivityAmount = currentServerTotalPlaytime,
};
// add new rating for current server
ctx.Add(newServerRating);
#endregion
#region OVERALL_RATING
// select all performance & time played for current client
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
where stats.ClientId == client.ClientId
where stats.ServerId != clientStats.ServerId
select new
{
stats.Performance,
stats.TimePlayed
};
var clientStatsList = await iqClientStats.ToListAsync();
// add the current server's so we don't have to pull it frmo the database
clientStatsList.Add(new
{
clientStats.Performance,
TimePlayed = currentServerTotalPlaytime
});
// weight the overall performance based on play time
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
// shouldn't happen but just in case the sum of time played is 0
if (double.IsNaN(performanceAverage))
{
performanceAverage = clientStatsList.Average(p => p.Performance);
}
int overallClientRanking = await ctx.Set<EFRating>()
.Where(GetRankingFunc())
.Where(r => r.RatingHistory.ClientId != client.ClientId)
.Where(r => r.Performance > performanceAverage)
.CountAsync() + 1;
// limit max average history to 40
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
{
var ratingToRemove = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderBy(r => r.When)
.First();
ctx.Remove(ratingToRemove);
}
// set the previous average newest to false
ratingToUnsetNewest = clientHistory.Ratings
.Where(r => r.ServerId == null)
.OrderByDescending(r => r.When)
.FirstOrDefault();
if (ratingToUnsetNewest != null)
{
if (ratingToUnsetNewest.Newest)
{
ctx.Update(ratingToUnsetNewest);
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
ratingToUnsetNewest.Newest = false;
}
}
// add new average rating
var averageRating = new EFRating()
{
Active = true,
Newest = true,
Performance = performanceAverage,
Ranking = overallClientRanking,
ServerId = null,
RatingHistoryId = clientHistory.RatingHistoryId,
ActivityAmount = clientStatsList.Sum(s => s.TimePlayed)
};
ctx.Add(averageRating);
#endregion
await ctx.SaveChangesAsync();
}
}
/// <summary>
/// Performs the incrementation of kills and deaths for client statistics
/// </summary>
@ -495,35 +923,35 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// calulate elo
if (Servers[attackerStats.ServerId].PlayerStats.Count > 1)
{
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != attackerStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
attackerStats.EloRating;
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != victimStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != attackerStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
victimStats.EloRating;*/
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
attackerStats.EloRating;
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != victimStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
victimStats.EloRating;*/
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) - Math.Log(Math.Max(1, attackerStats.EloRating));
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
attackerStats.EloRating += 6.0 * (1 - winPercentage);
victimStats.EloRating -= 6.0 * (1 - winPercentage);
@ -575,10 +1003,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
double spmMultiplier = 2.934 * Math.Pow(Servers[clientStats.ServerId].TeamCount(clientStats.Team == IW4Info.Team.Allies ? IW4Info.Team.Axis : IW4Info.Team.Allies), -0.454);
killSPM *= Math.Max(1, spmMultiplier);
// update this for ac tracking
clientStats.SessionSPM = killSPM;
// calculate how much the KDR should weigh
// 1.637 is a Eddie-Generated number that weights the KDR nicely
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;
double alpha = Math.Sqrt(2) / Math.Min(600, clientStats.Kills + clientStats.Deaths);
double alpha = Math.Sqrt(2) / Math.Min(600, Math.Max(clientStats.Kills + clientStats.Deaths, 1));
clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR;
double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3);
@ -686,14 +1117,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
int serverId = sv.GetHashCode();
var statsSvc = ContextThreads[serverId];
Log.WriteDebug("Syncing stats contexts");
// Log.WriteDebug("Syncing stats contexts");
await statsSvc.ServerStatsSvc.SaveChangesAsync();
//await statsSvc.ClientStatSvc.SaveChangesAsync();
await statsSvc.KillStatsSvc.SaveChangesAsync();
await statsSvc.ServerSvc.SaveChangesAsync();
statsSvc = null;
// this should prevent the gunk for having a long lasting context.
// this should prevent the gunk from having a long lasting context.
ContextThreads[serverId] = new ThreadSafeStatsService();
}

View File

@ -14,7 +14,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
get
{
return new GenericRepository<EFClientStatistics>();
return new GenericRepository<EFClientStatistics>(true);
}
}
public GenericRepository<EFServer> ServerSvc { get; private set; }
@ -30,11 +30,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public ThreadSafeStatsService()
{
//ClientStatSvc = new GenericRepository<EFClientStatistics>();
ServerSvc = new GenericRepository<EFServer>();
KillStatsSvc = new GenericRepository<EFClientKill>();
ServerStatsSvc = new GenericRepository<EFServerStatistics>();
//MessageSvc = new GenericRepository<EFClientMessage>();
}
}
}

View File

@ -10,9 +10,10 @@ namespace IW4MAdmin.Plugins.Stats
{
public enum Team
{
None,
Spectator,
Axis,
Allies
Allies,
Axis
}
public enum MeansOfDeath
@ -64,6 +65,7 @@ namespace IW4MAdmin.Plugins.Stats
right_foot,
left_foot,
gun,
shield
}
public enum WeaponName
@ -1368,7 +1370,9 @@ namespace IW4MAdmin.Plugins.Stats
dragunov_mp,
cobra_player_minigun_mp,
destructible_car,
sentry_minigun_mp
sentry_minigun_mp,
cobra_20mm_mp,
shield
}
public enum MapName

View File

@ -0,0 +1,51 @@
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IW4MAdmin.Plugins.Stats.Models
{
/// <summary>
/// This class houses the information for anticheat snapshots (used for validating a ban)
/// </summary>
public class EFACSnapshot : SharedEntity
{
[Key]
public int SnapshotId { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public EFClient Client { get; set; }
public DateTime When { get; set; }
public int CurrentSessionLength { get; set; }
public int TimeSinceLastEvent { get; set; }
public double EloRating { get; set; }
public int SessionScore { get; set; }
public double SessionSPM { get; set; }
public int Hits { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public double CurrentStrain { get; set; }
public double StrainAngleBetween { get; set; }
public double SessionAngleOffset { get; set; }
public int LastStrainAngleId { get; set; }
[ForeignKey("LastStrainAngleId")]
public Vector3 LastStrainAngle { get; set; }
public int HitOriginId { get; set; }
[ForeignKey("HitOriginId")]
public Vector3 HitOrigin { get; set; }
public int HitDestinationId { get; set; }
[ForeignKey("HitDestinationId")]
public Vector3 HitDestination { get; set; }
public double Distance { get; set; }
public int CurrentViewAngleId { get; set; }
[ForeignKey("CurrentViewAngleId")]
public Vector3 CurrentViewAngle { get; set; }
public IW4Info.WeaponName WeaponId { get; set; }
public IW4Info.HitLocation HitLocation { get; set; }
public IW4Info.MeansOfDeath HitType { get; set; }
public virtual ICollection<Vector3> PredictedViewAngles { get; set; }
}
}

View File

@ -29,6 +29,9 @@ namespace IW4MAdmin.Plugins.Stats.Models
public Vector3 DeathOrigin { get; set; }
public Vector3 ViewAngles { get; set; }
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;

View File

@ -0,0 +1,20 @@
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFClientRatingHistory : SharedEntity
{
[Key]
public int RatingHistoryId { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public virtual EFClient Client { get; set; }
public virtual ICollection<EFRating> Ratings { get; set; }
}
}

View File

@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
public double EloRating { get; set; }
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
public double RollingWeightedKDR { get; set; }
public double VisionAverage { get; set; }
[NotMapped]
public double Performance
{
@ -71,6 +72,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
DeathStreak = 0;
LastScore = 0;
SessionScores.Add(0);
Team = IW4Info.Team.None;
}
[NotMapped]
public int SessionScore
@ -96,5 +98,9 @@ namespace IW4MAdmin.Plugins.Stats.Models
private List<int> SessionScores = new List<int>() { 0 };
[NotMapped]
public IW4Info.Team Team { get; set; }
[NotMapped]
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
[NotMapped]
public double SessionSPM { get; set; }
}
}

View File

@ -0,0 +1,31 @@
using SharedLibraryCore.Database.Models;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFRating : SharedEntity
{
[Key]
public int RatingId { get; set; }
public int RatingHistoryId { get; set; }
[ForeignKey("RatingHistoryId")]
public virtual EFClientRatingHistory RatingHistory { get; set; }
// if null, indicates that the rating is an average rating
public int? ServerId { get; set; }
// [ForeignKey("ServerId")] can't make this nullable if this annotation is set
public virtual EFServer Server { get; set; }
[Required]
public double Performance { get; set; }
[Required]
public int Ranking { get; set; }
[Required]
// indicates if the rating is the latest
public bool Newest { get; set; }
[Required]
public int ActivityAmount { get; set; }
[Required]
public DateTime When { get; set; } = DateTime.UtcNow;
}
}

View File

@ -21,6 +21,18 @@ namespace Stats.Models
.Property(c => c.ServerId)
.HasColumnName("EFClientStatistics_ServerId");
builder.Entity<EFRating>()
.HasIndex(p => p.Performance);
builder.Entity<EFRating>()
.HasIndex(p => p.Ranking);
builder.Entity<EFRating>()
.HasIndex(p => p.When);
builder.Entity<EFClientMessage>()
.HasIndex(p => p.TimeSent);
// force pluralization
builder.Entity<EFClientKill>().ToTable("EFClientKills");
builder.Entity<EFClientMessage>().ToTable("EFClientMessages");

View File

@ -16,7 +16,7 @@ using IW4MAdmin.Plugins.Stats.Models;
namespace IW4MAdmin.Plugins.Stats
{
class Plugin : IPlugin
public class Plugin : IPlugin
{
public string Name => "Simple Stats";
@ -44,7 +44,7 @@ namespace IW4MAdmin.Plugins.Stats
await Manager.RemovePlayer(E.Origin);
break;
case GameEvent.EventType.Say:
if (!string.IsNullOrEmpty(E.Data) &&
if (!string.IsNullOrEmpty(E.Data) &&
E.Origin.ClientId > 1)
await Manager.AddMessageAsync(E.Origin.ClientId, E.Owner.GetHashCode(), E.Data);
break;
@ -55,6 +55,8 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.MapEnd:
break;
case GameEvent.EventType.JoinTeam:
break;
case GameEvent.EventType.Broadcast:
break;
case GameEvent.EventType.Tell:
@ -63,8 +65,6 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.Ban:
break;
case GameEvent.EventType.Remote:
break;
case GameEvent.EventType.Unknown:
break;
case GameEvent.EventType.Report:
@ -73,25 +73,31 @@ namespace IW4MAdmin.Plugins.Stats
break;
case GameEvent.EventType.ScriptKill:
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
if (killInfo.Length >= 13)
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[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:
if (!E.Owner.CustomCallback)
{
await Manager.AddStandardKill(E.Origin, E.Target);
break;
case GameEvent.EventType.Death:
}
break;
case GameEvent.EventType.Damage:
// if (!E.Owner.CustomCallback)
if (!E.Owner.CustomCallback)
{
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, E.Owner.GetHashCode());
}
break;
case GameEvent.EventType.ScriptDamage:
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
if (killInfo.Length >= 13)
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[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11], killInfo[12], killInfo[13], killInfo[14], killInfo[15]);
}
break;
}
}
@ -106,6 +112,13 @@ namespace IW4MAdmin.Plugins.Stats
await Config.Save();
}
// register the topstats page
// todo:generate the URL/Location instead of hardcoding
manager.GetPageList()
.Pages.Add(
Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"],
"/Stats/TopPlayersAsync");
// meta data info
async Task<List<ProfileMeta>> getStats(int clientId)
{
@ -122,6 +135,11 @@ namespace IW4MAdmin.Plugins.Stats
return new List<ProfileMeta>()
{
new ProfileMeta()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + await Manager.GetClientOverallRanking(clientId),
},
new ProfileMeta()
{
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
@ -161,7 +179,6 @@ namespace IW4MAdmin.Plugins.Stats
double chestAbdomenRatio = 0;
double hitOffsetAverage = 0;
double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain);
//double maxAngle = clientStats.Max(cs => cs.HitLocations.Max(hl => hl.MaxAngleDistance));
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
{
@ -223,12 +240,6 @@ namespace IW4MAdmin.Plugins.Stats
Value = Math.Round(maxStrain, 3),
Sensitive = true
},
/*new ProfileMeta()
{
Key = "Max Angle Distance",
Value = Math.Round(maxAngle, 1),
Sensitive = true
}*/
};
}
@ -240,7 +251,8 @@ namespace IW4MAdmin.Plugins.Stats
{
Key = "EventMessage",
Value = m.Message,
When = m.TimeSent
When = m.TimeSent,
Extra = m.ServerId.ToString()
}).ToList();
messageMeta.Add(new ProfileMeta()
{

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp2.1</TargetFramework>
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
@ -15,19 +15,26 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Cheat\Strain.cs~RF16f7b3.TMP" />
<Content Include="Web\Views\Stats\_MessageContext.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
<PackageReference Update="Microsoft.NETCore.App" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy &quot;$(TargetPath)&quot; &quot;$(SolutionDir)BUILD\Plugins&quot;" />
</Target>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="xcopy /E /K /Y /C /I &quot;$(ProjectDir)Web\Views&quot; &quot;$(SolutionDir)WebfrontCore\Views\Plugins&quot;&#xD;&#xA;xcopy /E /K /Y /C /I &quot;$(ProjectDir)Web\wwwroot\images&quot; &quot;$(SolutionDir)WebfrontCore\wwwroot\images&quot;" />
</Target>
</Project>

View File

@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using WebfrontCore.Controllers;
namespace IW4MAdmin.Plugins.Stats.Web.Controllers
{
public class StatsController : BaseController
{
[HttpGet]
public async Task<IActionResult> TopPlayersAsync()
{
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_DESC"];
return View("Index", await Plugin.Manager.GetTopStats(0, 50));
}
[HttpGet]
public async Task<IActionResult> GetTopPlayersAsync(int count, int offset)
{
return View("_List", await Plugin.Manager.GetTopStats(offset, count));
}
[HttpGet]
public async Task<IActionResult> GetMessageAsync(int serverId, DateTime when)
{
var whenUpper = when.AddMinutes(5);
var whenLower = when.AddMinutes(-5);
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
{
var iqMessages = from message in ctx.Set<Models.EFClientMessage>()
where message.ServerId == serverId
where message.TimeSent >= whenLower
where message.TimeSent <= whenUpper
select new SharedLibraryCore.Dtos.ChatInfo()
{
ClientId = message.ClientId,
Message = message.Message,
Name = message.Client.CurrentAlias.Name,
Time = message.TimeSent
};
#if DEBUG == true
var messagesSql = iqMessages.ToSql();
#endif
var messages = await iqMessages.ToListAsync();
return View("_MessageContext", messages);
}
}
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int clientId)
{
using (var ctx = new SharedLibraryCore.Database.DatabaseContext(true))
{
var penaltyInfo = await ctx.Set<Models.EFACSnapshot>()
.Where(s => s.ClientId == clientId)
.Include(s => s.LastStrainAngle)
.Include(s => s.HitOrigin)
.Include(s => s.HitDestination)
.Include(s => s.CurrentViewAngle)
.Include(s => s.PredictedViewAngles)
.OrderBy(s => s.When)
.ThenBy(s => s.Hits)
.ToListAsync();
return View("_PenaltyInfo", penaltyInfo);
}
}
}
}

View File

@ -0,0 +1,22 @@
using SharedLibraryCore.Dtos;
using System;
using System.Collections.Generic;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Web.Dtos
{
public class TopStatsInfo : SharedInfo
{
public int Ranking { get; set; }
public string Name { get; set; }
public int ClientId { get; set; }
public double KDR { get; set; }
public double Performance { get; set; }
public string TimePlayed { get; set; }
public string LastSeen { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public int RatingChange { get; set; }
public List<double> PerformanceHistory { get; set; }
}
}

View File

@ -0,0 +1,14 @@
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
<h4 class="pb-2 text-center ">@ViewBag.Title</h4>
<div id="stats_top_players" class="striped border-top border-bottom">
@await Html.PartialAsync("_List", Model)
</div>
@section scripts {
<environment include="Development">
<script type="text/javascript" src="~/js/loader.js"></script>
<script type="text/javascript" src="~/js/stats.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players', 50);</script>
}

View File

@ -0,0 +1,68 @@
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{
Layout = null;
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex.Set;
double getDeviation(double deviations) => Math.Pow(Math.E, 5.0813 + (deviations * 0.8694));
string rankIcon(double elo)
{
if (elo >= getDeviation(-1) && elo < getDeviation(-0.25))
return "0_no-place/menu_div_no_place.png";
if (elo >= getDeviation(-0.25) && elo < getDeviation(0.25))
return "1_iron/menu_div_iron_sub03.png";
if (elo >= getDeviation(0.25) && elo < getDeviation(0.6875))
return "2_bronze/menu_div_bronze_sub03.png";
if (elo >= getDeviation(0.6875) && elo < getDeviation(1))
return "3_silver/menu_div_silver_sub03.png";
if (elo >= getDeviation(1) && elo < getDeviation(1.25))
return "4_gold/menu_div_gold_sub03.png";
if (elo >= getDeviation(1.25) && elo < getDeviation(1.5))
return "5_platinum/menu_div_platinum_sub03.png";
if (elo >= getDeviation(1.5) && elo < getDeviation(1.75))
return "6_semipro/menu_div_semipro_sub03.png";
if (elo >= getDeviation(1.75))
return "7_pro/menu_div_pro_sub03.png";
return "0_no-place/menu_div_no_place.png";
}
}
@foreach (var stat in Model)
{
<div class="row ml-0 mr-0 pt-2 pb-2">
<div class="col-md-4 text-md-left text-center">
<div class="h2 d-flex flex-row justify-content-center justify-content-md-start align-items-center">
<div class="text-muted pr-1">#@stat.Ranking</div>
@if (stat.RatingChange > 0)
{
<div class="d-flex flex-column text-center">
<div class="oi oi-caret-top text-success client-rating-change-up"></div>
<div class="client-rating-change-amount text-success">@stat.RatingChange</div>
</div>
}
@if (stat.RatingChange < 0)
{
<div class="d-flex flex-column text-center">
<div class="client-rating-change-amount client-rating-change-amount-down text-danger">@Math.Abs(stat.RatingChange)</div>
<div class="oi oi-caret-bottom text-danger client-rating-change-down"></div>
</div>
}
<span class="text-muted pl-1 pr-1" style="font-size: 1.25rem;">&mdash;</span>
@Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId })
</div>
<span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br />
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_TIME_HOURS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
</div>
<div class="col-md-6 client-rating-graph" id="rating_history_@stat.ClientId" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))">
</div>
<div class="col-md-2 client-rating-icon text-md-right text-center align-items-center d-flex justify-content-center">
<img src="/images/icons/@rankIcon(stat.Performance)" />
</div>
</div>
}

View File

@ -0,0 +1,12 @@
@model IEnumerable<SharedLibraryCore.Dtos.ChatInfo>
@{
Layout = null;
}
<div class="client-message-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
<h5>@Model.First().Time.ToString()</h5>
@foreach (var message in Model)
{
<span class="text-white">@Html.ActionLink(@message.Name, "ProfileAsync", "Client", new { id = message.ClientId})</span><span> &mdash; @message.Message</span><br />
}
</div>

View File

@ -0,0 +1,29 @@
@model IEnumerable<IW4MAdmin.Plugins.Stats.Models.EFACSnapshot>
@{
Layout = null;
}
<div class="penalty-info-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
@foreach (var snapshot in Model)
{
<!-- this is not ideal, but I didn't want to manually write out all the properties-->
var snapProperties = typeof(IW4MAdmin.Plugins.Stats.Models.EFACSnapshot).GetProperties();
foreach (var prop in snapProperties)
{
<!-- this is another ugly hack-->
@if (prop.GetValue(snapshot) is System.Collections.Generic.HashSet<SharedLibraryCore.Helpers.Vector3>)
{
<span class="text-white">@prop.Name </span>
foreach (var v in (System.Collections.Generic.HashSet<SharedLibraryCore.Helpers.Vector3>)prop.GetValue(snapshot))
{
<span>@v.ToString(),</span><br />
}
}
else
{
<span class="text-white">@prop.Name </span> <span>&mdash; @prop.GetValue(snapshot)</span><br />
}
}
<div class="w-100 mt-1 mb-1 border-bottom"></div>
}
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More