1
0
mirror of https://github.com/RaidMax/IW4M-Admin.git synced 2025-06-07 21:58:06 -05:00

Merge branch 'develop' into release/pre

This commit is contained in:
RaidMax 2024-02-04 20:56:12 -06:00
commit 3d54126911
26 changed files with 194 additions and 46 deletions

View File

@ -53,6 +53,7 @@
<ItemGroup>
<ProjectReference Include="..\Integrations\Cod\Integrations.Cod.csproj" />
<ProjectReference Include="..\Integrations\Source\Integrations.Source.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj">
<Private>true</Private>
</ProjectReference>

View File

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class ClearAllReportsCommand : Command
{
public ClearAllReportsCommand(CommandConfiguration config, ITranslationLookup layout) : base(config, layout)
{
Name = "clearallreports";
Description = _translationLookup["COMMANDS_REPORTS_CLEAR_DESC"];
Alias = "car";
Permission = EFClient.Permission.Administrator;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
foreach (var server in gameEvent.Owner.Manager.GetServers())
{
server.Reports.Clear();
}
gameEvent.Origin.Tell(_translationLookup["COMMANDS_REPORTS_CLEAR_SUCCESS"]);
return Task.CompletedTask;
}
}

View File

@ -94,6 +94,15 @@ namespace IW4MAdmin.Application
Console.WriteLine($" Version {Utilities.GetVersionAsString()}");
Console.WriteLine("=====================================================");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("!!!! IMPORTANT !!!!");
Console.WriteLine("The next update of IW4MAdmin will require .NET 8.");
Console.WriteLine("This is a breaking change!");
Console.WriteLine(
"Please update the ASP.NET Core Runtime: https://dotnet.microsoft.com/en-us/download/dotnet/8.0");
Console.WriteLine("!!!!!!!!!!!!!!!!!!!");
Console.ForegroundColor = ConsoleColor.Gray;
await LaunchAsync();
}

View File

@ -4,11 +4,8 @@
<TargetFramework>net6.0</TargetFramework>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>RaidMax.IW4MAdmin.Data</PackageId>
<Title>RaidMax.IW4MAdmin.Data</Title>
<Authors />
<PackageVersion>1.2.0</PackageVersion>
<Version>1.0.0.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -1,4 +1,4 @@
name: '$(Date:yyyy.MM.dd)$(Rev:.r)'
name: '$(Date:yyyy.M.d)$(Rev:.r)'
trigger:
batch: true
@ -7,6 +7,10 @@ trigger:
- release/pre
- master
- develop
paths:
exclude:
- '**/*.yml'
- '*.yml'
pr: none
@ -66,7 +70,7 @@ jobs:
displayName: 'Build projects'
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber)'
msbuildArgs: '/p:DeployOnBuild=false /p:PackageAsSingleFile=false /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)" /p:Version=$(Build.BuildNumber) /p:PackageVersion=$(Build.BuildNumber)'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
@ -181,6 +185,13 @@ jobs:
artifact: 'IW4MAdmin.$(buildConfiguration)'
publishLocation: 'pipeline'
- task: PublishPipelineArtifact@1
displayName: 'Publish nuget package artifact'
inputs:
targetPath: '$(Build.Repository.LocalPath)/SharedLibraryCore/bin/$(buildConfiguration)/RaidMax.IW4MAdmin.SharedLibraryCore.$(Build.BuildNumber).nupkg'
artifact: 'SharedLibraryCore.$(Build.BuildNumber).nupkg'
publishLocation: 'pipeline'
- task: FtpUpload@2
condition: ne(variables['Build.SourceBranch'], 'refs/heads/develop')
displayName: 'Upload zip file to website'

View File

