update stats plugin for server caching and better DI usage
This commit is contained in:
parent
f41ce39180
commit
d9d5a56ab0
@ -9,7 +9,6 @@ using Data.Models.Client.Stats;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Stats.Config;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands;
|
||||
@ -33,7 +32,7 @@ class MostKillsCommand : Command
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var mostKills = await GetMostKills(StatManager.GetIdForServer(gameEvent.Owner), _statsConfig,
|
||||
var mostKills = await GetMostKills((gameEvent.Owner as IGameServer).LegacyDatabaseId, _statsConfig,
|
||||
_contextFactory, _translationLookup);
|
||||
if (!gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
|
||||
{
|
||||
|
@ -15,10 +15,10 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
class MostPlayedCommand : Command
|
||||
{
|
||||
public static async Task<List<string>> GetMostPlayed(Server s, ITranslationLookup translationLookup,
|
||||
public static async Task<List<string>> GetMostPlayed(IGameServer gameServer, ITranslationLookup translationLookup,
|
||||
IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
var serverId = StatManager.GetIdForServer(s);
|
||||
var serverId = gameServer.LegacyDatabaseId;
|
||||
|
||||
var mostPlayed = new List<string>
|
||||
{
|
||||
|
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client.Stats;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Stats.Config;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
@ -35,7 +34,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
if (gameEvent.Origin.ClientNumber >= 0)
|
||||
{
|
||||
var serverId = Helpers.StatManager.GetIdForServer(gameEvent.Owner);
|
||||
var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
|
||||
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var clientStats = await context.Set<EFClientStatistics>()
|
||||
|
@ -13,8 +13,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
public static async Task<List<string>> GetTopStats(IGameServer server, ITranslationLookup translationLookup, StatManager statManager)
|
||||
{
|
||||
var serverId = StatManager.GetIdForServer(server);
|
||||
var topStatsText = new List<string>()
|
||||
var serverId = server.LegacyDatabaseId;
|
||||
var topStatsText = new List<string>
|
||||
{
|
||||
$"(Color::Accent)--{translationLookup["PLUGINS_STATS_COMMANDS_TOP_TEXT"]}--"
|
||||
};
|
||||
@ -29,7 +29,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
// no one qualified
|
||||
if (topStatsText.Count == 1)
|
||||
{
|
||||
topStatsText = new List<string>()
|
||||
topStatsText = new List<string>
|
||||
{
|
||||
translationLookup["PLUGINS_STATS_TEXT_NOQUALIFY"]
|
||||
};
|
||||
|
@ -38,37 +38,36 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
_statManager = statManager;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
string statLine;
|
||||
EFClientStatistics pStats = null;
|
||||
|
||||
if (E.Data.Length > 0 && E.Target == null)
|
||||
if (gameEvent.Data.Length > 0 && gameEvent.Target == null)
|
||||
{
|
||||
E.Target = E.Owner.GetClientByName(E.Data).FirstOrDefault();
|
||||
gameEvent.Target = gameEvent.Owner.GetClientByName(gameEvent.Data).FirstOrDefault();
|
||||
|
||||
if (E.Target == null)
|
||||
if (gameEvent.Target == null)
|
||||
{
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_FAIL"]);
|
||||
}
|
||||
}
|
||||
|
||||
var serverId = StatManager.GetIdForServer(E.Owner);
|
||||
|
||||
var serverId = (gameEvent.Owner as IGameServer).LegacyDatabaseId;
|
||||
var totalRankedPlayers = await _statManager.GetTotalRankedPlayers(serverId);
|
||||
|
||||
// getting stats for a particular client
|
||||
if (E.Target != null)
|
||||
if (gameEvent.Target != null)
|
||||
{
|
||||
var performanceRanking = await _statManager.GetClientOverallRanking(E.Target.ClientId, serverId);
|
||||
var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Target.ClientId, serverId);
|
||||
var performanceRankingString = performanceRanking == 0
|
||||
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
||||
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{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)))
|
||||
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Target)))
|
||||
{
|
||||
pStats = E.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
|
||||
pStats = gameEvent.Target.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
|
||||
}
|
||||
|
||||
// target is not connected so we want to look up via database
|
||||
@ -76,7 +75,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
pStats = await context.Set<EFClientStatistics>()
|
||||
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Target.ClientId);
|
||||
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Target.ClientId);
|
||||
}
|
||||
|
||||
// if it's still null then they've not gotten a kill or death yet
|
||||
@ -89,15 +88,15 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
// getting self stats
|
||||
else
|
||||
{
|
||||
var performanceRanking = await _statManager.GetClientOverallRanking(E.Origin.ClientId, serverId);
|
||||
var performanceRanking = await _statManager.GetClientOverallRanking(gameEvent.Origin.ClientId, serverId);
|
||||
var performanceRankingString = performanceRanking == 0
|
||||
? _translationLookup["WEBFRONT_STATS_INDEX_UNRANKED"]
|
||||
: $"{_translationLookup["WEBFRONT_STATS_INDEX_RANKED"]} (Color::Accent)#{performanceRanking}/{totalRankedPlayers}";
|
||||
|
||||
// check if current client is connected to the server
|
||||
if (E.Owner.GetClientsAsList().Any(client => client.Equals(E.Origin)))
|
||||
if (gameEvent.Owner.GetClientsAsList().Any(client => client.Equals(gameEvent.Origin)))
|
||||
{
|
||||
pStats = E.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
|
||||
pStats = gameEvent.Origin.GetAdditionalProperty<EFClientStatistics>(StatManager.CLIENT_STATS_KEY);
|
||||
}
|
||||
|
||||
// happens if the user has not gotten a kill/death since connecting
|
||||
@ -105,7 +104,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
pStats = (await context.Set<EFClientStatistics>()
|
||||
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == E.Origin.ClientId));
|
||||
.FirstOrDefaultAsync(c => c.ServerId == serverId && c.ClientId == gameEvent.Origin.ClientId));
|
||||
}
|
||||
|
||||
// if it's still null then they've not gotten a kill or death yet
|
||||
@ -115,21 +114,21 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
pStats.KDR, pStats.Performance, performanceRankingString);
|
||||
}
|
||||
|
||||
if (E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
|
||||
if (gameEvent.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
|
||||
{
|
||||
var name = E.Target == null ? E.Origin.Name : E.Target.Name;
|
||||
E.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
|
||||
E.Owner.Broadcast(statLine);
|
||||
var name = gameEvent.Target == null ? gameEvent.Origin.Name : gameEvent.Target.Name;
|
||||
gameEvent.Owner.Broadcast(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(name));
|
||||
gameEvent.Owner.Broadcast(statLine);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (E.Target != null)
|
||||
if (gameEvent.Target != null)
|
||||
{
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(E.Target.Name));
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_STATS_COMMANDS_VIEW_SUCCESS"].FormatExt(gameEvent.Target.Name));
|
||||
}
|
||||
|
||||
E.Origin.Tell(statLine);
|
||||
gameEvent.Origin.Tell(statLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,22 +39,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
private readonly ILogger _log;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly StatsConfiguration _config;
|
||||
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 SemaphoreSlim _addPlayerWaiter = new(1, 1);
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
private readonly ILookupCache<EFServer> _serverCache;
|
||||
|
||||
public StatManager(ILogger<StatManager> logger, IDatabaseContextFactory contextFactory,
|
||||
StatsConfiguration statsConfig,
|
||||
IServerDistributionCalculator serverDistributionCalculator)
|
||||
IServerDistributionCalculator serverDistributionCalculator, ILookupCache<EFServer> serverCache)
|
||||
{
|
||||
_servers = new ConcurrentDictionary<long, ServerStats>();
|
||||
_log = logger;
|
||||
_contextFactory = contextFactory;
|
||||
_config = statsConfig;
|
||||
_serverDistributionCalculator = serverDistributionCalculator;
|
||||
_serverCache = serverCache;
|
||||
}
|
||||
|
||||
~StatManager()
|
||||
@ -62,12 +63,6 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
_addPlayerWaiter.Dispose();
|
||||
}
|
||||
|
||||
private void SetupServerIds()
|
||||
{
|
||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
serverModels = ctx.Set<EFServer>().ToList();
|
||||
}
|
||||
|
||||
public Expression<Func<EFRating, bool>> GetRankingFunc(long? serverId = null)
|
||||
{
|
||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||
@ -364,72 +359,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
if (serverModels == null)
|
||||
{
|
||||
SetupServerIds();
|
||||
}
|
||||
|
||||
var serverId = GetIdForServer(gameServer as Server);
|
||||
|
||||
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||
var serverSet = ctx.Set<EFServer>();
|
||||
// get the server from the database if it exists, otherwise create and insert a new one
|
||||
var cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.ServerId == serverId, token);
|
||||
|
||||
// the server might be using legacy server id
|
||||
if (cachedServerModel == null)
|
||||
{
|
||||
cachedServerModel = await serverSet.FirstOrDefaultAsync(s => s.EndPoint == gameServer.Id, token);
|
||||
|
||||
if (cachedServerModel != null)
|
||||
{
|
||||
// this provides a way to identify legacy server entries
|
||||
cachedServerModel.EndPoint = gameServer.Id;
|
||||
ctx.Update(cachedServerModel);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
// server has never been added before
|
||||
if (cachedServerModel == null)
|
||||
{
|
||||
cachedServerModel = new EFServer
|
||||
{
|
||||
Port = gameServer.ListenPort,
|
||||
EndPoint = gameServer.Id,
|
||||
ServerId = serverId,
|
||||
GameName = gameServer.GameCode,
|
||||
HostName = gameServer.ListenAddress
|
||||
};
|
||||
|
||||
cachedServerModel = serverSet.Add(cachedServerModel).Entity;
|
||||
// this doesn't need to be async as it's during initialization
|
||||
await ctx.SaveChangesAsync(token);
|
||||
}
|
||||
|
||||
// we want to set the gamename up if it's never been set, or it changed
|
||||
else if (!cachedServerModel.GameName.HasValue || cachedServerModel.GameName.Value != gameServer.GameCode)
|
||||
{
|
||||
cachedServerModel.GameName = gameServer.GameCode;
|
||||
ctx.Entry(cachedServerModel).Property(property => property.GameName).IsModified = true;
|
||||
await ctx.SaveChangesAsync(token);
|
||||
}
|
||||
|
||||
if (cachedServerModel.HostName == null || cachedServerModel.HostName != gameServer.ServerName)
|
||||
{
|
||||
cachedServerModel.HostName = gameServer.ServerName;
|
||||
ctx.Entry(cachedServerModel).Property(property => property.HostName).IsModified = true;
|
||||
await ctx.SaveChangesAsync(token);
|
||||
}
|
||||
|
||||
ctx.Entry(cachedServerModel).Property(property => property.IsPasswordProtected).IsModified = true;
|
||||
cachedServerModel.IsPasswordProtected = !string.IsNullOrEmpty(gameServer.GamePassword);
|
||||
await ctx.SaveChangesAsync(token);
|
||||
|
||||
// check to see if the stats have ever been initialized
|
||||
var serverStats = InitializeServerStats(cachedServerModel.ServerId);
|
||||
var cachedServer =
|
||||
await _serverCache.FirstAsync(cachedServer => cachedServer.EndPoint == gameServer.Id);
|
||||
var serverStats = InitializeServerStats(gameServer.LegacyDatabaseId);
|
||||
|
||||
_servers.TryAdd(serverId, new ServerStats(cachedServerModel, serverStats, gameServer as Server)
|
||||
_servers.TryAdd(cachedServer.ServerId, new ServerStats(cachedServer, serverStats, gameServer as Server)
|
||||
{
|
||||
IsTeamBased = gameServer.Gametype != "dm"
|
||||
});
|
||||
@ -459,7 +394,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
try
|
||||
{
|
||||
await _addPlayerWaiter.WaitAsync();
|
||||
long serverId = GetIdForServer(pl.CurrentServer);
|
||||
var serverId = (pl.CurrentServer as IGameServer).LegacyDatabaseId;
|
||||
|
||||
if (!_servers.ContainsKey(serverId))
|
||||
{
|
||||
@ -593,7 +528,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
return;
|
||||
}
|
||||
|
||||
var serverId = GetIdForServer(client.CurrentServer);
|
||||
var serverId = (client.CurrentServer as IGameServer).LegacyDatabaseId;
|
||||
var serverStats = _servers[serverId].ServerStatistics;
|
||||
|
||||
// get individual client's stats
|
||||
@ -954,7 +889,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
public async Task AddStandardKill(EFClient attacker, EFClient victim)
|
||||
{
|
||||
var serverId = GetIdForServer(attacker.CurrentServer);
|
||||
var serverId = (attacker.CurrentServer as IGameServer).LegacyDatabaseId;
|
||||
|
||||
var attackerStats = attacker.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
|
||||
var victimStats = victim.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY);
|
||||
@ -1582,8 +1517,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
public async Task Sync(IGameServer gameServer, CancellationToken token)
|
||||
{
|
||||
var serverId = GetIdForServer(gameServer);
|
||||
|
||||
var serverId = gameServer.LegacyDatabaseId;
|
||||
var waiter = _servers[serverId].OnSaving;
|
||||
try
|
||||
{
|
||||
@ -1622,25 +1556,5 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
_servers[serverId].IsTeamBased = isTeamBased;
|
||||
}
|
||||
|
||||
public static long GetIdForServer(IGameServer gameServer)
|
||||
{
|
||||
if (gameServer.Id == "66.150.121.184:28965")
|
||||
{
|
||||
return 886229536;
|
||||
}
|
||||
|
||||
// todo: this is not stable and will need to be migrated again...
|
||||
long id = HashCode.Combine(gameServer.ListenAddress, gameServer.ListenPort);
|
||||
id = id < 0 ? Math.Abs(id) : id;
|
||||
|
||||
#pragma warning disable CS0618
|
||||
var serverId = serverModels.FirstOrDefault(cachedServer => cachedServer.ServerId == gameServer.LegacyEndpoint ||
|
||||
#pragma warning restore CS0618
|
||||
cachedServer.EndPoint == gameServer.ToString() ||
|
||||
cachedServer.ServerId == id)?.ServerId;
|
||||
|
||||
return serverId ?? id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,8 +71,6 @@ public class Plugin : IPluginV2
|
||||
_statsConfig = statsConfig;
|
||||
_statManager = statManager;
|
||||
|
||||
IGameServerEventSubscriptions.MonitoringStarted +=
|
||||
async (monitorEvent, token) => await _statManager.EnsureServerAdded(monitorEvent.Server, token);
|
||||
IGameServerEventSubscriptions.MonitoringStopped +=
|
||||
async (monitorEvent, token) => await _statManager.Sync(monitorEvent.Server, token);
|
||||
IManagementEventSubscriptions.ClientStateInitialized += async (clientEvent, token) =>
|
||||
@ -97,6 +95,13 @@ public class Plugin : IPluginV2
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientEvent.Client.ClientId == 0)
|
||||
{
|
||||
_logger.LogWarning("No client id for {Client}, so we are not doing any stat calculation",
|
||||
clientEvent.Client.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var calculator in _statCalculators)
|
||||
{
|
||||
await calculator.CalculateForEvent(clientEvent);
|
||||
@ -108,7 +113,7 @@ public class Plugin : IPluginV2
|
||||
messageEvent.Client.ClientId > 1)
|
||||
{
|
||||
await _statManager.AddMessageAsync(messageEvent.Client.ClientId,
|
||||
StatManager.GetIdForServer(messageEvent.Server), true, messageEvent.Message, token);
|
||||
messageEvent.Server.LegacyDatabaseId, true, messageEvent.Message, token);
|
||||
}
|
||||
};
|
||||
IGameEventSubscriptions.MatchEnded += OnMatchEvent;
|
||||
@ -191,7 +196,7 @@ public class Plugin : IPluginV2
|
||||
await _statManager.AddScriptHit(!antiCheatDamageEvent.IsKill, antiCheatDamageEvent.CreatedAt.DateTime,
|
||||
antiCheatDamageEvent.Origin,
|
||||
antiCheatDamageEvent.Target,
|
||||
StatManager.GetIdForServer(antiCheatDamageEvent.Server), antiCheatDamageEvent.Server.Map.Name,
|
||||
antiCheatDamageEvent.Server.LegacyDatabaseId, antiCheatDamageEvent.Server.Map.Name,
|
||||
killInfo[7], killInfo[8],
|
||||
killInfo[5], killInfo[6], killInfo[3], killInfo[4], killInfo[9], killInfo[10], killInfo[11],
|
||||
killInfo[12], killInfo[13], killInfo[14], killInfo[15], killInfo[16], killInfo[17]);
|
||||
@ -205,13 +210,14 @@ public class Plugin : IPluginV2
|
||||
if (shouldPersist)
|
||||
{
|
||||
await _statManager.AddMessageAsync(commandEvent.Client.ClientId,
|
||||
StatManager.GetIdForServer(commandEvent.Client.CurrentServer), false, commandEvent.CommandText, token);
|
||||
(commandEvent.Client.CurrentServer as IGameServer).LegacyDatabaseId, false,
|
||||
commandEvent.CommandText, token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnMatchEvent(GameEventV2 gameEvent, CancellationToken token)
|
||||
{
|
||||
_statManager.SetTeamBased(StatManager.GetIdForServer(gameEvent.Server), gameEvent.Server.Gametype != "dm");
|
||||
_statManager.SetTeamBased(gameEvent.Server.LegacyDatabaseId, gameEvent.Server.Gametype != "dm");
|
||||
_statManager.ResetKillstreaks(gameEvent.Server);
|
||||
await _statManager.Sync(gameEvent.Server, token);
|
||||
|
||||
@ -510,10 +516,10 @@ public class Plugin : IPluginV2
|
||||
_databaseContextFactory));
|
||||
}
|
||||
|
||||
async Task<string> MostKills(Server gameServer)
|
||||
async Task<string> MostKills(IGameServer gameServer)
|
||||
{
|
||||
return string.Join(Environment.NewLine,
|
||||
await Commands.MostKillsCommand.GetMostKills(StatManager.GetIdForServer(gameServer), _statsConfig,
|
||||
await Commands.MostKillsCommand.GetMostKills(gameServer.LegacyDatabaseId, _statsConfig,
|
||||
_databaseContextFactory, _translationLookup));
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.2.11.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2023.4.5.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
Loading…
Reference in New Issue
Block a user