Compare commits

...

21 Commits

Author SHA1 Message Date
8b06da5783 use different api for country code/flag that support https 2021-07-02 10:04:56 -05:00
33a427bb8a add country flag and name to profile 2021-07-01 21:58:09 -05:00
c9d7a957dc add reset anticheat metric (!rsa) for issue #177 2021-07-01 13:12:19 -05:00
9c6ff6f353 use right game for estimated score 2021-07-01 13:06:31 -05:00
7444cb6472 actually fix steam id parsing 2021-07-01 10:14:58 -05:00
c7e5c9c8dd parse steam id properly for source games 2021-07-01 09:10:56 -05:00
0256fc35d2 add login/logout events to change tracker
default guest profile to minimum permissions
2021-06-30 21:13:25 -05:00
0019ed8dde fix run as command config not being honored properly 2021-06-30 18:10:45 -05:00
56aec53e72 fix bad key lookup in manager 2021-06-30 14:01:41 -05:00
1b773f21c6 fix alignment for long server names 2021-06-30 10:44:43 -05:00
bccbcce3c1 add lobby rating to home
add gametype (WIP) to home
misc UI tweaks
2021-06-30 09:57:07 -05:00
fc0bed2405 show "out of" ranked players for stats command 2021-06-29 17:14:25 -05:00
16cfb33109 improvements and consistencies to the top stats, most played and top players commands 2021-06-29 15:35:56 -05:00
42979dc5ae Use string for AC snapshot weapon and hit location
Add webfront logging
2021-06-29 15:02:01 -05:00
95cbc85144 fix issue with selecting wrong parser during setup
add minimum name length option
fix issue with stats spm
2021-06-27 20:31:39 -05:00
9cbca390fe Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2021-06-16 08:55:56 -05:00
38c0c81451 Added CSGO maps (#210)
Added all current default CSGO maps (Competitive, Wingman, Casual, War Games, Retakes, Danger Zone)
2021-06-16 08:54:49 -05:00
af4630ecb9 Additional CSGO compatibility improvements 2021-06-16 08:53:50 -05:00
dbceb23823 fix issue with custom event registration 2021-06-16 08:51:22 -05:00
e628ac0e9e improve CS:GO compatibility 2021-06-11 11:52:30 -05:00
3a1e8359c2 add one log indicator for games (Pluto IW5) that don't log to mods folder even when fs_game is specified. 2021-06-07 16:58:36 -05:00
85 changed files with 12969 additions and 1658 deletions

4
.gitignore vendored
View File

@ -245,4 +245,6 @@ launchSettings.json
/Tests/ApplicationTests/Files/replay.json
/GameLogServer/game_log_server_env
.idea/*
*.db
*.db
/Data/IW4MAdmin_Migration.db-shm
/Data/IW4MAdmin_Migration.db-wal

View File

@ -240,7 +240,7 @@ namespace IW4MAdmin.Application
}
// remove the update tasks as they have completed
foreach (var serverId in serverTasksToRemove)
foreach (var serverId in serverTasksToRemove.Where(serverId => runningUpdateTasks.ContainsKey(serverId)))
{
if (!runningUpdateTasks[serverId].tokenSource.Token.IsCancellationRequested)
{

View File

@ -3,7 +3,13 @@
"Using": [
"Serilog.Sinks.File"
],
"MinimumLevel": "Information",
"MinimumLevel": {
"Default": "Information",
"Override": {
"System": "Warning",
"Microsoft": "Warning"
}
},
"WriteTo": [
{
"Name": "File",

View File

@ -1008,7 +1008,7 @@
}
]
},
{
{
"Game": "SHG1",
"Maps": [
{
@ -1132,6 +1132,151 @@
"Name": "mp_urban"
}
]
},
{
"Game": "CSGO",
"Maps": [
{
"Name": "ar_baggage",
"Alias": "Baggage"
},
{
"Name": "ar_dizzy",
"Alias": "Dizzy"
},
{
"Name": "ar_lunacy",
"Alias": "Lunacy"
},
{
"Name": "ar_monastery",
"Alias": "Monastery"
},
{
"Name": "ar_shoots",
"Alias": "Shoots"
},
{
"Name": "cs_agency",
"Alias": "Agency"
},
{
"Name": "cs_assault",
"Alias": "Assault"
},
{
"Name": "cs_italy",
"Alias": "Italy"
},
{
"Name": "cs_militia",
"Alias": "Militia"
},
{
"Name": "cs_office",
"Alias": "Office"
},
{
"Name": "de_ancient",
"Alias": "Ancient"
},
{
"Name": "de_bank",
"Alias": "Bank"
},
{
"Name": "de_cache",
"Alias": "Cache"
},
{
"Name": "de_calavera",
"Alias": "Calavera"
},
{
"Name": "de_canals",
"Alias": "Canals"
},
{
"Name": "de_cbble",
"Alias": "Cobblestone"
},
{
"Name": "de_dust2",
"Alias": "Dust II"
},
{
"Name": "de_grind",
"Alias": "Grind"
},
{
"Name": "de_inferno",
"Alias": "Inferno"
},
{
"Name": "de_lake",
"Alias": "Lake"
},
{
"Name": "de_mirage",
"Alias": "Mirage"
},
{
"Name": "de_mocha",
"Alias": "Mocha"
},
{
"Name": "de_nuke",
"Alias": "Nuke"
},
{
"Name": "de_overpass",
"Alias": "Overpass"
},
{
"Name": "de_pitstop",
"Alias": "Pitstop"
},
{
"Name": "de_safehouse",
"Alias": "Safehouse"
},
{
"Name": "de_shortdust",
"Alias": "Shortdust"
},
{
"Name": "de_shortnuke",
"Alias": "Shortnuke"
},
{
"Name": "de_stmarc",
"Alias": "St. Marc"
},
{
"Name": "de_sugarcane",
"Alias": "Sugarcane"
},
{
"Name": "de_train",
"Alias": "Train"
},
{
"Name": "de_vertigo",
"Alias": "Vertigo"
},
{
"Name": "dz_blacksite",
"Alias": "Blacksite"
},
{
"Name": "dz_frostbite",
"Alias": "Frostbite"
},
{
"Name": "dz_sirocco",
"Alias": "Sirocco"
}
]
}
],
"GameStrings": {

View File

@ -120,7 +120,7 @@ namespace IW4MAdmin.Application.EventParsers
if (lineSplit.Length > 1)
{
var type = lineSplit[0];
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, null);
return _eventTypeMap.ContainsKey(type) ? (_eventTypeMap[type], type): (GameEvent.EventType.Unknown, lineSplit[0]);
}
foreach (var (key, value) in _regexMap)

View File

@ -22,7 +22,7 @@ namespace IW4MAdmin.Application.Factories
/// </summary>
/// <param name="translationLookup"></param>
/// <param name="rconConnectionFactory"></param>
public GameServerInstanceFactory(ITranslationLookup translationLookup,
public GameServerInstanceFactory(ITranslationLookup translationLookup,
IMetaService metaService,
IServiceProvider serviceProvider)
{
@ -39,7 +39,10 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager)
{
return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(), _serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
return new IW4MServer(config,
_serviceProvider.GetRequiredService<CommandConfiguration>(), _translationLookup, _metaService,
_serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>(),
_serviceProvider.GetRequiredService<ILookupCache<EFServer>>());
}
}
}
}

View File

@ -39,9 +39,11 @@ namespace IW4MAdmin
private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
private readonly ILookupCache<EFServer> _serverCache;
private readonly CommandConfiguration _commandConfiguration;
public IW4MServer(
ServerConfiguration serverConfiguration,
CommandConfiguration commandConfiguration,
ITranslationLookup lookup,
IMetaService metaService,
IServiceProvider serviceProvider,
@ -58,6 +60,7 @@ namespace IW4MAdmin
_serviceProvider = serviceProvider;
_messageFormatter = messageFormatter;
_serverCache = serverCache;
_commandConfiguration = commandConfiguration;
}
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -158,7 +161,7 @@ namespace IW4MAdmin
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration());
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
@ -1042,8 +1045,8 @@ namespace IW4MAdmin
EventParser = Manager.AdditionalEventParsers
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
RconParser = RconParser ?? Manager.AdditionalRConParsers[0];
EventParser = EventParser ?? Manager.AdditionalEventParsers[0];
RconParser ??= Manager.AdditionalRConParsers[0];
EventParser ??= Manager.AdditionalEventParsers[0];
RemoteConnection = RConConnectionFactory.CreateConnection(IP, Port, Password, RconParser.RConEngine);
RemoteConnection.SetConfiguration(RconParser);
@ -1177,12 +1180,12 @@ namespace IW4MAdmin
GameDirectory = EventParser.Configuration.GameDirectory ?? "",
ModDirectory = game.Value ?? "",
LogFile = logfile.Value,
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
IsOneLog = RconParser.IsOneLog
};
LogPath = GenerateLogPath(logInfo);
ServerLogger.LogInformation("Game log information {@logInfo}", logInfo);
if (!File.Exists(LogPath) && ServerConfig.GameLogServerUrl == null)
{
Console.WriteLine(loc["SERVER_ERROR_DNE"].FormatExt(LogPath));
@ -1223,12 +1226,12 @@ namespace IW4MAdmin
public static string GenerateLogPath(LogPathGeneratorInfo logInfo)
{
string logPath;
string workingDirectory = logInfo.BasePathDirectory;
var workingDirectory = logInfo.BasePathDirectory;
bool baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
var baseGameIsDirectory = !string.IsNullOrWhiteSpace(logInfo.BaseGameDirectory) &&
logInfo.BaseGameDirectory.IndexOfAny(Utilities.DirectorySeparatorChars) != -1;
bool baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
var baseGameIsRelative = logInfo.BaseGameDirectory.FixDirectoryCharacters()
.Equals(logInfo.GameDirectory.FixDirectoryCharacters(), StringComparison.InvariantCultureIgnoreCase);
// we want to see if base game is provided and it 'looks' like a directory
@ -1237,7 +1240,7 @@ namespace IW4MAdmin
workingDirectory = logInfo.BaseGameDirectory;
}
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory))
if (string.IsNullOrWhiteSpace(logInfo.ModDirectory) || logInfo.IsOneLog)
{
logPath = Path.Combine(workingDirectory, logInfo.GameDirectory, logInfo.LogFile);
}

View File

@ -41,5 +41,11 @@ namespace IW4MAdmin.Application.Misc
/// indicates if running on windows
/// </summary>
public bool IsWindows { get; set; } = true;
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
public bool IsOneLog { get; set; }
}
}

View File

@ -75,11 +75,12 @@ namespace IW4MAdmin.Application.RConParsers
public bool CanGenerateLogPath { get; set; } = true;
public string Name { get; set; } = "Call of Duty";
public string RConEngine { get; set; } = "COD";
public bool IsOneLog { get; set; }
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command)
{
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command);
return response.Skip(1).ToArray();
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
}
public async Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default)
@ -216,10 +217,15 @@ namespace IW4MAdmin.Application.RConParsers
continue;
}
int clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
int score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
var clientNumber = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConClientNumber]]);
var score = 0;
if (Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore] > 0)
{
score = int.Parse(match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConScore]]);
}
int ping = 999;
var ping = 999;
// their state can be CNCT, ZMBI etc
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConPing]].Length <= 3)
@ -228,7 +234,7 @@ namespace IW4MAdmin.Application.RConParsers
}
long networkId;
string name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();

View File

@ -8,62 +8,9 @@
<PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title>
<Authors />
<PackageVersion>1.0.1</PackageVersion>
<PackageVersion>1.0.3</PackageVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Migrations\MySql\20210210221342_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\MySql\20210210221342_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224014503_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224014503_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224030227_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224030227_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210224031245_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210224031245_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210227041237_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210227041237_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210227161333_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Postgresql\20210227161333_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Postgresql\20210307163752_AddRankingHistory.cs" />
<Compile Remove="Migrations\Postgresql\20210307163752_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209205243_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209205243_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209205948_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209205948_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209211745_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209211745_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210209212725_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210209212725_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210020314_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210020314_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210140835_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210140835_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210154738_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210154738_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210163803_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210163803_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210210193852_AddAdditionaClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210210193852_AddAdditionaClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210211033835_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210211033835_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210219013429_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210219013429_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210220171950_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210220171950_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210223163022_AddAdditionalClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210223163022_AddAdditionalClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210226215929_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210226215929_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210227160800_AddPerformancePercentileToClientStats.cs" />
<Compile Remove="Migrations\Sqlite\20210227160800_AddPerformancePercentileToClientStats.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210305033616_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210305033616_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210305033846_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210305033846_AddRankingHistory.Designer.cs" />
<Compile Remove="Migrations\Sqlite\20210306223712_AddRankingHistory.cs" />
<Compile Remove="Migrations\Sqlite\20210306223712_AddRankingHistory.Designer.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.10">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "ServerId",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot",
column: "ServerId");
migrationBuilder.AddForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot",
column: "ServerId",
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.MySql
{
public partial class AddHitLocationReferenceToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HitLocationReference",
table: "EFACSnapshot",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HitLocationReference",
table: "EFACSnapshot");
}
}
}

View File

@ -146,6 +146,9 @@ namespace Data.Migrations.MySql
b.Property<int>("Weapon")
.HasColumnType("int");
b.Property<string>("WeaponReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<DateTime>("When")
.HasColumnType("datetime(6)");
@ -237,6 +240,9 @@ namespace Data.Migrations.MySql
b.Property<int>("HitLocation")
.HasColumnType("int");
b.Property<string>("HitLocationReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<int>("HitOriginId")
.HasColumnType("int");
@ -255,6 +261,9 @@ namespace Data.Migrations.MySql
b.Property<double>("RecoilOffset")
.HasColumnType("double");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<double>("SessionAngleOffset")
.HasColumnType("double");
@ -279,6 +288,9 @@ namespace Data.Migrations.MySql
b.Property<int>("WeaponId")
.HasColumnType("int");
b.Property<string>("WeaponReference")
.HasColumnType("longtext CHARACTER SET utf8mb4");
b.Property<DateTime>("When")
.HasColumnType("datetime(6)");
@ -294,6 +306,8 @@ namespace Data.Migrations.MySql
b.HasIndex("LastStrainAngleId");
b.HasIndex("ServerId");
b.ToTable("EFACSnapshot");
});
@ -1103,6 +1117,10 @@ namespace Data.Migrations.MySql
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b =>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "ServerId",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFACSnapshot",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot",
column: "ServerId");
migrationBuilder.AddForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot",
column: "ServerId",
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Restrict);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_EFACSnapshot_EFServers_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropIndex(
name: "IX_EFACSnapshot_ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "ServerId",
table: "EFACSnapshot");
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFACSnapshot");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Postgresql
{
public partial class AddHitLocationReferenceToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HitLocationReference",
table: "EFACSnapshot",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HitLocationReference",
table: "EFACSnapshot");
}
}
}

View File

@ -151,6 +151,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("Weapon")
.HasColumnType("integer");
b.Property<string>("WeaponReference")
.HasColumnType("text");
b.Property<DateTime>("When")
.HasColumnType("timestamp without time zone");
@ -244,6 +247,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("HitLocation")
.HasColumnType("integer");
b.Property<string>("HitLocationReference")
.HasColumnType("text");
b.Property<int>("HitOriginId")
.HasColumnType("integer");
@ -262,6 +268,9 @@ namespace Data.Migrations.Postgresql
b.Property<double>("RecoilOffset")
.HasColumnType("double precision");
b.Property<long?>("ServerId")
.HasColumnType("bigint");
b.Property<double>("SessionAngleOffset")
.HasColumnType("double precision");
@ -286,6 +295,9 @@ namespace Data.Migrations.Postgresql
b.Property<int>("WeaponId")
.HasColumnType("integer");
b.Property<string>("WeaponReference")
.HasColumnType("text");
b.Property<DateTime>("When")
.HasColumnType("timestamp without time zone");
@ -301,6 +313,8 @@ namespace Data.Migrations.Postgresql
b.HasIndex("LastStrainAngleId");
b.HasIndex("ServerId");
b.ToTable("EFACSnapshot");
});
@ -1128,6 +1142,10 @@ namespace Data.Migrations.Postgresql
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b =>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddWeaponReferenceToEFClientKill : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "WeaponReference",
table: "EFClientKills",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "WeaponReference",
table: "EFClientKills");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddWeaponReferenceAndServerIdToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"PRAGMA foreign_keys = 0;
CREATE TABLE sqlitestudio_temp_table AS SELECT *
FROM EFACSnapshot;
DROP TABLE EFACSnapshot;
CREATE TABLE EFACSnapshot (
Active INTEGER NOT NULL,
TimeSinceLastEvent INTEGER NOT NULL,
SnapshotId INTEGER NOT NULL
CONSTRAINT PK_EFACSnapshot PRIMARY KEY AUTOINCREMENT,
ClientId INTEGER NOT NULL,
ServerId INTEGER CONSTRAINT FK_EFACSnapshot_EFServers_ServerId REFERENCES EFServers (ServerId) ON DELETE RESTRICT,
[When] TEXT NOT NULL,
CurrentSessionLength INTEGER NOT NULL,
EloRating REAL NOT NULL,
SessionScore INTEGER NOT NULL,
SessionSPM REAL NOT NULL,
Hits INTEGER NOT NULL,
Kills INTEGER NOT NULL,
Deaths INTEGER NOT NULL,
CurrentStrain REAL NOT NULL,
StrainAngleBetween REAL NOT NULL,
SessionAngleOffset REAL NOT NULL,
LastStrainAngleId INTEGER NOT NULL,
HitOriginId INTEGER NOT NULL,
HitDestinationId INTEGER NOT NULL,
Distance REAL NOT NULL,
CurrentViewAngleId INTEGER,
WeaponId INTEGER NOT NULL,
WeaponReference TEXT,
HitLocation INTEGER NOT NULL,
HitType INTEGER NOT NULL,
RecoilOffset REAL NOT NULL
DEFAULT 0.0,
SessionAverageSnapValue REAL NOT NULL
DEFAULT 0.0,
SessionSnapHits INTEGER NOT NULL
DEFAULT 0,
CONSTRAINT FK_EFACSnapshot_EFClients_ClientId FOREIGN KEY (
ClientId
)
REFERENCES EFClients (ClientId) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_CurrentViewAngleId FOREIGN KEY (
CurrentViewAngleId
)
REFERENCES Vector3 (Vector3Id) ON DELETE RESTRICT,
CONSTRAINT FK_EFACSnapshot_Vector3_HitDestinationId FOREIGN KEY (
HitDestinationId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_HitOriginId FOREIGN KEY (
HitOriginId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE,
CONSTRAINT FK_EFACSnapshot_Vector3_LastStrainAngleId FOREIGN KEY (
LastStrainAngleId
)
REFERENCES Vector3 (Vector3Id) ON DELETE CASCADE
);
INSERT INTO EFACSnapshot (
Active,
TimeSinceLastEvent,
SnapshotId,
ClientId,
[When],
CurrentSessionLength,
EloRating,
SessionScore,
SessionSPM,
Hits,
Kills,
Deaths,
CurrentStrain,
StrainAngleBetween,
SessionAngleOffset,
LastStrainAngleId,
HitOriginId,
HitDestinationId,
Distance,
CurrentViewAngleId,
WeaponId,
HitLocation,
HitType,
RecoilOffset,
SessionAverageSnapValue,
SessionSnapHits
)
SELECT Active,
TimeSinceLastEvent,
SnapshotId,
ClientId,
""When"",
CurrentSessionLength,
EloRating,
SessionScore,
SessionSPM,
Hits,
Kills,
Deaths,
CurrentStrain,
StrainAngleBetween,
SessionAngleOffset,
LastStrainAngleId,
HitOriginId,
HitDestinationId,
Distance,
CurrentViewAngleId,
WeaponId,
HitLocation,
HitType,
RecoilOffset,
SessionAverageSnapValue,
SessionSnapHits
FROM sqlitestudio_temp_table;
DROP TABLE sqlitestudio_temp_table;
CREATE INDEX IX_EFACSnapshot_ClientId ON EFACSnapshot (
""ClientId""
);
CREATE INDEX IX_EFACSnapshot_CurrentViewAngleId ON EFACSnapshot (
""CurrentViewAngleId""
);
CREATE INDEX IX_EFACSnapshot_HitDestinationId ON EFACSnapshot (
""HitDestinationId""
);
CREATE INDEX IX_EFACSnapshot_HitOriginId ON EFACSnapshot (
""HitOriginId""
);
CREATE INDEX IX_EFACSnapshot_LastStrainAngleId ON EFACSnapshot (
""LastStrainAngleId""
);
CREATE INDEX IX_EFACSnapshot_ServerId ON EFACSnapshot (
""_ServerId""
);
PRAGMA foreign_keys = 1;
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace Data.Migrations.Sqlite
{
public partial class AddHitLocationReferenceToEFACSnapshot : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HitLocationReference",
table: "EFACSnapshot",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HitLocationReference",
table: "EFACSnapshot");
}
}
}

View File

@ -145,6 +145,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("Weapon")
.HasColumnType("INTEGER");
b.Property<string>("WeaponReference")
.HasColumnType("TEXT");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
@ -236,6 +239,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("HitLocation")
.HasColumnType("INTEGER");
b.Property<string>("HitLocationReference")
.HasColumnType("TEXT");
b.Property<int>("HitOriginId")
.HasColumnType("INTEGER");
@ -254,6 +260,9 @@ namespace Data.Migrations.Sqlite
b.Property<double>("RecoilOffset")
.HasColumnType("REAL");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<double>("SessionAngleOffset")
.HasColumnType("REAL");
@ -278,6 +287,9 @@ namespace Data.Migrations.Sqlite
b.Property<int>("WeaponId")
.HasColumnType("INTEGER");
b.Property<string>("WeaponReference")
.HasColumnType("TEXT");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
@ -293,6 +305,8 @@ namespace Data.Migrations.Sqlite
b.HasIndex("LastStrainAngleId");
b.HasIndex("ServerId");
b.ToTable("EFACSnapshot");
});
@ -1102,6 +1116,10 @@ namespace Data.Migrations.Sqlite
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Data.Models.Server.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b =>

View File

@ -18,7 +18,9 @@ namespace Data.Models.Client
public int HitLoc { get; set; }
public int DeathType { get; set; }
public int Damage { get; set; }
[Obsolete]
public int Weapon { get; set; }
public string WeaponReference { get; set; }
public Vector3 KillOrigin { get; set; }
public Vector3 DeathOrigin { get; set; }
public Vector3 ViewAngles { get; set; }

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Numerics;
using Data.Models.Server;
namespace Data.Models.Client.Stats
{
@ -17,7 +18,9 @@ namespace Data.Models.Client.Stats
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public EFClient Client { get; set; }
public long? ServerId { get; set; }
[ForeignKey(nameof(ServerId))]
public EFServer Server { get; set; }
public DateTime When { get; set; }
public int CurrentSessionLength { get; set; }
public int TimeSinceLastEvent { get; set; }
@ -46,8 +49,11 @@ namespace Data.Models.Client.Stats
public int CurrentViewAngleId { get; set; }
[ForeignKey("CurrentViewAngleId")]
public Vector3 CurrentViewAngle { get; set; }
[Obsolete]
public int WeaponId { get; set; }
public string WeaponReference { get; set; }
public int HitLocation { get; set; }
public string HitLocationReference { get; set; }
public int HitType { get; set; }
public virtual ICollection<EFACSnapshotVector3> PredictedViewAngles { get; set; }
@ -55,5 +61,7 @@ namespace Data.Models.Client.Stats
public string CapturedViewAngles => PredictedViewAngles?.Count > 0 ?
string.Join(", ", PredictedViewAngles.OrderBy(_angle => _angle.ACSnapshotVector3Id).Select(_angle => _angle.Vector.ToString())) :
"";
[NotMapped] public string ServerName => Server?.HostName ?? "--";
}
}

View File

@ -378,8 +378,6 @@ Global
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x64.Build.0 = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.ActiveCfg = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Debug|x86.Build.0 = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU
@ -394,6 +392,8 @@ Global
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x64.Build.0 = Release|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.ActiveCfg = Release|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Release|x86.Build.0 = Release|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{A9348433-58C1-4B9C-8BB7-088B02529D9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -402,8 +402,6 @@ Global
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x64.Build.0 = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.ActiveCfg = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Debug|x86.Build.0 = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.ActiveCfg = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Mixed Platforms.Build.0 = Debug|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|x64.ActiveCfg = Debug|Any CPU
@ -418,6 +416,8 @@ Global
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x64.Build.0 = Release|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.ActiveCfg = Release|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Release|x86.Build.0 = Release|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.ActiveCfg = Prerelease|Any CPU
{9512295B-3045-40E0-9B7E-2409F2173E9D}.Prerelease|Any CPU.Build.0 = Prerelease|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -4,6 +4,12 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Integrations.Cod</AssemblyName>
<RootNamespace>Integrations.Cod</RootNamespace>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,48 @@
using System.Text;
namespace Integrations.Source.Extensions
{
public static class SourceExtensions
{
public static string ReplaceUnfriendlyCharacters(this string source)
{
var result = new StringBuilder();
var quoteStart = false;
var quoteIndex = 0;
var index = 0;
foreach (var character in source)
{
if (character == '%')
{
result.Append('‰');
}
else if ((character == '"' || character == '\'') && index + 1 != source.Length)
{
if (quoteIndex > 0)
{
result.Append(!quoteStart ? "«" : "»");
quoteStart = !quoteStart;
}
else
{
result.Append('"');
}
quoteIndex++;
}
else
{
result.Append(character);
}
index++;
}
return result.ToString();
}
}
}

View File

@ -4,6 +4,12 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Integrations.Source</AssemblyName>
<RootNamespace>Integrations.Source</RootNamespace>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Prerelease' ">
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>

View File

@ -1,6 +1,9 @@
using System.Linq;
using System;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Integrations.Source.Extensions;
using Integrations.Source.Interfaces;
using Microsoft.Extensions.Logging;
using RconSharp;
@ -20,82 +23,161 @@ namespace Integrations.Source
private readonly string _hostname;
private readonly int _port;
private readonly IRConClientFactory _rconClientFactory;
private readonly SemaphoreSlim _activeQuery;
private static readonly TimeSpan FloodDelay = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan ConnectionTimeout = TimeSpan.FromSeconds(30);
private DateTime _lastQuery = DateTime.Now;
private RconClient _rconClient;
private bool _authenticated;
private bool _needNewSocket = true;
public SourceRConConnection(ILogger<SourceRConConnection> logger, IRConClientFactory rconClientFactory,
string hostname, int port, string password)
{
_rconClient = rconClientFactory.CreateClient(hostname, port);
_rconClientFactory = rconClientFactory;
_password = password;
_hostname = hostname;
_port = port;
_logger = logger;
_activeQuery = new SemaphoreSlim(1, 1);
}
~SourceRConConnection()
{
_activeQuery.Dispose();
}
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "")
{
await _rconClient.ConnectAsync();
bool authenticated;
try
{
authenticated = await _rconClient.AuthenticateAsync(_password);
}
catch (SocketException ex)
{
// occurs when the server comes back from hibernation
// this is probably a bug in the library
if (ex.ErrorCode == 10053 || ex.ErrorCode == 10054)
await _activeQuery.WaitAsync();
await WaitForAvailable();
if (_needNewSocket)
{
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
try
{
_logger.LogWarning(ex,
"Server appears to resumed from hibernation, so we are using a new socket");
_rconClient?.Disconnect();
}
catch
{
// ignored
}
_rconClient = _rconClientFactory.CreateClient(_hostname, _port);
_authenticated = false;
_needNewSocket = false;
}
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogError("Could not login to server");
_logger.LogDebug("Connecting to RCon socket");
}
throw new NetworkException("Could not authenticate with server");
await TryConnectAndAuthenticate().WithTimeout(ConnectionTimeout);
var multiPacket = false;
if (type == StaticHelpers.QueryType.COMMAND_STATUS)
{
parameters = "status";
multiPacket = true;
}
parameters = parameters.ReplaceUnfriendlyCharacters();
parameters = parameters.StripColors();
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogDebug("Sending query {Type} with parameters \"{Parameters}\"", type, parameters);
}
var response = await _rconClient.ExecuteCommandAsync(parameters, multiPacket)
.WithTimeout(ConnectionTimeout);
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogDebug("Received RCon response {Response}", response);
}
var split = response.TrimEnd('\n').Split('\n');
return split.Take(split.Length - 1).ToArray();
}
if (!authenticated)
catch (TaskCanceledException)
{
_needNewSocket = true;
throw new NetworkException("Timeout while attempting to communicate with server");
}
catch (SocketException ex)
{
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogError("Could not login to server");
_logger.LogError(ex, "Socket exception encountered while attempting to communicate with server");
}
throw new ServerException("Could not authenticate to server with provided password");
_needNewSocket = true;
throw new NetworkException("Socket exception encountered while attempting to communicate with server");
}
if (type == StaticHelpers.QueryType.COMMAND_STATUS)
catch (Exception ex) when (ex.GetType() != typeof(NetworkException) &&
ex.GetType() != typeof(ServerException))
{
parameters = "status";
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogError(ex, "Could not execute RCon query {Parameters}", parameters);
}
throw new NetworkException("Unable to communicate with server");
}
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
finally
{
_logger.LogDebug("Sending query {Type} with parameters {Parameters}", type, parameters);
if (_activeQuery.CurrentCount == 0)
{
_activeQuery.Release();
}
_lastQuery = DateTime.Now;
}
}
var response = await _rconClient.ExecuteCommandAsync(parameters.StripColors(), true);
using (LogContext.PushProperty("Server", $"{_rconClient.Host}:{_rconClient.Port}"))
private async Task WaitForAvailable()
{
var diff = DateTime.Now - _lastQuery;
if (diff < FloodDelay)
{
_logger.LogDebug("Received RCon response {Response}", response);
await Task.Delay(FloodDelay - diff);
}
}
var split = response.TrimEnd('\n').Split('\n');
return split.Take(split.Length - 1).ToArray();
private async Task TryConnectAndAuthenticate()
{
if (!_authenticated)
{
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogDebug("Authenticating to RCon socket");
}
await _rconClient.ConnectAsync().WithTimeout(ConnectionTimeout);
_authenticated = await _rconClient.AuthenticateAsync(_password).WithTimeout(ConnectionTimeout);
if (!_authenticated)
{
using (LogContext.PushProperty("Server", $"{_hostname}:{_port}"))
{
_logger.LogError("Could not login to server");
}
throw new ServerException("Could not authenticate to server with provided password");
}
}
}
public void SetConfiguration(IRConParser config)

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -3,7 +3,7 @@ let eventParser;
const plugin = {
author: 'RaidMax',
version: 0.1,
version: 0.2,
name: 'CS:GO Parser',
engine: 'Source',
isParser: true,
@ -12,8 +12,8 @@ const plugin = {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.engine);
eventParser = manager.GenerateDynamicEventParser(this.engine);
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.RConEngine = this.engine;
rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr';
@ -24,18 +24,18 @@ const plugin = {
rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$';
rconParser.Configuration.MapStatus.AddMapping(113, 1);
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+';
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+';
rconParser.Configuration.MapStatus.AddMapping(114, 1);
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$';
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$';
rconParser.Configuration.Dvar.AddMapping(106, 1);
rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.Dvar.AddMapping(108, 3);
rconParser.Configuration.Dvar.AddMapping(109, 3);
rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$';
rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) +(\\d+:\\d+(?::\\d+)?) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$';
rconParser.Configuration.Status.AddMapping(100, 2);
rconParser.Configuration.Status.AddMapping(101, 7);
rconParser.Configuration.Status.AddMapping(101, -1);
rconParser.Configuration.Status.AddMapping(102, 6);
rconParser.Configuration.Status.AddMapping(103, 4)
rconParser.Configuration.Status.AddMapping(104, 3);
@ -90,6 +90,7 @@ const plugin = {
rconParser.GameName = 10; // CSGO
eventParser.Version = 'CSGO';
eventParser.GameName = 10; // CSGO
eventParser.URLProtocolFormat = 'steam://connect/{{ip}}:{{port}}';
},
onUnloadAsync: function () {

View File

@ -3,7 +3,7 @@ let eventParser;
const plugin = {
author: 'RaidMax',
version: 0.1,
version: 0.2,
name: 'CS:GO (SourceMod) Parser',
engine: 'Source',
isParser: true,
@ -12,8 +12,8 @@ const plugin = {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.engine);
eventParser = manager.GenerateDynamicEventParser(this.engine);
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.RConEngine = this.engine;
rconParser.Configuration.StatusHeader.Pattern = 'userid +name +uniqueid +connected +ping +loss +state +rate +adr';
@ -24,18 +24,18 @@ const plugin = {
rconParser.Configuration.HostnameStatus.Pattern = '^hostname: +(.+)$';
rconParser.Configuration.MapStatus.AddMapping(113, 1);
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d humans, \\d bots \\((\\d+).+';
rconParser.Configuration.MaxPlayersStatus.Pattern = '^players *: +\\d+ humans, \\d+ bots \\((\\d+).+';
rconParser.Configuration.MapStatus.AddMapping(114, 1);
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = (?:"(.+)" (?:\\( def\\. "(.*)" \\))|"(.+)" +(.+)) +- (.*)$';
rconParser.Configuration.Dvar.Pattern = '^"(.+)" = "(.+)" (?:\\( def. "(.*)" \\))?(?: |\\w)+- (.+)$';
rconParser.Configuration.Dvar.AddMapping(106, 1);
rconParser.Configuration.Dvar.AddMapping(107, 2);
rconParser.Configuration.Dvar.AddMapping(108, 3);
rconParser.Configuration.Dvar.AddMapping(109, 3);
rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) (\\d+:\\d+) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$';
rconParser.Configuration.Status.Pattern = '^#\\s*(\\d+) (\\d+) "(.+)" (\\S+) +(\\d+:\\d+(?::\\d+)?) (\\d+) (\\S+) (\\S+) (\\d+) (\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+)$';
rconParser.Configuration.Status.AddMapping(100, 2);
rconParser.Configuration.Status.AddMapping(101, 7);
rconParser.Configuration.Status.AddMapping(101, -1);
rconParser.Configuration.Status.AddMapping(102, 6);
rconParser.Configuration.Status.AddMapping(103, 4)
rconParser.Configuration.Status.AddMapping(104, 3);
@ -64,7 +64,7 @@ const plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'sm_kick #{0} {1}';
rconParser.Configuration.CommandPrefixes.TempBan = 'sm_kick #{0} {1}';
rconParser.Configuration.CommandPrefixes.Say = 'sm_say {0}';
rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} {1}';
rconParser.Configuration.CommandPrefixes.Tell = 'sm_psay #{0} "{1}"';
eventParser.Configuration.Say.Pattern = '^"(.+)<(\\d+)><(.+)><(.*?)>" say "(.*)"$';
eventParser.Configuration.Say.AddMapping(5, 1);
@ -90,6 +90,7 @@ const plugin = {
rconParser.GameName = 10; // CSGO
eventParser.Version = 'CSGOSM';
eventParser.GameName = 10; // CSGO
eventParser.URLProtocolFormat = 'steam://connect/{{ip}}:{{port}}';
},
onUnloadAsync: function () {

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.7,
version: 0.8,
name: 'Plutonium IW5 Parser',
isParser: true,
@ -35,6 +35,7 @@ var plugin = {
rconParser.Configuration.Status.AddMapping(103, 5);
rconParser.Configuration.Status.AddMapping(104, 6);
rconParser.IsOneLog = true;
rconParser.Version = 'IW5 MP 1.9 build 388110 Fri Sep 14 00:04:28 2012 win-x86';
rconParser.GameName = 3; // IW5
eventParser.Version = 'IW5 MP 1.9 build 388110 Fri Sep 14 00:04:28 2012 win-x86';

View File

@ -38,7 +38,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
double AngleDifferenceAverage;
EFClientStatistics ClientStats;
long LastOffset;
IW4Info.WeaponName LastWeapon;
string LastWeapon;
ILogger Log;
Strain Strain;
readonly DateTime ConnectionTime = DateTime.UtcNow;
@ -111,7 +111,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
hit.DeathType != (int)IW4Info.MeansOfDeath.MOD_HEAD_SHOT) ||
hit.HitLoc == (int)IW4Info.HitLocation.none || hit.TimeOffset - LastOffset < 0 ||
// hack: prevents false positives
((int)LastWeapon != hit.Weapon && (hit.TimeOffset - LastOffset) == 50))
(LastWeapon != hit.WeaponReference && (hit.TimeOffset - LastOffset) == 50))
{
return new[] {new DetectionPenaltyResult()
{
@ -119,7 +119,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
}};
}
LastWeapon = (IW4Info.WeaponName)(hit.Weapon);
LastWeapon = hit.WeaponReference;
HitLocationCount[(IW4Info.HitLocation)hit.HitLoc].Count++;
HitCount++;
@ -309,7 +309,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
try
{
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Recoil]
.Any(_weaponRegex => Regex.IsMatch(((IW4Info.WeaponName)(hit.Weapon)).ToString(), _weaponRegex));
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
}
catch (KeyNotFoundException)
@ -341,7 +341,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
{
shouldIgnoreDetection = false;
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Button]
.Any(_weaponRegex => Regex.IsMatch(((IW4Info.WeaponName)(hit.Weapon)).ToString(), _weaponRegex));
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
}
catch (KeyNotFoundException)
@ -454,7 +454,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
{
shouldIgnoreDetection = false; // reset previous value
shouldIgnoreDetection = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredDetectionSpecification[(Server.Game)hit.GameName][DetectionType.Chest]
.Any(_weaponRegex => Regex.IsMatch(((IW4Info.WeaponName)(hit.Weapon)).ToString(), _weaponRegex));
.Any(_weaponRegex => Regex.IsMatch(hit.WeaponReference, _weaponRegex));
}
catch (KeyNotFoundException)
@ -506,6 +506,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
{
When = hit.When,
ClientId = ClientStats.ClientId,
ServerId = ClientStats.ServerId,
SessionAngleOffset = AngleDifferenceAverage,
RecoilOffset = hitRecoilAverage,
CurrentSessionLength = (int)(DateTime.UtcNow - ConnectionTime).TotalMinutes,
@ -527,7 +528,7 @@ namespace IW4MAdmin.Plugins.Stats.Cheat
SessionSPM = Math.Round(ClientStats.SessionSPM, 0),
StrainAngleBetween = Strain.LastDistance,
TimeSinceLastEvent = (int)Strain.LastDeltaTime,
WeaponId = hit.Weapon,
WeaponReference = hit.WeaponReference,
SessionSnapHits = sessionSnapHits,
SessionAverageSnapValue = sessionAverageSnapAmount
};

View File

@ -11,6 +11,7 @@ using Data.Models.Client.Stats.Reference;
using Data.Models.Server;
using IW4MAdmin.Plugins.Stats.Client.Abstractions;
using IW4MAdmin.Plugins.Stats.Client.Game;
using IW4MAdmin.Plugins.Stats.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
@ -147,7 +148,7 @@ namespace IW4MAdmin.Plugins.Stats.Client
foreach (var client in gameEvent.Owner.GetClientsAsList())
{
var scores = client.GetAdditionalProperty<List<(int, DateTime)>>(SessionScores);
scores?.Add((client.Score, DateTime.Now));
scores?.Add((client.GetAdditionalProperty<int?>(StatManager.ESTIMATED_SCORE) ?? client.Score, DateTime.Now));
}
}
@ -590,7 +591,7 @@ namespace IW4MAdmin.Plugins.Stats.Client
if (sessionScores == null)
{
_logger.LogWarning($"No session scores available for {client}");
_logger.LogWarning("No session scores available for {Client}", client.ToString());
return;
}
@ -600,7 +601,7 @@ namespace IW4MAdmin.Plugins.Stats.Client
if (sessionScores.Count == 0)
{
stat.Score += client.Score;
stat.Score += client.Score > 0 ? client.Score : client.GetAdditionalProperty<int?>(Helpers.StatManager.ESTIMATED_SCORE) ?? 0 * 50;
}
else

View File

@ -2,7 +2,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore;
using System.Collections.Generic;
using Data.Abstractions;
@ -16,11 +15,12 @@ namespace IW4MAdmin.Plugins.Stats.Commands
{
class MostPlayedCommand : Command
{
public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory)
public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory)
{
long serverId = StatManager.GetIdForServer(s);
var serverId = StatManager.GetIdForServer(s);
List<string> mostPlayed = new List<string>()
var mostPlayed = new List<string>()
{
$"^5--{translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_TEXT"]}--"
};
@ -29,25 +29,28 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
var iqStats = (from stats in context.Set<EFClientStatistics>()
join client in context.Clients
on stats.ClientId equals client.ClientId
join alias in context.Aliases
on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId
where client.Level != EFClient.Permission.Banned
where client.LastConnection >= thirtyDaysAgo
orderby stats.TimePlayed descending
select new
{
alias.Name,
client.TotalConnectionTime,
stats.Kills
})
.Take(5);
join client in context.Clients
on stats.ClientId equals client.ClientId
join alias in context.Aliases
on client.CurrentAliasId equals alias.AliasId
where stats.ServerId == serverId
where client.Level != EFClient.Permission.Banned
where client.LastConnection >= thirtyDaysAgo
orderby stats.TimePlayed descending
select new
{
alias.Name,
stats.TimePlayed,
stats.Kills
})
.Take(5);
var iqList = await iqStats.ToListAsync();
mostPlayed.AddRange(iqList.Select(stats => translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills, (DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TotalConnectionTime)).HumanizeForCurrentCulture())));
mostPlayed.AddRange(iqList.Select((stats, index) =>
$"#{index + 1} " + translationLookup["COMMANDS_MOST_PLAYED_FORMAT"].FormatExt(stats.Name, stats.Kills,
(DateTime.UtcNow - DateTime.UtcNow.AddSeconds(-stats.TimePlayed))
.HumanizeForCurrentCulture())));
return mostPlayed;
@ -57,7 +60,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
private readonly IDatabaseContextFactory _contextFactory;
public MostPlayedCommand(CommandConfiguration config, ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{
Name = "mostplayed";
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTPLAYED_DESC"];
@ -88,4 +91,4 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
}
}
}
}

View File

@ -0,0 +1,94 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats.Cheat;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Stats.Commands
{
public class ResetAnticheatMetricsCommand : Command
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
public ResetAnticheatMetricsCommand(ILogger<ResetAnticheatMetricsCommand> logger, CommandConfiguration config,
ITranslationLookup translationLookup,
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
{
Name = "resetanticheat";
Description = translationLookup["PLUGINS_STATS_COMMANDS_RESETAC_DESC"];
Alias = "rsa";
Permission = EFClient.Permission.Owner;
RequiresTarget = true;
_contextFactory = contextFactory;
_logger = logger;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
try
{
var clientDetection =
gameEvent.Target.GetAdditionalProperty<Detection>(IW4MAdmin.Plugins.Stats.Helpers.StatManager
.CLIENT_DETECTIONS_KEY);
var clientStats =
gameEvent.Target.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers
.StatManager.CLIENT_STATS_KEY);
if (clientStats != null)
{
clientStats.MaxStrain = 0;
clientStats.AverageSnapValue = 0;
clientStats.SnapHitCount = 0;
clientStats.HitLocations.Clear();
}
clientDetection?.TrackedHits.Clear();
await using var context = _contextFactory.CreateContext();
var hitLocationCounts = await context.Set<EFHitLocationCount>()
.Where(loc => loc.EFClientStatisticsClientId == gameEvent.Target.ClientId)
.Select(loc => new EFHitLocationCount()
{
HitLocationCountId = loc.HitLocationCountId
})
.ToListAsync();
context.RemoveRange(hitLocationCounts);
await context.SaveChangesAsync();
var stats = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId == gameEvent.Target.ClientId)
.ToListAsync();
foreach (var stat in stats)
{
stat.MaxStrain = 0;
stat.AverageSnapValue = 0;
stat.SnapHitCount = 0;
}
context.UpdateRange(stats);
await context.SaveChangesAsync();
gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_RESETAC_SUCCESS"]);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not reset anticheat metrics for {Target}", gameEvent.Target);
throw;
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
};
var stats = await Plugin.Manager.GetTopStats(0, 5, serverId);
var statsList = stats.Select(stats => $"^3{stats.Name}^7 - ^5{stats.KDR} ^7{translationLookup["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}");
var statsList = stats.Select((stats, index) => $"#{index + 1} ^3{stats.Name}^7 - ^5{stats.KDR} ^7{translationLookup["PLUGINS_STATS_TEXT_KDR"]} | ^5{stats.Performance} ^7{translationLookup["PLUGINS_STATS_COMMANDS_PERFORMANCE"]}");
topStatsText.AddRange(statsList);

View File

@ -53,13 +53,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var serverId = StatManager.GetIdForServer(E.Owner);
var totalRankedPlayers = await Plugin.Manager.GetTotalRankedPlayers(serverId);
// getting stats for a particular client
if (E.Target != null)
{
var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Target.ClientId, serverId);
var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}/{totalRankedPlayers}";
// target is currently connected so we want their cached stats if they exist
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Target)))
@ -87,7 +89,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
var performanceRanking = await Plugin.Manager.GetClientOverallRanking(E.Origin.ClientId, serverId);
var performanceRankingString = performanceRanking == 0
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}";
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} #{performanceRanking}/{totalRankedPlayers}";
// check if current client is connected to the server
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin)))

View File

@ -38,6 +38,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private static List<EFServer> serverModels;
public static string CLIENT_STATS_KEY = "ClientStats";
public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
public static string ESTIMATED_SCORE = "EstimatedScore";
private readonly SemaphoreSlim _addPlayerWaiter = new SemaphoreSlim(1, 1);
private readonly IServerDistributionCalculator _serverDistributionCalculator;
@ -112,19 +113,33 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0;
}
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, long? serverId = null)
{
return (ranking) => ranking.ServerId == serverId
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
&& ranking.Client.LastConnection >= Extensions.FifteenDaysAgo()
&& ranking.ZScore != null
&& ranking.PerformanceMetric != null
&& ranking.Newest
&& ranking.Client.TotalConnectionTime >=
_configHandler.Configuration().TopPlayersMinPlayTime;
}
public async Task<int> GetTotalRankedPlayers(long serverId)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
return await context.Set<EFClientRankingHistory>()
.Where(GetNewRankingFunc(serverId: serverId))
.CountAsync();
}
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
{
await using var context = _contextFactory.CreateContext(false);
var clientIdsList = await context.Set<EFClientRankingHistory>()
.Where(ranking => ranking.ServerId == serverId)
.Where(ranking => ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned)
.Where(ranking => ranking.Client.LastConnection >= Extensions.FifteenDaysAgo())
.Where(ranking => ranking.ZScore != null)
.Where(ranking => ranking.PerformanceMetric != null)
.Where(ranking => ranking.Newest)
.Where(ranking =>
ranking.Client.TotalConnectionTime >= _configHandler.Configuration().TopPlayersMinPlayTime)
.Where(GetNewRankingFunc(serverId: serverId))
.OrderByDescending(ranking => ranking.PerformanceMetric)
.Select(ranking => ranking.ClientId)
.Skip(start)
@ -533,7 +548,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// sync their stats before they leave
if (clientStats != null)
{
clientStats = UpdateStats(clientStats);
clientStats = UpdateStats(clientStats, pl);
await SaveClientStats(clientStats);
if (_configHandler.Configuration().EnableAdvancedMetrics)
{
@ -609,7 +624,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = int.Parse(damage),
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
Weapon = (int) ParseEnum<IW4Info.WeaponName>.Get(weapon, typeof(IW4Info.WeaponName)),
WeaponReference = weapon,
ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset),
When = time,
@ -859,7 +874,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// update the total stats
_servers[serverId].ServerStatistics.TotalKills += 1;
// this happens when the round has changed
if (attackerStats.SessionScore == 0)
{
@ -871,18 +886,28 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
victimStats.LastScore = 0;
}
attackerStats.SessionScore = attacker.Score;
victimStats.SessionScore = victim.Score;
var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? attacker.Score
: attackerStats.SessionKills * 50;
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? victim.Score
: victimStats.SessionKills * 50;
attackerStats.SessionScore = estimatedAttackerScore;
victimStats.SessionScore = estimatedVictimScore;
attacker.SetAdditionalProperty(ESTIMATED_SCORE, estimatedAttackerScore);
victim.SetAdditionalProperty(ESTIMATED_SCORE, estimatedVictimScore);
// calculate for the clients
CalculateKill(attackerStats, victimStats);
CalculateKill(attackerStats, victimStats, attacker, victim);
// this should fix the negative SPM
// updates their last score after being calculated
attackerStats.LastScore = attacker.Score;
victimStats.LastScore = victim.Score;
attackerStats.LastScore = estimatedAttackerScore;
victimStats.LastScore = estimatedVictimScore;
// show encouragement/discouragement
string streakMessage = (attackerStats.ClientId != victimStats.ClientId)
var streakMessage = (attackerStats.ClientId != victimStats.ClientId)
? StreakMessage.MessageOnStreak(attackerStats.KillStreak, attackerStats.DeathStreak)
: StreakMessage.MessageOnStreak(-1, -1);
@ -1227,7 +1252,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// </summary>
/// <param name="attackerStats">Stats of the attacker</param>
/// <param name="victimStats">Stats of the victim</param>
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats)
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
EFClient attacker, EFClient victim)
{
bool suicide = attackerStats.ClientId == victimStats.ClientId;
@ -1246,43 +1272,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
victimStats.KillStreak = 0;
// process the attacker's stats after the kills
attackerStats = UpdateStats(attackerStats);
#region DEPRECATED
/* var validAttackerLobbyRatings = Servers[attackerStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != attackerStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != attackerStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
double attackerLobbyRating = validAttackerLobbyRatings.Count() > 0 ?
validAttackerLobbyRatings.Average(cs => cs.Value.EloRating) :
attackerStats.EloRating;
var validVictimLobbyRatings = Servers[victimStats.ServerId].PlayerStats
.Where(cs => cs.Value.ClientId != victimStats.ClientId)
.Where(cs =>
Servers[attackerStats.ServerId].IsTeamBased ?
cs.Value.Team != victimStats.Team :
cs.Value.Team != IW4Info.Team.Spectator)
.Where(cs => cs.Value.Team != IW4Info.Team.Spectator);
double victimLobbyRating = validVictimLobbyRatings.Count() > 0 ?
validVictimLobbyRatings.Average(cs => cs.Value.EloRating) :
victimStats.EloRating;*/
#endregion
attackerStats = UpdateStats(attackerStats, attacker);
// calculate elo
double attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
Math.Log(Math.Max(1, attackerStats.EloRating));
double winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
// double victimEloDifference = Math.Log(Math.Max(1, attackerStats.EloRating)) - Math.Log(Math.Max(1, victimStats.EloRating));
// double lossPercentage = 1.0 / (1 + Math.Pow(10, victimEloDifference/ Math.E));
var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
attackerStats.EloRating += 6.0 * (1 - winPercentage);
victimStats.EloRating -= 6.0 * (1 - winPercentage);
@ -1302,7 +1297,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// </summary>
/// <param name="clientStats">Client statistics</param>
/// <returns></returns>
private EFClientStatistics UpdateStats(EFClientStatistics clientStats)
private EFClientStatistics UpdateStats(EFClientStatistics clientStats, EFClient client)
{
// prevent NaN or inactive time lowering SPM
if ((DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0 < 0.01 ||
@ -1314,10 +1309,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return clientStats;
}
double timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
double timeSinceLastActive = (DateTime.UtcNow - clientStats.LastActive).TotalSeconds / 60.0;
var timeSinceLastCalc = (DateTime.UtcNow - clientStats.LastStatCalculation).TotalSeconds / 60.0;
int scoreDifference = 0;
var scoreDifference = 0;
// this means they've been tking or suicide and is the only time they can have a negative SPM
if (clientStats.RoundScore < 0)
{
@ -1329,17 +1323,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
scoreDifference = clientStats.RoundScore - clientStats.LastScore;
}
double killSPM = scoreDifference / timeSinceLastCalc;
double spmMultiplier = 2.934 *
var killSpm = scoreDifference / timeSinceLastCalc;
var spmMultiplier = 2.934 *
Math.Pow(
_servers[clientStats.ServerId]
.TeamCount((IW4Info.Team) 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;
clientStats.SessionSPM = clientStats.SessionScore / Math.Max(1, client.ConnectionLength / 60.0);
// calculate how much the KDR should weigh
// 1.637 is a Eddie-Generated number that weights the KDR nicely
@ -1358,7 +1352,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
// calculate the new weight against average times the weight against play time
clientStats.SPM = (killSPM * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
clientStats.SPM = (killSpm * SPMAgainstPlayWeight) + (clientStats.SPM * (1 - SPMAgainstPlayWeight));
if (clientStats.SPM < 0)
{
@ -1373,7 +1367,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
{
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
new {killSPM, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
clientStats.SPM = 0;
clientStats.Skill = 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -83,6 +83,7 @@ namespace IW4MAdmin.Plugins.Stats
await Manager.Sync(S);
break;
case GameEvent.EventType.MapEnd:
Manager.ResetKillstreaks(S);
await Manager.Sync(S);
break;
case GameEvent.EventType.Command:

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.3.19.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2021.6.29.1" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -61,7 +61,7 @@ namespace SharedLibraryCore
Client ??= new EFClient()
{
ClientId = -1,
Level = EFClient.Permission.User,
Level = EFClient.Permission.Banned,
CurrentAlias = new EFAlias() { Name = "Webfront Guest" }
};
}

View File

@ -11,7 +11,7 @@ namespace SharedLibraryCore.Commands
{
public class CommandProcessing
{
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig)
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig, CommandConfiguration commandConfig)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var Manager = E.Owner.Manager;
@ -40,7 +40,11 @@ namespace SharedLibraryCore.Commands
C.IsBroadcast = isBroadcast;
if (!C.AllowImpersonation && E.ImpersonationOrigin != null)
var allowImpersonation = commandConfig?.Commands?.ContainsKey(C.GetType().Name) ?? false
? commandConfig.Commands[C.GetType().Name].AllowImpersonation
: C.AllowImpersonation;
if (!allowImpersonation && E.ImpersonationOrigin != null)
{
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
throw new CommandException($"Command {C.Name} cannot be run as another client");

View File

@ -1359,13 +1359,13 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var Response = await E.Owner.ExecuteCommandAsync(E.Data.Trim());
foreach (string S in Response)
var response = await E.Owner.ExecuteCommandAsync(E.Data.Trim());
foreach (var item in response)
{
E.Origin.Tell(S);
E.Origin.Tell(item);
}
if (Response.Length == 0)
if (response.Length == 0)
{
E.Origin.Tell(_translationLookup["COMMANDS_RCON_SUCCESS"]);
}

View File

@ -146,6 +146,7 @@ namespace SharedLibraryCore.Configuration
[UIHint("ServerConfiguration")]
public ServerConfiguration[] Servers { get; set; }
[ConfigurationIgnore] public int MinimumNameLength { get; set; } = 3;
[ConfigurationIgnore] public string Id { get; set; }
[ConfigurationIgnore] public string SubscriptionId { get; set; }
[ConfigurationIgnore] public MapConfiguration[] Maps { get; set; }

View File

@ -65,7 +65,7 @@ namespace SharedLibraryCore.Configuration
{
RConParserVersion = rconParsers.FirstOrDefault(_parser => _parser.Name == selection.Item2)?.Version;
if (selection.Item1 > 0 && !rconParsers[selection.Item1 - 1].CanGenerateLogPath)
if (selection.Item1 > 0 && !rconParsers[selection.Item1].CanGenerateLogPath)
{
Console.WriteLine(loc["SETUP_SERVER_NO_LOG"]);
ManualLogPath = Utilities.PromptString(loc["SETUP_SERVER_LOG_PATH"]);

View File

@ -28,5 +28,6 @@ namespace SharedLibraryCore.Dtos
public string LastConnectionText => (DateTime.UtcNow - LastConnection).HumanizeForCurrentCulture();
public IDictionary<int, long> LinkedAccounts { get; set; }
public MetaType? MetaFilterType { get; set; }
public double? ZScore { get; set; }
}
}

View File

@ -23,5 +23,21 @@ namespace SharedLibraryCore.Dtos
public string IPAddress { get; set; }
public bool IsPasswordProtected { get; set; }
public string Endpoint => $"{IPAddress}:{Port}";
public double? LobbyZScore
{
get
{
var valid = Players.Where(player => player.ZScore != null && player.ZScore != 0)
.ToList();
if (!valid.Any())
{
return null;
}
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
}
}
}
}
}

View File

@ -151,6 +151,14 @@ namespace SharedLibraryCore
/// a client's permission was changed
/// </summary>
ChangePermission = 111,
/// <summary>
/// client logged in to webfront
/// </summary>
Login = 112,
/// <summary>
/// client logged out of webfront
/// </summary>
Logout = 113,
// events "generated" by IW4MAdmin
/// <summary>

View File

@ -71,6 +71,12 @@ namespace SharedLibraryCore.Interfaces
/// eg: COD, Source
/// </summary>
string RConEngine { get; }
/// <summary>
/// indicates that the game does not log to the mods folder (when mod is loaded),
/// but rather always to the fs_basegame directory
/// </summary>
bool IsOneLog { get; }
/// <summary>
/// retrieves the value of given dvar key if it exists in the override dict

View File

@ -457,9 +457,10 @@ namespace SharedLibraryCore.Database.Models
using (LogContext.PushProperty("Server", CurrentServer?.ToString()))
{
if (string.IsNullOrWhiteSpace(Name) || CleanedName.Replace(" ", "").Length < 3)
if (string.IsNullOrWhiteSpace(Name) || CleanedName.Replace(" ", "").Length <
(CurrentServer?.Manager?.GetApplicationSettings()?.Configuration()?.MinimumNameLength ?? 3))
{
Utilities.DefaultLogger.LogInformation("Kicking {client} because their name is too short", ToString());
Utilities.DefaultLogger.LogInformation("Kicking {Client} because their name is too short", ToString());
Kick(loc["SERVER_KICK_MINNAME"], Utilities.IW4MAdminClient(CurrentServer));
return false;
}
@ -468,14 +469,14 @@ namespace SharedLibraryCore.Database.Models
.DisallowedClientNames
?.Any(_name => Regex.IsMatch(Name, _name)) ?? false)
{
Utilities.DefaultLogger.LogInformation("Kicking {client} because their name is not allowed", ToString());
Utilities.DefaultLogger.LogInformation("Kicking {Client} because their name is not allowed", ToString());
Kick(loc["SERVER_KICK_GENERICNAME"], Utilities.IW4MAdminClient(CurrentServer));
return false;
}
if (Name.Where(c => char.IsControl(c)).Count() > 0)
{
Utilities.DefaultLogger.LogInformation("Kicking {client} because their name contains control characters", ToString());
Utilities.DefaultLogger.LogInformation("Kicking {Client} because their name contains control characters", ToString());
Kick(loc["SERVER_KICK_CONTROLCHARS"], Utilities.IW4MAdminClient(CurrentServer));
return false;
}
@ -487,7 +488,7 @@ namespace SharedLibraryCore.Database.Models
CurrentServer.GetClientsAsList().Count <= CurrentServer.MaxClients &&
CurrentServer.MaxClients != 0)
{
Utilities.DefaultLogger.LogInformation("Kicking {client} their spot is reserved", ToString());
Utilities.DefaultLogger.LogInformation("Kicking {Client} their spot is reserved", ToString());
Kick(loc["SERVER_KICK_SLOT_IS_RESERVED"], Utilities.IW4MAdminClient(CurrentServer));
return false;
}

View File

@ -66,7 +66,23 @@ namespace SharedLibraryCore.Services
CurrentValue = ((EFClient.Permission)e.Extra).ToString()
};
break;
default:
case GameEvent.EventType.Login:
change = new EFChangeHistory()
{
OriginEntityId = e.Origin.ClientId,
Comment = "Logged In To Webfront",
TypeOfChange = EFChangeHistory.ChangeType.Command,
CurrentValue = e.Data
};
break;
case GameEvent.EventType.Logout:
change = new EFChangeHistory()
{
OriginEntityId = e.Origin.ClientId,
Comment = "Logged Out of Webfront",
TypeOfChange = EFChangeHistory.ChangeType.Command,
CurrentValue = e.Data
};
break;
}

View File

@ -1,66 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2020.11.18.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
<LangVersion>8.0</LangVersion>
<PackageTags>IW4MAdmin</PackageTags>
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin/</RepositoryUrl>
<PackageProjectUrl>https://www.raidmax.org/IW4MAdmin/</PackageProjectUrl>
<Copyright>2020</Copyright>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2020.11.18.1</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.1.3" />
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="Humanizer.Core.ru" Version="2.8.26" />
<PackageReference Include="Humanizer.Core.de" Version="2.8.26" />
<PackageReference Include="Humanizer.Core.es" Version="2.8.26" />
<PackageReference Include="Humanizer.Core.pt" Version="2.8.26" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.7" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Npgsql" Version="4.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.7" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="if not exist &quot;$(ProjectDir)..\BUILD&quot; (&#xD;&#xA;if $(ConfigurationName) == Debug (&#xD;&#xA;md &quot;$(ProjectDir)..\BUILD&quot;&#xD;&#xA;)&#xD;&#xA;)&#xD;&#xA;if not exist &quot;$(ProjectDir)..\BUILD\Plugins&quot; (&#xD;&#xA;if $(ConfigurationName) == Debug (&#xD;&#xA;md &quot;$(ProjectDir)..\BUILD\Plugins&quot;&#xD;&#xA;)&#xD;&#xA;)" />
</Target>
</Project>

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2021.3.5.1</Version>
<Version>2021.6.29.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2021.3.19.1</PackageVersion>
<PackageVersion>2021.6.29.1</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
@ -44,7 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.10" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.1.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.1" />
<PackageReference Include="RaidMax.IW4MAdmin.Data" Version="1.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>

View File

@ -1,5 +1,4 @@

using Humanizer;
using Humanizer;
using Humanizer.Localisation;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos.Meta;
@ -323,7 +322,16 @@ namespace SharedLibraryCore
public static long ConvertGuidToLong(this string str, NumberStyles numberStyle, long? fallback = null)
{
// added for source games that provide the steam ID
str = str.Replace("STEAM_1", "").Replace(":", "");
var match = Regex.Match(str, @"^STEAM_(\d):(\d):(\d+)$");
if (match.Success)
{
var x = int.Parse(match.Groups[1].ToString());
var y = int.Parse(match.Groups[2].ToString());
var z = long.Parse(match.Groups[3].ToString());
return z * 2 + 0x0110000100000000 + y;
}
str = str.Substring(0, Math.Min(str.Length, 19));
var parsableAsNumber = Regex.Match(str, @"([A-F]|[a-f]|[0-9])+").Value;
@ -917,6 +925,18 @@ namespace SharedLibraryCore
}
}
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
await Task.WhenAny(task, Task.Delay(timeout));
return await task;
}
public static async Task WithTimeout(this Task task, TimeSpan timeout)
{
await Task.WhenAny(task, Task.Delay(timeout));
}
public static bool ShouldHideLevel(this Permission perm) => perm == Permission.Flagged;
/// <summary>

View File

@ -119,6 +119,14 @@ namespace WebfrontCore.Controllers.API
var claimsIdentity = new ClaimsIdentity(claims, "login");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent()
{
Origin = privilegedClient,
Type = GameEvent.EventType.Login,
Owner = Manager.GetServers().First(),
Data = HttpContext.Connection.RemoteIpAddress.ToString()
});
return Ok();
}
@ -137,6 +145,17 @@ namespace WebfrontCore.Controllers.API
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> LogoutAsync()
{
if (Authorized)
{
Manager.AddEvent(new GameEvent()
{
Origin = Client,
Type = GameEvent.EventType.Logout,
Owner = Manager.GetServers().First(),
Data = HttpContext.Connection.RemoteIpAddress.ToString()
});
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
@ -50,6 +51,14 @@ namespace WebfrontCore.Controllers
var claimsIdentity = new ClaimsIdentity(claims, "login");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent()
{
Origin = privilegedClient,
Type = GameEvent.EventType.Login,
Owner = Manager.GetServers().First(),
Data = HttpContext.Connection.RemoteIpAddress.ToString()
});
return Ok();
}
@ -66,6 +75,17 @@ namespace WebfrontCore.Controllers
[HttpGet]
public async Task<IActionResult> LogoutAsync()
{
if (Authorized)
{
Manager.AddEvent(new GameEvent()
{
Origin = Client,
Type = GameEvent.EventType.Logout,
Owner = Manager.GetServers().First(),
Data = HttpContext.Connection.RemoteIpAddress.ToString()
});
}
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToAction("Index", "Home");
}