@ -0,0 +1,55 @@
name: '$(Date:yyyy.M.d)$(Rev:.r)'
pr: none
pool:
vmImage: 'windows-2022'
variables:
buildPlatform: 'Any CPU'
outputFolder: '$(Build.ArtifactStagingDirectory)\Publish\$(buildConfiguration)'
releaseType: verified
buildConfiguration: Stable
isPreRelease: false
jobs:
- job: Build_Pack
steps:
- task: PowerShell@2
displayName: 'Setup Build configuration'
condition: or(eq(variables['Build.SourceBranch'], 'refs/heads/release/pre'), eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/chore/nuget-pipeline'))
inputs:
targetType: 'inline'
script: |
echo '##vso[task.setvariable variable=releaseType]prerelease'
echo '##vso[task.setvariable variable=buildConfiguration]Prerelease'
echo '##vso[task.setvariable variable=isPreRelease]true'
failOnStderr: true
- task: DotNetCoreCLI@2
displayName: 'Build Data'
inputs:
command: 'build'
projects: '**/Data.csproj'
arguments: '-c $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Build SLC'
inputs:
command: 'build'
projects: '**/SharedLibraryCore.csproj'
arguments: '-c $(buildConfiguration) /p:Version=$(Build.BuildNumber)'
- task: DotNetCoreCLI@2
displayName: 'Pack SLC'
inputs:
command: 'pack'
packagesToPack: '**/SharedLibraryCore.csproj'
versioningScheme: 'byBuildNumber'
- task: PublishPipelineArtifact@1
displayName: 'Publish nuget package artifact'
inputs:
targetPath: 'D:\a\1\a\RaidMax.IW4MAdmin.SharedLibraryCore.$(Build.BuildNumber).nupkg'
artifact: 'SharedLibraryCore.$(Build.BuildNumber).nupkg'
publishLocation: 'pipeline'

View File

@ -137,6 +137,8 @@ waitForFrameThread()
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
self endon( "disconnect" );
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
@ -246,4 +248,4 @@ Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self maps\mp\gametypes\_playerlogic::Callback_PlayerDisconnect();
}
}

View File

@ -143,6 +143,8 @@ waitForFrameThread()
waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
{
self endon( "disconnect" );
currentIndex = self.currentAnglePosition;
wait( 0.05 * afterFrameCount );
@ -260,4 +262,4 @@ Callback_PlayerDisconnect()
{
level notify( "disconnected", self );
self [[maps\mp\gametypes\_globallogic_player::callback_playerdisconnect]]();
}
}

View File

@ -193,7 +193,7 @@ NoClipImpl()
self God();
self Noclip();
self Hide();
self Show();
SetDvar( "sv_cheats", 0 );

View File

@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
version.txt = version.txt
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
DeploymentFiles\nuget-pipeline.yml = DeploymentFiles\nuget-pipeline.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"

View File

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

View File

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

View File

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

View File

