diff --git a/Application/ApplicationManager.cs b/Application/ApplicationManager.cs index 5ae2499e6..37e3806db 100644 --- a/Application/ApplicationManager.cs +++ b/Application/ApplicationManager.cs @@ -241,13 +241,17 @@ public IList GetServers() .ToList(); } - public IList GetCommands() + public IReadOnlyList Commands { - return _commands; + get + { + lock (_commands) + { + return _commands.ToImmutableList(); + } + } } - public IReadOnlyList Commands => _commands.ToImmutableList(); - private Task UpdateServerStates() { var index = 0; @@ -557,21 +561,25 @@ await scriptPlugin.Initialize(this, _scriptCommandFactory, _scriptPluginServiceR #region COMMANDS if (await ClientSvc.HasOwnerAsync(_isRunningTokenSource.Token)) { - _commands.RemoveAll(_cmd => _cmd.GetType() == typeof(OwnerCommand)); + lock (_commands) + { + _commands.RemoveAll(cmd => cmd.GetType() == typeof(OwnerCommand)); + } } - List commandsToAddToConfig = new List(); + List commandsToAddToConfig = []; var cmdConfig = _commandConfiguration.Configuration(); if (cmdConfig == null) { cmdConfig = new CommandConfiguration(); - commandsToAddToConfig.AddRange(_commands); + commandsToAddToConfig.AddRange(Commands); } else { - var unsavedCommands = _commands.Where(_cmd => !cmdConfig.Commands.Keys.Contains(_cmd.CommandConfigNameForType())); + var unsavedCommands = Commands + .Where(cmd => !cmdConfig.Commands.ContainsKey(cmd.CommandConfigNameForType())); commandsToAddToConfig.AddRange(unsavedCommands); } @@ -842,7 +850,14 @@ public void AddAdditionalCommand(IManagerCommand command) } } - public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName); + public void RemoveCommandByName(string commandName) + { + lock (_commands) + { + _commands.RemoveAll(command => command.Name == commandName); + } + } + public IAlertManager AlertManager => _alertManager; public async Task AddServerAsync(ServerConfiguration config, bool persistConfig = true, CancellationToken token = default) diff --git a/Application/CoreEventHandler.cs b/Application/CoreEventHandler.cs index 37f135f88..fbac588b3 100644 --- a/Application/CoreEventHandler.cs +++ b/Application/CoreEventHandler.cs @@ -134,7 +134,10 @@ private async Task BuildLegacyEventTask(IManager manager, CoreEvent coreEvent, G { if (manager.IsRunning || OverrideEvents.Contains(gameEvent.Type)) { - await manager.ExecuteEvent(gameEvent); + using var timeoutToken = new CancellationTokenSource(Utilities.DefaultCommandTimeout); + using var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(manager.CancellationToken, timeoutToken.Token); + + await manager.ExecuteEvent(gameEvent).WaitAsync(linkedToken.Token); await IGameEventSubscriptions.InvokeEventAsync(coreEvent, manager.CancellationToken); return; } diff --git a/Application/Main.cs b/Application/Main.cs index d4898377a..f4ecb7494 100644 --- a/Application/Main.cs +++ b/Application/Main.cs @@ -559,6 +559,7 @@ private static void ConfigureServices(IServiceCollection serviceCollection) .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs b/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs index da9f80155..f68c25e36 100644 --- a/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs +++ b/Application/Meta/AdministeredPenaltyResourceQueryHelper.cs @@ -52,7 +52,8 @@ public async Task> QueryR When = _penalty.When, ExpirationDate = _penalty.Expires, IsLinked = _penalty.OffenderId != query.ClientId, - IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag + IsSensitive = _penalty.Type == EFPenalty.PenaltyType.Flag, + Type = MetaType.Penalized }) .ToListAsync(); diff --git a/Application/Meta/MetaRegistration.cs b/Application/Meta/MetaRegistration.cs index 704935d3a..b3817f52f 100644 --- a/Application/Meta/MetaRegistration.cs +++ b/Application/Meta/MetaRegistration.cs @@ -10,6 +10,7 @@ using Humanizer; using Microsoft.Extensions.Logging; using Stats.Dtos; +using WebfrontCore.Core.Auth; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.Meta @@ -34,16 +35,17 @@ public void Register() metaService.AddRuntimeMeta(MetaType.Information, GetProfileMeta); metaService.AddRuntimeMeta(MetaType.ReceivedPenalty, - GetReceivedPenaltiesMeta); + GetReceivedPenaltiesMeta, nameof(WebfrontEntity.Penalty)); metaService.AddRuntimeMeta(MetaType.Penalized, - GetAdministeredPenaltiesMeta); + GetAdministeredPenaltiesMeta, nameof(WebfrontEntity.Penalty)); metaService.AddRuntimeMeta(MetaType.AliasUpdate, - GetUpdatedAliasMeta); + GetUpdatedAliasMeta, nameof(WebfrontEntity.MetaAliasUpdate)); metaService.AddRuntimeMeta(MetaType.ConnectionHistory, GetConnectionHistoryMeta); metaService.AddRuntimeMeta( - MetaType.PermissionLevel, GetPermissionLevelMeta); - metaService.AddRuntimeMeta(MetaType.ChatMessage, GetChatMessages); + MetaType.PermissionLevel, GetPermissionLevelMeta, nameof(WebfrontEntity.ClientLevel)); + metaService.AddRuntimeMeta(MetaType.ChatMessage, GetChatMessages, + nameof(WebfrontEntity.ChatMessage)); } private async Task> GetProfileMeta(ClientPaginationRequest request, diff --git a/Application/Meta/PermissionLevelChangedResourceQueryHelper.cs b/Application/Meta/PermissionLevelChangedResourceQueryHelper.cs index 8a5b80819..5cb7224ce 100644 --- a/Application/Meta/PermissionLevelChangedResourceQueryHelper.cs +++ b/Application/Meta/PermissionLevelChangedResourceQueryHelper.cs @@ -41,6 +41,7 @@ on change.OriginEntityId equals client.ClientId CurrentPermissionLevelValue = change.CurrentValue, When = change.TimeChanged, ClientId = change.TargetEntityId, + Type = MetaType.PermissionLevel, IsSensitive = true }; diff --git a/Application/Misc/MetaServiceV2.cs b/Application/Misc/MetaServiceV2.cs index 20890520f..0b0c34b31 100644 --- a/Application/Misc/MetaServiceV2.cs +++ b/Application/Misc/MetaServiceV2.cs @@ -8,24 +8,36 @@ using Data.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using SharedLibraryCore; +using SharedLibraryCore.Configuration; using SharedLibraryCore.Dtos; using SharedLibraryCore.Interfaces; using SharedLibraryCore.QueryHelper; +using WebfrontCore.Core.Auth; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace IW4MAdmin.Application.Misc; public class MetaServiceV2 : IMetaServiceV2 { - private readonly IDictionary> _metaActions; + protected class MetaRegistration + { + public required dynamic Action; + public required WebfrontEntity EntityType; + } + + private readonly IDictionary> _metaActions; private readonly IDatabaseContextFactory _contextFactory; + private readonly ApplicationConfiguration _configuration; private readonly ILogger _logger; - public MetaServiceV2(ILogger logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider) + public MetaServiceV2(ILogger logger, IDatabaseContextFactory contextFactory, + ApplicationConfiguration configuration) { _logger = logger; - _metaActions = new Dictionary>(); + _metaActions = new Dictionary>(); _contextFactory = contextFactory; + _configuration = configuration; } public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId, @@ -184,7 +196,7 @@ public async Task GetPersistentMetaValue(string metaKey, int clientId, Can if (meta is null) { - return default; + return null; } try @@ -194,7 +206,7 @@ public async Task GetPersistentMetaValue(string metaKey, int clientId, Can catch (Exception ex) { _logger.LogError(ex, "Could not deserialize meta with key {Key} and value {Value}", metaKey, meta.Value); - return default; + return null; } } @@ -208,7 +220,7 @@ public async Task GetPersistentMetaByLookup(string metaKey, string looku if (metaValue is null) { _logger.LogDebug("No meta exists for key {Key}, clientId {ClientId}", metaKey, clientId); - return default; + return null; } var lookupMeta = await context.EFMeta.FirstOrDefaultAsync(meta => meta.Key == lookupKey, token); @@ -217,7 +229,7 @@ public async Task GetPersistentMetaByLookup(string metaKey, string looku { _logger.LogWarning("No lookup meta exists for metaKey {MetaKey} and lookupKey {LookupKey}", metaKey, lookupKey); - return default; + return null; } var lookupId = int.Parse(metaValue.Value); @@ -225,7 +237,7 @@ public async Task GetPersistentMetaByLookup(string metaKey, string looku if (lookupValues is null) { - return default; + return null; } var foundLookup = lookupValues.FirstOrDefault(value => value.Id == lookupId); @@ -244,7 +256,7 @@ public async Task GetPersistentMetaByLookup(string metaKey, string looku _logger.LogWarning("No lookup meta found for provided lookup id {MetaKey}, {LookupKey}, {LookupId}", metaKey, lookupKey, lookupId); - return default; + return null; } public async Task RemovePersistentMeta(string metaKey, int clientId, CancellationToken token = default) @@ -357,14 +369,14 @@ public async Task GetPersistentMetaValue(string metaKey, CancellationToken { if (string.IsNullOrWhiteSpace(metaKey)) { - return default; + return null; } var meta = await GetPersistentMeta(metaKey, token); if (meta is null) { - return default; + return null; } try @@ -374,7 +386,7 @@ public async Task GetPersistentMetaValue(string metaKey, CancellationToken catch (Exception ex) { _logger.LogError(ex, "Could not serialize meta with key {Key} and value {Value}", metaKey, meta.Value); - return default; + return null; } } @@ -401,24 +413,37 @@ public async Task RemovePersistentMeta(string metaKey, CancellationToken token = } public void AddRuntimeMeta(MetaType metaKey, - Func>> metaAction) + Func>> metaAction, string entityPermission) where T : PaginationRequest where TReturnType : IClientMeta { - if (!_metaActions.ContainsKey(metaKey)) + var entity = Enum.Parse(entityPermission); + if (!_metaActions.TryGetValue(metaKey, out var action)) { - _metaActions.Add(metaKey, new List { metaAction }); + _metaActions.Add(metaKey, [new MetaRegistration { Action = metaAction, EntityType = entity }]); } else { - _metaActions[metaKey].Add(metaAction); + action.Add(new MetaRegistration { Action = metaAction, EntityType = entity }); } } - public async Task> GetRuntimeMeta(ClientPaginationRequest request, CancellationToken token = default) + public void AddRuntimeMeta(MetaType metaKey, + Func>> metaAction) + where T : PaginationRequest where TReturnType : IClientMeta + { + AddRuntimeMeta(metaKey, metaAction, nameof(WebfrontEntity.Default)); + } + + public async Task> GetRuntimeMeta(ClientPaginationRequest request, + CancellationToken token = default) { var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information) - .Select(async kvp => await kvp.Value[0](request, token))); + .SelectMany(kvp => kvp.Value) + .Where(reg => reg.EntityType == WebfrontEntity.Default || _configuration.HasPermission( + request.RequestPermission, + reg.EntityType, WebfrontPermission.Read)) + .Select(async reg => await reg.Action(request, token))); return metas.SelectMany(m => (IEnumerable)m) .OrderByDescending(m => m.When) @@ -434,14 +459,22 @@ public async Task> GetRuntimeMeta(ClientPaginationRequest requ var allMeta = new List(); var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration => - (IEnumerable)await individualMetaRegistration(request, token))); + (IEnumerable)await individualMetaRegistration.Action(request, token))); allMeta.AddRange(completedMeta.SelectMany(meta => meta)); return ProcessInformationMeta(allMeta); } - var meta = await _metaActions[metaType][0](request, token) as IEnumerable; + var registration = _metaActions[metaType][0]; + + if (registration.EntityType != WebfrontEntity.Default && !_configuration.HasPermission(request.RequestPermission, + registration.EntityType, WebfrontPermission.Read)) + { + return []; + } + + var meta = await registration.Action(request, token) as IEnumerable; return meta; } diff --git a/Application/Plugin/CSharpScript/CsPluginCommandRegistrar.cs b/Application/Plugin/CSharpScript/CsPluginCommandRegistrar.cs index 8afcdf342..a3cfda4f6 100644 --- a/Application/Plugin/CSharpScript/CsPluginCommandRegistrar.cs +++ b/Application/Plugin/CSharpScript/CsPluginCommandRegistrar.cs @@ -73,7 +73,7 @@ public void RegisterCommands(CsPluginInstance instance, Assembly assembly) /// private void RemoveConflictingCommands(Command newCommand, string fileName) { - var conflicts = manager.GetCommands() + var conflicts = manager.Commands .Where(existing => IsConflict((Command)existing, newCommand)) .ToList(); diff --git a/Application/QueryHelpers/ChatResourceQueryHelper.cs b/Application/QueryHelpers/ChatResourceQueryHelper.cs index 5613d213c..4e3b27dc6 100644 --- a/Application/QueryHelpers/ChatResourceQueryHelper.cs +++ b/Application/QueryHelpers/ChatResourceQueryHelper.cs @@ -83,7 +83,8 @@ public async Task> QueryResource(Chat ? Server.Game.IW4 : (Server.Game)message.Server.GameName.Value, SentIngame = message.SentIngame, - IsHidden = message.Server.IsPasswordProtected + IsHidden = message.Server.IsPasswordProtected, + Type = MetaType.ChatMessage }); iqResponse = query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending diff --git a/Data/Context/DatabaseContext.cs b/Data/Context/DatabaseContext.cs index 166a53d0d..0d9b98d0f 100644 --- a/Data/Context/DatabaseContext.cs +++ b/Data/Context/DatabaseContext.cs @@ -44,6 +44,7 @@ public abstract class DatabaseContext : DbContext #region MISC public DbSet InboxMessages { get; set; } + public DbSet Announcements { get; set; } public DbSet ServerSnapshots { get;set; } public DbSet ConnectionHistory { get; set; } @@ -155,6 +156,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(ent => ent.HasIndex(snapshot => snapshot.CapturedAt)); + modelBuilder.Entity(ent => + { + ent.HasIndex(a => a.IsActive); + ent.HasIndex(a => a.IsGlobalNotice); + ent.HasOne(a => a.CreatedByClient) + .WithMany() + .HasForeignKey(a => a.CreatedByClientId) + .OnDelete(DeleteBehavior.Restrict); + }); + modelBuilder.Entity().ToTable(nameof(EFAnnouncement)); + // force full name for database conversion modelBuilder.Entity().ToTable("EFClients"); modelBuilder.Entity().ToTable("EFAlias"); diff --git a/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.Designer.cs b/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.Designer.cs new file mode 100644 index 000000000..0714742bf --- /dev/null +++ b/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.Designer.cs @@ -0,0 +1,1763 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.MySql +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20260117221412_AddEFAnnouncement")] + partial class AddEFAnnouncement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("SnapshotId") + .HasColumnType("int"); + + b.Property("Vector3Id") + .HasColumnType("int"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AliasLinkId") + .HasColumnType("int"); + + b.Property("Connections") + .HasColumnType("int"); + + b.Property("CurrentAliasId") + .HasColumnType("int"); + + b.Property("FirstConnection") + .HasColumnType("datetime(6)"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("LastConnection") + .HasColumnType("datetime(6)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Masked") + .HasColumnType("tinyint(1)"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordSalt") + .HasColumnType("longtext"); + + b.Property("TotalConnectionTime") + .HasColumnType("int"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ConnectionType") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AttackerId") + .HasColumnType("int"); + + b.Property("Damage") + .HasColumnType("int"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("int"); + + b.Property("DeathType") + .HasColumnType("int"); + + b.Property("Fraction") + .HasColumnType("double"); + + b.Property("HitLoc") + .HasColumnType("int"); + + b.Property("IsKill") + .HasColumnType("tinyint(1)"); + + b.Property("KillOriginVector3Id") + .HasColumnType("int"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("int"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("int"); + + b.Property("VisibilityPercentage") + .HasColumnType("double"); + + b.Property("Weapon") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("SentIngame") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("datetime(6)"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CurrentSessionLength") + .HasColumnType("int"); + + b.Property("CurrentStrain") + .HasColumnType("double"); + + b.Property("CurrentViewAngleId") + .HasColumnType("int"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("Distance") + .HasColumnType("double"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("HitDestinationId") + .HasColumnType("int"); + + b.Property("HitLocation") + .HasColumnType("int"); + + b.Property("HitLocationReference") + .HasColumnType("longtext"); + + b.Property("HitOriginId") + .HasColumnType("int"); + + b.Property("HitType") + .HasColumnType("int"); + + b.Property("Hits") + .HasColumnType("int"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("LastStrainAngleId") + .HasColumnType("int"); + + b.Property("RecoilOffset") + .HasColumnType("double"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double"); + + b.Property("SessionSPM") + .HasColumnType("double"); + + b.Property("SessionScore") + .HasColumnType("int"); + + b.Property("SessionSnapHits") + .HasColumnType("int"); + + b.Property("StrainAngleBetween") + .HasColumnType("double"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.Property("WeaponReference") + .HasColumnType("longtext"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DamageInflicted") + .HasColumnType("int"); + + b.Property("DamageReceived") + .HasColumnType("int"); + + b.Property("DeathCount") + .HasColumnType("int"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitLocationId") + .HasColumnType("int"); + + b.Property("KillCount") + .HasColumnType("int"); + + b.Property("MeansOfDeathId") + .HasColumnType("int"); + + b.Property("ReceivedHitCount") + .HasColumnType("int"); + + b.Property("Score") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("UsageSeconds") + .HasColumnType("int"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("int"); + + b.Property("WeaponId") + .HasColumnType("int"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("PerformanceMetric") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AverageSnapValue") + .HasColumnType("double"); + + b.Property("Deaths") + .HasColumnType("int"); + + b.Property("EloRating") + .HasColumnType("double"); + + b.Property("Kills") + .HasColumnType("int"); + + b.Property("MaxStrain") + .HasColumnType("double"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double"); + + b.Property("SPM") + .HasColumnType("double"); + + b.Property("Skill") + .HasColumnType("double"); + + b.Property("SnapHitCount") + .HasColumnType("int"); + + b.Property("TimePlayed") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZScore") + .HasColumnType("double"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("int") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("int"); + + b.Property("HitOffsetAverage") + .HasColumnType("float"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("MaxAngleDistance") + .HasColumnType("float"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ActivityAmount") + .HasColumnType("int"); + + b.Property("Newest") + .HasColumnType("tinyint(1)"); + + b.Property("Performance") + .HasColumnType("double"); + + b.Property("Ranking") + .HasColumnType("int"); + + b.Property("RatingHistoryId") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("int"); + + b.Property("Attachment2Id") + .HasColumnType("int"); + + b.Property("Attachment3Id") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("Game") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("IPAddress") + .HasColumnType("int"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CurrentValue") + .HasColumnType("longtext"); + + b.Property("ImpersonationEntityId") + .HasColumnType("int"); + + b.Property("OriginEntityId") + .HasColumnType("int"); + + b.Property("PreviousValue") + .HasColumnType("longtext"); + + b.Property("TargetEntityId") + .HasColumnType("int"); + + b.Property("TimeChanged") + .HasColumnType("datetime(6)"); + + b.Property("TypeOfChange") + .HasColumnType("int"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ClientId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Extra") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("int"); + + b.Property("Updated") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("AutomatedOffense") + .HasColumnType("longtext"); + + b.Property("Expires") + .HasColumnType("datetime(6)"); + + b.Property("IsEvadedOffense") + .HasColumnType("tinyint(1)"); + + b.Property("LinkId") + .HasColumnType("int"); + + b.Property("OffenderId") + .HasColumnType("int"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PunisherId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("IPv4Address") + .HasColumnType("int"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("DestinationClientId") + .HasColumnType("int"); + + b.Property("IsDelivered") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("int"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("EndPoint") + .HasColumnType("longtext"); + + b.Property("GameName") + .HasColumnType("int"); + + b.Property("HostName") + .HasColumnType("longtext"); + + b.Property("IsPasswordProtected") + .HasColumnType("tinyint(1)"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("CapturedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClientCount") + .HasColumnType("int"); + + b.Property("ConnectionInterrupted") + .HasColumnType("tinyint(1)"); + + b.Property("MapId") + .HasColumnType("int"); + + b.Property("PeriodBlock") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("float"); + + b.Property("Y") + .HasColumnType("float"); + + b.Property("Z") + .HasColumnType("float"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.cs b/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.cs new file mode 100644 index 000000000..46f9ceb57 --- /dev/null +++ b/Data/Migrations/MySql/20260117221412_AddEFAnnouncement.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.MySql +{ + /// + public partial class AddEFAnnouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EFAnnouncement", + columns: table => new + { + AnnouncementId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Content = table.Column(type: "varchar(4096)", maxLength: 4096, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + StartAt = table.Column(type: "datetime(6)", nullable: true), + EndAt = table.Column(type: "datetime(6)", nullable: true), + IsActive = table.Column(type: "tinyint(1)", nullable: false), + IsGlobalNotice = table.Column(type: "tinyint(1)", nullable: false), + CreatedByClientId = table.Column(type: "int", nullable: false), + CreatedDateTime = table.Column(type: "datetime(6)", nullable: false), + UpdatedDateTime = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFAnnouncement", x => x.AnnouncementId); + table.ForeignKey( + name: "FK_EFAnnouncement_EFClients_CreatedByClientId", + column: x => x.CreatedByClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_CreatedByClientId", + table: "EFAnnouncement", + column: "CreatedByClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsActive", + table: "EFAnnouncement", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsGlobalNotice", + table: "EFAnnouncement", + column: "IsGlobalNotice"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFAnnouncement"); + } + } +} diff --git a/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs b/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs index f54e5221f..6c2902b41 100644 --- a/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/MySql/MySqlDatabaseContextModelSnapshot.cs @@ -1065,6 +1065,56 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFPenaltyIdentifiers", (string)null); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("varchar(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("int"); + + b.Property("CreatedDateTime") + .HasColumnType("datetime(6)"); + + b.Property("EndAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsGlobalNotice") + .HasColumnType("tinyint(1)"); + + b.Property("StartAt") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("datetime(6)"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.Property("InboxMessageId") @@ -1608,6 +1658,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Penalty"); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.HasOne("Data.Models.Client.EFClient", "DestinationClient") diff --git a/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.Designer.cs b/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.Designer.cs new file mode 100644 index 000000000..86ccd6668 --- /dev/null +++ b/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.Designer.cs @@ -0,0 +1,1763 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + [DbContext(typeof(PostgresqlDatabaseContext))] + [Migration("20260117221312_AddEFAnnouncement")] + partial class AddEFAnnouncement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ACSnapshotVector3Id")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("SnapshotId") + .HasColumnType("integer"); + + b.Property("Vector3Id") + .HasColumnType("integer"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AliasLinkId") + .HasColumnType("integer"); + + b.Property("Connections") + .HasColumnType("integer"); + + b.Property("CurrentAliasId") + .HasColumnType("integer"); + + b.Property("FirstConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("LastConnection") + .HasColumnType("timestamp without time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("Masked") + .HasColumnType("boolean"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("Password") + .HasColumnType("text"); + + b.Property("PasswordSalt") + .HasColumnType("text"); + + b.Property("TotalConnectionTime") + .HasColumnType("integer"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientConnectionId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ConnectionType") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KillId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AttackerId") + .HasColumnType("integer"); + + b.Property("Damage") + .HasColumnType("integer"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("integer"); + + b.Property("DeathType") + .HasColumnType("integer"); + + b.Property("Fraction") + .HasColumnType("double precision"); + + b.Property("HitLoc") + .HasColumnType("integer"); + + b.Property("IsKill") + .HasColumnType("boolean"); + + b.Property("KillOriginVector3Id") + .HasColumnType("integer"); + + b.Property("Map") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("VictimId") + .HasColumnType("integer"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("integer"); + + b.Property("VisibilityPercentage") + .HasColumnType("double precision"); + + b.Property("Weapon") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MessageId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("SentIngame") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TimeSent") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CurrentSessionLength") + .HasColumnType("integer"); + + b.Property("CurrentStrain") + .HasColumnType("double precision"); + + b.Property("CurrentViewAngleId") + .HasColumnType("integer"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("Distance") + .HasColumnType("double precision"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("HitDestinationId") + .HasColumnType("integer"); + + b.Property("HitLocation") + .HasColumnType("integer"); + + b.Property("HitLocationReference") + .HasColumnType("text"); + + b.Property("HitOriginId") + .HasColumnType("integer"); + + b.Property("HitType") + .HasColumnType("integer"); + + b.Property("Hits") + .HasColumnType("integer"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("LastStrainAngleId") + .HasColumnType("integer"); + + b.Property("RecoilOffset") + .HasColumnType("double precision"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SessionAngleOffset") + .HasColumnType("double precision"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("double precision"); + + b.Property("SessionSPM") + .HasColumnType("double precision"); + + b.Property("SessionScore") + .HasColumnType("integer"); + + b.Property("SessionSnapHits") + .HasColumnType("integer"); + + b.Property("StrainAngleBetween") + .HasColumnType("double precision"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.Property("WeaponReference") + .HasColumnType("text"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientHitStatisticId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DamageInflicted") + .HasColumnType("integer"); + + b.Property("DamageReceived") + .HasColumnType("integer"); + + b.Property("DeathCount") + .HasColumnType("integer"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitLocationId") + .HasColumnType("integer"); + + b.Property("KillCount") + .HasColumnType("integer"); + + b.Property("MeansOfDeathId") + .HasColumnType("integer"); + + b.Property("ReceivedHitCount") + .HasColumnType("integer"); + + b.Property("Score") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SuicideCount") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("UsageSeconds") + .HasColumnType("integer"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("integer"); + + b.Property("WeaponId") + .HasColumnType("integer"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ClientRankingHistoryId")); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("PerformanceMetric") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AverageSnapValue") + .HasColumnType("double precision"); + + b.Property("Deaths") + .HasColumnType("integer"); + + b.Property("EloRating") + .HasColumnType("double precision"); + + b.Property("Kills") + .HasColumnType("integer"); + + b.Property("MaxStrain") + .HasColumnType("double precision"); + + b.Property("RollingWeightedKDR") + .HasColumnType("double precision"); + + b.Property("SPM") + .HasColumnType("double precision"); + + b.Property("Skill") + .HasColumnType("double precision"); + + b.Property("SnapHitCount") + .HasColumnType("integer"); + + b.Property("TimePlayed") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ZScore") + .HasColumnType("double precision"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationCountId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("integer") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("bigint") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("integer"); + + b.Property("HitOffsetAverage") + .HasColumnType("real"); + + b.Property("Location") + .HasColumnType("integer"); + + b.Property("MaxAngleDistance") + .HasColumnType("real"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RatingId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ActivityAmount") + .HasColumnType("integer"); + + b.Property("Newest") + .HasColumnType("boolean"); + + b.Property("Performance") + .HasColumnType("double precision"); + + b.Property("Ranking") + .HasColumnType("integer"); + + b.Property("RatingHistoryId") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HitLocationId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MapId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MeansOfDeathId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("WeaponAttachmentComboId")); + + b.Property("Attachment1Id") + .HasColumnType("integer"); + + b.Property("Attachment2Id") + .HasColumnType("integer"); + + b.Property("Attachment3Id") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Game") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("DateAdded") + .HasColumnType("timestamp without time zone"); + + b.Property("IPAddress") + .HasColumnType("integer"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("character varying(24)"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AliasLinkId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ChangeHistoryId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CurrentValue") + .HasColumnType("text"); + + b.Property("ImpersonationEntityId") + .HasColumnType("integer"); + + b.Property("OriginEntityId") + .HasColumnType("integer"); + + b.Property("PreviousValue") + .HasColumnType("text"); + + b.Property("TargetEntityId") + .HasColumnType("integer"); + + b.Property("TimeChanged") + .HasColumnType("timestamp without time zone"); + + b.Property("TypeOfChange") + .HasColumnType("integer"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("MetaId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ClientId") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("Extra") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("LinkedMetaId") + .HasColumnType("integer"); + + b.Property("Updated") + .HasColumnType("timestamp without time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("AutomatedOffense") + .HasColumnType("text"); + + b.Property("Expires") + .HasColumnType("timestamp without time zone"); + + b.Property("IsEvadedOffense") + .HasColumnType("boolean"); + + b.Property("LinkId") + .HasColumnType("integer"); + + b.Property("OffenderId") + .HasColumnType("integer"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("text"); + + b.Property("PunisherId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("When") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PenaltyIdentifierId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IPv4Address") + .HasColumnType("integer"); + + b.Property("NetworkId") + .HasColumnType("bigint"); + + b.Property("PenaltyId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("InboxMessageId")); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("DestinationClientId") + .HasColumnType("integer"); + + b.Property("IsDelivered") + .HasColumnType("boolean"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("SourceClientId") + .HasColumnType("integer"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("EndPoint") + .HasColumnType("text"); + + b.Property("GameName") + .HasColumnType("integer"); + + b.Property("HostName") + .HasColumnType("text"); + + b.Property("IsPasswordProtected") + .HasColumnType("boolean"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerSnapshotId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("CapturedAt") + .HasColumnType("timestamp without time zone"); + + b.Property("ClientCount") + .HasColumnType("integer"); + + b.Property("ConnectionInterrupted") + .HasColumnType("boolean"); + + b.Property("MapId") + .HasColumnType("integer"); + + b.Property("PeriodBlock") + .HasColumnType("integer"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("StatisticId")); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("ServerId") + .HasColumnType("bigint"); + + b.Property("TotalKills") + .HasColumnType("bigint"); + + b.Property("TotalPlayTime") + .HasColumnType("bigint"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Vector3Id")); + + b.Property("X") + .HasColumnType("real"); + + b.Property("Y") + .HasColumnType("real"); + + b.Property("Z") + .HasColumnType("real"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.cs b/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.cs new file mode 100644 index 000000000..9a08ccbc1 --- /dev/null +++ b/Data/Migrations/Postgresql/20260117221312_AddEFAnnouncement.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Data.Migrations.Postgresql +{ + /// + public partial class AddEFAnnouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EFAnnouncement", + columns: table => new + { + AnnouncementId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Title = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Content = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + StartAt = table.Column(type: "timestamp without time zone", nullable: true), + EndAt = table.Column(type: "timestamp without time zone", nullable: true), + IsActive = table.Column(type: "boolean", nullable: false), + IsGlobalNotice = table.Column(type: "boolean", nullable: false), + CreatedByClientId = table.Column(type: "integer", nullable: false), + CreatedDateTime = table.Column(type: "timestamp without time zone", nullable: false), + UpdatedDateTime = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFAnnouncement", x => x.AnnouncementId); + table.ForeignKey( + name: "FK_EFAnnouncement_EFClients_CreatedByClientId", + column: x => x.CreatedByClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_CreatedByClientId", + table: "EFAnnouncement", + column: "CreatedByClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsActive", + table: "EFAnnouncement", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsGlobalNotice", + table: "EFAnnouncement", + column: "IsGlobalNotice"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFAnnouncement"); + } + } +} diff --git a/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs b/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs index 03e9a91b0..ea0658453 100644 --- a/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/Postgresql/PostgresqlDatabaseContextModelSnapshot.cs @@ -1065,6 +1065,56 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFPenaltyIdentifiers", (string)null); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("CreatedByClientId") + .HasColumnType("integer"); + + b.Property("CreatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("EndAt") + .HasColumnType("timestamp without time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsGlobalNotice") + .HasColumnType("boolean"); + + b.Property("StartAt") + .HasColumnType("timestamp without time zone"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UpdatedDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.Property("InboxMessageId") @@ -1608,6 +1658,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Penalty"); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.HasOne("Data.Models.Client.EFClient", "DestinationClient") diff --git a/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.Designer.cs b/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.Designer.cs new file mode 100644 index 000000000..1d6ee8b26 --- /dev/null +++ b/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.Designer.cs @@ -0,0 +1,1702 @@ +// +using System; +using Data.MigrationContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20260117221229_AddEFAnnouncement")] + partial class AddEFAnnouncement + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.Property("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("Vector3Id") + .HasColumnType("INTEGER"); + + b.HasKey("ACSnapshotVector3Id"); + + b.HasIndex("SnapshotId"); + + b.HasIndex("Vector3Id"); + + b.ToTable("EFACSnapshotVector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("TotalConnectionTime") + .HasColumnType("INTEGER"); + + b.HasKey("ClientId"); + + b.HasAlternateKey("NetworkId", "GameName"); + + b.HasIndex("AliasLinkId"); + + b.HasIndex("CurrentAliasId"); + + b.HasIndex("LastConnection"); + + b.HasIndex("NetworkId"); + + b.ToTable("EFClients", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.Property("ClientConnectionId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ConnectionType") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("ClientConnectionId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientConnectionHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.Property("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("KillId"); + + b.HasIndex("AttackerId"); + + b.HasIndex("DeathOriginVector3Id"); + + b.HasIndex("KillOriginVector3Id"); + + b.HasIndex("ServerId"); + + b.HasIndex("VictimId"); + + b.HasIndex("ViewAnglesVector3Id"); + + b.ToTable("EFClientKills", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("SentIngame") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TimeSent") + .HasColumnType("TEXT"); + + b.HasKey("MessageId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("TimeSent"); + + b.ToTable("EFClientMessages", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Property("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitLocationReference") + .HasColumnType("TEXT"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("WeaponReference") + .HasColumnType("TEXT"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("SnapshotId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CurrentViewAngleId"); + + b.HasIndex("HitDestinationId"); + + b.HasIndex("HitOriginId"); + + b.HasIndex("LastStrainAngleId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFACSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.Property("ClientHitStatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DamageInflicted") + .HasColumnType("INTEGER"); + + b.Property("DamageReceived") + .HasColumnType("INTEGER"); + + b.Property("DeathCount") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitLocationId") + .HasColumnType("INTEGER"); + + b.Property("KillCount") + .HasColumnType("INTEGER"); + + b.Property("MeansOfDeathId") + .HasColumnType("INTEGER"); + + b.Property("ReceivedHitCount") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SuicideCount") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("UsageSeconds") + .HasColumnType("INTEGER"); + + b.Property("WeaponAttachmentComboId") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.HasKey("ClientHitStatisticId"); + + b.HasIndex("HitLocationId"); + + b.HasIndex("MeansOfDeathId"); + + b.HasIndex("ServerId"); + + b.HasIndex("WeaponAttachmentComboId"); + + b.HasIndex("WeaponId"); + + b.HasIndex("ClientId", "ServerId"); + + b.ToTable("EFClientHitStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.Property("ClientRankingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("PerformanceMetric") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientRankingHistoryId"); + + b.HasIndex("ClientId"); + + b.HasIndex("CreatedDateTime"); + + b.HasIndex("Ranking"); + + b.HasIndex("ServerId"); + + b.HasIndex("UpdatedDateTime"); + + b.HasIndex("ZScore"); + + b.ToTable("EFClientRankingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Property("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("ZScore") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.HasIndex("ZScore"); + + b.HasIndex("ClientId", "TimePlayed", "ZScore"); + + b.ToTable("EFClientStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsClientId"); + + b.Property("EFClientStatisticsServerId") + .HasColumnType("INTEGER") + .HasColumnName("EFClientStatisticsServerId"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("MaxAngleDistance") + .HasColumnType("REAL"); + + b.HasKey("HitLocationCountId"); + + b.HasIndex("EFClientStatisticsServerId"); + + b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId"); + + b.ToTable("EFHitLocationCounts", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("RatingId"); + + b.HasIndex("RatingHistoryId"); + + b.HasIndex("ServerId"); + + b.HasIndex("Performance", "Ranking", "When"); + + b.HasIndex("When", "ServerId", "Performance", "ActivityAmount"); + + b.ToTable("EFRating", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFHitLocation", b => + { + b.Property("HitLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("HitLocationId"); + + b.HasIndex("Name"); + + b.ToTable("EFHitLocations", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMap", b => + { + b.Property("MapId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MapId"); + + b.ToTable("EFMaps", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFMeansOfDeath", b => + { + b.Property("MeansOfDeathId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("MeansOfDeathId"); + + b.ToTable("EFMeansOfDeath", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeapon", b => + { + b.Property("WeaponId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponId"); + + b.HasIndex("Name"); + + b.ToTable("EFWeapons", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachment", b => + { + b.Property("WeaponAttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentId"); + + b.ToTable("EFWeaponAttachments", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.Property("WeaponAttachmentComboId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment1Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment2Id") + .HasColumnType("INTEGER"); + + b.Property("Attachment3Id") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("Game") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("WeaponAttachmentComboId"); + + b.HasIndex("Attachment1Id"); + + b.HasIndex("Attachment2Id"); + + b.HasIndex("Attachment3Id"); + + b.ToTable("EFWeaponAttachmentCombos", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.Property("SearchableIPAddress") + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasComputedColumnSql("((IPAddress & 255) || '.' || ((IPAddress >> 8) & 255)) || '.' || ((IPAddress >> 16) & 255) || '.' || ((IPAddress >> 24) & 255)", true); + + b.Property("SearchableName") + .HasMaxLength(24) + .HasColumnType("TEXT"); + + b.HasKey("AliasId"); + + b.HasIndex("IPAddress"); + + b.HasIndex("LinkId"); + + b.HasIndex("Name"); + + b.HasIndex("SearchableIPAddress"); + + b.HasIndex("SearchableName"); + + b.HasIndex("Name", "IPAddress"); + + b.ToTable("EFAlias", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Property("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.Property("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("LinkedMetaId") + .HasColumnType("INTEGER"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("MetaId"); + + b.HasIndex("ClientId"); + + b.HasIndex("Key"); + + b.HasIndex("LinkedMetaId"); + + b.ToTable("EFMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.Property("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyId"); + + b.HasIndex("LinkId"); + + b.HasIndex("OffenderId"); + + b.HasIndex("PunisherId"); + + b.ToTable("EFPenalties", (string)null); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.Property("PenaltyIdentifierId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("IPv4Address") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("PenaltyId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("PenaltyIdentifierId"); + + b.HasIndex("IPv4Address"); + + b.HasIndex("NetworkId"); + + b.HasIndex("PenaltyId"); + + b.ToTable("EFPenaltyIdentifiers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.Property("InboxMessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("DestinationClientId") + .HasColumnType("INTEGER"); + + b.Property("IsDelivered") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("SourceClientId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("InboxMessageId"); + + b.HasIndex("DestinationClientId"); + + b.HasIndex("ServerId"); + + b.HasIndex("SourceClientId"); + + b.ToTable("InboxMessages"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServer", b => + { + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("IsPasswordProtected") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.Property("ServerSnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("CapturedAt") + .HasColumnType("TEXT"); + + b.Property("ClientCount") + .HasColumnType("INTEGER"); + + b.Property("ConnectionInterrupted") + .HasColumnType("INTEGER"); + + b.Property("MapId") + .HasColumnType("INTEGER"); + + b.Property("PeriodBlock") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.HasKey("ServerSnapshotId"); + + b.HasIndex("CapturedAt"); + + b.HasIndex("MapId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerSnapshot", (string)null); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics", (string)null); + }); + + modelBuilder.Entity("Data.Models.Vector3", b => + { + b.Property("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("Z") + .HasColumnType("REAL"); + + b.HasKey("Vector3Id"); + + b.ToTable("Vector3", (string)null); + }); + + modelBuilder.Entity("Data.Models.Client.EFACSnapshotVector3", b => + { + b.HasOne("Data.Models.Client.Stats.EFACSnapshot", "Snapshot") + .WithMany("PredictedViewAngles") + .HasForeignKey("SnapshotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "Vector") + .WithMany() + .HasForeignKey("Vector3Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Snapshot"); + + b.Navigation("Vector"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.HasOne("Data.Models.EFAliasLink", "AliasLink") + .WithMany() + .HasForeignKey("AliasLinkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.EFAlias", "CurrentAlias") + .WithMany() + .HasForeignKey("CurrentAliasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AliasLink"); + + b.Navigation("CurrentAlias"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientConnectionHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientKill", b => + { + b.HasOne("Data.Models.Client.EFClient", "Attacker") + .WithMany() + .HasForeignKey("AttackerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "DeathOrigin") + .WithMany() + .HasForeignKey("DeathOriginVector3Id"); + + b.HasOne("Data.Models.Vector3", "KillOrigin") + .WithMany() + .HasForeignKey("KillOriginVector3Id"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Victim") + .WithMany() + .HasForeignKey("VictimId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "ViewAngles") + .WithMany() + .HasForeignKey("ViewAnglesVector3Id"); + + b.Navigation("Attacker"); + + b.Navigation("DeathOrigin"); + + b.Navigation("KillOrigin"); + + b.Navigation("Server"); + + b.Navigation("Victim"); + + b.Navigation("ViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClientMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "CurrentViewAngle") + .WithMany() + .HasForeignKey("CurrentViewAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitDestination") + .WithMany() + .HasForeignKey("HitDestinationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "HitOrigin") + .WithMany() + .HasForeignKey("HitOriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Vector3", "LastStrainAngle") + .WithMany() + .HasForeignKey("LastStrainAngleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("CurrentViewAngle"); + + b.Navigation("HitDestination"); + + b.Navigation("HitOrigin"); + + b.Navigation("LastStrainAngle"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientHitStatistic", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFHitLocation", "HitLocation") + .WithMany() + .HasForeignKey("HitLocationId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFMeansOfDeath", "MeansOfDeath") + .WithMany() + .HasForeignKey("MeansOfDeathId"); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", "WeaponAttachmentCombo") + .WithMany() + .HasForeignKey("WeaponAttachmentComboId"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeapon", "Weapon") + .WithMany() + .HasForeignKey("WeaponId"); + + b.Navigation("Client"); + + b.Navigation("HitLocation"); + + b.Navigation("MeansOfDeath"); + + b.Navigation("Server"); + + b.Navigation("Weapon"); + + b.Navigation("WeaponAttachmentCombo"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRankingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFHitLocationCount", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany() + .HasForeignKey("EFClientStatisticsClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.EFClientStatistics", null) + .WithMany("HitLocations") + .HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFRating", b => + { + b.HasOne("Data.Models.Client.Stats.EFClientRatingHistory", "RatingHistory") + .WithMany("Ratings") + .HasForeignKey("RatingHistoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.Navigation("RatingHistory"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.Reference.EFWeaponAttachmentCombo", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment1") + .WithMany() + .HasForeignKey("Attachment1Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment2") + .WithMany() + .HasForeignKey("Attachment2Id"); + + b.HasOne("Data.Models.Client.Stats.Reference.EFWeaponAttachment", "Attachment3") + .WithMany() + .HasForeignKey("Attachment3Id"); + + b.Navigation("Attachment1"); + + b.Navigation("Attachment2"); + + b.Navigation("Attachment3"); + }); + + modelBuilder.Entity("Data.Models.EFAlias", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("Children") + .HasForeignKey("LinkId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + }); + + modelBuilder.Entity("Data.Models.EFMeta", b => + { + b.HasOne("Data.Models.Client.EFClient", "Client") + .WithMany("Meta") + .HasForeignKey("ClientId"); + + b.HasOne("Data.Models.EFMeta", "LinkedMeta") + .WithMany() + .HasForeignKey("LinkedMetaId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Client"); + + b.Navigation("LinkedMeta"); + }); + + modelBuilder.Entity("Data.Models.EFPenalty", b => + { + b.HasOne("Data.Models.EFAliasLink", "Link") + .WithMany("ReceivedPenalties") + .HasForeignKey("LinkId"); + + b.HasOne("Data.Models.Client.EFClient", "Offender") + .WithMany("ReceivedPenalties") + .HasForeignKey("OffenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Data.Models.Client.EFClient", "Punisher") + .WithMany("AdministeredPenalties") + .HasForeignKey("PunisherId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Link"); + + b.Navigation("Offender"); + + b.Navigation("Punisher"); + }); + + modelBuilder.Entity("Data.Models.EFPenaltyIdentifier", b => + { + b.HasOne("Data.Models.EFPenalty", "Penalty") + .WithMany() + .HasForeignKey("PenaltyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Penalty"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => + { + b.HasOne("Data.Models.Client.EFClient", "DestinationClient") + .WithMany() + .HasForeignKey("DestinationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId"); + + b.HasOne("Data.Models.Client.EFClient", "SourceClient") + .WithMany() + .HasForeignKey("SourceClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationClient"); + + b.Navigation("Server"); + + b.Navigation("SourceClient"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerSnapshot", b => + { + b.HasOne("Data.Models.Client.Stats.Reference.EFMap", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Server.EFServerStatistics", b => + { + b.HasOne("Data.Models.Server.EFServer", "Server") + .WithMany() + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Data.Models.Client.EFClient", b => + { + b.Navigation("AdministeredPenalties"); + + b.Navigation("Meta"); + + b.Navigation("ReceivedPenalties"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFACSnapshot", b => + { + b.Navigation("PredictedViewAngles"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientRatingHistory", b => + { + b.Navigation("Ratings"); + }); + + modelBuilder.Entity("Data.Models.Client.Stats.EFClientStatistics", b => + { + b.Navigation("HitLocations"); + }); + + modelBuilder.Entity("Data.Models.EFAliasLink", b => + { + b.Navigation("Children"); + + b.Navigation("ReceivedPenalties"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.cs b/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.cs new file mode 100644 index 000000000..71f6db17c --- /dev/null +++ b/Data/Migrations/Sqlite/20260117221229_AddEFAnnouncement.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations.Sqlite +{ + /// + public partial class AddEFAnnouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EFAnnouncement", + columns: table => new + { + AnnouncementId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", maxLength: 256, nullable: false), + Content = table.Column(type: "TEXT", maxLength: 4096, nullable: false), + StartAt = table.Column(type: "TEXT", nullable: true), + EndAt = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + IsGlobalNotice = table.Column(type: "INTEGER", nullable: false), + CreatedByClientId = table.Column(type: "INTEGER", nullable: false), + CreatedDateTime = table.Column(type: "TEXT", nullable: false), + UpdatedDateTime = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EFAnnouncement", x => x.AnnouncementId); + table.ForeignKey( + name: "FK_EFAnnouncement_EFClients_CreatedByClientId", + column: x => x.CreatedByClientId, + principalTable: "EFClients", + principalColumn: "ClientId", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_CreatedByClientId", + table: "EFAnnouncement", + column: "CreatedByClientId"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsActive", + table: "EFAnnouncement", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_EFAnnouncement_IsGlobalNotice", + table: "EFAnnouncement", + column: "IsGlobalNotice"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EFAnnouncement"); + } + } +} diff --git a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs index 1783e9b8f..a8d6706ba 100644 --- a/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs +++ b/Data/Migrations/Sqlite/SqliteDatabaseContextModelSnapshot.cs @@ -1014,6 +1014,54 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EFPenaltyIdentifiers", (string)null); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("CreatedByClientId") + .HasColumnType("INTEGER"); + + b.Property("CreatedDateTime") + .HasColumnType("TEXT"); + + b.Property("EndAt") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsGlobalNotice") + .HasColumnType("INTEGER"); + + b.Property("StartAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UpdatedDateTime") + .HasColumnType("TEXT"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("CreatedByClientId"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsGlobalNotice"); + + b.ToTable("EFAnnouncement", (string)null); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.Property("InboxMessageId") @@ -1549,6 +1597,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Penalty"); }); + modelBuilder.Entity("Data.Models.Misc.EFAnnouncement", b => + { + b.HasOne("Data.Models.Client.EFClient", "CreatedByClient") + .WithMany() + .HasForeignKey("CreatedByClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByClient"); + }); + modelBuilder.Entity("Data.Models.Misc.EFInboxMessage", b => { b.HasOne("Data.Models.Client.EFClient", "DestinationClient") diff --git a/Data/Models/Misc/EFAnnouncement.cs b/Data/Models/Misc/EFAnnouncement.cs new file mode 100644 index 000000000..aacf61920 --- /dev/null +++ b/Data/Models/Misc/EFAnnouncement.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Data.Models.Client; +using Stats.Models; + +namespace Data.Models.Misc; + +/// +/// Represents a Message of the Day announcement for the webfront +/// +public class EFAnnouncement : AuditFields +{ + [Key] + public int AnnouncementId { get; set; } + + /// + /// Title/header of the announcement + /// + [Required] + [MaxLength(256)] + public string Title { get; set; } = string.Empty; + + /// + /// Main content/body of the announcement + /// + [Required] + [MaxLength(4096)] + public string Content { get; set; } = string.Empty; + + /// + /// When the announcement should start displaying (null = immediately) + /// + public DateTime? StartAt { get; set; } + + /// + /// When the announcement should stop displaying (null = indefinite) + /// + public DateTime? EndAt { get; set; } + + /// + /// Whether this is the currently active announcement. Only one announcement can be active at a time. + /// + public bool IsActive { get; set; } + + /// + /// Whether this announcement displays on every page (true) or only the home/server overview page (false) + /// + public bool IsGlobalNotice { get; set; } + + /// + /// Client ID of the user who created this announcement + /// + public int CreatedByClientId { get; set; } + + [ForeignKey(nameof(CreatedByClientId))] + public virtual EFClient? CreatedByClient { get; set; } +} diff --git a/Plugins/Stats/Client/HitCalculator.cs b/Plugins/Stats/Client/HitCalculator.cs index e0bcb137d..dc1cd51ee 100644 --- a/Plugins/Stats/Client/HitCalculator.cs +++ b/Plugins/Stats/Client/HitCalculator.cs @@ -23,24 +23,32 @@ namespace IW4MAdmin.Plugins.Stats.Client; -public class HitState +public class HitState : IDisposable { + private bool _disposed; + public HitState() { OnTransaction = new SemaphoreSlim(1, 1); } - ~HitState() - { - OnTransaction.Dispose(); - } - public List Hits { get; set; } public DateTime? LastUsage { get; set; } public int? LastWeaponId { get; set; } public EFServer Server { get; set; } public SemaphoreSlim OnTransaction { get; } public int UpdateCount { get; set; } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + OnTransaction.Dispose(); + } } public class HitCalculator : IClientStatisticCalculator @@ -50,8 +58,6 @@ public class HitCalculator : IClientStatisticCalculator private readonly ConcurrentDictionary _clientHitStatistics = new(); - private readonly SemaphoreSlim _onTransaction = new SemaphoreSlim(1, 1); - private readonly ILookupCache _serverCache; private readonly ILookupCache _hitLocationCache; private readonly ILookupCache _weaponCache; @@ -111,34 +117,35 @@ public async Task CalculateForEvent(CoreEvent coreEvent) if (coreEvent is ClientStateDisposeEvent clientStateDisposeEvent) { - _clientHitStatistics.Remove(clientStateDisposeEvent.Client.ClientId, out var state); - - if (state == null) + // Atomically remove the state to prevent race conditions with hit processing + if (!_clientHitStatistics.TryRemove(clientStateDisposeEvent.Client.ClientId, out var state)) { _logger.LogWarning("No client hit state available for disconnecting client {Client}", clientStateDisposeEvent.Client.ToString()); return; } + var acquired = false; try { await state.OnTransaction.WaitAsync(); + acquired = true; HandleDisconnectCalculations(clientStateDisposeEvent.Client, state); await UpdateClientStatistics(clientStateDisposeEvent.Client.ClientId, state); } - catch (Exception ex) { _logger.LogError(ex, "Could not handle disconnect calculations for client {Client}", clientStateDisposeEvent.Client.ToString()); } - finally { - if (state.OnTransaction.CurrentCount == 0) + if (acquired) { state.OnTransaction.Release(); } + + state.Dispose(); } return; @@ -186,21 +193,41 @@ public async Task CalculateForEvent(CoreEvent coreEvent) _logger.LogDebug("Skipping hit because it does not contain the required data"); continue; } - + + HitState state; try { - await _onTransaction.WaitAsync(); - if (!_clientHitStatistics.ContainsKey(hitInfo.EntityId)) + var needsInitialization = !_clientHitStatistics.ContainsKey(hitInfo.EntityId); + state = _clientHitStatistics.GetOrAdd(hitInfo.EntityId, _ => new HitState + { + Hits = new List() + }); + + // If this is a new state, initialize it with database data + if (needsInitialization || state.Hits.Count == 0) { - _logger.LogDebug("Starting to track hits for {Client}", hitInfo.EntityId); - var clientHits = await GetHitsForClient(hitInfo.EntityId); - _clientHitStatistics.TryAdd(hitInfo.EntityId, new HitState + var acquired = false; + try { - Hits = clientHits, - Server = await _serverCache - .FirstAsync(server => - server.EndPoint == damageEvent.Server.Id && server.HostName != null) - }); + await state.OnTransaction.WaitAsync(); + acquired = true; + + if (state.Hits.Count == 0) + { + _logger.LogDebug("Starting to track hits for {Client}", hitInfo.EntityId); + state.Hits = await GetHitsForClient(hitInfo.EntityId); + state.Server = await _serverCache + .FirstAsync(server => + server.EndPoint == damageEvent.Server.Id && server.HostName != null); + } + } + finally + { + if (acquired) + { + state.OnTransaction.Release(); + } + } } } catch (Exception ex) @@ -209,37 +236,36 @@ public async Task CalculateForEvent(CoreEvent coreEvent) continue; } - finally + // Process hit calculations under per-client lock only + var lockAcquired = false; + try { - if (_onTransaction.CurrentCount == 0) + await state.OnTransaction.WaitAsync(); + lockAcquired = true; + + // Verify state is still valid (client may have disconnected) + if (!_clientHitStatistics.ContainsKey(hitInfo.EntityId)) { - _onTransaction.Release(); + _logger.LogDebug("Client {Client} disconnected during hit processing, skipping", hitInfo.EntityId); + continue; } - } - var state = _clientHitStatistics[hitInfo.EntityId]; - - try - { - await _onTransaction.WaitAsync(); - var calculatedHits = await RunTasksForHitInfo(hitInfo, state.Server.ServerId); + var calculatedHits = await RunTasksForHitInfo(hitInfo, state.Server?.ServerId); foreach (var clientHit in calculatedHits) { RunCalculation(clientHit, hitInfo, state); } } - catch (Exception ex) { _logger.LogError(ex, "Could not update hit calculations for {Client}", hitInfo.EntityId); } - finally { - if (_onTransaction.CurrentCount == 0) + if (lockAcquired) { - _onTransaction.Release(); + state.OnTransaction.Release(); } } } @@ -378,12 +404,15 @@ private async Task UpdateClientStatistics(int clientId, HitState locState = null } } - private async Task GetOrAddClientHit(int clientId, long? serverId = null, + private Task GetOrAddClientHit(int clientId, long? serverId = null, int? hitLocationId = null, int? weaponId = null, int? attachmentComboId = null, int? meansOfDeathId = null) { - var state = _clientHitStatistics[clientId]; - await state.OnTransaction.WaitAsync(); + // Note: Caller must hold state.OnTransaction lock + if (!_clientHitStatistics.TryGetValue(clientId, out var state)) + { + throw new InvalidOperationException($"No hit state found for client {clientId}"); + } var hitStat = state.Hits .FirstOrDefault(hit => hit.HitLocationId == hitLocationId @@ -394,8 +423,7 @@ private async Task GetOrAddClientHit(int clientId, long? s if (hitStat != null) { - state.OnTransaction.Release(); - return hitStat; + return Task.FromResult(hitStat); } hitStat = new EFClientHitStatistic() @@ -410,13 +438,6 @@ private async Task GetOrAddClientHit(int clientId, long? s try { - /*if (state.UpdateCount > MaxUpdatesBeforePersist) - { - await UpdateClientStatistics(clientId); - state.UpdateCount = 0; - } - - state.UpdateCount++;*/ state.Hits.Add(hitStat); } catch (Exception ex) @@ -425,15 +446,8 @@ private async Task GetOrAddClientHit(int clientId, long? s clientId); state.Hits.Remove(hitStat); } - finally - { - if (state.OnTransaction.CurrentCount == 0) - { - state.OnTransaction.Release(); - } - } - return hitStat; + return Task.FromResult(hitStat); } private async Task GetOrAddHitLocation(string location, Reference.Game game) diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 9f8651d5f..382f1cb48 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -1235,45 +1235,45 @@ public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clien await context.SaveChangesAsync(); performances.Add(clientStats); - } - - if (performances.Any(performance => performance.TimePlayed >= minPlayTime)) - { - var aggregateZScore = - performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime); - - int? aggregateRanking = await context.Set() - .Where(stat => stat.ClientId != clientId) - .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime)) - .GroupBy(stat => stat.ClientId) - .Where(group => - group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) > - aggregateZScore) - .Select(c => c.Key) - .CountAsync(); - var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore); - - if (newPerformanceMetric == null) + if (performances.Any(performance => performance.TimePlayed >= minPlayTime)) { - _log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}", - clientStats.Client?.ToString(), aggregateZScore); - return; - } + var aggregateZScore = + performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime); + + int? aggregateRanking = await context.Set() + .Where(stat => stat.ClientId != clientId) + .Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime)) + .GroupBy(stat => stat.ClientId) + .Where(group => + group.Sum(stat => stat.ZScore * stat.TimePlayed) / group.Sum(stat => stat.TimePlayed) > + aggregateZScore) + .Select(c => c.Key) + .CountAsync(); + + var newPerformanceMetric = await _serverDistributionCalculator.GetRatingForZScore(aggregateZScore); + + if (newPerformanceMetric == null) + { + _log.LogWarning("Could not determine performance metric for {Client} {AggregateZScore}", + clientStats.Client?.ToString(), aggregateZScore); + return; + } - var aggregateRankingSnapshot = new EFClientRankingHistory - { - ClientId = clientId, - ZScore = aggregateZScore, - Ranking = aggregateRanking, - PerformanceMetric = newPerformanceMetric, - Newest = true, - }; + var aggregateRankingSnapshot = new EFClientRankingHistory + { + ClientId = clientId, + ZScore = aggregateZScore, + Ranking = aggregateRanking, + PerformanceMetric = newPerformanceMetric, + Newest = true, + }; - context.Add(aggregateRankingSnapshot); + context.Add(aggregateRankingSnapshot); - await PruneOldRankings(context, clientId); - await context.SaveChangesAsync(); + await PruneOldRankings(context, clientId); + await context.SaveChangesAsync(); + } } } diff --git a/SharedLibraryCore/Commands/CommandProcessing.cs b/SharedLibraryCore/Commands/CommandProcessing.cs index 6e9a68c7f..478423911 100644 --- a/SharedLibraryCore/Commands/CommandProcessing.cs +++ b/SharedLibraryCore/Commands/CommandProcessing.cs @@ -24,7 +24,7 @@ public static async Task ValidateCommand(GameEvent gameEvent, Applicati gameEvent.Message = gameEvent.Data; Command matchedCommand = null; - foreach (var availableCommand in manager.GetCommands() + foreach (var availableCommand in manager.Commands .Where(c => c.Name != null)) { if ((availableCommand.SupportedGames?.Any() ?? false) && diff --git a/SharedLibraryCore/Configuration/WebfrontConfiguration.cs b/SharedLibraryCore/Configuration/WebfrontConfiguration.cs index 72908f7a5..6cd65aa47 100644 --- a/SharedLibraryCore/Configuration/WebfrontConfiguration.cs +++ b/SharedLibraryCore/Configuration/WebfrontConfiguration.cs @@ -16,6 +16,11 @@ public class WebfrontConfiguration public bool PreventUserCustomization { get; set; } public bool EnableColorCodes { get; set; } + /// + /// Whether users can dismiss the MOTD announcement. When false, the dismiss button is hidden. + /// + public bool AllowMotdDismiss { get; set; } = true; + /// /// Enable HTTPS with SSL certificate. When enabled, the webfront will serve over HTTPS only. /// @@ -37,7 +42,8 @@ public class WebfrontConfiguration nameof(Permission.User), [ "HelpPage.Read", "ProfilePage.Read", - "Interaction.Read" + "Interaction.Read", + "ChatMessage.Read" ] }, { @@ -48,7 +54,8 @@ public class WebfrontConfiguration "Interaction.Read", "ClientLevel.Read", "ConsolePage.Read", - "PrivilegedClientsPage.Read" + "PrivilegedClientsPage.Read", + "ChatMessage.Read" ] }, { @@ -63,7 +70,8 @@ public class WebfrontConfiguration "RecentPlayersPage.Read", "ClientNote.Read", "ConsolePage.Read", - "AdvancedSearch.Read" + "AdvancedSearch.Read", + "ChatMessage.Read" ] }, { @@ -81,7 +89,9 @@ public class WebfrontConfiguration "MetaAliasUpdate.Read", "ClientGuid.Read", "ConsolePage.Read", - "AuditPage.Read" + "AuditPage.Read", + "AntiCheat.Read", + "ChatMessage.Read" ] }, { diff --git a/SharedLibraryCore/Interfaces/IAnnouncementService.cs b/SharedLibraryCore/Interfaces/IAnnouncementService.cs new file mode 100644 index 000000000..48792a44e --- /dev/null +++ b/SharedLibraryCore/Interfaces/IAnnouncementService.cs @@ -0,0 +1,56 @@ +using Data.Models.Misc; + +namespace SharedLibraryCore.Interfaces; + +/// +/// Service for managing MOTD announcements +/// +public interface IAnnouncementService +{ + /// + /// Gets the currently active announcement that should be displayed + /// + /// If true, only return global announcements; if false, return any active announcement + /// The active announcement or null if none + Task GetActiveAnnouncementAsync(bool globalOnly = false); + + /// + /// Gets all announcements ordered by creation date descending + /// + Task> GetAllAnnouncementsAsync(); + + /// + /// Gets an announcement by its ID + /// + Task GetAnnouncementByIdAsync(int id); + + /// + /// Creates a new announcement + /// + Task CreateAnnouncementAsync(EFAnnouncement announcement); + + /// + /// Updates an existing announcement + /// + Task UpdateAnnouncementAsync(EFAnnouncement announcement); + + /// + /// Deletes an announcement + /// + Task DeleteAnnouncementAsync(int id); + + /// + /// Sets an announcement as the active one (deactivates all others) + /// + Task ActivateAnnouncementAsync(int id); + + /// + /// Deactivates an announcement + /// + Task DeactivateAnnouncementAsync(int id); + + /// + /// Event raised when an announcement is created, updated, or deleted + /// + event Action OnAnnouncementChanged; +} diff --git a/SharedLibraryCore/Interfaces/IManager.cs b/SharedLibraryCore/Interfaces/IManager.cs index 4765dde41..760b14597 100644 --- a/SharedLibraryCore/Interfaces/IManager.cs +++ b/SharedLibraryCore/Interfaces/IManager.cs @@ -38,7 +38,6 @@ public interface IManager IList GetServers(); List Servers { get; } - IList GetCommands(); IList GetMessageTokens(); IList GetActiveClients(); EFClient FindActiveClient(EFClient client); diff --git a/SharedLibraryCore/Interfaces/IMetaServiceV2.cs b/SharedLibraryCore/Interfaces/IMetaServiceV2.cs index c545710a0..4e03cdb20 100644 --- a/SharedLibraryCore/Interfaces/IMetaServiceV2.cs +++ b/SharedLibraryCore/Interfaces/IMetaServiceV2.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Data.Models; +using Data.Models; using SharedLibraryCore.Dtos; using SharedLibraryCore.QueryHelper; @@ -171,6 +167,16 @@ void AddRuntimeMeta(MetaType metaKey, Func>> metaAction) where TReturn : IClientMeta where T : PaginationRequest; + /// + /// adds a meta task to the runtime meta list + /// + /// type of meta + /// action to perform + /// required entity permission + void AddRuntimeMeta(MetaType metaKey, + Func>> metaAction, string entityPermission) + where TReturn : IClientMeta where T : PaginationRequest; + /// /// retrieves all the runtime meta information for given client idea /// diff --git a/SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs b/SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs index b08282967..d86518fbe 100644 --- a/SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs +++ b/SharedLibraryCore/QueryHelper/ClientPaginationRequest.cs @@ -1,10 +1,15 @@ -using SharedLibraryCore.Dtos; +using System.Text.Json.Serialization; +using Data.Models.Client; +using SharedLibraryCore.Dtos; namespace SharedLibraryCore.QueryHelper { public class ClientPaginationRequest : PaginationRequest { public int ClientId { get; set; } + [JsonIgnore] public bool IsPrivileged { get; set; } + [JsonIgnore] + public EFClient.Permission RequestPermission { get; set; } } } diff --git a/SharedLibraryCore/Services/AnnouncementService.cs b/SharedLibraryCore/Services/AnnouncementService.cs new file mode 100644 index 000000000..2ea49a23f --- /dev/null +++ b/SharedLibraryCore/Services/AnnouncementService.cs @@ -0,0 +1,196 @@ +using Data.Abstractions; +using Data.Context; +using Data.Models.Misc; +using Microsoft.EntityFrameworkCore; +using SharedLibraryCore.Interfaces; + +namespace SharedLibraryCore.Services; + +/// +/// Service for managing MOTD announcements +/// +public class AnnouncementService(ILogger logger, IDatabaseContextFactory contextFactory) + : IAnnouncementService +{ + public async Task GetActiveAnnouncementAsync(bool globalOnly = false) + { + await using var context = contextFactory.CreateContext(); + var now = DateTime.UtcNow; + + var query = context.Announcements + .AsNoTracking() + .Where(a => a.IsActive); + + if (globalOnly) + { + query = query.Where(a => a.IsGlobalNotice); + } + + // Filter by date range + query = query.Where(a => + (!a.StartAt.HasValue || a.StartAt <= now) && + (!a.EndAt.HasValue || a.EndAt >= now)); + + return await query + .Include(a => a.CreatedByClient) + .ThenInclude(c => c.CurrentAlias) + .OrderByDescending(a => a.CreatedDateTime) + .FirstOrDefaultAsync(); + } + + public async Task> GetAllAnnouncementsAsync() + { + await using var context = contextFactory.CreateContext(); + return await context.Announcements + .AsNoTracking() + .Include(a => a.CreatedByClient) + .ThenInclude(c => c.CurrentAlias) + .OrderByDescending(a => a.CreatedDateTime) + .ToListAsync(); + } + + public async Task GetAnnouncementByIdAsync(int id) + { + await using var context = contextFactory.CreateContext(); + return await context.Announcements + .AsNoTracking() + .Include(a => a.CreatedByClient) + .ThenInclude(c => c.CurrentAlias) + .FirstOrDefaultAsync(a => a.AnnouncementId == id); + } + + public async Task CreateAnnouncementAsync(EFAnnouncement announcement) + { + await using var context = contextFactory.CreateContext(); + + if (announcement.IsActive) + { + await DeactivateAllAsync(context); + } + + announcement.CreatedDateTime = DateTime.UtcNow; + context.Announcements.Add(announcement); + await context.SaveChangesAsync(); + + logger.LogInformation("Created announcement {AnnouncementId}: {Title}", + announcement.AnnouncementId, announcement.Title); + + NotifyChanged(); + + return announcement; + } + + public async Task UpdateAnnouncementAsync(EFAnnouncement announcement) + { + await using var context = contextFactory.CreateContext(); + + var existing = await context.Announcements + .FirstOrDefaultAsync(a => a.AnnouncementId == announcement.AnnouncementId); + + if (existing == null) + { + throw new InvalidOperationException($"Announcement {announcement.AnnouncementId} not found"); + } + + if (announcement.IsActive && !existing.IsActive) + { + await DeactivateAllAsync(context); + } + + existing.Title = announcement.Title; + existing.Content = announcement.Content; + existing.StartAt = announcement.StartAt; + existing.EndAt = announcement.EndAt; + existing.IsActive = announcement.IsActive; + existing.IsGlobalNotice = announcement.IsGlobalNotice; + existing.UpdatedDateTime = DateTime.UtcNow; + + await context.SaveChangesAsync(); + + logger.LogInformation("Updated announcement {AnnouncementId}: {Title}", + existing.AnnouncementId, existing.Title); + + NotifyChanged(); + + return existing; + } + + public async Task DeleteAnnouncementAsync(int id) + { + await using var context = contextFactory.CreateContext(); + + var announcement = await context.Announcements + .FirstOrDefaultAsync(a => a.AnnouncementId == id); + + if (announcement != null) + { + context.Announcements.Remove(announcement); + await context.SaveChangesAsync(); + + logger.LogInformation("Deleted announcement {AnnouncementId}: {Title}", + announcement.AnnouncementId, announcement.Title); + NotifyChanged(); + } + } + + public async Task ActivateAnnouncementAsync(int id) + { + await using var context = contextFactory.CreateContext(); + + // Deactivate all announcements first + await DeactivateAllAsync(context); + + var announcement = await context.Announcements + .FirstOrDefaultAsync(a => a.AnnouncementId == id); + + if (announcement != null) + { + announcement.IsActive = true; + announcement.UpdatedDateTime = DateTime.UtcNow; + await context.SaveChangesAsync(); + + logger.LogInformation("Activated announcement {AnnouncementId}: {Title}", + announcement.AnnouncementId, announcement.Title); + NotifyChanged(); + } + } + + public async Task DeactivateAnnouncementAsync(int id) + { + await using var context = contextFactory.CreateContext(); + + var announcement = await context.Announcements + .FirstOrDefaultAsync(a => a.AnnouncementId == id); + + if (announcement != null) + { + announcement.IsActive = false; + announcement.UpdatedDateTime = DateTime.UtcNow; + await context.SaveChangesAsync(); + + logger.LogInformation("Deactivated announcement {AnnouncementId}: {Title}", + announcement.AnnouncementId, announcement.Title); + NotifyChanged(); + } + } + + private static async Task DeactivateAllAsync(DatabaseContext context) + { + var activeAnnouncements = await context.Announcements + .Where(a => a.IsActive) + .ToListAsync(); + + foreach (var announcement in activeAnnouncements) + { + announcement.IsActive = false; + announcement.UpdatedDateTime = DateTime.UtcNow; + } + } + + public event Action? OnAnnouncementChanged; + + private void NotifyChanged() + { + OnAnnouncementChanged?.Invoke(); + } +} diff --git a/WebfrontCore/Components/Features/Admin/Models/AnnouncementModels.cs b/WebfrontCore/Components/Features/Admin/Models/AnnouncementModels.cs new file mode 100644 index 000000000..34fdc4b7a --- /dev/null +++ b/WebfrontCore/Components/Features/Admin/Models/AnnouncementModels.cs @@ -0,0 +1,47 @@ +namespace WebfrontCore.Components.Features.Admin.Models; + +/// +/// DTO for displaying announcement information +/// +public class AnnouncementInfo +{ + public int AnnouncementId { get; set; } + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime? StartAt { get; set; } + public DateTime? EndAt { get; set; } + public bool IsActive { get; set; } + public bool IsGlobalNotice { get; set; } + public string CreatedByName { get; set; } = string.Empty; + public int CreatedByClientId { get; set; } + public DateTime CreatedDateTime { get; set; } + public DateTime? UpdatedDateTime { get; set; } + + /// + /// Whether the announcement is currently within its display date range + /// + public bool IsCurrentlyDisplayable => IsActive && + (!StartAt.HasValue || StartAt <= DateTime.UtcNow) && + (!EndAt.HasValue || EndAt >= DateTime.UtcNow); +} + +/// +/// Request model for creating a new announcement +/// +public class CreateAnnouncementRequest +{ + public string Title { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; + public DateTime? StartAt { get; set; } + public DateTime? EndAt { get; set; } + public bool IsActive { get; set; } + public bool IsGlobalNotice { get; set; } +} + +/// +/// Request model for updating an existing announcement +/// +public class UpdateAnnouncementRequest : CreateAnnouncementRequest +{ + public int AnnouncementId { get; set; } +} diff --git a/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor new file mode 100644 index 000000000..c8f287d11 --- /dev/null +++ b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor @@ -0,0 +1,223 @@ +@page "/Admin/Announcements" +@page "/announcements" +@rendermode InteractiveServer +@attribute [Authorize(Policy = "Permissions.AnnouncementPage.Read")] + +@inject IWebfrontDataService DataService +@inject ITranslationLookup Loc +@inject AppState AppState + +@Loc["WEBFRONT_NAV_ANNOUNCEMENTS"] | @AppState.WebfrontBranding + +
+ +
+
+ +
+
+

@Loc["WEBFRONT_NAV_ANNOUNCEMENTS"]

+
+ @Loc["WEBFRONT_ANNOUNCEMENT_SUBTITLE"] +
+
+ +
+ + + +
+
+ + @if (_showForm) + { +
+

+ + @(_editingId.HasValue ? Loc["WEBFRONT_ACTION_EDIT"] : Loc["WEBFRONT_ACTION_CREATE"]) +

+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ @Loc["WEBFRONT_ANNOUNCEMENT_IS_GLOBAL"] + @Loc["WEBFRONT_ANNOUNCEMENT_IS_GLOBAL_DESC"] +
+
+
+ +
+ @Loc["WEBFRONT_ANNOUNCEMENT_IS_ACTIVE"] + @Loc["WEBFRONT_ANNOUNCEMENT_IS_ACTIVE_DESC"] +
+
+
+ +
+ + +
+ + @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ + @_errorMessage +
+ } +
+
+
+ } + +
+ @if (_announcements == null) + { +
+ + @Loc["WEBFRONT_STATUS_LOADING"] +
+ } + else if (!_announcements.Any()) + { +
+
+ +
+

@Loc["WEBFRONT_ANNOUNCEMENT_NONE"]

+

@Loc["WEBFRONT_ANNOUNCEMENT_NONE_DESC"]

+
+ } + else + { + @foreach (var announcement in _announcements) + { +
+ @if (announcement.IsActive) + { +
+ } + +
+
+
+

@announcement.Title

+ + @if (announcement.IsActive) + { + + @Loc["WEBFRONT_ANNOUNCEMENT_ACTIVE"] + + } + else + { + + @Loc["WEBFRONT_ANNOUNCEMENT_INACTIVE"] + + } + + @if (announcement.IsGlobalNotice) + { + + @Loc["WEBFRONT_ANNOUNCEMENT_GLOBAL"] + + } +
+ +

@announcement.Content

+ +
+ + + @(announcement.StartAt?.ToString("g") ?? Loc["WEBFRONT_ANNOUNCEMENT_TIME_NOW"]) + + + + @(announcement.EndAt?.ToString("g") ?? Loc["WEBFRONT_ANNOUNCEMENT_TIME_FOREVER"]) + + + +
+
+ +
+ + @if (announcement.IsActive) + { + + + + } + else + { + + + + } + + + + + + + + + + + +
+
+
+ } + } +
+
diff --git a/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor.cs b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor.cs new file mode 100644 index 000000000..0e96c42a3 --- /dev/null +++ b/WebfrontCore/Components/Features/Admin/Pages/AnnouncementManagement.razor.cs @@ -0,0 +1,123 @@ +using WebfrontCore.Components.Features.Admin.Models; + +namespace WebfrontCore.Components.Features.Admin.Pages; + +public partial class AnnouncementManagement +{ + private IEnumerable? _announcements; + private bool _showForm; + private int? _editingId; + private string _formTitle = ""; + private string _formContent = ""; + private DateTime? _formStartAt; + private DateTime? _formEndAt; + private bool _formIsGlobal; + private bool _formIsActive; + private string? _errorMessage; + + protected override async Task OnInitializedAsync() + { + await LoadAnnouncements(); + } + + private async Task LoadAnnouncements() + { + _announcements = await DataService.GetAllAnnouncementsAsync(); + } + + private void ShowCreateForm() + { + _editingId = null; + _formTitle = ""; + _formContent = ""; + _formStartAt = null; + _formEndAt = null; + _formIsGlobal = false; + _formIsActive = false; + _errorMessage = null; + _showForm = true; + } + + private void EditAnnouncement(AnnouncementInfo announcement) + { + _editingId = announcement.AnnouncementId; + _formTitle = announcement.Title; + _formContent = announcement.Content; + _formStartAt = announcement.StartAt; + _formEndAt = announcement.EndAt; + _formIsGlobal = announcement.IsGlobalNotice; + _formIsActive = announcement.IsActive; + _errorMessage = null; + _showForm = true; + } + + private void CancelForm() + { + _showForm = false; + _editingId = null; + _errorMessage = null; + } + + private async Task SaveAnnouncement() + { + if (string.IsNullOrWhiteSpace(_formTitle)) + { + _errorMessage = Loc["WEBFRONT_VALIDATION_REQUIRED"]; + return; + } + + try + { + if (_editingId.HasValue) + { + await DataService.UpdateAnnouncementAsync(new UpdateAnnouncementRequest + { + AnnouncementId = _editingId.Value, + Title = _formTitle, + Content = _formContent, + StartAt = _formStartAt, + EndAt = _formEndAt, + IsGlobalNotice = _formIsGlobal, + IsActive = _formIsActive + }); + } + else + { + await DataService.CreateAnnouncementAsync(new CreateAnnouncementRequest + { + Title = _formTitle, + Content = _formContent, + StartAt = _formStartAt, + EndAt = _formEndAt, + IsGlobalNotice = _formIsGlobal, + IsActive = _formIsActive + }, AppState.User?.ClientId ?? 0); + } + + CancelForm(); + await LoadAnnouncements(); + } + catch (Exception ex) + { + _errorMessage = ex.Message; + } + } + + private async Task ActivateAnnouncement(int id) + { + await DataService.ActivateAnnouncementAsync(id); + await LoadAnnouncements(); + } + + private async Task DeactivateAnnouncement(int id) + { + await DataService.DeactivateAnnouncementAsync(id); + await LoadAnnouncements(); + } + + private async Task DeleteAnnouncement(int id) + { + await DataService.DeleteAnnouncementAsync(id); + await LoadAnnouncements(); + } +} \ No newline at end of file diff --git a/WebfrontCore/Components/Features/Clients/Components/ClientActivitySparkline.razor b/WebfrontCore/Components/Features/Clients/Components/ClientActivitySparkline.razor index 6cb4a79f6..d6ea5166d 100644 --- a/WebfrontCore/Components/Features/Clients/Components/ClientActivitySparkline.razor +++ b/WebfrontCore/Components/Features/Clients/Components/ClientActivitySparkline.razor @@ -21,7 +21,7 @@ var value = _activityData[i]; var date = DateTime.UtcNow.Date.AddDays(-29 + i); var height = value > 0 ? Math.Max(4, (int)(value / _maxValue * 32)) : 2; - var colorClass = value > 0 ? "bg-success" : "bg-line"; + var colorClass = value > 0 ? "bg-primary" : "bg-line"; var playTime = TimeSpan.FromMinutes(value); var tooltipText = value > 0 ? $"{FormatPlaytime(playTime)} • {date:MMM d}" diff --git a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor index 9b0f8bd59..a799f2455 100644 --- a/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor +++ b/WebfrontCore/Components/Features/Clients/Components/ClientMetaList.razor @@ -44,9 +44,13 @@ else
@{ var (dotColor, _) = GetItemStyle(item); + var utcTime = item.When.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + var tooltipText = $"{item.Type.ToTranslatedName()} • {utcTime}"; } -
+ +
+
@* Content *@ @@ -189,7 +193,7 @@ else var state = GetState(Meta); } -
+
@foreach (var match in Utilities.SplitTranslationTokens(AppState.Loc(localizationKey))) { @@ -239,7 +243,7 @@ else var state = GetState(Meta); } -
+
@foreach (var match in Utilities.SplitTranslationTokens(AppState.Loc(localizationKey))) { @@ -373,7 +377,7 @@ else ; RenderFragment UpdatedAliasTemplate => (Meta) => @ -
+
@foreach (var token in Utilities.SplitTranslationTokens(AppState.Loc("WEBFRONT_PROFILE_META_CONNECT_ALIAS"))) { if (token.IsInterpolation) @@ -406,7 +410,7 @@ else @{ var localizationKey = $"WEBFRONT_CLIENT_META_CONNECTION_{Meta.ConnectionType.ToString().ToUpper()}"; } -
+
@foreach (var token in Utilities.SplitTranslationTokens(AppState.Loc(localizationKey))) { if (token.IsInterpolation) @@ -435,7 +439,7 @@ else ; RenderFragment PermissionLevelChangedTemplate => (Meta) => @ -
+
@foreach (var token in Utilities.SplitTranslationTokens(AppState.Loc("WEBFRONT_CLIENT_META_PERMISSION_CHANGED"))) { if (token.IsInterpolation) @@ -467,7 +471,7 @@ else ; RenderFragment ClientNoteTemplate => (Meta) => @ -
+
@AppState.Loc("WEBFRONT_CLIENT_META_NOTE") @Meta.Note diff --git a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor index e307c9748..e6f21a949 100644 --- a/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor +++ b/WebfrontCore/Components/Features/Clients/Pages/AdvancedFind.razor @@ -75,6 +75,10 @@ + @if (client.CurrentClientName != client.MatchedClientName) + { + [] + } @@ -140,6 +144,10 @@
+ @if (client.CurrentClientName != client.MatchedClientName) + { + [] + } @(global::SharedLibraryCore.Utilities.MakeAbbreviation(AppState.Loc("GAME_" + client.Game))) diff --git a/WebfrontCore/Components/Features/Clients/Pages/Profile.razor.cs b/WebfrontCore/Components/Features/Clients/Pages/Profile.razor.cs index e05959c72..12f375f37 100644 --- a/WebfrontCore/Components/Features/Clients/Pages/Profile.razor.cs +++ b/WebfrontCore/Components/Features/Clients/Pages/Profile.razor.cs @@ -79,9 +79,29 @@ private IEnumerable GetFilterableMetaTypes() var ignoredTypes = new[] { MetaType.Information, MetaType.Other, MetaType.QuickMessage }; return Enum.GetValues() .Where(meta => !ignoredTypes.Contains(meta)) + .Where(meta => + { + if (meta == MetaType.All) return true; + var entity = GetEntityForMetaType(meta); + return entity == WebfrontEntity.Default || HasPermission(entity, WebfrontPermission.Read); + }) .OrderByDescending(meta => meta == MetaType.All); } + private static WebfrontEntity GetEntityForMetaType(MetaType type) + { + return type switch + { + MetaType.AliasUpdate => WebfrontEntity.MetaAliasUpdate, + MetaType.ChatMessage => WebfrontEntity.ChatMessage, + MetaType.Penalized => WebfrontEntity.Penalty, + MetaType.ReceivedPenalty => WebfrontEntity.Penalty, + MetaType.ConnectionHistory => WebfrontEntity.ClientIPAddress, + MetaType.PermissionLevel => WebfrontEntity.ClientLevel, + _ => WebfrontEntity.Default + }; + } + private static string GetShortCode(string name) { if (string.IsNullOrEmpty(name)) diff --git a/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor b/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor new file mode 100644 index 000000000..b5631f888 --- /dev/null +++ b/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor @@ -0,0 +1,41 @@ +@inject IWebfrontDataService DataService +@inject IJSRuntime JsRuntime +@inject ITranslationLookup Loc +@inject IAnnouncementService AnnouncementService +@inject AppState AppState +@implements IDisposable + +@if (_announcement != null && !_isDismissed) +{ +
+
+ +
+
+
+ @_announcement.Title + @if (!string.IsNullOrEmpty(_announcement.CreatedByName)) + { + + + @_announcement.CreatedByName + + } +
+ @_announcement.Content +
+ @if (_allowDismiss) + { + + + + } +
+} diff --git a/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor.cs b/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor.cs new file mode 100644 index 000000000..eac5efc0e --- /dev/null +++ b/WebfrontCore/Components/Features/Home/AnnouncementBanner.razor.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using WebfrontCore.Components.Features.Admin.Models; + +namespace WebfrontCore.Components.Features.Home; + +public partial class AnnouncementBanner +{ + /// + /// If true, only show global/sitewide announcements. If false, show any active announcement. + /// + [Parameter] + public bool GlobalOnly { get; set; } + + [Parameter] public bool ExcludeGlobal { get; set; } + private AnnouncementInfo? _announcement; + private bool _isDismissed = true; + private bool _allowDismiss = true; + private string _dismissKey = ""; + + protected override async Task OnInitializedAsync() + { + _allowDismiss = AppState.WebfrontConfig.AllowMotdDismiss; + AnnouncementService.OnAnnouncementChanged += OnAnnouncementChanged; + await LoadAnnouncement(); + } + + private async Task LoadAnnouncement() + { + _announcement = await DataService.GetActiveAnnouncementAsync(GlobalOnly); + + if (_announcement != null) + { + if (ExcludeGlobal && _announcement.IsGlobalNotice) + { + _announcement = null; + } + else + { + _dismissKey = $"motd_dismiss_{_announcement.AnnouncementId}"; + } + } + + StateHasChanged(); + } + + private void OnAnnouncementChanged() + { + _ = InvokeAsync(async () => + { + await LoadAnnouncement(); + // Re-check dismissal status for the new announcement if needed + if (_announcement != null) + { + var isDismissedStr = await JsRuntime.InvokeAsync("localStorage.getItem", _dismissKey); + _isDismissed = !string.IsNullOrEmpty(isDismissedStr); + + StateHasChanged(); + } + }); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender && _announcement != null) + { + var isDismissedStr = await JsRuntime.InvokeAsync("localStorage.getItem", _dismissKey); + if (string.IsNullOrEmpty(isDismissedStr)) + { + _isDismissed = false; + StateHasChanged(); + } + } + } + + private async Task Dismiss() + { + _isDismissed = true; + if (_announcement != null) + { + await JsRuntime.InvokeVoidAsync("localStorage.setItem", _dismissKey, "true"); + } + } + + public void Dispose() + { + AnnouncementService.OnAnnouncementChanged -= OnAnnouncementChanged; + } +} \ No newline at end of file diff --git a/WebfrontCore/Components/Features/Home/Pages/Home.razor b/WebfrontCore/Components/Features/Home/Pages/Home.razor index edbba7a57..96218cc94 100644 --- a/WebfrontCore/Components/Features/Home/Pages/Home.razor +++ b/WebfrontCore/Components/Features/Home/Pages/Home.razor @@ -15,6 +15,9 @@
+ + +
@ChildContent -
+
+ class="bg-surface-alt text-foreground text-xs px-3 py-2 rounded-lg shadow-xl border border-line w-max max-w-[200px] md:max-w-[320px] text-center whitespace-normal break-words @(_showMobile ? "select-text" : "")"> @Text
@@ -55,11 +55,13 @@ else private string BaseVisibilityClasses => "opacity-0 invisible group-hover/tooltip:opacity-100 group-hover/tooltip:visible " + (_showMobile ? "!opacity-100 !visible " : "") + - "transition-all duration-200 pointer-events-none"; + "transition-all duration-200 " + + (_showMobile ? "" : "pointer-events-none"); private void ToggleTooltip() { // Only toggle on touch devices (mobile) + // Clicks on tooltip content are prevented from reaching here via @onclick:stopPropagation _showMobile = !_showMobile; } diff --git a/WebfrontCore/Components/UI/Layout/MainLayout.razor b/WebfrontCore/Components/UI/Layout/MainLayout.razor index afc38db3b..cb8ce1295 100644 --- a/WebfrontCore/Components/UI/Layout/MainLayout.razor +++ b/WebfrontCore/Components/UI/Layout/MainLayout.razor @@ -17,7 +17,7 @@ else - +
@@ -30,6 +30,9 @@ else + + + @Body diff --git a/WebfrontCore/Components/UI/Layout/NavMenu.razor b/WebfrontCore/Components/UI/Layout/NavMenu.razor index a59c6101e..cfed690ab 100644 --- a/WebfrontCore/Components/UI/Layout/NavMenu.razor +++ b/WebfrontCore/Components/UI/Layout/NavMenu.razor @@ -170,6 +170,16 @@ + + + + + @(Loc("WEBFRONT_NAV_ANNOUNCEMENTS")) + + + diff --git a/WebfrontCore/Components/_Imports.razor b/WebfrontCore/Components/_Imports.razor index b4ff284a2..209e787e9 100644 --- a/WebfrontCore/Components/_Imports.razor +++ b/WebfrontCore/Components/_Imports.razor @@ -45,11 +45,13 @@ @using WebfrontCore @using WebfrontCore.Components.Features @using WebfrontCore.Components.Features.Admin.Components +@using WebfrontCore.Components.Features.Admin.Models @using WebfrontCore.Components.Features.Auth.Components @using WebfrontCore.Components.Features.Clients.Components @using WebfrontCore.Components.Features.Search.Components @using WebfrontCore.Components.Features.Servers.Components @using WebfrontCore.Components.Features.Servers.Models +@using WebfrontCore.Components.Features.Home @using WebfrontCore.Components.UI.Layout @using WebfrontCore.Components.UI.Navigation diff --git a/WebfrontCore/Controllers/API/ClientController.cs b/WebfrontCore/Controllers/API/ClientController.cs index a70a6a35b..9f48414fa 100644 --- a/WebfrontCore/Controllers/API/ClientController.cs +++ b/WebfrontCore/Controllers/API/ClientController.cs @@ -163,7 +163,7 @@ public async Task Login([FromRoute] int clientId, [FromBody, Requ } } - [HttpPost("/logout")] + [HttpPost("logout")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task Logout() @@ -174,7 +174,7 @@ public async Task Logout() { Origin = Client, Type = GameEvent.EventType.Logout, - Owner = Manager.GetServers().First(), + Owner = Manager.Servers.First(), Data = HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var gameStringValues) ? gameStringValues.ToString() : HttpContext.Connection.RemoteIpAddress?.ToString() diff --git a/WebfrontCore/Core/Auth/AccountController.cs b/WebfrontCore/Core/Auth/AccountController.cs index abc1f64d9..bc0d3860e 100644 --- a/WebfrontCore/Core/Auth/AccountController.cs +++ b/WebfrontCore/Core/Auth/AccountController.cs @@ -56,7 +56,7 @@ public async Task Login([FromForm] LoginRequest request) { Origin = privilegedClient, Type = GameEvent.EventType.Login, - Owner = Manager.GetServers().First(), + Owner = Manager.Servers.First(), Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For") ? HttpContext.Request.Headers["X-Forwarded-For"].ToString() : HttpContext.Connection.RemoteIpAddress?.ToString() @@ -93,7 +93,7 @@ public async Task Logout() { Origin = Client, Type = GameEvent.EventType.Logout, - Owner = Manager.GetServers().First(), + Owner = Manager.Servers.First(), Data = HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var value) ? value.ToString() : HttpContext.Connection.RemoteIpAddress?.ToString() diff --git a/WebfrontCore/Core/Auth/WebfrontEntity.cs b/WebfrontCore/Core/Auth/WebfrontEntity.cs index f25a83eb1..f149cb6ae 100644 --- a/WebfrontCore/Core/Auth/WebfrontEntity.cs +++ b/WebfrontCore/Core/Auth/WebfrontEntity.cs @@ -2,6 +2,7 @@ public enum WebfrontEntity { + Default, ClientIPAddress, ClientGuid, ClientLevel, @@ -19,6 +20,9 @@ public enum WebfrontEntity Interaction, AdvancedSearch, AuditLogDataDetails, + AnnouncementPage, + Announcement, + ChatMessage } public enum WebfrontPermission diff --git a/WebfrontCore/Core/Services/AppState.cs b/WebfrontCore/Core/Services/AppState.cs index 07d10b7b0..3a6c617cf 100644 --- a/WebfrontCore/Core/Services/AppState.cs +++ b/WebfrontCore/Core/Services/AppState.cs @@ -12,6 +12,8 @@ public class AppState(ApplicationConfiguration appConfig) public string WebfrontBranding => !string.IsNullOrEmpty(appConfig.Webfront.CustomBranding) ? appConfig.Webfront.CustomBranding : "IW4MAdmin"; + + public WebfrontConfiguration WebfrontConfig => appConfig.Webfront; public bool SidebarCollapsed { diff --git a/WebfrontCore/Core/Services/IWebfrontDataService.cs b/WebfrontCore/Core/Services/IWebfrontDataService.cs index 00499b1f4..de779af36 100644 --- a/WebfrontCore/Core/Services/IWebfrontDataService.cs +++ b/WebfrontCore/Core/Services/IWebfrontDataService.cs @@ -77,5 +77,12 @@ public interface IWebfrontDataService /// Cancellation token /// True if server was removed successfully Task RemoveServerAsync(string serverId, bool persist = false, CancellationToken token = default); + + Task GetActiveAnnouncementAsync(bool globalOnly = false); + Task> GetAllAnnouncementsAsync(); + Task CreateAnnouncementAsync(CreateAnnouncementRequest request, int createdByClientId); + Task UpdateAnnouncementAsync(UpdateAnnouncementRequest request); + Task DeleteAnnouncementAsync(int id); + Task ActivateAnnouncementAsync(int id); + Task DeactivateAnnouncementAsync(int id); } - diff --git a/WebfrontCore/Core/Services/WebfrontDataService.cs b/WebfrontCore/Core/Services/WebfrontDataService.cs index 8e787679c..f0bc65826 100644 --- a/WebfrontCore/Core/Services/WebfrontDataService.cs +++ b/WebfrontCore/Core/Services/WebfrontDataService.cs @@ -50,6 +50,7 @@ public class WebfrontDataService : IWebfrontDataService private readonly ITranslationLookup _translationLookup; private readonly SharedLibraryCore.Configuration.ApplicationConfiguration _appConfig; private readonly ILookup _pluginTypeNames; + private readonly IAnnouncementService _announcementService; public WebfrontDataService(IManager manager, IServerDataViewer serverDataViewer, @@ -72,7 +73,8 @@ public WebfrontDataService(IManager manager, ITranslationLookup translationLookup, ApplicationConfiguration appConfig, IEnumerable v1Plugins, - IEnumerable v2Plugins) + IEnumerable v2Plugins, + IAnnouncementService announcementService) { _manager = manager; _serverDataViewer = serverDataViewer; @@ -97,6 +99,7 @@ public WebfrontDataService(IManager manager, _pluginTypeNames = v1Plugins.Select(plugin => (plugin.GetType(), plugin.Name)) .Concat(v2Plugins.Select(plugin => (plugin.GetType(), plugin.Name))) .ToLookup(selector => selector.Item1, selector => selector.Name); + _announcementService = announcementService; } public async Task> GetServersAsync(Reference.Game? game = null) @@ -519,7 +522,8 @@ public async Task GetNavigationDataAsync() var meta = await _metaService.GetRuntimeMeta(new ClientPaginationRequest { ClientId = client.ClientId, - Before = DateTime.UtcNow + Before = DateTime.UtcNow, + RequestPermission = GetRequestingPermission() }, MetaType.Information); @@ -606,6 +610,7 @@ public async Task> GetClientMetaAsync(ClientMetaRe Count = request.Count, Offset = request.Offset, Before = request.StartAt.HasValue ? DateTime.FromFileTimeUtc(request.StartAt.Value) : DateTime.UtcNow, + RequestPermission = level, IsPrivileged = level >= Data.Models.Client.EFClient.Permission.Trusted }; @@ -658,25 +663,6 @@ public async Task> GetClientMetaAsync(ClientMetaRe } return meta?.Cast().ToList() ?? []; - - async Task> PostProcessChatMeta(Task> metaResult) - { - var result = (await metaResult).ToList(); - - foreach (var m in result) - { - if (m is not MessageResponse mr) - { - continue; - } - - mr.Message = mr.IsHidden && level < Data.Models.Client.EFClient.Permission.Trusted - ? mr.HiddenMessage - : mr.Message; - } - - return result; - } } public Task GetServerScoreboardAsync(string serverId) @@ -1105,7 +1091,7 @@ public async Task> GetHelpCommandsAsync() userLevel = user.Level; } - var commands = _manager.GetCommands() + var commands = _manager.Commands .Where(command => command.Permission <= userLevel) .OrderByDescending(command => command.Permission) .GroupBy(command => @@ -1251,12 +1237,15 @@ public async Task> GetChatContextAsync(string serverId, var whenUpper = whenTime.AddMinutes(5); var whenLower = whenTime.AddMinutes(-5); + var level = GetRequestingPermission(); + var messages = await _chatQueryHelper.QueryResource(new ChatSearchQuery { ServerId = serverId, SentBefore = whenUpper, SentAfter = whenLower, - IsPrivileged = GetRequestingPermission() > Data.Models.Client.EFClient.Permission.Trusted + RequestPermission = level, + IsPrivileged = level > Data.Models.Client.EFClient.Permission.Trusted }); return messages.Results.OrderBy(message => message.When).ToList(); @@ -1451,7 +1440,7 @@ public async Task LoginAsync(ServiceLoginRequest request) { Origin = privilegedClient, Type = GameEvent.EventType.Login, - Owner = _manager.GetServers().First(), + Owner = _manager.Servers.First(), Data = request.IpAddress }); @@ -1584,4 +1573,81 @@ public async Task RemoveServerAsync(string serverId, bool persist = false, { return await _manager.RemoveServerAsync(serverId, persist, token); } + + public async Task GetActiveAnnouncementAsync(bool globalOnly = false) + { + var announcement = await _announcementService.GetActiveAnnouncementAsync(globalOnly); + return announcement == null ? null : MapToAnnouncementInfo(announcement); + } + + public async Task> GetAllAnnouncementsAsync() + { + var announcements = await _announcementService.GetAllAnnouncementsAsync(); + return announcements.Select(MapToAnnouncementInfo); + } + + public async Task CreateAnnouncementAsync(CreateAnnouncementRequest request, int createdByClientId) + { + var announcement = new Data.Models.Misc.EFAnnouncement + { + Title = request.Title, + Content = request.Content, + StartAt = request.StartAt, + EndAt = request.EndAt, + IsActive = request.IsActive, + IsGlobalNotice = request.IsGlobalNotice, + CreatedByClientId = createdByClientId + }; + var result = await _announcementService.CreateAnnouncementAsync(announcement); + return MapToAnnouncementInfo(result); + } + + public async Task UpdateAnnouncementAsync(UpdateAnnouncementRequest request) + { + var announcement = new Data.Models.Misc.EFAnnouncement + { + AnnouncementId = request.AnnouncementId, + Title = request.Title, + Content = request.Content, + StartAt = request.StartAt, + EndAt = request.EndAt, + IsActive = request.IsActive, + IsGlobalNotice = request.IsGlobalNotice + }; + var result = await _announcementService.UpdateAnnouncementAsync(announcement); + return MapToAnnouncementInfo(result); + } + + public async Task DeleteAnnouncementAsync(int id) + { + await _announcementService.DeleteAnnouncementAsync(id); + } + + public async Task ActivateAnnouncementAsync(int id) + { + await _announcementService.ActivateAnnouncementAsync(id); + } + + public async Task DeactivateAnnouncementAsync(int id) + { + await _announcementService.DeactivateAnnouncementAsync(id); + } + + private static AnnouncementInfo MapToAnnouncementInfo(Data.Models.Misc.EFAnnouncement announcement) + { + return new AnnouncementInfo + { + AnnouncementId = announcement.AnnouncementId, + Title = announcement.Title, + Content = announcement.Content, + StartAt = announcement.StartAt, + EndAt = announcement.EndAt, + IsActive = announcement.IsActive, + IsGlobalNotice = announcement.IsGlobalNotice, + CreatedByName = announcement.CreatedByClient?.CurrentAlias?.Name ?? "Unknown", + CreatedByClientId = announcement.CreatedByClientId, + CreatedDateTime = announcement.CreatedDateTime, + UpdatedDateTime = announcement.UpdatedDateTime + }; + } } diff --git a/WebfrontCore/Program.cs b/WebfrontCore/Program.cs index 4f4359ed6..a0af50049 100644 --- a/WebfrontCore/Program.cs +++ b/WebfrontCore/Program.cs @@ -142,20 +142,17 @@ private static void ConfigureServices(IServiceCollection services) }); }); - services.AddStackPolicy(options => + services.AddRateLimiter(options => { - options.MaxConcurrentRequests = - int.Parse(Environment.GetEnvironmentVariable("MaxConcurrentRequests") ?? "1"); - options.RequestQueueLimit = int.Parse(Environment.GetEnvironmentVariable("RequestQueueLimit") ?? "1"); + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.AddConcurrencyLimiter("concurrencyPolicy", opt => + { + opt.PermitLimit = 30; + opt.QueueLimit = 10; + opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + }); }); - services.AddRateLimiter(options => options.AddConcurrencyLimiter("concurrencyPolicy", opt => - { - opt.PermitLimit = 2; - opt.QueueLimit = 25; - opt.QueueProcessingOrder = QueueProcessingOrder.NewestFirst; - })); - // Add framework services var mvcBuilder = services.AddControllers(options => options.SuppressAsyncSuffixInActionNames = false); services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters(); diff --git a/WebfrontCore/wwwroot/images/banners/iw7.jpg b/WebfrontCore/wwwroot/images/banners/iw7.jpg new file mode 100644 index 000000000..93aae2bd4 Binary files /dev/null and b/WebfrontCore/wwwroot/images/banners/iw7.jpg differ diff --git a/WebfrontCore/wwwroot/images/icons/iw7.jpg b/WebfrontCore/wwwroot/images/icons/iw7.jpg new file mode 100644 index 000000000..c24f8f626 Binary files /dev/null and b/WebfrontCore/wwwroot/images/icons/iw7.jpg differ