View File

@ -183,6 +183,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
.Include(s => s.HitOrigin)
.Include(s => s.HitDestination)
.Include(s => s.CurrentViewAngle)
.Include(s => s.Server)
.Include(s => s.PredictedViewAngles)
.ThenInclude(_angles => _angles.Vector)
.OrderBy(s => s.When)

View File

@ -3,6 +3,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Linq;
using Data.Models.Client.Stats;
namespace WebfrontCore.Controllers
{
@ -39,7 +40,8 @@ namespace WebfrontCore.Controllers
Name = p.Name,
ClientId = p.ClientId,
Level = p.Level.ToLocalizedLevelName(),
LevelInt = (int)p.Level
LevelInt = (int)p.Level,
ZScore = p.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers.StatManager.CLIENT_STATS_KEY)?.ZScore
}).ToList(),
ChatHistory = s.ChatHistory.ToList(),
PlayerHistory = s.ClientHistory.ToArray(),

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Middleware;

View File

@ -113,6 +113,7 @@ namespace WebfrontCore
services.AddSingleton<IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo>, AdvancedClientStatsResourceQueryHelper>();
services.AddSingleton(typeof(IDataValueCache<,>), typeof(DataValueCache<,>));
// todo: this needs to be handled more gracefully
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ILoggerFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IConfigurationHandlerFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IDatabaseContextFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IAuditInformationRepository>());

