diff --git a/UncreatedWarfare/Commands/GroupCommand.cs b/UncreatedWarfare/Commands/GroupCommand.cs index a2971564..02aff965 100644 --- a/UncreatedWarfare/Commands/GroupCommand.cs +++ b/UncreatedWarfare/Commands/GroupCommand.cs @@ -107,9 +107,20 @@ public async UniTask ExecuteAsync(CancellationToken token) throw Context.Reply(_translations.GroupNotFound, groupId.ToString(Data.LocalLocale)); } - if (!Context.Player.UnturnedPlayer.quests.ServerAssignToGroup(groupInfo.groupID, EPlayerGroupRank.MEMBER, true)) + if (newTeam.IsValid) { - throw Context.Reply(_translations.GroupNotFound, groupId.ToString(Data.LocalLocale)); + await _teamManager.JoinTeamAsync(Context.Player, newTeam, token); + if (Context.Player.Team != newTeam) + { + throw Context.Reply(_translations.GroupNotFound, groupId.ToString(Data.LocalLocale)); + } + } + else + { + if (!Context.Player.UnturnedPlayer.quests.ServerAssignToGroup(groupInfo.groupID, EPlayerGroupRank.MEMBER, true)) + { + throw Context.Reply(_translations.GroupNotFound, groupId.ToString(Data.LocalLocale)); + } } if (newTeam != null && newTeam.IsValid) diff --git a/UncreatedWarfare/Commands/Migrate/MigrateLegacyPointsCommand.cs b/UncreatedWarfare/Commands/Migrate/MigrateLegacyPointsCommand.cs new file mode 100644 index 00000000..b782b6d8 --- /dev/null +++ b/UncreatedWarfare/Commands/Migrate/MigrateLegacyPointsCommand.cs @@ -0,0 +1,106 @@ +#if DEBUG +using Microsoft.EntityFrameworkCore; +using MySqlConnector; +using System.Collections.Generic; +using System.Globalization; +using Uncreated.Warfare.Database; +using Uncreated.Warfare.Database.Manual; +using Uncreated.Warfare.Interaction.Commands; +using Uncreated.Warfare.Models.Factions; +using Uncreated.Warfare.Stats; + +namespace Uncreated.Warfare.Commands; + +[Command("offenses"), HideFromHelp, SubCommandOf(typeof(MigrateCommand))] +public class MigrateLegacyPointsCommand : IExecutableCommand +{ + private readonly IManualMySqlProvider _mySqlProvider; + private readonly IPointsStore _pointsSql; + private readonly WarfareDbContext _dbContext; + public CommandContext Context { get; set; } + + public MigrateLegacyPointsCommand(IManualMySqlProvider mySqlProvider, IPointsStore pointsSql, WarfareDbContext dbContext) + { + _mySqlProvider = mySqlProvider; + _pointsSql = pointsSql; + _dbContext = dbContext; + } + + public async UniTask ExecuteAsync(CancellationToken token) + { + Context.AssertRanByTerminal(); + + if (Context.TryGet(0, out int seasonId)) + { + if (seasonId is < 1 or > 3) + throw Context.ReplyString("Only seasons 1, 2, 3 can be migrated."); + + await MigrateSeason(seasonId, token); + return; + } + + for (int i = 1; i <= 3; ++i) + { + await MigrateSeason(i, token); + } + } + + private async Task MigrateSeason(int seasonId, CancellationToken token) + { + string tableName = seasonId == 1 ? "levels" : ("s" + seasonId.ToString(CultureInfo.InvariantCulture) + "_levels"); + string creditsName = seasonId == 1 ? "OfficerPoints" : "Credits"; + string xpName = seasonId == 1 ? "XP" : "Experience"; + + // s0 (no longer have this data), s1 (usa vs russia), s2 (usa vs russia), s3 (usa vs mec) + (string t1, string t2) = seasonId switch + { + 3 => ("usa", "mec"), + _ => ("usa", "russia") + }; + + Faction t1Faction = await _dbContext.Factions.FirstAsync(x => x.InternalName == t1, token).ConfigureAwait(false); + Faction t2Faction = await _dbContext.Factions.FirstAsync(x => x.InternalName == t2, token).ConfigureAwait(false); + + try + { + int rowCt = 0; + List points = new List(65536); + + await _mySqlProvider.QueryAsync($"SELECT `Steam64`,`Team`,`{xpName}`,`{creditsName}` FROM `{tableName}`;", null, token, reader => + { + LegacyPointsInfo pt = default; + pt.Steam64 = reader.GetUInt64(0); + pt.Team = reader.GetUInt64(1); + pt.Experience = reader.IsDBNull(2) ? 0 : reader.GetUInt32(2); + pt.Credits = reader.IsDBNull(3) ? (seasonId == 1 ? 0u : 500u /* starting credits */) : reader.GetUInt32(3); + points.Add(pt); + }).ConfigureAwait(false); + + foreach (LegacyPointsInfo pt in points) + { + uint factionId; + + if (pt.Team == 1) + factionId = t1Faction.Key; + else if (pt.Team == 2) + factionId = t2Faction.Key; + else continue; + + await _pointsSql.SetPointsAsync(new CSteamID(pt.Steam64), factionId, seasonId, pt.Experience, pt.Credits, token); + } + } + catch (MySqlException ex) + { + Context.Logger.LogWarning(ex, "Table may not exist: {0}.", tableName); + } + } + + private struct LegacyPointsInfo + { + public ulong Steam64; + public ulong Team; + public uint Experience; + public uint Credits; + } +} +#endif \ No newline at end of file diff --git a/UncreatedWarfare/Components/SpottedComponent.cs b/UncreatedWarfare/Components/SpottedComponent.cs index 8f1766b3..fb006972 100644 --- a/UncreatedWarfare/Components/SpottedComponent.cs +++ b/UncreatedWarfare/Components/SpottedComponent.cs @@ -57,11 +57,6 @@ public class SpottedComponent : MonoBehaviour #endif public void Initialize(Spotted type, Team ownerTeam, IServiceProvider serviceProvider) { - _serviceProvider = serviceProvider; - _playerService = serviceProvider.GetRequiredService(); - - _logger = serviceProvider.GetRequiredService>(); - _ownerTeam = ownerTeam; _vehicle = null; VehicleType = null; @@ -79,6 +74,8 @@ public void Initialize(Spotted type, Team ownerTeam, IServiceProvider servicePro CurrentSpotter = null; IsLaserTarget = type == Spotted.FOB; + InitializeCommon(serviceProvider); + AssetConfiguration assetConfig = serviceProvider.GetRequiredService(); IAssetLink? effect; @@ -119,14 +116,14 @@ public void Initialize(Spotted type, Team ownerTeam, IServiceProvider servicePro } public void Initialize(VehicleType type, InteractableVehicle vehicle, IServiceProvider serviceProvider) { - _serviceProvider = serviceProvider; - _playerService = serviceProvider.GetRequiredService(); CurrentSpotter = null; IsLaserTarget = type.IsGroundVehicle(); _vehicle = vehicle; VehicleType = type; + InitializeCommon(serviceProvider); + AssetConfiguration assetConfig = serviceProvider.GetRequiredService(); IAssetLink? effect; switch (type) @@ -252,6 +249,15 @@ public void Initialize(VehicleType type, InteractableVehicle vehicle, IServicePr _logger.LogConditional("Spotter initialized: {0}.", this); } + + private void InitializeCommon(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _playerService = serviceProvider.GetRequiredService(); + + _logger = serviceProvider.GetRequiredService>(); + } + #if ENABLE_SPOTTED_BUFF private static void OnExitVehicle(ExitVehicle e) { @@ -479,7 +485,7 @@ private void TryAnnounce(WarfarePlayer spotter, string targetName) if (IsActive) return; - ToastMessage.QueueMessage(spotter, new ToastMessage(ToastMessageStyle.Mini, T.SpottedToast.Translate(spotter))); + spotter.SendToast(new ToastMessage(ToastMessageStyle.Mini, T.SpottedToast.Translate(spotter))); Team t = spotter.Team; Color t1 = t.Faction.Color; diff --git a/UncreatedWarfare/Data.cs b/UncreatedWarfare/Data.cs index 2797c08f..36d771b3 100644 --- a/UncreatedWarfare/Data.cs +++ b/UncreatedWarfare/Data.cs @@ -11,15 +11,9 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; -using DanielWillett.ModularRpcs.Abstractions; -using DanielWillett.ModularRpcs.Routing; -using DanielWillett.ModularRpcs.Serialization; using Uncreated.Warfare.Components; using Uncreated.Warfare.Interaction; -using Uncreated.Warfare.Levels; using Uncreated.Warfare.Sessions; -using Uncreated.Warfare.Util; -using Uncreated.Warfare.Logging; using Uncreated.Warfare.Players; using Uncreated.Warfare.Translations.Languages; @@ -34,82 +28,25 @@ public static class Data public static class Paths { public static readonly char[] BadFileNameCharacters = { '>', ':', '"', '/', '\\', '|', '?', '*' }; + public static readonly string BaseDirectory = Path.Combine(Environment.CurrentDirectory, "Uncreated", "Warfare") + Path.DirectorySeparatorChar; - private static string? _mapCache; - public static string MapStorage - { - get - { - if (_mapCache is null) - { - if (Provider.map == default) throw new Exception("Map not yet set."); - _mapCache = Path.Combine(BaseDirectory, "Maps", Provider.map.RemoveMany(false, BadFileNameCharacters)) + Path.DirectorySeparatorChar; - } - return _mapCache; - } - } - private static string? _flagCache; - private static string? _structureCache; - private static string? _vehicleCache; - public static readonly string PointsStorage = Path.Combine(BaseDirectory, "Points") + Path.DirectorySeparatorChar; - public static readonly string CooldownStorage = Path.Combine(BaseDirectory, "Cooldowns") + Path.DirectorySeparatorChar; - public static readonly string SquadStorage = Path.Combine(BaseDirectory, "Squads") + Path.DirectorySeparatorChar; - public static readonly string KitsStorage = Path.Combine(BaseDirectory, "Kits") + Path.DirectorySeparatorChar; - public static readonly string FOBStorage = Path.Combine(BaseDirectory, "FOBs") + Path.DirectorySeparatorChar; public static readonly string LangStorage = Path.Combine(BaseDirectory, "Lang") + Path.DirectorySeparatorChar; public static readonly string Logs = Path.Combine(BaseDirectory, "Logs") + Path.DirectorySeparatorChar; public static readonly string Sync = Path.Combine(BaseDirectory, "Sync") + Path.DirectorySeparatorChar; public static readonly string ActionLog = Path.Combine(Logs, "ActionLog") + Path.DirectorySeparatorChar; - public static readonly string PendingOffenses = Path.Combine(BaseDirectory, "Offenses") + Path.DirectorySeparatorChar; - public static readonly string TraitDataStorage = Path.Combine(BaseDirectory, "traits.json"); - public static readonly string ConfigSync = Path.Combine(Sync, "config.json"); - public static readonly string KitSync = Path.Combine(Sync, "kits.json"); - public static readonly string CurrentLog = Path.Combine(Logs, "current.txt"); - public static readonly string FunctionLog = Path.Combine(Logs, "funclog.txt"); public static readonly string Heartbeat = Path.Combine(BaseDirectory, "Stats", "heartbeat.dat"); public static readonly string HeartbeatBackup = Path.Combine(BaseDirectory, "Stats", "heartbeat_last.dat"); - public static string FlagStorage => _flagCache ??= Path.Combine(MapStorage, "Flags") + Path.DirectorySeparatorChar; - public static string StructureStorage => _structureCache ??= Path.Combine(MapStorage, "Structures") + Path.DirectorySeparatorChar; - public static string VehicleStorage => _vehicleCache ??= Path.Combine(MapStorage, "Vehicles") + Path.DirectorySeparatorChar; - public static void OnMapChanged() - { - _mapCache = null; - _flagCache = null; - _structureCache = null; - _vehicleCache = null; - } - } - public static class Keys - { - public const PlayerKey GiveUp = PlayerKey.PluginKey3; - public const PlayerKey SelfRevive = PlayerKey.PluginKey2; - public const PlayerKey SpawnCountermeasures = PlayerKey.PluginKey3; - public const PlayerKey ActionMenu = PlayerKey.PluginKey2; - public const PlayerKey DropSupplyOverride = PlayerKey.PluginKey1; } public const string SuppressCategory = "Microsoft.Performance"; public const string SuppressID = "IDE0051"; public static readonly Regex ChatFilter = new Regex(@"(?:[nńǹňñṅņṇṋṉn̈ɲƞᵰᶇɳȵɴnŋnjvṼṽṿʋᶌᶌⱱⱴᴠʌv\|\\\/]\W{0,}[il1ÍíìĭîǐïḯĩįīỉȉȋịḭɨᵻᶖiıɪɩifiIij\|\!]\W{0,}[gqb96ǴǵğĝǧġģḡǥɠᶃɢȝgŋɢɢɋƣʠqȹḂḃḅḇƀɓƃᵬᶀʙbȸ](?!h|(?:an)|(?:[e|a|o]t)|(?:un)|(?:rab)|(?:rain)|(?:low)|(?:ue)|(?:uy))(?!n\shadi)\W{0,}[gqb96ǴǵğĝǧġģḡǥɠᶃɢȝgŋɢɢɋƣʠqȹḂḃḅḇƀɓƃᵬᶀʙbȸ]{0,}\W{0,}[gqb96ǴǵğĝǧġģḡǥɠᶃɢȝgŋɢɢɋƣʠqȹḂḃḅḇƀɓƃᵬᶀʙbȸ]{0,}\W{0,}[ae]{0,1}\W{0,}[r]{0,}(?:ia){0,})|(?:c\W{0,}h\W{0,}i{1,}\W{0,}n{1,}\W{0,}k{1,})|(?:[fḟƒᵮᶂꜰfffffifflfifl]\W{0,}[aáàâǎăãảȧạäåḁāąᶏⱥȁấầẫẩậắằẵẳặǻǡǟȃɑᴀɐɒaæᴁᴭᵆǽǣᴂ]\W{0,}[gqb96ǴǵğĝǧġģḡǥɠᶃɢȝgŋɢɢɋƣʠqȹḂḃḅḇƀɓƃᵬᶀʙbȸ]{1,}\W{0,}o{0,}\W{0,}t{0,1}(?!ain))", RegexOptions.IgnoreCase); - public static readonly Regex NameRichTextReplaceFilter = new Regex("<.*>"); - public static readonly Regex PluginKeyMatch = new Regex(@"\", RegexOptions.IgnoreCase); + public static readonly Regex PluginKeyMatch = new Regex(@"\", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static CultureInfo LocalLocale = Languages.CultureEnglishUS; // todo set from config - public static readonly CultureInfo AdminLocale = Languages.CultureEnglishUS; - public static Dictionary Colors; - public static Dictionary ColorsHex; - public static Dictionary ExtraPoints; - public static Dictionary DefaultPlayerNames; - public static Dictionary OriginalPlayerNames; public static Dictionary PlaytimeComponents; public static bool UseFastKits; public static bool UseElectricalGrid; - internal static MethodInfo ReplicateStance; - public static Points Points; - public static SessionManager Sessions; -#if NETSTANDARD || NETFRAMEWORK - public static IStripeService WarfareStripeService; -#endif internal static ClientInstanceMethod? SendUpdateBarricadeState; internal static ClientInstanceMethod? SendWearShirt; internal static ClientInstanceMethod? SendWearPants; @@ -119,15 +56,10 @@ public static class Keys internal static ClientInstanceMethod? SendWearMask; internal static ClientInstanceMethod? SendWearGlasses; internal static ClientInstanceMethod SendChangeText; - internal static ClientStaticMethod SendMultipleBarricades; - internal static ClientStaticMethod SendChatIndividual; internal static ClientStaticMethod? SendSwapVehicleSeats; - internal static ClientStaticMethod? SendEnterVehicle; internal static ClientInstanceMethod? SendInventory; // internal static ClientInstanceMethod? SendScreenshotDestination; - internal static InstanceSetter? SetPrivateStance; - internal static InstanceSetter? SetStorageInventory; internal static InstanceSetter SetOwnerHasInventory; internal static InstanceGetter GetOwnerHasInventory; internal static InstanceGetter GetItemsSlots; @@ -136,176 +68,9 @@ public static class Keys internal static Action>? ServerSpawnLegacyImpact; internal static Action SendInitialInventoryState; internal static Func? PullFromTransportConnectionListPool; - internal static Action? RefreshIsConnectedToPower; internal static SteamPlayer NilSteamPlayer; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Is([NotNullWhen(true)] out TGamemode? gamemode) - { - gamemode = default; - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Is() => false; - - internal static Task LoadVariables(CancellationToken token) - { - return Task.CompletedTask; -#if false - OriginalPlayerNames = new Dictionary(Provider.maxPlayers); - PlaytimeComponents = new Dictionary(Provider.maxPlayers); - - /* CREATE DIRECTORIES */ - L.Log("Validating directories...", ConsoleColor.Magenta); - - /* CONSTRUCT FRAMEWORK */ - L.Log("Instantiating Framework...", ConsoleColor.Magenta); -#if DEBUG - //L.Log("Connection string: " + UCWarfare.Config.SQL.GetConnectionString(), ConsoleColor.DarkGray); -#endif - - - /* REFLECT PRIVATE VARIABLES */ - L.Log("Getting RPCs...", ConsoleColor.Magenta); - IDisposable indent = L.IndentLog(1); - - SendChangeText = - ReflectionUtility.FindRequiredRpc>("SendChangeText"); - SendMultipleBarricades = - ReflectionUtility.FindRequiredRpc("SendMultipleBarricades"); - SendChatIndividual = - ReflectionUtility.FindRequiredRpc>("SendChatEntry"); - SendDestroyItem = - ReflectionUtility.FindRequiredRpc>("SendDestroyItem"); - - SendUpdateBarricadeState = - ReflectionUtility.FindRpc>("SendUpdateState"); - SendInventory = ReflectionUtility.FindRpc("SendInventory"); - SendWearShirt = - ReflectionUtility.FindRpc>("SendWearShirt"); - SendWearPants = - ReflectionUtility.FindRpc>("SendWearPants"); - SendWearHat = - ReflectionUtility.FindRpc>("SendWearHat"); - SendWearBackpack = - ReflectionUtility.FindRpc>("SendWearBackpack"); - SendWearVest = - ReflectionUtility.FindRpc>("SendWearVest"); - SendWearMask = - ReflectionUtility.FindRpc>("SendWearMask"); - SendWearGlasses = - ReflectionUtility.FindRpc>("SendWearGlasses"); - SendSwapVehicleSeats = - ReflectionUtility.FindRpc>("SendSwapVehicleSeats"); - SendEnterVehicle = - ReflectionUtility.FindRpc>("SendEnterVehicle"); - // SendScreenshotDestination = ReflectionUtility.FindRpc("SendScreenshotDestination"); - - UseFastKits = true; - if (SendWearShirt is null || SendWearPants is null || SendWearHat is null || SendWearBackpack is null || SendWearVest is null || SendWearMask is null || SendWearGlasses is null || SendInventory is null) - { - L.LogWarning("Unable to gather all the RPCs needed for Fast Kits, kits will not work as quick."); - UseFastKits = false; - } - - SetPrivateStance = Accessor.GenerateInstanceSetter("_stance"); - SetStorageInventory = Accessor.GenerateInstanceSetter("_items"); - RefreshIsConnectedToPower = - (Action?)Accessor.GenerateInstanceCaller("RefreshIsConnectedToPower"); - GetUseableGunReloading = Accessor.GenerateInstanceGetter("isReloading"); - - SendInitialInventoryState = - Accessor.GenerateInstanceCaller>("SendInitialPlayerState", throwOnError: true)!; - try - { - GetItemsSlots = Accessor.GenerateInstanceGetter("slots", throwOnError: true)!; - SetOwnerHasInventory = - Accessor.GenerateInstanceSetter("ownerHasInventory", throwOnError: true)!; - GetOwnerHasInventory = - Accessor.GenerateInstanceGetter("ownerHasInventory", throwOnError: true)!; - } - catch (Exception ex) - { - UseFastKits = false; - L.LogWarning("Couldn't generate a fast kits accessors, kits will not work as quick."); - L.LogError(ex); - } - try - { - ReplicateStance = - typeof(PlayerStance).GetMethod("replicateStance", BindingFlags.Instance | BindingFlags.NonPublic)!; - } - catch (Exception ex) - { - L.LogWarning("Couldn't get replicateState from PlayerStance, players will spawn while prone. (" + ex.Message + ")."); - } - try - { - ServerSpawnLegacyImpact = - (Action>?)typeof(DamageTool) - .GetMethod("ServerSpawnLegacyImpact", BindingFlags.Static | BindingFlags.NonPublic)? - .CreateDelegate(typeof(Action>)); - } - catch (Exception ex) - { - L.LogWarning("Couldn't get ServerSpawnLegacyImpact from DamageTool, explosives will not play the flesh sound. (" + ex.Message + ")."); - } - - try - { - GetRecentKiller = Accessor.GenerateInstanceGetter("recentKiller"); - } - catch (Exception ex) - { - L.LogWarning("Couldn't get PlayerLife.recentKiller from PlayerLife, other reputation sources will be ignored. (" + ex.Message + ")."); - } - - PullFromTransportConnectionListPool = null; - try - { - MethodInfo? method = typeof(Provider).Assembly - .GetType("SDG.Unturned.TransportConnectionListPool", true, false)?.GetMethod("Get", - BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); - if (method != null) - { - PullFromTransportConnectionListPool = - (Func)method.CreateDelegate(typeof(Func)); - } - else - { - L.LogWarning("Couldn't find Get in TransportConnectionListPool, list pooling will not be used."); - } - } - catch (Exception ex) - { - L.LogWarning("Couldn't get Get from TransportConnectionListPool, list pooling will not be used (" + ex.Message + ")."); - } - - indent.Dispose(); - - - L.Log("Loading first gamemode...", ConsoleColor.Magenta); - - SteamPlayerID id = new SteamPlayerID(CSteamID.Nil, 0, "Nil", "Nil", "Nil", CSteamID.Nil); - GameObject obj = Provider.gameMode.getPlayerGameObject(id); - obj.SetActive(false); - NetId nilNetId = new NetId(uint.MaxValue - 100); - - NilSteamPlayer = new SteamPlayer(null, nilNetId, id, obj.transform, false, false, -1, 0, 0, 0, - Color.white, Color.white, Color.white, false, 0, 0, 0, 0, 0, 0, 0, Array.Empty(), Array.Empty(), Array.Empty(), EPlayerSkillset.NONE, - Provider.language, CSteamID.Nil, EClientPlatform.Windows); - - for (uint i = nilNetId.id; i < uint.MaxValue; ++i) - { - if (!NetIdRegistry.Release(new NetId(i))) - break; - } - - Object.Destroy(obj); -#endif - } public static PooledTransportConnectionList GetPooledTransportConnectionList(int capacity = -1) { PooledTransportConnectionList? rtn = null; diff --git a/UncreatedWarfare/Events/DispatchHandlers/ProviderDispatches.cs b/UncreatedWarfare/Events/DispatchHandlers/ProviderDispatches.cs index 3ebf6587..5374b80a 100644 --- a/UncreatedWarfare/Events/DispatchHandlers/ProviderDispatches.cs +++ b/UncreatedWarfare/Events/DispatchHandlers/ProviderDispatches.cs @@ -34,6 +34,8 @@ private void ProviderOnServerConnected(CSteamID steam64) Team team = Team.NoTeam; if (steamPlayer.player.quests.isMemberOfAGroup) { + steamPlayer.player.quests.leaveGroup(true); + /* todo make some kind of 'joining previous team...' section of the lobby if (!_warfare.IsLayoutActive()) { steamPlayer.player.quests.leaveGroup(true); @@ -44,6 +46,7 @@ private void ProviderOnServerConnected(CSteamID steam64) if (team == Team.NoTeam) steamPlayer.player.quests.leaveGroup(true); } + */ } ulong s64 = steam64.m_SteamID; diff --git a/UncreatedWarfare/Events/DispatchHandlers/VehicleDispatches.cs b/UncreatedWarfare/Events/DispatchHandlers/VehicleDispatches.cs index 659b14d0..e3a72dec 100644 --- a/UncreatedWarfare/Events/DispatchHandlers/VehicleDispatches.cs +++ b/UncreatedWarfare/Events/DispatchHandlers/VehicleDispatches.cs @@ -2,6 +2,7 @@ using Uncreated.Warfare.Components; using Uncreated.Warfare.Configuration; using Uncreated.Warfare.Events.Models.Vehicles; +using Uncreated.Warfare.Layouts.Teams; using Uncreated.Warfare.Players; using Uncreated.Warfare.Players.Management; @@ -95,4 +96,56 @@ private void VehicleManagerOnToggledVehicleLock(InteractableVehicle vehicle) _ = DispatchEventAsync(args, CancellationToken.None); } -} + + /// + /// Invoked by when a vehicle is destroyed. + /// + private void VehicleManagerOnVehicleExploded(InteractableVehicle vehicle) + { + VehicleComponent component = vehicle.gameObject.GetOrAddComponent(); + + ITeamManager? teamManager = _warfare.IsLayoutActive() ? _warfare.ScopedProvider.Resolve>() : null; + + WarfarePlayer? instigator = null, lastDriver = null; + CSteamID instigatorId = CSteamID.Nil, lastDriverId = CSteamID.Nil; + + Team? instigatorTeam = null; + + WarfarePlayer? owner = _playerService.GetOnlinePlayerOrNull(vehicle.lockedOwner); + + if (component.LastInstigator != 0) + { + instigatorId = new CSteamID(component.LastInstigator); + instigator = _playerService.GetOnlinePlayerOrNull(instigatorId); + + instigatorTeam = instigator == null ? null : teamManager?.GetTeam(instigator.UnturnedPlayer.quests.groupID); + } + + if (component.LastDriver != 0) + { + lastDriverId = new CSteamID(component.LastDriver); + lastDriver = _playerService.GetOnlinePlayerOrNull(instigatorId); + } + + EDamageOrigin origin = component.LastDamageOrigin; + InteractableVehicle? activeVehicle = component.LastDamagedFromVehicle; + + VehicleExploded args = new VehicleExploded + { + Vehicle = vehicle, + Component = component, + DamageOrigin = origin, + InstigatorVehicle = activeVehicle, + Team = teamManager?.GetTeam(vehicle.lockedGroup) ?? Team.NoTeam, + Instigator = instigator, + InstigatorId = instigatorId, + InstigatorTeam = instigatorTeam, + LastDriver = lastDriver, + LastDriverId = lastDriverId, + Owner = owner, + OwnerId = vehicle.lockedOwner + }; + + _ = DispatchEventAsync(args, CancellationToken.None); + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Events/EventDispatcher2.cs b/UncreatedWarfare/Events/EventDispatcher2.cs index fa24cdcb..880e87aa 100644 --- a/UncreatedWarfare/Events/EventDispatcher2.cs +++ b/UncreatedWarfare/Events/EventDispatcher2.cs @@ -75,6 +75,7 @@ UniTask IHostedService.StartAsync(CancellationToken token) /* Vehicles */ VehicleManager.OnToggleVehicleLockRequested += VehicleManagerOnToggleVehicleLockRequested; VehicleManager.OnToggledVehicleLock += VehicleManagerOnToggledVehicleLock; + VehicleManager.OnVehicleExploded += VehicleManagerOnVehicleExploded; /* Items */ ItemManager.onTakeItemRequested += ItemManagerOnTakeItemRequested; @@ -114,6 +115,7 @@ UniTask IHostedService.StopAsync(CancellationToken token) /* Vehicles */ VehicleManager.OnToggleVehicleLockRequested -= VehicleManagerOnToggleVehicleLockRequested; VehicleManager.OnToggledVehicleLock -= VehicleManagerOnToggledVehicleLock; + VehicleManager.OnVehicleExploded -= VehicleManagerOnVehicleExploded; /* Items */ ItemManager.onTakeItemRequested -= ItemManagerOnTakeItemRequested; diff --git a/UncreatedWarfare/Events/Models/Vehicles/VehicleDestroyed.cs b/UncreatedWarfare/Events/Models/Vehicles/VehicleDestroyed.cs deleted file mode 100644 index 41ef2cce..00000000 --- a/UncreatedWarfare/Events/Models/Vehicles/VehicleDestroyed.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using Uncreated.Warfare.Components; -using Uncreated.Warfare.Players; -using Uncreated.Warfare.Players.Management; - -namespace Uncreated.Warfare.Events.Models.Vehicles; -public class VehicleDestroyed -{ - private readonly InteractableVehicle _vehicle; - private readonly VehicleComponent? _component; - private readonly SpottedComponent? _spotter; - private readonly WarfarePlayer? _lockedOwner; - private readonly WarfarePlayer? _instigator; - private readonly WarfarePlayer? _lastDriver; - private readonly ulong _ownerId; - private readonly ulong _instigatorId; - private readonly ulong _lastDriverId; - private readonly ulong _lockedTeam; - public InteractableVehicle Vehicle => _vehicle; - public VehicleComponent? Component => _component; - public SpottedComponent? Spotter => _spotter; - public WarfarePlayer? Owner => _lockedOwner; - public WarfarePlayer? Instigator => _instigator; - public WarfarePlayer? LastDriver => _lastDriver; - public ulong OwnerId => _ownerId; - public ulong InstigatorId => _instigatorId; - public ulong LastDriverId => _lastDriverId; - public ulong Team => _lockedTeam; - public InteractableVehicle? ActiveVehicle { get; set; } - public KeyValuePair[] Assists { get; set; } - public EDamageOrigin DamageOrigin { get; } - public VehicleDestroyed(InteractableVehicle vehicle, IPlayerService playerService, SpottedComponent? spotted) - { - _spotter = spotted; - _vehicle = vehicle; - _lockedTeam = vehicle.lockedGroup.m_SteamID; - _ownerId = vehicle.lockedOwner.m_SteamID; - _lockedOwner = playerService.GetOnlinePlayerOrNull(_ownerId); - _component = vehicle.GetComponent(); - if (_component != null) - { - if (_component.LastInstigator != 0) - { - _instigator = playerService.GetOnlinePlayerOrNull(_component.LastInstigator); - _instigatorId = _component.LastInstigator; - _lastDriver = playerService.GetOnlinePlayerOrNull(_component.LastDriver); - } - else - { - _instigator = _lastDriver = playerService.GetOnlinePlayerOrNull(_component.LastDriver); - _instigatorId = _component.LastDriver; - } - - _lastDriverId = _component.LastDriver; - DamageOrigin = _component.LastDamageOrigin; - ActiveVehicle = _component.LastDamagedFromVehicle; - } - else if (vehicle.passengers.Length > 0 && vehicle.passengers[0].player != null && vehicle.passengers[0].player.player != null) - { - _lastDriver = _instigator = playerService.GetOnlinePlayerOrNull(vehicle.passengers[0].player.player); - _lastDriverId = _lastDriver is null ? 0 : _lastDriver.Steam64.m_SteamID; - } - } -} diff --git a/UncreatedWarfare/Events/Models/Vehicles/VehicleExploded.cs b/UncreatedWarfare/Events/Models/Vehicles/VehicleExploded.cs new file mode 100644 index 00000000..48ac7574 --- /dev/null +++ b/UncreatedWarfare/Events/Models/Vehicles/VehicleExploded.cs @@ -0,0 +1,67 @@ +using Uncreated.Warfare.Components; +using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Players; + +namespace Uncreated.Warfare.Events.Models.Vehicles; +public class VehicleExploded +{ + /// + /// The actual vehicle that was destroyed. + /// + public required InteractableVehicle Vehicle { get; init; } + + /// + /// The data of the vehicle that was destroyed. + /// + public required VehicleComponent Component { get; init; } + + /// + /// The owner (requester usually) of this vehicle. + /// + public required WarfarePlayer? Owner { get; init; } + + /// + /// The owner (requester usually) of this vehicle's Steam ID. + /// + public required CSteamID OwnerId { get; init; } + + /// + /// The player who most likely caused the explosion if they're online. + /// + public required WarfarePlayer? Instigator { get; init; } + + /// + /// The player who most likely caused the explosion's Steam ID. + /// + public required CSteamID InstigatorId { get; init; } + + /// + /// The team of the player who most likely caused the explosion when it happened. May be if the instigator is known but not in a team. + /// + public required Team? InstigatorTeam { get; init; } + + /// + /// The vehicle the instigator was in when this vehicle was destroyed. + /// + public required InteractableVehicle? InstigatorVehicle { get; init; } + + /// + /// The last player to drive this vehicle if they're online. + /// + public required WarfarePlayer? LastDriver { get; init; } + + /// + /// The last player to drive this vehicle's Steam ID. + /// + public required CSteamID LastDriverId { get; init; } + + /// + /// The team this vehicle belongs to. + /// + public required Team Team { get; init; } + + /// + /// The origin of the damage done to this vehicle. + /// + public required EDamageOrigin DamageOrigin { get; init; } +} diff --git a/UncreatedWarfare/FOBs/Construction/RepairStation.cs b/UncreatedWarfare/FOBs/Construction/RepairStation.cs index 7b5824dc..7cee3314 100644 --- a/UncreatedWarfare/FOBs/Construction/RepairStation.cs +++ b/UncreatedWarfare/FOBs/Construction/RepairStation.cs @@ -1,16 +1,11 @@ -using Autofac.Features.OwnedInstances; -using Microsoft.Extensions.Configuration; -using System; +using System; using System.Collections.Generic; -using System.Text; using Uncreated.Warfare.Buildables; using Uncreated.Warfare.Components; using Uncreated.Warfare.Configuration; using Uncreated.Warfare.Fobs; using Uncreated.Warfare.FOBs.SupplyCrates; using Uncreated.Warfare.Layouts.Teams; -using Uncreated.Warfare.Levels; -using Uncreated.Warfare.Translations; using Uncreated.Warfare.Util; using Uncreated.Warfare.Util.Timing; diff --git a/UncreatedWarfare/FOBs/Construction/ShovelableInfo.cs b/UncreatedWarfare/FOBs/Construction/ShovelableInfo.cs index 95757b82..fff4239a 100644 --- a/UncreatedWarfare/FOBs/Construction/ShovelableInfo.cs +++ b/UncreatedWarfare/FOBs/Construction/ShovelableInfo.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Uncreated.Warfare.Configuration; +using Uncreated.Warfare.Configuration; namespace Uncreated.Warfare.FOBs.Construction; public class ShovelableInfo { - required public IAssetLink Foundation { get; set; } - required public ShovelableType ConstuctionType { get; set; } - required public int RequiredHits { get; set; } + public required IAssetLink? Foundation { get; set; } + public required ShovelableType ConstuctionType { get; set; } + public required int RequiredHits { get; set; } public IAssetLink? CompletedStructure { get; set; } public IAssetLink? CompletedEffect { get; set; } public EmplacementInfo? Emplacement { get; set; } diff --git a/UncreatedWarfare/FOBs/FobEvents.cs b/UncreatedWarfare/FOBs/FobEvents.cs index 4505325e..071a8791 100644 --- a/UncreatedWarfare/FOBs/FobEvents.cs +++ b/UncreatedWarfare/FOBs/FobEvents.cs @@ -55,8 +55,8 @@ private BuildableContainer CreateBuildableContainer(BarricadePlaced e) } private void CreateShoveable(BarricadePlaced e, BuildableContainer container, BuildableFob? newFob) { - ShovelableInfo? shovelableInfo = _configuration.GetRequiredSection("Shovelables").Get>() - .FirstOrDefault(s => s.Foundation.Guid == e.Buildable.Asset.GUID); + ShovelableInfo? shovelableInfo = (_configuration.GetRequiredSection("Shovelables").Get>() ?? Array.Empty()) + .FirstOrDefault(s => s.Foundation != null && s.Foundation.Guid == e.Buildable.Asset.GUID); if (shovelableInfo != null) { diff --git a/UncreatedWarfare/Injures/PlayerInjureComponent.cs b/UncreatedWarfare/Injures/PlayerInjureComponent.cs index 75c4ddf2..12ddf8cc 100644 --- a/UncreatedWarfare/Injures/PlayerInjureComponent.cs +++ b/UncreatedWarfare/Injures/PlayerInjureComponent.cs @@ -2,7 +2,6 @@ using StackCleaner; using System; using System.Globalization; -using System.IO; using Uncreated.Framework.UI; using Uncreated.Warfare.Configuration; using Uncreated.Warfare.Deaths; @@ -17,6 +16,7 @@ using Uncreated.Warfare.Players.Extensions; using Uncreated.Warfare.Players.Management; using Uncreated.Warfare.Players.UI; +using Uncreated.Warfare.Stats; using Uncreated.Warfare.Translations; using Uncreated.Warfare.Translations.Util; using Uncreated.Warfare.Util; @@ -98,6 +98,7 @@ private static bool ShouldDamageInjurePlayer(in DamagePlayerParameters parameter private IPlayerService _playerService; private EventDispatcher2 _eventDispatcher; private AssetConfiguration _assetConfiguration; + private PointsTranslations _xpTranslations; private bool _isInjured; private bool _isReviving; private WarfarePlayer? _reviver; @@ -139,10 +140,12 @@ void IPlayerComponent.Init(IServiceProvider serviceProvider, bool isOnJoin) _assetConfiguration = serviceProvider.GetRequiredService(); _eventDispatcher = serviceProvider.GetRequiredService(); _cooldownManager = serviceProvider.GetRequiredService(); + _xpTranslations = serviceProvider.GetRequiredService>().Value; if (!isOnJoin) return; + PlayerKeys.PressedPluginKey3 += OnPressedGiveUp; PlayerKeys.PressedPluginKey2 += OnPressedReviveSelf; @@ -249,7 +252,10 @@ public void Injure(in DamagePlayerParameters parameters) { // send Injured toast to killer Team killerTeam = killer.Team; - ToastMessage.QueueMessage(killer, new ToastMessage(ToastMessageStyle.Mini, (killerTeam != player.Team ? T.XPToastEnemyInjured : T.XPToastFriendlyInjured).Translate(killer))); + killer.SendToast(new ToastMessage(ToastMessageStyle.Mini, (killerTeam != player.Team + ? _xpTranslations.XPToastEnemyInjured + : _xpTranslations.XPToastFriendlyInjured) + .Translate(killer))); } PlayerInjured args = new PlayerInjured(in _injureParameters) @@ -468,10 +474,10 @@ void IEventListener.HandleEvent(PlayerAided e, IServiceProvider ser } else { - ToastMessage.QueueMessage(e.Medic, new ToastMessage(ToastMessageStyle.Mini, - T.XPToastGainXP.Translate(0, e.Medic, false) - + "\n" - + TranslationFormattingUtility.Colorize(T.XPToastHealedTeammate.Translate(e.Medic), new Color32(173, 173, 173, 255), TranslationOptions.TMProUI, StackColorFormatType.None) )); + e.Medic.SendToast(new ToastMessage(ToastMessageStyle.Mini, + _xpTranslations.XPToastGainXP.Translate(0, e.Medic, false) + +"\n" + + TranslationFormattingUtility.Colorize(_xpTranslations.XPToastHealedTeammate.Translate(e.Medic), new Color32(173, 173, 173, 255), TranslationOptions.TMProUI, StackColorFormatType.None) )); } } diff --git a/UncreatedWarfare/Kits/Translations/RequestTranslations.cs b/UncreatedWarfare/Kits/Translations/RequestTranslations.cs index 473163b7..5ee87219 100644 --- a/UncreatedWarfare/Kits/Translations/RequestTranslations.cs +++ b/UncreatedWarfare/Kits/Translations/RequestTranslations.cs @@ -1,9 +1,9 @@ using System; using Uncreated.Warfare.FOBs.Deployment; -using Uncreated.Warfare.Levels; using Uncreated.Warfare.Models.Kits; using Uncreated.Warfare.NewQuests; using Uncreated.Warfare.Players; +using Uncreated.Warfare.Stats; using Uncreated.Warfare.Teams; using Uncreated.Warfare.Translations; using Uncreated.Warfare.Translations.Addons; @@ -97,7 +97,7 @@ public class RequestTranslations : PropertiesTranslationCollection public readonly Translation RequestKitLimited = new Translation("<#a8918a>Your team already has a max of <#d9e882>{0} players using this kit. Try again later."); [TranslationData("Sent when a player tries to request a kit but they're too low level.", "Name of the level needed", "Number of the level needed")] - public readonly Translation RequestKitLowLevel = new Translation("<#b3ab9f>You must be <#ffc29c>{0} (L {1}) to use this kit.", arg0Fmt: LevelData.FormatName, arg1Fmt: LevelData.FormatNumeric); + public readonly Translation RequestKitLowLevel = new Translation("<#b3ab9f>You must be <#ffc29c>{0} ({1}) to use this kit.", arg0Fmt: WarfareRank.FormatName, arg1Fmt: WarfareRank.FormatLPrefixedNumeric); [TranslationData("Sent when a player tries to request a kit but they're missing a completed quest.", "Name of the quest")] public readonly Translation RequestKitQuestIncomplete = new Translation("<#b3ab9f>You have to complete {0} to request this kit.", arg0Fmt: QuestTemplate.FormatColorQuestAsset); @@ -130,7 +130,7 @@ public class RequestTranslations : PropertiesTranslationCollection public readonly Translation RequestVehicleWrongClass = new Translation("<#b3ab9f>You need a <#cedcde>{0} kit in order to request this vehicle."); [TranslationData("Sent when a player tries to request a vehicle but they're too low level.", "Name of the level needed", "Number of the level needed")] - public readonly Translation RequestVehicleMissingLevels = new Translation("<#b3ab9f>You must be <#ffc29c>{0} (L {1}) to request this vehicle.", arg0Fmt: LevelData.FormatName, arg1Fmt: LevelData.FormatNumeric); + public readonly Translation RequestVehicleMissingLevels = new Translation("<#b3ab9f>You must be <#ffc29c>{0} ({1}) to request this vehicle.", arg0Fmt: WarfareRank.FormatName, arg1Fmt: WarfareRank.FormatLPrefixedNumeric); [TranslationData("Sent when a player tries to request a vehicle but they're missing a completed quest.", "Name of the quest")] public readonly Translation RequestVehicleQuestIncomplete = new Translation("<#b3ab9f>You have to complete {0} to request this vehicle.", arg0Fmt: QuestTemplate.FormatColorQuestAsset); diff --git a/UncreatedWarfare/Layouts/Teams/TwoSidedTeamManager.cs b/UncreatedWarfare/Layouts/Teams/TwoSidedTeamManager.cs index 5d7cfa97..6dbdd1ff 100644 --- a/UncreatedWarfare/Layouts/Teams/TwoSidedTeamManager.cs +++ b/UncreatedWarfare/Layouts/Teams/TwoSidedTeamManager.cs @@ -13,6 +13,7 @@ using Uncreated.Warfare.Maps; using Uncreated.Warfare.Models.Factions; using Uncreated.Warfare.Players; +using Uncreated.Warfare.Stats; using Uncreated.Warfare.Teams; using Uncreated.Warfare.Util; @@ -24,9 +25,11 @@ namespace Uncreated.Warfare.Layouts.Teams; public class TwoSidedTeamManager : ITeamManager { private readonly ILogger _logger; + private readonly PointsService _points; private readonly IServiceProvider _serviceProvider; private readonly Team[] _teams = new Team[2]; private readonly EventDispatcher2 _eventDispatcher; + private readonly IPointsStore _pointsSql; private int _blufor; private int _opfor; @@ -63,13 +66,15 @@ public class TwoSidedTeamManager : ITeamManager /// There is no Opfor team. public Team Opfor => _opfor == -1 ? throw new InvalidOperationException("This layout does not have an Opfor team.") : _teams[_opfor]; - public TwoSidedTeamManager(ILogger logger, IServiceProvider serviceProvider) + public TwoSidedTeamManager(ILogger logger, PointsService points, IServiceProvider serviceProvider) { _logger = logger; + _points = points; _serviceProvider = serviceProvider; AllTeams = new ReadOnlyCollection(_teams); _eventDispatcher = serviceProvider.GetRequiredService(); + _pointsSql = serviceProvider.GetRequiredService(); } /// @@ -88,6 +93,7 @@ public Team GetTeam(CSteamID groupId) public async UniTask JoinTeamAsync(WarfarePlayer player, Team team, CancellationToken token = default) { await UniTask.SwitchToMainThread(token); + team ??= Team.NoTeam; Team oldTeam = player.Team; @@ -98,11 +104,31 @@ public async UniTask JoinTeamAsync(WarfarePlayer player, Team team, Cancellation { player.UnturnedPlayer.quests.leaveGroup(force: true); } - else if (!player.UnturnedPlayer.quests.ServerAssignToGroup(team.GroupId, EPlayerGroupRank.MEMBER, bypassMemberLimit: true)) + else { - throw new ArgumentException($"Group for team {team.Faction.Name} doesn't exist.", nameof(team)); + if (player.UnturnedPlayer.quests.isMemberOfAGroup) + { + player.UnturnedPlayer.quests.leaveGroup(force: true); + } + + // download points for new team + PlayerPoints pts = await _pointsSql.GetPointsAsync(player.Steam64, team.Faction.PrimaryKey, CancellationToken.None).ConfigureAwait(false); + + // setup default credit value if there was no row in the db + if (!pts.WasFound) + { + pts = await _pointsSql.AddToCreditsAsync(player.Steam64, team.Faction.PrimaryKey, _points.DefaultCredits, CancellationToken.None).ConfigureAwait(false); + } + + await UniTask.SwitchToMainThread(CancellationToken.None); + + if (!player.UnturnedPlayer.quests.ServerAssignToGroup(team.GroupId, EPlayerGroupRank.MEMBER, bypassMemberLimit: true)) + throw new ArgumentException($"Group for team {team.Faction.Name} doesn't exist.", nameof(team)); + + player.CachedPoints = pts; } + player.UpdateTeam(team); PlayerTeamChanged args = new PlayerTeamChanged { GroupId = team.GroupId, diff --git a/UncreatedWarfare/Levels/LevelData.cs b/UncreatedWarfare/Levels/LevelData.cs deleted file mode 100644 index 09b66a6a..00000000 --- a/UncreatedWarfare/Levels/LevelData.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Uncreated.Warfare.Translations; -using Uncreated.Warfare.Translations.Util; -using Uncreated.Warfare.Translations.ValueFormatters; - -namespace Uncreated.Warfare.Levels; - -public readonly struct LevelData : ITranslationArgument -{ - public int TotalXP { get; } - public int CurrentXP { get; } - public int RequiredXP { get; } - public int Level { get; } - public string Name { get; } - public string Abbreviation { get; } - public string NextName { get; } - public string NextAbbreviation { get; } - public string ProgressBar { get; } - public LevelData(int xp) - { - TotalXP = xp; - //Level = Points.GetLevel(xp); - //int startXP = Points.GetLevelXP(Level); - //CurrentXP = xp - startXP; - //RequiredXP = Points.GetNextLevelXP(Level) - startXP; - //Name = GetRankName(Level); - //Abbreviation = GetRankAbbreviation(Level); - //NextName = GetRankName(Level + 1); - //NextAbbreviation = GetRankAbbreviation(Level + 1); - //ProgressBar = Points.GetProgressBar(CurrentXP, RequiredXP); - } - public static string GetRankName(int level) - { - return level switch - { - 0 => "Recruit", - 1 => "Private", - 2 => "Private 1st Class", - 3 => "Corporal", - 4 => "Specialist", - 5 => "Sergeant", - 6 => "Staff Sergeant", - 7 => "Sergeant Major", - 8 => "Warrant Officer", - _ => "Level " + level.ToString(Data.LocalLocale), - }; - } - public static string GetRankAbbreviation(int level) - { - return level switch - { - 0 => "Rec.", - 1 => "Pvt.", - 2 => "Pfc.", - 3 => "Cpl.", - 4 => "Spec.", - 5 => "Sgt.", - 6 => "Ssg.", - 7 => "S.M.", - 8 => "W.O.", - _ => "L " + level.ToString(Data.LocalLocale), - }; - } - - - public static readonly SpecialFormat FormatNumeric = new SpecialFormat("Numeric", "x"); - - public static readonly SpecialFormat FormatAbbreviation = new SpecialFormat("Abbreviation", "a"); - - public static readonly SpecialFormat FormatName = new SpecialFormat("Name", "n"); - public string Translate(ITranslationValueFormatter formatter, in ValueFormatParameters parameters) - { - if (FormatNumeric.Match(in parameters)) - return Level.ToString(Data.LocalLocale); - - if (FormatAbbreviation.Match(in parameters)) - return Abbreviation; - - return Name; - } -} diff --git a/UncreatedWarfare/Levels/Points.cs b/UncreatedWarfare/Levels/Points.cs deleted file mode 100644 index 22de6689..00000000 --- a/UncreatedWarfare/Levels/Points.cs +++ /dev/null @@ -1,1779 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text.Json.Serialization; -using Uncreated.Warfare.Layouts.Teams; -using Uncreated.Warfare.Models.Localization; -using Uncreated.Warfare.Players; -using Uncreated.Warfare.Translations; -using Uncreated.Warfare.Vehicles; - -namespace Uncreated.Warfare.Levels; - -public sealed class Points -{ -#if false - private static readonly string UpdateAllPointsQuery = "SELECT `Steam64`, `Team`, `Experience`, `Credits` FROM `" + WarfareSQL.TableLevels + "` WHERE `Steam64` in ("; - - public static readonly XPUI XPUI; - public static readonly CreditsUI CreditsUI; - public static List Transactions; - - private static readonly Config PointsConfigObj; - private static bool _first = true; - - public static PointsConfig PointsConfig => PointsConfigObj.Data; - static Points() - { - if (Provider.isInitialized) // allows external access to this class without initializing everything - { - PointsConfigObj = new Config(Data.Paths.BaseDirectory, "points.json"); - Transactions = new List(16); - XPUI = new XPUI(); - CreditsUI = new CreditsUI(); - } - } - - public override void Load() - { - EventDispatcher.GroupChanged += OnGroupChanged; - EventDispatcher.VehicleDestroyed += OnVehicleDestoryed; - EventDispatcher.PlayerPendingAsync += OnPlayerPendingAsync; - KitManager.OnKitChanged += OnKitChanged; - EventDispatcher.PlayerLeaving += OnPlayerLeft; - if (!_first) ReloadConfig(); - else _first = false; - } - public override void Unload() - { - EventDispatcher.PlayerLeaving -= OnPlayerLeft; - KitManager.OnKitChanged -= OnKitChanged; - EventDispatcher.PlayerPendingAsync -= OnPlayerPendingAsync; - EventDispatcher.VehicleDestroyed -= OnVehicleDestoryed; - EventDispatcher.GroupChanged -= OnGroupChanged; - } - private async Task OnPlayerPendingAsync(PlayerPending e, CancellationToken token) - { - Task? t1 = null; - Task? t2 = null; - if (Data.DatabaseManager != null) - { - t1 = Data.DatabaseManager.TryInsertInitialRow(e.Steam64, token); - } - if (Data.RemoteSQL != null) - { - t2 = Data.RemoteSQL.TryInsertInitialRow(e.Steam64, token); - } - if (t1 != null) await t1.ConfigureAwait(false); - if (t2 != null) await t2.ConfigureAwait(false); - } - public static void ReloadConfig() - { - PointsConfigObj.Reload(); - } - - public void HideUI(UCPlayer player) - { - XPUI.Clear(player); - CreditsUI.Clear(player); - } - public void ShowUI(UCPlayer player) - { - XPUI.SendTo(player); - CreditsUI.SendTo(player); - } - public void UpdateUI(UCPlayer player) - { - player.PointsDirtyMask = 0b00001111; - XPUI.Update(player, true); - CreditsUI.Update(player, true); - } - - private static void OnGroupChanged(GroupChanged e) - { - UCPlayer player = e.Player; - player.PointsDirtyMask = 0b00001111; - UCWarfare.RunTask(async () => - { - if (e.NewTeam is 1 or 2) - { - (int credits, int xp) = await Data.DatabaseManager.GetCreditsAndXP(e.Steam64, e.NewTeam).ConfigureAwait(false); - await UCWarfare.ToUpdate(); - player.CachedXP = xp; - player.CachedCredits = credits; - XPUI.Update(player, true); - CreditsUI.Update(player, true); - } - else - { - XPUI.Clear(player); - CreditsUI.Clear(player); - } - }, ctx: "Reload xp on group change."); - } - public static readonly int[] Levels = - { - 1000, - 4000, - 8000, - 13000, - 20000, - 29000, - 40000, - 55000 - }; - /// Get the current level given an amount of . - public static int GetLevel(int xp) - { - for (int i = 0; i < Levels.Length; i++) - { - if (xp < Levels[i]) - return i; - } - return Levels.Length; - } - /// Get the given 's starting xp. - public static int GetLevelXP(int level) - { - if (level >= Levels.Length) - return Levels[^1]; - - if (level > 0) - return Levels[level - 1]; - - return 0; - } - /// Get the level after the given 's starting xp (or the given 's end xp. - public static int GetNextLevelXP(int level) - { - if (level >= Levels.Length) - return 100000; - - if (level >= 0) - return Levels[level]; - - return 0; - } - /// Get the percentage from 0-1 a player is through their current level at the given . - public static float GetLevelProgressXP(int xp) - { - int lvl = GetLevel(xp); - int start = GetLevelXP(lvl); - xp -= start; - return xp / (float)(GetNextLevelXP(lvl) - start); - } - /// Get the percentage from 0-1 a player is through their current level at the given and . - public static float GetLevelProgressXP(int xp, int lvl) - { - int strt = GetLevelXP(lvl); - return (float)(xp - strt) / (GetNextLevelXP(lvl) - strt); - } - public static void AwardCredits(UCPlayer player, int amount, Translation message, T0 arg0, bool redmessage = false, bool isPurchase = false, bool @lock = true) => - AwardCredits(player, amount, message.Translate(player, arg0), redmessage, isPurchase, @lock); - public static void AwardCredits(UCPlayer player, int amount, Translation message, T0 arg0, T1 arg1, bool redmessage = false, bool isPurchase = false, bool @lock = true) => - AwardCredits(player, amount, message.Translate(player, arg0, arg1), redmessage, isPurchase, @lock); - public static Task AwardCreditsAsync(UCPlayer player, int amount, Translation message, bool redmessage = false, bool isPurchase = false, bool @lock = true, CancellationToken token = default) => - AwardCreditsAsync(player, amount, message.Translate(player), redmessage, isPurchase, @lock, token); - public static async Task AwardCreditsAsync(CreditsParameters parameters, CancellationToken token = default, bool @lock = true) - { - using CombinedTokenSources tokens = token.CombineTokensIfNeeded(UCWarfare.UnloadCancel); - UCPlayer? player = parameters.Player; - Task? remote = null; - bool locked = false; - try - { - ulong team = parameters.Team; - if (team is < 1 or > 2) - { - if (player == null || !player.IsOnline) - { - PlayerSave? save = PlayerManager.GetSave(parameters.Steam64); - if (save != null) - team = save.Team; - } - else team = player.GetTeam(); - - if (team is < 1 or > 2) - return; - } - - if (@lock && player != null) - { - await player.PurchaseSync.WaitAsync(token).ConfigureAwait(false); - locked = true; - } - if (parameters.Amount == 0 || PointsConfigObj.Data.GlobalXPMultiplier <= 0f || parameters.StartingMultiplier <= 0f) - return; - int amount = Mathf.RoundToInt(parameters.Amount * PointsConfigObj.Data.GlobalXPMultiplier * parameters.StartingMultiplier); - - bool redmessage = amount < 0 && parameters.IsPunishment; - int currentAmount = await Data.DatabaseManager.AddCredits(parameters.Steam64, team, amount, token).ConfigureAwait(false); - if (Data.RemoteSQL != null) - { - remote = Data.RemoteSQL.AddCredits(parameters.Steam64, team, amount, token); - } - int oldamt = currentAmount - amount; - await UniTask.SwitchToMainThread(token); - - if (player != null) - player.CachedCredits = currentAmount; - - ActionLog.Add(ActionLogType.CreditsChanged, oldamt + " >> " + currentAmount, parameters.Steam64); - - if (player != null && player.IsOnline && !player.HasUIHidden && !Data.Gamemode.LeaderboardUp()) - { - Translation key = T.XPToastGainCredits; - if (amount < 0) - key = redmessage ? T.XPToastLoseCredits : T.XPToastPurchaseCredits; - - string number = key.Translate(player, Math.Abs(amount)); - ToastMessage.QueueMessage(player, - !string.IsNullOrEmpty(parameters.Message) - ? new ToastMessage(ToastMessageStyle.Mini, number + "\n" + parameters.Message!.Colorize("adadad")) - : new ToastMessage(ToastMessageStyle.Mini, number)); - - if (!parameters.IsPurchase && player.Player.TryGetPlayerData(out UCPlayerData c)) - { - if (c.Stats is IExperienceStats kd) - kd.AddCredits(amount); - } - - player.PointsDirtyMask |= 0b00001000; - CreditsUI.Update(player, false); - } - } - catch (Exception ex) - { - L.LogError("Error giving credits to " + (player?.Name.PlayerName ?? string.Empty) + " (" + parameters.Steam64 + ")"); - L.LogError(ex); - } - finally - { - if (locked) - player!.PurchaseSync.Release(); - } - if (remote != null && !remote.IsCompleted) - await remote.ConfigureAwait(false); - } - public static Task AwardCreditsAsync(UCPlayer player, int amount, Translation message, T0 arg0, bool redmessage = false, bool isPurchase = false, bool @lock = true, CancellationToken token = default) => - AwardCreditsAsync(player, amount, message.Translate(player, arg0), redmessage, isPurchase, @lock, token); - public static Task AwardCreditsAsync(UCPlayer player, int amount, Translation message, T0 arg0, T1 arg1, bool redmessage = false, bool isPurchase = false, bool @lock = true, CancellationToken token = default) => - AwardCreditsAsync(player, amount, message.Translate(player, arg0, arg1), redmessage, isPurchase, @lock, token); - public static void AwardCredits(UCPlayer player, int amount, string? message = null, bool redmessage = false, bool isPurchase = false, bool @lock = true) - { - CreditsParameters parameters = new CreditsParameters(player, player.GetTeam(), amount, message, redmessage) - { - IsPurchase = isPurchase - }; - AwardCredits(in parameters, @lock); - } - public static void AwardCredits(in CreditsParameters parameters, bool @lock = true) - { - UCWarfare.RunTask(AwardCreditsAsync, parameters, UCWarfare.UnloadCancel, @lock, ctx: "Award " + parameters.Amount + " credits to " + parameters.Steam64 + "."); - } - public static Task AwardCreditsAsync(UCPlayer player, int amount, string? message = null, bool redmessage = false, bool isPurchase = false, bool @lock = true, CancellationToken token = default) - { - CreditsParameters parameters = new CreditsParameters(player, player.GetTeam(), amount, message, redmessage) - { - IsPurchase = isPurchase - }; - return AwardCreditsAsync(parameters, token, @lock); - } - public static void AwardXP(UCPlayer player, XPReward reward, int amount) - { - AwardXP(new XPParameters(player, 0, amount) - { - Reward = reward, - Message = PointsConfig.GetDefaultTranslation(player.Locale.LanguageInfo, player.Locale.CultureInfo, reward) - }); - } - public static void AwardXP(UCPlayer player, XPReward reward, float multiplier = 1f) - { - AwardXP(new XPParameters(player, 0, reward) - { - Multiplier = multiplier, - Message = PointsConfig.GetDefaultTranslation(player.Locale.LanguageInfo, player.Locale.CultureInfo, reward) - }); - } - public static void AwardXP(UCPlayer player, XPReward reward, Translation translation, float multiplier = 1f) - { - string? t; - if (translation is Translation vehicleTranslation && - PointsConfig.TryGetVehicleType(reward, out VehicleType type)) - { - t = vehicleTranslation.Translate(player, false, type); - } - else t = translation.Translate(player); - AwardXP(new XPParameters(player, 0, reward) { Multiplier = multiplier, Message = t }); - } - public static void AwardXP(UCPlayer player, XPReward reward, Translation translation, int amount) - { - string? t; - if (translation is Translation vehicleTranslation && PointsConfig.TryGetVehicleType(reward, out VehicleType type)) - { - t = vehicleTranslation.Translate(player, false, type); - } - else t = translation.Translate(player); - AwardXP(new XPParameters(player, 0, amount) { Reward = reward, Message = t }); - } - public static void AwardXP(UCPlayer player, XPReward reward, string message, float multiplier = 1f) - { - AwardXP(new XPParameters(player, 0, reward) { Multiplier = multiplier, Message = message }); - } - public static void AwardXP(UCPlayer player, XPReward reward, string message, int amount) - { - AwardXP(new XPParameters(player, 0, amount) { Reward = reward, Message = message }); - } - public static void AwardXP(in XPParameters parameters) - { - UCWarfare.RunTask(AwardXPAsync, parameters, UCWarfare.UnloadCancel, ctx: "Award xp to " + parameters.Steam64 + "."); - } - public static async Task AwardXPAsync(XPParameters parameters, CancellationToken token = default) - { - try - { - // cancel on unload - using CombinedTokenSources tokens = token.CombineTokensIfNeeded(UCWarfare.UnloadCancel); - Task? remote = null; - await UniTask.SwitchToMainThread(token); - - int origAmt = parameters.Amount; - PointsConfig.XPData.TryGetValue(parameters.Reward.ToString(), out PointsConfig.XPRewardData? data); - if (origAmt == 0) - { - if (data == null || data.Amount == 0) - return; - origAmt = data.Amount; - } - - if (!Data.TrackStats || PointsConfigObj.Data.GlobalXPMultiplier == 0f) return; - float multiplier = -1; - UCPlayer? player = parameters.Player; - - if (player is { IsOnline: true } && data is not { IgnoresXPBoosts: true }) - { - // checks for any xp boost buffs - for (int i = 0; i < player.ActiveBuffs.Length; ++i) - { - if (player.ActiveBuffs[i] is IXPBoostBuff buff) - { - if (buff.Multiplier > multiplier) - multiplier = buff.Multiplier; - } - } - } - - if (multiplier < 0f) - multiplier = 1f; - else if (multiplier == 0f) - return; - float amt = origAmt; - if (data is not { IgnoresGlobalMultiplier: true }) - amt *= PointsConfig.GlobalXPMultiplier; - - if (data is not { IgnoresXPBoosts: true }) - amt *= multiplier; - - amt *= parameters.Multiplier; - - if (amt == 0) - return; - - ulong team = parameters.Team; - bool awardCredits = data != null ? data.CreditReward is { Amount: not 0 } or { Percentage: not 0 } : parameters.AwardCredits; - - // get current team if not specified - if (team is < 1 or > 2) - { - if (player is not { IsOnline: true }) - { - PlayerSave? save = PlayerManager.GetSave(parameters.Steam64); - if (save == null) - return; - team = save.Team; - } - else team = player.GetTeam(); - } - - if (team is < 1 or > 2) - return; - LevelData oldLevel = default; - int amtXp = Mathf.RoundToInt(amt); - int credits = 0; - - // calculate number of credits - if (awardCredits) - { - float mult; - if (data != null) - { - mult = data.CreditReward!.Percentage; - if (mult == 0) - mult = data.CreditReward.Amount / (float)origAmt; - } - else mult = (parameters.OverrideCreditPercentage ?? (PointsConfig.DefaultCreditPercentage / 100f)); - float cAmt = mult * origAmt; - if (cAmt > 0) - credits = Mathf.CeilToInt(cAmt); - else if (cAmt < 0) - credits = Mathf.FloorToInt(cAmt); - } - if (player != null) - { - await player.PurchaseSync.WaitAsync(token).ConfigureAwait(false); - oldLevel = player.Level; - } - try - { - int currentAmount; - int credsAmt = -1; - if (player is { IsOnline: true }) - { - // Begin adding to remote database. Will not be awaited yet. - if (Data.RemoteSQL != null && Data.RemoteSQL.Opened) - { - if (credits != 0) - remote = Data.RemoteSQL.AddCreditsAndXP(parameters.Steam64, team, credits, amtXp, token); - else - remote = Data.RemoteSQL.AddXP(parameters.Steam64, team, amtXp, token); - } - if (credits != 0) - { - (credsAmt, currentAmount) = await Data.DatabaseManager.AddCreditsAndXP(parameters.Steam64, team, credits, amtXp, token).ConfigureAwait(false); - } - else - { - currentAmount = await Data.DatabaseManager.AddXP(parameters.Steam64, team, amtXp, token).ConfigureAwait(false); - } - - // action logs - ActionLog.Add(ActionLogType.XPChanged, (currentAmount - amtXp).ToString(CultureInfo.InvariantCulture) + " >> " + - currentAmount.ToString(CultureInfo.InvariantCulture) + - " (" + (amtXp > 0 ? "+" : "-") - + Math.Abs(amtXp).ToString(CultureInfo.InvariantCulture) + - ") (" + parameters.Reward + ")", parameters.Steam64); - if (credits != 0) - { - ActionLog.Add(ActionLogType.CreditsChanged, (credsAmt - credits).ToString(CultureInfo.InvariantCulture) + " >> " + - credsAmt.ToString(CultureInfo.InvariantCulture) + - " (" + (credits > 0 ? "+" : "-") - + Math.Abs(credits).ToString(CultureInfo.InvariantCulture) + - ") (With XP: " + parameters.Reward + ")", parameters.Steam64); - } - await UniTask.SwitchToMainThread(token); - - if (player.IsOnline) - { - player.CachedXP = currentAmount; - if (credits != 0) - player.CachedCredits = credsAmt; - - // leaderboard stats - if (data is not { ExcludeFromLeaderboard: true } && player.Player.TryGetPlayerData(out UCPlayerData c) && c.Stats is IExperienceStats kd) - { - kd.AddXP(amtXp); - if (credits != 0) - kd.AddCredits(credits); - } - } - - // toasts - if (player.IsOnline && !player.HasUIHidden && !Data.Gamemode.LeaderboardUp()) - { - string number = (amtXp >= 0 ? T.XPToastGainXP : T.XPToastLoseXP).Translate(player, Math.Abs(amtXp)); - - number = number.Colorize(amtXp > 0 ? "e3e3e3" : "d69898"); - - ToastMessage.QueueMessage(player, - !string.IsNullOrEmpty(parameters.Message) - ? new ToastMessage(ToastMessageStyle.Mini, number + "\n" + parameters.Message!.Colorize("adadad")) - : new ToastMessage(ToastMessageStyle.Mini, number)); - if (credits != 0) - { - Translation key = credits < 0 ? T.XPToastLoseCredits : T.XPToastGainCredits; - - number = key.Translate(player, Math.Abs(credits)); - ToastMessage.QueueMessage(player, !string.IsNullOrEmpty(parameters.Message) - ? new ToastMessage(ToastMessageStyle.Mini, number + "\n" + parameters.Message!.Colorize("adadad")) - : new ToastMessage(ToastMessageStyle.Mini, number)); - - player.PointsDirtyMask |= 0b00001000; - CreditsUI.Update(player, false); - } - } - } - else if (awardCredits) - { - if (Data.RemoteSQL != null) - { - (int credits, int xp)[] arr = await Task.WhenAll( - Data.DatabaseManager.AddCreditsAndXP(parameters.Steam64, team, credits, amtXp, token), - Data.RemoteSQL.AddCreditsAndXP(parameters.Steam64, team, credits, amtXp, token)) - .ConfigureAwait(false); - currentAmount = arr.Length > 0 ? arr[0].xp : amtXp; - } - else - currentAmount = (await Data.DatabaseManager.AddCreditsAndXP(parameters.Steam64, team, credits, amtXp, token).ConfigureAwait(false)).Item2; - await UniTask.SwitchToMainThread(token); - } - else - { - currentAmount = await Data.DatabaseManager.AddXP(parameters.Steam64, team, amtXp, token).ConfigureAwait(false); - await UniTask.SwitchToMainThread(token); - } - - if (player == null) - oldLevel = new LevelData(Mathf.RoundToInt(currentAmount - amt)); - } - finally - { - player?.PurchaseSync.Release(); - } - - // check for promotions or demotions - if (player is { IsOnline: true }) - { - player.PointsDirtyMask |= 0b00000001; - if (!player.HasUIHidden && !Data.Gamemode.LeaderboardUp()) - { - if (player.Level.Level > oldLevel.Level) - { - ToastMessage.QueueMessage(player, - new ToastMessage(ToastMessageStyle.Large, new string[] { T.ToastPromoted.Translate(player), player.Level.Name.ToUpper(), string.Empty })); - player.PointsDirtyMask |= 0b00000010; - Signs.UpdateAllSigns(player); - } - else if (player.Level.Level < oldLevel.Level) - { - ToastMessage.QueueMessage(player, - new ToastMessage(ToastMessageStyle.Large, new string[] { T.ToastDemoted.Translate(player), player.Level.Name.ToUpper(), string.Empty })); - player.PointsDirtyMask |= 0b00000010; - Signs.UpdateAllSigns(player); - } - } - - XPUI.Update(player, false); - for (int i = 0; i < player.ActiveBuffs.Length; ++i) - if (player.ActiveBuffs[i] is IXPBoostBuff buff) - buff.OnXPBoostUsed(amtXp, awardCredits); - - // reputation changes - if (parameters.AwardReputation) - { - float percentage = (parameters.OverrideReputationPercentage ?? (data?.ReputationReward == null ? 10f : data.ReputationReward.Percentage)) / 100f; - int repAmt = parameters.OverrideReputationAmount ?? - (data?.ReputationReward == null || data.ReputationReward.Amount == 0 - ? Mathf.RoundToInt(amt * percentage) - : data.ReputationReward.Amount); - - if (repAmt != 0) - player.AddReputation(repAmt); - } - } - - if (remote != null && !remote.IsCompleted) - await remote.ConfigureAwait(false); - } - catch (Exception ex) - { - L.LogError("Exception awarding " + parameters.Amount + " XP to " + parameters.Steam64 + "."); - L.LogError(ex); - throw; - } - } - public static string GetProgressBar(float currentPoints, float totalPoints, int barLength = 50) - { - float ratio = currentPoints / totalPoints; - - int progress = Mathf.RoundToInt(ratio * barLength); - if (progress > barLength) - progress = barLength; - - return new string(PointsConfig.ProgressBlockCharacter, progress); - } - public static void TryAwardDriverAssist(PlayerDied args, XPReward reward, int amount, int rep) - { - if (args.DriverAssist is null || !args.DriverAssist.IsOnline) return; - if (amount == 0 && PointsConfig.XPData.TryGetValue(reward.ToString(), out PointsConfig.XPRewardData data)) - amount = data.Amount; - - AwardXP(new XPParameters(args.DriverAssist.Steam64, args.DeadTeam, amount) - { - Reward = XPReward.KillAssist, - Message = T.XPToastKillDriverAssist.Translate(args.DriverAssist.Locale.LanguageInfo, args.DriverAssist.Locale.CultureInfo), - OverrideReputationAmount = rep - }); - /* - if (quota != 0 && args.ActiveVehicle != null && args.ActiveVehicle.TryGetComponent(out VehicleComponent comp)) - comp.Quota += quota;*/ - } - public static void TryAwardDriverAssist(Player gunner, XPReward reward, int amount = 0, int rep = 0, float quota = 0) - { - InteractableVehicle vehicle = gunner.movement.getVehicle(); - if (vehicle != null) - { - if (amount == 0 && PointsConfig.XPData.TryGetValue(reward.ToString(), out PointsConfig.XPRewardData data)) - amount = data.Amount; - SteamPlayer driver = vehicle.passengers[0].player; - if (driver != null && - driver.playerID.steamID.m_SteamID != gunner.channel.owner.playerID.steamID.m_SteamID && - UCPlayer.FromSteamPlayer(driver) is { } ucplayer) - { - AwardXP(ucplayer, reward, T.XPToastKillDriverAssist, amount); - } - - //if (vehicle.transform.TryGetComponent(out VehicleComponent component)) - //{ - // component.Quota += quota; - //} - } - } - public static void TryAwardFOBCreatorXP(FOB fob, XPReward reward, float multiplier = 1f) - { - UCPlayer? creator = UCPlayer.FromID(fob.Owner); - - if (creator != null) - AwardXP(creator, reward, multiplier); - } - public static void OnPlayerDeath(PlayerDied e) - { - if (e.Killer is null || !e.Killer.IsOnline) return; - - double deadWorth = e.WasSuicide ? 0d : (3.5d * Math.Pow(e.Player.Player.skills.reputation, 1d / 3d)); - if (deadWorth is > 0d and < 10d) - deadWorth = 10d; - else if (deadWorth is < 0d and > -5d) - deadWorth = -5d; - - if (e.WasTeamkill) - { - AwardXP(new XPParameters(e.Killer, 0, (int)Math.Round(Math.Min(-deadWorth, -5d))) - { - Multiplier = 1f, - Message = PointsConfig.GetDefaultTranslation(e.Killer.Locale.LanguageInfo, e.Killer.Locale.CultureInfo, XPReward.Teamkill), - OverrideReputationAmount = (int)Math.Round(-deadWorth / 5d, MidpointRounding.AwayFromZero), - Reward = XPReward.Teamkill - }); - return; - } - - if (e.WasSuicide) - { - AwardXP(e.Killer, XPReward.Suicide); - return; - } - - int xpWorth = (int)Math.Round(Math.Max(deadWorth, 10d)); - - AwardXP(new XPParameters(e.Killer, 0, xpWorth) - { - Multiplier = 1f, - Message = PointsConfig.GetDefaultTranslation(e.Killer.Locale.LanguageInfo, e.Killer.Locale.CultureInfo, XPReward.EnemyKilled), - OverrideReputationAmount = (int)Math.Round(deadWorth * 0.2d, MidpointRounding.AwayFromZero), - Reward = XPReward.EnemyKilled - }); - - int assistHighXp = (int)Math.Round(xpWorth * 0.75d, MidpointRounding.AwayFromZero); - int assistLowXp = (int)Math.Round(xpWorth * 0.5d); - int assistHighRep = (int)Math.Round(deadWorth * 0.15d, MidpointRounding.AwayFromZero); - int assistLowRep = (int)Math.Round(deadWorth * 0.10d, MidpointRounding.AwayFromZero); - - if (e.Player.Player.TryGetPlayerData(out UCPlayerData component)) - { - ulong killerID = e.Killer.Steam64; - ulong victimID = e.Player.Steam64; - - UCPlayer? assister = UCPlayer.FromID(component.SecondLastAttacker.Key); - if (assister != null && assister.Steam64 != killerID && assister.Steam64 != victimID && (DateTime.Now - component.SecondLastAttacker.Value).TotalSeconds <= 30) - { - AwardXP(new XPParameters(assister, 0, assistLowXp) - { - Multiplier = 1f, - Message = PointsConfig.GetDefaultTranslation(assister.Locale.LanguageInfo, assister.Locale.CultureInfo, XPReward.KillAssist), - OverrideReputationAmount = assistLowRep, - Reward = XPReward.KillAssist - }); - } - - if (e.Player.Player.TryGetComponent(out SpottedComponent spotted) && - PointsConfig.XPData.TryGetValue(nameof(XPReward.EnemyKilled), out PointsConfig.XPRewardData data)) - { - spotted.OnTargetKilled(assistHighXp, assistHighRep); - } - - component.ResetAttackers(); - } - - TryAwardDriverAssist(e, XPReward.EnemyKilled, assistHighXp, assistHighRep); - } - private static void OnPlayerLeft(PlayerEvent e) - { - if (Data.RemoteSQL != null && Data.RemoteSQL.Opened) - { - UCPlayer caller = e.Player; - Task.Run(async () => - { - await caller.PurchaseSync.WaitAsync(); - try - { - uint t1Xp = 0; - uint t2Xp = 0; - uint t1Cd = 0; - uint t2Cd = 0; - await Data.DatabaseManager.QueryAsync( - "SELECT `Experience`, `Credits`, `Team` FROM `" + WarfareSQL.TableLevels + "` WHERE `Steam64` = @0 LIMIT 2;", - new object[] { caller.Steam64 }, - reader => - { - ulong team = reader.GetUInt64(2); - if (team == 1) - { - t1Xp = reader.GetUInt32(0); - t1Cd = reader.GetUInt32(1); - } - else if (team == 2) - { - t2Xp = reader.GetUInt32(0); - t2Cd = reader.GetUInt32(1); - } - }); - await Data.RemoteSQL.NonQueryAsync( - "INSERT INTO `" + WarfareSQL.TableLevels + "` (`Steam64`, `Team`, `Experience`, `Credits`) VALUES (@0, 1, @1, @2), (@0, 2, @3, @4) AS vals " + - "ON DUPLICATE KEY UPDATE `Experience` = vals.Experience, `Credits` = vals.Credits;", - new object[] { caller.Steam64, t1Xp, t1Cd, t2Xp, t2Cd }).ConfigureAwait(false); - } - catch (Exception ex) - { - L.LogError("Error trying to sync player levels with remote server."); - L.LogError(ex); - } - finally - { - caller.PurchaseSync.Release(); - } - }); - } - } - public static async Task UpdateAllPointsAsync(CancellationToken token = default) - { - if (PlayerManager.OnlinePlayers.Count < 1) - return; - StringBuilder builder = new StringBuilder(UpdateAllPointsQuery.Length + PlayerManager.OnlinePlayers.Count * 18 + 1); - builder.Append(UpdateAllPointsQuery); - for (int i = 0; i < PlayerManager.OnlinePlayers.Count; ++i) - { - PlayerManager.OnlinePlayers[i].IsDownloadingXP = true; - if (i != 0) - builder.Append(','); - builder.Append(PlayerManager.OnlinePlayers[i].Steam64); - } - - builder.Append(");"); - - string query = builder.ToString(); - List data = new List(PlayerManager.OnlinePlayers.Count); - void ReadLoop(MySqlDataReader reader) - { - data.Add(new XPData(reader.GetUInt64(0), reader.GetUInt64(1), reader.GetUInt32(2), reader.GetUInt32(3))); - } - - if (Data.AdminSql.Opened) - { - await Data.AdminSql.QueryAsync(query, null, ReadLoop, token).ConfigureAwait(false); - } - else if (Data.AdminSql != Data.DatabaseManager && Data.DatabaseManager.Opened) - { - await Data.DatabaseManager.QueryAsync(query, null, ReadLoop, token).ConfigureAwait(false); - } - else - { - L.LogWarning("No SQL connections to download levels."); - return; - } - - foreach (UCPlayer player in PlayerManager.OnlinePlayers.ToList()) - { - if (!player.IsOnline) continue; - ulong id = player.Steam64; - for (int j = 0; j < data.Count; ++j) - { - if (data[j].Steam64 == id) - goto c; - } - - await UpdatePointsAsync(player, true, token.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(player.DisconnectToken, token).Token : player.DisconnectToken).ConfigureAwait(false); - c:; - } - - for (int j = 0; j < data.Count; ++j) - { - XPData levels = data[j]; - UCPlayer? pl = UCPlayer.FromID(levels.Steam64); - if (pl is null || pl.GetTeam() != levels.Team) - continue; - await pl.PurchaseSync.WaitAsync(token.CanBeCanceled ? CancellationTokenSource.CreateLinkedTokenSource(pl.DisconnectToken, token).Token : pl.DisconnectToken).ConfigureAwait(false); - try - { - pl.UpdatePoints(levels.XP, levels.Credits); - } - finally - { - pl.PurchaseSync.Release(); - } - } - - await UCWarfare.ToUpdate(); - for (int i = 0; i < PlayerManager.OnlinePlayers.Count; ++i) - { - UCPlayer pl = PlayerManager.OnlinePlayers[i]; - pl.IsDownloadingXP = false; - pl.PointsDirtyMask |= 0b00001011; - XPUI.Update(pl, false); - CreditsUI.Update(pl, false); - } - } - private record struct XPData(ulong Steam64, ulong Team, uint XP, uint Credits); - private static void OnKitChanged(UCPlayer player, Kit? kit, Kit? oldkit) - { - Branch oldbranch = Branch.Default; - if (oldkit != null) - oldbranch = oldkit.Branch; - if (player.KitBranch != oldbranch) - { - player.PointsDirtyMask |= 0b00000100; - XPUI.Update(player, false); - } - } - public static async Task UpdatePointsAsync(UCPlayer caller, bool @lock, CancellationToken token = default) - { - if (caller is null) throw new ArgumentNullException(nameof(caller)); - caller.IsDownloadingXP = true; - if (@lock) - await caller.PurchaseSync.WaitAsync(token).ConfigureAwait(false); - try - { - ulong team = caller.GetTeam(); - if (team is 1 or 2) - { - bool found = false; - bool found2 = false; - uint cd = 1; - uint xp = 1; - uint cd2 = 1; - uint xp2 = 1; - Task? t2 = null; - if (Data.RemoteSQL != null && Data.RemoteSQL.Opened) - { - t2 = Data.RemoteSQL.QueryAsync( - "SELECT `Experience`, `Credits` FROM `" + WarfareSQL.TableLevels + "` WHERE `Steam64` = @0 AND `Team` = @1 LIMIT 1;", - new object[] { caller.Steam64, team }, - reader => - { - xp2 = reader.GetUInt32(0); - cd2 = reader.GetUInt32(1); - found2 = true; - }, token); - } - - await Data.DatabaseManager.QueryAsync( - "SELECT `Experience`, `Credits` FROM `" + WarfareSQL.TableLevels + "` WHERE `Steam64` = @0 AND `Team` = @1 LIMIT 1;", - new object[] { caller.Steam64, team }, - reader => - { - xp = reader.GetUInt32(0); - cd = reader.GetUInt32(1); - found = true; - }, token).ConfigureAwait(false); - if (found) - caller.UpdatePoints(xp, cd); - if (t2 != null) - await t2.ConfigureAwait(false); - if (!found && found2) - { - cd = cd2; - xp = xp2; - L.LogWarning(caller.Steam64 + - " Missing local levels, Remote: (XP: " + xp2 + ", Credits: " + cd2 + ")."); - } - - if (cd != cd2 || xp != xp2) - { - WarfareSQL? target; - if (found2) - { - L.LogWarning("Inconsistancy between remote and local experience/credit values for " + - caller.Steam64 + - " Remote: (XP: " + xp2 + ", Credits: " + cd2 + "), Local: (" + xp + ", " + cd + - ")."); - if (xp2 > xp) - { - xp = xp2; - cd = cd2; - target = Data.DatabaseManager; - } - else - target = Data.RemoteSQL; - } - else target = found ? Data.RemoteSQL : null; - - if (target != null && target.Opened) - await target.NonQueryAsync( - "INSERT INTO `" + WarfareSQL.TableLevels + "` (`Steam64`, `Team`, `Experience`, `Credits`) VALUES (@0, @1, @2, @3) ON DUPLICATE KEY UPDATE `Experience` = @2, `Credits` = @3;", - new object[] { caller.Steam64, team, xp, cd }, token).ConfigureAwait(false); - } - - if (!found && !found2) - { - xp = 0; - cd = (uint)PointsConfig.StartingCredits; - } - caller.UpdatePoints(xp, cd); - } - } - catch (Exception ex) - { - L.LogError("Error downloading " + caller.Steam64 + " (" + caller.Name.PlayerName + ")'s XP and Credits."); - L.LogError(ex); - return; - } - finally - { - if (@lock) - caller.PurchaseSync.Release(); - caller.IsDownloadingXP = false; - caller.HasDownloadedXP = true; - } - await UniTask.SwitchToMainThread(token); - caller.PointsDirtyMask |= 0b00001011; - XPUI.Update(caller, false); - CreditsUI.Update(caller, false); - - Signs.UpdateAllSigns(caller); - } - /* - public static void AwardSquadXP(UCPlayer ucplayer, float range, int xp, int ofp, string KeyplayerTranslationKey, string squadTranslationKey, float squadMultiplier) - { - string xpstr = Translation.Translate(KeyplayerTranslationKey, ucplayer.Steam64); - string sqstr = Translation.Translate(squadTranslationKey, ucplayer.Steam64); - Points.AwardXP(ucplayer.Player, xp, xpstr); - - if (ucplayer.Squad != null && ucplayer.Squad.Members.Count > 1) - { - if (ucplayer == ucplayer.Squad.Leader) - OfficerManager.AddOfficerPoints(ucplayer.Player, ofp, sqstr); - - int squadxp = (int)Math.Round(xp * squadMultiplier); - int squadofp = (int)Math.Round(ofp * squadMultiplier); - - if (squadxp > 0) - { - for (int i = 0; i < ucplayer.Squad.Members.Count; i++) - { - UCPlayer member = ucplayer.Squad.Members[i]; - if (member != ucplayer && ucplayer.IsNearOtherPlayer(member, range)) - { - Points.AwardXP(member.Player, squadxp, sqstr); - if (member.IsSquadLeader()) - OfficerManager.AddOfficerPoints(ucplayer.Player, squadofp, sqstr); - } - } - } - } - } - */ - - private static void OnVehicleDestoryed(VehicleDestroyed e) - { - if (e.Instigator is null || e.VehicleData == null || e.Component == null) - return; - XPReward xpreward = e.VehicleData.Type switch - { - VehicleType.Humvee => XPReward.VehicleHumvee, - VehicleType.TransportGround => XPReward.VehicleTransportGround, - VehicleType.ScoutCar => XPReward.VehicleScoutCar, - VehicleType.LogisticsGround => XPReward.VehicleLogisticsGround, - VehicleType.APC => XPReward.VehicleAPC, - VehicleType.IFV => XPReward.VehicleIFV, - VehicleType.MBT => XPReward.VehicleMBT, - VehicleType.TransportAir => XPReward.VehicleTransportAir, - VehicleType.AttackHeli => XPReward.VehicleAttackHeli, - VehicleType.Jet => XPReward.VehicleJet, - VehicleType.AA => XPReward.VehicleAA, - VehicleType.HMG => XPReward.VehicleHMG, - VehicleType.ATGM => XPReward.VehicleATGM, - VehicleType.Mortar => XPReward.VehicleMortar, - _ => XPReward.VehicleOther - }; - if (xpreward != XPReward.VehicleOther) - { - ulong dteam = e.Instigator.GetTeam(); - bool vehicleWasEnemy = (dteam == 1 && e.Team == 2) || (dteam == 2 && e.Team == 1); - bool vehicleWasFriendly = dteam == e.Team; - // if (!vehicleWasFriendly) - // Stats.StatsManager.ModifyTeam(dteam, t => t.VehiclesDestroyed++, false); - int fullXP = 0; - if (PointsConfig.XPData.TryGetValue(xpreward.ToString(), out PointsConfig.XPRewardData data) && data.Amount > 0) - fullXP = data.Amount; - - - float totalDamage = 0; - int l = 0; - foreach (KeyValuePair> entry in e.Component.DamageTable) - { - if ((DateTime.Now - entry.Value.Value).TotalSeconds < 60) - { - totalDamage += entry.Value.Key; - ++l; - } - } - - string teamHexColor = TeamManager.GetTeamHexColor(e.Team); - if (vehicleWasEnemy) - { - Asset asset = Assets.find(e.Component.LastItem); - string reason = string.Empty; - if (asset != null) - { - if (asset is ItemAsset item) - reason = item.itemName; - else if (asset is VehicleAsset v) - reason = "suicide " + v.vehicleName; - } - - int distance = Mathf.RoundToInt((e.Instigator.Position - e.Vehicle.transform.position).magnitude); - - if (reason.Length == 0) - Chat.Broadcast(T.VehicleDestroyedUnknown, e.Instigator, e.Vehicle.asset, teamHexColor); - else - Chat.Broadcast(T.VehicleDestroyed, e.Instigator, e.Vehicle.asset, reason, distance, teamHexColor); - - ActionLog.Add(ActionLogType.OwnedVehicleDied, $"{e.Vehicle.asset.vehicleName} / {e.Vehicle.id} / {e.Vehicle.asset.GUID:N} ID: {e.Vehicle.instanceID}" + - $" - Destroyed by {e.Instigator.Steam64.ToString(CultureInfo.InvariantCulture)}", e.OwnerId); - - if (e.Instigator != null) - QuestManager.OnVehicleDestroyed(e, e.Instigator); - - float resMax = 0f; - UCPlayer? resMaxPl = null; - DateTime now = DateTime.Now; - KeyValuePair[] assists = new KeyValuePair[l]; - foreach (KeyValuePair> entry in e.Component.DamageTable) - { - if ((now - entry.Value.Value).TotalSeconds < 60) - { - float responsibleness = entry.Value.Key / totalDamage; - int reward = Mathf.RoundToInt(responsibleness * fullXP); - int repReward = (int)Math.Round(reward / 10d, MidpointRounding.AwayFromZero); - if (l > 1) - assists[--l] = new KeyValuePair(entry.Key, responsibleness); - UCPlayer? attacker = UCPlayer.FromID(entry.Key); - if (attacker != null && attacker.GetTeam() != e.Team) - { - ulong team = attacker.GetTeam(); - if (entry.Key == e.InstigatorId) - { - AwardXP(new XPParameters(attacker, team, reward) - { - Reward = xpreward, - OverrideReputationAmount = repReward - }); - UCPlayer? pl = e.LastDriver ?? e.Owner; - if (pl is not null && pl.Steam64 != e.InstigatorId) - TryAwardDriverAssist(pl, xpreward, (int)Math.Round(reward * 0.75d, MidpointRounding.AwayFromZero), (int)Math.Round(repReward * 0.75d, MidpointRounding.AwayFromZero), e.VehicleData.TicketCost); - - if (e.Spotter != null) - { - e.Spotter.OnTargetKilled((int)Math.Round(reward * 0.75d, MidpointRounding.AwayFromZero), (int)Math.Round(repReward * 0.75d, MidpointRounding.AwayFromZero)); - Destroy(e.Spotter); - } - - if (attacker.Player.TryGetPlayerData(out UCPlayerData c)) - { - if (c.Stats is IPVPModeStats kd) - { - if (VehicleData.IsGroundVehicle(e.VehicleData.Type)) - kd.AddVehicleKill(); - else if (VehicleData.IsAircraft(e.VehicleData.Type)) - kd.AddAircraftKill(); - } - } - } - else if (responsibleness > 0.1F) - { - AwardXP(new XPParameters(attacker, team, reward) - { - Reward = xpreward, - Message = T.XPToastKillVehicleAssist.Translate(attacker.Locale.LanguageInfo, attacker.Locale.CultureInfo), - OverrideReputationAmount = repReward - }); - } - - if (responsibleness > resMax) - { - resMax = responsibleness; - resMaxPl = attacker; - } - } - } - } - - Array.Sort(assists, (a, b) => b.Value.CompareTo(a.Value)); - e.Assists = assists.ToArray(); - if (resMaxPl != null && resMax > 0.4f && e.InstigatorId != resMaxPl.Steam64) - { - QuestManager.OnVehicleDestroyed(e, resMaxPl); - } - - // if (e.InstigatorId != 0) - // Stats.StatsManager.ModifyStats(e.InstigatorId, s => s.VehiclesDestroyed++, false); - // Stats.StatsManager.ModifyVehicle(e.Vehicle.id, v => v.TimesDestroyed++); - } - else if (vehicleWasFriendly) - { - Translation message = e.Component.IsAircraft ? T.XPToastFriendlyAircraftDestroyed : T.XPToastFriendlyVehicleDestroyed; - Translation vehicleTeamkilled = T.VehicleTeamkilled; - Chat.Broadcast(vehicleTeamkilled, e.Instigator, e.Vehicle.asset, teamHexColor); - string val = T.VehicleTeamkilled.Translate(Localization.GetDefaultLanguage(), e.Instigator, e.Vehicle.asset, teamHexColor); - - ActionLog.Add(ActionLogType.OwnedVehicleDied, $"{e.Vehicle.asset.vehicleName} / {e.Vehicle.id} / {e.Vehicle.asset.GUID:N} ID: {e.Vehicle.instanceID}" + - $" - Destroyed by {e.InstigatorId}", e.OwnerId); - if (e.Instigator is not null) - AwardCredits(e.Instigator, -Mathf.Clamp(e.VehicleData.CreditCost, 5, 1000), message, e.VehicleData.Type, true, @lock: false); - - DateTimeOffset now = DateTimeOffset.UtcNow; - int ct = e.Vehicle.passengers.Count(x => x.player != null); - RelatedActor[] actors = new RelatedActor[ct + 1]; - for (int i = e.Vehicle.passengers.Length - 1; i >= 0; --i) - { - SteamPlayer steamPlayer = e.Vehicle.passengers[i].player; - if (steamPlayer == null) - continue; - - actors[--ct] = new RelatedActor(i == 0 ? "Driver" : ("Passenger #" + i.ToString(CultureInfo.InvariantCulture)), - false, Actors.GetActor(steamPlayer.playerID.steamID.m_SteamID)); - } - - actors[^1] = new RelatedActor("Owner", false, Actors.GetActor(e.OwnerId)); - - VehicleTeamkill log = new VehicleTeamkill - { - Player = e.InstigatorId, - Actors = actors, - RelevantLogsBegin = e.Component.TimeRequested <= 0 ? null : now.Subtract(TimeSpan.FromSeconds(Time.realtimeSinceStartup - e.Component.TimeRequested)), - RelevantLogsEnd = now, - StartedTimestamp = now, - ResolvedTimestamp = now, - Message = val, - Origin = e.DamageOrigin, - Reputation = -50, - Vehicle = e.Vehicle.asset.GUID, - VehicleName = e.Vehicle.asset.vehicleName - }; - - if (e.Instigator is { IsOnline: true }) - e.Instigator.AddReputation(-50); - else - log.PendingReputation = -50; - - UCWarfare.RunTask(Data.ModerationSql.AddOrUpdate, log, CancellationToken.None, ctx: "Log vehicle teamkill."); - } - /* - float missingQuota = vc.Quota - vc.RequiredQuota; - if (missingQuota < 0) - { - // give quota penalty - if (vc.RequiredQuota != -1 && (vehicleWasEnemy || wasCrashed)) - { - for (byte i = 0; i < vehicle.passengers.Length; i++) - { - Passenger passenger = vehicle.passengers[i]; - - if (passenger.player is not null) - { - vc.EvaluateUsage(passenger.player); - } - } - - double totalTime = 0; - foreach (KeyValuePair entry in vc.UsageTable) - totalTime += entry.Value; - - foreach (KeyValuePair entry in vc.UsageTable) - { - float responsibleness = (float)(entry.Value / totalTime); - int penalty = Mathf.RoundToInt(responsibleness * missingQuota * 60F); - - UCPlayer? assetWaster = UCPlayer.FromID(entry.Key); - if (assetWaster != null) - Points.AwardXP(assetWaster, penalty, Translation.Translate("xp_wasting_assets", assetWaster)); - } - } - } - */ - Data.Reporter?.OnVehicleDied(e.OwnerId, Data.Is(out IVehicles vgm) && vgm.VehicleSpawner.TryGetSpawn(e.Vehicle, out SqlItem spawn) - ? spawn.PrimaryKey : PrimaryKey.NotAssigned, e.InstigatorId, e.Vehicle.asset.GUID, e.Component.LastItem, e.Component.LastDamageOrigin, vehicleWasFriendly); - } - } -#endif -} -public enum XPReward -{ - Custom = 0, - OnDuty, - EnemyKilled, - KillAssist, - Teamkill, - Suicide, - Revive, - RadioDestroyed, - FriendlyRadioDestroyed, - BunkerDestroyed, - FriendlyBunkerDestroyed, - BunkerDeployment, - FortificationDestroyed, - FriendlyFortificationDestroyed, - BuildableDestroyed, - FriendlyBuildableDestroyed, - CacheDestroyed, - FriendlyCacheDestroyed, - FlagCaptured, - FlagNeutralized, - AttackingFlag, - DefendingFlag, - TransportingPlayer, - Shoveling, - BunkerBuilt, - Resupply, - RepairVehicle, - UnloadSupplies, - VehicleOther, - VehicleHumvee, - VehicleTransportGround, - VehicleScoutCar, - VehicleLogisticsGround, - VehicleAPC, - VehicleIFV, - VehicleMBT, - VehicleTransportAir, - VehicleAttackHeli, - VehicleJet, - VehicleAA, - VehicleHMG, - VehicleATGM, - VehicleMortar -} - -public class PointsConfig -{ - public const float DefaultCreditPercentage = 15f; - - [JsonPropertyName("player_starting_credits")] - public int StartingCredits { get; set; } - - [JsonPropertyName("block_char")] - public char ProgressBlockCharacter { get; set; } - - [JsonPropertyName("xp_data")] - public Dictionary XPData { get; set; } - - [JsonPropertyName("global_xp_multiplier")] - public float GlobalXPMultiplier { get; set; } - - public void SetDefaults() - { - StartingCredits = 500; - ProgressBlockCharacter = '█'; - XPData = new Dictionary(35) - { - { nameof(XPReward.Custom), - new XPRewardData(0) - { - IgnoresGlobalMultiplier = true, - IgnoresXPBoosts = true, - CreditReward = null, - ReputationReward = new CreditRewardData(0), - ExcludeFromLeaderboard = true - } - }, - { nameof(XPReward.OnDuty), - new XPRewardData(5) - { - IgnoresGlobalMultiplier = true, - IgnoresXPBoosts = true, - ExcludeFromLeaderboard = true, - CreditReward = new CreditRewardData(1), - ReputationReward = new CreditRewardData(0) - } - }, - { nameof(XPReward.EnemyKilled), new XPRewardData(10, DefaultCreditPercentage, 0f) }, // custom amount handling - { nameof(XPReward.CacheDestroyed), new XPRewardData(800, DefaultCreditPercentage, 5f) }, - { nameof(XPReward.FriendlyCacheDestroyed), - new XPRewardData(-8000) - { - CreditReward = new CreditRewardData(DefaultCreditPercentage) - { - IsPunishment = true - }, - ReputationReward = new CreditRewardData(-200) - { - IsPunishment = true - } - } - }, - { nameof(XPReward.KillAssist), new XPRewardData(5, DefaultCreditPercentage, 0f) }, // custom reputation handling - { nameof(XPReward.Teamkill), // custom reputation handling - new XPRewardData(-30) - { - CreditReward = new CreditRewardData(DefaultCreditPercentage) - { - IsPunishment = true - }, - ReputationReward = new CreditRewardData(0f) - { - IsPunishment = true - } - } - }, - { nameof(XPReward.Suicide), - new XPRewardData(-20) - { - CreditReward = new CreditRewardData(DefaultCreditPercentage) - { - IsPunishment = true - }, - ReputationReward = new CreditRewardData(-1) - { - IsPunishment = true - } - } - }, - { nameof(XPReward.Revive), new XPRewardData(30, DefaultCreditPercentage * 1.5f, 10f) }, - { nameof(XPReward.RadioDestroyed), new XPRewardData(80, DefaultCreditPercentage * 1.5f, 12.5f) }, - { nameof(XPReward.FriendlyRadioDestroyed), - new XPRewardData(-1000) - { - CreditReward = new CreditRewardData(DefaultCreditPercentage) - { - IsPunishment = true - }, - ReputationReward = new CreditRewardData(-100) - { - IsPunishment = true - } - } - }, - { nameof(XPReward.BunkerDestroyed), new XPRewardData(60, DefaultCreditPercentage * 1.5f, 10f) }, - { nameof(XPReward.FriendlyBunkerDestroyed), - new XPRewardData(-800) - { - CreditReward = new CreditRewardData(DefaultCreditPercentage) - { - IsPunishment = true - }, - ReputationReward = new CreditRewardData(-75) - { - IsPunishment = true - } - } - }, - { nameof(XPReward.BunkerDeployment), new XPRewardData(10, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.FlagCaptured), new XPRewardData(50, DefaultCreditPercentage, 4f) }, - { nameof(XPReward.FlagNeutralized), new XPRewardData(80, DefaultCreditPercentage, 3.75f) }, - { nameof(XPReward.AttackingFlag), new XPRewardData(8, DefaultCreditPercentage, 12.5f) }, - { nameof(XPReward.DefendingFlag), new XPRewardData(6, DefaultCreditPercentage, 16.667f) }, - { nameof(XPReward.TransportingPlayer), new XPRewardData(10, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.Shoveling), new XPRewardData(2, 50f, 0f) }, - { nameof(XPReward.BunkerBuilt), new XPRewardData(100, DefaultCreditPercentage * 1.5f, 4f) }, - { nameof(XPReward.Resupply), new XPRewardData(20, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.RepairVehicle), new XPRewardData(3, DefaultCreditPercentage, 33.333f) }, - { nameof(XPReward.UnloadSupplies), new XPRewardData(10, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.FriendlyFortificationDestroyed), new XPRewardData(0, DefaultCreditPercentage, 5f) }, // dependant amount - { nameof(XPReward.FortificationDestroyed), new XPRewardData(0, DefaultCreditPercentage, 5f) }, // dependant amount - { nameof(XPReward.FriendlyBuildableDestroyed), new XPRewardData(0, DefaultCreditPercentage, 5f) }, // dependant amount - { nameof(XPReward.BuildableDestroyed), new XPRewardData(0, DefaultCreditPercentage, 5f) }, // dependant amount - { nameof(XPReward.VehicleHumvee), new XPRewardData(25, DefaultCreditPercentage, 20f) }, - { nameof(XPReward.VehicleTransportGround), new XPRewardData(20, DefaultCreditPercentage, 20f) }, - { nameof(XPReward.VehicleScoutCar), new XPRewardData(30, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleLogisticsGround), new XPRewardData(25, DefaultCreditPercentage, 12.5f) }, - { nameof(XPReward.VehicleAPC), new XPRewardData(60, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleIFV), new XPRewardData(70, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleMBT), new XPRewardData(100, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleTransportAir), new XPRewardData(30, DefaultCreditPercentage, 20f) }, - { nameof(XPReward.VehicleAttackHeli), new XPRewardData(150, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleJet), new XPRewardData(200, DefaultCreditPercentage, 10f) }, - { nameof(XPReward.VehicleAA), new XPRewardData(20, DefaultCreditPercentage, 15f) }, - { nameof(XPReward.VehicleHMG), new XPRewardData(20, DefaultCreditPercentage, 15f) }, - { nameof(XPReward.VehicleATGM), new XPRewardData(20, DefaultCreditPercentage, 15f) }, - { nameof(XPReward.VehicleMortar), new XPRewardData(20, DefaultCreditPercentage, 15f) } - }; - - GlobalXPMultiplier = 1f; - } - internal static bool TryGetVehicleType(XPReward reward, out VehicleType type) - { - switch (reward) - { - case XPReward.VehicleHumvee: - type = VehicleType.Humvee; - break; - case XPReward.VehicleTransportGround: - type = VehicleType.TransportGround; - break; - case XPReward.VehicleScoutCar: - type = VehicleType.ScoutCar; - break; - case XPReward.VehicleLogisticsGround: - type = VehicleType.LogisticsGround; - break; - case XPReward.VehicleAPC: - type = VehicleType.APC; - break; - case XPReward.VehicleIFV: - type = VehicleType.IFV; - break; - case XPReward.VehicleMBT: - type = VehicleType.MBT; - break; - case XPReward.VehicleTransportAir: - type = VehicleType.TransportAir; - break; - case XPReward.VehicleAttackHeli: - type = VehicleType.AttackHeli; - break; - case XPReward.VehicleJet: - type = VehicleType.Jet; - break; - case XPReward.VehicleAA: - type = VehicleType.AA; - break; - case XPReward.VehicleHMG: - type = VehicleType.HMG; - break; - case XPReward.VehicleATGM: - type = VehicleType.ATGM; - break; - case XPReward.VehicleMortar: - type = VehicleType.Mortar; - break; - default: - type = VehicleType.None; - return false; - } - - return true; - } - internal static string GetDefaultTranslation(LanguageInfo language, CultureInfo culture, XPReward reward) - { - if (TryGetVehicleType(reward, out VehicleType vtype)) - { - return T.XPToastVehicleDestroyed.Translate(vtype, language, culture); - } - - Translation? t = reward switch - { - XPReward.UnloadSupplies => T.XPToastSuppliesUnloaded, - XPReward.OnDuty => T.XPToastOnDuty, - XPReward.EnemyKilled => T.XPToastEnemyKilled, - XPReward.KillAssist => T.XPToastKillAssist, - XPReward.Teamkill => T.XPToastFriendlyKilled, - XPReward.Suicide => T.XPToastSuicide, - XPReward.Revive => T.XPToastHealedTeammate, - XPReward.RadioDestroyed => T.XPToastFOBDestroyed, - XPReward.FriendlyRadioDestroyed => T.XPToastFriendlyFOBDestroyed, - XPReward.BunkerDestroyed => T.XPToastBunkerDestroyed, - XPReward.FriendlyBunkerDestroyed => T.XPToastFriendlyBunkerDestroyed, - XPReward.BunkerDeployment => T.XPToastFOBUsed, - XPReward.FlagCaptured => T.XPToastFlagCaptured, - XPReward.FlagNeutralized => T.XPToastFlagNeutralized, - XPReward.AttackingFlag => T.XPToastFlagAttackTick, - XPReward.DefendingFlag => T.XPToastFlagDefenseTick, - XPReward.TransportingPlayer => T.XPToastTransportingPlayers, - XPReward.Resupply => T.XPToastResuppliedTeammate, - XPReward.RepairVehicle => T.XPToastRepairedVehicle, - XPReward.CacheDestroyed => T.XPToastCacheDestroyed, - XPReward.FriendlyCacheDestroyed => T.XPToastFriendlyCacheDestroyed, - _ => null - }; - if (t == null) - return "{" + reward.ToString().ToUpperInvariant() + "}"; - return t.Translate(language); - } - - public sealed class XPRewardData - { - [JsonPropertyName("amount")] - public int Amount { get; set; } - - [JsonPropertyName("ignores_global_multiplier")] - public bool IgnoresGlobalMultiplier { get; set; } - - [JsonPropertyName("ignores_xp_boosts")] - public bool IgnoresXPBoosts { get; set; } - - [JsonPropertyName("credit_reward")] - public CreditRewardData? CreditReward { get; set; } - - [JsonPropertyName("reputation_reward")] - public CreditRewardData? ReputationReward { get; set; } - - [JsonPropertyName("exclude_from_leaderboard")] - public bool ExcludeFromLeaderboard { get; set; } - - public XPRewardData() { } - public XPRewardData(int amount) - { - Amount = amount; - } - public XPRewardData(int amount, float creditPercentage, float reputationPercentage) - { - Amount = amount; - CreditReward = new CreditRewardData(creditPercentage); - ReputationReward = new CreditRewardData(reputationPercentage); - } - } - - public sealed class CreditRewardData - { - [JsonPropertyName("amount")] - public int Amount { get; set; } - - [JsonPropertyName("percentage")] - public float Percentage { get; set; } - - [JsonPropertyName("is_punishment")] - public bool IsPunishment { get; set; } - - [JsonPropertyName("is_purchase")] - public bool IsPurchase { get; set; } - - public CreditRewardData() { } - public CreditRewardData(int amount) - { - Amount = amount; - } - public CreditRewardData(float percentage) - { - Percentage = percentage / 100f; - } - } -} -public struct CreditsParameters -{ - public readonly WarfarePlayer? Player; - public readonly CSteamID Steam64; - public readonly int Amount; - public readonly Team Team; - public bool IsPunishment = false; - /// Prevents updating stats. - public bool IsPurchase = false; - public float StartingMultiplier = 1f; - public string? Message; - - public static CreditsParameters WithTranslation(WarfarePlayer player, Translation translation, int amount) => - new CreditsParameters(player, player.Team, amount, translation.Translate(player)); - public static CreditsParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, int amount) => - new CreditsParameters(player, player.Team, amount, translation.Translate(arg0, player)); - public static CreditsParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, T1 arg1, int amount) => - new CreditsParameters(player, player.Team, amount, translation.Translate(arg0, arg1, player)); - public static CreditsParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, int amount) => - new CreditsParameters(player, team, amount, translation.Translate(player)); - public static CreditsParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, int amount) => - new CreditsParameters(player, team, amount, translation.Translate(arg0, player)); - public static CreditsParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, T1 arg1, int amount) => - new CreditsParameters(player, team, amount, translation.Translate(arg0, arg1, player)); - public CreditsParameters(CSteamID player, Team team, int amount) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Amount = amount; - Team = team; - Message = null; - IsPunishment = amount < 0; - } - public CreditsParameters(CSteamID player, Team team, int amount, string? message, bool isPunishment = true) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Team = team; - Amount = amount; - Message = message; - IsPunishment = amount < 0 && isPunishment; - } - public CreditsParameters(WarfarePlayer player, Team team, int amount) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Amount = amount; - Team = team; - Message = null; - IsPunishment = amount < 0; - } - public CreditsParameters(WarfarePlayer player, Team team, int amount, string? message, bool isPunishment = true) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Team = team; - Amount = amount; - Message = message; - IsPunishment = amount < 0 && isPunishment; - } - - // public readonly Task Award() => Points.AwardCreditsAsync(this); - // public readonly Task Award(CancellationToken token) => Points.AwardCreditsAsync(this, token); -} -public class XPParameters -{ - public readonly WarfarePlayer? Player; - public readonly CSteamID Steam64; - public readonly int Amount; - public readonly Team Team; - - public XPReward Reward; - public string? Message; - public bool AwardCredits; - public bool AwardReputation = true; - public float Multiplier = 1f; - public bool IgnoreXPBuff = false; - public bool IgnoreConfigXPBoost = false; - public bool AnnounceRankChange = true; - public float? OverrideCreditPercentage; - public float? OverrideReputationPercentage; - public int? OverrideReputationAmount; - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, int amount, bool awardCredits = true) => - new XPParameters(player, player.Team, amount, translation.Translate(player), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, int amount, bool awardCredits = true) => - new XPParameters(player, player.Team, amount, translation.Translate(arg0, player), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, T1 arg1, int amount, bool awardCredits = true) => - new XPParameters(player, player.Team, amount, translation.Translate(arg0, arg1, player, false), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, int amount, bool awardCredits = true) => - new XPParameters(player, team, amount, translation.Translate(player), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, int amount, bool awardCredits = true) => - new XPParameters(player, team, amount, translation.Translate(arg0, player), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, T1 arg1, int amount, bool awardCredits = true) => - new XPParameters(player, team, amount, translation.Translate(arg0, arg1, player), awardCredits); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, XPReward reward) => - new XPParameters(player, player.Team, reward, translation.Translate(player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, XPReward reward) => - new XPParameters(player, player.Team, reward, translation.Translate(arg0, player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, T1 arg1, XPReward reward) => - new XPParameters(player, player.Team, reward, translation.Translate(arg0, arg1, player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, XPReward reward) => - new XPParameters(player, team, reward, translation.Translate(player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, XPReward reward) => - new XPParameters(player, team, reward, translation.Translate(arg0, player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, T1 arg1, XPReward reward) => - new XPParameters(player, team, reward, translation.Translate(arg0, arg1, player), true); - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, XPReward reward, int amount) => - new XPParameters(player, player.Team, amount, translation.Translate(player), true) { Reward = reward }; - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, XPReward reward, int amount) => - new XPParameters(player, player.Team, amount, translation.Translate(arg0, player), true) { Reward = reward }; - public static XPParameters WithTranslation(WarfarePlayer player, Translation translation, T0 arg0, T1 arg1, XPReward reward, int amount) => - new XPParameters(player, player.Team, amount, translation.Translate(arg0, arg1, player), true) { Reward = reward }; - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, XPReward reward, int amount) => - new XPParameters(player, team, amount, translation.Translate(player), true) { Reward = reward }; - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, XPReward reward, int amount) => - new XPParameters(player, team, amount, translation.Translate(arg0, player), true) { Reward = reward }; - public static XPParameters WithTranslation(WarfarePlayer player, Team team, Translation translation, T0 arg0, T1 arg1, XPReward reward, int amount) => - new XPParameters(player, team, amount, translation.Translate(arg0, arg1, player), true) { Reward = reward }; - public XPParameters(CSteamID player, Team team, int amount) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Amount = amount; - Team = team; - AwardCredits = true; - Message = null; - Reward = XPReward.Custom; - } - public XPParameters(CSteamID player, Team team, XPReward reward) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Amount = 0; - Team = team; - AwardCredits = true; - Message = null; - Reward = reward; - } - public XPParameters(CSteamID player, Team team, int amount, string? message, bool awardCredits) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Team = team; - Amount = amount; - Message = message; - AwardCredits = awardCredits; - Reward = XPReward.Custom; - } - public XPParameters(CSteamID player, Team team, XPReward reward, string? message, bool awardCredits) - { - if (player.GetEAccountType() != EAccountType.k_EAccountTypeIndividual) - throw new ArgumentException("Invalid Steam64 ID: " + player, nameof(player)); - - Steam64 = player; - Player = null; - Team = team; - Amount = 0; - Message = message; - AwardCredits = awardCredits; - Reward = reward; - } - public XPParameters(WarfarePlayer player, Team team, int amount) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Amount = amount; - Team = team; - AwardCredits = true; - Message = null; - Reward = XPReward.Custom; - } - public XPParameters(WarfarePlayer player, Team team, XPReward reward) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Amount = 0; - Team = team; - AwardCredits = true; - Message = null; - Reward = reward; - } - public XPParameters(WarfarePlayer player, Team team, XPReward reward, string? message, bool awardCredits) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Team = team; - Amount = 0; - Message = message; - AwardCredits = awardCredits; - Reward = reward; - } - public XPParameters(WarfarePlayer player, Team team, int amount, string? message, bool awardCredits) - { - Player = player ?? throw new ArgumentNullException(nameof(player)); - Steam64 = player.Steam64; - Team = team; - Amount = amount; - Message = message; - AwardCredits = awardCredits; - Reward = XPReward.Custom; - } - - // public Task Award() => Points.AwardXPAsync(this); - // public Task Award(CancellationToken token) => Points.AwardXPAsync(this, token); -} \ No newline at end of file diff --git a/UncreatedWarfare/Levels/PointsUI.cs b/UncreatedWarfare/Levels/PointsUI.cs deleted file mode 100644 index 091b5949..00000000 --- a/UncreatedWarfare/Levels/PointsUI.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Uncreated.Framework.UI; -using Uncreated.Framework.UI.Reflection; -using Uncreated.Warfare.Configuration; - -namespace Uncreated.Warfare.Levels; - -[UnturnedUI(BasePath = "Canvas/Image")] -public class XPUI : UnturnedUI -{ - public readonly UnturnedUIElement Parent = new UnturnedUIElement("~/Canvas/Image"); - public readonly UnturnedLabel Rank = new UnturnedLabel("Image/Rank"); - public readonly UnturnedLabel XP = new UnturnedLabel("Image/XP"); - public readonly UnturnedLabel Next = new UnturnedLabel("Image/Next"); - public readonly UnturnedLabel Progress = new UnturnedLabel("Image/Progress"); - - public XPUI(AssetConfiguration assetConfig, ILoggerFactory loggerFactory) : base(loggerFactory, assetConfig.GetAssetLink("UI:XP"), reliable: false) { } -// public void SendTo(WarfarePlayer player) -// { -// GameThread.AssertCurrent(); - -// ITransportConnection c = player.Connection; -// L.LogDebug("Sending xp ui to " + player + " (" + Convert.ToString(player.PointsDirtyMask, 2) + ")"); - -// LevelData data = player.Level; -// SendToPlayer(c, -// data.Abbreviation, -// data.CurrentXP.ToString(player.Locale.CultureInfo) + "/" + data.RequiredXP.ToString(player.Locale.CultureInfo), -// data.NextAbbreviation, -// data.ProgressBar); -// if (player.HasUIHidden || Data.Gamemode.LeaderboardUp()) -// { -// Parent.SetVisibility(c, false); -// player.PointsDirtyMask |= 0b00100111; -// return; -// } - -// player.PointsDirtyMask &= unchecked((byte)~0b10000111); -// } - -// public void Update(UCPlayer player, bool full) -// { -// L.LogDebug("Updating xp ui for " + player + " (" + Convert.ToString(player.PointsDirtyMask, 2) + ")"); -// GameThread.AssertCurrent(); -// if (player.HasUIHidden || Data.Gamemode.LeaderboardUp()) -// return; -// if ((player.PointsDirtyMask & 0b10000000) == 0) -// { -// SendTo(player); -// return; -// } -// ITransportConnection c = player.Connection; -// LevelData data = player.Level; -// if (full || (player.PointsDirtyMask & 0b00000001) > 0) -// { -// XP.SetText(c, data.CurrentXP.ToString(player.Locale.CultureInfo) + "/" + data.RequiredXP.ToString(player.Locale.CultureInfo)); -// } -// if (full || (player.PointsDirtyMask & 0b00000010) > 0) -// { -// Rank.SetText(c, data.Abbreviation); -// Next.SetText(c, data.NextAbbreviation); -// } -// player.PointsDirtyMask &= unchecked((byte)~0b00100111); -// } - -// public void Clear(UCPlayer player) -// { -// L.LogDebug("Clearing xp ui for " + player); -// player.PointsDirtyMask |= 0b10100000; -// Parent.SetVisibility(player.Connection, false); -// } -} -public class CreditsUI : UnturnedUI -{ - public readonly UnturnedLabel Credits = new UnturnedLabel("Canvas/Image/Credits"); - public readonly UnturnedUIElement Parent = new UnturnedUIElement("Canvas/Image"); - private static string? _creditsColor; - public string CreditsColor => _creditsColor ??= "ffffff"; // todo use credit color - public CreditsUI(AssetConfiguration assetConfig, ILoggerFactory loggerFactory) : base(loggerFactory, assetConfig.GetAssetLink("UI:Credits")) { } - //public void SendTo(UCPlayer player) - //{ - // L.LogDebug("Sending creds ui to " + player + " (" + Convert.ToString(player.PointsDirtyMask, 2) + ")"); - // GameThread.AssertCurrent(); - // SendToPlayer(player.Connection, GetCreditsString(player)); - // player.PointsDirtyMask &= unchecked((byte)~0b01011000); - // if (player.HasUIHidden || Data.Gamemode.LeaderboardUp()) - // { - // Parent.SetVisibility(player.Connection, false); - // player.PointsDirtyMask |= 0b00010000; - // } - //} - - //public void Update(UCPlayer player, bool full) - //{ - // L.LogDebug("Updating creds ui for " + player + " (" + Convert.ToString(player.PointsDirtyMask, 2) + ")"); - // GameThread.AssertCurrent(); - // if ((!full && (player.PointsDirtyMask & 0b00001000) == 0) || player.HasUIHidden || Data.Gamemode.LeaderboardUp()) - // return; - // if ((player.PointsDirtyMask & 0b10000000) == 0) - // { - // SendTo(player); - // return; - // } - // if ((player.PointsDirtyMask & 0b00010000) > 0) - // { - // Parent.SetVisibility(player.Connection, true); - // } - // Credits.SetText(player.Connection, GetCreditsString(player)); - // player.PointsDirtyMask &= unchecked((byte)~0b00011000); - //} - - //public void Clear(UCPlayer player) - //{ - // L.LogDebug("Clearing creds ui for " + player); - // player.PointsDirtyMask |= 0b01010000; - // Parent.SetVisibility(player.Connection, false); - //} - - //private string GetCreditsString(UCPlayer player) - //{ - // return "C " + player.CachedCredits.ToString(player.Locale.CultureInfo); - //} -} \ No newline at end of file diff --git a/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.Designer.cs b/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.Designer.cs new file mode 100644 index 00000000..d7a59722 --- /dev/null +++ b/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.Designer.cs @@ -0,0 +1,2752 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Uncreated.Warfare.Database; + +namespace Uncreated.Warfare.Migrations +{ + [DbContext(typeof(WarfareDbContext))] + [Migration("20241029233410_AddPointsTable")] + partial class AddPointsTable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasCharSet("utf8mb4", DelegationModes.ApplyToColumns) + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.17"); + + modelBuilder.Entity("Uncreated.Warfare.Models.Authentication.HomebaseAuthenticationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AuthKey") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("Identity") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("varchar(16)"); + + b.Property("LastConnectTime") + .HasColumnType("datetime"); + + b.Property("Region") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.ToTable("homebase_auth_keys"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableInstanceId", b => + { + b.Property("SaveId") + .HasColumnType("int") + .HasColumnName("pk"); + + b.Property("RegionId") + .HasColumnType("tinyint unsigned"); + + b.Property("InstanceId") + .HasColumnType("int unsigned"); + + b.HasKey("SaveId", "RegionId"); + + b.ToTable("buildables_instance_ids"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableItemDisplayData", b => + { + b.Property("SaveId") + .HasColumnType("int") + .HasColumnName("pk"); + + b.Property("DynamicProps") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Mythic") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("Rotation") + .HasColumnType("tinyint unsigned"); + + b.Property("Skin") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("Tags") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.HasKey("SaveId"); + + b.ToTable("buildables_display_data"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableSave", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("pk"); + + b.Property("Group") + .HasColumnType("bigint unsigned"); + + b.Property("IsStructure") + .HasColumnType("tinyint(1)"); + + b.Property("Item") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("MapId") + .HasColumnType("int") + .HasColumnName("Map"); + + b.Property("Owner") + .HasColumnType("bigint unsigned"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("RotationX") + .HasColumnType("tinyint unsigned"); + + b.Property("RotationY") + .HasColumnType("tinyint unsigned"); + + b.Property("RotationZ") + .HasColumnType("tinyint unsigned"); + + b.Property("State") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varbinary(255)"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("buildables"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableStorageItem", b => + { + b.Property("SaveId") + .HasColumnType("int") + .HasColumnName("Save"); + + b.Property("PositionX") + .HasColumnType("tinyint unsigned"); + + b.Property("PositionY") + .HasColumnType("tinyint unsigned"); + + b.Property("Amount") + .HasColumnType("tinyint unsigned"); + + b.Property("Item") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("Quality") + .HasColumnType("tinyint unsigned"); + + b.Property("Rotation") + .HasColumnType("tinyint unsigned"); + + b.Property("State") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varbinary(255)"); + + b.HasKey("SaveId", "PositionX", "PositionY"); + + b.ToTable("buildables_stored_items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.Faction", b => + { + b.Property("Key") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Abbreviation") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("Emoji") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("FlagImageUrl") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("HexColor") + .IsRequired() + .HasColumnType("char(6)"); + + b.Property("InternalName") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("varchar(16)") + .HasColumnName("Id"); + + b.Property("KitPrefix") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("ShortName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.Property("SpriteIndex") + .HasColumnType("int"); + + b.Property("UnarmedKitId") + .HasColumnType("int unsigned") + .HasColumnName("UnarmedKitId"); + + b.HasKey("Key"); + + b.HasIndex("UnarmedKitId"); + + b.ToTable("factions"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.FactionAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Asset") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.Property("Redirect") + .IsRequired() + .HasColumnType("enum('Shirt','Pants','Vest','Hat','Mask','Backpack','Glasses','AmmoSupply','BuildSupply','RallyPoint','Radio','AmmoBag','AmmoCrate','RepairStation','Bunker','EntrenchingTool','UAV','RepairStationBuilt','AmmoCrateBuilt','BunkerBuilt','Cache','RadioDamaged','LaserDesignator')"); + + b.Property("VariantKey") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.HasKey("Id"); + + b.HasIndex("FactionId"); + + b.ToTable("faction_assets"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.FactionLocalization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Abbreviation") + .HasMaxLength(8) + .HasColumnType("varchar(8)"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("ShortName") + .HasMaxLength(24) + .HasColumnType("varchar(24)"); + + b.HasKey("Id"); + + b.HasIndex("FactionId"); + + b.HasIndex("LanguageId"); + + b.ToTable("faction_translations"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.GameData.GameRecord", b => + { + b.Property("GameId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("EndTimestamp") + .HasColumnType("datetime"); + + b.Property("Gamemode") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Map") + .HasColumnType("int"); + + b.Property("Region") + .HasColumnType("tinyint unsigned"); + + b.Property("Season") + .HasColumnType("int"); + + b.Property("StartTimestamp") + .HasColumnType("datetime"); + + b.Property("WinnerFactionId") + .HasColumnType("int unsigned") + .HasColumnName("Winner"); + + b.HasKey("GameId"); + + b.HasIndex("WinnerFactionId"); + + b.ToTable("stats_games"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.GameData.SessionRecord", b => + { + b.Property("SessionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("EndedTimestamp") + .HasColumnType("datetime") + .HasColumnName("EndedTimestampUTC"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.Property("FinishedGame") + .HasColumnType("tinyint(1)"); + + b.Property("GameId") + .HasColumnType("bigint unsigned") + .HasColumnName("Game"); + + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("KitName") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("LengthSeconds") + .HasColumnType("double"); + + b.Property("MapId") + .HasColumnType("int") + .HasColumnName("Map"); + + b.Property("NextSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("NextSession"); + + b.Property("PreviousSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("PreviousSession"); + + b.Property("SeasonId") + .HasColumnType("int") + .HasColumnName("Season"); + + b.Property("SquadLeader") + .HasColumnType("bigint unsigned") + .HasColumnName("SquadLeader"); + + b.Property("SquadName") + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("SquadName"); + + b.Property("StartedGame") + .HasColumnType("tinyint(1)"); + + b.Property("StartedTimestamp") + .HasColumnType("datetime") + .HasColumnName("StartedTimestampUTC"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("int"); + + b.Property("UnexpectedTermination") + .HasColumnType("tinyint(1)"); + + b.HasKey("SessionId"); + + b.HasIndex("FactionId"); + + b.HasIndex("GameId"); + + b.HasIndex("KitId"); + + b.HasIndex("MapId"); + + b.HasIndex("NextSessionId") + .IsUnique(); + + b.HasIndex("PreviousSessionId") + .IsUnique(); + + b.HasIndex("SeasonId"); + + b.HasIndex("SquadLeader"); + + b.HasIndex("Steam64"); + + b.ToTable("stats_sessions"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.ItemWhitelist", b => + { + b.Property("PrimaryKey") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("pk"); + + b.Property("Amount") + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("char(32)"); + + b.HasKey("PrimaryKey"); + + b.ToTable("item_whitelists"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Bundles.EliteBundle", b => + { + b.Property("PrimaryKey") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Cost") + .HasColumnType("decimal(65,30)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.Property("Id") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.HasKey("PrimaryKey"); + + b.HasIndex("FactionId"); + + b.ToTable("kits_bundles"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Bundles.KitEliteBundle", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("BundleId") + .HasColumnType("int unsigned") + .HasColumnName("Bundle"); + + b.HasKey("KitId", "BundleId"); + + b.HasIndex("BundleId"); + + b.ToTable("kits_bundle_items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Kit", b => + { + b.Property("PrimaryKey") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Branch") + .IsRequired() + .HasColumnType("enum('Infantry','Armor','Airforce','SpecOps','Navy')"); + + b.Property("Class") + .IsRequired() + .HasColumnType("enum('Unarmed','Squadleader','Rifleman','Medic','Breacher','AutomaticRifleman','Grenadier','MachineGunner','LAT','HAT','Marksman','Sniper','APRifleman','CombatEngineer','Crewman','Pilot','SpecOps')"); + + b.Property("CreatedTimestamp") + .HasColumnType("datetime") + .HasColumnName("CreatedAt"); + + b.Property("Creator") + .HasColumnType("bigint unsigned"); + + b.Property("CreditCost") + .HasColumnType("int"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("FactionFilterIsWhitelist") + .HasColumnType("tinyint(1)"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.Property("InternalName") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("varchar(25)") + .HasColumnName("Id"); + + b.Property("LastEditedTimestamp") + .HasColumnType("datetime") + .HasColumnName("LastEditedAt"); + + b.Property("LastEditor") + .HasColumnType("bigint unsigned"); + + b.Property("MapFilterIsWhitelist") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumCost") + .HasColumnType("decimal(65,30)"); + + b.Property("RequestCooldown") + .HasColumnType("float"); + + b.Property("RequiresNitro") + .HasColumnType("tinyint(1)"); + + b.Property("Season") + .HasColumnType("int"); + + b.Property("SquadLevel") + .IsRequired() + .HasColumnType("enum('Member','Commander')"); + + b.Property("TeamLimit") + .HasColumnType("float"); + + b.Property("Type") + .IsRequired() + .HasColumnType("enum('Public','Elite','Special','Loadout','Template')"); + + b.Property("WeaponText") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("Weapons"); + + b.HasKey("PrimaryKey"); + + b.HasIndex("FactionId"); + + b.ToTable("kits"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitAccess", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("AccessType") + .IsRequired() + .HasColumnType("enum('Unknown','Credits','Event','Purchase','QuestReward')"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("GivenAt"); + + b.HasKey("KitId", "Steam64"); + + b.HasIndex("Steam64"); + + b.ToTable("kits_access"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFavorite", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.HasKey("KitId", "Steam64"); + + b.HasIndex("Steam64"); + + b.ToTable("kits_favorites"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFilteredFaction", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Faction"); + + b.HasKey("KitId", "FactionId"); + + b.HasIndex("FactionId"); + + b.ToTable("kits_faction_filters"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFilteredMap", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Map") + .HasColumnType("int unsigned") + .HasColumnName("Map"); + + b.HasKey("KitId", "Map"); + + b.ToTable("kits_map_filters"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitHotkey", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Slot") + .HasColumnType("tinyint unsigned"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Item") + .HasColumnType("char(32)"); + + b.Property("Page") + .IsRequired() + .HasColumnType("enum('Primary','Secondary','Hands','Backpack','Vest','Shirt','Pants','Storage','Area')"); + + b.Property("Redirect") + .HasColumnType("enum('Shirt','Pants','Vest','Hat','Mask','Backpack','Glasses','AmmoSupply','BuildSupply','RallyPoint','Radio','AmmoBag','AmmoCrate','RepairStation','Bunker','EntrenchingTool','UAV','RepairStationBuilt','AmmoCrateBuilt','BunkerBuilt','Cache','RadioDamaged','LaserDesignator')"); + + b.Property("X") + .HasColumnType("tinyint unsigned"); + + b.Property("Y") + .HasColumnType("tinyint unsigned"); + + b.HasKey("KitId", "Slot", "Steam64"); + + b.HasIndex("Steam64"); + + b.ToTable("kits_hotkeys"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Amount") + .HasColumnType("tinyint unsigned"); + + b.Property("ClothingSlot") + .HasColumnType("enum('Shirt','Pants','Vest','Hat','Mask','Backpack','Glasses')"); + + b.Property("Item") + .HasColumnType("char(32)"); + + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Metadata") + .HasMaxLength(18) + .HasColumnType("varbinary(18)"); + + b.Property("Page") + .HasColumnType("enum('Primary','Secondary','Hands','Backpack','Vest','Shirt','Pants','Storage','Area')"); + + b.Property("Redirect") + .HasColumnType("enum('Shirt','Pants','Vest','Hat','Mask','Backpack','Glasses','AmmoSupply','BuildSupply','RallyPoint','Radio','AmmoBag','AmmoCrate','RepairStation','Bunker','EntrenchingTool','UAV','RepairStationBuilt','AmmoCrateBuilt','BunkerBuilt','Cache','RadioDamaged','LaserDesignator')"); + + b.Property("RedirectVariant") + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + + b.Property("Rotation") + .HasColumnType("tinyint unsigned"); + + b.Property("X") + .HasColumnType("tinyint unsigned"); + + b.Property("Y") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("KitId"); + + b.ToTable("kits_items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitLayoutTransformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("NewPage") + .IsRequired() + .HasColumnType("enum('Primary','Secondary','Hands','Backpack','Vest','Shirt','Pants','Storage','Area')"); + + b.Property("NewRotation") + .HasColumnType("tinyint unsigned"); + + b.Property("NewX") + .HasColumnType("tinyint unsigned"); + + b.Property("NewY") + .HasColumnType("tinyint unsigned"); + + b.Property("OldPage") + .IsRequired() + .HasColumnType("enum('Primary','Secondary','Hands','Backpack','Vest','Shirt','Pants','Storage','Area')"); + + b.Property("OldX") + .HasColumnType("tinyint unsigned"); + + b.Property("OldY") + .HasColumnType("tinyint unsigned"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.HasKey("Id"); + + b.HasIndex("KitId"); + + b.HasIndex("Steam64"); + + b.ToTable("kits_layouts"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitSkillset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("Level") + .HasColumnType("tinyint unsigned") + .HasColumnName("Level"); + + b.Property("Skill") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("enum('OVERKILL','SHARPSHOOTER','DEXTERITY','CARDIO','EXERCISE','DIVING','PARKOUR','SNEAKYBEAKY','VITALITY','IMMUNITY','TOUGHNESS','STRENGTH','WARMBLOODED','SURVIVAL','HEALING','CRAFTING','OUTDOORS','COOKING','FISHING','AGRICULTURE','MECHANIC','ENGINEER')") + .HasColumnName("Skill"); + + b.HasKey("Id"); + + b.HasIndex("KitId"); + + b.ToTable("kits_skillsets"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitTranslation", b => + { + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.HasKey("KitId", "LanguageId"); + + b.HasIndex("LanguageId"); + + b.ToTable("kits_sign_text"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitUnlockRequirement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Json") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("KitId") + .HasColumnType("int unsigned") + .HasColumnName("Kit"); + + b.HasKey("Id"); + + b.HasIndex("KitId"); + + b.ToTable("kits_unlock_requirements"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Alias") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.HasKey("Id"); + + b.HasIndex("LanguageId"); + + b.ToTable("lang_aliases"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageContributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Contributor") + .HasColumnType("bigint unsigned") + .HasColumnName("Contributor"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.HasKey("Id"); + + b.HasIndex("Contributor"); + + b.HasIndex("LanguageId"); + + b.ToTable("lang_credits"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageCulture", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("CultureCode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("varchar(16)"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.HasKey("Id"); + + b.HasIndex("LanguageId"); + + b.ToTable("lang_cultures"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageInfo", b => + { + b.Property("Key") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("Code") + .IsRequired() + .HasColumnType("char(5)"); + + b.Property("DefaultCultureCode") + .HasMaxLength(16) + .HasColumnType("varchar(16)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("FallbackTranslationLanguageCode") + .HasColumnType("char(5)"); + + b.Property("HasTranslationSupport") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("NativeName") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("RequiresIMGUI") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("SteamLanguageName") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("SupportsPluralization") + .HasColumnType("tinyint(1)"); + + b.HasKey("Key"); + + b.ToTable("lang_info"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguagePreferences", b => + { + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Culture") + .HasMaxLength(16) + .HasColumnType("varchar(16)"); + + b.Property("LanguageId") + .HasColumnType("int unsigned") + .HasColumnName("Language"); + + b.Property("LastUpdated") + .HasColumnType("datetime"); + + b.Property("UseCultureForCommandInput") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("UseCultureForCmdInput"); + + b.HasKey("Steam64"); + + b.HasIndex("LanguageId"); + + b.ToTable("lang_preferences"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.MapData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ReleasedSeasonId") + .HasColumnType("int") + .HasColumnName("SeasonReleased"); + + b.Property("Team1FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Team1Faction"); + + b.Property("Team2FactionId") + .HasColumnType("int unsigned") + .HasColumnName("Team2Faction"); + + b.Property("WorkshopId") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ReleasedSeasonId"); + + b.HasIndex("Team1FactionId"); + + b.HasIndex("Team2FactionId"); + + b.ToTable("maps"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.MapWorkshopDependency", b => + { + b.Property("MapId") + .HasColumnType("int") + .HasColumnName("Map"); + + b.Property("WorkshopId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("IsRemoved") + .HasColumnType("tinyint(1)"); + + b.HasKey("MapId", "WorkshopId"); + + b.ToTable("maps_dependencies"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.SeasonData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ReleaseTimestamp") + .HasColumnType("datetime") + .HasColumnName("ReleaseTimestampUTC"); + + b.HasKey("Id"); + + b.ToTable("seasons"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.AidRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("Health") + .HasColumnType("float"); + + b.Property("Instigator") + .HasColumnType("bigint unsigned") + .HasColumnName("Instigator"); + + b.Property("InstigatorPositionX") + .HasColumnType("float"); + + b.Property("InstigatorPositionY") + .HasColumnType("float"); + + b.Property("InstigatorPositionZ") + .HasColumnType("float"); + + b.Property("InstigatorSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("InstigatorSession"); + + b.Property("IsRevive") + .HasColumnType("tinyint(1)"); + + b.Property("Item") + .IsRequired() + .HasColumnType("char(32)"); + + b.Property("ItemName") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(48) + .HasColumnType("varchar(48)") + .HasDefaultValue("00000000000000000000000000000000"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.HasKey("Id"); + + b.HasIndex("Instigator"); + + b.HasIndex("InstigatorSessionId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_aid_records"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.DamageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("Cause") + .IsRequired() + .HasColumnType("enum('BLEEDING','BONES','FREEZING','BURNING','FOOD','WATER','GUN','MELEE','ZOMBIE','ANIMAL','SUICIDE','KILL','INFECTION','PUNCH','BREATH','ROADKILL','VEHICLE','GRENADE','SHRED','LANDMINE','ARENA','MISSILE','CHARGE','SPLASH','SENTRY','ACID','BOULDER','BURNER','SPIT','SPARK')"); + + b.Property("Damage") + .HasColumnType("float"); + + b.Property("Distance") + .HasColumnType("float"); + + b.Property("Instigator") + .HasColumnType("bigint unsigned") + .HasColumnName("Instigator"); + + b.Property("InstigatorPositionX") + .HasColumnType("float"); + + b.Property("InstigatorPositionY") + .HasColumnType("float"); + + b.Property("InstigatorPositionZ") + .HasColumnType("float"); + + b.Property("InstigatorSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("InstigatorSession"); + + b.Property("IsInjure") + .HasColumnType("tinyint(1)"); + + b.Property("IsInjured") + .HasColumnType("tinyint(1)"); + + b.Property("IsSuicide") + .HasColumnType("tinyint(1)"); + + b.Property("IsTeamkill") + .HasColumnType("tinyint(1)"); + + b.Property("Limb") + .IsRequired() + .HasColumnType("enum('LEFT_FOOT','LEFT_LEG','RIGHT_FOOT','RIGHT_LEG','LEFT_HAND','LEFT_ARM','RIGHT_HAND','RIGHT_ARM','LEFT_BACK','RIGHT_BACK','LEFT_FRONT','RIGHT_FRONT','SPINE','SKULL')"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("PrimaryAsset") + .HasColumnType("char(32)"); + + b.Property("PrimaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("RelatedPlayer") + .HasColumnType("bigint unsigned") + .HasColumnName("RelatedPlayer"); + + b.Property("RelatedPlayerPositionX") + .HasColumnType("float"); + + b.Property("RelatedPlayerPositionY") + .HasColumnType("float"); + + b.Property("RelatedPlayerPositionZ") + .HasColumnType("float"); + + b.Property("RelatedPlayerSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("RelatedPlayerSession"); + + b.Property("SecondaryAsset") + .HasColumnType("char(32)"); + + b.Property("SecondaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("TimeDeployedSeconds") + .HasColumnType("float"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.Property("Vehicle") + .HasColumnType("char(32)"); + + b.Property("VehicleName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.HasKey("Id"); + + b.HasIndex("Instigator"); + + b.HasIndex("InstigatorSessionId"); + + b.HasIndex("RelatedPlayer"); + + b.HasIndex("RelatedPlayerSessionId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_damage"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.DeathRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("DeathCause") + .IsRequired() + .HasColumnType("enum('BLEEDING','BONES','FREEZING','BURNING','FOOD','WATER','GUN','MELEE','ZOMBIE','ANIMAL','SUICIDE','KILL','INFECTION','PUNCH','BREATH','ROADKILL','VEHICLE','GRENADE','SHRED','LANDMINE','ARENA','MISSILE','CHARGE','SPLASH','SENTRY','ACID','BOULDER','BURNER','SPIT','SPARK')"); + + b.Property("DeathMessage") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Distance") + .HasColumnType("float"); + + b.Property("Instigator") + .HasColumnType("bigint unsigned") + .HasColumnName("Instigator"); + + b.Property("InstigatorPositionX") + .HasColumnType("float"); + + b.Property("InstigatorPositionY") + .HasColumnType("float"); + + b.Property("InstigatorPositionZ") + .HasColumnType("float"); + + b.Property("InstigatorSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("InstigatorSession"); + + b.Property("IsBleedout") + .HasColumnType("tinyint(1)"); + + b.Property("IsSuicide") + .HasColumnType("tinyint(1)"); + + b.Property("IsTeamkill") + .HasColumnType("tinyint(1)"); + + b.Property("KillShotId") + .HasColumnType("bigint unsigned") + .HasColumnName("KillShot"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("PrimaryAsset") + .HasColumnType("char(32)"); + + b.Property("PrimaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("RelatedPlayer") + .HasColumnType("bigint unsigned") + .HasColumnName("RelatedPlayer"); + + b.Property("RelatedPlayerPositionX") + .HasColumnType("float"); + + b.Property("RelatedPlayerPositionY") + .HasColumnType("float"); + + b.Property("RelatedPlayerPositionZ") + .HasColumnType("float"); + + b.Property("RelatedPlayerSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("RelatedPlayerSession"); + + b.Property("SecondaryAsset") + .HasColumnType("char(32)"); + + b.Property("SecondaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("TimeDeployedSeconds") + .HasColumnType("float"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.Property("Vehicle") + .HasColumnType("char(32)"); + + b.Property("VehicleName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.HasKey("Id"); + + b.HasIndex("Instigator"); + + b.HasIndex("InstigatorSessionId"); + + b.HasIndex("KillShotId") + .IsUnique(); + + b.HasIndex("RelatedPlayer"); + + b.HasIndex("RelatedPlayerSessionId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_deaths"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobItemBuilderRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("FobItemId") + .HasColumnType("bigint unsigned") + .HasColumnName("FobItem"); + + b.Property("Hits") + .HasColumnType("float"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("Responsibility") + .HasColumnType("double"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.HasKey("Id"); + + b.HasIndex("FobItemId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_fob_items_builders"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobItemRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("BuiltAt") + .HasColumnType("datetime") + .HasColumnName("BuiltAtUTC"); + + b.Property("DestroyedAt") + .HasColumnType("datetime") + .HasColumnName("DestroyedAtUTC"); + + b.Property("DestroyedByRoundEnd") + .HasColumnType("tinyint(1)"); + + b.Property("FobId") + .IsRequired() + .HasColumnType("bigint unsigned") + .HasColumnName("Fob"); + + b.Property("FobItemAngleX") + .HasColumnType("float"); + + b.Property("FobItemAngleY") + .HasColumnType("float"); + + b.Property("FobItemAngleZ") + .HasColumnType("float"); + + b.Property("FobItemPositionX") + .HasColumnType("float"); + + b.Property("FobItemPositionY") + .HasColumnType("float"); + + b.Property("FobItemPositionZ") + .HasColumnType("float"); + + b.Property("Instigator") + .HasColumnType("bigint unsigned") + .HasColumnName("Instigator"); + + b.Property("InstigatorPositionX") + .HasColumnType("float"); + + b.Property("InstigatorPositionY") + .HasColumnType("float"); + + b.Property("InstigatorPositionZ") + .HasColumnType("float"); + + b.Property("InstigatorSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("InstigatorSession"); + + b.Property("Item") + .HasColumnType("char(32)"); + + b.Property("ItemName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PlayerKills") + .HasColumnType("int"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("PrimaryAsset") + .HasColumnType("char(32)"); + + b.Property("PrimaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("SecondaryAsset") + .HasColumnType("char(32)"); + + b.Property("SecondaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("Teamkilled") + .HasColumnType("tinyint(1)"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.Property("Type") + .IsRequired() + .HasColumnType("enum('Fob','AmmoCrate','RepairStation','Fortification','Emplacement')"); + + b.Property("UseTimeSeconds") + .HasColumnType("double"); + + b.Property("VehicleKills") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FobId"); + + b.HasIndex("Instigator"); + + b.HasIndex("InstigatorSessionId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_fob_items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("AmmoCratesBuilt") + .HasColumnType("int"); + + b.Property("AmmoCratesDestroyed") + .HasColumnType("int"); + + b.Property("AmmoLoaded") + .HasColumnType("int"); + + b.Property("AmmoSpent") + .HasColumnType("int"); + + b.Property("BuildLoaded") + .HasColumnType("int"); + + b.Property("BuildSpent") + .HasColumnType("int"); + + b.Property("BunkersBuilt") + .HasColumnType("int"); + + b.Property("BunkersDestroyed") + .HasColumnType("int"); + + b.Property("DeploymentCount") + .HasColumnType("int"); + + b.Property("DestroyedAt") + .HasColumnType("datetime") + .HasColumnName("DestroyedAtUTC"); + + b.Property("DestroyedByRoundEnd") + .HasColumnType("tinyint(1)"); + + b.Property("EmplacementPlayerKills") + .HasColumnType("int"); + + b.Property("EmplacementVehicleKills") + .HasColumnType("int"); + + b.Property("EmplacementsBuilt") + .HasColumnType("int"); + + b.Property("EmplacementsDestroyed") + .HasColumnType("int"); + + b.Property("FobAngleX") + .HasColumnType("float"); + + b.Property("FobAngleY") + .HasColumnType("float"); + + b.Property("FobAngleZ") + .HasColumnType("float"); + + b.Property("FobName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("FobNumber") + .HasColumnType("int"); + + b.Property("FobPositionX") + .HasColumnType("float"); + + b.Property("FobPositionY") + .HasColumnType("float"); + + b.Property("FobPositionZ") + .HasColumnType("float"); + + b.Property("FobType") + .IsRequired() + .HasColumnType("enum('Other','RadioFob','SpecialFob','Cache')"); + + b.Property("FortificationsBuilt") + .HasColumnType("int"); + + b.Property("FortificationsDestroyed") + .HasColumnType("int"); + + b.Property("Instigator") + .HasColumnType("bigint unsigned") + .HasColumnName("Instigator"); + + b.Property("InstigatorPositionX") + .HasColumnType("float"); + + b.Property("InstigatorPositionY") + .HasColumnType("float"); + + b.Property("InstigatorPositionZ") + .HasColumnType("float"); + + b.Property("InstigatorSessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("InstigatorSession"); + + b.Property("NearestLocation") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("PositionX") + .HasColumnType("float"); + + b.Property("PositionY") + .HasColumnType("float"); + + b.Property("PositionZ") + .HasColumnType("float"); + + b.Property("PrimaryAsset") + .HasColumnType("char(32)"); + + b.Property("PrimaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("RepairStationsBuilt") + .HasColumnType("int"); + + b.Property("RepairStationsDestroyed") + .HasColumnType("int"); + + b.Property("SecondaryAsset") + .HasColumnType("char(32)"); + + b.Property("SecondaryAssetName") + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.Property("SessionId") + .HasColumnType("bigint unsigned") + .HasColumnName("Session"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.Property("Team") + .HasColumnType("tinyint unsigned"); + + b.Property("Teamkilled") + .HasColumnType("tinyint(1)"); + + b.Property("TeleportCount") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("datetime") + .HasColumnName("TimestampUTC"); + + b.HasKey("Id"); + + b.HasIndex("Instigator"); + + b.HasIndex("InstigatorSessionId"); + + b.HasIndex("SessionId"); + + b.HasIndex("Steam64"); + + b.HasIndex("Team"); + + b.ToTable("stats_fobs"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Users.Permission", b => + { + b.Property("PrimaryKey") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned") + .HasColumnName("pk"); + + b.Property("IsGroup") + .HasColumnType("tinyint(1)"); + + b.Property("PermissionOrGroup") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned"); + + b.HasKey("PrimaryKey"); + + b.HasIndex("Steam64"); + + b.ToTable("user_permissions"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Users.WarfareUserData", b => + { + b.Property("Steam64") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned"); + + b.Property("CharacterName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("DiscordId") + .HasColumnType("bigint unsigned"); + + b.Property("DisplayName") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("FirstJoined") + .HasColumnType("datetime"); + + b.Property("LastJoined") + .HasColumnType("datetime"); + + b.Property("NickName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PlayerName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("varchar(48)"); + + b.HasKey("Steam64"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Moderation.PlayerHWID", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned"); + + b.Property("FirstLogin") + .HasColumnType("datetime"); + + b.Property("HWID") + .IsRequired() + .HasColumnType("binary(20)"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("LastLogin") + .HasColumnType("datetime"); + + b.Property("LoginCount") + .HasColumnType("int"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.HasKey("Id"); + + b.HasIndex("HWID"); + + b.HasIndex("Steam64"); + + b.ToTable("hwids"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Moderation.PlayerIPAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int unsigned"); + + b.Property("FirstLogin") + .HasColumnType("datetime"); + + b.Property("IPAddress") + .HasColumnType("varchar(45)") + .HasColumnName("Unpacked"); + + b.Property("LastLogin") + .HasColumnType("datetime"); + + b.Property("LoginCount") + .HasColumnType("int"); + + b.Property("PackedIP") + .HasColumnType("int unsigned") + .HasColumnName("Packed"); + + b.Property("Steam64") + .HasColumnType("bigint unsigned") + .HasColumnName("Steam64"); + + b.HasKey("Id"); + + b.HasIndex("PackedIP"); + + b.HasIndex("Steam64"); + + b.ToTable("ip_addresses"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableInstanceId", b => + { + b.HasOne("Uncreated.Warfare.Models.Buildables.BuildableSave", "Save") + .WithMany("InstanceIds") + .HasForeignKey("SaveId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Save"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableItemDisplayData", b => + { + b.HasOne("Uncreated.Warfare.Models.Buildables.BuildableSave", "Save") + .WithOne("DisplayData") + .HasForeignKey("Uncreated.Warfare.Models.Buildables.BuildableItemDisplayData", "SaveId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Save"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableSave", b => + { + b.HasOne("Uncreated.Warfare.Models.Seasons.MapData", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableStorageItem", b => + { + b.HasOne("Uncreated.Warfare.Models.Buildables.BuildableSave", "Save") + .WithMany("Items") + .HasForeignKey("SaveId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Save"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.Faction", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "UnarmedKit") + .WithMany() + .HasForeignKey("UnarmedKitId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("UnarmedKit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.FactionAsset", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany("Assets") + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faction"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.FactionLocalization", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany("Translations") + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faction"); + + b.Navigation("Language"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.GameData.GameRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "WinnerFaction") + .WithMany() + .HasForeignKey("WinnerFactionId"); + + b.Navigation("WinnerFaction"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.GameData.SessionRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Uncreated.Warfare.Models.GameData.GameRecord", "Game") + .WithMany("Sessions") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany() + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Uncreated.Warfare.Models.Seasons.MapData", "Map") + .WithMany() + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "NextSession") + .WithOne() + .HasForeignKey("Uncreated.Warfare.Models.GameData.SessionRecord", "NextSessionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "PreviousSession") + .WithOne() + .HasForeignKey("Uncreated.Warfare.Models.GameData.SessionRecord", "PreviousSessionId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Uncreated.Warfare.Models.Seasons.SeasonData", "Season") + .WithMany() + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "SquadLeaderData") + .WithMany() + .HasForeignKey("SquadLeader"); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Faction"); + + b.Navigation("Game"); + + b.Navigation("Kit"); + + b.Navigation("Map"); + + b.Navigation("NextSession"); + + b.Navigation("PlayerData"); + + b.Navigation("PreviousSession"); + + b.Navigation("Season"); + + b.Navigation("SquadLeaderData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Bundles.EliteBundle", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Faction"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Bundles.KitEliteBundle", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Bundles.EliteBundle", "Bundle") + .WithMany("Kits") + .HasForeignKey("BundleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("Bundles") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bundle"); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Kit", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Faction"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitAccess", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("Access") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFavorite", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany() + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFilteredFaction", b => + { + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Faction") + .WithMany() + .HasForeignKey("FactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("FactionFilter") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Faction"); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitFilteredMap", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("MapFilter") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitHotkey", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany() + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitItemModel", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("ItemModels") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitLayoutTransformation", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany() + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitSkillset", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("Skillsets") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitTranslation", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("Translations") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + + b.Navigation("Language"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.KitUnlockRequirement", b => + { + b.HasOne("Uncreated.Warfare.Models.Kits.Kit", "Kit") + .WithMany("UnlockRequirementsModels") + .HasForeignKey("KitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Kit"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageAlias", b => + { + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany("Aliases") + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Language"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageContributor", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "ContributorData") + .WithMany() + .HasForeignKey("Contributor") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany("Contributors") + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ContributorData"); + + b.Navigation("Language"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageCulture", b => + { + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany("SupportedCultures") + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Language"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguagePreferences", b => + { + b.HasOne("Uncreated.Warfare.Models.Localization.LanguageInfo", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Language"); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.MapData", b => + { + b.HasOne("Uncreated.Warfare.Models.Seasons.SeasonData", "SeasonReleased") + .WithMany("Maps") + .HasForeignKey("ReleasedSeasonId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Team1Faction") + .WithMany() + .HasForeignKey("Team1FactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Factions.Faction", "Team2Faction") + .WithMany() + .HasForeignKey("Team2FactionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SeasonReleased"); + + b.Navigation("Team1Faction"); + + b.Navigation("Team2Faction"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.MapWorkshopDependency", b => + { + b.HasOne("Uncreated.Warfare.Models.Seasons.MapData", "Map") + .WithMany("Dependencies") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.AidRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "InstigatorData") + .WithMany() + .HasForeignKey("Instigator") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "InstigatorSession") + .WithMany() + .HasForeignKey("InstigatorSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InstigatorData"); + + b.Navigation("InstigatorSession"); + + b.Navigation("PlayerData"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.DamageRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "InstigatorData") + .WithMany() + .HasForeignKey("Instigator") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "InstigatorSession") + .WithMany() + .HasForeignKey("InstigatorSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "RelatedPlayerData") + .WithMany() + .HasForeignKey("RelatedPlayer") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "RelatedPlayerSession") + .WithMany() + .HasForeignKey("RelatedPlayerSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InstigatorData"); + + b.Navigation("InstigatorSession"); + + b.Navigation("PlayerData"); + + b.Navigation("RelatedPlayerData"); + + b.Navigation("RelatedPlayerSession"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.DeathRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "InstigatorData") + .WithMany() + .HasForeignKey("Instigator") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "InstigatorSession") + .WithMany() + .HasForeignKey("InstigatorSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Stats.Records.DamageRecord", "KillShot") + .WithOne() + .HasForeignKey("Uncreated.Warfare.Models.Stats.Records.DeathRecord", "KillShotId"); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "RelatedPlayerData") + .WithMany() + .HasForeignKey("RelatedPlayer") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "RelatedPlayerSession") + .WithMany() + .HasForeignKey("RelatedPlayerSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InstigatorData"); + + b.Navigation("InstigatorSession"); + + b.Navigation("KillShot"); + + b.Navigation("PlayerData"); + + b.Navigation("RelatedPlayerData"); + + b.Navigation("RelatedPlayerSession"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobItemBuilderRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Stats.Records.FobItemRecord", "FobItem") + .WithMany("Builders") + .HasForeignKey("FobItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("FobItem"); + + b.Navigation("PlayerData"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobItemRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Stats.Records.FobRecord", "Fob") + .WithMany("Items") + .HasForeignKey("FobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "InstigatorData") + .WithMany() + .HasForeignKey("Instigator") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "InstigatorSession") + .WithMany() + .HasForeignKey("InstigatorSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Fob"); + + b.Navigation("InstigatorData"); + + b.Navigation("InstigatorSession"); + + b.Navigation("PlayerData"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobRecord", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "InstigatorData") + .WithMany() + .HasForeignKey("Instigator") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "InstigatorSession") + .WithMany() + .HasForeignKey("InstigatorSessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.GameData.SessionRecord", "Session") + .WithMany() + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany() + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("InstigatorData"); + + b.Navigation("InstigatorSession"); + + b.Navigation("PlayerData"); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Moderation.PlayerHWID", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany("HWIDs") + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Moderation.PlayerIPAddress", b => + { + b.HasOne("Uncreated.Warfare.Models.Users.WarfareUserData", "PlayerData") + .WithMany("IPAddresses") + .HasForeignKey("Steam64") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Buildables.BuildableSave", b => + { + b.Navigation("DisplayData"); + + b.Navigation("InstanceIds"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Factions.Faction", b => + { + b.Navigation("Assets"); + + b.Navigation("Translations"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.GameData.GameRecord", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Bundles.EliteBundle", b => + { + b.Navigation("Kits"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Kits.Kit", b => + { + b.Navigation("Access"); + + b.Navigation("Bundles"); + + b.Navigation("FactionFilter"); + + b.Navigation("ItemModels"); + + b.Navigation("MapFilter"); + + b.Navigation("Skillsets"); + + b.Navigation("Translations"); + + b.Navigation("UnlockRequirementsModels"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Localization.LanguageInfo", b => + { + b.Navigation("Aliases"); + + b.Navigation("Contributors"); + + b.Navigation("SupportedCultures"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.MapData", b => + { + b.Navigation("Dependencies"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Seasons.SeasonData", b => + { + b.Navigation("Maps"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobItemRecord", b => + { + b.Navigation("Builders"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Stats.Records.FobRecord", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Uncreated.Warfare.Models.Users.WarfareUserData", b => + { + b.Navigation("HWIDs"); + + b.Navigation("IPAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.cs b/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.cs new file mode 100644 index 00000000..a0ba9084 --- /dev/null +++ b/UncreatedWarfare/Migrations/20241029233410_AddPointsTable.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Uncreated.Warfare.Stats; + +namespace Uncreated.Warfare.Migrations +{ + public partial class AddPointsTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: MySqlPointsStore.TablePoints, + columns: table => new + { + Steam64 = table.Column(name: MySqlPointsStore.ColumnPointsSteam64, nullable: false), + Faction = table.Column(name: MySqlPointsStore.ColumnPointsFaction, nullable: false), + Season = table.Column(name: MySqlPointsStore.ColumnPointsSeason, nullable: false), + XP = table.Column(name: MySqlPointsStore.ColumnPointsXP, nullable: false, defaultValue: 0d), + Credits = table.Column(name: MySqlPointsStore.ColumnPointsCredits, nullable: false, defaultValue: 0d) + }, + constraints: table => + { + table.PrimaryKey("PK_user_points", x => new { x.Steam64, x.Faction, x.Season }); + table.ForeignKey( + name: "FK_user_points_seasons_Season", + column: x => x.Season, + principalTable: "seasons", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_user_points_factions_Faction", + column: x => x.Faction, + principalTable: "factions", + principalColumn: "pk", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: MySqlPointsStore.TableReputation, + columns: table => new + { + Steam64 = table.Column(name: MySqlPointsStore.ColumnReputationSteam64, nullable: false), + Reputation = table.Column(name: MySqlPointsStore.ColumnReputationValue, nullable: false, defaultValue: 0d) + }, + constraints: table => + { + table.PrimaryKey("PK_user_reputation", x => new { x.Steam64 }); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: MySqlPointsStore.TableReputation + ); + + migrationBuilder.DropTable( + name: MySqlPointsStore.TablePoints + ); + } + } +} diff --git a/UncreatedWarfare/NewQuests/QuestRewards.cs b/UncreatedWarfare/NewQuests/QuestRewards.cs index 852b7531..1f16db34 100644 --- a/UncreatedWarfare/NewQuests/QuestRewards.cs +++ b/UncreatedWarfare/NewQuests/QuestRewards.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using System; using Uncreated.Warfare.Kits; -using Uncreated.Warfare.Levels; using Uncreated.Warfare.Players; using Uncreated.Warfare.Translations; @@ -39,7 +38,7 @@ public async UniTask GrantRewardAsync(WarfarePlayer player, QuestTracker tracker { ITranslationValueFormatter formatter = serviceProvider.GetRequiredService(); - XPParameters parameters = new XPParameters(player, player.Team, XP, tracker.Quest.Name.ToUpper() + " REWARD", false); + // todo XPParameters parameters = new XPParameters(player, player.Team, XP, tracker.Quest.Name.ToUpper() + " REWARD", false); // await Points.AwardXPAsync(parameters, token).ConfigureAwait(false); } @@ -70,7 +69,7 @@ public async UniTask GrantRewardAsync(WarfarePlayer player, QuestTracker tracker { ITranslationValueFormatter formatter = serviceProvider.GetRequiredService(); - CreditsParameters parameters = new CreditsParameters(player, player.Team, Credits, tracker.Quest.Name.ToUpper() + " REWARD"); + // todo CreditsParameters parameters = new CreditsParameters(player, player.Team, Credits, tracker.Quest.Name.ToUpper() + " REWARD"); // await Points.AwardCreditsAsync(parameters, token).ConfigureAwait(false); } diff --git a/UncreatedWarfare/Players/Components/PlayerReputationComponent.cs b/UncreatedWarfare/Players/Components/PlayerReputationComponent.cs index 8ad7f45a..05d20825 100644 --- a/UncreatedWarfare/Players/Components/PlayerReputationComponent.cs +++ b/UncreatedWarfare/Players/Components/PlayerReputationComponent.cs @@ -45,6 +45,24 @@ public void AddReputation(int reputation) } } + /// + /// Sets the reputation value for a player. + /// + /// Thread-safe + public void SetReputation(int reputation) + { + reputation = Player.UnturnedPlayer.skills.reputation - reputation; + if (GameThread.IsCurrent) + { + _pendingReputation = 0; + ModifyReputationIntl(reputation); + } + else + { + _pendingReputation = reputation; + } + } + private void ModifyReputationIntl(int deltaReputation) { Patches.CancelReputationPatch.IsSettingReputation = true; diff --git a/UncreatedWarfare/Players/Costs/CreditUnlockCost.cs b/UncreatedWarfare/Players/Costs/CreditUnlockCost.cs index 0b218139..ebb258f9 100644 --- a/UncreatedWarfare/Players/Costs/CreditUnlockCost.cs +++ b/UncreatedWarfare/Players/Costs/CreditUnlockCost.cs @@ -1,6 +1,4 @@ -using Uncreated.Warfare.Levels; - -namespace Uncreated.Warfare.Players.Costs; +namespace Uncreated.Warfare.Players.Costs; #if false /// diff --git a/UncreatedWarfare/Players/Costs/XPUnlockCost.cs b/UncreatedWarfare/Players/Costs/XPUnlockCost.cs index 5f0e3d66..8ba9042a 100644 --- a/UncreatedWarfare/Players/Costs/XPUnlockCost.cs +++ b/UncreatedWarfare/Players/Costs/XPUnlockCost.cs @@ -1,7 +1,4 @@ -using System; -using Uncreated.Warfare.Levels; - -namespace Uncreated.Warfare.Players.Costs; +namespace Uncreated.Warfare.Players.Costs; #if false /// diff --git a/UncreatedWarfare/Players/Extensions/PlayerReputationExtensions.cs b/UncreatedWarfare/Players/Extensions/PlayerReputationExtensions.cs index 7f45ae5b..85a0feb8 100644 --- a/UncreatedWarfare/Players/Extensions/PlayerReputationExtensions.cs +++ b/UncreatedWarfare/Players/Extensions/PlayerReputationExtensions.cs @@ -4,11 +4,20 @@ namespace Uncreated.Warfare.Players.Extensions; public static class PlayerReputationExtensions { /// - /// Adds (or subtracts) a certain reputation value to a player. + /// Adds (or subtracts) a certain displayed reputation value to a player. /// - /// Thread-safe + /// Thread-safe. This DOES NOT update the value in the database. public static void AddReputation(this WarfarePlayer player, int reputation) { player.Component().AddReputation(reputation); } + + /// + /// Sets the displayed reputation value for a player. + /// + /// Thread-safe. This DOES NOT update the value in the database. + public static void SetReputation(this WarfarePlayer player, int reputation) + { + player.Component().SetReputation(reputation); + } } diff --git a/UncreatedWarfare/Players/Management/IPlayerService.cs b/UncreatedWarfare/Players/Management/IPlayerService.cs index 274470ae..2247b587 100644 --- a/UncreatedWarfare/Players/Management/IPlayerService.cs +++ b/UncreatedWarfare/Players/Management/IPlayerService.cs @@ -54,4 +54,15 @@ public interface IPlayerService /// Re-allow players to join after calling . /// void ReleasePlayerConnectionLock(); + + /// + /// Quickly check if a player is online. + /// + /// + bool IsPlayerOnline(ulong steam64); + + /// + /// Quickly check if a player is online. + /// + bool IsPlayerOnlineThreadSafe(ulong steam64); } \ No newline at end of file diff --git a/UncreatedWarfare/Players/Management/NullPlayerService.cs b/UncreatedWarfare/Players/Management/NullPlayerService.cs index 31bf958c..564dfe23 100644 --- a/UncreatedWarfare/Players/Management/NullPlayerService.cs +++ b/UncreatedWarfare/Players/Management/NullPlayerService.cs @@ -49,4 +49,16 @@ Task IPlayerService.TakePlayerConnectionLock(CancellationToken token) /// void IPlayerService.ReleasePlayerConnectionLock() { } + + /// + public bool IsPlayerOnline(ulong steam64) + { + return false; + } + + /// + public bool IsPlayerOnlineThreadSafe(ulong steam64) + { + return false; + } } diff --git a/UncreatedWarfare/Players/Management/PlayerService.cs b/UncreatedWarfare/Players/Management/PlayerService.cs index 039e0204..9115e8d6 100644 --- a/UncreatedWarfare/Players/Management/PlayerService.cs +++ b/UncreatedWarfare/Players/Management/PlayerService.cs @@ -302,6 +302,23 @@ internal PlayerTaskData StartPendingPlayerTasks(PlayerPending args, Cancellation return new PlayerTaskData(args, src, playerTasks, tasks); } + /// + public bool IsPlayerOnline(ulong steam64) + { + GameThread.AssertCurrent(); + + return _onlinePlayersDictionary.ContainsPlayer(steam64); + } + + /// + public bool IsPlayerOnlineThreadSafe(ulong steam64) + { + lock (_onlinePlayersDictionary) + { + return _onlinePlayersDictionary.ContainsPlayer(steam64); + } + } + /// public WarfarePlayer GetOnlinePlayer(ulong steam64) { @@ -366,11 +383,4 @@ public PlayerOfflineException(ulong steam64) : base( { } -} - -public class PlayerComponentNotFoundException : Exception -{ - public PlayerComponentNotFoundException(Type type, WarfarePlayer player) - : base($"The component {Accessor.ExceptionFormatter.Format(type)} could not be found on player {player.Steam64.m_SteamID.ToString(CultureInfo.InvariantCulture)}.") - { } } \ No newline at end of file diff --git a/UncreatedWarfare/Players/Management/PlayerServiceExtensions.cs b/UncreatedWarfare/Players/Management/PlayerServiceExtensions.cs index c9b6dd60..b72f3615 100644 --- a/UncreatedWarfare/Players/Management/PlayerServiceExtensions.cs +++ b/UncreatedWarfare/Players/Management/PlayerServiceExtensions.cs @@ -200,6 +200,23 @@ public static WarfarePlayer GetOnlinePlayerThreadSafe(this IPlayerService player return playerService.GetOnlinePlayerOrNullThreadSafe(steamId.m_SteamID); } + /// + /// Quickly check if a player is online. + /// + /// + public static bool IsPlayerOnline(this IPlayerService playerService, CSteamID steamId) + { + return playerService.IsPlayerOnline(steamId.m_SteamID); + } + + /// + /// Quickly check if a player is online. + /// + public static bool IsPlayerOnlineThreadSafe(this IPlayerService playerService, CSteamID steamId) + { + return playerService.IsPlayerOnlineThreadSafe(steamId.m_SteamID); + } + /// /// Search for a player by their name. /// diff --git a/UncreatedWarfare/Players/PendingTasks/CheckReputationPlayerTask.cs b/UncreatedWarfare/Players/PendingTasks/CheckReputationPlayerTask.cs new file mode 100644 index 00000000..3aee1e39 --- /dev/null +++ b/UncreatedWarfare/Players/PendingTasks/CheckReputationPlayerTask.cs @@ -0,0 +1,31 @@ +using System; +using Uncreated.Warfare.Events.Models.Players; +using Uncreated.Warfare.Stats; + +namespace Uncreated.Warfare.Players.PendingTasks; +internal class CheckReputationPlayerTask(IPointsStore pointsSql) : IPlayerPendingTask +{ + private double _reputation; + public async Task RunAsync(PlayerPending e, CancellationToken token) + { + _reputation = await pointsSql.GetReputationAsync(e.Steam64, token).ConfigureAwait(false); + return true; + } + + public void Apply(WarfarePlayer player) + { + Patches.CancelReputationPatch.IsSettingReputation = true; + try + { + PlayerSkills playerSkill = player.UnturnedPlayer.skills; + + playerSkill.askRep(playerSkill.reputation - (int)Math.Round(_reputation)); + } + finally + { + Patches.CancelReputationPatch.IsSettingReputation = false; + } + } + + bool IPlayerPendingTask.CanReject => false; +} \ No newline at end of file diff --git a/UncreatedWarfare/Players/UI/TipService.cs b/UncreatedWarfare/Players/UI/TipService.cs index 8d1c2bee..463eae7f 100644 --- a/UncreatedWarfare/Players/UI/TipService.cs +++ b/UncreatedWarfare/Players/UI/TipService.cs @@ -66,7 +66,7 @@ void IEventListener.HandleEvent(PlayerLeft e, IServiceProvider servi private static void GiveTip(WarfarePlayer player, string translation) { - ToastMessage.QueueMessage(player, new ToastMessage(ToastMessageStyle.Tip, translation) { Resend = Data.PluginKeyMatch.IsMatch(translation) }); + player.SendToast(new ToastMessage(ToastMessageStyle.Tip, translation) { Resend = Data.PluginKeyMatch.IsMatch(translation) }); } } diff --git a/UncreatedWarfare/Players/UI/ToastMessage.cs b/UncreatedWarfare/Players/UI/ToastMessage.cs index a6d55e1f..46a2aeaf 100644 --- a/UncreatedWarfare/Players/UI/ToastMessage.cs +++ b/UncreatedWarfare/Players/UI/ToastMessage.cs @@ -54,35 +54,4 @@ public static ToastMessage Popup(string title, string? desc, string? btn1 = null return new ToastMessage(state, ToastMessageStyle.Popup, args!); } - public static void QueueMessage(LanguageSet set, in ToastMessage message) - { - while (set.MoveNext()) - { - set.Next.Component().Queue(in message); - } - } - public static void QueueMessage(WarfarePlayer player, in ToastMessage message) - { - player.Component().Queue(in message); - } - public static void QueueMessage(SteamPlayer player, in ToastMessage message) - { - // todo if (UCPlayer.FromSteamPlayer(player) is { } pl) - // todo pl.Toasts.Queue(in message); - } - public static void QueueMessage(Player player, in ToastMessage message) - { - // todo if (UCPlayer.FromPlayer(player) is { } pl) - // todo pl.Toasts.Queue(in message); - } - public static void QueueMessage(ulong steam64, in ToastMessage message) - { - // todo if (UCPlayer.FromID(steam64) is { } pl) - // todo pl.Toasts.Queue(in message); - } - public static void QueueMessage(CSteamID steam64, in ToastMessage message) - { - // todo if (UCPlayer.FromCSteamID(steam64) is { } pl) - // todo pl.Toasts.Queue(in message); - } } diff --git a/UncreatedWarfare/Players/UI/ToastPlayerExtensions.cs b/UncreatedWarfare/Players/UI/ToastPlayerExtensions.cs new file mode 100644 index 00000000..981a059a --- /dev/null +++ b/UncreatedWarfare/Players/UI/ToastPlayerExtensions.cs @@ -0,0 +1,24 @@ +using Uncreated.Warfare.Translations; + +namespace Uncreated.Warfare.Players.UI; +public static class ToastPlayerExtensions +{ + /// + /// Send a toast to a set of players. + /// + public static void SendToasts(this LanguageSet set, ToastMessage message) + { + while (set.MoveNext()) + { + set.Next.Component().Queue(in message); + } + } + + /// + /// Send a toast to a player. + /// + public static void SendToast(this WarfarePlayer player, ToastMessage message) + { + player.Component().Queue(in message); + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Players/WarfarePlayer.cs b/UncreatedWarfare/Players/WarfarePlayer.cs index ec1ba5b3..e71a65de 100644 --- a/UncreatedWarfare/Players/WarfarePlayer.cs +++ b/UncreatedWarfare/Players/WarfarePlayer.cs @@ -1,5 +1,4 @@ using SDG.NetTransport; -using Stripe.BillingPortal; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -9,6 +8,7 @@ using Uncreated.Warfare.Models.Localization; using Uncreated.Warfare.Players.Management; using Uncreated.Warfare.Players.Saves; +using Uncreated.Warfare.Stats; using Uncreated.Warfare.Steam.Models; using Uncreated.Warfare.Translations; using Uncreated.Warfare.Translations.Util; @@ -33,6 +33,7 @@ public class WarfarePlayer : IPlayer, ICommandUser, IComponentContainer _components; @@ -46,6 +47,7 @@ public class WarfarePlayer : IPlayer, ICommandUser, IComponentContainer ref _cachedPoints; /// /// List of steam IDs of this player's friends, if theyre public. @@ -135,7 +137,7 @@ internal WarfarePlayer(Player player, in PlayerService.PlayerTaskData taskData, /// Get the given component type from . /// /// Always returns a value or throws. - /// Component not found. + /// Component not found. [Pure] public TComponentType Component() where TComponentType : IPlayerComponent { diff --git a/UncreatedWarfare/Stats/EventHandlers/EventHandlerDestroyVehicle.cs b/UncreatedWarfare/Stats/EventHandlers/EventHandlerDestroyVehicle.cs new file mode 100644 index 00000000..e5b237dc --- /dev/null +++ b/UncreatedWarfare/Stats/EventHandlers/EventHandlerDestroyVehicle.cs @@ -0,0 +1,59 @@ +using System; +using Uncreated.Warfare.Events; +using Uncreated.Warfare.Events.Models; +using Uncreated.Warfare.Events.Models.Vehicles; +using Uncreated.Warfare.Translations; +using Uncreated.Warfare.Vehicles; + +namespace Uncreated.Warfare.Stats.EventHandlers; + +internal class EventHandlerDestroyVehicle : IAsyncEventListener +{ + private readonly PointsService _points; + private readonly VehicleInfoStore _vehicleInfo; + private readonly PointsTranslations _translations; + + public EventHandlerDestroyVehicle(PointsService points, VehicleInfoStore vehicleInfo, TranslationInjection translations) + { + _points = points; + _vehicleInfo = vehicleInfo; + _translations = translations.Value; + } + + [EventListener(Priority = int.MinValue)] + public async UniTask HandleEventAsync(VehicleExploded e, IServiceProvider serviceProvider, CancellationToken token = default) + { + WarfareVehicleInfo? vehicleInfo = _vehicleInfo.GetVehicleInfo(e.Vehicle.asset); + + if (vehicleInfo == null || vehicleInfo.Type <= 0) + return; + + uint faction = e.InstigatorTeam?.Faction.PrimaryKey ?? 0; + + if (faction == 0) + return; + + CSteamID instigator = e.InstigatorId; + + if (e.InstigatorTeam!.GroupId.m_SteamID == e.Team) + { + EventInfo @event = _points.GetEvent("DestroyFriendlyVehicle:" + vehicleInfo.Type); + + Translation translation = vehicleInfo.Type.IsAircraft() + ? _translations.XPToastAircraftDestroyed + : _translations.XPToastVehicleDestroyed; + + await _points.ApplyEvent(instigator, faction, @event.Resolve().WithTranslation(translation, e.Instigator), token).ConfigureAwait(false); + } + else + { + EventInfo @event = _points.GetEvent("DestroyEnemyVehicle:" + vehicleInfo.Type); + + Translation translation = vehicleInfo.Type.IsAircraft() + ? _translations.XPToastAircraftDestroyed + : _translations.XPToastVehicleDestroyed; + + await _points.ApplyEvent(instigator, faction, @event.Resolve().WithTranslation(translation, e.Instigator), token).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Stats/MySqlPointsStore.cs b/UncreatedWarfare/Stats/MySqlPointsStore.cs new file mode 100644 index 00000000..e3239684 --- /dev/null +++ b/UncreatedWarfare/Stats/MySqlPointsStore.cs @@ -0,0 +1,694 @@ +using System; +using Uncreated.Warfare.Database.Manual; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Extensions; +using Uncreated.Warfare.Players.Management; + +namespace Uncreated.Warfare.Stats; + +/// +/// Handles storing and loading player's points and reputation. +/// +/// Contains operations for read, add/subtract, and set, including bulk operations for both XP and credits. +public interface IPointsStore +{ + /// + /// Queries both XP and credits for a player. + /// + Task GetPointsAsync(CSteamID player, uint factionId, int season, CancellationToken token = default); + + /// + /// Queries both XP and credits for a player. + /// + Task GetPointsAsync(CSteamID player, uint factionId, CancellationToken token = default) + { + return GetPointsAsync(player, factionId, WarfareModule.Season, token); + } + + + /// + /// Queries credits for a player. + /// + async Task GetCreditsAsync(CSteamID player, uint factionId, int season, CancellationToken token = default) + { + return (await GetPointsAsync(player, factionId, season, token).ConfigureAwait(false)).Credits; + } + + /// + /// Queries credits for a player. + /// + Task GetCreditsAsync(CSteamID player, uint factionId, CancellationToken token = default) + { + return GetCreditsAsync(player, factionId, WarfareModule.Season, token); + } + + + /// + /// Queries XP for a player. + /// + async Task GetXPAsync(CSteamID player, uint factionId, int season, CancellationToken token = default) + { + return (await GetPointsAsync(player, factionId, season, token).ConfigureAwait(false)).XP; + } + + /// + /// Queries XP for a player. + /// + Task GetXPAsync(CSteamID player, uint factionId, CancellationToken token = default) + { + return GetXPAsync(player, factionId, WarfareModule.Season, token); + } + + + /// + /// Queries reputation for a player. + /// + Task GetReputationAsync(CSteamID player, CancellationToken token = default); + + + /// + /// Adds to XP and credits for a player. Can be negative. + /// + /// The resulting point count. + Task AddToPointsAsync(CSteamID player, uint factionId, int season, double deltaXp, double deltaCredits, CancellationToken token = default); + + /// + /// Adds to XP and credits for a player. Can be negative. + /// + /// The resulting point count. + Task AddToPointsAsync(CSteamID player, uint factionId, double deltaXp, double deltaCredits, CancellationToken token = default) + { + return AddToPointsAsync(player, factionId, WarfareModule.Season, deltaXp, deltaCredits, token); + } + + + /// + /// Adds to XP for a player. Can be negative. + /// + /// The resulting point count. + Task AddToXPAsync(CSteamID player, uint factionId, int season, double deltaXp, CancellationToken token = default) + { + return AddToPointsAsync(player, factionId, season, deltaXp, deltaCredits: 0d, token); + } + + /// + /// Adds to XP for a player. Can be negative. + /// + /// The resulting point count. + Task AddToXPAsync(CSteamID player, uint factionId, double deltaXp, CancellationToken token = default) + { + return AddToXPAsync(player, factionId, WarfareModule.Season, deltaXp, token); + } + + + /// + /// Adds to credits for a player. Can be negative. + /// + /// The resulting point count. + Task AddToCreditsAsync(CSteamID player, uint factionId, int season, double deltaCredits, CancellationToken token = default) + { + return AddToPointsAsync(player, factionId, season, deltaXp: 0d, deltaCredits, token); + } + + /// + /// Adds to credits for a player. Can be negative. + /// + /// The resulting point count. + Task AddToCreditsAsync(CSteamID player, uint factionId, double deltaCredits, CancellationToken token = default) + { + return AddToCreditsAsync(player, factionId, WarfareModule.Season, deltaCredits, token); + } + + /// + /// Adds to reputation for a player. Can be negative. + /// + /// The resulting reputation count. + Task AddToReputationAsync(CSteamID player, double deltaReputation, CancellationToken token = default); + + /// + /// Directly sets the credits and XP of a player. + /// + Task SetPointsAsync(CSteamID player, uint factionId, int season, double xp, double credits, CancellationToken token = default); + + /// + /// Directly sets the credits and XP of a player. + /// + Task SetPointsAsync(CSteamID player, uint factionId, double xp, double credits, CancellationToken token = default) + { + return SetPointsAsync(player, factionId, WarfareModule.Season, xp, credits, token); + } + + /// + /// Directly sets the XP of a player. + /// + Task SetXPAsync(CSteamID player, uint factionId, int season, double xp, CancellationToken token = default); + + /// + /// Directly sets the XP of a player. + /// + Task SetXPAsync(CSteamID player, uint factionId, double xp, CancellationToken token = default) + { + return SetXPAsync(player, factionId, WarfareModule.Season, xp, token); + } + + /// + /// Directly sets the credits of a player. + /// + Task SetCreditsAsync(CSteamID player, uint factionId, int season, double credits, CancellationToken token = default); + + /// + /// Directly sets the credits of a player. + /// + Task SetCreditsAsync(CSteamID player, uint factionId, double credits, CancellationToken token = default) + { + return SetCreditsAsync(player, factionId, WarfareModule.Season, credits, token); + } + + + /// + /// Directly sets the reputation of a player. + /// + Task SetReputationAsync(CSteamID player, double reputation, CancellationToken token = default); + + + /// + /// Attempts to remove an amount of XP and credits from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemovePoints(CSteamID player, uint factionId, int season, double xpToRemove, double creditsToRemove, CancellationToken token = default); + + /// + /// Attempts to remove an amount of XP and credits from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemovePoints(CSteamID player, uint factionId, double xpToRemove, double creditsToRemove, CancellationToken token = default) + { + return TryRemovePoints(player, factionId, WarfareModule.Season, xpToRemove, creditsToRemove, token); + } + + /// + /// Attempts to remove an amount of credits from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemoveCredits(CSteamID player, uint factionId, int season, double creditsToRemove, CancellationToken token = default); + + /// + /// Attempts to remove an amount of credits from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemoveCredits(CSteamID player, uint factionId, double creditsToRemove, CancellationToken token = default) + { + return TryRemoveCredits(player, factionId, WarfareModule.Season, creditsToRemove, token); + } + + /// + /// Attempts to remove an amount of XP from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemoveXP(CSteamID player, uint factionId, int season, double xpToRemove, CancellationToken token = default); + + /// + /// Attempts to remove an amount of XP from a player only if they have enough to spare. + /// + /// If the removal was successful. + Task TryRemoveXP(CSteamID player, uint factionId, double xpToRemove, CancellationToken token = default) + { + return TryRemoveXP(player, factionId, WarfareModule.Season, xpToRemove, token); + } +} + +/// +/// Handles storing and loading player's points and reputation from a MySQL database. +/// +public class MySqlPointsStore : IPointsStore +{ + private readonly IManualMySqlProvider _sql; + private readonly IPlayerService _playerService; + + // selects stats + private const string GetPointsQuery = $"SELECT `{ColumnPointsXP}`,`{ColumnPointsCredits}` FROM `{TablePoints}` WHERE " + + $"`{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 LIMIT 1;"; + + // selects credits only + private const string GetCreditsQuery = $"SELECT `{ColumnPointsCredits}` FROM `{TablePoints}` WHERE " + + $"`{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 LIMIT 1;"; + + // selects xp only + private const string GetXPQuery = $"SELECT `{ColumnPointsCredits}` FROM `{TablePoints}` WHERE " + + $"`{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 LIMIT 1;"; + + // selects reputation + private const string GetReputationQuery = $"SELECT `{ColumnReputationValue}` FROM `{TableReputation}` WHERE " + + $"`{ColumnReputationSteam64}`=@0 LIMIT 1;"; + + private const string SetPointsQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,@3,@4) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsXP}`=`new`.`{ColumnPointsXP}`," + + $"`{TablePoints}`.`{ColumnPointsCredits}`=`new`.`{ColumnPointsCredits}`;"; + + private const string SetXPQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,@3,0) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsXP}`=`new`.`{ColumnPointsXP}`;"; + + private const string SetCreditsQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,0,@3) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsCredits}`=`new`.`{ColumnPointsCredits}`;"; + + private const string SetReputationQuery = $"INSERT INTO `{TableReputation}` (`{ColumnReputationSteam64}`,`{ColumnReputationValue}`) " + + $"VALUES (@0,@1) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TableReputation}`.`{ColumnReputationValue}`=`new`.`{ColumnReputationValue}`;"; + + // adds XP and credits to the current value, clamping at 0. can be negative + private const string AddOrUpdatePointsQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,GREATEST(0,@3),GREATEST(0,@4)) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsXP}`=GREATEST(0,`{TablePoints}`.`{ColumnPointsXP}`+`new`.`{ColumnPointsXP}`)," + + $"`{TablePoints}`.`{ColumnPointsCredits}`=GREATEST(0,`{TablePoints}`.`{ColumnPointsCredits}`+`new`.`{ColumnPointsCredits}`);{GetPointsQuery}"; + + // adds XP to the current value, clamping at 0. can be negative + private const string AddOrUpdateXPQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,GREATEST(0,@3),0) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsXP}`=GREATEST(0,`{TablePoints}`.`{ColumnPointsXP}`+`new`.`{ColumnPointsXP}`);{GetPointsQuery}"; + + // adds credits to the current value, clamping at 0. can be negative + private const string AddOrUpdateCreditsQuery = $"INSERT INTO `{TablePoints}` (`{ColumnPointsSteam64}`,`{ColumnPointsFaction}`,`{ColumnPointsSeason}`,`{ColumnPointsXP}`,`{ColumnPointsCredits}`) " + + $"VALUES (@0,@1,@2,0,GREATEST(0,@3)) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TablePoints}`.`{ColumnPointsCredits}`=GREATEST(0,`{TablePoints}`.`{ColumnPointsCredits}`+`new`.`{ColumnPointsCredits}`);{GetPointsQuery}"; + + // adds reputation to the current value. can be negative + private const string AddOrUpdateReputationQuery = $"INSERT INTO `{TableReputation}` (`{ColumnReputationSteam64}`,`{ColumnReputationValue}`) " + + $"VALUES (@0,@1) AS `new` " + + $"ON DUPLICATE KEY UPDATE " + + $"`{TableReputation}`.`{ColumnReputationValue}`=`{TableReputation}`.`{ColumnReputationValue}`+`new`.`{ColumnReputationValue}`;{GetReputationQuery}"; + + // removes a certain amount of xp and credits only if theres enough of both + private const string TryRemovePointsQuery = $"UPDATE `{TablePoints} " + + $"SET `{ColumnPointsXP}` = GREATEST(0,`{ColumnPointsXP}`-@3) " + + $"SET `{ColumnPointsCredits}` = GREATEST(0,`{ColumnPointsCredits}`-@4) " + + $"WHERE `{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 AND" + + $"`{ColumnPointsXP}`>=@3 AND" + + $"`{ColumnPointsCredits}`>=@4;"; + + // removes a certain amount of xp only if theres enough + private const string TryRemoveXPQuery = $"UPDATE `{TablePoints} " + + $"SET `{ColumnPointsCredits}` = GREATEST(0,`{ColumnPointsCredits}`-@3) " + + $"WHERE `{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 AND" + + $"`{ColumnPointsCredits}`>=@3;"; + + // removes a certain amount of credits only if theres enough + private const string TryRemoveCreditsQuery = $"UPDATE `{TablePoints} " + + $"SET `{ColumnPointsCredits}` = GREATEST(0,`{ColumnPointsCredits}`-@3) " + + $"WHERE `{ColumnPointsSteam64}`=@0 AND " + + $"`{ColumnPointsFaction}`=@1 AND " + + $"`{ColumnPointsSeason}`=@2 AND" + + $"`{ColumnPointsCredits}`>=@3;"; + + public const string TablePoints = "user_points"; + public const string ColumnPointsSteam64 = "Steam64"; + public const string ColumnPointsSeason = "Season"; + public const string ColumnPointsFaction = "Faction"; + public const string ColumnPointsXP = "XP"; + public const string ColumnPointsCredits = "Credits"; + + public const string TableReputation = "user_reputation"; + public const string ColumnReputationSteam64 = "Steam64"; + public const string ColumnReputationValue = "Reputation"; + + public MySqlPointsStore(IManualMySqlProvider sql, IPlayerService playerService) + { + _sql = sql; + _playerService = playerService; + } + + + /// + public async Task GetPointsAsync(CSteamID player, uint factionId, int season, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season ]; + + PlayerPoints pts = default; + await _sql.QueryAsync(GetPointsQuery, parameters, token, + reader => + { + pts.XP = reader.GetDouble(0); + pts.Credits = reader.GetDouble(1); + pts.WasFound = true; + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, pts.XP, pts.Credits, null); + + return pts; + } + + /// + public async Task GetCreditsAsync(CSteamID player, uint factionId, int season, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season ]; + + double credits = 0d; + await _sql.QueryAsync(GetCreditsQuery, parameters, token, + reader => + { + credits = reader.GetDouble(0); + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, null, credits, null); + + return credits; + } + + /// + public async Task GetXPAsync(CSteamID player, uint factionId, int season, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season ]; + + double xp = 0d; + await _sql.QueryAsync(GetXPQuery, parameters, token, + reader => + { + xp = reader.GetDouble(0); + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, xp, null, null); + + return xp; + } + + /// + public async Task GetReputationAsync(CSteamID player, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + object[] parameters = [ player.m_SteamID ]; + + double reputation = 0d; + await _sql.QueryAsync(GetReputationQuery, parameters, token, + reader => + { + reputation = reader.GetDouble(0); + } + ).ConfigureAwait(false); + + OnPointsCacheable(player, 0, null, null, reputation); + + return reputation; + } + + /// + public async Task AddToPointsAsync(CSteamID player, uint factionId, int season, double deltaXp, double deltaCredits, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (deltaCredits == 0) + { + return await AddToXPAsync(player, factionId, season, deltaXp, token); + } + if (deltaXp == 0) + { + return await AddToCreditsAsync(player, factionId, season, deltaCredits, token); + } + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season, deltaXp, deltaCredits ]; + + PlayerPoints pts = default; + await _sql.QueryAsync(AddOrUpdatePointsQuery, parameters, token, + reader => + { + pts.XP = reader.GetDouble(0); + pts.Credits = reader.GetDouble(1); + pts.WasFound = true; + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, pts.XP, pts.Credits, null); + + return pts; + } + + /// + public async Task AddToXPAsync(CSteamID player, uint factionId, int season, double deltaXp, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season, deltaXp ]; + + PlayerPoints pts = default; + await _sql.QueryAsync(AddOrUpdateXPQuery, parameters, token, + reader => + { + pts.XP = reader.GetDouble(0); + pts.Credits = reader.GetDouble(1); + pts.WasFound = true; + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, pts.XP, pts.Credits, null); + + return pts; + } + + /// + public async Task AddToCreditsAsync(CSteamID player, uint factionId, int season, double deltaCredits, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] parameters = [ player.m_SteamID, factionId, season, deltaCredits ]; + + PlayerPoints pts = default; + await _sql.QueryAsync(AddOrUpdateCreditsQuery, parameters, token, + reader => + { + pts.XP = reader.GetDouble(0); + pts.Credits = reader.GetDouble(1); + pts.WasFound = true; + } + ).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, pts.XP, pts.Credits, null); + + return pts; + } + + /// + public async Task AddToReputationAsync(CSteamID player, double deltaReputation, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + object[] parameters = [ player.m_SteamID, deltaReputation ]; + + double rep = 0; + await _sql.QueryAsync(AddOrUpdateReputationQuery, parameters, token, + reader => + { + rep = reader.GetDouble(0); + } + ).ConfigureAwait(false); + + OnPointsCacheable(player, 0, null, null, rep); + + return rep; + } + + /// + public Task SetPointsAsync(CSteamID player, uint factionId, int season, double xp, double credits, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, xp, credits ]; + + return _sql.NonQueryAsync(SetPointsQuery, paramters, token); + } + + /// + public async Task SetXPAsync(CSteamID player, uint factionId, int season, double xp, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, xp ]; + + await _sql.NonQueryAsync(SetXPQuery, paramters, token).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, xp, null, null); + } + + /// + public async Task SetCreditsAsync(CSteamID player, uint factionId, int season, double credits, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, credits ]; + + await _sql.NonQueryAsync(SetCreditsQuery, paramters, token).ConfigureAwait(false); + + if (season == WarfareModule.Season) + OnPointsCacheable(player, factionId, null, credits, null); + } + + /// + public async Task SetReputationAsync(CSteamID player, double reputation, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + object[] paramters = [ player.m_SteamID, reputation ]; + + await _sql.NonQueryAsync(SetReputationQuery, paramters, token).ConfigureAwait(false); + + OnPointsCacheable(player, 0, null, null, reputation); + } + + /// + public async Task TryRemovePoints(CSteamID player, uint factionId, int season, double xpToRemove, double creditsToRemove, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, xpToRemove, creditsToRemove ]; + + bool success = await _sql.NonQueryAsync(TryRemovePointsQuery, paramters, token).ConfigureAwait(false) > 0; + if (!success) + return false; + + if (season == WarfareModule.Season && WarfareModule.IsActive && _playerService.IsPlayerOnlineThreadSafe(player)) + await GetPointsAsync(player, factionId, season, token).ConfigureAwait(false); + return true; + } + + /// + public async Task TryRemoveXP(CSteamID player, uint factionId, int season, double xpToRemove, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, xpToRemove ]; + + bool success = await _sql.NonQueryAsync(TryRemoveXPQuery, paramters, token).ConfigureAwait(false) > 0; + if (!success) + return false; + + if (season == WarfareModule.Season && WarfareModule.IsActive && _playerService.IsPlayerOnlineThreadSafe(player)) + await GetPointsAsync(player, factionId, season, token).ConfigureAwait(false); + return true; + } + + /// + public async Task TryRemoveCredits(CSteamID player, uint factionId, int season, double creditsToRemove, CancellationToken token = default) + { + token.ThrowIfCancellationRequested(); + + if (season < 0) + throw new ArgumentOutOfRangeException(nameof(season)); + + object[] paramters = [ player.m_SteamID, factionId, season, creditsToRemove ]; + + bool success = await _sql.NonQueryAsync(TryRemoveCreditsQuery, paramters, token).ConfigureAwait(false) > 0; + if (!success) + return false; + + if (season == WarfareModule.Season && WarfareModule.IsActive && _playerService.IsPlayerOnlineThreadSafe(player)) + await GetPointsAsync(player, factionId, season, token).ConfigureAwait(false); + return true; + } + + protected virtual void OnPointsCacheable(CSteamID player, uint factionId, double? xp, double? creds, double? rep) + { + WarfarePlayer? pl = _playerService.GetOnlinePlayerOrNullThreadSafe(player); + if (pl == null) + return; + + if (rep.HasValue) + pl.SetReputation((int)Math.Round(rep.Value)); + + if (!xp.HasValue && !creds.HasValue || pl.Team.Faction.PrimaryKey != factionId) + return; + + QueuePointsCacheChange(pl, factionId, xp, creds); + } + + private static void QueuePointsCacheChange(WarfarePlayer pl, uint factionId, double? xp, double? creds) + { + UniTask.Create(async () => + { + await UniTask.SwitchToMainThread(); + if (!pl.IsOnline || pl.Team.Faction.PrimaryKey != factionId) + return; + + if (xp.HasValue) + pl.CachedPoints.XP = xp.Value; + if (creds.HasValue) + pl.CachedPoints.Credits = creds.Value; + }); + } +} + +/// +/// Value pair of XP and credits for a player. +/// +public struct PlayerPoints +{ + public bool WasFound { get; set; } + public double XP { get; set; } + public double Credits { get; set; } +} \ No newline at end of file diff --git a/UncreatedWarfare/Stats/PointsConfiguration.cs b/UncreatedWarfare/Stats/PointsConfiguration.cs new file mode 100644 index 00000000..1aef4ebd --- /dev/null +++ b/UncreatedWarfare/Stats/PointsConfiguration.cs @@ -0,0 +1,9 @@ +using System; +using Uncreated.Warfare.Configuration; + +namespace Uncreated.Warfare.Stats; + +/// +/// Home for storing point presets. +/// +public class PointsConfiguration(IServiceProvider serviceProvider) : BaseAlternateConfigurationFile(serviceProvider, "Points.yml"); \ No newline at end of file diff --git a/UncreatedWarfare/Stats/PointsService.cs b/UncreatedWarfare/Stats/PointsService.cs new file mode 100644 index 00000000..003a06eb --- /dev/null +++ b/UncreatedWarfare/Stats/PointsService.cs @@ -0,0 +1,465 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Players.Extensions; +using Uncreated.Warfare.Players.Management; +using Uncreated.Warfare.Players.UI; +using Uncreated.Warfare.Translations; +using Uncreated.Warfare.Translations.Addons; +using Uncreated.Warfare.Translations.Util; +using Uncreated.Warfare.Vehicles; + +namespace Uncreated.Warfare.Stats; +public class PointsService +{ + private readonly PointsConfiguration _configuration; + private readonly IPointsStore _pointsSql; + private readonly IPlayerService _playerService; + private readonly IConfigurationSection _event; + private readonly PointsTranslations _translations; + private readonly WarfareRank _startingRank; + private readonly WarfareRank[] _ranks; + + private static readonly Color32 MessageColor = new Color32(173, 173, 173, 255); + + public double DefaultCredits => _configuration.GetValue("DefaultCredits"); + public double GlobalMultiplier => _configuration.GetValue("GlobalPointMultiplier", 1d); + + + /// + /// Ordered list of all configured ranks. + /// + public IReadOnlyList Ranks { get; } + + public PointsService(PointsConfiguration configuration, IPointsStore pointsSql, IPlayerService playerService, TranslationInjection translations) + { + _translations = translations.Value; + _configuration = configuration; + _pointsSql = pointsSql; + _playerService = playerService; + _event = configuration.GetSection("Events"); + IConfigurationSection levelsSection = configuration.GetSection("Levels"); + + int ct = 0; + using (IEnumerator enumerator = levelsSection.GetChildren().GetEnumerator()) + { + if (!enumerator.MoveNext()) + throw new InvalidOperationException("There must be at least one level defined in config."); + + // the value of Next is initialized in this constructor and it creates a linked list kind of structure of ranks + _startingRank = new WarfareRank(null, enumerator, 0, ref ct); + } + + _ranks = new WarfareRank[ct]; + + ct = -1; + for (WarfareRank? rank = _startingRank; rank != null; rank = rank.Next) + { + _ranks[++ct] = rank; + } + + Ranks = new ReadOnlyCollection(_ranks); + } + + /// + /// Find rank info from a one-based level. + /// + /// Thrown if a value less than or equal to 0 is inputted for . + public WarfareRank? GetRankFromLevel(int level) + { + --level; + + if (level < 0) + throw new ArgumentOutOfRangeException(nameof(level)); + + return level >= _ranks.Length ? null : _ranks[level]; + } + + /// + /// Find rank info from an experience value. + /// + public WarfareRank GetRankFromExperience(double experience) + { + if (experience < 0) + return _startingRank; + + for (WarfareRank? rank = _startingRank; rank != null; rank = rank.Next) + { + if (rank.CumulativeExperience <= experience) + return rank; + } + + return _ranks[^1]; + } + + /// + /// Find rank info from its name, then its abbreviation. + /// + public WarfareRank? GetRankByName(string name) + { + for (WarfareRank? rank = _startingRank; rank != null; rank = rank.Next) + { + if (rank.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + return rank; + } + + for (WarfareRank? rank = _startingRank; rank != null; rank = rank.Next) + { + if (rank.Name.Replace(".", string.Empty).Equals(name.Replace(".", string.Empty), StringComparison.InvariantCultureIgnoreCase)) + return rank; + } + + return null; + } + + /// + /// Get an event from settings. + /// + public EventInfo GetEvent(string eventId) + { + return new EventInfo(_event.GetSection(eventId)); + } + + public Task ApplyEvent(CSteamID playerId, uint factionId, ResolvedEventInfo @event, CancellationToken token = default) + { + return ApplyEvent(playerId, factionId, WarfareModule.Season, @event, token); + } + + public async Task ApplyEvent(CSteamID playerId, uint factionId, int season, ResolvedEventInfo @event, CancellationToken token = default) + { + await UniTask.SwitchToMainThread(token); + + bool hideToast = @event.HideToast || @event.Message == null; + + double xp = @event.XP; + double rep = @event.Reputation; + double credits = @event.Credits; + + if (!@event.IgnoresGlobalMultiplier) + { + double mod = GlobalMultiplier; + xp *= mod; + credits *= mod; + } + + // todo XP boosts + + // only for display it's fine to use cached points + PlayerPoints oldPoints; + WarfarePlayer? player = _playerService.GetOnlinePlayerOrNull(playerId); + if (player is { CachedPoints.WasFound: true }) + { + oldPoints = player.CachedPoints; + } + else + { + oldPoints = await _pointsSql.GetPointsAsync(playerId, factionId, season, token).ConfigureAwait(false); + } + + + PlayerPoints newPoints = await _pointsSql.AddToPointsAsync(playerId, factionId, season, xp, credits, token).ConfigureAwait(false); + + await _pointsSql.AddToReputationAsync(playerId, rep, token).ConfigureAwait(false); + + await UniTask.SwitchToMainThread(token); + + player?.AddReputation((int)Math.Round(rep)); + if (player is { IsOnline: true } && !hideToast) + { + // XP toast + if (xp != 0) + { + string numberTxt = (xp > 0 ? _translations.XPToastGainXP : _translations.XPToastLoseXP).Translate(Math.Abs(xp), player); + + string text = !string.IsNullOrWhiteSpace(@event.Message) + ? numberTxt + "\n" + TranslationFormattingUtility.Colorize(@event.Message, MessageColor) + : numberTxt; + + player.SendToast(new ToastMessage(ToastMessageStyle.Mini, text)); + } + + // credits toast + if (credits != 0) + { + string numberTxt = (credits > 0 ? _translations.XPToastGainCredits : _translations.XPToastLoseCredits).Translate(Math.Abs(credits), player); + + string text = !string.IsNullOrWhiteSpace(@event.Message) + ? numberTxt + "\n" + TranslationFormattingUtility.Colorize(@event.Message, MessageColor) + : numberTxt; + + player.SendToast(new ToastMessage(ToastMessageStyle.Mini, text)); + } + } + } +} + +public class ResolvedEventInfo +{ + public double XP { get; } + public double Credits { get; } + public double Reputation { get; } + public bool HideToast { get; } + public bool ExcludeFromLeaderboard { get; } + public bool IgnoresBoosts { get; } + public bool IgnoresGlobalMultiplier { get; } + public string? Message { get; set; } + public ResolvedEventInfo(EventInfo @event) : this(@event, null, null, null) { } + public ResolvedEventInfo(EventInfo @event, double? overrideXp, double? overrideCredits, double? overrideReputation) + { + XP = overrideXp ?? @event.XP; + Credits = overrideCredits ?? @event.Credits; + Reputation = overrideReputation ?? @event.Reputation; + + HideToast = @event.Configuration.GetValue("HideToast", false); + ExcludeFromLeaderboard = @event.Configuration.GetValue("ExcludeFromLeaderboard", false); + IgnoresBoosts = @event.Configuration.GetValue("IgnoresBoosts", false); + IgnoresGlobalMultiplier = @event.Configuration.GetValue("IgnoresGlobalMultiplier", false); + } + + public ResolvedEventInfo WithTranslation(Translation translation, WarfarePlayer? player) + { + Message = player == null ? translation.Translate() : translation.Translate(player); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, in LanguageSet set) + { + Message = translation.Translate(in set); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, WarfarePlayer? player) + { + Message = player == null ? translation.Translate(arg0) : translation.Translate(arg0, player); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, in LanguageSet set) + { + Message = translation.Translate(arg0, in set); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, T1 arg1, WarfarePlayer? player) + { + Message = player == null ? translation.Translate(arg0, arg1) : translation.Translate(arg0, arg1, player); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, T1 arg1, in LanguageSet set) + { + Message = translation.Translate(arg0, arg1, in set); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, T1 arg1, T2 arg2, WarfarePlayer? player) + { + Message = player == null ? translation.Translate(arg0, arg1, arg2) : translation.Translate(arg0, arg1, arg2, player); + return this; + } + + public ResolvedEventInfo WithTranslation(Translation translation, T0 arg0, T1 arg1, T2 arg2, in LanguageSet set) + { + Message = translation.Translate(arg0, arg1, arg2, in set); + return this; + } +} + +public readonly struct EventInfo +{ + public IConfigurationSection Configuration { get; } + public double XP => Configuration.GetValue("XP", 0d); + public double Credits => ParsePercentageOrValueOfXP("Credits", 0.15); + public double Reputation => ParsePercentageOrValueOfXP("Reputation", 0); + public EventInfo(IConfigurationSection configuration) + { + Configuration = configuration; + } + + private double ParsePercentageOrValueOfXP(string key, double defaultPercentage) + { + string? valueStr = Configuration[key]; + if (string.IsNullOrWhiteSpace(valueStr)) + { + return XP * defaultPercentage; + } + + ReadOnlySpan span = valueStr.Trim(); + bool percent = span.Length != 0 && span[^1] == '%'; + + double value = double.Parse(percent ? span[..^1] : span, NumberStyles.Number, CultureInfo.InvariantCulture); + return percent ? value / 100 * XP : value; + } + + + public ResolvedEventInfo Resolve() => new ResolvedEventInfo(this); + + public static implicit operator ResolvedEventInfo(EventInfo @event) + { + return @event.Resolve(); + } +} + +public class PointsTranslations : PropertiesTranslationCollection +{ + protected override string FileName => "Points"; + + [TranslationData(IsPriorityTranslation = false)] + public readonly Translation XPToastGainXP = new Translation("+{0} XP", TranslationOptions.TMProUI, "F0"); + + [TranslationData(IsPriorityTranslation = false)] + public readonly Translation XPToastLoseXP = new Translation("-{0} XP", TranslationOptions.TMProUI, "F0"); + + [TranslationData(IsPriorityTranslation = false)] + public readonly Translation XPToastGainCredits = new Translation("+{0} C", TranslationOptions.TMProUI, "F0"); + + [TranslationData(IsPriorityTranslation = false)] + public readonly Translation XPToastPurchaseCredits = new Translation("-{0} C", TranslationOptions.TMProUI, "F0"); + + [TranslationData(IsPriorityTranslation = false)] + public readonly Translation XPToastLoseCredits = new Translation("-{0} C", TranslationOptions.TMProUI, "F0"); + + [TranslationData("Sent to a player when they move up to the next level.")] + public readonly Translation ToastPromoted = new Translation("YOU HAVE BEEN PROMOTED TO", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player when they move down to the previous level.")] + public readonly Translation ToastDemoted = new Translation("YOU HAVE BEEN DEMOTED TO", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup when XP or credits given from the console.")] + public Translation XPToastFromOperator = new Translation("FROM OPERATOR", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup when XP or credits are given to them by an admin.")] + public Translation XPToastFromPlayer = new Translation("FROM ADMIN", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they heal their teammate.")] + public Translation XPToastHealedTeammate = new Translation("HEALED TEAMMATE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they injure an enemy.")] + public Translation XPToastEnemyInjured = new Translation("DOWNED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they injure their teammate.")] + public Translation XPToastFriendlyInjured = new Translation("DOWNED FRIENDLY", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they kill an enemy.")] + public Translation XPToastEnemyKilled = new Translation("KILLED ENEMY", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they damage an enemy that was later killed by someone else.")] + public Translation XPToastKillAssist = new Translation("ASSIST", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they damage a vehicle that was later destroyed by someone else.")] + public Translation XPToastKillVehicleAssist = new Translation("VEHICLE ASSIST", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they drive a vehicle with a gunner that killed an enemy.")] + public Translation XPToastKillDriverAssist = new Translation("DRIVER ASSIST", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they spot enemy forces.")] + public Translation XPToastSpotterAssist = new Translation("SPOTTER", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they kill a friendly.")] + public Translation XPToastFriendlyKilled = new Translation("TEAMKILLED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they kill themselves.")] + public Translation XPToastSuicide = new Translation("SUICIDE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a FOB.")] + public Translation XPToastFOBDestroyed = new Translation("FOB DESTROYED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a friendly FOB.")] + public Translation XPToastFriendlyFOBDestroyed = new Translation("FRIENDLY FOB DESTROYED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a bunker.")] + public Translation XPToastBunkerDestroyed = new Translation("BUNKER DESTROYED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a friendly bunker.")] + public Translation XPToastFriendlyBunkerDestroyed = new Translation("FRIENDLY BUNKER DESTROYED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after someone spawns in a FOB or bunker they placed.")] + public Translation XPToastFOBUsed = new Translation("FOB IN USE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they help resupply a FOB with building or ammo supplies.")] + public Translation XPToastSuppliesUnloaded = new Translation("RESUPPLIED FOB", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after a teammate retreives ammo from an ammo crate or ammo bag they placed.")] + public Translation XPToastResuppliedTeammate = new Translation("RESUPPLIED TEAMMATE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after a repair station they placed repairs a vehicle.")] + public Translation XPToastRepairedVehicle = new Translation("REPAIRED VEHICLE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after a FOB they placed helps repair a vehicle.")] + public Translation XPToastFOBRepairedVehicle = new Translation("FOB REPAIRED VEHICLE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a ground vehicle.")] + public Translation XPToastVehicleDestroyed = new Translation("{0} DESTROYED", TranslationOptions.TMProUI, UppercaseAddon.Instance); + + [TranslationData("Sent to a player on the points popup after they destroy an air vehicle.")] + public Translation XPToastAircraftDestroyed = new Translation("{0} SHOT DOWN", TranslationOptions.TMProUI, UppercaseAddon.Instance); + + [TranslationData("Sent to a player on the points popup after they destroy a friendly ground vehicle.")] + public Translation XPToastFriendlyVehicleDestroyed = new Translation("FRIENDLY {0} DESTROYED", TranslationOptions.TMProUI, UppercaseAddon.Instance); + + [TranslationData("Sent to a player on the points popup after they destroy a friendly air vehicle.")] + public Translation XPToastFriendlyAircraftDestroyed = new Translation("FRIENDLY {0} SHOT DOWN", TranslationOptions.TMProUI, UppercaseAddon.Instance); + + [TranslationData("Sent to a player on the points popup after they help drive other players.")] + public Translation XPToastTransportingPlayers = new Translation("TRANSPORTING PLAYERS", TranslationOptions.TMProUI); + + // todo description + [TranslationData] + public Translation XPToastAceArmorRefund = new Translation("ACE ARMOR SHARE", TranslationOptions.TMProUI); + + + [TranslationData("Sent to a player on the points popup after they help capture a flag.")] + public Translation XPToastFlagCaptured = new Translation("FLAG CAPTURED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they help take a flag from the other team.")] + public Translation XPToastFlagNeutralized = new Translation("FLAG NEUTRALIZED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup while they're capturing or neutralizing a flag owned by the other team.")] + public Translation XPToastFlagAttackTick = new Translation("ATTACK", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup while they're defending a flag they own.")] + public Translation XPToastFlagDefenseTick = new Translation("DEFENSE", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy an enemy cache in Insurgency.")] + public Translation XPToastCacheDestroyed = new Translation("CACHE DESTROYED", TranslationOptions.TMProUI); + + [TranslationData("Sent to a player on the points popup after they destroy a friendly cache in Insurgency.")] + public Translation XPToastFriendlyCacheDestroyed = new Translation("FRIENDLY CACHE DESTROYED", TranslationOptions.TMProUI); + + + [TranslationData("Sent to a player on the points popup after they get XP from a squad bonus.")] + public Translation XPToastSquadBonus = new Translation("SQUAD BONUS", TranslationOptions.TMProUI); + + [TranslationData(IsPriorityTranslation = false)] + public Translation XPToastOnDuty = new Translation("ON DUTY", TranslationOptions.TMProUI); + + + [TranslationData(IsPriorityTranslation = false)] + public Translation FOBToastGainBuild = new Translation("+{0} BUILD", TranslationOptions.TMProUI); + + [TranslationData(IsPriorityTranslation = false)] + public Translation FOBToastLoseBuild = new Translation("-{0} BUILD", TranslationOptions.TMProUI); + + [TranslationData(IsPriorityTranslation = false)] + public Translation FOBToastGainAmmo = new Translation("+{0} AMMO", TranslationOptions.TMProUI); + + [TranslationData(IsPriorityTranslation = false)] + public Translation FOBToastLoseAmmo = new Translation("-{0} AMMO", TranslationOptions.TMProUI); + + + [TranslationData("Hint to tell a player to load supplies into the vehicle.")] + public Translation FOBResourceToastLoadSupplies = new Translation("LOAD SUPPLIES"); + + [TranslationData("Hint to tell a player to rearm a vehicle.")] + public Translation FOBResourceToastRearmVehicle = new Translation("REARM VEHICLE"); + + [TranslationData("Sent to a player on the points popup after they rearm a vehicle.")] + public Translation FOBResourceToastRearmPlayer = new Translation("REARM"); + + [TranslationData("Sent to a player on the points popup after they repair a vehicle.")] + public Translation FOBResourceToastRepairVehicle = new Translation("REPAIR"); + +} \ No newline at end of file diff --git a/UncreatedWarfare/Stats/PointsUI.cs b/UncreatedWarfare/Stats/PointsUI.cs new file mode 100644 index 00000000..815df2b0 --- /dev/null +++ b/UncreatedWarfare/Stats/PointsUI.cs @@ -0,0 +1,149 @@ +using System; +using System.Globalization; +using Uncreated.Framework.UI; +using Uncreated.Framework.UI.Data; +using Uncreated.Framework.UI.Reflection; +using Uncreated.Warfare.Configuration; +using Uncreated.Warfare.Interaction.UI; +using Uncreated.Warfare.Players; +using Uncreated.Warfare.Translations.Util; +using Uncreated.Warfare.Util; + +namespace Uncreated.Warfare.Stats; + +[UnturnedUI(BasePath = "Container/Box")] +public class PointsUI : UnturnedUI +{ + private static readonly UnturnedUIElement[] PositionElements = + [ + new UnturnedUIElement("LogicPositionBase"), + new UnturnedUIElement("LogicPositionGun"), + new UnturnedUIElement("LogicPositionVehicle"), + new UnturnedUIElement("LogicPositionGunAndVehicle") + ]; + + private readonly PointsConfiguration _config; + private readonly PointsService _points; + private readonly ImageProgressBar _xpBar = new ImageProgressBar("XpProgress") { NeedsToSetLabel = false }; + private readonly UnturnedLabel _lblCurrentRank = new UnturnedLabel("LabelCurrentRank"); + private readonly UnturnedLabel _lblNextRank = new UnturnedLabel("LabelNextRank"); + private readonly UnturnedLabel _lblCredits = new UnturnedLabel("LabelCredits"); + private readonly UnturnedLabel _lblUsername = new UnturnedLabel("LabelUsername"); + private readonly UnturnedLabel _lblStatistic = new UnturnedLabel("LabelStatistic"); + + public PointsUI(PointsConfiguration config, PointsService points, AssetConfiguration assetConfig, ILoggerFactory loggerFactory) + : base(loggerFactory, assetConfig.GetAssetLink("UI:Points")) + { + _config = config; + _points = points; + } + + private PointsUIData GetUIData(CSteamID steam64) + { + return GetOrAddData(steam64, (steam64) => new PointsUIData(steam64, this)); + } + + private static int GetPositionLogicIndex(WarfarePlayer player) + { + InteractableVehicle? vehicle = player.UnturnedPlayer.movement.getVehicle(); + + bool vehicleBoxVisible = vehicle != null && + vehicle is { isDead: false, isUnderwater: false } && + player.UnturnedPlayer.movement.getSeat() == 0 && + player.UnturnedPlayer.isPluginWidgetFlagActive(EPluginWidgetFlags.ShowVehicleStatus); + + bool gunBoxVisible = player.UnturnedPlayer.equipment.useable is UseableGun; + + return (gunBoxVisible ? 1 : 0) + (vehicleBoxVisible ? 1 : 0) * 2; + } + + /// + /// Updates all elements on the points UI if they need to be updated. + /// + /// The UI will be cleared if the player is not on a team. + public void UpdatePointsUI(WarfarePlayer player) + { + GameThread.AssertCurrent(); + + PointsUIData data = GetUIData(player.Steam64); + + if (!player.Team.IsValid) + { + if (!data.HasUI) + return; + + data.HasUI = false; + ClearFromPlayer(player.Connection); + return; + } + + bool wasJustSent = false; + if (!data.HasUI) + { + wasJustSent = true; + SendToPlayer(player.Connection); + data.HasUI = true; + data.LastExperienceValue = -1; + data.LastCreditsValue = -1; + data.LastRank = -1; + data.Position = 0; + } + + WarfareRank rank = _points.GetRankFromExperience(player.CachedPoints.XP); + + int displayedXp = (int)Math.Round(player.CachedPoints.XP); + int displayedPartialXp = (int)Math.Round(player.CachedPoints.XP - rank.CumulativeExperience); + + if (data.LastExperienceValue != displayedXp) + { + data.LastExperienceValue = displayedPartialXp; + _xpBar.SetProgress(player.Connection, displayedXp); + _xpBar.Label.SetText(player.Connection, displayedPartialXp.ToString(player.Locale.CultureInfo) + "/" + rank.Experience.ToString(player.Locale.CultureInfo)); + } + + if (data.LastRank != rank.RankIndex) + { + data.LastRank = rank.RankIndex; + _lblCurrentRank.SetText(player.Connection, rank.Name); // todo icons + _lblNextRank.SetText(player.Connection, rank.Next?.Name ?? string.Empty); + } + + int displayedCredits = (int)Math.Round(player.CachedPoints.Credits); + if (data.LastCreditsValue != displayedCredits) + { + data.LastCreditsValue = displayedCredits; + HexStringHelper.TryParseColor32(_config["CreditsColor"], CultureInfo.InvariantCulture, out Color32 color); + + string creditString = "<#" + HexStringHelper.FormatHexColor(color) + ">C " + displayedCredits.ToString(CultureInfo.InvariantCulture); + + _lblCredits.SetText(player.Connection, creditString); + } + + int expectedPosition = GetPositionLogicIndex(player); + if (data.Position != expectedPosition) + { + data.Position = expectedPosition; + PositionElements[expectedPosition].Show(player); + } + + if (!wasJustSent) + return; + + _lblUsername.SetText(player.Connection, TranslationFormattingUtility.Colorize(player.Names.GetDisplayNameOrCharacterName(), player.Team.Faction.Color)); + _lblStatistic.SetText(player.Connection, "Score: 0"); // todo + } + + private class PointsUIData(CSteamID steam64, PointsUI ui) : IUnturnedUIData + { + public CSteamID Player => steam64; + public UnturnedUI Owner => ui; + + public bool HasUI { get; set; } + public int LastExperienceValue { get; set; } + public int LastCreditsValue { get; set; } + public int LastRank { get; set; } + public int Position { get; set; } + + UnturnedUIElement? IUnturnedUIData.Element => null; + } +} \ No newline at end of file diff --git a/UncreatedWarfare/Stats/StatsCoroutine.cs b/UncreatedWarfare/Stats/StatsCoroutine.cs index 4123aaf8..a4ca2612 100644 --- a/UncreatedWarfare/Stats/StatsCoroutine.cs +++ b/UncreatedWarfare/Stats/StatsCoroutine.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using Uncreated.Warfare.Interaction; -using Uncreated.Warfare.Levels; -using Uncreated.Warfare.Logging; -using Uncreated.Warfare.Players.Management.Legacy; +using System.Collections.Generic; namespace Uncreated.Warfare.Stats; diff --git a/UncreatedWarfare/Stats/WarfareRank.cs b/UncreatedWarfare/Stats/WarfareRank.cs new file mode 100644 index 00000000..59e0e2cf --- /dev/null +++ b/UncreatedWarfare/Stats/WarfareRank.cs @@ -0,0 +1,115 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using Uncreated.Warfare.Translations; +using Uncreated.Warfare.Translations.Util; +using Uncreated.Warfare.Translations.ValueFormatters; + +namespace Uncreated.Warfare.Stats; +public class WarfareRank : ITranslationArgument +{ + /// + /// The name of the rank. + /// + public string Name { get; } + + /// + /// An abbreviation for the name of the rank. + /// + public string Abbreviation { get; } + + /// + /// The experience to get from the this rank to the next rank. + /// + public double Experience { get; } + + /// + /// The total experience to get to this rank. + /// + public double CumulativeExperience { get; } + + /// + /// The zero-based index of this rank. + /// + public int RankIndex { get; } + + /// + /// The one-based level/index of this rank. + /// + public int Level { get; } + + /// + /// A reference to the next rank. + /// + public WarfareRank? Next { get; } + + /// + /// A reference to the previous rank. + /// + public WarfareRank? Previous { get; } + + internal WarfareRank(WarfareRank? previous, IEnumerator configEnumerator, int index, ref int ct) + { + ++ct; + IConfigurationSection section = configEnumerator.Current!; + + RankIndex = index; + Level = index + 1; + Name = section.GetValue("Name") ?? throw new InvalidOperationException($"Rank {index} missing Name."); + Abbreviation = section.GetValue("Abbreviation") ?? throw new InvalidOperationException($"Rank {index} missing Abbreviation."); + Experience = section.GetValue("Experience", 0d); + if (Experience < 0d) + { + throw new InvalidOperationException($"Rank {index} has invalid Experience."); + } + + Previous = previous; + Next = configEnumerator.MoveNext() ? new WarfareRank(this, configEnumerator, index, ref ct) : null; + + CumulativeExperience = Previous != null ? Previous.CumulativeExperience + Previous.Experience : 0; + if (Experience == 0d && Next != null) + { + throw new InvalidOperationException($"Rank {index} has missing or invalid Experience."); + } + } + + /// + /// Calculates the progress that a player with a certain amount of total would be to getting to the next rank. + /// + /// An unclamped percentage on the [0, 1] scale. + public double GetProgress(double xp) + { + return (xp - CumulativeExperience) / Experience; + } + + + public static readonly SpecialFormat FormatNumeric = new SpecialFormat("Numeric", "x"); + + public static readonly SpecialFormat FormatLPrefixedNumeric = new SpecialFormat("L-Prefixed Numeric", "Lx"); + + public static readonly SpecialFormat FormatAbbreviation = new SpecialFormat("Abbreviation", "a"); + + public static readonly SpecialFormat FormatName = new SpecialFormat("Name", "n"); + + + /// + public string Translate(ITranslationValueFormatter formatter, in ValueFormatParameters parameters) + { + if (FormatNumeric.Match(in parameters)) + return Level.ToString(parameters.Culture); + + if (FormatLPrefixedNumeric.Match(in parameters)) + return "L " + Level.ToString(parameters.Culture); + + if (FormatAbbreviation.Match(in parameters)) + return Abbreviation; + + return Name; + } + + /// + public override string ToString() + { + return $"L{Level} - {Name} ({Abbreviation})"; + } +} diff --git a/UncreatedWarfare/Translations/DefaultTranslations.cs b/UncreatedWarfare/Translations/DefaultTranslations.cs index 03c5f4ee..e7398925 100644 --- a/UncreatedWarfare/Translations/DefaultTranslations.cs +++ b/UncreatedWarfare/Translations/DefaultTranslations.cs @@ -1342,115 +1342,6 @@ internal static class T public static readonly Translation PhaseBreifingInvasionDefense = new Translation("PREPARATION PHASE\nFORTIFY {0}", TranslationOptions.TMProUI, Flags.ColorShortNameFormat); #endregion - #region XP Toasts - private const string SectionXPCreditsToast = "XP / Credit / Supply Toast Messages"; - [TranslationData(SectionXPCreditsToast, "XP or credits given from the console.")] - public static readonly Translation XPToastFromOperator = new Translation("FROM OPERATOR", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, "XP or credits given by an admin.")] - public static readonly Translation XPToastFromPlayer = new Translation("FROM ADMIN", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastHealedTeammate = new Translation("HEALED TEAMMATE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastEnemyInjured = new Translation("DOWNED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyInjured = new Translation("DOWNED FRIENDLY", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastEnemyKilled = new Translation("KILLED ENEMY", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastKillAssist = new Translation("ASSIST", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastKillVehicleAssist = new Translation("VEHICLE ASSIST", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastKillDriverAssist = new Translation("DRIVER ASSIST", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastSpotterAssist = new Translation("SPOTTER", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyKilled = new Translation("TEAMKILLED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastSuicide = new Translation("SUICIDE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFOBDestroyed = new Translation("FOB DESTROYED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyFOBDestroyed = new Translation("FRIENDLY FOB DESTROYED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastBunkerDestroyed = new Translation("BUNKER DESTROYED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyBunkerDestroyed = new Translation("FRIENDLY BUNKER DESTROYED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFOBUsed = new Translation("FOB IN USE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastSuppliesUnloaded = new Translation("RESUPPLIED FOB", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastResuppliedTeammate = new Translation("RESUPPLIED TEAMMATE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastRepairedVehicle = new Translation("REPAIRED VEHICLE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFOBRepairedVehicle = new Translation("FOB REPAIRED VEHICLE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastVehicleDestroyed = new Translation("{0} DESTROYED", TranslationOptions.TMProUI, UppercaseAddon.Instance); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastAircraftDestroyed = new Translation("{0} SHOT DOWN", TranslationOptions.TMProUI, UppercaseAddon.Instance); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyVehicleDestroyed = new Translation("FRIENDLY {0} DESTROYED", TranslationOptions.TMProUI, UppercaseAddon.Instance); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyAircraftDestroyed = new Translation("FRIENDLY {0} SHOT DOWN", TranslationOptions.TMProUI, UppercaseAddon.Instance); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastTransportingPlayers = new Translation("TRANSPORTING PLAYERS", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastAceArmorRefund = new Translation("ACE ARMOR SHARE", TranslationOptions.TMProUI); - - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFlagCaptured = new Translation("FLAG CAPTURED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFlagNeutralized = new Translation("FLAG NEUTRALIZED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFlagAttackTick = new Translation("ATTACK", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFlagDefenseTick = new Translation("DEFENSE", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastCacheDestroyed = new Translation("CACHE DESTROYED", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastFriendlyCacheDestroyed = new Translation("FRIENDLY CACHE DESTROYED", TranslationOptions.TMProUI); - - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastSquadBonus = new Translation("SQUAD BONUS", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation XPToastOnDuty = new Translation("ON DUTY", TranslationOptions.TMProUI); - - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation FOBToastGainBuild = new Translation("+{0} BUILD", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation FOBToastLoseBuild = new Translation("-{0} BUILD", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation FOBToastGainAmmo = new Translation("+{0} AMMO", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation FOBToastLoseAmmo = new Translation("-{0} AMMO", TranslationOptions.TMProUI); - - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation FOBResourceToastLoadSupplies = new Translation("LOAD SUPPLIES"); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation FOBResourceToastRearmVehicle = new Translation("REARM VEHICLE"); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation FOBResourceToastRearmPlayer = new Translation("REARM"); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation FOBResourceToastRepairVehicle = new Translation("REPAIR"); - - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation XPToastGainXP = new Translation("+{0} XP", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation XPToastLoseXP = new Translation("-{0} XP", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation XPToastGainCredits = new Translation("+{0} C", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation XPToastPurchaseCredits = new Translation("-{0} C", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast, IsPriorityTranslation = false)] - public static readonly Translation XPToastLoseCredits = new Translation("-{0} C", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation ToastPromoted = new Translation("YOU HAVE BEEN PROMOTED TO", TranslationOptions.TMProUI); - [TranslationData(SectionXPCreditsToast)] - public static readonly Translation ToastDemoted = new Translation("YOU HAVE BEEN DEMOTED TO", TranslationOptions.TMProUI); - #endregion - #region Injured UI [TranslationData(SectionRevives)] public static readonly Translation InjuredUIHeader = new Translation("You are injured", TranslationOptions.TMProUI); diff --git a/UncreatedWarfare/Util/Containers/IComponentContainer.cs b/UncreatedWarfare/Util/Containers/IComponentContainer.cs index 08f64c13..a9dcb03b 100644 --- a/UncreatedWarfare/Util/Containers/IComponentContainer.cs +++ b/UncreatedWarfare/Util/Containers/IComponentContainer.cs @@ -1,23 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Uncreated.Warfare.Players; +using Uncreated.Warfare.Util.List; namespace Uncreated.Warfare.Util.Containers; + /// /// A container for neatly separating and storing sub components of type . /// -public interface IComponentContainer +public interface IComponentContainer where TComponentType : notnull { /// - /// Get the given component type from . + /// Get the given component type from a list of components. /// + /// Component not found. /// Always returns a value or throws an exception. [Pure] public T Component() where T : TComponentType; + /// - /// Get the given component type from . + /// Get the given component type from a list of components. /// [Pure] public T? ComponentOrNull() where T : TComponentType; -} +} \ No newline at end of file diff --git a/UncreatedWarfare/Util/List/SingleUseTypeDictionary.cs b/UncreatedWarfare/Util/List/SingleUseTypeDictionary.cs index ed9531c4..7e0db888 100644 --- a/UncreatedWarfare/Util/List/SingleUseTypeDictionary.cs +++ b/UncreatedWarfare/Util/List/SingleUseTypeDictionary.cs @@ -1,7 +1,5 @@ using DanielWillett.ReflectionTools; -using Stripe; using System; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -69,7 +67,17 @@ private static class IndexCache public class ComponentNotFoundException : Exception { + public Type Type { get; } + public ComponentNotFoundException(Type type, object context) : base($"The component {Accessor.ExceptionFormatter.Format(type)} could not be found on {context}.") - { } + { + Type = type; + } + + public ComponentNotFoundException(Type type, string message) + : base(message) + { + Type = type; + } } \ No newline at end of file diff --git a/UncreatedWarfare/Vehicles/AbandonService.cs b/UncreatedWarfare/Vehicles/AbandonService.cs index 393585ec..f38d388e 100644 --- a/UncreatedWarfare/Vehicles/AbandonService.cs +++ b/UncreatedWarfare/Vehicles/AbandonService.cs @@ -105,7 +105,7 @@ public async UniTask AbandonVehicle(InteractableVehicle vehicle, bool resp } else if (owner != null) { - ToastMessage.QueueMessage(owner, new ToastMessage(ToastMessageStyle.Mini, _formatter.Colorize(_translations.AbandonCompensationToastTransferred.Translate(owner), new Color32(173, 173, 173, 255), TranslationOptions.TMProUI))); + owner.SendToast(new ToastMessage(ToastMessageStyle.Mini, _formatter.Colorize(_translations.AbandonCompensationToastTransferred.Translate(owner), new Color32(173, 173, 173, 255), TranslationOptions.TMProUI))); } await _vehicleService.DeleteVehicleAsync(vehicle, token); diff --git a/UncreatedWarfare/WarfareModule.cs b/UncreatedWarfare/WarfareModule.cs index 78c4db5f..143300af 100644 --- a/UncreatedWarfare/WarfareModule.cs +++ b/UncreatedWarfare/WarfareModule.cs @@ -28,14 +28,12 @@ using Uncreated.Warfare.Events.ListenerProviders; using Uncreated.Warfare.Fobs; using Uncreated.Warfare.FOBs.Deployment; -using Uncreated.Warfare.FOBs.UI; using Uncreated.Warfare.Interaction; using Uncreated.Warfare.Interaction.Commands; using Uncreated.Warfare.Kits; using Uncreated.Warfare.Kits.Whitelists; using Uncreated.Warfare.Layouts; using Uncreated.Warfare.Layouts.UI; -using Uncreated.Warfare.Levels; using Uncreated.Warfare.Lobby; using Uncreated.Warfare.Logging; using Uncreated.Warfare.Maps; @@ -52,6 +50,8 @@ using Uncreated.Warfare.Signs; using Uncreated.Warfare.Squads; using Uncreated.Warfare.Squads.UI; +using Uncreated.Warfare.Stats; +using Uncreated.Warfare.Stats.EventHandlers; using Uncreated.Warfare.Steam; using Uncreated.Warfare.StrategyMaps; using Uncreated.Warfare.Teams; @@ -422,8 +422,7 @@ private void ConfigureServices(ContainerBuilder bldr) bldr.RegisterType().SingleInstance(); bldr.RegisterType().SingleInstance(); bldr.RegisterType().SingleInstance(); - bldr.RegisterType().SingleInstance(); - bldr.RegisterType().SingleInstance(); + bldr.RegisterType().SingleInstance(); bldr.RegisterType().SingleInstance(); bldr.RegisterType().SingleInstance(); @@ -522,6 +521,21 @@ private void ConfigureServices(ContainerBuilder bldr) bldr.RegisterType() .SingleInstance(); + // Stats + bldr.RegisterType() + .AsImplementedInterfaces(); + + bldr.RegisterType() + .As() + .SingleInstance(); + + bldr.RegisterType() + .SingleInstance(); + + bldr.RegisterType() + .AsSelf().AsImplementedInterfaces() + .SingleInstance(); + // Kits KitManager.ConfigureServices(bldr);