diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 1057fe4..0af4a55 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -15,28 +15,33 @@ jobs: name: "NuGet Deployment" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout Repository - with: - fetch-depth: 0 + - uses: actions/checkout@v3 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Install dependencies run: dotnet restore + + - name: API Update version + run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits.API/Kits.API.csproj" + - name: API Update package version + run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits.API/Kits.API.csproj" + - name: API Update informational version + run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits.API/Kits.API.csproj" + - name: Update version run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits/Kits.csproj" - name: Update package version run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits/Kits.csproj" - name: Update informational version - run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits/Kits.csproj" - - name: Build + run: "sed -i \"s#0.0.0#${{ github.event.inputs.version }}#\" Kits/Kits.csproj" + - name: Build API + run: dotnet build Kits.API/Kits.API.csproj --configuration Release --no-restore + - name: Build Kits run: dotnet build Kits/Kits.csproj --configuration Release --no-restore - name: Deploy to NuGet - run: dotnet nuget push Kits/bin/Release/*.nupkg - --api-key ${{ secrets.NUGET_DEPLOY_KEY }} - --source https://api.nuget.org/v3/index.json + run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_DEPLOY_KEY }} --source https://api.nuget.org/v3/index.json - name: Release to GitHub uses: actions/create-release@master env: @@ -48,4 +53,4 @@ jobs: Changelog: ${{ github.event.inputs.update_notes }} release_name: DiFFoZ.Kits v${{ github.event.inputs.version }} - tag_name: v${{ github.event.inputs.version }} + tag_name: v${{ github.event.inputs.version }} \ No newline at end of file diff --git a/Kits.API/Configuration/GenericOptions.cs b/Kits.API/Configuration/GenericOptions.cs new file mode 100644 index 0000000..0ceea20 --- /dev/null +++ b/Kits.API/Configuration/GenericOptions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Kits.API.Configuration; +public class GenericOptions where TProvider : class +{ + private readonly List<(Type implementationService, string name)> m_KitProviders = new(); + public IReadOnlyCollection<(Type implementationService, string name)> KitProviders => m_KitProviders.AsReadOnly(); + + public void AddProvider(string name) where T : TProvider + { + AddProvider(typeof(T), name); + } + + public void AddProvider(Type type, string name) + { + if (!typeof(TProvider).IsAssignableFrom(type)) + { + throw new Exception($"Type {type} must be an instance of {typeof(TProvider).Name}!"); + } + + if (m_KitProviders.Any(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + return; + } + + m_KitProviders.Add((type, name)); + } + + public void RemoveProvider() where T : TProvider + { + RemoveProvider(typeof(T)); + } + + public void RemoveProvider(Type type) + { + m_KitProviders.RemoveAll(c => c.implementationService == type); + } + + public Type? FindType(string type) + { + return m_KitProviders.Find(x => x.name.Equals(type, StringComparison.OrdinalIgnoreCase)) + .implementationService; + } +} diff --git a/Kits.API/Cooldowns/IKitCooldownStore.cs b/Kits.API/Cooldowns/IKitCooldownStore.cs new file mode 100644 index 0000000..b3ca96e --- /dev/null +++ b/Kits.API/Cooldowns/IKitCooldownStore.cs @@ -0,0 +1,14 @@ +using OpenMod.API.Ioc; +using OpenMod.API.Permissions; +using System; +using System.Threading.Tasks; + +namespace Kits.API.Cooldowns; + +[Service] +public interface IKitCooldownStore +{ + Task GetLastCooldownAsync(IPermissionActor actor, string kitName); + + Task RegisterCooldownAsync(IPermissionActor actor, string kitName, DateTime time); +} \ No newline at end of file diff --git a/Kits.API/Cooldowns/IKitCooldownStoreProvider.cs b/Kits.API/Cooldowns/IKitCooldownStoreProvider.cs new file mode 100644 index 0000000..8e43e6f --- /dev/null +++ b/Kits.API/Cooldowns/IKitCooldownStoreProvider.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace Kits.API.Cooldowns; +public interface IKitCooldownStoreProvider : IKitCooldownStore +{ + Task InitAsync(); +} diff --git a/Kits.API/Cooldowns/KitCooldownOptions.cs b/Kits.API/Cooldowns/KitCooldownOptions.cs new file mode 100644 index 0000000..e8b2f38 --- /dev/null +++ b/Kits.API/Cooldowns/KitCooldownOptions.cs @@ -0,0 +1,6 @@ +using Kits.API.Configuration; + +namespace Kits.API.Cooldowns; +public class KitCooldownOptions : GenericOptions +{ +} diff --git a/Kits.API/Databases/IKitStoreProvider.cs b/Kits.API/Databases/IKitStoreProvider.cs new file mode 100644 index 0000000..6e71deb --- /dev/null +++ b/Kits.API/Databases/IKitStoreProvider.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Kits.API.Databases; + +public interface IKitStoreProvider : IKitStore +{ + Task InitAsync(); +} \ No newline at end of file diff --git a/Kits.API/Databases/KitStoreOptions.cs b/Kits.API/Databases/KitStoreOptions.cs new file mode 100644 index 0000000..72529c7 --- /dev/null +++ b/Kits.API/Databases/KitStoreOptions.cs @@ -0,0 +1,6 @@ +using Kits.API.Configuration; + +namespace Kits.API.Databases; +public class KitStoreOptions : GenericOptions +{ +} diff --git a/Kits.API/IKitManager.cs b/Kits.API/IKitManager.cs new file mode 100644 index 0000000..deac115 --- /dev/null +++ b/Kits.API/IKitManager.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kits.API.Models; +using OpenMod.API.Commands; +using OpenMod.API.Ioc; +using OpenMod.Extensions.Games.Abstractions.Players; + +namespace Kits.API; + +[Service] +public interface IKitManager +{ + Task GiveKitAsync(IPlayerUser user, string name, ICommandActor? instigator = null, bool forceGiveKit = false); + + Task GiveKitAsync(IPlayerUser user, Kit kit, ICommandActor? instigator = null, bool forceGiveKit = false); + + Task> GetAvailableKitsForPlayerAsync(IPlayerUser player); +} \ No newline at end of file diff --git a/Kits.API/IKitStore.cs b/Kits.API/IKitStore.cs new file mode 100644 index 0000000..43a9ce3 --- /dev/null +++ b/Kits.API/IKitStore.cs @@ -0,0 +1,30 @@ +using Kits.API.Databases; +using Kits.API.Models; +using OpenMod.API.Ioc; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kits.API; + +/// +/// The service for getting kits +/// +[Service] +public interface IKitStore +{ + /// + /// Gets the kits from + /// + /// Kits list + Task> GetKitsAsync(); + + Task FindKitByNameAsync(string name); + + Task IsKitExists(string name); + + Task AddKitAsync(Kit kit); + + Task RemoveKitAsync(string name); + + Task UpdateKitAsync(Kit kit); +} \ No newline at end of file diff --git a/Kits.API/Kits.API.csproj b/Kits.API/Kits.API.csproj new file mode 100644 index 0000000..c11c374 --- /dev/null +++ b/Kits.API/Kits.API.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + EUPL-1.2 + Kits.API + Kits.API + true + true + Kits.API + 10 + enable + nullable + 0.0.0 + 0.0.0 + 0.0.0 + EvolutionPlugins + EvolutionPlugins + DiFFoZ.Kits.API + API of OpenMod kits plugin + https://github.com/DiFFoZ/Kits + git + https://github.com/DiFFoZ/Kits + README.md + + + + + + + + + + + + + diff --git a/Kits.API/Models/Kit.cs b/Kits.API/Models/Kit.cs new file mode 100644 index 0000000..5b6d164 --- /dev/null +++ b/Kits.API/Models/Kit.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using OpenMod.Extensions.Economy.Abstractions; +using OpenMod.Extensions.Games.Abstractions.Items; +using OpenMod.Extensions.Games.Abstractions.Players; +using OpenMod.Extensions.Games.Abstractions.Vehicles; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Threading.Tasks; +using YamlDotNet.Serialization; + +namespace Kits.API.Models; + +public sealed class Kit +{ + [YamlIgnore] + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [StringLength(25)] + [Required] + public string Name { get; set; } = string.Empty; + + public float Cooldown { get; set; } + + [Column(TypeName = "decimal(18,2)")] // by default it creates decimal(65, 30) + public decimal Cost { get; set; } + + [Column(TypeName = "decimal(18,2)")] // by default it creates decimal(65, 30) + public decimal Money { get; set; } + + [StringLength(5)] + public string? VehicleId { get; set; } + + [MaxLength(ushort.MaxValue)] + [Column(TypeName = "blob")] // by default it creates longblob (2^32 - 1), we need only ushort.MaxValue length ( 65535 ) + public List? Items { get; set; } + + public async Task GiveKitToPlayer(IPlayerUser playerUser, IServiceProvider serviceProvider) + { + var stringLocalizer = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + if (Items != null && playerUser.Player is IHasInventory hasInventory && hasInventory.Inventory != null) + { + var itemSpawner = serviceProvider.GetRequiredService(); + + foreach (var item in Items) + { + var result = await itemSpawner.GiveItemAsync(hasInventory.Inventory, item.ItemAssetId, + item.State); + if (result == null) + { + logger.LogWarning("Item {Id} was unable to give to player {Name})", item.ItemAssetId, playerUser.FullActorName); + } + } + } + + if (!string.IsNullOrEmpty(VehicleId)) + { + var vehicleSpawner = serviceProvider.GetRequiredService(); + + var result = await vehicleSpawner.SpawnVehicleAsync(playerUser.Player, VehicleId!); + if (result == null) + { + logger.LogWarning("Vehicle {Id} was unable to give to player {Name})", VehicleId, playerUser.FullActorName); + } + } + + if (Money != 0) + { + var economyProvider = serviceProvider.GetRequiredService(); + + await economyProvider.UpdateBalanceAsync(playerUser.Id, playerUser.Type, Money, + stringLocalizer["commands:kit:balanceUpdateReason:got"]); + } + } +} diff --git a/Kits.API/Models/KitItem.cs b/Kits.API/Models/KitItem.cs new file mode 100644 index 0000000..d839b3e --- /dev/null +++ b/Kits.API/Models/KitItem.cs @@ -0,0 +1,45 @@ +using OpenMod.Extensions.Games.Abstractions.Items; +using System; +using System.IO; +using System.Linq; + +namespace Kits.API.Models; + +public class KitItem +{ + public string ItemAssetId { get; set; } + + public KitItemState State { get; set; } + + public KitItem() : this(null, null) + { + } + + public KitItem(string? itemAssetId, IItemState? itemState) + { + ItemAssetId = itemAssetId ?? string.Empty; + State = new KitItemState(itemState); + } + + public void Serialize(BinaryWriter bw) + { + bw.Write(ItemAssetId); + State.Serialize(bw); + } + + public void Deserialize(BinaryReader br) + { + ItemAssetId = br.ReadString(); + State.Deserialize(br); + } + + public override int GetHashCode() + { + var itemStateDataHash = State.StateData.Aggregate(new HashCode(), (hash, i) => + { + hash.Add(i); + return hash; + }).ToHashCode(); + return HashCode.Combine(ItemAssetId, State.ItemAmount, State.ItemQuality, State.ItemDurability, itemStateDataHash); + } +} \ No newline at end of file diff --git a/Kits.API/Models/KitItemState.cs b/Kits.API/Models/KitItemState.cs new file mode 100644 index 0000000..87d9977 --- /dev/null +++ b/Kits.API/Models/KitItemState.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using OpenMod.Extensions.Games.Abstractions.Items; + +namespace Kits.API.Models; + +/// +public class KitItemState : IItemState +{ + public double ItemQuality { get; set; } + + public double ItemDurability { get; set; } + + public double ItemAmount { get; set; } + + public byte[] StateData { get; set; } + + public KitItemState() : this(null) + { + } + + public KitItemState(IItemState? itemState) + { + ItemAmount = itemState?.ItemAmount ?? 0; + ItemDurability = itemState?.ItemDurability ?? 0; + ItemQuality = itemState?.ItemQuality ?? 0; + StateData = itemState?.StateData ?? Array.Empty(); + } + + public void Serialize(BinaryWriter bw) + { + bw.Write(ItemQuality); + bw.Write(ItemDurability); + bw.Write(ItemAmount); + bw.Write(StateData.Length); + bw.Write(StateData); + } + + public void Deserialize(BinaryReader br) + { + ItemQuality = br.ReadDouble(); + ItemDurability = br.ReadDouble(); + ItemAmount = br.ReadDouble(); + StateData = br.ReadBytes(br.ReadInt32()); + } +} \ No newline at end of file diff --git a/Kits.sln b/Kits.sln index 1f024e7..0b5174f 100644 --- a/Kits.sln +++ b/Kits.sln @@ -1,14 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30204.135 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33706.43 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kits", "Kits\Kits.csproj", "{6B69ACA9-CCA3-4132-A498-72F126E9D8F2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3E0BB3AB-F7F2-4870-B0DC-E75A7CB14B2A}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kits.API", "Kits.API\Kits.API.csproj", "{C01A7068-329C-4F04-8A6E-219017B34C24}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "api", "api", "{19EB88F4-90BD-493C-BD03-625387776648}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,10 +19,17 @@ Global {6B69ACA9-CCA3-4132-A498-72F126E9D8F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B69ACA9-CCA3-4132-A498-72F126E9D8F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B69ACA9-CCA3-4132-A498-72F126E9D8F2}.Release|Any CPU.Build.0 = Release|Any CPU + {C01A7068-329C-4F04-8A6E-219017B34C24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C01A7068-329C-4F04-8A6E-219017B34C24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C01A7068-329C-4F04-8A6E-219017B34C24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C01A7068-329C-4F04-8A6E-219017B34C24}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C01A7068-329C-4F04-8A6E-219017B34C24} = {19EB88F4-90BD-493C-BD03-625387776648} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BEBF1D37-F1C1-4674-9D07-36B1EAC90A6B} EndGlobalSection diff --git a/Kits/API/IKitCooldownStore.cs b/Kits/API/IKitCooldownStore.cs deleted file mode 100644 index c0ae104..0000000 --- a/Kits/API/IKitCooldownStore.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading.Tasks; -using OpenMod.API.Ioc; -using OpenMod.Extensions.Games.Abstractions.Players; - -namespace Kits.API -{ - [Service] - public interface IKitCooldownStore - { - Task GetLastCooldownAsync(IPlayerUser player, string kitName); - - Task RegisterCooldownAsync(IPlayerUser player, string kitName, DateTime time); - } -} \ No newline at end of file diff --git a/Kits/API/IKitDatabase.cs b/Kits/API/IKitDatabase.cs deleted file mode 100644 index ff3e7b1..0000000 --- a/Kits/API/IKitDatabase.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Kits.API -{ - public interface IKitDatabase - { - Task LoadDatabaseAsync(); - - Task> GetKitsAsync(); - - Task FindKitByNameAsync(string name); - - Task AddKitAsync(Kit kit); - - Task RemoveKitAsync(string name); - - Task UpdateKitAsync(Kit kit); - } -} \ No newline at end of file diff --git a/Kits/API/IKitManager.cs b/Kits/API/IKitManager.cs deleted file mode 100644 index 053a249..0000000 --- a/Kits/API/IKitManager.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using OpenMod.API.Commands; -using OpenMod.API.Ioc; -using OpenMod.Extensions.Games.Abstractions.Players; - -namespace Kits.API -{ - [Service] - public interface IKitManager - { - Task GiveKitAsync(IPlayerUser user, string name, ICommandActor? instigator = null, bool forceGiveKit = false); - - Task> GetAvailableKitsForPlayerAsync(IPlayerUser player); - } -} \ No newline at end of file diff --git a/Kits/API/IKitStore.cs b/Kits/API/IKitStore.cs deleted file mode 100644 index fe002c5..0000000 --- a/Kits/API/IKitStore.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using OpenMod.API.Ioc; - -namespace Kits.API -{ - [Service] - public interface IKitStore - { - IKitDatabase Database { get; } - - Task> GetKitsAsync(); - - Task FindKitByNameAsync(string kitName); - - Task AddKitAsync(Kit kit); - - Task RemoveKitAsync(string kitName); - } -} \ No newline at end of file diff --git a/Kits/API/Kit.cs b/Kits/API/Kit.cs deleted file mode 100644 index 5172310..0000000 --- a/Kits/API/Kit.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Logging; -using OpenMod.Extensions.Economy.Abstractions; -using OpenMod.Extensions.Games.Abstractions.Items; -using OpenMod.Extensions.Games.Abstractions.Players; -using OpenMod.Extensions.Games.Abstractions.Vehicles; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Threading.Tasks; -using YamlDotNet.Serialization; - -namespace Kits.API -{ -#nullable disable - public class Kit - { - [YamlIgnore] - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [StringLength(25)] - [Required] - public string Name { get; set; } - - public float Cooldown { get; set; } - - [Column(TypeName = "decimal(18,2)")] // by default it creates decimal(65, 30) - public decimal Cost { get; set; } - - [Column(TypeName = "decimal(18,2)")] // by default it creates decimal(65, 30) - public decimal Money { get; set; } - -#nullable enable - [StringLength(5)] - public string? VehicleId { get; set; } - - [MaxLength(ushort.MaxValue)] - [Column(TypeName = "blob")] // by default it creates longblob (2^32 - 1), we need only ushort.MaxValue length ( 65535 ) - public List? Items { get; set; } - - public virtual async Task GiveKitToPlayer(IPlayerUser playerUser, IServiceProvider serviceProvider) - { - if (Items == null) - { - return; - } - - var economyProvider = serviceProvider.GetRequiredService(); - var stringLocalizer = serviceProvider.GetRequiredService(); - var logger = serviceProvider.GetRequiredService>(); - - if (playerUser.Player is IHasInventory hasInventory && hasInventory.Inventory != null) - { - var itemSpawner = serviceProvider.GetRequiredService(); - - foreach (var item in Items) - { - var result = await itemSpawner.GiveItemAsync(hasInventory.Inventory, item.ItemAssetId, - item.State); - if (result == null) - { - logger.LogWarning("Item {Id} was unable to give to player {Name})", item.ItemAssetId, playerUser.FullActorName); - } - } - } - - if (!string.IsNullOrEmpty(VehicleId)) - { - var vehicleSpawner = serviceProvider.GetRequiredService(); - - var result = await vehicleSpawner.SpawnVehicleAsync(playerUser.Player, VehicleId!); - if (result == null) - { - logger.LogWarning("Vehicle {Id} was unable to give to player {Name})", VehicleId, playerUser.FullActorName); - } - } - - if (Money != 0) - { - await economyProvider.UpdateBalanceAsync(playerUser.Id, playerUser.Type, Money, - stringLocalizer["commands:kit:balanceUpdateReason:got"]); - } - } - } -} diff --git a/Kits/API/KitItem.cs b/Kits/API/KitItem.cs deleted file mode 100644 index dcbb868..0000000 --- a/Kits/API/KitItem.cs +++ /dev/null @@ -1,35 +0,0 @@ -using OpenMod.Extensions.Games.Abstractions.Items; -using System.IO; - -namespace Kits.API -{ - public class KitItem - { - public string ItemAssetId { get; set; } - - public KitItemState State { get; set; } - - - public KitItem() : this(null, null) - { - } - - public KitItem(string? itemAssetId, IItemState? itemState) - { - ItemAssetId = itemAssetId ?? string.Empty; - State = new KitItemState(itemState); - } - - public void Serialize(BinaryWriter bw) - { - bw.Write(ItemAssetId); - State.Serialize(bw); - } - - public void Deserialize(BinaryReader br) - { - ItemAssetId = br.ReadString(); - State.Deserialize(br); - } - } -} \ No newline at end of file diff --git a/Kits/API/KitItemState.cs b/Kits/API/KitItemState.cs deleted file mode 100644 index 93da244..0000000 --- a/Kits/API/KitItemState.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.IO; -using OpenMod.Extensions.Games.Abstractions.Items; - -namespace Kits.API -{ - public class KitItemState : IItemState - { - public double ItemQuality { get; set; } - - public double ItemDurability { get; set; } - - public double ItemAmount { get; set; } - - public byte[] StateData { get; set; } - - public KitItemState() : this(null) - { - } - - public KitItemState(IItemState? itemState) - { - ItemAmount = itemState?.ItemAmount ?? 0; - ItemDurability = itemState?.ItemDurability ?? 0; - ItemQuality = itemState?.ItemQuality ?? 0; - StateData = itemState?.StateData ?? Array.Empty(); - } - - public void Serialize(BinaryWriter bw) - { - bw.Write(ItemQuality); - bw.Write(ItemDurability); - bw.Write(ItemAmount); - bw.Write(StateData.Length); - bw.Write(StateData); - } - - public void Deserialize(BinaryReader br) - { - ItemQuality = br.ReadDouble(); - ItemDurability = br.ReadDouble(); - ItemAmount = br.ReadDouble(); - StateData = br.ReadBytes(br.ReadInt32()); - } - } -} \ No newline at end of file diff --git a/Kits/Commands/CommandKit.cs b/Kits/Commands/CommandKit.cs index f7e612f..86f6d6f 100644 --- a/Kits/Commands/CommandKit.cs +++ b/Kits/Commands/CommandKit.cs @@ -1,53 +1,45 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Kits.API; +using Kits.API; using OpenMod.API.Permissions; using OpenMod.Core.Commands; using OpenMod.Core.Permissions; using OpenMod.Extensions.Games.Abstractions.Players; +using System; +using System.Linq; +using System.Threading.Tasks; -namespace Kits.Commands +namespace Kits.Commands; + +[Command("kit")] +[CommandSyntax("[create|remove] [player] ")] +[RegisterCommandPermission("give.other", Description = "Give the kit to another player but it will checks if another player has permission, money, etc..")] // will check if player has permission to get kit +[RegisterCommandPermission("give.other.force", Description = "Force give the kit to another player without checks if the player has permission, money, etc..")] // will not check if player has permission, money, etc.. to get kit +public class CommandKit : Command { - [Command("kit")] - [CommandSyntax("[create|remove] [player] ")] - [RegisterCommandPermission("give.other", Description = "Give the kit to another player but it will checks if another player has permission, money, etc..")] // will check if player has permission to get kit - [RegisterCommandPermission("give.other.force", Description = "Force give the kit to another player without checks if the player has permission, money, etc..")] // will not check if player has permission, money, etc.. to get kit - public class CommandKit : Command - { - private readonly IKitManager m_KitManager; + private readonly IKitManager m_KitManager; - public CommandKit(IServiceProvider serviceProvider, IKitManager kitManager) : base(serviceProvider) - { - m_KitManager = kitManager; - } + public CommandKit(IServiceProvider serviceProvider, IKitManager kitManager) : base(serviceProvider) + { + m_KitManager = kitManager; + } - protected override async Task OnExecuteAsync() - { - var giveKitUser = (Context.Parameters.Count == 2 ? await Context.Parameters.GetAsync(0) - : Context.Actor as IPlayerUser) ?? throw new CommandWrongUsageException(Context); + protected override async Task OnExecuteAsync() + { + var giveKitUser = (Context.Parameters.Count == 2 + ? await Context.Parameters.GetAsync(0) : Context.Actor as IPlayerUser) + ?? throw new CommandWrongUsageException(Context); - var isNotExecutor = giveKitUser != Context.Actor; - var forceGiveKit = false; - var kitName = Context.Parameters.LastOrDefault() ?? throw new CommandWrongUsageException(Context); - - if (isNotExecutor) - { - if (await CheckPermissionAsync("give.other.force") == PermissionGrantResult.Grant) - { - forceGiveKit = true; - } - else if (await CheckPermissionAsync("give.other") == PermissionGrantResult.Grant) - { - forceGiveKit = false; - } - else - { - throw new NotEnoughPermissionException(Context, "give.other"); - } - } + var isNotExecutor = giveKitUser != Context.Actor; + var forceGiveKit = false; + var kitName = Context.Parameters.LastOrDefault() ?? throw new CommandWrongUsageException(Context); - await m_KitManager.GiveKitAsync(giveKitUser, kitName, isNotExecutor ? Context.Actor : null, forceGiveKit); + if (isNotExecutor) + { + forceGiveKit = await CheckPermissionAsync("give.other.force") == PermissionGrantResult.Grant + || (await CheckPermissionAsync("give.other") == PermissionGrantResult.Grant + ? false + : throw new NotEnoughPermissionException(Context, "give.other")); } + + await m_KitManager.GiveKitAsync(giveKitUser, kitName, isNotExecutor ? Context.Actor : null, forceGiveKit); } } \ No newline at end of file diff --git a/Kits/Commands/CommandKitCreate.cs b/Kits/Commands/CommandKitCreate.cs index 7295106..69b6f13 100644 --- a/Kits/Commands/CommandKitCreate.cs +++ b/Kits/Commands/CommandKitCreate.cs @@ -1,5 +1,5 @@ -using JetBrains.Annotations; -using Kits.API; +using Kits.API; +using Kits.API.Models; using Kits.Extensions; using Microsoft.Extensions.Localization; using OpenMod.API.Commands; @@ -8,111 +8,123 @@ using OpenMod.Extensions.Games.Abstractions.Players; using OpenMod.Extensions.Games.Abstractions.Vehicles; using System; +using System.Drawing; using System.Linq; using System.Threading.Tasks; -namespace Kits.Commands +namespace Kits.Commands; + +[Command("create")] +[CommandAlias("add")] +[CommandAlias("+")] +[CommandActor(typeof(IPlayerUser))] +[CommandParent(typeof(CommandKit))] +[CommandSyntax(" [cooldown] [cost] [money] [vehicleId]")] +public class CommandKitCreate : Command { - [Command("create")] - [CommandAlias("add")] - [CommandAlias("+")] - [CommandActor(typeof(IPlayerUser))] - [CommandParent(typeof(CommandKit))] - [CommandSyntax(" [cooldown] [cost] [money] [vehicleId]")] - [UsedImplicitly] - public class CommandKitCreate : Command + private static readonly bool s_IsUnturned = AppDomain.CurrentDomain.GetAssemblies() + .Any(x => x.GetName().Name.Equals("OpenMod.Unturned.Module.Shared")); + + private readonly IKitStore m_KitStore; + private readonly IStringLocalizer m_StringLocalizer; + private readonly IVehicleDirectory m_VehicleDirectory; + + public CommandKitCreate(IServiceProvider serviceProvider, IKitStore kitStore, + IStringLocalizer stringLocalizer, IVehicleDirectory vehicleDirectory) : base(serviceProvider) { - private static readonly bool s_IsUnturned = AppDomain.CurrentDomain.GetAssemblies() - .Any(x => x.GetName().Name.Equals("OpenMod.Unturned.Module.Shared")); + m_KitStore = kitStore; + m_StringLocalizer = stringLocalizer; + m_VehicleDirectory = vehicleDirectory; + } - private readonly IKitStore m_KitStore; - private readonly IStringLocalizer m_StringLocalizer; - private readonly IVehicleDirectory m_VehicleDirectory; + protected override async Task OnExecuteAsync() + { + if (Context.Parameters.Count is < 1 or > 5) + { + throw new CommandWrongUsageException(Context); + } - public CommandKitCreate(IServiceProvider serviceProvider, IKitStore kitStore, - IStringLocalizer stringLocalizer, IVehicleDirectory vehicleDirectory) : base(serviceProvider) + var playerUser = (IPlayerUser)Context.Actor; + if (playerUser.Player is not IHasInventory hasInventory) { - m_KitStore = kitStore; - m_StringLocalizer = stringLocalizer; - m_VehicleDirectory = vehicleDirectory; + throw new UserFriendlyException("IPlayer doesn't have compatibility IHasInventory"); } - protected override async Task OnExecuteAsync() + TimeSpan cooldown; + try { - if (Context.Parameters.Count is < 1 or > 5) - { - throw new CommandWrongUsageException(Context); - } - - var playerUser = (IPlayerUser)Context.Actor; - if (playerUser.Player is not IHasInventory hasInventory) - { - throw new UserFriendlyException("IPlayer doesn't have compatibility IHasInventory"); - } - - var name = Context.Parameters[0]; - var cooldown = Context.Parameters.Count >= 2 + // many users reports that cooldown not working (it actually works, but require a specific format) + cooldown = Context.Parameters.Count >= 2 ? await Context.Parameters.GetAsync(1) : TimeSpan.Zero; - var cost = Context.Parameters.Count >= 3 ? await Context.Parameters.GetAsync(2) : 0; - var money = Context.Parameters.Count >= 4 ? await Context.Parameters.GetAsync(3) : 0; - var vehicleId = Context.Parameters.Count == 5 ? Context.Parameters[4] : null; - - var shouldForceCreate = cost != 0 || money != 0 || !string.IsNullOrEmpty(vehicleId); - - if (cooldown < TimeSpan.Zero) - { - throw new UserFriendlyException("The cooldown cannot be negative!"); - } - - if (cost < 0) - { - throw new UserFriendlyException("The cost cannot be negative!"); - } - - if (money < 0) - { - throw new UserFriendlyException("The money cannot be negative!"); - } - - if (!string.IsNullOrEmpty(vehicleId) && await m_VehicleDirectory.FindByIdAsync(vehicleId!) == null) - { - throw new UserFriendlyException($"The vehicle {vehicleId} not found"); - } - - var kits = await m_KitStore.GetKitsAsync(); - if (kits.Any(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - throw new UserFriendlyException(m_StringLocalizer["commands:kit:exist", new { Name = name }]); - } - - var items = hasInventory.Inventory! - .SelectMany(x => x.Items - .Select(c => c.Item.ConvertIItemToKitItem())) - .ToList(); - - if (!shouldForceCreate && items.Count == 0) - { - throw new UserFriendlyException(m_StringLocalizer["commands:kit:create:noItems"]); - } - - var kit = new Kit - { - Cooldown = (float)cooldown.TotalSeconds, - Items = items, - Name = name, - Cost = cost, - Money = money, - VehicleId = vehicleId - }; - - if (s_IsUnturned) - { - UnturnedExtension.AddClothes(playerUser, kit.Items); - } - - await m_KitStore.AddKitAsync(kit); - await PrintAsync(m_StringLocalizer["commands:kit:create:success", new { Kit = kit }]); } + catch (UserFriendlyException ufe) + { + await PrintAsync(ufe.Message, Color.DarkRed); // "Invalid time span format" + await PrintAsync("Valid examples format for cooldown parameter: 5s / 5m / 5h / 5d", Color.DarkRed); + + return; + } + + var name = Context.Parameters[0]; + // cooldown [1] + var cost = Context.Parameters.Count >= 3 ? await Context.Parameters.GetAsync(2) : 0; + var money = Context.Parameters.Count >= 4 ? await Context.Parameters.GetAsync(3) : 0; + var vehicleId = Context.Parameters.Count == 5 ? Context.Parameters[4] : null; + + var shouldForceCreate = cost != 0 || money != 0 || !string.IsNullOrEmpty(vehicleId); + + if (cooldown < TimeSpan.Zero) + { + throw new UserFriendlyException("The cooldown cannot be negative!"); + } + + if (cost < 0) + { + throw new UserFriendlyException("The cost cannot be negative!"); + } + + if (money < 0) + { + throw new UserFriendlyException("The money cannot be negative!"); + } + + if (!string.IsNullOrEmpty(vehicleId) && await m_VehicleDirectory.FindByIdAsync(vehicleId!) == null) + { + throw new UserFriendlyException($"The vehicle {vehicleId} not found"); + } + + if ((await m_KitStore.FindKitByNameAsync(name)) != null) + { + throw new UserFriendlyException(m_StringLocalizer["commands:kit:exist", new { Name = name }]); + } + + var items = hasInventory.Inventory! + .SelectMany(x => x.Items + .Select(c => c.Item.ConvertIItemToKitItem())) + .ToList(); + + if (!shouldForceCreate && items.Count == 0) + { + throw new UserFriendlyException(m_StringLocalizer["commands:kit:create:noItems"]); + } + + var kit = new Kit + { + Cooldown = (float)cooldown.TotalSeconds, + Items = items, + Name = name, + Cost = cost, + Money = money, + VehicleId = vehicleId + }; + + if (s_IsUnturned) + { + UnturnedExtension.AddClothes(playerUser, kit.Items); + } + + await m_KitStore.AddKitAsync(kit); + await PrintAsync(m_StringLocalizer["commands:kit:create:success", new { Kit = kit }]); } } \ No newline at end of file diff --git a/Kits/Commands/CommandKitMigrate.cs b/Kits/Commands/CommandKitMigrate.cs deleted file mode 100644 index be8fec5..0000000 --- a/Kits/Commands/CommandKitMigrate.cs +++ /dev/null @@ -1,48 +0,0 @@ -extern alias JetBrainsAnnotations; -using JetBrainsAnnotations::JetBrains.Annotations; -using Kits.API; -using Kits.Databases; -using Kits.Databases.Mysql; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using OpenMod.API.Persistence; -using OpenMod.Core.Commands; -using OpenMod.Core.Console; -using System; -using System.Threading.Tasks; -using Command = OpenMod.Core.Commands.Command; - -namespace Kits.Commands -{ - [Command("migrate")] - [CommandActor(typeof(ConsoleActor))] - [CommandParent(typeof(CommandKit))] - [UsedImplicitly] - public class CommandKitMigrate : Command - { - private readonly IServiceProvider m_ServiceProvider; - private readonly IConfiguration m_Configuration; - - public CommandKitMigrate(IServiceProvider serviceProvider, IConfiguration configuration) : base(serviceProvider) - { - m_ServiceProvider = serviceProvider; - m_Configuration = configuration; - } - - protected override async Task OnExecuteAsync() - { - var mysql = new MySqlKitDatabase(m_ServiceProvider); - await mysql.LoadDatabaseAsync(); - - await using var dbContext = mysql.GetDbContext(); - - var oldTableName = m_Configuration["database:connectionTableName"] ?? "kits"; - var newTableName = dbContext.Model.FindEntityType(typeof(Kit)).GetTableName(); - - // maybe has other option to migrate data to other table - var affected = await dbContext.Database.ExecuteSqlRawAsync($"INSERT INTO `{newTableName}` (Id, Name, Cooldown, Cost, Money, VehicleId, Items) SELECT Id, Name, Cooldown, Cost, Money, VehicleId, Items FROM `{oldTableName}`"); - - await PrintAsync($"Successfully migrated {affected} kit(s) to the new table"); - } - } -} \ No newline at end of file diff --git a/Kits/Commands/CommandKitRemove.cs b/Kits/Commands/CommandKitRemove.cs index 65f6c02..37aa791 100644 --- a/Kits/Commands/CommandKitRemove.cs +++ b/Kits/Commands/CommandKitRemove.cs @@ -1,40 +1,37 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Kits.API; +using Kits.API; using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; +using System; +using System.Threading.Tasks; + +namespace Kits.Commands; -namespace Kits.Commands +[Command("remove")] +[CommandAlias("-")] +[CommandAlias("delete")] +[CommandParent(typeof(CommandKit))] +[CommandSyntax("")] +public class CommandKitRemove : Command { - [Command("remove")] - [CommandAlias("-")] - [CommandAlias("delete")] - [CommandParent(typeof(CommandKit))] - [CommandSyntax("")] - [UsedImplicitly] - public class CommandKitRemove : Command + private readonly IKitStore m_KitStore; + private readonly IStringLocalizer m_StringLocalizer; + + public CommandKitRemove(IServiceProvider serviceProvider, IKitStore kitStore, + IStringLocalizer stringLocalizer) : base(serviceProvider) { - private readonly IKitStore m_KitStore; - private readonly IStringLocalizer m_StringLocalizer; + m_KitStore = kitStore; + m_StringLocalizer = stringLocalizer; + } - public CommandKitRemove(IServiceProvider serviceProvider, IKitStore kitStore, - IStringLocalizer stringLocalizer) : base(serviceProvider) + protected override async Task OnExecuteAsync() + { + if (Context.Parameters.Length != 1) { - m_KitStore = kitStore; - m_StringLocalizer = stringLocalizer; + throw new CommandWrongUsageException(Context); } - protected override async Task OnExecuteAsync() - { - if (Context.Parameters.Length != 1) - { - throw new CommandWrongUsageException(Context); - } - - var kitName = Context.Parameters[0]; - await m_KitStore.RemoveKitAsync(kitName); - await PrintAsync(m_StringLocalizer["commands:kit:remove:success", new { Name = kitName }]); - } + var kitName = Context.Parameters[0]; + await m_KitStore.RemoveKitAsync(kitName); + await PrintAsync(m_StringLocalizer["commands:kit:remove:success", new { Name = kitName }]); } } \ No newline at end of file diff --git a/Kits/Commands/CommandKitUpdate.cs b/Kits/Commands/CommandKitUpdate.cs new file mode 100644 index 0000000..617a1fc --- /dev/null +++ b/Kits/Commands/CommandKitUpdate.cs @@ -0,0 +1,56 @@ +using Kits.API; +using Kits.Extensions; +using Microsoft.Extensions.Localization; +using OpenMod.API.Commands; +using OpenMod.Core.Commands; +using OpenMod.Extensions.Games.Abstractions.Items; +using OpenMod.Extensions.Games.Abstractions.Players; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Kits.Commands; +[Command("update")] +[CommandParent(typeof(CommandKit))] +[CommandActor(typeof(IPlayerUser))] +[CommandSyntax("")] +public class CommandKitUpdate : Command +{ + private readonly IKitStore m_KitStore; + private readonly IStringLocalizer m_StringLocalizer; + + public CommandKitUpdate(IServiceProvider serviceProvider, IKitStore kitStore, IStringLocalizer stringLocalizer) : base(serviceProvider) + { + m_KitStore = kitStore; + m_StringLocalizer = stringLocalizer; + } + + protected override async Task OnExecuteAsync() + { + if (Context.Parameters.Count != 1) + { + throw new CommandWrongUsageException(Context); + } + + var name = Context.Parameters[0]; + var kit = await m_KitStore.FindKitByNameAsync(name) ?? throw new UserFriendlyException(m_StringLocalizer["commands:kit:notFound", + new + { + Name = name + }]); + + var user = (IPlayerUser)Context.Actor; + if (user.Player is not IHasInventory hasInventory) + { + throw new Exception("IPlayer doesn't have compatibility IHasInventory"); + } + + kit.Items = hasInventory.Inventory! + .SelectMany(x => x.Items + .Select(c => c.Item.ConvertIItemToKitItem())) + .ToList(); + await m_KitStore.UpdateKitAsync(kit); + + await PrintAsync("Updated the kit."); + } +} diff --git a/Kits/Commands/CommandKits.cs b/Kits/Commands/CommandKits.cs index ed8157e..2461d6e 100644 --- a/Kits/Commands/CommandKits.cs +++ b/Kits/Commands/CommandKits.cs @@ -1,119 +1,114 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Text; -using System.Threading.Tasks; -using EvolutionPlugins.Economy.Stub.Services; -using JetBrains.Annotations; +using EvolutionPlugins.Economy.Stub.Services; using Kits.API; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Logging; using OpenMod.API.Permissions; using OpenMod.Core.Commands; using OpenMod.Core.Permissions; using OpenMod.Extensions.Economy.Abstractions; using OpenMod.Extensions.Games.Abstractions.Players; using SmartFormat.ZString; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Threading.Tasks; + +namespace Kits.Commands; -namespace Kits.Commands +[Command("kits")] +[RegisterCommandPermission("show.other", Description = "Shows the available kits of another player")] +[CommandSyntax("[player]")] +public class CommandKits : Command { - [Command("kits")] - [RegisterCommandPermission("show.other", Description = "Shows the available kits of another player")] - [CommandSyntax("[player]")] - [UsedImplicitly] - public class CommandKits : Command + private readonly IKitManager m_KitManager; + private readonly IStringLocalizer m_StringLocalizer; + private readonly IEconomyProvider m_EconomyProvider; + private readonly IConfiguration m_Configuration; + + public CommandKits(IServiceProvider serviceProvider, IKitManager kitManager, IStringLocalizer stringLocalizer, + IEconomyProvider economyProvider, IConfiguration configuration) : base(serviceProvider) { - private readonly IKitManager m_KitManager; - private readonly IStringLocalizer m_StringLocalizer; - private readonly IEconomyProvider m_EconomyProvider; - private readonly IConfiguration m_Configuration; + m_KitManager = kitManager; + m_StringLocalizer = stringLocalizer; + m_EconomyProvider = economyProvider; + m_Configuration = configuration; + } - public CommandKits(IServiceProvider serviceProvider, IKitManager kitManager, IStringLocalizer stringLocalizer, - IEconomyProvider economyProvider, IConfiguration configuration) : base(serviceProvider) - { - m_KitManager = kitManager; - m_StringLocalizer = stringLocalizer; - m_EconomyProvider = economyProvider; - m_Configuration = configuration; - } + protected override async Task OnExecuteAsync() + { + var showKitsUser = (Context.Parameters.Count == 1 ? await Context.Parameters.GetAsync(0) + : Context.Actor as IPlayerUser) ?? throw new CommandWrongUsageException(Context); - protected override async Task OnExecuteAsync() + var isNotExecutor = showKitsUser != Context.Actor; + if (isNotExecutor && await CheckPermissionAsync("show.other") != PermissionGrantResult.Grant) { - var showKitsUser = (Context.Parameters.Count == 1 ? await Context.Parameters.GetAsync(0) - : Context.Actor as IPlayerUser) ?? throw new CommandWrongUsageException(Context); - - var isNotExecutor = showKitsUser != Context.Actor; - if (isNotExecutor && await CheckPermissionAsync("show.other") != PermissionGrantResult.Grant) - { - throw new NotEnoughPermissionException(Context, "show.other"); - } + throw new NotEnoughPermissionException(Context, "show.other"); + } - var moneySymbol = "$"; - var moneyName = string.Empty; + var moneySymbol = "$"; + var moneyName = string.Empty; - // prevent some exceptions - if (m_EconomyProvider is not EconomyProviderStub) - { - moneySymbol = m_EconomyProvider.CurrencySymbol; - moneyName = m_EconomyProvider.CurrencyName; - } + // prevent some exceptions + if (m_EconomyProvider is not EconomyProviderStub) + { + moneySymbol = m_EconomyProvider.CurrencySymbol; + moneyName = m_EconomyProvider.CurrencyName; + } - var kits = await m_KitManager.GetAvailableKitsForPlayerAsync(showKitsUser); - kits = kits.Count > 0 ? kits : null; + var kits = await m_KitManager.GetAvailableKitsForPlayerAsync(showKitsUser); + kits = kits.Count > 0 ? kits : null; - await PrintAsync(m_StringLocalizer["commands:kits", new - { - Kits = kits, - MoneySymbol = moneySymbol, - MoneyName = moneyName - }]); - } + await PrintAsync(m_StringLocalizer["commands:kits", new + { + Kits = kits, + MoneySymbol = moneySymbol, + MoneyName = moneyName + }]); + } - public override async Task PrintAsync(string message) + public override async Task PrintAsync(string message) + { + if (m_Configuration.GetValue("wrapLines", true)) { - if (m_Configuration.GetValue("wrapLines", true)) + foreach (var msg in WrapLines(message)) { - foreach (var msg in WrapLines(message)) - { - await PrintAsync(msg, Color.White); - } - return; + await PrintAsync(msg, Color.White); } - - await PrintAsync(message, Color.White); + return; } - private static IEnumerable WrapLines(string line) - { - const int MaxLength = 90; + await PrintAsync(message, Color.White); + } - using var currentLine = new ZStringBuilder(false); + private static IEnumerable WrapLines(string line) + { + const int MaxLength = 90; - foreach (var currentWord in line.Split(' ')) + using var currentLine = new ZStringBuilder(false); + + foreach (var currentWord in line.Split(' ')) + { + if (currentLine.Length > MaxLength || + currentLine.Length + currentWord.Length > MaxLength) { - if (currentLine.Length > MaxLength || - currentLine.Length + currentWord.Length > MaxLength) - { - yield return currentLine.ToString(); - currentLine.Clear(); - } - - if (currentLine.Length > 0) - { - currentLine.Append(" "); - currentLine.Append(currentWord); - } - else - { - currentLine.Append(currentWord); - } + yield return currentLine.ToString(); + currentLine.Clear(); } if (currentLine.Length > 0) { - yield return currentLine.ToString(); + currentLine.Append(" "); + currentLine.Append(currentWord); } + else + { + currentLine.Append(currentWord); + } + } + + if (currentLine.Length > 0) + { + yield return currentLine.ToString(); } } } \ No newline at end of file diff --git a/Kits/Cooldowns/DataStore/KitCooldownData.cs b/Kits/Cooldowns/DataStore/KitCooldownData.cs new file mode 100644 index 0000000..92a1e2e --- /dev/null +++ b/Kits/Cooldowns/DataStore/KitCooldownData.cs @@ -0,0 +1,9 @@ +using System; + +namespace Kits.Cooldowns.DataStore; + +public class KitCooldownData +{ + public string? KitName { get; set; } + public DateTime KitCooldown { get; set; } +} \ No newline at end of file diff --git a/Kits/Cooldowns/DataStore/KitsCooldownData.cs b/Kits/Cooldowns/DataStore/KitsCooldownData.cs new file mode 100644 index 0000000..34d7ab0 --- /dev/null +++ b/Kits/Cooldowns/DataStore/KitsCooldownData.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Kits.Cooldowns.DataStore; + +public class KitsCooldownData +{ + public Dictionary>? KitsCooldown { get; set; } +} \ No newline at end of file diff --git a/Kits/Cooldowns/KitCooldownStore.cs b/Kits/Cooldowns/KitCooldownStore.cs new file mode 100644 index 0000000..5b229a8 --- /dev/null +++ b/Kits/Cooldowns/KitCooldownStore.cs @@ -0,0 +1,104 @@ +using Autofac; +using Kits.API.Cooldowns; +using Kits.Cooldowns.Providers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenMod.API.Ioc; +using OpenMod.API.Permissions; +using OpenMod.API.Plugins; +using OpenMod.Core.Helpers; +using OpenMod.Core.Permissions; +using System; +using System.Reflection; +using System.Threading.Tasks; + +[assembly: RegisterPermission("nocooldown", Description = "Allows use kit without waiting for cooldown")] + +namespace Kits.Cooldowns; + +[ServiceImplementation(Lifetime = ServiceLifetime.Singleton)] +public class KitCooldownStore : IKitCooldownStore, IAsyncDisposable +{ + private static readonly string s_NoCooldownFullPermission = "nocooldown"; + static KitCooldownStore() + { + s_NoCooldownFullPermission = typeof(KitCooldownStore).Assembly.GetCustomAttribute().Id + ":" + s_NoCooldownFullPermission; + } + + private readonly IPluginAccessor m_Plugin; + private readonly IPermissionChecker m_PermissionChecker; + private readonly IOptions m_Options; + private readonly ILogger m_Logger; + + private IKitCooldownStoreProvider m_CooldownProvider = null!; + + public KitCooldownStore(IPluginAccessor plugin, IPermissionChecker permissionChecker, IOptions options, ILogger logger) + { + m_Plugin = plugin; + m_PermissionChecker = permissionChecker; + m_Options = options; + m_Logger = logger; + + AsyncHelper.RunSync(InitAsync); + } + + internal async Task InitAsync() + { + // todo: reinit if plugin configuration was updated + + var plugin = m_Plugin.Instance ?? throw new Exception("Plugin is not loaded"); + var configuration = plugin.LifetimeScope.Resolve(); + + var type = configuration["cooldowns:connectionType"] ?? string.Empty; + var providerType = m_Options.Value.FindType(type); + + if (providerType != null) + { + m_Logger.LogDebug("Cooldown store type set to `{DatabaseType}`", type); + try + { + var serviceProvider = plugin.LifetimeScope.Resolve(); + m_CooldownProvider = (IKitCooldownStoreProvider)ActivatorUtilities.CreateInstance(serviceProvider, providerType); + } + catch (Exception ex) + { + m_Logger.LogError(ex, "Failed to initialize {CooldownStoreName}. Defaulting to `datastore`", providerType.Name); + } + } + else + { + m_Logger.LogWarning("Unable to parse {DatabaseType}. Setting to default: `datastore`", type); + } + + m_CooldownProvider ??= new DataStoreKitCooldownStoreProvider(plugin); + + await m_CooldownProvider.InitAsync(); + } + + public async Task GetLastCooldownAsync(IPermissionActor actor, string kitName) + { + return await HasNoCooldownPermission(actor) ? null : await m_CooldownProvider.GetLastCooldownAsync(actor, kitName); + } + + public async Task RegisterCooldownAsync(IPermissionActor actor, string kitName, DateTime time) + { + if (await HasNoCooldownPermission(actor)) + { + return; + } + + await m_CooldownProvider.RegisterCooldownAsync(actor, kitName, time); + } + + private async Task HasNoCooldownPermission(IPermissionActor actor) + { + return await m_PermissionChecker.CheckPermissionAsync(actor, s_NoCooldownFullPermission) is PermissionGrantResult.Grant; + } + + public ValueTask DisposeAsync() + { + return m_CooldownProvider == null ? new() : new(m_CooldownProvider.DisposeSyncOrAsync()); + } +} \ No newline at end of file diff --git a/Kits/Cooldowns/MySql/KitCooldown.cs b/Kits/Cooldowns/MySql/KitCooldown.cs new file mode 100644 index 0000000..cdd77f9 --- /dev/null +++ b/Kits/Cooldowns/MySql/KitCooldown.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Kits.Cooldowns.MySql; +public sealed class KitCooldown +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public int Id { get; set; } + + [Required] + [StringLength(50)] + public string PlayerId { get; set; } = string.Empty; + + [StringLength(25)] + public string Kit { get; set; } = string.Empty; + + public DateTime UsedTime { get; set; } +} diff --git a/Kits/Cooldowns/MySql/KitCooldownsDbContext.cs b/Kits/Cooldowns/MySql/KitCooldownsDbContext.cs new file mode 100644 index 0000000..6d80938 --- /dev/null +++ b/Kits/Cooldowns/MySql/KitCooldownsDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenMod.EntityFrameworkCore; +using OpenMod.EntityFrameworkCore.Configurator; +using System; + +namespace Kits.Cooldowns.MySql; + +// Due to how EF Core works, we cannot change the table name in runtime +// so if user want to store cooldowns in MySQL, but each server should use the own set of cooldowns then +// the user should create another DBs (let's say Server1, Server2) and set the connection string in configuration. +// Of course if user wants only global cooldowns then no configuration needed. +[ConnectionString("cooldown")] +public class KitCooldownsDbContext : OpenModDbContext +{ + public DbSet KitCooldowns => Set(); + + public KitCooldownsDbContext(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public KitCooldownsDbContext(IDbContextConfigurator configurator, IServiceProvider serviceProvider) : base(configurator, serviceProvider) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + var dateTimeConverter = new ValueConverter( + v => v.ToUniversalTime(), + v => DateTime.SpecifyKind(v, DateTimeKind.Utc)); + + var property = modelBuilder + .Entity() + .Property(x => x.UsedTime); + + // adding conversion to save DateTime as UTC and get back as UTC + property.HasConversion(dateTimeConverter); + } +} diff --git a/Kits/Cooldowns/MySql/KitCooldownsDbContextFactory.cs b/Kits/Cooldowns/MySql/KitCooldownsDbContextFactory.cs new file mode 100644 index 0000000..01e1115 --- /dev/null +++ b/Kits/Cooldowns/MySql/KitCooldownsDbContextFactory.cs @@ -0,0 +1,6 @@ +using OpenMod.EntityFrameworkCore.MySql; + +namespace Kits.Cooldowns.MySql; +public class KitCooldownsDbContextFactory : OpenModMySqlDbContextFactory +{ +} diff --git a/Kits/Cooldowns/Providers/DataStoreKitCooldownStoreProvider.cs b/Kits/Cooldowns/Providers/DataStoreKitCooldownStoreProvider.cs new file mode 100644 index 0000000..6538cb2 --- /dev/null +++ b/Kits/Cooldowns/Providers/DataStoreKitCooldownStoreProvider.cs @@ -0,0 +1,90 @@ +using Kits.API.Cooldowns; +using Kits.Cooldowns.DataStore; +using OpenMod.API.Permissions; +using OpenMod.API.Persistence; +using OpenMod.Core.Helpers; +using System; +using System.Threading.Tasks; + +namespace Kits.Cooldowns.Providers; +public class DataStoreKitCooldownStoreProvider : IKitCooldownStoreProvider, IDisposable +{ + private const string c_CooldownKey = "cooldowns"; + + private readonly KitsPlugin m_Plugin; + + private KitsCooldownData m_KitsCooldownData = null!; + private IDisposable? m_FileWatcher; + + private IDataStore DataStore => m_Plugin.DataStore; + + public DataStoreKitCooldownStoreProvider(KitsPlugin plugin) + { + m_Plugin = plugin; + } + + public Task GetLastCooldownAsync(IPermissionActor actor, string kitName) + { + if (!m_KitsCooldownData.KitsCooldown!.TryGetValue(actor.Id, out var kitCooldowns)) + { + return Task.FromResult(null); + } + + var kitCooldown = kitCooldowns!.Find(x => + x.KitName?.Equals(kitName, StringComparison.CurrentCultureIgnoreCase) == true); + return Task.FromResult(kitCooldown == null ? null : DateTime.Now - kitCooldown.KitCooldown); + } + + public async Task RegisterCooldownAsync(IPermissionActor actor, string kitName, DateTime time) + { + if (m_KitsCooldownData.KitsCooldown!.TryGetValue(actor.Id, out var kitCooldowns)) + { + var kitCooldown = kitCooldowns!.Find(x => x.KitName == kitName); + if (kitCooldown == null) + { + kitCooldown = new() { KitName = kitName }; + kitCooldowns.Add(kitCooldown); + } + + kitCooldown.KitCooldown = time; + } + else + { + m_KitsCooldownData.KitsCooldown.Add(actor.Id, + new() { new() { KitCooldown = time, KitName = kitName } }); + } + await SaveData(); + } + + private async Task LoadFromDisk() + { + if (await DataStore.ExistsAsync(c_CooldownKey)) + { + m_KitsCooldownData = await DataStore.LoadAsync(c_CooldownKey) ?? + new() { KitsCooldown = new() }; + } + else + { + m_KitsCooldownData = new() { KitsCooldown = new() }; + await SaveData(); + } + } + + public async Task InitAsync() + { + await LoadFromDisk(); + m_FileWatcher = DataStore.AddChangeWatcher(c_CooldownKey, m_Plugin, + () => AsyncHelper.RunSync(LoadFromDisk)); + } + + private Task SaveData() + { + return DataStore.SaveAsync(c_CooldownKey, m_KitsCooldownData); + } + + public void Dispose() + { + m_FileWatcher?.Dispose(); + m_FileWatcher = null; + } +} diff --git a/Kits/Cooldowns/Providers/MySqlKitCooldownStoreProvider.cs b/Kits/Cooldowns/Providers/MySqlKitCooldownStoreProvider.cs new file mode 100644 index 0000000..4b6542f --- /dev/null +++ b/Kits/Cooldowns/Providers/MySqlKitCooldownStoreProvider.cs @@ -0,0 +1,72 @@ +using Autofac; +using Kits.API.Cooldowns; +using Kits.Cooldowns.MySql; +using Microsoft.EntityFrameworkCore; +using OpenMod.API.Permissions; +using OpenMod.Core.Users; +using System; +using System.Threading.Tasks; + +namespace Kits.Cooldowns.Providers; +public class MySqlKitCooldownStoreProvider : IKitCooldownStoreProvider +{ + private readonly ILifetimeScope m_LifetimeScope; + + public MySqlKitCooldownStoreProvider(ILifetimeScope lifetimeScope) + { + m_LifetimeScope = lifetimeScope; + } + + public async Task InitAsync() + { + await using var context = GetDbContext(); + await context.Database.MigrateAsync(); + } + + public async Task GetLastCooldownAsync(IPermissionActor actor, string kitName) + { + EnsureActorIsPlayer(actor); + + await using var context = GetDbContext(); + DateTime? usedTime = (await context.KitCooldowns.FirstOrDefaultAsync(c => c.Kit == kitName && c.PlayerId == actor.Id)) + ?.UsedTime; + + return usedTime == null ? null : DateTime.UtcNow - usedTime; + } + + public async Task RegisterCooldownAsync(IPermissionActor actor, string kitName, DateTime time) + { + EnsureActorIsPlayer(actor); + + await using var context = GetDbContext(); + + var cooldown = await context.KitCooldowns.FirstOrDefaultAsync(c => c.Kit == kitName && c.PlayerId == actor.Id); + if (cooldown == null) + { + cooldown = new() + { + Kit = kitName, + PlayerId = actor.Id, + UsedTime = time + }; + + context.KitCooldowns.Add(cooldown); + } + else + { + cooldown.UsedTime = time; + } + + await context.SaveChangesAsync(); + } + + private void EnsureActorIsPlayer(IPermissionActor actor) + { + if (!actor.Type.Equals(KnownActorTypes.Player, StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("Cooldowns are only handled for Player actor type"); + } + } + + protected virtual KitCooldownsDbContext GetDbContext() => m_LifetimeScope.Resolve(); +} diff --git a/Kits/Databases/DataStore/KitsData.cs b/Kits/Databases/DataStore/KitsData.cs new file mode 100644 index 0000000..d6a0f70 --- /dev/null +++ b/Kits/Databases/DataStore/KitsData.cs @@ -0,0 +1,9 @@ +using Kits.API.Models; +using System.Collections.Generic; + +namespace Kits.Databases.DataStore; + +public class KitsData +{ + public List? Kits { get; set; } +} \ No newline at end of file diff --git a/Kits/Databases/DataStoreKitDatabase.cs b/Kits/Databases/DataStoreKitDatabase.cs deleted file mode 100644 index 6903619..0000000 --- a/Kits/Databases/DataStoreKitDatabase.cs +++ /dev/null @@ -1,115 +0,0 @@ -extern alias JetBrainsAnnotations; -using Kits.API; -using Kits.Models; -using OpenMod.API.Commands; -using OpenMod.Core.Helpers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Kits.Databases -{ - public class DataStoreKitDatabase : KitDatabaseCore, IKitDatabase, IDisposable - { - private const string c_KitsKey = "kits"; - - private KitsData m_Data = null!; - private IDisposable? m_FileWatcher; - - public DataStoreKitDatabase(Kits plugin) : base(plugin) - { - } - - public async Task AddKitAsync(Kit kit) - { - if (kit is null) - { - throw new ArgumentNullException(nameof(kit)); - } - - if (m_Data.Kits.Any(x => x.Name.Equals(kit.Name, StringComparison.OrdinalIgnoreCase))) - { - throw new UserFriendlyException(StringLocalizer["commands:kit:exist"]); - } - - m_Data.Kits?.Add(kit); - await SaveToDisk(); - return true; - } - - public Task FindKitByNameAsync(string name) - { - return Task.FromResult(m_Data.Kits?.Find(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))); - } - - public Task> GetKitsAsync() - { - return Task.FromResult((IReadOnlyCollection)(m_Data.Kits ?? new())); - } - - public async Task LoadDatabaseAsync() - { - await LoadFromDisk(); - - m_FileWatcher = Plugin.DataStore.AddChangeWatcher(c_KitsKey, Plugin, - () => AsyncHelper.RunSync(LoadFromDisk)); - - await SaveToDisk(); - } - - private async Task LoadFromDisk() - { - if (await Plugin.DataStore.ExistsAsync(c_KitsKey)) - { - m_Data = await Plugin.DataStore.LoadAsync(c_KitsKey) ?? new() { Kits = new() }; - } - else - { - m_Data = new() { Kits = new() }; - } - } - - public async Task RemoveKitAsync(string name) - { - var index = m_Data.Kits?.FindIndex(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (index < 0) - { - throw new UserFriendlyException(StringLocalizer["commands:kit:remove:fail", new { Name = name }]); - } - - m_Data.Kits?.RemoveAt(index!.Value); - await SaveToDisk(); - return true; - } - - public async Task UpdateKitAsync(Kit kit) - { - if (kit is null) - { - throw new ArgumentNullException(nameof(kit)); - } - - var index = m_Data.Kits?.FindIndex( - x => x.Name.Equals(kit.Name, StringComparison.OrdinalIgnoreCase)); - if (index < 0) - { - return false; - } - - m_Data.Kits![index!.Value] = kit; - await SaveToDisk(); - return true; - } - - public void Dispose() - { - m_FileWatcher?.Dispose(); - } - - private Task SaveToDisk() - { - return Plugin.DataStore.SaveAsync(c_KitsKey, m_Data); - } - } -} \ No newline at end of file diff --git a/Kits/Databases/DataStoreKitStoreProvider.cs b/Kits/Databases/DataStoreKitStoreProvider.cs new file mode 100644 index 0000000..1ef5111 --- /dev/null +++ b/Kits/Databases/DataStoreKitStoreProvider.cs @@ -0,0 +1,139 @@ +using Autofac; +using Kits.API.Databases; +using Kits.API.Models; +using Kits.Databases.DataStore; +using OpenMod.API; +using OpenMod.API.Commands; +using OpenMod.Core.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Kits.Databases; + +public class DataStoreKitStoreProvider : KitStoreProviderCore, IKitStoreProvider, IDisposable +{ + private const string c_KitsKey = "kits"; + + private KitsData m_Data = null!; + private IDisposable? m_FileWatcher; + + public DataStoreKitStoreProvider(ILifetimeScope lifetimeScope) : base(lifetimeScope) + { + } + + public async Task InitAsync() + { + await LoadFromDisk(); + + var component = LifetimeScope.Resolve(); + m_FileWatcher = DataStore.AddChangeWatcher(c_KitsKey, component, + () => AsyncHelper.RunSync(LoadFromDisk)); + } + + private async Task LoadFromDisk() + { + if (await DataStore.ExistsAsync(c_KitsKey)) + { + m_Data = await DataStore.LoadAsync(c_KitsKey) ?? new(); + m_Data.Kits ??= new(); + return; + } + + m_Data = new() { Kits = new() }; + await SaveToDisk(); + } + + public async Task AddKitAsync(Kit kit) + { + if (kit is null) + { + throw new ArgumentNullException(nameof(kit)); + } + + if (m_Data.Kits.Any(x => x.Name.Equals(kit.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new UserFriendlyException(StringLocalizer["commands:kit:exist"]); + } + + // Tries to fix the issue when sometimes state of a item resetting to zero + // https://github.com/EvolutionPlugins/Kits/issues/7 + if (kit.Items != null) + { + foreach (var item in kit.Items.Where(x => x?.State != null)) + { + item.State.StateData = item.State.StateData?.ToArray() ?? Array.Empty(); + } + } + + m_Data.Kits?.Add(kit); + await SaveToDisk(); + } + + public Task FindKitByNameAsync(string name) + { + return Task.FromResult(m_Data.Kits?.Find(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))); + } + + public Task> GetKitsAsync() + { + return Task.FromResult((IReadOnlyCollection)(m_Data.Kits ?? new())); + } + + public async Task RemoveKitAsync(string name) + { + var index = m_Data.Kits?.FindIndex(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + throw new UserFriendlyException(StringLocalizer["commands:kit:remove:fail", new { Name = name }]); + } + + m_Data.Kits?.RemoveAt(index!.Value); + await SaveToDisk(); + } + + public async Task UpdateKitAsync(Kit kit) + { + if (kit is null) + { + throw new ArgumentNullException(nameof(kit)); + } + + var index = m_Data.Kits?.FindIndex( + x => x.Name.Equals(kit.Name, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + return; + } + + // Tries to fix the issue when sometimes state of a item resetting to zero + // https://github.com/EvolutionPlugins/Kits/issues/7 + if (kit.Items != null) + { + foreach (var item in kit.Items.Where(x => x?.State != null)) + { + item.State.StateData = item.State.StateData?.ToArray() ?? Array.Empty(); + } + } + + m_Data.Kits![index!.Value] = kit; + await SaveToDisk(); + } + + public Task IsKitExists(string name) + { + return Task.FromResult(m_Data.Kits?.Find(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) != null); + } + + private Task SaveToDisk() + { + return DataStore.SaveAsync(c_KitsKey, m_Data); + } + + public void Dispose() + { + m_FileWatcher?.Dispose(); + m_FileWatcher = null; + } +} \ No newline at end of file diff --git a/Kits/Databases/KitDatabaseCore.cs b/Kits/Databases/KitDatabaseCore.cs deleted file mode 100644 index a690fe2..0000000 --- a/Kits/Databases/KitDatabaseCore.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Autofac; -using Microsoft.Extensions.Localization; - -namespace Kits.Databases -{ - public abstract class KitDatabaseCore - { - protected Kits Plugin { get; } - protected virtual string TableName => Plugin.Configuration["database:connectionTableName"]; - protected virtual string Connection => Plugin.Configuration["database:connection"]; - protected IStringLocalizer StringLocalizer { get; } - - protected KitDatabaseCore(Kits plugin) - { - StringLocalizer = plugin.LifetimeScope.Resolve(); - Plugin = plugin; - } - } -} \ No newline at end of file diff --git a/Kits/Databases/KitStoreProviderCore.cs b/Kits/Databases/KitStoreProviderCore.cs new file mode 100644 index 0000000..ab698df --- /dev/null +++ b/Kits/Databases/KitStoreProviderCore.cs @@ -0,0 +1,19 @@ +using Autofac; +using Microsoft.Extensions.Localization; +using OpenMod.API.Persistence; + +namespace Kits.Databases; + +public abstract class KitStoreProviderCore +{ + protected IDataStore DataStore { get; } + protected IStringLocalizer StringLocalizer { get; } + protected ILifetimeScope LifetimeScope { get; } + + protected KitStoreProviderCore(ILifetimeScope lifetimeScope) + { + StringLocalizer = lifetimeScope.Resolve(); + DataStore = lifetimeScope.Resolve(); + LifetimeScope = lifetimeScope; + } +} \ No newline at end of file diff --git a/Kits/Databases/MySqlKitDatabase.cs b/Kits/Databases/MySqlKitDatabase.cs deleted file mode 100644 index 8c93c95..0000000 --- a/Kits/Databases/MySqlKitDatabase.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Autofac; -using Kits.API; -using Kits.Databases.Mysql; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using OpenMod.API.Commands; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Kits.Databases -{ - public class MySqlKitDatabase : KitDatabaseCore, IKitDatabase - { - public MySqlKitDatabase(IServiceProvider provider) : this(provider.GetRequiredService()) - { - } - - public MySqlKitDatabase(Kits plugin) : base(plugin) - { - } - - public virtual KitsDbContext GetDbContext() => Plugin.LifetimeScope.Resolve(); - - public async Task LoadDatabaseAsync() - { - await using var context = GetDbContext(); - await context.Database.MigrateAsync(); - } - - public async Task AddKitAsync(Kit kit) - { - await using var context = GetDbContext(); - - if (await context.Kits.Where(x => x.Name.Equals(kit.Name, StringComparison.OrdinalIgnoreCase)).AnyAsync()) - { - throw new UserFriendlyException(StringLocalizer["commands:kit:exist"]); - } - - context.Kits.Add(kit); - return await context.SaveChangesAsync() > 0; - } - - public async Task FindKitByNameAsync(string name) - { - await using var context = GetDbContext(); - - return await context.Kits - .Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefaultAsync(); - } - - public async Task> GetKitsAsync() - { - await using var context = GetDbContext(); - return await context.Kits.ToListAsync(); - } - - public async Task RemoveKitAsync(string name) - { - await using var context = GetDbContext(); - - var kit = await context.Kits - .Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefaultAsync(); - if (kit == null) - { - return false; - } - - context.Kits.Remove(kit); - - return await context.SaveChangesAsync() > 0; - } - - public async Task UpdateKitAsync(Kit kit) - { - await using var context = GetDbContext(); - - context.Kits.Update(kit); - return await context.SaveChangesAsync() > 0; - } - } -} \ No newline at end of file diff --git a/Kits/Databases/MySqlKitStoreProvider.cs b/Kits/Databases/MySqlKitStoreProvider.cs new file mode 100644 index 0000000..c6ef95f --- /dev/null +++ b/Kits/Databases/MySqlKitStoreProvider.cs @@ -0,0 +1,182 @@ +using Autofac; +using Kits.API.Databases; +using Kits.API.Models; +using Kits.Databases.MySql; +using Kits.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using OpenMod.API.Commands; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Kits.Databases; + +public class MySqlKitStoreProvider : KitStoreProviderCore, IKitStoreProvider, IDisposable +{ + private const string c_CacheKey = "evolutionplugins-kits-kits"; + private readonly IMemoryCache m_MemoryCache; + private readonly AsyncLock m_AsyncLock = new(); + + public MySqlKitStoreProvider(ILifetimeScope lifetimeScope, IMemoryCache memoryCache) : base(lifetimeScope) + { + m_MemoryCache = memoryCache; + } + + protected virtual KitsDbContext GetDbContext() => LifetimeScope.Resolve(); + + public async Task InitAsync() + { + await using var context = GetDbContext(); + await context.Database.MigrateAsync(); + } + + public async Task AddKitAsync(Kit kit) + { + await using var context = GetDbContext(); + + if (await context.Kits.AnyAsync(x => x.Name == kit.Name)) + { + throw new UserFriendlyException(StringLocalizer["commands:kit:exist"]); + } + + context.Kits.Add(kit); + await context.SaveChangesAsync(); + + if (m_MemoryCache.TryGetValue>(c_CacheKey, out var kits)) + { + using var _ = await m_AsyncLock.GetLockAsync(); + kits.Add(kit); + } + } + + public async Task FindKitByNameAsync(string name) + { + if (TryGetCachedKits(out var kits)) + { + using var _ = await m_AsyncLock.GetLockAsync(); + return kits.Find(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + } + + await using var context = GetDbContext(); + + return await context.Kits + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Name == name); + } + + public async Task> GetKitsAsync() + { + return await GetOrCreatedCachedListOfKitsAsync(); + } + + public async Task RemoveKitAsync(string name) + { + await using var context = GetDbContext(); + + var kit = await context.Kits + .FirstOrDefaultAsync(x => x.Name == name); + if (kit == null) + { + return; + } + + context.Kits.Remove(kit); + await context.SaveChangesAsync(); + + if (!TryGetCachedKits(out var kits)) + { + return; + } + + using var _ = await m_AsyncLock.GetLockAsync(); + + var kitIndex = kits.FindIndex(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + if (kitIndex == -1) + { + return; + } + + kits.RemoveAt(kitIndex); + } + + public async Task UpdateKitAsync(Kit kit) + { + await using var context = GetDbContext(); + + // start to track the old kit + var oldKit = await context.Kits.FindAsync(kit.Id); + + // update values + UpdateValues(oldKit, kit); + + await context.SaveChangesAsync(); + + if (!TryGetCachedKits(out var kits)) + { + return; + } + + using var _ = await m_AsyncLock.GetLockAsync(); + var newOldKit = kits.Find(x => x.Name.Equals(kit.Name, StringComparison.InvariantCultureIgnoreCase)); + if (newOldKit == null) + { + return; + } + + UpdateValues(newOldKit, kit); + + static void UpdateValues(Kit oldKit, Kit newKit) + { + oldKit.Name = newKit.Name; + oldKit.Cooldown = newKit.Cooldown; + oldKit.Cost = newKit.Cost; + oldKit.Money = newKit.Money; + oldKit.VehicleId = newKit.VehicleId; + oldKit.Items = newKit.Items; + } + } + + public async Task IsKitExists(string name) + { + if (TryGetCachedKits(out var kits)) + { + using var _ = await m_AsyncLock.GetLockAsync(); + return kits.Exists(x => x.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)); + } + + await using var context = GetDbContext(); + return await context.Kits.AnyAsync(x => x.Name == name); + } + + private bool TryGetCachedKits(out List kits) + { + return m_MemoryCache.TryGetValue(c_CacheKey, out kits); + } + + private async Task> GetOrCreatedCachedListOfKitsAsync() + { + if (!TryGetCachedKits(out var kits)) + { + using var _ = await m_AsyncLock.GetLockAsync(); + if (TryGetCachedKits(out kits)) + { + return kits; + } + + await using var context = GetDbContext(); + kits = await context.Kits.ToListAsync(); + + // todo: maybe configure the cache time + m_MemoryCache.Set(c_CacheKey, kits, TimeSpan.FromHours(1)); + } + + return kits; + } + + public void Dispose() + { + m_AsyncLock.Dispose(); + } +} \ No newline at end of file diff --git a/Kits/Databases/Mysql/KitsDbContext.cs b/Kits/Databases/Mysql/KitsDbContext.cs index 8eac7a0..135dd12 100644 --- a/Kits/Databases/Mysql/KitsDbContext.cs +++ b/Kits/Databases/Mysql/KitsDbContext.cs @@ -1,44 +1,45 @@ -using Kits.API; +using Kits.API.Models; using Kits.Extensions; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.ChangeTracking; using OpenMod.EntityFrameworkCore; using OpenMod.EntityFrameworkCore.Configurator; using System; +using System.Collections.Generic; +using System.Linq; -namespace Kits.Databases.Mysql +namespace Kits.Databases.MySql; + +public class KitsDbContext : OpenModDbContext { - public class KitsDbContext : OpenModDbContext + public DbSet Kits => Set(); + + public KitsDbContext(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public KitsDbContext(IDbContextConfigurator configurator, IServiceProvider serviceProvider) : base(configurator, serviceProvider) { - public DbSet Kits - { - get - { - return Set(); - } - } - - public KitsDbContext(IServiceProvider serviceProvider) : base(serviceProvider) - { - } - - public KitsDbContext(IDbContextConfigurator configurator, IServiceProvider serviceProvider) : base(configurator, serviceProvider) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.ApplyConfiguration(new KitConfiguration()); - base.OnModelCreating(modelBuilder); - } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); // https://stackoverflow.com/questions/44829824/how-to-store-json-in-an-entity-field-with-ef-core - public class KitConfiguration : IEntityTypeConfiguration - { - public virtual void Configure(EntityTypeBuilder builder) - { - builder.Property(c => c.Items).HasByteArrayConversion(); - } - } + var property = modelBuilder + .Entity() + .Property(x => x.Items); + + property.HasConversion( + v => v.ConvertToByteArray(), + v => v.ConvertToKitItems()); + + var comparer = new ValueComparer?>( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, i) => HashCode.Combine(a, i.GetHashCode())), + c => c.ToList()); + + property.Metadata.SetValueComparer(comparer); } } diff --git a/Kits/Databases/Mysql/KitsDbContextFactory.cs b/Kits/Databases/Mysql/KitsDbContextFactory.cs index 8fe37b2..1328e26 100644 --- a/Kits/Databases/Mysql/KitsDbContextFactory.cs +++ b/Kits/Databases/Mysql/KitsDbContextFactory.cs @@ -1,8 +1,7 @@ using OpenMod.EntityFrameworkCore.MySql; -namespace Kits.Databases.Mysql +namespace Kits.Databases.MySql; + +public class KitsDbContextFactory : OpenModMySqlDbContextFactory { - public class KitsDbContextFactory : OpenModMySqlDbContextFactory - { - } } diff --git a/Kits/Extensions/ConvertorExtension.cs b/Kits/Extensions/ConvertorExtension.cs index 34dbbcd..8c4c710 100644 --- a/Kits/Extensions/ConvertorExtension.cs +++ b/Kits/Extensions/ConvertorExtension.cs @@ -1,92 +1,61 @@ -using Kits.API; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Kits.API.Models; using OpenMod.Extensions.Games.Abstractions.Items; -using System; using System.Collections.Generic; using System.IO; -namespace Kits.Extensions -{ - public static class ConvertorExtension - { - public static readonly byte s_SaveVersion = 1; +namespace Kits.Extensions; - public static KitItem ConvertIItemToKitItem(this IItem item) - { - return new(item.Asset.ItemAssetId, item.State); - } +internal static class ConvertorExtension +{ + public static readonly byte s_SaveVersion = 1; - public static byte[] ConvertToByteArray(this List? items) - { - if (items == null) - { - return Array.Empty(); - } + public static KitItem ConvertIItemToKitItem(this IItem item) + { + return new(item.Asset.ItemAssetId, item.State); + } - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); + public static byte[] ConvertToByteArray(this List? items) + { + using var ms = new MemoryStream(); + using var bw = new BinaryWriter(ms); - bw.Write(s_SaveVersion); - bw.Write(items.Count); + bw.Write(s_SaveVersion); + bw.Write(items?.Count ?? 0); + if (items != null) + { foreach (var item in items) { item.Serialize(bw); } - - return ms.ToArray(); } - public static List ConvertToKitItems(this byte[]? block) - { - var output = new List(); - - if (block == null) - { - return output; - } - - using var ms = new MemoryStream(block, false); - using var br = new BinaryReader(ms); + return ms.ToArray(); + } - br.ReadByte(); // save version, for now ignored + public static List? ConvertToKitItems(this byte[]? block) + { + if (block == null || block.Length == 0) + { + return null; + } - var count = br.ReadInt32(); - for (var i = 0; i < count; i++) - { - var kitItem = new KitItem(); - kitItem.Deserialize(br); + var output = new List(); - output.Add(kitItem); - } + using var ms = new MemoryStream(block, false); + using var br = new BinaryReader(ms); - return output; - } + br.ReadByte(); // save version, for now ignored - // https://stackoverflow.com/questions/44829824/how-to-store-json-in-an-entity-field-with-ef-core - public static PropertyBuilder?> HasByteArrayConversion(this PropertyBuilder?> propertyBuilder) + var count = br.ReadInt32(); + for (var i = 0; i < count; i++) { - var converter = new ValueConverter?, byte[]> - ( - v => v.ConvertToByteArray(), - v => v.ConvertToKitItems() - ); + var kitItem = new KitItem(); + kitItem.Deserialize(br); - /* var comparer = new ValueComparer?> - ( - (l, r) => l == r, - v => v == null ? 0 : v.GetHashCode(), - v => v.ConvertToByteArray().ConvertToKitItems() - );*/ - - propertyBuilder.HasConversion(converter); - propertyBuilder.Metadata.SetValueConverter(converter); - //propertyBuilder.Metadata.SetValueComparer(comparer); - //propertyBuilder.HasColumnType("longblob"); - - return propertyBuilder; + output.Add(kitItem); } + + return output; } } \ No newline at end of file diff --git a/Kits/Extensions/UnturnedExtension.cs b/Kits/Extensions/UnturnedExtension.cs index 214c945..d75c1d5 100644 --- a/Kits/Extensions/UnturnedExtension.cs +++ b/Kits/Extensions/UnturnedExtension.cs @@ -1,128 +1,128 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Kits.API; +using Kits.API.Models; using OpenMod.Extensions.Games.Abstractions.Players; using SDG.Unturned; using Steamworks; +using System; +using System.Collections.Generic; +using System.Linq; -namespace Kits.Extensions +namespace Kits.Extensions; + +// todo: remove this when https://github.com/openmod/openmod/issues/355 got implemented +internal static class UnturnedExtension { - public static class UnturnedExtension - { - private static readonly bool s_IsUnturned = AppDomain.CurrentDomain.GetAssemblies() - .Any(x => x.GetName().Name.Equals("OpenMod.Unturned")); + private static readonly bool s_IsUnturned = AppDomain.CurrentDomain.GetAssemblies() + .Any(x => x.GetName().Name.Equals("OpenMod.Unturned")); - public static void AddClothes(IPlayerUser user, IList kitsItems) + public static void AddClothes(IPlayerUser user, IList kitsItems) + { + if (!s_IsUnturned) { - if (!s_IsUnturned) - { - return; - } + return; + } - if (!ulong.TryParse(user.Id, out var id)) - { - return; - } + if (!ulong.TryParse(user.Id, out var id)) + { + return; + } - var player = PlayerTool.getPlayer((CSteamID)id); - if (player == null) - { - return; - } + var player = PlayerTool.getPlayer((CSteamID)id); + if (player == null) + { + return; + } - var clothing = player.clothing; + var clothing = player.clothing; - if (clothing.hat != 0) - { - kitsItems.Insert(0, - new(clothing.hat.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.hatQuality, - ItemQuality = clothing.hatQuality, - StateData = clothing.hatState - })); - } + if (clothing.hat != 0) + { + kitsItems.Insert(0, + new(clothing.hat.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.hatQuality, + ItemQuality = clothing.hatQuality, + StateData = clothing.hatState + })); + } - if (clothing.glasses != 0) - { - kitsItems.Insert(0, - new(clothing.glasses.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.glassesQuality, - ItemQuality = clothing.glassesQuality, - StateData = clothing.glassesState - })); - } + if (clothing.glasses != 0) + { + kitsItems.Insert(0, + new(clothing.glasses.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.glassesQuality, + ItemQuality = clothing.glassesQuality, + StateData = clothing.glassesState + })); + } - if (clothing.mask != 0) - { - kitsItems.Insert(0, - new(clothing.mask.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.maskQuality, - ItemQuality = clothing.maskQuality, - StateData = clothing.maskState - })); - } + if (clothing.mask != 0) + { + kitsItems.Insert(0, + new(clothing.mask.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.maskQuality, + ItemQuality = clothing.maskQuality, + StateData = clothing.maskState + })); + } - if (clothing.pants != 0) - { - kitsItems.Insert(0, - new(clothing.pants.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.pantsQuality, - ItemQuality = clothing.pantsQuality, - StateData = clothing.pantsState - })); - } + if (clothing.pants != 0) + { + kitsItems.Insert(0, + new(clothing.pants.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.pantsQuality, + ItemQuality = clothing.pantsQuality, + StateData = clothing.pantsState + })); + } - if (clothing.vest != 0) - { - kitsItems.Insert(0, - new(clothing.vest.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.vestQuality, - ItemQuality = clothing.vestQuality, - StateData = clothing.vestState - })); - } + if (clothing.vest != 0) + { + kitsItems.Insert(0, + new(clothing.vest.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.vestQuality, + ItemQuality = clothing.vestQuality, + StateData = clothing.vestState + })); + } - if (clothing.shirt != 0) - { - kitsItems.Insert(0, - new(clothing.shirt.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.shirtQuality, - ItemQuality = clothing.shirtQuality, - StateData = clothing.shirtState - })); - } + if (clothing.shirt != 0) + { + kitsItems.Insert(0, + new(clothing.shirt.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.shirtQuality, + ItemQuality = clothing.shirtQuality, + StateData = clothing.shirtState + })); + } - if (clothing.backpack != 0) - { - kitsItems.Insert(0, - new(clothing.backpack.ToString(), - new KitItemState() - { - ItemAmount = 1, - ItemDurability = clothing.backpackQuality, - ItemQuality = clothing.backpackQuality, - StateData = clothing.backpackState - })); - } + if (clothing.backpack != 0) + { + kitsItems.Insert(0, + new(clothing.backpack.ToString(), + new KitItemState() + { + ItemAmount = 1, + ItemDurability = clothing.backpackQuality, + ItemQuality = clothing.backpackQuality, + StateData = clothing.backpackState + })); } } } \ No newline at end of file diff --git a/Kits/Helpers/AsyncLock.cs b/Kits/Helpers/AsyncLock.cs new file mode 100644 index 0000000..60aee10 --- /dev/null +++ b/Kits/Helpers/AsyncLock.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Kits.Helpers; +internal class AsyncLock : IDisposable +{ + private readonly SemaphoreSlim m_SemaphoreSlim; + private readonly ReleaserLock m_ReleaserLock; + + public AsyncLock() + { + m_SemaphoreSlim = new SemaphoreSlim(1); + m_ReleaserLock = new ReleaserLock(this); + } + + public async ValueTask GetLockAsync() + { + await m_SemaphoreSlim.WaitAsync(); + return m_ReleaserLock; + } + + public ReleaserLock GetLock() + { + m_SemaphoreSlim.Wait(); + return m_ReleaserLock; + } + + internal void Release() + { + m_SemaphoreSlim.Release(); + } + + public void Dispose() + { + m_SemaphoreSlim.Dispose(); + } + + public readonly struct ReleaserLock : IDisposable + { + private readonly AsyncLock m_Lock; + + internal ReleaserLock(AsyncLock @lock) + { + m_Lock = @lock; + } + + public readonly void Dispose() + { + m_Lock.Release(); + } + } +} diff --git a/Kits/Kits.cs b/Kits/Kits.cs deleted file mode 100644 index 10ba55e..0000000 --- a/Kits/Kits.cs +++ /dev/null @@ -1,38 +0,0 @@ -using JetBrains.Annotations; -using Kits.API; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using OpenMod.API.Plugins; -using OpenMod.Core.Plugins; -using System; -using System.Threading.Tasks; - -[assembly: PluginMetadata("Kits", DisplayName = "Kits", Author = "EvolutionPlugins", - Website = "https://discord.gg/6KymqGv")] - -namespace Kits -{ - [UsedImplicitly] - public class Kits : OpenModUniversalPlugin - { - private readonly ILogger m_Logger; - private readonly IServiceProvider m_ServiceProvider; - - public Kits(ILogger logger, IServiceProvider serviceProvider) : base(serviceProvider) - { - m_Logger = logger; - m_ServiceProvider = serviceProvider; - } - - protected override Task OnLoadAsync() - { - m_Logger.LogInformation("Made with <3 by EvolutionPlugins"); - m_Logger.LogInformation("https://github.com/evolutionplugins \\ https://github.com/diffoz"); - m_Logger.LogInformation("Discord support: https://discord.gg/6KymqGv"); - - m_ServiceProvider.GetRequiredService(); /* just load database service */ - - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/Kits/Kits.csproj b/Kits/Kits.csproj index 5090a29..72df0ae 100644 --- a/Kits/Kits.csproj +++ b/Kits/Kits.csproj @@ -9,7 +9,7 @@ true true Kits - latest + 10 enable nullable 0.0.0 @@ -22,26 +22,23 @@ https://github.com/DiFFoZ/Kits git https://github.com/DiFFoZ/Kits - true - true + README.md - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + @@ -49,16 +46,12 @@ + + + + - + - - - - JetBrainsAnnotations - - - - \ No newline at end of file diff --git a/Kits/KitsPlugin.cs b/Kits/KitsPlugin.cs new file mode 100644 index 0000000..703b6c1 --- /dev/null +++ b/Kits/KitsPlugin.cs @@ -0,0 +1,30 @@ +using Kits.API; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenMod.API.Plugins; +using OpenMod.Core.Plugins; +using System; +using System.Threading.Tasks; + +[assembly: PluginMetadata("Kits", DisplayName = "Kits", Author = "EvolutionPlugins", + Website = "https://discord.gg/6KymqGv")] + +namespace Kits; + +public class KitsPlugin : OpenModUniversalPlugin +{ + public KitsPlugin(IServiceProvider serviceProvider) : base(serviceProvider) + { + // loads the provider to add kits permission to help.md + _ = serviceProvider.GetRequiredService(); + } + + protected override Task OnLoadAsync() + { + Logger.LogInformation("Made with <3 by EvolutionPlugins"); + Logger.LogInformation("https://github.com/evolutionplugins \\ https://github.com/diffoz"); + Logger.LogInformation("Discord support: https://discord.gg/6KymqGv"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Kits/Migrations/20210623164103_Initial.Designer.cs b/Kits/Migrations/20210623164103_Initial.Designer.cs index 841de86..4c9d8a8 100644 --- a/Kits/Migrations/20210623164103_Initial.Designer.cs +++ b/Kits/Migrations/20210623164103_Initial.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Kits.Databases.Mysql; +using Kits.Databases.MySql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/Kits/Migrations/20210623164103_Initial.cs b/Kits/Migrations/20210623164103_Initial.cs index 045b6d8..ca07516 100644 --- a/Kits/Migrations/20210623164103_Initial.cs +++ b/Kits/Migrations/20210623164103_Initial.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; namespace Kits.Migrations diff --git a/Kits/Migrations/KitsDbContextModelSnapshot.cs b/Kits/Migrations/KitsDbContextModelSnapshot.cs index ef2e332..8d32938 100644 --- a/Kits/Migrations/KitsDbContextModelSnapshot.cs +++ b/Kits/Migrations/KitsDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using Kits.Databases.Mysql; +using Kits.Databases.MySql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/Kits/Models/KitCooldownData.cs b/Kits/Models/KitCooldownData.cs deleted file mode 100644 index 3a6f8f0..0000000 --- a/Kits/Models/KitCooldownData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Kits.Models -{ - public class KitCooldownData - { - public string? KitName { get; set; } - public DateTime KitCooldown { get; set; } - } -} \ No newline at end of file diff --git a/Kits/Models/KitsCooldownData.cs b/Kits/Models/KitsCooldownData.cs deleted file mode 100644 index 859bcb1..0000000 --- a/Kits/Models/KitsCooldownData.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Kits.Models -{ - public class KitsCooldownData - { - public Dictionary>? KitsCooldown { get; set; } - } -} \ No newline at end of file diff --git a/Kits/Models/KitsData.cs b/Kits/Models/KitsData.cs deleted file mode 100644 index 9744024..0000000 --- a/Kits/Models/KitsData.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Kits.API; -using System.Collections.Generic; - -namespace Kits.Models -{ - public class KitsData - { - public List? Kits { get; set; } - } -} \ No newline at end of file diff --git a/Kits/PluginContainerConfigurator.cs b/Kits/PluginContainerConfigurator.cs index a67ae84..2523def 100644 --- a/Kits/PluginContainerConfigurator.cs +++ b/Kits/PluginContainerConfigurator.cs @@ -1,14 +1,15 @@ -using Kits.Databases.Mysql; +using Kits.Cooldowns.MySql; +using Kits.Databases.MySql; using OpenMod.API.Plugins; using OpenMod.EntityFrameworkCore.MySql.Extensions; -namespace Kits +namespace Kits; + +public class PluginContainerConfigurator : IPluginContainerConfigurator { - public class PluginContainerConfigurator : IPluginContainerConfigurator + public void ConfigureContainer(IPluginServiceConfigurationContext context) { - public void ConfigureContainer(IPluginServiceConfigurationContext context) - { - context.ContainerBuilder.AddMySqlDbContext(); - } + context.ContainerBuilder.AddMySqlDbContext(); + context.ContainerBuilder.AddMySqlDbContext(); } } diff --git a/Kits/ServiceConfigurator.cs b/Kits/ServiceConfigurator.cs index 71f1c53..48e1c0b 100644 --- a/Kits/ServiceConfigurator.cs +++ b/Kits/ServiceConfigurator.cs @@ -1,20 +1,34 @@ -extern alias JetBrainsAnnotations; -using EvolutionPlugins.Economy.Stub; -using JetBrainsAnnotations::JetBrains.Annotations; +using EvolutionPlugins.Economy.Stub; +using Kits.API.Cooldowns; +using Kits.API.Databases; +using Kits.Cooldowns.Providers; +using Kits.Databases; using Microsoft.Extensions.DependencyInjection; using OpenMod.API.Ioc; using OpenMod.API.Prioritization; -namespace Kits +namespace Kits; + +[Priority(Priority = Priority.Lowest)] +public class ServiceConfigurator : IServiceConfigurator { - [UsedImplicitly] - [Priority(Priority = Priority.Lowest)] - public class ServiceConfigurator : IServiceConfigurator + public void ConfigureServices(IOpenModServiceConfigurationContext openModStartupContext, + IServiceCollection serviceCollection) { - public void ConfigureServices(IOpenModServiceConfigurationContext openModStartupContext, - IServiceCollection serviceCollection) + serviceCollection.AddEconomyStub(); + + serviceCollection.Configure(o => { - serviceCollection.AddEconomyStub(); - } + o.AddProvider("datastore"); + o.AddProvider("mysql"); + }); + + serviceCollection.Configure(o => + { + o.AddProvider("datastore"); + o.AddProvider("mysql"); + }); + + serviceCollection.AddMemoryCache(); } } \ No newline at end of file diff --git a/Kits/Services/KitCooldownStore.cs b/Kits/Services/KitCooldownStore.cs deleted file mode 100644 index 49646ab..0000000 --- a/Kits/Services/KitCooldownStore.cs +++ /dev/null @@ -1,116 +0,0 @@ -using JetBrains.Annotations; -using Kits.API; -using Kits.Models; -using Microsoft.Extensions.DependencyInjection; -using OpenMod.API.Ioc; -using OpenMod.API.Permissions; -using OpenMod.API.Persistence; -using OpenMod.Core.Helpers; -using OpenMod.Core.Permissions; -using OpenMod.Extensions.Games.Abstractions.Players; -using System; -using System.Threading.Tasks; - -[assembly: RegisterPermission("nocooldown", Description = "Allows use kit without waiting for cooldown")] - -namespace Kits.Providers -{ - [PluginServiceImplementation(Lifetime = ServiceLifetime.Singleton)] - [UsedImplicitly] - public class KitCooldownStore : IKitCooldownStore, IDisposable - { - private const string c_CooldownKey = "cooldowns"; - private const string c_NoCooldownPermission = "nocooldown"; - - private readonly IDataStore m_DataStore; - private readonly Kits m_Plugin; - private readonly IPermissionChecker m_PermissionChecker; - - private KitsCooldownData m_KitsCooldownData = null!; - private IDisposable? m_FileWatcher = null!; - - public KitCooldownStore(Kits plugin, IPermissionChecker permissionChecker) - { - m_DataStore = plugin.DataStore; - m_Plugin = plugin; - m_PermissionChecker = permissionChecker; - - AsyncHelper.RunSync(LoadData); - } - - public async Task GetLastCooldownAsync(IPlayerUser player, string kitName) - { - if (await m_PermissionChecker.CheckPermissionAsync(player, c_NoCooldownPermission) == - PermissionGrantResult.Grant - || !m_KitsCooldownData.KitsCooldown!.TryGetValue(player.Id, out var kitCooldowns)) - { - return null; - } - - var kitCooldown = kitCooldowns!.Find(x => - x.KitName?.Equals(kitName, StringComparison.CurrentCultureIgnoreCase) == true); - return kitCooldown == null ? null : DateTime.Now - kitCooldown.KitCooldown; - } - - public async Task RegisterCooldownAsync(IPlayerUser player, string kitName, DateTime time) - { - if (await m_PermissionChecker.CheckPermissionAsync(player, c_NoCooldownPermission) == - PermissionGrantResult.Grant) - { - return; - } - - kitName = kitName.ToLower(); - - if (m_KitsCooldownData.KitsCooldown!.TryGetValue(player.Id, out var kitCooldowns)) - { - var kitCooldown = kitCooldowns!.Find(x => x.KitName == kitName); - if (kitCooldown == null) - { - kitCooldown = new() { KitName = kitName }; - kitCooldowns.Add(kitCooldown); - } - - kitCooldown.KitCooldown = time; - } - else - { - m_KitsCooldownData.KitsCooldown.Add(player.Id, - new() { new() { KitCooldown = time, KitName = kitName } }); - } - - await SaveData(); - } - - private async Task LoadFromDisk() - { - if (await m_DataStore.ExistsAsync(c_CooldownKey)) - { - m_KitsCooldownData = await m_DataStore.LoadAsync(c_CooldownKey) ?? - new() { KitsCooldown = new() }; - } - else - { - m_KitsCooldownData = new() { KitsCooldown = new() }; - await SaveData(); - } - } - - private async Task LoadData() - { - await LoadFromDisk(); - m_FileWatcher = m_DataStore.AddChangeWatcher(c_CooldownKey, m_Plugin, - () => AsyncHelper.RunSync(LoadFromDisk)); - } - - private Task SaveData() - { - return m_DataStore.SaveAsync(c_CooldownKey, m_KitsCooldownData); - } - - public void Dispose() - { - m_FileWatcher?.Dispose(); - } - } -} \ No newline at end of file diff --git a/Kits/Services/KitManager.cs b/Kits/Services/KitManager.cs index 1039dd5..0591eda 100644 --- a/Kits/Services/KitManager.cs +++ b/Kits/Services/KitManager.cs @@ -1,97 +1,125 @@ -using JetBrains.Annotations; +using Autofac; +using Cysharp.Text; using Kits.API; +using Kits.API.Cooldowns; +using Kits.API.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; using OpenMod.API.Commands; using OpenMod.API.Ioc; using OpenMod.API.Permissions; +using OpenMod.API.Plugins; using OpenMod.API.Prioritization; using OpenMod.Extensions.Economy.Abstractions; using OpenMod.Extensions.Games.Abstractions.Players; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; using System.Threading.Tasks; -namespace Kits.Providers +namespace Kits.Services; + +[ServiceImplementation(Lifetime = ServiceLifetime.Transient, Priority = Priority.Lowest)] +public class KitManager : IKitManager { - [PluginServiceImplementation(Lifetime = ServiceLifetime.Transient, Priority = Priority.Lowest)] - [UsedImplicitly] - public class KitManager : IKitManager + private static readonly string s_PrefixPermissionKits; + static KitManager() + { + s_PrefixPermissionKits = typeof(KitManager).Assembly.GetCustomAttribute().Id + ":kits."; + } + + private readonly IEconomyProvider m_EconomyProvider; + private readonly IKitCooldownStore m_KitCooldownStore; + private readonly IKitStore m_KitStore; + private readonly IPermissionChecker m_PermissionChecker; + private readonly IServiceProvider m_ServiceProvider; + private readonly ILogger m_Logger; + private readonly IStringLocalizer? m_StringLocalizer; + + public KitManager(IEconomyProvider economyProvider, IKitCooldownStore kitCooldownStore, IKitStore kitStore, IPermissionChecker permissionChecker, + IPluginAccessor pluginAccessor, IServiceProvider serviceProvider, ILogger logger) { - private readonly IEconomyProvider m_EconomyProvider; - private readonly IKitCooldownStore m_KitCooldownStore; - private readonly IKitStore m_KitStore; - private readonly IStringLocalizer m_StringLocalizer; - private readonly IPermissionChecker m_PermissionChecker; - private readonly IServiceProvider m_ServiceProvider; - - public KitManager(IEconomyProvider economyProvider, IKitCooldownStore kitCooldownStore, IKitStore kitStore, - IStringLocalizer stringLocalizer, IPermissionChecker permissionChecker, IServiceProvider serviceProvider) + m_EconomyProvider = economyProvider; + m_KitCooldownStore = kitCooldownStore; + m_KitStore = kitStore; + m_PermissionChecker = permissionChecker; + m_ServiceProvider = serviceProvider; + m_Logger = logger; + + m_StringLocalizer = pluginAccessor.Instance?.LifetimeScope.Resolve(); + } + + public async Task GiveKitAsync(IPlayerUser user, string name, ICommandActor? instigator = null, bool forceGiveKit = false) + { + var kit = await m_KitStore.FindKitByNameAsync(name); + if (kit == null) { - m_EconomyProvider = economyProvider; - m_KitCooldownStore = kitCooldownStore; - m_KitStore = kitStore; - m_StringLocalizer = stringLocalizer; - m_PermissionChecker = permissionChecker; - m_ServiceProvider = serviceProvider; + throw new UserFriendlyException(m_StringLocalizer!["commands:kit:notFound", new { Name = name }]); } - public async Task GiveKitAsync(IPlayerUser user, string name, ICommandActor? instigator = null, - bool forceGiveKit = false) - { - var kit = await m_KitStore.FindKitByNameAsync(name); - if (kit == null) - { - throw new UserFriendlyException(m_StringLocalizer["commands:kit:notFound", new { Name = name }]); - } + await GiveKitAsync(user, kit, instigator, forceGiveKit); + } - if (!forceGiveKit && await m_PermissionChecker.CheckPermissionAsync(user, - $"{KitStore.c_KitsKey}.{kit.Name}") != PermissionGrantResult.Grant) - { - throw new UserFriendlyException(m_StringLocalizer["commands:kit:noPermission", new { Kit = kit }]); - } + public async Task GiveKitAsync(IPlayerUser user, Kit kit, ICommandActor? instigator = null, bool forceGiveKit = false) + { + if (!forceGiveKit && await CheckPermissionKitAsync(user, kit.Name) != PermissionGrantResult.Grant) + { + throw new UserFriendlyException(m_StringLocalizer!["commands:kit:noPermission", new { Kit = kit }]); + } - var cooldown = await m_KitCooldownStore.GetLastCooldownAsync(user, name); - if (!forceGiveKit && cooldown != null) - { - if (cooldown.Value.TotalSeconds < kit.Cooldown) + var cooldown = await m_KitCooldownStore.GetLastCooldownAsync(user, kit.Name); + if (!forceGiveKit && cooldown != null && cooldown.Value.TotalSeconds < kit.Cooldown) + { + throw new UserFriendlyException(m_StringLocalizer!["commands:kit:cooldown", + new { - throw new UserFriendlyException(m_StringLocalizer["commands:kit:cooldown", - new { Kit = kit, Cooldown = kit.Cooldown - cooldown.Value.TotalSeconds }]); - } - } + Kit = kit, + Cooldown = kit.Cooldown - cooldown.Value.TotalSeconds, + CooldownSpan = TimeSpan.FromSeconds(kit.Cooldown) - cooldown.Value + }]); + } - if (!forceGiveKit && kit.Cost != 0) - { - await m_EconomyProvider.UpdateBalanceAsync(user.Id, user.Type, -kit.Cost, - m_StringLocalizer["commands:kit:balanceUpdateReason:buy", new { Kit = kit }]); - } + if (!forceGiveKit && kit.Cost != 0) + { + await m_EconomyProvider.UpdateBalanceAsync(user.Id, user.Type, -kit.Cost, + m_StringLocalizer!["commands:kit:balanceUpdateReason:buy", new { Kit = kit }]); + } - await m_KitCooldownStore.RegisterCooldownAsync(user, name, DateTime.Now); + await m_KitCooldownStore.RegisterCooldownAsync(user, kit.Name, DateTime.Now); - await kit.GiveKitToPlayer(user, m_ServiceProvider); + await kit.GiveKitToPlayer(user, m_ServiceProvider); - await user.PrintMessageAsync(m_StringLocalizer["commands:kit:success", new { Kit = kit }]); + await user.PrintMessageAsync(m_StringLocalizer!["commands:kit:success", new { Kit = kit }]); - if (instigator != null) - { - await instigator.PrintMessageAsync(m_StringLocalizer["commands:kit:success", new { Kit = kit }]); - } + if (instigator != null) + { + await instigator.PrintMessageAsync(m_StringLocalizer["commands:kit:success", new { Kit = kit }]); } + } - public async Task> GetAvailableKitsForPlayerAsync(IPlayerUser player) + public async Task> GetAvailableKitsForPlayerAsync(IPlayerUser player) + { + var sw = m_Logger.IsEnabled(LogLevel.Debug) ? Stopwatch.StartNew() : null; + + var list = new List(); + foreach (var kit in await m_KitStore.GetKitsAsync()) { - var list = new List(); - foreach (var kit in await m_KitStore.GetKitsAsync()) + if (await CheckPermissionKitAsync(player, kit.Name) == PermissionGrantResult.Grant) { - if (await m_PermissionChecker.CheckPermissionAsync(player, - $"{KitStore.c_KitsKey}.{kit.Name}") == PermissionGrantResult.Grant) - { - list.Add(kit); - } + list.Add(kit); } - - return list; } + + m_Logger.LogDebug("Get available kits for user was take: {SpentMs}ms", sw?.ElapsedMilliseconds ?? 0); + + return list; + } + + private Task CheckPermissionKitAsync(IPermissionActor actor, string kitName) + { + var permission = ZString.Concat(s_PrefixPermissionKits, kitName); + return m_PermissionChecker.CheckPermissionAsync(actor, permission); } } \ No newline at end of file diff --git a/Kits/Services/KitStore.cs b/Kits/Services/KitStore.cs index 0e17351..78e6d10 100644 --- a/Kits/Services/KitStore.cs +++ b/Kits/Services/KitStore.cs @@ -1,144 +1,233 @@ -using JetBrains.Annotations; +using Autofac; +using Cysharp.Text; using Kits.API; +using Kits.API.Databases; +using Kits.API.Models; using Kits.Databases; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OpenMod.API; using OpenMod.API.Eventing; using OpenMod.API.Ioc; using OpenMod.API.Permissions; +using OpenMod.API.Plugins; using OpenMod.Core.Helpers; using OpenMod.Core.Plugins.Events; using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace Kits.Providers +namespace Kits.Services; + +[ServiceImplementation(Lifetime = ServiceLifetime.Singleton)] +public class KitStore : IKitStore, IAsyncDisposable { - [PluginServiceImplementation(Lifetime = ServiceLifetime.Singleton)] - [UsedImplicitly] - public class KitStore : IKitStore, IAsyncDisposable - { - public const string c_KitsKey = "kits"; + private readonly IPermissionRegistry m_PermissionRegistry; + private readonly ILogger m_Logger; + private readonly IEventBus m_EventBus; + private readonly IOptions m_Options; + private readonly IRuntime m_Runtime; + + private IOpenModPlugin? m_Plugin; + private IDisposable? m_ConfigurationChangedWatcher; + private int m_KitsCount; - private readonly Kits m_Plugin; - private readonly IPermissionRegistry m_PermissionRegistry; - private readonly ILogger m_Logger; - private readonly IServiceProvider m_Provider; - private readonly IDisposable? m_ConfigurationChangedWatcher; + public IKitStoreProvider DatabaseProvider { get; private set; } = null!; - private IKitDatabase m_Database = null!; + public KitStore(IPermissionRegistry permissionRegistry, ILogger logger, + IEventBus eventBus, IOptions options, IRuntime runtime) + { + m_PermissionRegistry = permissionRegistry; + m_Logger = logger; + m_EventBus = eventBus; + m_Options = options; + m_Runtime = runtime; + ScheduleWaitForPluginLoading(); + } - public IKitDatabase Database => m_Database; + private void ScheduleWaitForPluginLoading() + { + IDisposable eventHandler = NullDisposable.Instance; - public KitStore(Kits plugin, IPermissionRegistry permissionRegistry, ILogger logger, - IEventBus eventBus, IServiceProvider provider) + eventHandler = m_EventBus.Subscribe(m_Runtime, async (_, _, @event) => { - m_Plugin = plugin; - m_PermissionRegistry = permissionRegistry; - m_Logger = logger; - m_Provider = provider; - AsyncHelper.RunSync(ParseLoadDatabase); - - m_ConfigurationChangedWatcher = - eventBus.Subscribe(plugin, PluginConfigurationChanged); - } + if (@event.Plugin is not KitsPlugin) + { + return; + } + + m_Plugin = @event.Plugin; + eventHandler.Dispose(); + await InitAsync(); + }); + } + + public async Task InitAsync() + { + await ParseLoadDatabase(); + + m_ConfigurationChangedWatcher = m_EventBus.Subscribe(m_Runtime, PluginConfigurationChangedAsync); + } - private Task PluginConfigurationChanged(IServiceProvider serviceprovider, object? sender, - PluginConfigurationChangedEvent @event) + private Task PluginConfigurationChangedAsync(IServiceProvider _, object? __, + PluginConfigurationChangedEvent @event) + { + if (@event.Plugin is not KitsPlugin || m_Plugin == null) { - return @event.Plugin != m_Plugin ? Task.CompletedTask : ParseLoadDatabase(); + return Task.CompletedTask; } - private async Task ParseLoadDatabase() - { - var type = m_Plugin.Configuration["database:connectionType"]; - m_Database = type.ToLower() switch - { - "mysql" => new MySqlKitDatabase(m_Provider), - "datastore" => new DataStoreKitDatabase(m_Plugin), - _ => null! - }; + return ParseLoadDatabase(); + } - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (m_Database == null) + private async Task ParseLoadDatabase() + { + DatabaseProvider?.DisposeSyncOrAsync(); + DatabaseProvider = null!; + + var configuration = m_Plugin!.LifetimeScope.Resolve(); + var type = configuration["database:connectionType"] ?? string.Empty; + var databaseType = m_Options.Value.FindType(type); + + if (databaseType != null) + { + m_Logger.LogDebug("Database type set to `{DatabaseType}`", type); + try { - m_Database = new DataStoreKitDatabase(m_Plugin); - m_Logger.LogWarning("Unable to parse {DatabaseType}. Setting to default: `datastore`", type); + var serviceProvider = m_Plugin.LifetimeScope.Resolve(); + DatabaseProvider = (IKitStoreProvider)ActivatorUtilities.CreateInstance(serviceProvider, databaseType); } - else + catch (Exception ex) { - m_Logger.LogInformation("Datastore type set to `{DatabaseType}`", type); + m_Logger.LogError(ex, "Failed to initialize {DatabaseName}. Defaulting to `datastore`", databaseType.Name); } - - await m_Database.LoadDatabaseAsync(); - await RegisterPermissionsAsync(); } - - public Task> GetKitsAsync() + else { - return m_Database.GetKitsAsync(); + m_Logger.LogWarning("Unable to parse {DatabaseType}. Setting to default: `datastore`", type); } - public async Task AddKitAsync(Kit kit) + DatabaseProvider ??= new DataStoreKitStoreProvider(m_Plugin.LifetimeScope); + try { - if (kit?.Name == null || kit?.Items == null) - { - throw new ArgumentNullException(nameof(kit)); - } + await DatabaseProvider.InitAsync(); + } + catch (Exception ex) + { + m_Logger.LogError(ex, "Failed to initialize {DatabaseProviderName}. Resetting to the default store provider", + DatabaseProvider.GetType().Name); - if (await m_Database.AddKitAsync(kit)) + if (DatabaseProvider is not DataStoreKitStoreProvider) { - RegisterPermission(kit.Name); + // dispose safely + try + { + await DatabaseProvider.DisposeSyncOrAsync(); + } + catch (Exception ex2) + { + m_Logger.LogError(ex2, "Failed to dispose {DatabaseProviderName}", DatabaseProvider.GetType().Name); + } + + DatabaseProvider = new DataStoreKitStoreProvider(m_Plugin.LifetimeScope); + await DatabaseProvider.InitAsync(); } } + await RegisterPermissionsAsync(); + } - public async Task FindKitByNameAsync(string kitName) + public async Task> GetKitsAsync() + { + var kits = await DatabaseProvider.GetKitsAsync(); + if (kits.Count > m_KitsCount) { - if (string.IsNullOrEmpty(kitName)) - { - throw new ArgumentException($"'{nameof(kitName)}' cannot be null or empty.", nameof(kitName)); - } + // new kits added from other server or manual manipulation in db + // registering new permission to prevent exception - var kit = await m_Database.FindKitByNameAsync(kitName); - if (kit?.Name is not null) - { - RegisterPermission(kit.Name); - } + await RegisterPermissionsAsync(kits); + } + + return kits; + } - return kit; + public async Task AddKitAsync(Kit kit) + { + if (kit?.Name == null || kit?.Items == null) + { + throw new ArgumentNullException(nameof(kit)); } - public Task RemoveKitAsync(string kitName) + await DatabaseProvider.AddKitAsync(kit); + RegisterPermission(kit.Name); + } + + public async Task FindKitByNameAsync(string kitName) + { + if (string.IsNullOrEmpty(kitName)) { - if (string.IsNullOrEmpty(kitName)) - { - return Task.FromException(new ArgumentException( - $"'{nameof(kitName)}' cannot be null or empty.", nameof(kitName))); - } + throw new ArgumentException($"'{nameof(kitName)}' cannot be null or empty.", nameof(kitName)); + } - return m_Database.RemoveKitAsync(kitName); + var kit = await DatabaseProvider.FindKitByNameAsync(kitName); + if (kit?.Name is not null) + { + // kit was maybe manually added, registering the permission for safety reason + RegisterPermission(kit.Name); } - protected virtual async Task RegisterPermissionsAsync() + return kit; + } + + public Task RemoveKitAsync(string kitName) + { + return string.IsNullOrEmpty(kitName) + ? Task.FromException(new ArgumentException( + $"'{nameof(kitName)}' cannot be null or empty.", nameof(kitName))) + : DatabaseProvider.RemoveKitAsync(kitName); + } + + public Task UpdateKitAsync(Kit kit) + { + return DatabaseProvider.UpdateKitAsync(kit); + } + + public Task IsKitExists(string name) + { + return DatabaseProvider.IsKitExists(name); + } + + protected virtual async Task RegisterPermissionsAsync(IReadOnlyCollection? kits = null) + { + kits ??= await DatabaseProvider.GetKitsAsync(); + m_KitsCount = kits.Count; + + foreach (var kit in kits) { - foreach (var kit in await m_Database.GetKitsAsync()) + if (kit.Name != null) { - if (kit.Name != null) - { - RegisterPermission(kit.Name); - } + RegisterPermission(kit.Name); } } + } - protected virtual void RegisterPermission(string kitName) - { - m_PermissionRegistry.RegisterPermission(m_Plugin, "kits." + kitName.ToLower()); - } + protected virtual void RegisterPermission(string kitName) + { + m_PermissionRegistry.RegisterPermission(m_Plugin!, ZString.Concat("kits.", kitName.ToLower()), + $"Grants access to the {kitName} kit"); + } + + public async ValueTask DisposeAsync() + { + m_ConfigurationChangedWatcher?.Dispose(); + m_ConfigurationChangedWatcher = null; - public ValueTask DisposeAsync() + if (DatabaseProvider != null) { - m_ConfigurationChangedWatcher?.Dispose(); - return new(m_Database.DisposeSyncOrAsync()); + await DatabaseProvider.DisposeSyncOrAsync(); } + + DatabaseProvider = null!; } } \ No newline at end of file diff --git a/Kits/config.yaml b/Kits/config.yaml index 8f51975..d38737d 100644 --- a/Kits/config.yaml +++ b/Kits/config.yaml @@ -2,9 +2,18 @@ ConnectionStrings: # https://www.connectionstrings.com/mysql/ default: "Server=127.0.0.1;Database=unturned;User=root;Password=root;" + # Not needed if you're using datastore cooldowns provider + cooldown: "Server=127.0.0.1;Database=unturned;User=root;Password=root;" # Type of datastore that will be used - # datastore (default): will create 'kits.data.yaml' file that saves all kits + # datastore: will create 'kits.data.yaml' file that saves all kits (default) # mysql: will create table that saves all kits connectionType: "datastore" +# Wrap lines when pritting list of kits? It was before handled by OpenMod, but now it's per plugin configuration wrapLines: true + +cooldowns: + # Type of store for cooldowns that will be used: + # datastore: will create 'cooldowns.data.yaml' file that saves all cooldowns (default) + # mysql: will create table that saves all cooldowns + connectionType: "datastore" \ No newline at end of file diff --git a/Kits/translations.yaml b/Kits/translations.yaml index 4a5ac3e..6eff9f6 100644 --- a/Kits/translations.yaml +++ b/Kits/translations.yaml @@ -1,9 +1,11 @@ +# Learn about of SmartFormat.NET: https://github.com/axuno/SmartFormat/wiki/Syntax%2C-Terminology + commands: kits: "Your available kits: {Kits:cond:{Kits:list:{Name}{Cost:cond:!=0?({}{MoneySymbol})|}|, }|'none'}" kit: success: "Kit {Kit.Name} successfully received" exist: "Kit with name {Name} already exists" - cooldown: "Kit {Kit.Name} have cooldown. Needs wait for {Cooldown:0.##} sec" + cooldown: "Kit {Kit.Name} have cooldown. Needs wait for {CooldownSpan:abbr}" noPermission: "You don't have permission to get a kit {Kit.Name}" notFound: "Kit {Name} not found" noMoney: "You don't have money to get a kit {Kit.Name}. Needs at least {Money}{MoneyName}{MoneySymbol}" diff --git a/README.md b/README.md index 424a553..c026e10 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Kits -OpenMod universal plugin. Add kits system. +OpenMod universal plugin. Adds the kits system. [![Nuget](https://img.shields.io/nuget/v/DiFFoZ.Kits)](https://www.nuget.org/packages/DiFFoZ.Kits/) [![Nuget](https://img.shields.io/nuget/dt/DiFFoZ.Kits?label=NuGet%20downloads)](https://www.nuget.org/packages/DiFFoZ.Kits/) @@ -11,7 +11,7 @@ Run command `openmod install DiFFoZ.Kits` # Commands _Maybe is outdated so check help.md to get all commands_ - kit <name> - Give a kit to yourself. -- kit create <name> [cooldown] - Create a kit. +- kit create <name> [cooldown] [cost] [money] [vehicleId] - Create a kit. - kit remove <name> - Remove a kit. - kits - Shows a list of available kits for player.