@ -8,10 +8,11 @@
<OutputType>Library</OutputType>
<Configurations>Debug;Release;Prerelease</Configurations>
<Platforms>AnyCPU</Platforms>
<RootNamespace>IW4MAdmin.Plugins.Mute</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -131,8 +131,11 @@ public class MuteManager
{
var newPenalty = new EFPenalty
{
Type = muteState is MuteState.Unmuted ? EFPenalty.PenaltyType.Unmute :
dateTime is null ? EFPenalty.PenaltyType.Mute : EFPenalty.PenaltyType.TempMute,
Type = muteState is MuteState.Unmuted
? EFPenalty.PenaltyType.Unmute
: dateTime is null
? EFPenalty.PenaltyType.Mute
: EFPenalty.PenaltyType.TempMute,
Expires = muteState is MuteState.Unmuted ? DateTime.UtcNow : dateTime,
Offender = target,
Offense = reason,
@ -148,10 +151,9 @@ public class MuteManager
{
await using var context = _databaseContextFactory.CreateContext();
var mutePenalties = await context.Penalties
.Where(penalty => penalty.OffenderId == client.ClientId &&
(penalty.Type == EFPenalty.PenaltyType.Mute ||
penalty.Type == EFPenalty.PenaltyType.TempMute) &&
(penalty.Expires == null || penalty.Expires > DateTime.UtcNow))
.Where(penalty => penalty.OffenderId == client.ClientId)
.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Mute || penalty.Type == EFPenalty.PenaltyType.TempMute)
.Where(penalty => penalty.Expires == null || penalty.Expires > DateTime.UtcNow)
.ToListAsync();
foreach (var mutePenalty in mutePenalties)
@ -169,19 +171,20 @@ public class MuteManager
switch (muteStateMeta.MuteState)
{
case MuteState.Muted:
await server.ExecuteCommandAsync($"muteClient {client.ClientNumber}");
var muteCommand = string.Format(server.RconParser.Configuration.CommandPrefixes.Mute, client.ClientNumber);
await server.ExecuteCommandAsync(muteCommand);
muteStateMeta.CommandExecuted = true;
break;
case MuteState.Unmuted:
await server.ExecuteCommandAsync($"unmute {client.ClientNumber}");
var unMuteCommand = string.Format(server.RconParser.Configuration.CommandPrefixes.Unmute, client.ClientNumber);
await server.ExecuteCommandAsync(unMuteCommand);
muteStateMeta.CommandExecuted = true;
break;
}
}
private async Task<MuteState?> ReadPersistentDataV1(EFClient client) => TryParse<MuteState>(
(await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value,
out var muteState)
(await _metaService.GetPersistentMeta(Plugin.MuteKey, client.ClientId))?.Value, out var muteState)
? muteState
: null;

View File

@ -21,7 +21,7 @@ public class Plugin : IPluginV2
public const string MuteKey = "IW4MMute";
public static IManager Manager { get; private set; } = null!;
public static readonly Server.Game[] SupportedGames = {Server.Game.IW4};
public static Server.Game[] SupportedGames { get; private set; } = Array.Empty<Server.Game>();
private static readonly string[] DisabledCommands = {nameof(PrivateMessageAdminsCommand), "PrivateMessageCommand"};
private readonly IInteractionRegistration _interactionRegistration;
private readonly IRemoteCommandService _remoteCommandService;
@ -34,12 +34,14 @@ public class Plugin : IPluginV2
_interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService;
_muteManager = muteManager;
IManagementEventSubscriptions.Load += OnLoad;
IManagementEventSubscriptions.Unload += OnUnload;
IManagementEventSubscriptions.ClientStateInitialized += OnClientStateInitialized;
IGameServerEventSubscriptions.ClientDataUpdated += OnClientDataUpdated;
IGameServerEventSubscriptions.MonitoringStarted += OnServerMonitoredStarted;
IGameEventSubscriptions.ClientMessaged += OnClientMessaged;
}
@ -61,7 +63,7 @@ public class Plugin : IPluginV2
var muteMeta = Task.Run(() => _muteManager.GetCurrentMuteState(gameEvent.Origin), cancellationToken)
.GetAwaiter().GetResult();
if (muteMeta.MuteState is not MuteState.Muted)
{
return true;
@ -91,7 +93,7 @@ public class Plugin : IPluginV2
});
return Task.CompletedTask;
}
private Task OnUnload(IManager manager, CancellationToken token)
{
_interactionRegistration.UnregisterInteraction(MuteInteraction);
@ -152,21 +154,21 @@ public class Plugin : IPluginV2
{
return;
}
var muteMetaJoin = await _muteManager.GetCurrentMuteState(state.Client);
switch (muteMetaJoin)
{
case { MuteState: MuteState.Muted }:
case {MuteState: MuteState.Muted}:
// Let the client know when their mute expires.
state.Client.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_REMAINING_TIME"].FormatExt(
muteMetaJoin is { Expiration: not null }
muteMetaJoin is {Expiration: not null}
? muteMetaJoin.Expiration.Value.HumanizeForCurrentCulture()
: Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_MUTE_NEVER"],
muteMetaJoin.Reason));
break;
case { MuteState: MuteState.Unmuting }:
case {MuteState: MuteState.Unmuting}:
// Handle unmute of unmuted players.
await _muteManager.Unmute(state.Client.CurrentServer, Utilities.IW4MAdminClient(), state.Client,
muteMetaJoin.Reason ?? string.Empty);
@ -319,4 +321,29 @@ public class Plugin : IPluginV2
}
};
}
private Task OnServerMonitoredStarted(MonitorStartEvent serverEvent, CancellationToken token)
{
var game = (Server.Game)serverEvent.Server.GameCode;
lock (SupportedGames)
{
if (SupportedGames.Contains(game))
{
return Task.CompletedTask;
}
var server = Manager.GetServers().FirstOrDefault(x => x == serverEvent.Server);
var commandIsEmpty = string.IsNullOrWhiteSpace(server?.RconParser.Configuration.CommandPrefixes.Mute);
if (commandIsEmpty)
{
return Task.CompletedTask;
}
SupportedGames = SupportedGames.Append(game).ToArray();
}
return Task.CompletedTask;
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<OutputType.>Library</OutputType.>
<TargetFramework>net6.0</TargetFramework>
<ApplicationIcon />
<StartupObject />
@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2024.2.5.3" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,6 +19,8 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Kick = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'tempbanclient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.Mute = 'muteClient {0}';
rconParser.Configuration.CommandPrefixes.Unmute = 'unmute {0}';
rconParser.Configuration.DefaultRConPort = 28960;
rconParser.Configuration.DefaultInstallationDirectoryHint = 'HKEY_CURRENT_USER\\Software\\Classes\\iw4x\\shell\\open\\command';

View File

@ -20,6 +20,8 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.CommandPrefixes.Mute = 'muteClient {0}';
rconParser.Configuration.CommandPrefixes.Unmute = 'unmuteClient {0}';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)?(.*)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';

