You've already forked IW4M-Admin
Compare commits
49 Commits
2.1
...
2.2-prerel
Author | SHA1 | Date | |
---|---|---|---|
f4ac815d07 | |||
7fa0b52543 | |||
d45729d7e1 | |||
5d93e7ac57 | |||
0f9d2e92e1 | |||
4a46abc46d | |||
7c708f06f3 | |||
98adfb12d2 | |||
a786541484 | |||
b9086fd145 | |||
3d8108f339 | |||
39596db56e | |||
ba5b1e19a6 | |||
385879618d | |||
0c90d02e44 | |||
cfbacabb4a | |||
672d45df7c | |||
20d4ab27d3 | |||
e77ef69ee8 | |||
cc7628d058 | |||
46bdc2ac33 | |||
bbefd53db4 | |||
56cb8c50e7 | |||
0538d9f479 | |||
1343d4959e | |||
ac64d8d3c1 | |||
b5939bbdaf | |||
a0fafe5797 | |||
bbade07646 | |||
3c0e101f14 | |||
d0be08629d | |||
9d00d5a16a | |||
396e5c9215 | |||
4ec16d3aa2 | |||
f40bcce44f | |||
6071ad8653 | |||
16d7ccd590 | |||
87541c4a5a | |||
af6361144e | |||
454238a192 | |||
e7c7145da1 | |||
5be6b75ccf | |||
e60f612f95 | |||
ba023ceeb5 | |||
e3dba96d72 | |||
6d0f859a93 | |||
696e2d12c9 | |||
bf68e5672f | |||
2204686b08 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -220,7 +220,12 @@ Thumbs.db
|
|||||||
DEPLOY
|
DEPLOY
|
||||||
global.min.css
|
global.min.css
|
||||||
global.min.js
|
global.min.js
|
||||||
bootstrap-custom.css
|
|
||||||
bootstrap-custom.min.css
|
bootstrap-custom.min.css
|
||||||
**/Master/static
|
**/Master/static
|
||||||
**/Master/dev_env
|
**/Master/dev_env
|
||||||
|
/WebfrontCore/Views/Plugins/Stats
|
||||||
|
/WebfrontCore/wwwroot/**/dds
|
||||||
|
|
||||||
|
/DiscordWebhook/env
|
||||||
|
/DiscordWebhook/config.dev.json
|
||||||
|
/GameLogServer/env
|
||||||
|
@ -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, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
12
Application/API/GameLogServer/IGameLogServer.cs
Normal file
12
Application/API/GameLogServer/IGameLogServer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
17
Application/API/GameLogServer/LogInfo.cs
Normal file
17
Application/API/GameLogServer/LogInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ namespace IW4MAdmin.Application.API.Master
|
|||||||
public class Endpoint
|
public class Endpoint
|
||||||
{
|
{
|
||||||
#if !DEBUG
|
#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
|
#else
|
||||||
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
private static IMasterApi api = RestClient.For<IMasterApi>("http://127.0.0.1");
|
||||||
#endif
|
#endif
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
|
||||||
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Application</PackageId>
|
||||||
<Version>2.1.0</Version>
|
<Version>2.1.9.2</Version>
|
||||||
<Authors>RaidMax</Authors>
|
<Authors>RaidMax</Authors>
|
||||||
<Company>Forever None</Company>
|
<Company>Forever None</Company>
|
||||||
<Product>IW4MAdmin</Product>
|
<Product>IW4MAdmin</Product>
|
||||||
@ -23,12 +23,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="RestEase" Version="1.4.5" />
|
<PackageReference Include="RestEase" Version="1.4.7" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
|
<TieredCompilation>true</TieredCompilation>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -75,19 +76,21 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
<PackageReference Update="Microsoft.NETCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
<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>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<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>
|
||||||
|
|
||||||
<Target Name="PostPublish" AfterTargets="Publish">
|
<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>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -2,6 +2,9 @@ set SolutionDir=%1
|
|||||||
set ProjectDir=%2
|
set ProjectDir=%2
|
||||||
set TargetDir=%3
|
set TargetDir=%3
|
||||||
set OutDir=%4
|
set OutDir=%4
|
||||||
|
set Version=%5
|
||||||
|
|
||||||
|
echo %Version% > "%SolutionDir%DEPLOY\version.txt"
|
||||||
|
|
||||||
echo Copying dependency configs
|
echo Copying dependency configs
|
||||||
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
|
copy "%SolutionDir%WebfrontCore\%OutDir%*.deps.json" "%TargetDir%"
|
||||||
@ -18,3 +21,14 @@ echo Copying plugins for publish
|
|||||||
del %SolutionDir%BUILD\Plugins\Tests.dll
|
del %SolutionDir%BUILD\Plugins\Tests.dll
|
||||||
xcopy /Y "%SolutionDir%BUILD\Plugins" "%SolutionDir%Publish\Windows\Plugins\"
|
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\"
|
@ -54,6 +54,9 @@ del "%SolutionDir%Publish\Windows\*pdb"
|
|||||||
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
|
if exist "%SolutionDir%Publish\WindowsPrerelease\web.config" del "%SolutionDir%Publish\WindowsPrerelease\web.config"
|
||||||
del "%SolutionDir%Publish\WindowsPrerelease\*pdb"
|
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\WindowsPrerelease\StartIW4MAdmin.cmd"
|
||||||
@echo dotnet IW4MAdmin.dll > "%SolutionDir%Publish\Windows\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"
|
||||||
|
82
Application/Core/ClientAuthentication.cs
Normal file
82
Application/Core/ClientAuthentication.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -171,7 +171,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "IW4 Credits",
|
"Alias": "Test map",
|
||||||
"Name": "iw4_credits"
|
"Name": "iw4_credits"
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -190,6 +190,11 @@
|
|||||||
"Name": "mp_cargoship_sh"
|
"Name": "mp_cargoship_sh"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Cargoship",
|
||||||
|
"Name": "mp_cargoship"
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Shipment",
|
"Alias": "Shipment",
|
||||||
"Name": "mp_shipment"
|
"Name": "mp_shipment"
|
||||||
@ -216,23 +221,43 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Favela - Tropical",
|
"Alias": "Tropical Favela",
|
||||||
"Name": "mp_fav_tropical"
|
"Name": "mp_fav_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Estate - Tropical",
|
"Alias": "Tropical Estate",
|
||||||
"Name": "mp_estate_tropical"
|
"Name": "mp_estate_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Crash - Tropical",
|
"Alias": "Tropical Crash",
|
||||||
"Name": "mp_crash_tropical"
|
"Name": "mp_crash_tropical"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"Alias": "Forgotten City",
|
"Alias": "Forgotten City",
|
||||||
"Name": "mp_bloc_sh"
|
"Name": "mp_bloc_sh"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Crossfire",
|
||||||
|
"Name": "mp_cross_fire"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Bloc",
|
||||||
|
"Name": "mp_bloc"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Alias": "Oilrig",
|
||||||
|
"Name": "oilrig"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Name": "Village",
|
||||||
|
"Alias": "co_hunted"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -10,29 +10,38 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
class IW4EventParser : IEventParser
|
class IW4EventParser : IEventParser
|
||||||
{
|
{
|
||||||
|
private const string SayRegex = @"(say|sayteam);(.{1,32});([0-9]+)(.*);(.*)";
|
||||||
|
|
||||||
public virtual GameEvent GetEvent(Server server, string logLine)
|
public virtual GameEvent GetEvent(Server server, string logLine)
|
||||||
{
|
{
|
||||||
|
logLine = Regex.Replace(logLine, @"([0-9]+:[0-9]+ |^[0-9]+ )", "").Trim();
|
||||||
string[] lineSplit = logLine.Split(';');
|
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,
|
Type = GameEvent.EventType.JoinTeam,
|
||||||
Data = logLine,
|
Data = eventType,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6)),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
|
||||||
Owner = server
|
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 (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] == '@')
|
if (message[0] == '!' || message[0] == '@')
|
||||||
{
|
{
|
||||||
@ -40,7 +49,7 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Command,
|
Type = GameEvent.EventType.Command,
|
||||||
Data = message,
|
Data = message,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
Origin = origin,
|
||||||
Owner = server,
|
Owner = server,
|
||||||
Message = message
|
Message = message
|
||||||
};
|
};
|
||||||
@ -50,52 +59,127 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Say,
|
Type = GameEvent.EventType.Say,
|
||||||
Data = message,
|
Data = message,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2)),
|
Origin = origin,
|
||||||
Owner = server,
|
Owner = server,
|
||||||
Message = message
|
Message = message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("ScriptKill"))
|
if (eventType == "K")
|
||||||
{
|
{
|
||||||
|
if (!server.CustomCallback)
|
||||||
|
{
|
||||||
|
var origin = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 6));
|
||||||
|
var target = server.GetPlayersAsList().First(c => c.ClientNumber == Utilities.ClientIdFromString(lineSplit, 2));
|
||||||
|
|
||||||
|
return new GameEvent()
|
||||||
|
{
|
||||||
|
Type = GameEvent.EventType.Kill,
|
||||||
|
Data = logLine,
|
||||||
|
Origin = origin,
|
||||||
|
Target = target,
|
||||||
|
Owner = server
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.ScriptKill,
|
Type = GameEvent.EventType.ScriptKill,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("ScriptDamage"))
|
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()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.ScriptDamage,
|
Type = GameEvent.EventType.ScriptDamage,
|
||||||
Data = logLine,
|
Data = logLine,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[2].ConvertLong()),
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanedEventLine[0] == 'D')
|
// damage
|
||||||
|
if (eventType == "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)
|
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()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
Type = GameEvent.EventType.Damage,
|
Type = GameEvent.EventType.Damage,
|
||||||
Data = cleanedEventLine,
|
Data = eventType,
|
||||||
Origin = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[5].ConvertLong()),
|
Origin = origin,
|
||||||
Target = server.GetPlayersAsList().First(c => c.NetworkId == lineSplit[1].ConvertLong()),
|
Target = target,
|
||||||
Owner = server
|
Owner = server
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cleanedEventLine.Contains("ExitLevel"))
|
// 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()
|
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()
|
return new GameEvent()
|
||||||
{
|
{
|
||||||
|
@ -11,101 +11,6 @@ namespace IW4MAdmin.Application.EventParsers
|
|||||||
{
|
{
|
||||||
class T6MEventParser : IW4EventParser
|
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";
|
public override string GetGameDir() => $"t6r{Path.DirectorySeparatorChar}data";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,105 +1,23 @@
|
|||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
|
using SharedLibraryCore.Events;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
{
|
{
|
||||||
class GameEventHandler : IEventHandler
|
class GameEventHandler : IEventHandler
|
||||||
{
|
{
|
||||||
private ConcurrentQueue<GameEvent> EventQueue;
|
readonly IManager Manager;
|
||||||
private Queue<GameEvent> StatusSensitiveQueue;
|
|
||||||
private IManager Manager;
|
|
||||||
|
|
||||||
public GameEventHandler(IManager mgr)
|
public GameEventHandler(IManager mgr)
|
||||||
{
|
{
|
||||||
EventQueue = new ConcurrentQueue<GameEvent>();
|
|
||||||
StatusSensitiveQueue = new Queue<GameEvent>();
|
|
||||||
|
|
||||||
Manager = mgr;
|
Manager = mgr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddEvent(GameEvent gameEvent)
|
public void AddEvent(GameEvent gameEvent)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
((Manager as ApplicationManager).OnServerEvent)(this, new GameEventArgs(null, false, gameEvent));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
87
Application/IO/GameLogEventDetection.cs
Normal file
87
Application/IO/GameLogEventDetection.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,14 @@ using System.Text;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Application.IO
|
namespace IW4MAdmin.Application.IO
|
||||||
{
|
{
|
||||||
class GameLogReader
|
class GameLogReader : IGameLogReader
|
||||||
{
|
{
|
||||||
IEventParser Parser;
|
IEventParser Parser;
|
||||||
string LogFile;
|
readonly string LogFile;
|
||||||
|
|
||||||
|
public long Length => new FileInfo(LogFile).Length;
|
||||||
|
|
||||||
|
public int UpdateInterval => 300;
|
||||||
|
|
||||||
public GameLogReader(string logFile, IEventParser parser)
|
public GameLogReader(string logFile, IEventParser parser)
|
||||||
{
|
{
|
||||||
@ -18,7 +22,7 @@ namespace IW4MAdmin.Application.IO
|
|||||||
Parser = parser;
|
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
|
// allocate the bytes for the new log lines
|
||||||
List<string> logLines = new List<string>();
|
List<string> logLines = new List<string>();
|
||||||
|
72
Application/IO/GameLogReaderHttp.cs
Normal file
72
Application/IO/GameLogReaderHttp.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -114,11 +114,11 @@
|
|||||||
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
|
"COMMANDS_WARNCLEAR_DESC": "remove all warnings for a client",
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
"COMMANDS_WARNCLEAR_SUCCESS": "All warning cleared for",
|
||||||
"COMMANDS_WHO_DESC": "give information about yourself",
|
"COMMANDS_WHO_DESC": "give information about yourself",
|
||||||
"GLOBAL_DAYS": "days",
|
"GLOBAL_TIME_DAYS": "days",
|
||||||
"GLOBAL_ERROR": "Error",
|
"GLOBAL_ERROR": "Error",
|
||||||
"GLOBAL_HOURS": "hours",
|
"GLOBAL_TIME_HOURS": "hours",
|
||||||
"GLOBAL_INFO": "Info",
|
"GLOBAL_INFO": "Info",
|
||||||
"GLOBAL_MINUTES": "minutes",
|
"GLOBAL_TIME_MINUTES": "minutes",
|
||||||
"GLOBAL_REPORT": "If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command",
|
"GLOBAL_REPORT": "If you suspect someone of ^5CHEATING ^7use the ^5!report ^7command",
|
||||||
"GLOBAL_VERBOSE": "Verbose",
|
"GLOBAL_VERBOSE": "Verbose",
|
||||||
"GLOBAL_WARNING": "Warning",
|
"GLOBAL_WARNING": "Warning",
|
||||||
|
@ -114,11 +114,11 @@
|
|||||||
"COMMANDS_WARNCLEAR_DESC": "eliminar todas las advertencias de un cliente",
|
"COMMANDS_WARNCLEAR_DESC": "eliminar todas las advertencias de un cliente",
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Todas las advertencias borradas para",
|
"COMMANDS_WARNCLEAR_SUCCESS": "Todas las advertencias borradas para",
|
||||||
"COMMANDS_WHO_DESC": "da información sobre ti",
|
"COMMANDS_WHO_DESC": "da información sobre ti",
|
||||||
"GLOBAL_DAYS": "días",
|
"GLOBAL_TIME_DAYS": "días",
|
||||||
"GLOBAL_ERROR": "Error",
|
"GLOBAL_ERROR": "Error",
|
||||||
"GLOBAL_HOURS": "horas",
|
"GLOBAL_TIME_HOURS": "horas",
|
||||||
"GLOBAL_INFO": "Información",
|
"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_REPORT": "Si sospechas que alguien ^5usa cheats ^7usa el comando ^5!report",
|
||||||
"GLOBAL_VERBOSE": "Detallado",
|
"GLOBAL_VERBOSE": "Detallado",
|
||||||
"GLOBAL_WARNING": "Advertencia",
|
"GLOBAL_WARNING": "Advertencia",
|
||||||
|
@ -114,11 +114,11 @@
|
|||||||
"COMMANDS_WARNCLEAR_DESC": "remove todos os avisos para um cliente",
|
"COMMANDS_WARNCLEAR_DESC": "remove todos os avisos para um cliente",
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Todos as advertências foram apagados para",
|
"COMMANDS_WARNCLEAR_SUCCESS": "Todos as advertências foram apagados para",
|
||||||
"COMMANDS_WHO_DESC": "dá informações sobre você",
|
"COMMANDS_WHO_DESC": "dá informações sobre você",
|
||||||
"GLOBAL_DAYS": "dias",
|
"GLOBAL_TIME_DAYS": "dias",
|
||||||
"GLOBAL_ERROR": "Erro",
|
"GLOBAL_ERROR": "Erro",
|
||||||
"GLOBAL_HOURS": "horas",
|
"GLOBAL_TIME_HOURS": "horas",
|
||||||
"GLOBAL_INFO": "Informação",
|
"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_REPORT": "Se você está suspeitando alguém de alguma ^5TRAPAÇA ^7use o comando ^5!report",
|
||||||
"GLOBAL_VERBOSE": "Detalhe",
|
"GLOBAL_VERBOSE": "Detalhe",
|
||||||
"GLOBAL_WARNING": "AVISO",
|
"GLOBAL_WARNING": "AVISO",
|
||||||
|
@ -114,11 +114,11 @@
|
|||||||
"COMMANDS_WARNCLEAR_DESC": "удалить все предупреждения у игрока",
|
"COMMANDS_WARNCLEAR_DESC": "удалить все предупреждения у игрока",
|
||||||
"COMMANDS_WARNCLEAR_SUCCESS": "Все предупреждения очищены у",
|
"COMMANDS_WARNCLEAR_SUCCESS": "Все предупреждения очищены у",
|
||||||
"COMMANDS_WHO_DESC": "предоставить информацию о себе",
|
"COMMANDS_WHO_DESC": "предоставить информацию о себе",
|
||||||
"GLOBAL_DAYS": "дней",
|
"GLOBAL_TIME_DAYS": "дней",
|
||||||
"GLOBAL_ERROR": "Ошибка",
|
"GLOBAL_ERROR": "Ошибка",
|
||||||
"GLOBAL_HOURS": "часов",
|
"GLOBAL_TIME_HOURS": "часов",
|
||||||
"GLOBAL_INFO": "Информация",
|
"GLOBAL_INFO": "Информация",
|
||||||
"GLOBAL_MINUTES": "минут",
|
"GLOBAL_TIME_MINUTES": "минут",
|
||||||
"GLOBAL_REPORT": "Если вы подозреваете кого-то в ^5ЧИТЕРСТВЕ^7, используйте команду ^5!report",
|
"GLOBAL_REPORT": "Если вы подозреваете кого-то в ^5ЧИТЕРСТВЕ^7, используйте команду ^5!report",
|
||||||
"GLOBAL_VERBOSE": "Подробно",
|
"GLOBAL_VERBOSE": "Подробно",
|
||||||
"GLOBAL_WARNING": "Предупреждение",
|
"GLOBAL_WARNING": "Предупреждение",
|
||||||
|
@ -17,8 +17,8 @@ namespace IW4MAdmin.Application
|
|||||||
Assert
|
Assert
|
||||||
}
|
}
|
||||||
|
|
||||||
string FileName;
|
readonly string FileName;
|
||||||
object ThreadLock;
|
readonly object ThreadLock;
|
||||||
|
|
||||||
public Logger(string fn)
|
public Logger(string fn)
|
||||||
{
|
{
|
||||||
@ -39,7 +39,7 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
catch (Exception) { }
|
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)
|
lock (ThreadLock)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
@ -23,18 +23,16 @@ namespace IW4MAdmin.Application
|
|||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
|
AppDomain.CurrentDomain.SetData("DataDirectory", OperatingDirectory);
|
||||||
//System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.BelowNormal;
|
|
||||||
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
Console.ForegroundColor = ConsoleColor.Gray;
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
|
|
||||||
Version = Assembly.GetExecutingAssembly().GetName().Version.Major + Assembly.GetExecutingAssembly().GetName().Version.Minor / 10.0f;
|
Version = Utilities.GetVersionAsDouble();
|
||||||
Version = Math.Round(Version, 2);
|
|
||||||
|
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
Console.WriteLine(" IW4M ADMIN");
|
Console.WriteLine(" IW4M ADMIN");
|
||||||
Console.WriteLine(" by RaidMax ");
|
Console.WriteLine(" by RaidMax ");
|
||||||
Console.WriteLine($" Version {Version.ToString("0.0")}");
|
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
|
||||||
Console.WriteLine("=====================================================");
|
Console.WriteLine("=====================================================");
|
||||||
|
|
||||||
Index loc = null;
|
Index loc = null;
|
||||||
@ -48,8 +46,7 @@ namespace IW4MAdmin.Application
|
|||||||
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
|
Localization.Configure.Initialize(ServerManager.GetApplicationSettings().Configuration()?.CustomLocale);
|
||||||
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
|
|
||||||
using (var db = new DatabaseContext(ServerManager.GetApplicationSettings().Configuration()?.ConnectionString))
|
ServerManager.Logger.WriteInfo($"Version is {Version}");
|
||||||
new ContextSeed(db).Seed().Wait();
|
|
||||||
|
|
||||||
var api = API.Master.Endpoint.Get();
|
var api = API.Master.Endpoint.Get();
|
||||||
|
|
||||||
@ -107,10 +104,10 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
ServerManager.Init().Wait();
|
ServerManager.Init().Wait();
|
||||||
|
|
||||||
var consoleTask = Task.Run(() =>
|
var consoleTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
String userInput;
|
String userInput;
|
||||||
Player Origin = ServerManager.GetClientService().Get(1).Result.AsPlayer();
|
Player Origin = Utilities.IW4MAdminClient;
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@ -137,7 +134,7 @@ namespace IW4MAdmin.Application
|
|||||||
};
|
};
|
||||||
|
|
||||||
ServerManager.GetEventHandler().AddEvent(E);
|
ServerManager.GetEventHandler().AddEvent(E);
|
||||||
E.OnProcessed.Wait(5000);
|
await E.WaitAsync(30 * 1000);
|
||||||
}
|
}
|
||||||
Console.Write('>');
|
Console.Write('>');
|
||||||
|
|
||||||
@ -164,7 +161,7 @@ namespace IW4MAdmin.Application
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnShutdownComplete.Reset();
|
OnShutdownComplete.Reset();
|
||||||
ServerManager.Start().Wait();
|
ServerManager.Start();
|
||||||
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
ServerManager.Logger.WriteVerbose(loc["MANAGER_SHUTDOWN_SUCCESS"]);
|
||||||
OnShutdownComplete.Set();
|
OnShutdownComplete.Set();
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Text;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
@ -12,13 +13,10 @@ using SharedLibraryCore.Helpers;
|
|||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using SharedLibraryCore.Services;
|
using SharedLibraryCore.Services;
|
||||||
using IW4MAdmin.Application.API;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using WebfrontCore;
|
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using Newtonsoft.Json;
|
using SharedLibraryCore.Database;
|
||||||
using Newtonsoft.Json.Linq;
|
using SharedLibraryCore.Events;
|
||||||
using System.Text;
|
|
||||||
using IW4MAdmin.Application.API.Master;
|
using IW4MAdmin.Application.API.Master;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application
|
namespace IW4MAdmin.Application
|
||||||
@ -30,20 +28,26 @@ namespace IW4MAdmin.Application
|
|||||||
public Dictionary<int, Player> PrivilegedClients { get; set; }
|
public Dictionary<int, Player> PrivilegedClients { get; set; }
|
||||||
public ILogger Logger { get; private set; }
|
public ILogger Logger { get; private set; }
|
||||||
public bool Running { 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 DateTime StartTime { get; private set; }
|
||||||
|
public string Version => Assembly.GetEntryAssembly().GetName().Version.ToString();
|
||||||
|
|
||||||
static ApplicationManager Instance;
|
static ApplicationManager Instance;
|
||||||
List<AsyncStatus> TaskStatuses;
|
readonly List<AsyncStatus> TaskStatuses;
|
||||||
List<Command> Commands;
|
List<Command> Commands;
|
||||||
List<MessageToken> MessageTokens;
|
readonly List<MessageToken> MessageTokens;
|
||||||
ClientService ClientSvc;
|
ClientService ClientSvc;
|
||||||
AliasService AliasSvc;
|
readonly AliasService AliasSvc;
|
||||||
PenaltyService PenaltySvc;
|
readonly PenaltyService PenaltySvc;
|
||||||
BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
public BaseConfigurationHandler<ApplicationConfiguration> ConfigHandler;
|
||||||
EventApi Api;
|
|
||||||
GameEventHandler Handler;
|
GameEventHandler Handler;
|
||||||
ManualResetEventSlim OnEvent;
|
ManualResetEventSlim OnQuit;
|
||||||
|
readonly IPageList PageList;
|
||||||
|
readonly SemaphoreSlim ProcessingEvent = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
private ApplicationManager()
|
private ApplicationManager()
|
||||||
{
|
{
|
||||||
@ -56,11 +60,141 @@ namespace IW4MAdmin.Application
|
|||||||
AliasSvc = new AliasService();
|
AliasSvc = new AliasService();
|
||||||
PenaltySvc = new PenaltyService();
|
PenaltySvc = new PenaltyService();
|
||||||
PrivilegedClients = new Dictionary<int, Player>();
|
PrivilegedClients = new Dictionary<int, Player>();
|
||||||
Api = new EventApi();
|
|
||||||
ServerEventOccurred += Api.OnServerEvent;
|
|
||||||
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
ConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||||
StartTime = DateTime.UtcNow;
|
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()
|
public IList<Server> GetServers()
|
||||||
@ -78,16 +212,39 @@ namespace IW4MAdmin.Application
|
|||||||
return Instance ?? (Instance = new ApplicationManager());
|
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)
|
while (Running)
|
||||||
{
|
{
|
||||||
taskList.Clear();
|
// select the server ids that have completed the update task
|
||||||
foreach (var server in Servers)
|
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
|
try
|
||||||
{
|
{
|
||||||
@ -97,49 +254,25 @@ namespace IW4MAdmin.Application
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.WriteWarning($"Failed to update status for {server}");
|
Logger.WriteWarning($"Failed to update status for {server}");
|
||||||
Logger.WriteDebug($"Exception: {e.Message}");
|
Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
Logger.WriteDebug($"StackTrace: {e.StackTrace}");
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#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.GetMaxThreads(out int workerThreads, out int n);
|
||||||
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
ThreadPool.GetAvailableThreads(out int availableThreads, out int m);
|
||||||
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
Logger.WriteDebug($"There are {workerThreads - availableThreads} active threading tasks");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
await Task.WhenAll(taskList.ToArray());
|
|
||||||
|
|
||||||
GameEvent sensitiveEvent;
|
|
||||||
while ((sensitiveEvent = Handler.GetNextSensitiveEvent()) != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await sensitiveEvent.Owner.ExecuteEvent(sensitiveEvent);
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Logger.WriteDebug($"Processed Sensitive Event {sensitiveEvent.Type}");
|
await Task.Delay(10000);
|
||||||
|
#else
|
||||||
|
await Task.Delay(ConfigHandler.Configuration().RConPollRate);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (NetworkException e)
|
// trigger the event processing loop to end
|
||||||
{
|
SetHasEvent();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Init()
|
public async Task Init()
|
||||||
@ -147,6 +280,12 @@ namespace IW4MAdmin.Application
|
|||||||
Running = true;
|
Running = true;
|
||||||
|
|
||||||
#region DATABASE
|
#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))
|
var ipList = (await ClientSvc.Find(c => c.Level > Player.Permission.Trusted))
|
||||||
.Select(c => new
|
.Select(c => new
|
||||||
{
|
{
|
||||||
@ -218,7 +357,7 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
if (string.IsNullOrEmpty(config.WebfrontBindUrl))
|
||||||
{
|
{
|
||||||
config.WebfrontBindUrl = "http://127.0.0.1:1624";
|
config.WebfrontBindUrl = "http://0.0.0.0:1624";
|
||||||
await ConfigHandler.Save();
|
await ConfigHandler.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,6 +427,8 @@ namespace IW4MAdmin.Application
|
|||||||
Commands.Add(new CKillServer());
|
Commands.Add(new CKillServer());
|
||||||
Commands.Add(new CSetPassword());
|
Commands.Add(new CSetPassword());
|
||||||
Commands.Add(new CPing());
|
Commands.Add(new CPing());
|
||||||
|
Commands.Add(new CSetGravatar());
|
||||||
|
Commands.Add(new CNextMap());
|
||||||
|
|
||||||
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
foreach (Command C in SharedLibraryCore.Plugins.PluginImporter.ActiveCommands)
|
||||||
Commands.Add(C);
|
Commands.Add(C);
|
||||||
@ -310,7 +451,15 @@ namespace IW4MAdmin.Application
|
|||||||
|
|
||||||
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
|
Logger.WriteVerbose($"{Utilities.CurrentLocalization.LocalizationIndex["MANAGER_MONITORING_TEXT"]} {ServerInstance.Hostname}");
|
||||||
// add the start event for this server
|
// 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)
|
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
|
// 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
|
var _ = Task.Run(() => SendHeartbeat(new HeartbeatState()));
|
||||||
#if !DEBUG
|
_ = Task.Run(() => UpdateServerStates());
|
||||||
// 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;
|
|
||||||
|
|
||||||
while (Running)
|
while (Running)
|
||||||
{
|
{
|
||||||
// wait for new event to be added
|
OnQuit.Wait();
|
||||||
OnEvent.Wait();
|
OnQuit.Reset();
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
#if !DEBUG
|
|
||||||
foreach (var S in _servers)
|
|
||||||
await S.Broadcast("^1" + Utilities.CurrentLocalization.LocalizationIndex["BROADCAST_OFFLINE"]);
|
|
||||||
#endif
|
|
||||||
_servers.Clear();
|
_servers.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
Running = false;
|
Running = false;
|
||||||
|
|
||||||
// trigger the event processing loop to end
|
|
||||||
SetHasEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger GetLogger()
|
public ILogger GetLogger()
|
||||||
@ -487,28 +575,23 @@ namespace IW4MAdmin.Application
|
|||||||
return MessageTokens;
|
return MessageTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IList<Player> GetActiveClients()
|
public IList<Player> GetActiveClients() => _servers.SelectMany(s => s.Players).Where(p => p != null).ToList();
|
||||||
{
|
|
||||||
var ActiveClients = new List<Player>();
|
|
||||||
|
|
||||||
foreach (var server in _servers)
|
|
||||||
ActiveClients.AddRange(server.Players.Where(p => p != null));
|
|
||||||
|
|
||||||
return ActiveClients;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientService GetClientService() => ClientSvc;
|
public ClientService GetClientService() => ClientSvc;
|
||||||
public AliasService GetAliasService() => AliasSvc;
|
public AliasService GetAliasService() => AliasSvc;
|
||||||
public PenaltyService GetPenaltyService() => PenaltySvc;
|
public PenaltyService GetPenaltyService() => PenaltySvc;
|
||||||
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
|
public IConfigurationHandler<ApplicationConfiguration> GetApplicationSettings() => ConfigHandler;
|
||||||
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
|
public IDictionary<int, Player> GetPrivilegedClients() => PrivilegedClients;
|
||||||
public IEventApi GetEventApi() => Api;
|
|
||||||
public bool ShutdownRequested() => !Running;
|
public bool ShutdownRequested() => !Running;
|
||||||
public IEventHandler GetEventHandler() => Handler;
|
public IEventHandler GetEventHandler() => Handler;
|
||||||
|
|
||||||
public void SetHasEvent()
|
public void SetHasEvent()
|
||||||
{
|
{
|
||||||
OnEvent.Set();
|
OnQuit.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IList<Assembly> GetPluginAssemblies() => SharedLibraryCore.Plugins.PluginImporter.PluginAssemblies;
|
||||||
|
|
||||||
|
public IPageList GetPageList() => PageList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
26
Application/PageList.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
|
@ -1,14 +1,13 @@
|
|||||||
using Application.RconParsers;
|
using SharedLibraryCore.RCon;
|
||||||
using SharedLibraryCore.RCon;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
class IW3RConParser : IW4RConParser
|
class IW3RConParser : IW4RConParser
|
||||||
{
|
{
|
||||||
private static CommandPrefix Prefixes = new CommandPrefix()
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
{
|
||||||
Tell = "tell {0} {1}",
|
Tell = "tell {0} {1}",
|
||||||
Say = "say {0}",
|
Say = "say {0}",
|
||||||
|
@ -10,11 +10,11 @@ using SharedLibraryCore;
|
|||||||
using SharedLibraryCore.RCon;
|
using SharedLibraryCore.RCon;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
class IW4RConParser : IRConParser
|
class IW4RConParser : IRConParser
|
||||||
{
|
{
|
||||||
private static CommandPrefix Prefixes = new CommandPrefix()
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
{
|
||||||
Tell = "tellraw {0} {1}",
|
Tell = "tellraw {0} {1}",
|
||||||
Say = "sayraw {0}",
|
Say = "sayraw {0}",
|
||||||
@ -23,7 +23,7 @@ namespace Application.RconParsers
|
|||||||
TempBan = "tempbanclient {0} \"{1}\""
|
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)
|
public async Task<string[]> ExecuteCommandAsync(Connection connection, string command)
|
||||||
{
|
{
|
||||||
@ -111,17 +111,12 @@ namespace Application.RconParsers
|
|||||||
Name = name,
|
Name = name,
|
||||||
NetworkId = networkId,
|
NetworkId = networkId,
|
||||||
ClientNumber = clientNumber,
|
ClientNumber = clientNumber,
|
||||||
IPAddress = ip,
|
IPAddress = ip == 0 ? int.MinValue : ip,
|
||||||
Ping = ping,
|
Ping = ping,
|
||||||
Score = score,
|
Score = score,
|
||||||
IsBot = ip == 0
|
IsBot = ip == 0,
|
||||||
|
State = Player.ClientState.Connecting
|
||||||
};
|
};
|
||||||
|
|
||||||
if (P.IsBot)
|
|
||||||
{
|
|
||||||
P.IPAddress = P.ClientNumber + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusPlayers.Add(P);
|
StatusPlayers.Add(P);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,19 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using SharedLibraryCore.RCon;
|
using SharedLibraryCore.RCon;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
using System.Text;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
public class IW5MRConParser : IRConParser
|
public class IW5MRConParser : IRConParser
|
||||||
{
|
{
|
||||||
private static CommandPrefix Prefixes = new CommandPrefix()
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
{
|
||||||
Tell = "tell {0} {1}",
|
Tell = "tell {0} {1}",
|
||||||
Say = "say {0}",
|
Say = "say {0}",
|
||||||
@ -155,7 +153,8 @@ namespace Application.RconParsers
|
|||||||
IPAddress = ipAddress,
|
IPAddress = ipAddress,
|
||||||
Ping = Ping,
|
Ping = Ping,
|
||||||
Score = score,
|
Score = score,
|
||||||
IsBot = false
|
IsBot = false,
|
||||||
|
State = Player.ClientState.Connecting
|
||||||
};
|
};
|
||||||
|
|
||||||
StatusPlayers.Add(p);
|
StatusPlayers.Add(p);
|
||||||
|
@ -12,47 +12,11 @@ using System.Text;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
namespace Application.RconParsers
|
namespace IW4MAdmin.Application.RconParsers
|
||||||
{
|
{
|
||||||
public class T6MRConParser : IRConParser
|
public class T6MRConParser : IRConParser
|
||||||
{
|
{
|
||||||
class T6MResponse
|
private static readonly CommandPrefix Prefixes = new CommandPrefix()
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
Tell = "tell {0} {1}",
|
Tell = "tell {0} {1}",
|
||||||
Say = "say {0}",
|
Say = "say {0}",
|
||||||
@ -73,7 +37,6 @@ namespace Application.RconParsers
|
|||||||
{
|
{
|
||||||
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
|
string[] LineSplit = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, $"get {dvarName}");
|
||||||
|
|
||||||
|
|
||||||
if (LineSplit.Length < 2)
|
if (LineSplit.Length < 2)
|
||||||
{
|
{
|
||||||
var e = new DvarException($"DVAR \"{dvarName}\" does not exist");
|
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");
|
string[] response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, "status");
|
||||||
return ClientsFromStatus(response);
|
return ClientsFromStatus(response);
|
||||||
|
|
||||||
//return ClientsFromResponse(connection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
public async Task<bool> SetDvarAsync(Connection connection, string dvarName, object dvarValue)
|
||||||
@ -114,41 +75,6 @@ namespace Application.RconParsers
|
|||||||
return true;
|
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)
|
private List<Player> ClientsFromStatus(string[] status)
|
||||||
{
|
{
|
||||||
List<Player> StatusPlayers = new List<Player>();
|
List<Player> StatusPlayers = new List<Player>();
|
||||||
@ -174,9 +100,6 @@ namespace Application.RconParsers
|
|||||||
#endif
|
#endif
|
||||||
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
int ipAddress = regex.Value.Split(':')[0].ConvertToIP();
|
||||||
regex = Regex.Match(responseLine, @"[0-9]{1,2}\s+[0-9]+\s+");
|
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()
|
var p = new Player()
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
@ -184,7 +107,8 @@ namespace Application.RconParsers
|
|||||||
ClientNumber = clientId,
|
ClientNumber = clientId,
|
||||||
IPAddress = ipAddress,
|
IPAddress = ipAddress,
|
||||||
Ping = Ping,
|
Ping = Ping,
|
||||||
Score = score,
|
Score = 0,
|
||||||
|
State = Player.ClientState.Connecting,
|
||||||
IsBot = networkId == 0
|
IsBot = networkId == 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,21 +14,22 @@ using SharedLibraryCore.Database.Models;
|
|||||||
using SharedLibraryCore.Dtos;
|
using SharedLibraryCore.Dtos;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Exceptions;
|
using SharedLibraryCore.Exceptions;
|
||||||
|
using SharedLibraryCore.Localization;
|
||||||
|
|
||||||
using Application.Misc;
|
using IW4MAdmin.Application.RconParsers;
|
||||||
using Application.RconParsers;
|
|
||||||
using IW4MAdmin.Application.EventParsers;
|
using IW4MAdmin.Application.EventParsers;
|
||||||
using IW4MAdmin.Application.IO;
|
using IW4MAdmin.Application.IO;
|
||||||
using SharedLibraryCore.Localization;
|
|
||||||
|
|
||||||
namespace IW4MAdmin
|
namespace IW4MAdmin
|
||||||
{
|
{
|
||||||
public class IW4MServer : Server
|
public class IW4MServer : Server
|
||||||
{
|
{
|
||||||
private static Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
private static readonly Index loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
private GameLogEvent LogEvent;
|
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()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
@ -49,24 +50,38 @@ namespace IW4MAdmin
|
|||||||
return id;
|
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)
|
override public async Task<bool> AddPlayer(Player polledPlayer)
|
||||||
{
|
{
|
||||||
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
|
if ((polledPlayer.Ping == 999 && !polledPlayer.IsBot) ||
|
||||||
polledPlayer.Ping < 1 ||
|
polledPlayer.Ping < 1 ||
|
||||||
polledPlayer.ClientNumber < 0)
|
polledPlayer.ClientNumber < 0)
|
||||||
{
|
{
|
||||||
//Logger.WriteDebug($"Skipping client not in connected state {P}");
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Players[polledPlayer.ClientNumber] != null &&
|
// set this when they are waiting for authentication
|
||||||
Players[polledPlayer.ClientNumber].NetworkId == polledPlayer.NetworkId)
|
if (Players[polledPlayer.ClientNumber] == null &&
|
||||||
|
polledPlayer.State == Player.ClientState.Connecting)
|
||||||
{
|
{
|
||||||
// update their ping & score
|
Players[polledPlayer.ClientNumber] = polledPlayer;
|
||||||
Players[polledPlayer.ClientNumber].Ping = polledPlayer.Ping;
|
return false;
|
||||||
Players[polledPlayer.ClientNumber].Score = polledPlayer.Score;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
if (polledPlayer.Name.Length < 3)
|
if (polledPlayer.Name.Length < 3)
|
||||||
{
|
{
|
||||||
@ -76,7 +91,7 @@ namespace IW4MAdmin
|
|||||||
return false;
|
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");
|
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"]);
|
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
|
// we need to update their new ip and name to the virtual property
|
||||||
client.Name = polledPlayer.Name;
|
client.Name = polledPlayer.Name;
|
||||||
client.IPAddress = polledPlayer.IPAddress;
|
client.IPAddress = polledPlayer.IPAddress;
|
||||||
|
|
||||||
await Manager.GetClientService().Update(client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (existingAlias.Name == polledPlayer.Name)
|
else
|
||||||
{
|
{
|
||||||
client.CurrentAlias = existingAlias;
|
client.CurrentAlias = existingAlias;
|
||||||
client.CurrentAliasId = existingAlias.AliasId;
|
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();
|
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
|
// Do the player specific stuff
|
||||||
player.ClientNumber = polledPlayer.ClientNumber;
|
player.ClientNumber = polledPlayer.ClientNumber;
|
||||||
player.IsBot = polledPlayer.IsBot;
|
player.IsBot = polledPlayer.IsBot;
|
||||||
player.Score = polledPlayer.Score;
|
player.Score = polledPlayer.Score;
|
||||||
player.CurrentServer = this;
|
player.CurrentServer = this;
|
||||||
|
|
||||||
|
player.DelayedEvents = (Players[player.ClientNumber]?.DelayedEvents) ?? new Queue<GameEvent>();
|
||||||
Players[player.ClientNumber] = player;
|
Players[player.ClientNumber] = player;
|
||||||
|
|
||||||
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress);
|
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(player.AliasLinkId, player.IPAddress);
|
||||||
var currentBan = activePenalties.FirstOrDefault(b => b.Expires > DateTime.UtcNow);
|
var currentBan = activePenalties.FirstOrDefault(b => b.Expires > DateTime.UtcNow);
|
||||||
var currentAutoFlag = activePenalties.Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1)
|
var currentAutoFlag = activePenalties.Where(p => p.Type == Penalty.PenaltyType.Flag && p.PunisherId == 1)
|
||||||
|
.Where(p => p.Active)
|
||||||
.OrderByDescending(p => p.When)
|
.OrderByDescending(p => p.When)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
// remove their auto flag status after a week
|
// 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;
|
player.Level = Player.Permission.User;
|
||||||
}
|
}
|
||||||
|
#if DEBUG == false
|
||||||
if (currentBan != null)
|
if (currentBan != null)
|
||||||
{
|
{
|
||||||
Logger.WriteInfo($"Banned client {player} trying to connect...");
|
Logger.WriteInfo($"Banned client {player} trying to connect...");
|
||||||
var autoKickClient = (await Manager.GetClientService().Get(1)).AsPlayer();
|
var autoKickClient = Utilities.IW4MAdminClient;
|
||||||
autoKickClient.CurrentServer = this;
|
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);
|
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)
|
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
|
// they didn't fully connect so empty their slot
|
||||||
Players[player.ClientNumber] = null;
|
Players[player.ClientNumber] = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
player.State = Player.ClientState.Connected;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.WriteInfo($"Client {player} connecting...");
|
catch (Exception ex)
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (Exception E)
|
|
||||||
{
|
{
|
||||||
Manager.GetLogger().WriteError($"{loc["SERVER_ERROR_ADDPLAYER"]} {polledPlayer.Name}::{polledPlayer.NetworkId}");
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,166 +264,40 @@ namespace IW4MAdmin
|
|||||||
if (cNum >= 0 && Players[cNum] != null)
|
if (cNum >= 0 && Players[cNum] != null)
|
||||||
{
|
{
|
||||||
Player Leaving = Players[cNum];
|
Player Leaving = Players[cNum];
|
||||||
Logger.WriteInfo($"Client {Leaving} disconnecting...");
|
|
||||||
|
|
||||||
var e = new GameEvent(GameEvent.EventType.Disconnect, "", Leaving, null, this);
|
// occurs when the player disconnects via log before being authenticated by RCon
|
||||||
Manager.GetEventHandler().AddEvent(e);
|
if (Leaving.State != Player.ClientState.Connected)
|
||||||
|
{
|
||||||
|
Players[cNum] = null;
|
||||||
|
}
|
||||||
|
|
||||||
// wait until the disconnect event is complete
|
else
|
||||||
e.OnProcessed.Wait();
|
{
|
||||||
|
Logger.WriteInfo($"Client {Leaving} [{Leaving.State.ToString().ToLower()}] disconnecting...");
|
||||||
Leaving.TotalConnectionTime += (int)(DateTime.UtcNow - Leaving.ConnectionTime).TotalSeconds;
|
Leaving.State = Player.ClientState.Disconnecting;
|
||||||
|
Leaving.TotalConnectionTime += Leaving.ConnectionLength;
|
||||||
Leaving.LastConnection = DateTime.UtcNow;
|
Leaving.LastConnection = DateTime.UtcNow;
|
||||||
await Manager.GetClientService().Update(Leaving);
|
await Manager.GetClientService().Update(Leaving);
|
||||||
Players[cNum] = null;
|
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
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (Args[0].Length < 3 && cNum > -1 && cNum < MaxClients) // user specifying target by client num
|
|
||||||
{
|
|
||||||
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}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
E.Data = E.Data.Trim();
|
|
||||||
return C;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task ExecuteEvent(GameEvent E)
|
public override async Task ExecuteEvent(GameEvent E)
|
||||||
{
|
{
|
||||||
bool canExecuteCommand = true;
|
bool canExecuteCommand = true;
|
||||||
await ProcessEvent(E);
|
|
||||||
Manager.GetEventApi().OnServerEvent(this, E);
|
|
||||||
|
|
||||||
|
if (!await ProcessEvent(E))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Command C = null;
|
Command C = null;
|
||||||
if (E.Type == GameEvent.EventType.Command)
|
if (E.Type == GameEvent.EventType.Command)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
C = await ValidateCommand(E);
|
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (CommandException e)
|
catch (CommandException e)
|
||||||
@ -399,39 +311,23 @@ namespace IW4MAdmin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this allows us to catch exceptions but still run it parallel
|
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||||
async Task pluginHandlingAsync(Task onEvent, string pluginName)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await onEvent;
|
await plugin.OnEventAsync(E, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this happens if a plugin (login) wants to stop commands from executing
|
|
||||||
catch (AuthorizationException e)
|
catch (AuthorizationException e)
|
||||||
{
|
{
|
||||||
await E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
E.Origin.Tell($"{loc["COMMAND_NOTAUTHORIZED"]} - {e.Message}");
|
||||||
canExecuteCommand = false;
|
canExecuteCommand = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception Except)
|
catch (Exception Except)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{pluginName}]");
|
Logger.WriteError($"{loc["SERVER_PLUGIN_ERROR"]} [{plugin.Name}]");
|
||||||
Logger.WriteDebug(String.Format("Error Message: {0}", Except.Message));
|
Logger.WriteDebug(Except.GetExceptionInfo());
|
||||||
Logger.WriteDebug(String.Format("Error Trace: {0}", Except.StackTrace));
|
|
||||||
while (Except.InnerException != null)
|
|
||||||
{
|
|
||||||
Except = Except.InnerException;
|
|
||||||
Logger.WriteDebug($"Inner exception: {Except.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// hack: this prevents commands from getting executing that 'shouldn't' be
|
||||||
if (E.Type == GameEvent.EventType.Command &&
|
if (E.Type == GameEvent.EventType.Command &&
|
||||||
@ -439,7 +335,7 @@ namespace IW4MAdmin
|
|||||||
(canExecuteCommand ||
|
(canExecuteCommand ||
|
||||||
E.Origin?.Level == Player.Permission.Console))
|
E.Origin?.Level == Player.Permission.Console))
|
||||||
{
|
{
|
||||||
await (((Command)E.Extra).ExecuteAsync(E));
|
var _ = (((Command)E.Extra).ExecuteAsync(E));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,44 +344,122 @@ namespace IW4MAdmin
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="E"></param>
|
/// <param name="E"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
override protected async Task ProcessEvent(GameEvent E)
|
override protected async Task<bool> ProcessEvent(GameEvent E)
|
||||||
{
|
{
|
||||||
if (E.Type == GameEvent.EventType.Connect)
|
if (E.Type == GameEvent.EventType.Connect)
|
||||||
{
|
{
|
||||||
// this may be a fix for a hard to reproduce null exception error
|
E.Origin.State = Player.ClientState.Authenticated;
|
||||||
lock (ChatHistory)
|
// add them to the server
|
||||||
|
if (!await AddPlayer(E.Origin))
|
||||||
{
|
{
|
||||||
|
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()
|
ChatHistory.Add(new ChatInfo()
|
||||||
{
|
{
|
||||||
Name = E.Origin?.Name ?? "ERROR!",
|
Name = E.Origin?.Name ?? "ERROR!",
|
||||||
Message = "CONNECTED",
|
Message = "CONNECTED",
|
||||||
Time = DateTime.UtcNow
|
Time = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (E.Origin.Level > Player.Permission.Moderator)
|
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)
|
else if (E.Type == GameEvent.EventType.Join)
|
||||||
{
|
{
|
||||||
// special case for IW5 when connect is from the log
|
await OnPlayerJoined(E.Origin);
|
||||||
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 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)
|
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()
|
||||||
{
|
{
|
||||||
@ -493,6 +467,13 @@ namespace IW4MAdmin
|
|||||||
Message = "DISCONNECTED",
|
Message = "DISCONNECTED",
|
||||||
Time = DateTime.UtcNow
|
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)
|
if (E.Type == GameEvent.EventType.Broadcast)
|
||||||
{
|
{
|
||||||
// this is a little ugly but I don't want to change the abstract class
|
// 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))
|
while (ChatHistory.Count > Math.Ceiling((double)ClientNum / 2))
|
||||||
@ -577,60 +561,45 @@ namespace IW4MAdmin
|
|||||||
// so there will still be at least 1 client left
|
// so there will still be at least 1 client left
|
||||||
if (ClientNum < 2)
|
if (ClientNum < 2)
|
||||||
ChatHistory.Clear();
|
ChatHistory.Clear();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
async Task<int> PollPlayersAsync()
|
/// 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;
|
var now = DateTime.Now;
|
||||||
|
#endif
|
||||||
List<Player> CurrentPlayers = null;
|
var currentClients = GetPlayersAsList();
|
||||||
try
|
var polledClients = await this.GetStatusAsync();
|
||||||
{
|
|
||||||
CurrentPlayers = await this.GetStatusAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the server has lost connection
|
|
||||||
catch (NetworkException)
|
|
||||||
{
|
|
||||||
Throttled = true;
|
|
||||||
return ClientNum;
|
|
||||||
}
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
|
Logger.WriteInfo($"Polling players took {(DateTime.Now - now).TotalMilliseconds}ms");
|
||||||
#endif
|
#endif
|
||||||
Throttled = false;
|
Throttled = false;
|
||||||
|
|
||||||
var clients = GetPlayersAsList();
|
foreach (var client in polledClients)
|
||||||
foreach (var client in clients)
|
|
||||||
{
|
{
|
||||||
if (GameName == Game.IW5)
|
// todo: move out somehwere
|
||||||
{
|
var existingClient = Players[client.ClientNumber] ?? client;
|
||||||
if (!CurrentPlayers.Select(c => c.ClientNumber).Contains(client.ClientNumber))
|
existingClient.Ping = client.Ping;
|
||||||
await RemovePlayer(client.ClientNumber);
|
existingClient.Score = client.Score;
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
var disconnectingClients = currentClients.Except(polledClients);
|
||||||
{
|
var connectingClients = polledClients.Except(currentClients.Where(c => c.State == Player.ClientState.Connected));
|
||||||
if (!CurrentPlayers.Select(c => c.NetworkId).Contains(client.NetworkId))
|
|
||||||
await RemovePlayer(client.ClientNumber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < CurrentPlayers.Count; i++)
|
return new List<Player>[] { connectingClients.ToList(), disconnectingClients.ToList() };
|
||||||
{
|
|
||||||
// todo: wait til GUID is included in status to fix this
|
|
||||||
if (GameName != Game.IW5)
|
|
||||||
await AddPlayer(CurrentPlayers[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CurrentPlayers.Count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime start = DateTime.Now;
|
DateTime start = DateTime.Now;
|
||||||
DateTime playerCountStart = DateTime.Now;
|
DateTime playerCountStart = DateTime.Now;
|
||||||
DateTime lastCount = DateTime.Now;
|
DateTime lastCount = DateTime.Now;
|
||||||
DateTime tickTime = DateTime.Now;
|
|
||||||
|
|
||||||
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
override public async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||||
{
|
{
|
||||||
@ -638,20 +607,69 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
if (Manager.ShutdownRequested())
|
if (Manager.ShutdownRequested())
|
||||||
{
|
{
|
||||||
for (int i = 0; i < Players.Count; i++)
|
// todo: fix up disconnect
|
||||||
await RemovePlayer(i);
|
//for (int i = 0; i < Players.Count; i++)
|
||||||
|
// await RemovePlayer(i);
|
||||||
|
|
||||||
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
foreach (var plugin in SharedLibraryCore.Plugins.PluginImporter.ActivePlugins)
|
||||||
await plugin.OnUnloadAsync();
|
await plugin.OnUnloadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// only check every 2 minutes if the server doesn't seem to be responding
|
// only check every 2 minutes if the server doesn't seem to be responding
|
||||||
if ((DateTime.Now - LastPoll).TotalMinutes < 2 && ConnectionErrors >= 1)
|
/* if ((DateTime.Now - LastPoll).TotalMinutes < 0.5 && ConnectionErrors >= 1)
|
||||||
return true;
|
return true;*/
|
||||||
|
|
||||||
try
|
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)
|
if (ConnectionErrors > 0)
|
||||||
{
|
{
|
||||||
@ -708,7 +726,9 @@ namespace IW4MAdmin
|
|||||||
string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine);
|
string[] messages = this.ProcessMessageToken(Manager.GetMessageTokens(), BroadcastMessages[NextMessage]).Split(Environment.NewLine);
|
||||||
|
|
||||||
foreach (string message in messages)
|
foreach (string message in messages)
|
||||||
await Broadcast(message);
|
{
|
||||||
|
Broadcast(message);
|
||||||
|
}
|
||||||
|
|
||||||
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
|
NextMessage = NextMessage == (BroadcastMessages.Count - 1) ? 0 : NextMessage + 1;
|
||||||
start = DateTime.Now;
|
start = DateTime.Now;
|
||||||
@ -723,6 +743,7 @@ namespace IW4MAdmin
|
|||||||
if (e is NetworkException)
|
if (e is NetworkException)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
|
Logger.WriteError($"{loc["SERVER_ERROR_COMMUNICATION"]} {IP}:{Port}");
|
||||||
|
Logger.WriteDebug(e.GetExceptionInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -731,15 +752,17 @@ namespace IW4MAdmin
|
|||||||
catch (Exception E)
|
catch (Exception E)
|
||||||
{
|
{
|
||||||
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}");
|
Logger.WriteError($"{loc["SERVER_ERROR_EXCEPTION"]} {IP}:{Port}");
|
||||||
Logger.WriteDebug("Error Message: " + E.Message);
|
Logger.WriteDebug(E.GetExceptionInfo());
|
||||||
Logger.WriteDebug("Error Trace: " + E.StackTrace);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Initialize()
|
||||||
{
|
{
|
||||||
RconParser = ServerConfig.UseT6MParser ? (IRConParser)new T6MRConParser() : new IW3RConParser();
|
RconParser = ServerConfig.UseT6MParser ?
|
||||||
|
(IRConParser)new T6MRConParser() :
|
||||||
|
new IW3RConParser();
|
||||||
|
|
||||||
if (ServerConfig.UseIW5MParser)
|
if (ServerConfig.UseIW5MParser)
|
||||||
RconParser = new IW5MRConParser();
|
RconParser = new IW5MRConParser();
|
||||||
|
|
||||||
@ -806,9 +829,6 @@ namespace IW4MAdmin
|
|||||||
this.MaxClients = maxplayers;
|
this.MaxClients = maxplayers;
|
||||||
this.FSGame = game;
|
this.FSGame = game;
|
||||||
this.Gametype = gametype;
|
this.Gametype = gametype;
|
||||||
|
|
||||||
//wait this.SetDvarAsync("sv_kickbantime", 60);
|
|
||||||
|
|
||||||
if (logsync.Value == 0 || logfile.Value == string.Empty)
|
if (logsync.Value == 0 || logfile.Value == string.Empty)
|
||||||
{
|
{
|
||||||
// this DVAR isn't set until the a map is loaded
|
// this DVAR isn't set until the a map is loaded
|
||||||
@ -823,41 +843,48 @@ namespace IW4MAdmin
|
|||||||
CustomCallback = await ScriptLoaded();
|
CustomCallback = await ScriptLoaded();
|
||||||
string mainPath = EventParser.GetGameDir();
|
string mainPath = EventParser.GetGameDir();
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
basepath.Value = @"\\192.168.88.253\mw2";
|
// basepath.Value = @"D:\";
|
||||||
#endif
|
#endif
|
||||||
string logPath;
|
string logPath = string.Empty;
|
||||||
if (GameName == Game.IW5)
|
|
||||||
|
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;
|
logPath = ServerConfig.ManualLogPath;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
logPath = game == string.Empty ?
|
logPath = LogPath;
|
||||||
$"{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}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hopefully fix wine drive name mangling
|
// hopefully fix wine drive name mangling
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
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"]}");
|
Logger.WriteError($"{logPath} {loc["SERVER_ERROR_DNE"]}");
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
|
throw new ServerException($"{loc["SERVER_ERROR_LOG"]} {logPath}");
|
||||||
|
//#else
|
||||||
|
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogEvent = new GameLogEvent(this, logPath, logfile.Value);
|
LogEvent = new GameLogEventDetection(this, logPath, logfile.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.WriteInfo($"Log file is {logPath}");
|
Logger.WriteInfo($"Log file is {logPath}");
|
||||||
|
|
||||||
|
_ = Task.Run(() => LogEvent.PollForChanges());
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
await Broadcast(loc["BROADCAST_ONLINE"]);
|
Broadcast(loc["BROADCAST_ONLINE"]);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -880,13 +907,12 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
if (Target.Warnings >= 4)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Target.Warnings++;
|
String message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
|
||||||
String Message = $"^1{loc["SERVER_WARNING"]} ^7[^3{Target.Warnings}^7]: ^3{Target.Name}^7, {Reason}";
|
Target.CurrentServer.Broadcast(message);
|
||||||
await Target.CurrentServer.Broadcast(Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Penalty newPenalty = new Penalty()
|
Penalty newPenalty = new Penalty()
|
||||||
@ -904,7 +930,7 @@ namespace IW4MAdmin
|
|||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
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
|
// ensure player gets kicked if command not performed on them in game
|
||||||
if (Target.ClientNumber < 0)
|
if (Target.ClientNumber < 0)
|
||||||
@ -945,7 +971,7 @@ namespace IW4MAdmin
|
|||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
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
|
// ensure player gets banned if command not performed on them in game
|
||||||
if (Target.ClientNumber < 0)
|
if (Target.ClientNumber < 0)
|
||||||
@ -984,7 +1010,7 @@ namespace IW4MAdmin
|
|||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
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
|
// ensure player gets banned if command not performed on them in game
|
||||||
if (Target.ClientNumber < 0)
|
if (Target.ClientNumber < 0)
|
||||||
@ -1007,6 +1033,7 @@ namespace IW4MAdmin
|
|||||||
{
|
{
|
||||||
// this is set only because they're still in the server.
|
// this is set only because they're still in the server.
|
||||||
Target.Level = Player.Permission.Banned;
|
Target.Level = Player.Permission.Banned;
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
string formattedString = String.Format(RconParser.GetCommandPrefixes().Kick, Target.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{Message} ^7({loc["SERVER_BAN_APPEAL"]} {Website})^7");
|
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);
|
await Target.CurrentServer.ExecuteCommandAsync(formattedString);
|
||||||
@ -1024,7 +1051,8 @@ namespace IW4MAdmin
|
|||||||
Punisher = Origin,
|
Punisher = Origin,
|
||||||
Active = true,
|
Active = true,
|
||||||
When = DateTime.UtcNow,
|
When = DateTime.UtcNow,
|
||||||
Link = Target.AliasLink
|
Link = Target.AliasLink,
|
||||||
|
AutomatedOffense = Origin.AdministeredPenalties.FirstOrDefault()?.AutomatedOffense
|
||||||
};
|
};
|
||||||
|
|
||||||
await Manager.GetPenaltyService().Create(newPenalty);
|
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("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("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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
214
DiscordWebhook/DiscordWebhook.py
Normal file
214
DiscordWebhook/DiscordWebhook.py
Normal 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()
|
95
DiscordWebhook/DiscordWebhook.pyproj
Normal file
95
DiscordWebhook/DiscordWebhook.pyproj
Normal 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>
|
6
DiscordWebhook/config.json
Normal file
6
DiscordWebhook/config.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"IW4MAdminUrl": "",
|
||||||
|
"DiscordWebhookNotificationUrl": "",
|
||||||
|
"DiscordWebhookInformationUrl": "",
|
||||||
|
"NotifyRoleIds": []
|
||||||
|
}
|
7
DiscordWebhook/requirements.txt
Normal file
7
DiscordWebhook/requirements.txt
Normal 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
|
99
GameLogServer/GameLogServer.pyproj
Normal file
99
GameLogServer/GameLogServer.pyproj
Normal 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>
|
9
GameLogServer/GameLogServer/__init__.py
Normal file
9
GameLogServer/GameLogServer/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
The flask application package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_restful import Api
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
api = Api(app)
|
75
GameLogServer/GameLogServer/log_reader.py
Normal file
75
GameLogServer/GameLogServer/log_reader.py
Normal 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()
|
17
GameLogServer/GameLogServer/log_resource.py
Normal file
17
GameLogServer/GameLogServer/log_resource.py
Normal 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
|
||||||
|
}
|
9
GameLogServer/GameLogServer/server.py
Normal file
9
GameLogServer/GameLogServer/server.py
Normal 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>')
|
12
GameLogServer/requirements.txt
Normal file
12
GameLogServer/requirements.txt
Normal 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
|
15
GameLogServer/runserver.py
Normal file
15
GameLogServer/runserver.py
Normal 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)
|
@ -10,6 +10,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
_commands.gsc = _commands.gsc
|
_commands.gsc = _commands.gsc
|
||||||
_customcallbacks.gsc = _customcallbacks.gsc
|
_customcallbacks.gsc = _customcallbacks.gsc
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
|
RunPublishPre.cmd = RunPublishPre.cmd
|
||||||
version.txt = version.txt
|
version.txt = version.txt
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
@ -31,7 +32,17 @@ Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Master", "Master\Master.pyp
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Plugins\Tests\Tests.csproj", "{B72DEBFB-9D48-4076-8FF5-1FD72A830845}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -298,6 +345,7 @@ Global
|
|||||||
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{D9F2ED28-6FA5-40CA-9912-E7A849147AB1} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
|
{B72DEBFB-9D48-4076-8FF5-1FD72A830845} = {26E8B310-269E-46D4-A612-24601F16065F}
|
||||||
{6C706CE5-A206-4E46-8712-F8C48D526091} = {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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
SolutionGuid = {84F8F8E0-1F73-41E0-BD8D-BB6676E2EE87}
|
||||||
|
@ -73,6 +73,9 @@
|
|||||||
<Compile Include="master\resources\null.py">
|
<Compile Include="master\resources\null.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="Master\resources\server.py">
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Include="master\resources\version.py">
|
<Compile Include="master\resources\version.py">
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
@ -110,6 +113,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="FolderProfile.pubxml" />
|
<None Include="FolderProfile.pubxml" />
|
||||||
<Content Include="master\config\master.json" />
|
<Content Include="master\config\master.json" />
|
||||||
|
<Content Include="master\templates\serverlist.html" />
|
||||||
<Content Include="requirements.txt" />
|
<Content Include="requirements.txt" />
|
||||||
<Content Include="master\templates\index.html" />
|
<Content Include="master\templates\index.html" />
|
||||||
<Content Include="master\templates\layout.html" />
|
<Content Include="master\templates\layout.html" />
|
||||||
|
@ -15,9 +15,9 @@ class Base():
|
|||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
func=self._remove_staleinstances,
|
func=self._remove_staleinstances,
|
||||||
trigger=IntervalTrigger(seconds=120),
|
trigger=IntervalTrigger(seconds=60),
|
||||||
id='stale_instance_remover',
|
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
|
replace_existing=True
|
||||||
)
|
)
|
||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
@ -41,7 +41,7 @@ class Base():
|
|||||||
|
|
||||||
def _remove_staleinstances(self):
|
def _remove_staleinstances(self):
|
||||||
for key, value in list(self.instance_list.items()):
|
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))
|
print('[_remove_staleinstances] removing stale instance {id}'.format(id=key))
|
||||||
del self.instance_list[key]
|
del self.instance_list[key]
|
||||||
del self.token_list[key]
|
del self.token_list[key]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
class ServerModel(object):
|
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.id = id
|
||||||
self.port = port
|
self.port = port
|
||||||
self.game = game
|
self.game = game
|
||||||
@ -9,6 +9,7 @@ class ServerModel(object):
|
|||||||
self.maxclientnum = maxclientnum
|
self.maxclientnum = maxclientnum
|
||||||
self.map = map
|
self.map = map
|
||||||
self.gametype = gametype
|
self.gametype = gametype
|
||||||
|
self.ip = ip
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<ServerModel(id={id})>'.format(id=self.id)
|
return '<ServerModel(id={id})>'.format(id=self.id)
|
||||||
|
@ -8,10 +8,11 @@ import datetime
|
|||||||
class Authenticate(Resource):
|
class Authenticate(Resource):
|
||||||
def post(self):
|
def post(self):
|
||||||
instance_id = request.json['id']
|
instance_id = request.json['id']
|
||||||
if ctx.get_token(instance_id) is not False:
|
#todo: see why this is failing
|
||||||
return { 'message' : 'that id already has a token'}, 401
|
#if ctx.get_token(instance_id) is not False:
|
||||||
else:
|
# return { 'message' : 'that id already has a token'}, 401
|
||||||
expires = datetime.timedelta(days=1)
|
#else:
|
||||||
|
expires = datetime.timedelta(days=30)
|
||||||
token = create_access_token(instance_id, expires_delta=expires)
|
token = create_access_token(instance_id, expires_delta=expires)
|
||||||
ctx.add_token(instance_id, token)
|
ctx.add_token(instance_id, token)
|
||||||
return { 'access_token' : token }, 200
|
return { 'access_token' : token }, 200
|
||||||
|
@ -4,6 +4,7 @@ from flask_jwt_extended import jwt_required
|
|||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
from master.schema.instanceschema import InstanceSchema
|
from master.schema.instanceschema import InstanceSchema
|
||||||
from master import ctx
|
from master import ctx
|
||||||
|
import json
|
||||||
|
|
||||||
class Instance(Resource):
|
class Instance(Resource):
|
||||||
def get(self, id=None):
|
def get(self, id=None):
|
||||||
@ -21,6 +22,8 @@ class Instance(Resource):
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def put(self, id):
|
def put(self, id):
|
||||||
try:
|
try:
|
||||||
|
for server in request.json['servers']:
|
||||||
|
server['ip'] = request.remote_addr
|
||||||
instance = InstanceSchema().load(request.json)
|
instance = InstanceSchema().load(request.json)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
return {'message' : err.messages }, 400
|
return {'message' : err.messages }, 400
|
||||||
@ -30,6 +33,8 @@ class Instance(Resource):
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def post(self):
|
def post(self):
|
||||||
try:
|
try:
|
||||||
|
for server in request.json['servers']:
|
||||||
|
server['ip'] = request.remote_addr
|
||||||
instance = InstanceSchema().load(request.json)
|
instance = InstanceSchema().load(request.json)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
return {'message' : err.messages }, 400
|
return {'message' : err.messages }, 400
|
||||||
|
6
Master/master/resources/server.py
Normal file
6
Master/master/resources/server.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from flask_restful import Resource
|
||||||
|
|
||||||
|
class Server(Resource):
|
||||||
|
"""description of class"""
|
||||||
|
|
||||||
|
|
@ -6,6 +6,7 @@ from master.resources.authenticate import Authenticate
|
|||||||
from master.resources.version import Version
|
from master.resources.version import Version
|
||||||
from master.resources.history_graph import HistoryGraph
|
from master.resources.history_graph import HistoryGraph
|
||||||
from master.resources.localization import Localization
|
from master.resources.localization import Localization
|
||||||
|
from master.resources.server import Server
|
||||||
|
|
||||||
api.add_resource(Null, '/null')
|
api.add_resource(Null, '/null')
|
||||||
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
|
api.add_resource(Instance, '/instance/', '/instance/<string:id>')
|
||||||
@ -13,3 +14,4 @@ api.add_resource(Version, '/version')
|
|||||||
api.add_resource(Authenticate, '/authenticate')
|
api.add_resource(Authenticate, '/authenticate')
|
||||||
api.add_resource(HistoryGraph, '/history/', '/history/<int:history_count>')
|
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')
|
@ -6,17 +6,20 @@ class ServerSchema(Schema):
|
|||||||
required=True,
|
required=True,
|
||||||
validate=validate.Range(1, 2147483647, 'invalid id')
|
validate=validate.Range(1, 2147483647, 'invalid id')
|
||||||
)
|
)
|
||||||
|
ip = fields.Str(
|
||||||
|
required=True
|
||||||
|
)
|
||||||
port = fields.Int(
|
port = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Range(1, 665535, 'invalid port')
|
validate=validate.Range(1, 65535, 'invalid port')
|
||||||
)
|
)
|
||||||
game = fields.String(
|
game = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 8, 'invalid game name')
|
validate=validate.Length(1, 5, 'invalid game name')
|
||||||
)
|
)
|
||||||
hostname = fields.String(
|
hostname = fields.String(
|
||||||
required=True,
|
required=True,
|
||||||
validate=validate.Length(1, 48, 'invalid hostname')
|
validate=validate.Length(1, 64, 'invalid hostname')
|
||||||
)
|
)
|
||||||
clientnum = fields.Int(
|
clientnum = fields.Int(
|
||||||
required=True,
|
required=True,
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||||
crossorigin="anonymous"></script>
|
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 %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
113
Master/master/templates/serverlist.html
Normal file
113
Master/master/templates/serverlist.html
Normal 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">×</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 %}
|
@ -4,8 +4,9 @@ Routes and views for the flask application.
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from master import app
|
from master import app, ctx
|
||||||
from master.resources.history_graph import HistoryGraph
|
from master.resources.history_graph import HistoryGraph
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
@ -19,3 +20,16 @@ def home():
|
|||||||
client_count = _history_graph[0]['client_count'],
|
client_count = _history_graph[0]['client_count'],
|
||||||
server_count = _history_graph[0]['server_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
|
||||||
|
)
|
||||||
|
14
Plugins/IW4ScriptCommands/CommandInfo.cs
Normal file
14
Plugins/IW4ScriptCommands/CommandInfo.cs
Normal 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)}";
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,197 @@
|
|||||||
using SharedLibraryCore;
|
//using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Objects;
|
//using SharedLibraryCore.Objects;
|
||||||
using System.Threading.Tasks;
|
//using System;
|
||||||
|
//using System.Collections.Generic;
|
||||||
|
//using System.Linq;
|
||||||
|
//using System.Text;
|
||||||
|
//using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace IW4ScriptCommands.Commands
|
//namespace IW4ScriptCommands.Commands
|
||||||
{
|
//{
|
||||||
class Balance : Command
|
// class Balance : Command
|
||||||
{
|
// {
|
||||||
public Balance() : base("balance", "balance teams", "bal", Player.Permission.Trusted, false, null)
|
// 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)
|
// public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
// {
|
||||||
await E.Owner.ExecuteCommandAsync("sv_iw4madmin_command balance");
|
// string teamsString = (await E.Owner.GetDvarAsync<string>("sv_iw4madmin_teams")).Value;
|
||||||
await E.Origin.Tell("Balance command sent");
|
|
||||||
}
|
// 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");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -13,10 +13,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||||
|
<ProjectReference Include="..\Stats\Stats.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
<PackageReference Update="Microsoft.NETCore.App"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -15,7 +15,35 @@ namespace IW4ScriptCommands
|
|||||||
|
|
||||||
public string Author => "RaidMax";
|
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;
|
public Task OnLoadAsync(IManager manager) => Task.CompletedTask;
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
||||||
Required = true
|
Required = true
|
||||||
}
|
}
|
||||||
}){ }
|
})
|
||||||
|
{ }
|
||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
@ -25,12 +26,12 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
|||||||
if (hashedPassword[0] == client.Password)
|
if (hashedPassword[0] == client.Password)
|
||||||
{
|
{
|
||||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
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
|
else
|
||||||
{
|
{
|
||||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.Login</PackageId>
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
<PackageReference Update="Microsoft.NETCore.App"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
|
@ -16,9 +16,9 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
{
|
{
|
||||||
OffensiveWords = new List<string>()
|
OffensiveWords = new List<string>()
|
||||||
{
|
{
|
||||||
"nigger",
|
@"\s*n+.*i+.*g+.*e+.*r+\s*",
|
||||||
"nigga",
|
@"\s*n+.*i+.*g+.*a+\s*",
|
||||||
"fuck"
|
@"\s*f+u+.*c+.*k+.*\s*"
|
||||||
};
|
};
|
||||||
|
|
||||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using SharedLibraryCore.Configuration;
|
using SharedLibraryCore.Configuration;
|
||||||
@ -21,10 +22,10 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
ConcurrentDictionary<int, Tracking> ProfanityCounts;
|
||||||
IManager Manager;
|
IManager Manager;
|
||||||
|
|
||||||
public async Task OnEventAsync(GameEvent E, Server S)
|
public Task OnEventAsync(GameEvent E, Server S)
|
||||||
{
|
{
|
||||||
if (!Settings.Configuration().EnableProfanityDeterment)
|
if (!Settings.Configuration().EnableProfanityDeterment)
|
||||||
return;
|
return Task.CompletedTask; ;
|
||||||
|
|
||||||
if (E.Type == GameEvent.EventType.Connect)
|
if (E.Type == GameEvent.EventType.Connect)
|
||||||
{
|
{
|
||||||
@ -36,9 +37,18 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
var objectionalWords = Settings.Configuration().OffensiveWords;
|
var objectionalWords = Settings.Configuration().OffensiveWords;
|
||||||
bool containsObjectionalWord = objectionalWords.FirstOrDefault(w => E.Origin.Name.ToLower().Contains(w)) != null;
|
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)
|
if (containsObjectionalWord)
|
||||||
{
|
{
|
||||||
await E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
|
E.Origin.Kick(Settings.Configuration().ProfanityKickMessage, new Player()
|
||||||
{
|
{
|
||||||
ClientId = 1
|
ClientId = 1
|
||||||
});
|
});
|
||||||
@ -56,14 +66,25 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
if (E.Type == GameEvent.EventType.Say)
|
if (E.Type == GameEvent.EventType.Say)
|
||||||
{
|
{
|
||||||
var objectionalWords = Settings.Configuration().OffensiveWords;
|
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)
|
if (containsObjectionalWord)
|
||||||
{
|
{
|
||||||
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
|
var clientProfanity = ProfanityCounts[E.Origin.ClientId];
|
||||||
if (clientProfanity.Infringements >= Settings.Configuration().KickAfterInfringementCount)
|
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
|
ClientId = 1
|
||||||
});
|
});
|
||||||
@ -73,13 +94,14 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
|||||||
{
|
{
|
||||||
clientProfanity.Infringements++;
|
clientProfanity.Infringements++;
|
||||||
|
|
||||||
await clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
|
clientProfanity.Client.Warn(Settings.Configuration().ProfanityWarningMessage, new Player()
|
||||||
{
|
{
|
||||||
ClientId = 1
|
ClientId = 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnLoadAsync(IManager manager)
|
public async Task OnLoadAsync(IManager manager)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.ProfanityDeterment</PackageId>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
<PackageReference Update="Microsoft.NETCore.App"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
|
30
Plugins/ScriptPlugins/SharedGUIDKick.js
Normal file
30
Plugins/ScriptPlugins/SharedGUIDKick.js
Normal 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) {
|
||||||
|
}
|
||||||
|
};
|
66
Plugins/ScriptPlugins/VPNDetection.js
Normal file
66
Plugins/ScriptPlugins/VPNDetection.js
Normal 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) {
|
||||||
|
}
|
||||||
|
};
|
@ -19,16 +19,18 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
Strain
|
Strain
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public ChangeTracking<EFACSnapshot> Tracker { get; private set; }
|
||||||
|
|
||||||
int Kills;
|
int Kills;
|
||||||
int HitCount;
|
int HitCount;
|
||||||
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
Dictionary<IW4Info.HitLocation, int> HitLocationCount;
|
||||||
ChangeTracking Tracker;
|
|
||||||
double AngleDifferenceAverage;
|
double AngleDifferenceAverage;
|
||||||
EFClientStatistics ClientStats;
|
EFClientStatistics ClientStats;
|
||||||
DateTime LastHit;
|
DateTime LastHit;
|
||||||
long LastOffset;
|
long LastOffset;
|
||||||
ILogger Log;
|
ILogger Log;
|
||||||
Strain Strain;
|
Strain Strain;
|
||||||
|
readonly DateTime ConnectionTime = DateTime.UtcNow;
|
||||||
|
|
||||||
public Detection(ILogger log, EFClientStatistics clientStats)
|
public Detection(ILogger log, EFClientStatistics clientStats)
|
||||||
{
|
{
|
||||||
@ -38,7 +40,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
|
HitLocationCount.Add((IW4Info.HitLocation)loc, 0);
|
||||||
ClientStats = clientStats;
|
ClientStats = clientStats;
|
||||||
Strain = new Strain();
|
Strain = new Strain();
|
||||||
Tracker = new ChangeTracking();
|
Tracker = new ChangeTracking<EFACSnapshot>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -51,7 +53,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
if ((kill.DeathType != IW4Info.MeansOfDeath.MOD_PISTOL_BULLET &&
|
||||||
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
kill.DeathType != IW4Info.MeansOfDeath.MOD_RIFLE_BULLET &&
|
||||||
kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
kill.DeathType != IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
|
||||||
kill.HitLoc == IW4Info.HitLocation.none)
|
kill.HitLoc == IW4Info.HitLocation.none || kill.TimeOffset - LastOffset < 0)
|
||||||
return new DetectionPenaltyResult()
|
return new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
ClientPenalty = Penalty.PenaltyType.Any,
|
ClientPenalty = Penalty.PenaltyType.Any,
|
||||||
@ -83,7 +85,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
double newAverage = (previousAverage * (hitLoc.HitCount - 1) + realAgainstPredict) / hitLoc.HitCount;
|
||||||
hitLoc.HitOffsetAverage = (float)newAverage;
|
hitLoc.HitOffsetAverage = (float)newAverage;
|
||||||
|
|
||||||
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset &&
|
if (hitLoc.HitOffsetAverage > Thresholds.MaxOffset(hitLoc.HitCount) &&
|
||||||
hitLoc.HitCount > 100)
|
hitLoc.HitCount > 100)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("*** Reached Max Lifetime Average for Angle Difference ***");
|
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;
|
double sessAverage = (AngleDifferenceAverage * (HitCount - 1) + realAgainstPredict) / HitCount;
|
||||||
AngleDifferenceAverage = sessAverage;
|
AngleDifferenceAverage = sessAverage;
|
||||||
|
|
||||||
if (sessAverage > Thresholds.MaxOffset &&
|
if (sessAverage > Thresholds.MaxOffset(HitCount) &&
|
||||||
HitCount > 30)
|
HitCount > 30)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("*** Reached Max Session Average for Angle Difference ***");
|
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 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;
|
LastOffset = kill.TimeOffset;
|
||||||
|
|
||||||
if (currentStrain > ClientStats.MaxStrain)
|
if (currentStrain > ClientStats.MaxStrain)
|
||||||
@ -138,7 +139,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// flag
|
// flag
|
||||||
if (currentStrain > Thresholds.MaxStrainFlag)
|
if (currentStrain > Thresholds.MaxStrainFlag &&
|
||||||
|
HitCount >= 10)
|
||||||
{
|
{
|
||||||
result = new DetectionPenaltyResult()
|
result = new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
@ -150,7 +152,8 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ban
|
// ban
|
||||||
if (currentStrain > Thresholds.MaxStrainBan)
|
if (currentStrain > Thresholds.MaxStrainBan &&
|
||||||
|
HitCount >= 15)
|
||||||
{
|
{
|
||||||
result = new DetectionPenaltyResult()
|
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
|
// 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 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 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
|
// 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 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;
|
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)
|
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
||||||
{
|
{
|
||||||
// ban on headshot
|
// ban on headshot
|
||||||
if (currentHeadshotRatio > maxHeadshotLerpValueForFlag)
|
if (currentHeadshotRatio > maxHeadshotLerpValueForBan)
|
||||||
{
|
{
|
||||||
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
Log.WriteDebug("**Maximum Headshot Ratio Reached For Ban**");
|
||||||
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
Log.WriteDebug($"ClientId: {kill.AttackerId}");
|
||||||
@ -351,16 +354,34 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
#endregion
|
#endregion
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
Tracker.OnChange(new DetectionTracking(ClientStats, kill, Strain));
|
Tracker.OnChange(new EFACSnapshot()
|
||||||
|
|
||||||
if (result != null)
|
|
||||||
{
|
{
|
||||||
foreach (string change in Tracker.GetChanges())
|
When = kill.When,
|
||||||
{
|
ClientId = ClientStats.ClientId,
|
||||||
Log.WriteDebug(change);
|
SessionAngleOffset = AngleDifferenceAverage,
|
||||||
Log.WriteDebug("--------------SNAPSHOT END-----------");
|
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()
|
return result ?? new DetectionPenaltyResult()
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
using SharedLibraryCore.Objects;
|
using SharedLibraryCore.Objects;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
namespace IW4MAdmin.Plugins.Stats.Cheat
|
||||||
{
|
{
|
||||||
|
@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,13 +6,13 @@ using System.Text;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Cheat
|
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 double CurrentStrain;
|
||||||
private Vector3 LastAngle;
|
public double LastDistance { get; private set; }
|
||||||
private double LastDeltaTime;
|
public Vector3 LastAngle { get; private set; }
|
||||||
private double LastDistance;
|
public double LastDeltaTime { get; private set; }
|
||||||
|
|
||||||
public int TimesReachedMaxStrain { get; private set; }
|
public int TimesReachedMaxStrain { get; private set; }
|
||||||
|
|
||||||
@ -53,11 +53,6 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
return CurrentStrain;
|
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);
|
private double GetDecay(double deltaTime) => Math.Pow(StrainDecayBase, Math.Pow(2.0, deltaTime / 250.0) / 1000.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,8 +27,10 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
|
|||||||
public const int HighSampleMinKills = 100;
|
public const int HighSampleMinKills = 100;
|
||||||
public const double KillTimeThreshold = 0.2;
|
public const double KillTimeThreshold = 0.2;
|
||||||
|
|
||||||
public const double MaxStrainBan = 0.4;
|
public const double MaxStrainBan = 1.12;
|
||||||
public const double MaxOffset = 1.2;
|
|
||||||
|
//=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 const double MaxStrainFlag = 0.36;
|
||||||
|
|
||||||
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
public static double GetMarginOfError(int numKills) => 1.6455 / Math.Sqrt(numKills);
|
||||||
|
@ -63,12 +63,16 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
if (!E.Message.IsBroadcastCommand())
|
if (!E.Message.IsBroadcastCommand())
|
||||||
{
|
{
|
||||||
foreach (var stat in topStats)
|
foreach (var stat in topStats)
|
||||||
await E.Origin.Tell(stat);
|
{
|
||||||
|
E.Origin.Tell(stat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var stat in topStats)
|
foreach (var stat in topStats)
|
||||||
await E.Owner.Broadcast(stat);
|
{
|
||||||
|
E.Owner.Broadcast(stat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,12 +34,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
|
|
||||||
// fixme: this doesn't work properly when another context exists
|
// fixme: this doesn't work properly when another context exists
|
||||||
await svc.SaveChangesAsync();
|
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
|
else
|
||||||
{
|
{
|
||||||
await E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
|
E.Origin.Tell(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_RESET_FAIL"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
$"^5--{Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var db = new DatabaseContext())
|
using (var db = new DatabaseContext(true))
|
||||||
{
|
{
|
||||||
db.ChangeTracker.AutoDetectChangesEnabled = false;
|
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||||
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
|
||||||
|
|
||||||
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
|
|
||||||
|
|
||||||
var iqStats = (from stats in db.Set<EFClientStatistics>()
|
var iqStats = (from stats in db.Set<EFClientStatistics>()
|
||||||
join client in db.Clients
|
join client in db.Clients
|
||||||
@ -36,9 +33,9 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
join alias in db.Aliases
|
join alias in db.Aliases
|
||||||
on client.CurrentAliasId equals alias.AliasId
|
on client.CurrentAliasId equals alias.AliasId
|
||||||
where stats.ServerId == serverId
|
where stats.ServerId == serverId
|
||||||
where stats.TimePlayed >= 3600
|
where stats.TimePlayed >= Plugin.Config.Configuration().TopPlayersMinPlayTime
|
||||||
where client.Level != Player.Permission.Banned
|
where client.Level != Player.Permission.Banned
|
||||||
where client.LastConnection >= thirtyDaysAgo
|
where client.LastConnection >= fifteenDaysAgo
|
||||||
orderby stats.Performance descending
|
orderby stats.Performance descending
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
@ -48,6 +45,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
})
|
})
|
||||||
.Take(5);
|
.Take(5);
|
||||||
|
|
||||||
|
#if DEBUG == true
|
||||||
|
var statsSql = iqStats.ToSql();
|
||||||
|
#endif
|
||||||
|
|
||||||
var statsList = (await iqStats.ToListAsync())
|
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"]}");
|
.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())
|
if (!E.Message.IsBroadcastCommand())
|
||||||
{
|
{
|
||||||
foreach (var stat in topStats)
|
foreach (var stat in topStats)
|
||||||
await E.Origin.Tell(stat);
|
{
|
||||||
|
E.Origin.Tell(stat);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var stat in topStats)
|
foreach (var stat in topStats)
|
||||||
await E.Owner.Broadcast(stat);
|
{
|
||||||
|
E.Owner.Broadcast(stat);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,25 +26,17 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
{
|
{
|
||||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
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;
|
String statLine;
|
||||||
EFClientStatistics pStats;
|
EFClientStatistics pStats;
|
||||||
|
|
||||||
if (E.Data.Length > 0 && E.Target == null)
|
if (E.Data.Length > 0 && E.Target == null)
|
||||||
{
|
{
|
||||||
await E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault();
|
||||||
return;
|
|
||||||
|
if (E.Target == null)
|
||||||
|
{
|
||||||
|
E.Origin.Tell(loc["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientStats = new GenericRepository<EFClientStatistics>();
|
var clientStats = new GenericRepository<EFClientStatistics>();
|
||||||
@ -52,28 +44,31 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
|
|
||||||
if (E.Target != null)
|
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()}";
|
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
|
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()}";
|
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())
|
if (E.Message.IsBroadcastCommand())
|
||||||
{
|
{
|
||||||
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
string name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
||||||
await E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
|
E.Owner.Broadcast($"{loc["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"]} ^5{name}^7");
|
||||||
await E.Owner.Broadcast(statLine);
|
E.Owner.Broadcast(statLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (E.Target != null)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats.Config
|
namespace IW4MAdmin.Plugins.Stats.Config
|
||||||
{
|
{
|
||||||
class StatsConfiguration : IBaseConfiguration
|
public class StatsConfiguration : IBaseConfiguration
|
||||||
{
|
{
|
||||||
public bool EnableAntiCheat { get; set; }
|
public bool EnableAntiCheat { get; set; }
|
||||||
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
public List<StreakMessageConfiguration> KillstreakMessages { get; set; }
|
||||||
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
public List<StreakMessageConfiguration> DeathstreakMessages { get; set; }
|
||||||
|
public int TopPlayersMinPlayTime { get; set; }
|
||||||
|
public bool StoreClientKills { get; set; }
|
||||||
public string Name() => "Stats";
|
public string Name() => "Stats";
|
||||||
public IBaseConfiguration Generate()
|
public IBaseConfiguration Generate()
|
||||||
{
|
{
|
||||||
@ -47,6 +49,9 @@ namespace IW4MAdmin.Plugins.Stats.Config
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TopPlayersMinPlayTime = 3600 * 3;
|
||||||
|
StoreClientKills = false;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public static double[] AngleStuff(Vector3 a, Vector3 b)
|
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);
|
double deltaY = 180.0 - Math.Abs(Math.Abs(a.Y - b.Y) - 180.0);
|
||||||
|
|
||||||
return new[] { deltaX, deltaY };
|
return new[] { deltaX, deltaY };
|
||||||
|
@ -23,7 +23,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public int TeamCount(IW4Info.Team teamName)
|
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);
|
return IsTeamBased ? Math.Max(PlayerStats.Count(p => p.Value.Team == teamName), 1) : Math.Max(PlayerStats.Count - 1, 1);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,13 @@ using SharedLibraryCore.Objects;
|
|||||||
using SharedLibraryCore.Commands;
|
using SharedLibraryCore.Commands;
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using System.Text.RegularExpressions;
|
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
|
namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||||
{
|
{
|
||||||
@ -21,19 +28,158 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
private ILogger Log;
|
private ILogger Log;
|
||||||
private IManager Manager;
|
private IManager Manager;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim OnProcessingPenalty;
|
||||||
|
|
||||||
public StatManager(IManager mgr)
|
public StatManager(IManager mgr)
|
||||||
{
|
{
|
||||||
Servers = new ConcurrentDictionary<int, ServerStats>();
|
Servers = new ConcurrentDictionary<int, ServerStats>();
|
||||||
ContextThreads = new ConcurrentDictionary<int, ThreadSafeStatsService>();
|
ContextThreads = new ConcurrentDictionary<int, ThreadSafeStatsService>();
|
||||||
Log = mgr.GetLogger();
|
Log = mgr.GetLogger();
|
||||||
Manager = mgr;
|
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();
|
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||||
Log = null;
|
return (r) => r.ServerId == serverId &&
|
||||||
Servers = null;
|
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>
|
/// <summary>
|
||||||
@ -97,8 +243,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerStats = Servers[serverId].PlayerStats;
|
var playerStats = Servers[serverId].PlayerStats;
|
||||||
var statsSvc = ContextThreads[serverId];
|
|
||||||
var detectionStats = Servers[serverId].PlayerDetections;
|
var detectionStats = Servers[serverId].PlayerDetections;
|
||||||
|
var statsSvc = ContextThreads[serverId];
|
||||||
|
|
||||||
if (playerStats.ContainsKey(pl.ClientId))
|
if (playerStats.ContainsKey(pl.ClientId))
|
||||||
{
|
{
|
||||||
@ -128,8 +274,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Active = true,
|
Active = true,
|
||||||
HitCount = 0,
|
HitCount = 0,
|
||||||
Location = hl
|
Location = hl
|
||||||
})
|
}).ToList()
|
||||||
.ToList()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// insert if they've not been added
|
// insert if they've not been added
|
||||||
@ -147,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Location = hl
|
Location = hl
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// for stats before rating
|
// for stats before rating
|
||||||
@ -165,6 +310,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
clientStats.LastActive = DateTime.UtcNow;
|
clientStats.LastActive = DateTime.UtcNow;
|
||||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||||
clientStats.SessionScore = pl.Score;
|
clientStats.SessionScore = pl.Score;
|
||||||
|
clientStats.LastScore = pl.Score;
|
||||||
|
|
||||||
Log.WriteInfo($"Adding {pl} to stats");
|
Log.WriteInfo($"Adding {pl} to stats");
|
||||||
|
|
||||||
@ -225,7 +371,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
if (match.Success)
|
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 attackerStats = Servers[serverId].PlayerStats[attackerClientId];
|
||||||
var victimStats = Servers[serverId].PlayerStats[victimClientId];
|
var victimStats = Servers[serverId].PlayerStats[victimClientId];
|
||||||
IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString());
|
IW4Info.Team victimTeam = (IW4Info.Team)Enum.Parse(typeof(IW4Info.Team), match.Groups[4].ToString());
|
||||||
@ -240,7 +386,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task AddScriptHit(bool isDamage, DateTime time, Player attacker, Player victim, int serverId, string map, string hitLoc, string type,
|
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];
|
var statsSvc = ContextThreads[serverId];
|
||||||
Vector3 vDeathOrigin = null;
|
Vector3 vDeathOrigin = null;
|
||||||
@ -278,7 +425,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var kill = new EFClientKill()
|
var hit = new EFClientKill()
|
||||||
{
|
{
|
||||||
Active = true,
|
Active = true,
|
||||||
AttackerId = attacker.ClientId,
|
AttackerId = attacker.ClientId,
|
||||||
@ -296,11 +443,14 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
When = time,
|
When = time,
|
||||||
IsKillstreakKill = isKillstreakKill[0] != '0',
|
IsKillstreakKill = isKillstreakKill[0] != '0',
|
||||||
AdsPercent = float.Parse(Ads),
|
AdsPercent = float.Parse(Ads),
|
||||||
|
Fraction = double.Parse(fraction),
|
||||||
|
VisibilityPercentage = double.Parse(visibilityPercentage),
|
||||||
|
IsKill = !isDamage,
|
||||||
AnglesList = snapshotAngles
|
AnglesList = snapshotAngles
|
||||||
};
|
};
|
||||||
|
|
||||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
|
if ((hit.DeathType == IW4Info.MeansOfDeath.MOD_SUICIDE &&
|
||||||
kill.Damage == 100000)
|
hit.Damage == 100000) || hit.HitLoc == IW4Info.HitLocation.shield)
|
||||||
{
|
{
|
||||||
// suicide by switching teams so let's not count it against them
|
// suicide by switching teams so let's not count it against them
|
||||||
return;
|
return;
|
||||||
@ -311,7 +461,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
await AddStandardKill(attacker, victim);
|
await AddStandardKill(attacker, victim);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kill.IsKillstreakKill)
|
if (hit.IsKillstreakKill)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -322,40 +472,76 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
clientStatsSvc.Update(clientStats);
|
clientStatsSvc.Update(clientStats);
|
||||||
|
|
||||||
// increment their hit count
|
// increment their hit count
|
||||||
if (kill.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
if (hit.DeathType == IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
|
||||||
kill.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
hit.DeathType == IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
|
||||||
kill.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
hit.DeathType == IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
|
||||||
{
|
{
|
||||||
clientStats.HitLocations.Single(hl => hl.Location == kill.HitLoc).HitCount += 1;
|
clientStats.HitLocations.Single(hl => hl.Location == hit.HitLoc).HitCount += 1;
|
||||||
|
|
||||||
//statsSvc.ClientStatSvc.Update(clientStats);
|
|
||||||
// await statsSvc.ClientStatSvc.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//statsSvc.KillStatsSvc.Insert(kill);
|
using (var ctx = new DatabaseContext())
|
||||||
//await statsSvc.KillStatsSvc.SaveChangesAsync();
|
{
|
||||||
|
await OnProcessingPenalty.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Plugin.Config.Configuration().StoreClientKills)
|
||||||
|
{
|
||||||
|
ctx.Set<EFClientKill>().Add(hit);
|
||||||
|
}
|
||||||
|
|
||||||
if (Plugin.Config.Configuration().EnableAntiCheat)
|
if (Plugin.Config.Configuration().EnableAntiCheat)
|
||||||
{
|
{
|
||||||
async Task executePenalty(Cheat.DetectionPenaltyResult penalty)
|
await ApplyPenalty(clientDetection.ProcessKill(hit, isDamage), clientDetection, attacker, ctx);
|
||||||
{
|
await ApplyPenalty(clientDetection.ProcessTotalRatio(clientStats), clientDetection, attacker, ctx);
|
||||||
// prevent multiple bans from occuring
|
|
||||||
if (attacker.Level == Player.Permission.Banned)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await clientStatsSvc.SaveChangesAsync();
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.WriteError("AC ERROR");
|
||||||
|
Log.WriteDebug(ex.GetExceptionInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
OnProcessingPenalty.Release(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ApplyPenalty(Cheat.DetectionPenaltyResult penalty, Cheat.Detection clientDetection, Player attacker, DatabaseContext ctx)
|
||||||
|
{
|
||||||
switch (penalty.ClientPenalty)
|
switch (penalty.ClientPenalty)
|
||||||
{
|
{
|
||||||
case Penalty.PenaltyType.Ban:
|
case Penalty.PenaltyType.Ban:
|
||||||
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
|
if (attacker.Level == Player.Permission.Banned)
|
||||||
{
|
{
|
||||||
ClientId = 1
|
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;
|
break;
|
||||||
case Penalty.PenaltyType.Flag:
|
case Penalty.PenaltyType.Flag:
|
||||||
if (attacker.Level != Player.Permission.User)
|
if (attacker.Level != Player.Permission.User)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
var e = new GameEvent()
|
var e = new GameEvent()
|
||||||
{
|
{
|
||||||
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
Data = penalty.Type == Cheat.Detection.DetectionType.Bone ?
|
||||||
@ -372,46 +558,99 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
Owner = attacker.CurrentServer,
|
Owner = attacker.CurrentServer,
|
||||||
Type = GameEvent.EventType.Flag
|
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);
|
await new CFlag().ExecuteAsync(e);
|
||||||
|
if (clientDetection.Tracker.HasChanges)
|
||||||
|
{
|
||||||
|
SaveTrackedSnapshots(clientDetection, ctx);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await executePenalty(clientDetection.ProcessKill(kill, isDamage));
|
void SaveTrackedSnapshots(Cheat.Detection clientDetection, DatabaseContext ctx)
|
||||||
await executePenalty(clientDetection.ProcessTotalRatio(clientStats));
|
{
|
||||||
|
// todo: why does this cause duplicate primary key
|
||||||
|
var change = clientDetection.Tracker.GetNextChange();
|
||||||
|
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
|
||||||
|
{
|
||||||
|
|
||||||
await clientStatsSvc.SaveChangesAsync();
|
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)
|
public async Task AddStandardKill(Player attacker, Player victim)
|
||||||
{
|
{
|
||||||
int serverId = attacker.CurrentServer.GetHashCode();
|
int serverId = attacker.CurrentServer.GetHashCode();
|
||||||
|
|
||||||
EFClientStatistics attackerStats = null;
|
EFClientStatistics attackerStats = null;
|
||||||
try
|
if (!Servers[serverId].PlayerStats.ContainsKey(attacker.ClientId))
|
||||||
|
{
|
||||||
|
attackerStats = await AddPlayer(attacker);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
{
|
{
|
||||||
attackerStats = Servers[serverId].PlayerStats[attacker.ClientId];
|
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
|
victimStats = await AddPlayer(victim);
|
||||||
Log.WriteWarning($"[Stats::AddStandardKill] kill attacker ClientId is invalid {attacker.ClientId}-{attacker}");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EFClientStatistics victimStats = null;
|
else
|
||||||
try
|
|
||||||
{
|
{
|
||||||
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
|
victimStats = Servers[serverId].PlayerStats[victim.ClientId];
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (KeyNotFoundException)
|
|
||||||
{
|
|
||||||
Log.WriteWarning($"[Stats::AddStandardKill] kill victim ClientId is invalid {victim.ClientId}-{victim}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Log.WriteDebug("Calculating standard kill");
|
Log.WriteDebug("Calculating standard kill");
|
||||||
#endif
|
#endif
|
||||||
@ -442,7 +681,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
StreakMessage.MessageOnStreak(-1, -1);
|
StreakMessage.MessageOnStreak(-1, -1);
|
||||||
|
|
||||||
if (streakMessage != string.Empty)
|
if (streakMessage != string.Empty)
|
||||||
await attacker.Tell(streakMessage);
|
{
|
||||||
|
attacker.Tell(streakMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// fixme: why?
|
// fixme: why?
|
||||||
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
|
if (double.IsNaN(victimStats.SPM) || double.IsNaN(victimStats.Skill))
|
||||||
@ -459,6 +700,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
attackerStats.Skill = 0.0;
|
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?
|
// todo: do we want to save this immediately?
|
||||||
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
|
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
|
||||||
clientStatsSvc.Update(attackerStats);
|
clientStatsSvc.Update(attackerStats);
|
||||||
@ -466,6 +718,182 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
await clientStatsSvc.SaveChangesAsync();
|
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>
|
/// <summary>
|
||||||
/// Performs the incrementation of kills and deaths for client statistics
|
/// Performs the incrementation of kills and deaths for client statistics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -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);
|
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);
|
killSPM *= Math.Max(1, spmMultiplier);
|
||||||
|
|
||||||
|
// update this for ac tracking
|
||||||
|
clientStats.SessionSPM = killSPM;
|
||||||
|
|
||||||
// calculate how much the KDR should weigh
|
// calculate how much the KDR should weigh
|
||||||
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
// 1.637 is a Eddie-Generated number that weights the KDR nicely
|
||||||
double currentKDR = clientStats.SessionDeaths == 0 ? clientStats.SessionKills : clientStats.SessionKills / clientStats.SessionDeaths;
|
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;
|
clientStats.RollingWeightedKDR = (alpha * currentKDR) + (1.0 - alpha) * clientStats.KDR;
|
||||||
double KDRWeight = Math.Round(Math.Pow(clientStats.RollingWeightedKDR, 1.637 / Math.E), 3);
|
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();
|
int serverId = sv.GetHashCode();
|
||||||
var statsSvc = ContextThreads[serverId];
|
var statsSvc = ContextThreads[serverId];
|
||||||
|
|
||||||
Log.WriteDebug("Syncing stats contexts");
|
// Log.WriteDebug("Syncing stats contexts");
|
||||||
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
await statsSvc.ServerStatsSvc.SaveChangesAsync();
|
||||||
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
//await statsSvc.ClientStatSvc.SaveChangesAsync();
|
||||||
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
await statsSvc.KillStatsSvc.SaveChangesAsync();
|
||||||
await statsSvc.ServerSvc.SaveChangesAsync();
|
await statsSvc.ServerSvc.SaveChangesAsync();
|
||||||
|
|
||||||
statsSvc = null;
|
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();
|
ContextThreads[serverId] = new ThreadSafeStatsService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return new GenericRepository<EFClientStatistics>();
|
return new GenericRepository<EFClientStatistics>(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public GenericRepository<EFServer> ServerSvc { get; private set; }
|
public GenericRepository<EFServer> ServerSvc { get; private set; }
|
||||||
@ -30,11 +30,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public ThreadSafeStatsService()
|
public ThreadSafeStatsService()
|
||||||
{
|
{
|
||||||
//ClientStatSvc = new GenericRepository<EFClientStatistics>();
|
|
||||||
ServerSvc = new GenericRepository<EFServer>();
|
ServerSvc = new GenericRepository<EFServer>();
|
||||||
KillStatsSvc = new GenericRepository<EFClientKill>();
|
KillStatsSvc = new GenericRepository<EFClientKill>();
|
||||||
ServerStatsSvc = new GenericRepository<EFServerStatistics>();
|
ServerStatsSvc = new GenericRepository<EFServerStatistics>();
|
||||||
//MessageSvc = new GenericRepository<EFClientMessage>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
{
|
{
|
||||||
public enum Team
|
public enum Team
|
||||||
{
|
{
|
||||||
|
None,
|
||||||
Spectator,
|
Spectator,
|
||||||
Axis,
|
Allies,
|
||||||
Allies
|
Axis
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MeansOfDeath
|
public enum MeansOfDeath
|
||||||
@ -64,6 +65,7 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
right_foot,
|
right_foot,
|
||||||
left_foot,
|
left_foot,
|
||||||
gun,
|
gun,
|
||||||
|
shield
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WeaponName
|
public enum WeaponName
|
||||||
@ -1368,7 +1370,9 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
dragunov_mp,
|
dragunov_mp,
|
||||||
cobra_player_minigun_mp,
|
cobra_player_minigun_mp,
|
||||||
destructible_car,
|
destructible_car,
|
||||||
sentry_minigun_mp
|
sentry_minigun_mp,
|
||||||
|
cobra_20mm_mp,
|
||||||
|
shield
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MapName
|
public enum MapName
|
||||||
|
51
Plugins/Stats/Models/EFACSnapshot.cs
Normal file
51
Plugins/Stats/Models/EFACSnapshot.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,9 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
public Vector3 DeathOrigin { get; set; }
|
public Vector3 DeathOrigin { get; set; }
|
||||||
public Vector3 ViewAngles { get; set; }
|
public Vector3 ViewAngles { get; set; }
|
||||||
public DateTime When { 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
|
// http://wiki.modsrepository.com/index.php?title=Call_of_Duty_5:_Gameplay_standards for conversion to meters
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Distance => Vector3.Distance(KillOrigin, DeathOrigin) * 0.0254;
|
public double Distance => Vector3.Distance(KillOrigin, DeathOrigin) * 0.0254;
|
||||||
|
20
Plugins/Stats/Models/EFClientRatingHistory.cs
Normal file
20
Plugins/Stats/Models/EFClientRatingHistory.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
public double EloRating { get; set; }
|
public double EloRating { get; set; }
|
||||||
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
public virtual ICollection<EFHitLocationCount> HitLocations { get; set; }
|
||||||
public double RollingWeightedKDR { get; set; }
|
public double RollingWeightedKDR { get; set; }
|
||||||
|
public double VisionAverage { get; set; }
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Performance
|
public double Performance
|
||||||
{
|
{
|
||||||
@ -71,6 +72,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
DeathStreak = 0;
|
DeathStreak = 0;
|
||||||
LastScore = 0;
|
LastScore = 0;
|
||||||
SessionScores.Add(0);
|
SessionScores.Add(0);
|
||||||
|
Team = IW4Info.Team.None;
|
||||||
}
|
}
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int SessionScore
|
public int SessionScore
|
||||||
@ -96,5 +98,9 @@ namespace IW4MAdmin.Plugins.Stats.Models
|
|||||||
private List<int> SessionScores = new List<int>() { 0 };
|
private List<int> SessionScores = new List<int>() { 0 };
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public IW4Info.Team Team { get; set; }
|
public IW4Info.Team Team { get; set; }
|
||||||
|
[NotMapped]
|
||||||
|
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
|
||||||
|
[NotMapped]
|
||||||
|
public double SessionSPM { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
Plugins/Stats/Models/EFRating.cs
Normal file
31
Plugins/Stats/Models/EFRating.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,18 @@ namespace Stats.Models
|
|||||||
.Property(c => c.ServerId)
|
.Property(c => c.ServerId)
|
||||||
.HasColumnName("EFClientStatistics_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
|
// force pluralization
|
||||||
builder.Entity<EFClientKill>().ToTable("EFClientKills");
|
builder.Entity<EFClientKill>().ToTable("EFClientKills");
|
||||||
builder.Entity<EFClientMessage>().ToTable("EFClientMessages");
|
builder.Entity<EFClientMessage>().ToTable("EFClientMessages");
|
||||||
|
@ -16,7 +16,7 @@ using IW4MAdmin.Plugins.Stats.Models;
|
|||||||
|
|
||||||
namespace IW4MAdmin.Plugins.Stats
|
namespace IW4MAdmin.Plugins.Stats
|
||||||
{
|
{
|
||||||
class Plugin : IPlugin
|
public class Plugin : IPlugin
|
||||||
{
|
{
|
||||||
public string Name => "Simple Stats";
|
public string Name => "Simple Stats";
|
||||||
|
|
||||||
@ -55,6 +55,8 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
break;
|
break;
|
||||||
case GameEvent.EventType.MapEnd:
|
case GameEvent.EventType.MapEnd:
|
||||||
break;
|
break;
|
||||||
|
case GameEvent.EventType.JoinTeam:
|
||||||
|
break;
|
||||||
case GameEvent.EventType.Broadcast:
|
case GameEvent.EventType.Broadcast:
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Tell:
|
case GameEvent.EventType.Tell:
|
||||||
@ -63,8 +65,6 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Ban:
|
case GameEvent.EventType.Ban:
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Remote:
|
|
||||||
break;
|
|
||||||
case GameEvent.EventType.Unknown:
|
case GameEvent.EventType.Unknown:
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Report:
|
case GameEvent.EventType.Report:
|
||||||
@ -73,25 +73,31 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
break;
|
break;
|
||||||
case GameEvent.EventType.ScriptKill:
|
case GameEvent.EventType.ScriptKill:
|
||||||
string[] killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
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],
|
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;
|
break;
|
||||||
case GameEvent.EventType.Kill:
|
case GameEvent.EventType.Kill:
|
||||||
if (!E.Owner.CustomCallback)
|
if (!E.Owner.CustomCallback)
|
||||||
|
{
|
||||||
await Manager.AddStandardKill(E.Origin, E.Target);
|
await Manager.AddStandardKill(E.Origin, E.Target);
|
||||||
break;
|
}
|
||||||
case GameEvent.EventType.Death:
|
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.Damage:
|
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());
|
Manager.AddDamageEvent(E.Data, E.Origin.ClientId, E.Target.ClientId, E.Owner.GetHashCode());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case GameEvent.EventType.ScriptDamage:
|
case GameEvent.EventType.ScriptDamage:
|
||||||
killInfo = (E.Data != null) ? E.Data.Split(';') : new string[0];
|
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],
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +112,13 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
await Config.Save();
|
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
|
// meta data info
|
||||||
async Task<List<ProfileMeta>> getStats(int clientId)
|
async Task<List<ProfileMeta>> getStats(int clientId)
|
||||||
{
|
{
|
||||||
@ -122,6 +135,11 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
|
|
||||||
return new List<ProfileMeta>()
|
return new List<ProfileMeta>()
|
||||||
{
|
{
|
||||||
|
new ProfileMeta()
|
||||||
|
{
|
||||||
|
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||||
|
Value = "#" + await Manager.GetClientOverallRanking(clientId),
|
||||||
|
},
|
||||||
new ProfileMeta()
|
new ProfileMeta()
|
||||||
{
|
{
|
||||||
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
Key = Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_KILLS"],
|
||||||
@ -161,7 +179,6 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
double chestAbdomenRatio = 0;
|
double chestAbdomenRatio = 0;
|
||||||
double hitOffsetAverage = 0;
|
double hitOffsetAverage = 0;
|
||||||
double maxStrain = clientStats.Count(c => c.MaxStrain > 0) == 0 ? 0 : clientStats.Max(cs => cs.MaxStrain);
|
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)
|
if (clientStats.Where(cs => cs.HitLocations.Count > 0).FirstOrDefault() != null)
|
||||||
{
|
{
|
||||||
@ -223,12 +240,6 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
Value = Math.Round(maxStrain, 3),
|
Value = Math.Round(maxStrain, 3),
|
||||||
Sensitive = true
|
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",
|
Key = "EventMessage",
|
||||||
Value = m.Message,
|
Value = m.Message,
|
||||||
When = m.TimeSent
|
When = m.TimeSent,
|
||||||
|
Extra = m.ServerId.ToString()
|
||||||
}).ToList();
|
}).ToList();
|
||||||
messageMeta.Add(new ProfileMeta()
|
messageMeta.Add(new ProfileMeta()
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<ApplicationIcon />
|
<ApplicationIcon />
|
||||||
<StartupObject />
|
<StartupObject />
|
||||||
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
|
<PackageId>RaidMax.IW4MAdmin.Plugins.Stats</PackageId>
|
||||||
@ -15,19 +15,26 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Cheat\Strain.cs~RF16f7b3.TMP" />
|
<Content Include="Web\Views\Stats\_MessageContext.cshtml">
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
<ProjectReference Include="..\..\SharedLibraryCore\SharedLibraryCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\WebfrontCore\WebfrontCore.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Microsoft.NETCore.App" Version="2.0.7" />
|
<PackageReference Update="Microsoft.NETCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
<Exec Command="copy "$(TargetPath)" "$(SolutionDir)BUILD\Plugins"" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||||
|
<Exec Command="xcopy /E /K /Y /C /I "$(ProjectDir)Web\Views" "$(SolutionDir)WebfrontCore\Views\Plugins"
xcopy /E /K /Y /C /I "$(ProjectDir)Web\wwwroot\images" "$(SolutionDir)WebfrontCore\wwwroot\images"" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
80
Plugins/Stats/Web/Controllers/StatsController.cs
Normal file
80
Plugins/Stats/Web/Controllers/StatsController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
Plugins/Stats/Web/Dtos/TopStatsInfo.cs
Normal file
22
Plugins/Stats/Web/Dtos/TopStatsInfo.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
14
Plugins/Stats/Web/Views/Stats/Index.cshtml
Normal file
14
Plugins/Stats/Web/Views/Stats/Index.cshtml
Normal 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>
|
||||||
|
}
|
68
Plugins/Stats/Web/Views/Stats/_List.cshtml
Normal file
68
Plugins/Stats/Web/Views/Stats/_List.cshtml
Normal 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;">—</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>
|
||||||
|
}
|
12
Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml
Normal file
12
Plugins/Stats/Web/Views/Stats/_MessageContext.cshtml
Normal 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> — @message.Message</span><br />
|
||||||
|
}
|
||||||
|
</div>
|
29
Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml
Normal file
29
Plugins/Stats/Web/Views/Stats/_PenaltyInfo.cshtml
Normal 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>— @prop.GetValue(snapshot)</span><br />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<div class="w-100 mt-1 mb-1 border-bottom"></div>
|
||||||
|
}
|
||||||
|
</div>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user