View File

@ -3,6 +3,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using System.Linq;
using System.Net;
using Data.Models.Client.Stats;
using static SharedLibraryCore.Server;
namespace WebfrontCore.ViewComponents
@ -30,7 +31,8 @@ namespace WebfrontCore.ViewComponents
ClientId = p.ClientId,
Level = p.Level.ToLocalizedLevelName(),
LevelInt = (int)p.Level,
Tag = p.Tag
Tag = p.Tag,
ZScore = p.GetAdditionalProperty<EFClientStatistics>(IW4MAdmin.Plugins.Stats.Helpers.StatManager.CLIENT_STATS_KEY)?.ZScore
}).ToList(),
ChatHistory = s.ChatHistory.ToList(),
Online = !s.Throttled,

View File

@ -22,8 +22,8 @@
}
</div>
<!-- Name/Level Column -->
<div class="w-75 d-block d-lg-inline-flex flex-column flex-fill text-center text-lg-left pb-3 pb-lg-0 pt-3 pt-lg-0 pl-3 pr-3 ml-auto mr-auto" style="overflow-wrap: anywhere">
<div class="mt-n2 flex-fill d-block d-lg-inline-flex">
<div class="w-50 d-block d-lg-inline-flex flex-column flex-fill text-center text-lg-left pb-3 pb-lg-0 pt-3 pt-lg-0 pl-3 pr-3 ml-auto mr-auto" style="overflow-wrap: anywhere">
<div class="mt-n2 d-block d-lg-inline-flex @(ViewBag.Authorized ? "" : "flex-fill")">
<div id="profile_name" class="client-name h1 mb-0">
<color-code value="@Model.Name" allow="@ViewBag.EnableColorCodes"></color-code>
</div>
@ -35,6 +35,11 @@
@if (ViewBag.Authorized)
{
<div class="d-flex flex-row justify-content-start flex-fill flex-column flex-lg-row mr-lg-2 mb-2 mb-md-0">
<div class="ip-lookup-profile align-self-center mr-0 mr-lg-2 ml-lg-n1" data-ip="@Model.IPAddress"></div>
<div id="ip_lookup_country" class="h4 mb-2 mb-lg-0 align-self-center text-muted"></div>
</div>
<div id="profile_aliases" class="text-muted pt-0 pt-lg-2 pb-2">
@foreach (var linked in Model.LinkedAccounts)
{

View File

@ -7,15 +7,16 @@
@foreach (var snapshot in Model)
{
<!-- this is not ideal, but I didn't want to manually write out all the properties-->
var snapProperties = Model.First().GetType().GetProperties();
var snapProperties = Model.First().GetType().GetProperties().OrderBy(prop => prop.Name);
foreach (var prop in snapProperties)
{
@if ((prop.Name.EndsWith("Id") && prop.Name != "WeaponId") || new[] { "Active", "Client", "PredictedViewAngles" }.Contains(prop.Name))
@if ((prop.Name.EndsWith("Id") && prop.Name != "WeaponId" || prop.Name == "Server") || new[] {"Active", "Client", "PredictedViewAngles"}.Contains(prop.Name))
{
continue;
}
<span class="text-white">@prop.Name </span> <span>&mdash; @prop.GetValue(snapshot).ToString()</span><br />
<span class="text-white">@prop.Name </span>
<span>&mdash; @prop.GetValue(snapshot)?.ToString()?.StripColors()</span><br/>
}
<div class="w-100 mt-1 mb-1 border-bottom"></div>
}

View File

@ -92,7 +92,7 @@
</a>
@if (ViewBag.Authorized)
{
<span class="oi oi-circle-x ml-1 profile-action align-baseline action-kick-button flex-column" data-action="kick" data-action-id="@Model.Players[i].ClientId" aria-hidden="true"></span>
<span class="oi oi-circle-x profile-action align-baseline action-kick-button flex-column mt-0" data-action="kick" data-action-id="@Model.Players[i].ClientId" aria-hidden="true"></span>
}
<br />
</div>

View File

@ -16,8 +16,26 @@
}
</div>
<div class="text-center col-md-4">@Model.Map</div>
<div class="text-center text-md-right col-md-4"><span class="server-clientcount">@Model.ClientCount</span>/<span class="server-maxclients">@Model.MaxClients</span></div>
<div class="text-center col-md-4 align-self-center">
<span>@Model.Map</span>
@if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1)
{
<span>&ndash;</span>
<span>@Model.GameType.ToUpper()</span>
}
</div>
<div class="text-center text-md-right col-md-4 d-flex align-self-center justify-content-center justify-content-md-end flex-column-reverse flex-sm-row">
@if (Model.LobbyZScore != null)
{
<div title="@ViewBag.Localization["WEBFRONT_HOME_RATING_DESC"]" class="cursor-help d-flex flex-row-reverse flex-md-row justify-content-center">
<span>@(Model.LobbyZScore ?? 0)</span>
<span class="oi oi-bolt align-self-center mr-1 ml-1"></span>
</div>
}
<div>
<span class="server-clientcount">@Model.ClientCount</span>/<span class="server-maxclients">@Model.MaxClients</span>
</div>
</div>
@if (ViewBag.Authorized)
{
@ -27,10 +45,10 @@
}
</div>
<div id="server_clientactivity_@Model.ID" class="bg-dark row server-activity pt-2 pb-2">
<div id="server_clientactivity_@Model.ID" class="bg-dark row server-activity @(Model.ClientCount > 0 ? "pt-2 pb-2" : "")">
@await Html.PartialAsync("../Server/_ClientActivity", Model)
</div>
<div class="row server-history mb-4">
<div class="server-history-row" id="server_history_@Model.ID" data-serverid="@Model.ID" data-clienthistory='@Html.Raw(Json.Serialize(Model.PlayerHistory))' data-online="@Model.Online"></div>
</div>
</div>