View File

@ -13,6 +13,7 @@ var plugin = {
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
eventParser.Configuration.GameDirectory = '';
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
@ -21,6 +22,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.OverrideDvarNameMapping.Add('fs_homepath', 'fs_basegame');
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Configuration.OverrideCommandTimeouts.Clear();

View File

@ -1,5 +1,4 @@
using IW4MAdmin.Plugins.Stats.Cheat;
using IW4MAdmin.Plugins.Stats.Config;
using IW4MAdmin.Plugins.Stats.Web.Dtos;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
@ -8,6 +7,7 @@ using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
@ -21,8 +21,6 @@ using Data.Models.Server;
using Humanizer.Localisation;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using MySqlConnector;
using Npgsql;
using Stats.Client.Abstractions;
using Stats.Config;
using Stats.Helpers;
@ -488,9 +486,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
catch (DbUpdateException updateException) when (
updateException.InnerException is PostgresException { SqlState: "23503" }
|| updateException.InnerException is SqliteException { SqliteErrorCode: 787 }
|| updateException.InnerException is MySqlException { SqlState: "23503" })
updateException.InnerException is DbException { SqlState: "23503" } or SqliteException { SqliteErrorCode: 787 })
{
_log.LogWarning("Trying to add {Client} to stats before they have been added to the database",
pl.ToString());

View File

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

View File

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

View File

@ -16,5 +16,7 @@
public string RConGetInfo { get; set; }
public string RConResponse { get; set; }
public string RconGetInfoResponseHeader { get; set; }
public string Mute { get; set; }
public string Unmute { get; set; }
}
}
}

View File

@ -54,8 +54,11 @@
<PackageReference Include="SimpleCrypto.NetCore" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj" />
<ItemGroup>
<ProjectReference Include="..\Data\Data.csproj">
<ReferenceOutputAssembly>true</ReferenceOutputAssembly>
<IncludeAssets>Data.dll</IncludeAssets>
</ProjectReference>
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
@ -65,7 +68,7 @@
<PropertyGroup>
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>
<Target DependsOnTargets="BuildOnlySettings;ResolveReferences" Name="CopyProjectReferencesToPackage">
<Target DependsOnTargets="ResolveReferences" Name="CopyProjectReferencesToPackage">
<ItemGroup>
<BuildOutputInPackage Include="@(ReferenceCopyLocalPaths-&gt;WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" />
</ItemGroup>

View File

@ -63,6 +63,7 @@
<ItemGroup>
<ProjectReference Include="..\Plugins\Stats\Stats.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
<ItemGroup>