View File

@ -437,3 +437,7 @@ div.card {
#hitlocation_container {
background-color: #141414;
}
.cursor-help {
cursor: help;
}

View File

@ -201,3 +201,11 @@
background-color: $dark;
color: $white !important;
}
.ip-lookup-profile {
height: 2.5rem;
min-width: 3.0rem;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

View File

@ -1,7 +1,7 @@
$(document).ready(function () {
/*
Expand alias tab if they have any
*/
/*
Expand alias tab if they have any
*/
$('#profile_aliases_btn').click(function (e) {
const aliases = $('#profile_aliases').text().trim();
if (aliases && aliases.length !== 0) {
@ -10,6 +10,27 @@
}
});
const ipAddresses = $('.ip-lookup-profile');
$.each(ipAddresses, function (index, address) {
let ip = $(address).data('ip');
if (ip.length === 0) {
return;
}
$.get('https://ip2c.org/' + ip, function (result) {
const countryCode = result.split(';')[1].toLowerCase();
const country = result.split(';')[3];
if (country === 'Unknown') {
return;
}
$('#ip_lookup_country').text(country);
if (countryCode !== 'zz' && countryCode !== '') {
$(address).css('background-image', `url(https://www.countryflags.io/${countryCode}/flat/64.png)`);
}
});
});
/* set the end time for initial event query */
startAt = $('.loader-data-time').last().data('time');
@ -35,7 +56,7 @@
$(this).children().filter('.client-message-prefix').removeClass('oi-chevron-right');
$(this).children().filter('.client-message-prefix').addClass('oi-chevron-bottom');
$.get('/Stats/GetMessageAsync', {
'serverId': $(this).data('serverid'),
'when': $(this).data('when')
@ -102,7 +123,7 @@
$('#mainModal .modal-body').append(response.city);
}
if (response.region.length > 0) {
$('#mainModal .modal-body').append((response.city.length > 0 ? ', ' : '') + response.region);
$('#mainModal .modal-body').append((response.city.length > 0 ? ', ' : '') + response.region);
}
if (response.country.length > 0) {
$('#mainModal .modal-body').append((response.country.length > 0 ? ', ' : '') + response.country);