From 675bd6912b9e6e2319cf6bbf5fd10f3e23626aaa Mon Sep 17 00:00:00 2001 From: Stephen White Date: Tue, 1 Mar 2022 16:58:25 -0400 Subject: [PATCH] Major Deathmatch rework --- Deathmatch.API/Deathmatch.API.csproj | 2 +- Deathmatch.API/Loadouts/ILoadout.cs | 9 +- Deathmatch.API/Loadouts/ILoadoutCategory.cs | 15 +- Deathmatch.API/Loadouts/ILoadoutSelector.cs | 6 +- Deathmatch.API/Matches/IMatchExecutor.cs | 23 +- .../Matches/MatchExecutorExtensions.cs | 78 +++++++ .../Matches}/MatchExtensions.cs | 3 +- .../Matches}/MatchManagerExtensions.cs | 5 +- Deathmatch.API/Matches/MatchStatus.cs | 11 +- Deathmatch.Core/Commands/CommandDMStart.cs | 32 ++- Deathmatch.Core/Commands/CommandJoin.cs | 18 +- .../Loadouts/Base/AddLoadoutCommand.cs | 77 +++++++ .../RemoveLoadoutCommand.cs} | 36 ++-- ...{CommandGiveLoadout.cs => CGiveLoadout.cs} | 8 +- .../{CommandLoadout.cs => CLoadout.cs} | 38 ++-- .../{CommandLoadouts.cs => CLoadouts.cs} | 12 +- .../Commands/Loadouts/CommandCreateLoadout.cs | 84 -------- .../Configuration/DeathmatchConfig.cs | 14 ++ Deathmatch.Core/Deathmatch.Core.csproj | 4 +- Deathmatch.Core/DeathmatchPlugin.cs | 30 ++- Deathmatch.Core/Helpers/StringHelper.cs | 2 +- Deathmatch.Core/Items/Item.cs | 8 +- Deathmatch.Core/Loadouts/BasicLoadout.cs | 22 ++ Deathmatch.Core/Loadouts/Loadout.cs | 88 -------- Deathmatch.Core/Loadouts/LoadoutBase.cs | 133 ++++++++++-- Deathmatch.Core/Loadouts/LoadoutCategory.cs | 70 ------ .../Loadouts/LoadoutCategoryBase.cs | 109 ++++++++++ .../Loadouts/LoadoutCategoryExtensions.cs | 88 ++++++++ Deathmatch.Core/Loadouts/LoadoutExtensions.cs | 68 ------ Deathmatch.Core/Loadouts/LoadoutManager.cs | 1 - .../Loadouts/LoadoutManagerExtensions.cs | 51 +++++ Deathmatch.Core/Loadouts/LoadoutSelection.cs | 9 - Deathmatch.Core/Loadouts/LoadoutSelector.cs | 163 ++++++-------- Deathmatch.Core/Matches/MatchBase.cs | 55 +++-- Deathmatch.Core/Matches/MatchExecutor.cs | 201 +++++++++--------- Deathmatch.Core/Matches/MatchManager.cs | 5 +- Deathmatch.Core/Players/GamePlayer.cs | 8 +- Deathmatch.Core/Spawns/SpawnDirectory.cs | 44 ++++ Deathmatch.Core/translations.yaml | 10 +- .../{CommandFFA.cs => CFreeForAll.cs} | 7 +- FreeForAll/Commands/Loadouts/CLoadouts.cs | 24 +++ FreeForAll/Commands/Loadouts/CLoadoutsAdd.cs | 27 +++ .../Commands/Loadouts/CLoadoutsRemove.cs | 23 ++ .../Spawns/{CommandSpawns.cs => CSpawns.cs} | 6 +- FreeForAll/Commands/Spawns/CSpawnsAction.cs | 35 +++ .../{CommandSpawnsAdd.cs => CSpawnsAdd.cs} | 14 +- FreeForAll/Commands/Spawns/CSpawnsClear.cs | 26 +++ FreeForAll/Commands/Spawns/CSpawnsList.cs | 37 ++++ ...ommandSpawnsRemove.cs => CSpawnsRemove.cs} | 17 +- .../Commands/Spawns/CommandSpawnsAction.cs | 41 ---- .../Commands/Spawns/CommandSpawnsClear.cs | 32 --- .../Commands/Spawns/CommandSpawnsList.cs | 42 ---- FreeForAll/Configuration/AutoRespawnConfig.cs | 12 ++ FreeForAll/Configuration/FreeForAllConfig.cs | 18 ++ FreeForAll/Configuration/GameRewardsConfig.cs | 19 ++ FreeForAll/Configuration/RewardConfig.cs | 14 ++ FreeForAll/FreeForAll.csproj | 4 +- FreeForAll/FreeForAllPlugin.cs | 71 +------ FreeForAll/Loadouts/FFALoadoutCategory.cs | 18 ++ FreeForAll/Matches/MatchEventListener.cs | 1 - FreeForAll/Matches/MatchFFA.cs | 139 ++++++------ FreeForAll/PluginContainerConfigurator.cs | 19 ++ .../{CommandTDM.cs => CTeamDeathmatch.cs} | 7 +- TeamDeathmatch/Commands/Loadouts/CLoadouts.cs | 24 +++ .../Commands/Loadouts/CLoadoutsAdd.cs | 26 +++ .../Commands/Loadouts/CLoadoutsRemove.cs | 22 ++ .../Spawns/{CommandSpawns.cs => CSpawns.cs} | 6 +- .../Commands/Spawns/CSpawnsAction.cs | 78 +++++++ .../{CommandSpawnsAdd.cs => CSpawnsAdd.cs} | 17 +- ...{CommandSpawnsClear.cs => CSpawnsClear.cs} | 14 +- .../{CommandSpawnsList.cs => CSpawnsList.cs} | 17 +- ...ommandSpawnsRemove.cs => CSpawnsRemove.cs} | 17 +- .../Commands/Spawns/CommandSpawnsAction.cs | 82 ------- .../Configuration/AutoRespawnConfig.cs | 12 ++ .../Configuration/GameRewardsConfig.cs | 19 ++ TeamDeathmatch/Configuration/RewardConfig.cs | 14 ++ .../TeamDeathmatchConfiguration.cs | 20 ++ TeamDeathmatch/Items/TeamItem.cs | 8 + TeamDeathmatch/Loadouts/TDMLoadoutCategory.cs | 18 ++ TeamDeathmatch/Loadouts/TeamLoadout.cs | 15 +- .../Loadouts/TeamLoadoutCategory.cs | 72 ------- TeamDeathmatch/Matches/MatchEventListener.cs | 12 +- TeamDeathmatch/Matches/MatchTDM.cs | 133 +++++++----- TeamDeathmatch/PluginContainerConfigurator.cs | 23 ++ TeamDeathmatch/Spawns/BlueSpawnDirectory.cs | 14 ++ TeamDeathmatch/Spawns/RedSpawnDirectory.cs | 14 ++ TeamDeathmatch/TeamDeathmatch.csproj | 4 +- TeamDeathmatch/TeamDeathmatchPlugin.cs | 78 +------ TeamDeathmatch/config.yaml | 3 + 89 files changed, 1759 insertions(+), 1296 deletions(-) create mode 100644 Deathmatch.API/Matches/MatchExecutorExtensions.cs rename {Deathmatch.Core/Matches/Extensions => Deathmatch.API/Matches}/MatchExtensions.cs (98%) rename {Deathmatch.Core/Matches/Extensions => Deathmatch.API/Matches}/MatchManagerExtensions.cs (90%) create mode 100644 Deathmatch.Core/Commands/Loadouts/Base/AddLoadoutCommand.cs rename Deathmatch.Core/Commands/Loadouts/{CommandDeleteLoadout.cs => Base/RemoveLoadoutCommand.cs} (54%) rename Deathmatch.Core/Commands/Loadouts/{CommandGiveLoadout.cs => CGiveLoadout.cs} (93%) rename Deathmatch.Core/Commands/Loadouts/{CommandLoadout.cs => CLoadout.cs} (63%) rename Deathmatch.Core/Commands/Loadouts/{CommandLoadouts.cs => CLoadouts.cs} (90%) delete mode 100644 Deathmatch.Core/Commands/Loadouts/CommandCreateLoadout.cs create mode 100644 Deathmatch.Core/Configuration/DeathmatchConfig.cs create mode 100644 Deathmatch.Core/Loadouts/BasicLoadout.cs delete mode 100644 Deathmatch.Core/Loadouts/Loadout.cs delete mode 100644 Deathmatch.Core/Loadouts/LoadoutCategory.cs create mode 100644 Deathmatch.Core/Loadouts/LoadoutCategoryBase.cs create mode 100644 Deathmatch.Core/Loadouts/LoadoutCategoryExtensions.cs delete mode 100644 Deathmatch.Core/Loadouts/LoadoutExtensions.cs create mode 100644 Deathmatch.Core/Loadouts/LoadoutManagerExtensions.cs delete mode 100644 Deathmatch.Core/Loadouts/LoadoutSelection.cs create mode 100644 Deathmatch.Core/Spawns/SpawnDirectory.cs rename FreeForAll/Commands/{CommandFFA.cs => CFreeForAll.cs} (69%) create mode 100644 FreeForAll/Commands/Loadouts/CLoadouts.cs create mode 100644 FreeForAll/Commands/Loadouts/CLoadoutsAdd.cs create mode 100644 FreeForAll/Commands/Loadouts/CLoadoutsRemove.cs rename FreeForAll/Commands/Spawns/{CommandSpawns.cs => CSpawns.cs} (72%) create mode 100644 FreeForAll/Commands/Spawns/CSpawnsAction.cs rename FreeForAll/Commands/Spawns/{CommandSpawnsAdd.cs => CSpawnsAdd.cs} (55%) create mode 100644 FreeForAll/Commands/Spawns/CSpawnsClear.cs create mode 100644 FreeForAll/Commands/Spawns/CSpawnsList.cs rename FreeForAll/Commands/Spawns/{CommandSpawnsRemove.cs => CSpawnsRemove.cs} (62%) delete mode 100644 FreeForAll/Commands/Spawns/CommandSpawnsAction.cs delete mode 100644 FreeForAll/Commands/Spawns/CommandSpawnsClear.cs delete mode 100644 FreeForAll/Commands/Spawns/CommandSpawnsList.cs create mode 100644 FreeForAll/Configuration/AutoRespawnConfig.cs create mode 100644 FreeForAll/Configuration/FreeForAllConfig.cs create mode 100644 FreeForAll/Configuration/GameRewardsConfig.cs create mode 100644 FreeForAll/Configuration/RewardConfig.cs create mode 100644 FreeForAll/Loadouts/FFALoadoutCategory.cs create mode 100644 FreeForAll/PluginContainerConfigurator.cs rename TeamDeathmatch/Commands/{CommandTDM.cs => CTeamDeathmatch.cs} (68%) create mode 100644 TeamDeathmatch/Commands/Loadouts/CLoadouts.cs create mode 100644 TeamDeathmatch/Commands/Loadouts/CLoadoutsAdd.cs create mode 100644 TeamDeathmatch/Commands/Loadouts/CLoadoutsRemove.cs rename TeamDeathmatch/Commands/Spawns/{CommandSpawns.cs => CSpawns.cs} (73%) create mode 100644 TeamDeathmatch/Commands/Spawns/CSpawnsAction.cs rename TeamDeathmatch/Commands/Spawns/{CommandSpawnsAdd.cs => CSpawnsAdd.cs} (58%) rename TeamDeathmatch/Commands/Spawns/{CommandSpawnsClear.cs => CSpawnsClear.cs} (52%) rename TeamDeathmatch/Commands/Spawns/{CommandSpawnsList.cs => CSpawnsList.cs} (58%) rename TeamDeathmatch/Commands/Spawns/{CommandSpawnsRemove.cs => CSpawnsRemove.cs} (64%) delete mode 100644 TeamDeathmatch/Commands/Spawns/CommandSpawnsAction.cs create mode 100644 TeamDeathmatch/Configuration/AutoRespawnConfig.cs create mode 100644 TeamDeathmatch/Configuration/GameRewardsConfig.cs create mode 100644 TeamDeathmatch/Configuration/RewardConfig.cs create mode 100644 TeamDeathmatch/Configuration/TeamDeathmatchConfiguration.cs create mode 100644 TeamDeathmatch/Loadouts/TDMLoadoutCategory.cs delete mode 100644 TeamDeathmatch/Loadouts/TeamLoadoutCategory.cs create mode 100644 TeamDeathmatch/PluginContainerConfigurator.cs create mode 100644 TeamDeathmatch/Spawns/BlueSpawnDirectory.cs create mode 100644 TeamDeathmatch/Spawns/RedSpawnDirectory.cs diff --git a/Deathmatch.API/Deathmatch.API.csproj b/Deathmatch.API/Deathmatch.API.csproj index de0b61a..ba9b70e 100644 --- a/Deathmatch.API/Deathmatch.API.csproj +++ b/Deathmatch.API/Deathmatch.API.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Deathmatch.API/Loadouts/ILoadout.cs b/Deathmatch.API/Loadouts/ILoadout.cs index 5319037..7425a52 100644 --- a/Deathmatch.API/Loadouts/ILoadout.cs +++ b/Deathmatch.API/Loadouts/ILoadout.cs @@ -1,4 +1,7 @@ -using Deathmatch.API.Players; +using Cysharp.Threading.Tasks; +using Deathmatch.API.Players; +using OpenMod.API.Permissions; +using System.Threading.Tasks; namespace Deathmatch.API.Loadouts { @@ -6,8 +9,8 @@ public interface ILoadout { string Title { get; } - string? Permission { get; } + UniTask GiveToPlayer(IGamePlayer player); - void GiveToPlayer(IGamePlayer player); + Task IsPermitted(IPermissionActor actor); } } diff --git a/Deathmatch.API/Loadouts/ILoadoutCategory.cs b/Deathmatch.API/Loadouts/ILoadoutCategory.cs index cb11416..46e051c 100644 --- a/Deathmatch.API/Loadouts/ILoadoutCategory.cs +++ b/Deathmatch.API/Loadouts/ILoadoutCategory.cs @@ -1,6 +1,5 @@ using OpenMod.API; using System.Collections.Generic; -using System.Threading.Tasks; namespace Deathmatch.API.Loadouts { @@ -11,12 +10,16 @@ public interface ILoadoutCategory IReadOnlyCollection Aliases { get; } IOpenModComponent Component { get; } - - IReadOnlyCollection GetLoadouts(); - Task SaveLoadouts(); + ILoadout? GetDefaultLoadout(); - void AddLoadout(ILoadout loadout); - bool RemoveLoadout(ILoadout loadout); + IReadOnlyCollection GetLoadouts(); + } + + public interface ILoadoutCategory : ILoadoutCategory where TLoadout : ILoadout + { + new TLoadout? GetDefaultLoadout(); + + new IReadOnlyCollection GetLoadouts(); } } diff --git a/Deathmatch.API/Loadouts/ILoadoutSelector.cs b/Deathmatch.API/Loadouts/ILoadoutSelector.cs index 63723b5..cffd0ac 100644 --- a/Deathmatch.API/Loadouts/ILoadoutSelector.cs +++ b/Deathmatch.API/Loadouts/ILoadoutSelector.cs @@ -1,13 +1,13 @@ using Deathmatch.API.Players; using OpenMod.API.Ioc; -using System.Threading.Tasks; namespace Deathmatch.API.Loadouts { [Service] public interface ILoadoutSelector { - ILoadout? GetLoadout(IGamePlayer player, string category); - Task SetLoadout(IGamePlayer player, string category, string loadout); + ILoadout? GetSelectedLoadout(IGamePlayer player, ILoadoutCategory category); + + void SetSelectedLoadout(IGamePlayer player, ILoadoutCategory category, ILoadout loadout); } } diff --git a/Deathmatch.API/Matches/IMatchExecutor.cs b/Deathmatch.API/Matches/IMatchExecutor.cs index 29e522a..6d07bc5 100644 --- a/Deathmatch.API/Matches/IMatchExecutor.cs +++ b/Deathmatch.API/Matches/IMatchExecutor.cs @@ -9,12 +9,33 @@ namespace Deathmatch.API.Matches [Service] public interface IMatchExecutor { + /// + /// The current running match. + /// IMatch? CurrentMatch { get; } + /// + /// Gets all players in the current match pool. + /// + /// All players in the current match pool. IReadOnlyCollection GetParticipants(); - UniTask AddParticipant(IGamePlayer player); + + /// + /// Adds the player to the match pool and also the current match if one is running. + /// + /// The player to add. + /// true if the player was added to the match pool, false otherwise. + UniTask AddParticipant(IGamePlayer player); + + UniTask RemoveParticipant(IGamePlayer user); + /// + /// Attempts to start a match. + /// + /// + /// true when a match is started successfully. false if a match is already running. + /// When a match could not be started due to user error. UniTask StartMatch(IMatchRegistration? registration = null); } } diff --git a/Deathmatch.API/Matches/MatchExecutorExtensions.cs b/Deathmatch.API/Matches/MatchExecutorExtensions.cs new file mode 100644 index 0000000..b0c0bfe --- /dev/null +++ b/Deathmatch.API/Matches/MatchExecutorExtensions.cs @@ -0,0 +1,78 @@ +using Deathmatch.API.Players; +using OpenMod.Unturned.Players; +using OpenMod.Unturned.Users; +using SDG.Unturned; +using Steamworks; +using System; +using System.Linq; + +namespace Deathmatch.API.Matches +{ + public static class MatchExecutorExtensions + { + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, Predicate predicate) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => predicate(x)); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, ulong steamId) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x.SteamId.m_SteamID == steamId); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, CSteamID steamId) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x.SteamId == steamId); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, Player player) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x.Player == player); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, UnturnedPlayer player) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x.SteamId == player.SteamId); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, UnturnedUser user) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x.SteamId == user.SteamId); + } + + public static IGamePlayer? GetParticipant(this IMatchExecutor matchExecutor, IGamePlayer player) + { + return matchExecutor.GetParticipants().FirstOrDefault(x => x == player); + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, ulong steamId) + { + return matchExecutor.GetParticipant(steamId) != null; + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, CSteamID steamId) + { + return matchExecutor.GetParticipant(steamId) != null; + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, Player player) + { + return matchExecutor.GetParticipant(player) != null; + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, UnturnedPlayer player) + { + return matchExecutor.GetParticipant(player) != null; + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, UnturnedUser user) + { + return matchExecutor.GetParticipant(user) != null; + } + + public static bool IsParticipant(this IMatchExecutor matchExecutor, IGamePlayer player) + { + return matchExecutor.GetParticipant(player) != null; + } + } +} diff --git a/Deathmatch.Core/Matches/Extensions/MatchExtensions.cs b/Deathmatch.API/Matches/MatchExtensions.cs similarity index 98% rename from Deathmatch.Core/Matches/Extensions/MatchExtensions.cs rename to Deathmatch.API/Matches/MatchExtensions.cs index a9ece6f..9b06ffb 100644 --- a/Deathmatch.Core/Matches/Extensions/MatchExtensions.cs +++ b/Deathmatch.API/Matches/MatchExtensions.cs @@ -1,5 +1,4 @@ using Cysharp.Threading.Tasks; -using Deathmatch.API.Matches; using Deathmatch.API.Players; using OpenMod.Unturned.Players; using OpenMod.Unturned.Users; @@ -9,7 +8,7 @@ using System.Collections.Generic; using System.Linq; -namespace Deathmatch.Core.Matches.Extensions +namespace Deathmatch.API.Matches { public static class MatchExtensions { diff --git a/Deathmatch.Core/Matches/Extensions/MatchManagerExtensions.cs b/Deathmatch.API/Matches/MatchManagerExtensions.cs similarity index 90% rename from Deathmatch.Core/Matches/Extensions/MatchManagerExtensions.cs rename to Deathmatch.API/Matches/MatchManagerExtensions.cs index 5144960..3102543 100644 --- a/Deathmatch.Core/Matches/Extensions/MatchManagerExtensions.cs +++ b/Deathmatch.API/Matches/MatchManagerExtensions.cs @@ -1,10 +1,9 @@ -using Deathmatch.API.Matches; -using Deathmatch.API.Matches.Registrations; +using Deathmatch.API.Matches.Registrations; using System; using System.Collections.Generic; using System.Linq; -namespace Deathmatch.Core.Matches.Extensions +namespace Deathmatch.API.Matches { public static class MatchManagerExtensions { diff --git a/Deathmatch.API/Matches/MatchStatus.cs b/Deathmatch.API/Matches/MatchStatus.cs index 27cca96..fbc7220 100644 --- a/Deathmatch.API/Matches/MatchStatus.cs +++ b/Deathmatch.API/Matches/MatchStatus.cs @@ -5,7 +5,7 @@ public enum MatchStatus /// /// Default value. Shouldn't be used. /// - Uninitialized, + Unknown, /// /// When a match has been initialized but has not yet been called. @@ -33,8 +33,13 @@ public enum MatchStatus Ended, /// - /// When an exception occurred either during or . + /// When an exception occurred during . /// - Exception + ExceptionWhenStarting, + + /// + /// When an exception occurred during . + /// + ExceptionWhenEnding } } diff --git a/Deathmatch.Core/Commands/CommandDMStart.cs b/Deathmatch.Core/Commands/CommandDMStart.cs index 572aca4..17e9907 100644 --- a/Deathmatch.Core/Commands/CommandDMStart.cs +++ b/Deathmatch.Core/Commands/CommandDMStart.cs @@ -1,11 +1,11 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Matches; -using Deathmatch.Core.Helpers; +using Deathmatch.API.Matches.Registrations; using Microsoft.Extensions.Localization; +using OpenMod.API.Commands; using OpenMod.Core.Commands; using OpenMod.Unturned.Commands; using System; -using Deathmatch.Core.Matches.Extensions; namespace Deathmatch.Core.Commands { @@ -30,27 +30,25 @@ public CommandDMStart(IMatchManager matchManager, protected override async UniTask OnExecuteAsync() { - var title = await Context.Parameters.GetAsync(0, null); + IMatchRegistration? registration = null; - var registration = string.IsNullOrWhiteSpace(title) - ? _matchManager.GetMatchRegistrations().RandomElement() - : _matchManager.GetMatchRegistration(title!); - - if (registration == null) + if (Context.Parameters.Length > 0) { - await PrintAsync(_stringLocalizer["commands:dmstart:not_found"]); - return; - } + var title = await Context.Parameters.GetAsync(0, null) ?? string.Empty; - if (await _matchExecutor.StartMatch(registration)) - { - await PrintAsync(_stringLocalizer["commands:dmstart:success", - new { registration.Title }]); + registration = (string.IsNullOrEmpty(title) ? null : _matchManager.GetMatchRegistration(title)) + ?? throw new UserFriendlyException(_stringLocalizer["commands:dmstart:not_found"]); } - else + + if (!await _matchExecutor.StartMatch(registration)) { - await PrintAsync(_stringLocalizer["commands:dmstart:failure"]); + throw new UserFriendlyException(_stringLocalizer["commands:dmstart:match_running"]); } + + registration = _matchExecutor.CurrentMatch?.Registration ?? throw new Exception( + $"Match started but {nameof(IMatchExecutor)}.{nameof(IMatchExecutor.CurrentMatch)} or registration is null"); + + await PrintAsync(_stringLocalizer["commands:dmstart:success", new {registration.Title}]); } } } diff --git a/Deathmatch.Core/Commands/CommandJoin.cs b/Deathmatch.Core/Commands/CommandJoin.cs index 60fe88f..f948df0 100644 --- a/Deathmatch.Core/Commands/CommandJoin.cs +++ b/Deathmatch.Core/Commands/CommandJoin.cs @@ -1,6 +1,7 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Matches; using Deathmatch.API.Players; +using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; using OpenMod.Unturned.Commands; using OpenMod.Unturned.Users; @@ -16,20 +17,31 @@ public class CommandJoin : UnturnedCommand { private readonly IGamePlayerManager _playerManager; private readonly IMatchExecutor _matchExecutor; + private readonly IStringLocalizer _stringLocalizer; - public CommandJoin(IGamePlayerManager playerManager, + public CommandJoin(IServiceProvider serviceProvider, + IGamePlayerManager playerManager, IMatchExecutor matchExecutor, - IServiceProvider serviceProvider) : base(serviceProvider) + IStringLocalizer stringLocalizer) : base(serviceProvider) { _playerManager = playerManager; _matchExecutor = matchExecutor; + _stringLocalizer = stringLocalizer; } protected override async UniTask OnExecuteAsync() { var player = _playerManager.GetPlayer((UnturnedUser)Context.Actor); - await _matchExecutor.AddParticipant(player); + if (await _matchExecutor.AddParticipant(player)) + { + await player.PrintMessageAsync(_stringLocalizer["commands:join:success"]); + } + else + { + + await player.PrintMessageAsync(_stringLocalizer["commands:join:failure"]); + } } } } \ No newline at end of file diff --git a/Deathmatch.Core/Commands/Loadouts/Base/AddLoadoutCommand.cs b/Deathmatch.Core/Commands/Loadouts/Base/AddLoadoutCommand.cs new file mode 100644 index 0000000..07342a0 --- /dev/null +++ b/Deathmatch.Core/Commands/Loadouts/Base/AddLoadoutCommand.cs @@ -0,0 +1,77 @@ +using Cysharp.Threading.Tasks; +using Deathmatch.API.Loadouts; +using Deathmatch.API.Players; +using Deathmatch.Core.Items; +using Deathmatch.Core.Loadouts; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using OpenMod.API.Commands; +using OpenMod.API.Permissions; +using OpenMod.Unturned.Commands; +using OpenMod.Unturned.Users; +using SilK.Unturned.Extras.Localization; +using System; + +namespace Deathmatch.Core.Commands.Loadouts.Base +{ + public abstract class AddLoadoutCommand : UnturnedCommand + where TLoadoutCategory : LoadoutCategoryBase + where TLoadout : LoadoutBase + where TItem : Item + { + private readonly IGamePlayerManager _playerManager; + private readonly ILoadoutManager _loadoutManager; + private readonly IStringLocalizer _stringLocalizer; + private readonly IPermissionRegistry _permissionRegistry; + + protected AddLoadoutCommand(IServiceProvider serviceProvider) : base(serviceProvider) + { + _playerManager = serviceProvider.GetRequiredService(); + _loadoutManager = serviceProvider.GetRequiredService(); + _stringLocalizer = serviceProvider.GetRequiredService>(); + _permissionRegistry = serviceProvider.GetRequiredService(); + } + + protected abstract TLoadout CreateLoadout(string title, string permission); + + protected override async UniTask OnExecuteAsync() + { + await UniTask.SwitchToMainThread(); + + var player = _playerManager.GetPlayer((UnturnedUser)Context.Actor); + + var loadoutTitle = await Context.Parameters.GetAsync(0); + + var category = _loadoutManager.GetCategory(); + + if (category == null) + { + throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_gamemode"]); + } + + var oldLoadout = category.GetLoadout(loadoutTitle); + + var title = loadoutTitle.ToLower(); + var permission = "loadouts." + title; + + var newLoadout = CreateLoadout(title, permission); + + await newLoadout.LoadItemsFromPlayer(player); + + if (oldLoadout != null) + { + category.RemoveLoadout(oldLoadout); + } + + _permissionRegistry.RegisterPermission(category.Component, permission); + + category.AddLoadout(newLoadout); + + await category.Save(); + + await PrintAsync(_stringLocalizer[ + "commands:create_loadout:success" + (oldLoadout == null ? "" : "_overwrite"), + new { Loadout = newLoadout.Title, GameMode = category.Title }]); + } + } +} diff --git a/Deathmatch.Core/Commands/Loadouts/CommandDeleteLoadout.cs b/Deathmatch.Core/Commands/Loadouts/Base/RemoveLoadoutCommand.cs similarity index 54% rename from Deathmatch.Core/Commands/Loadouts/CommandDeleteLoadout.cs rename to Deathmatch.Core/Commands/Loadouts/Base/RemoveLoadoutCommand.cs index 53f5bdc..081597c 100644 --- a/Deathmatch.Core/Commands/Loadouts/CommandDeleteLoadout.cs +++ b/Deathmatch.Core/Commands/Loadouts/Base/RemoveLoadoutCommand.cs @@ -1,51 +1,51 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Loadouts; +using Deathmatch.Core.Items; using Deathmatch.Core.Loadouts; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using OpenMod.API.Commands; -using OpenMod.API.Prioritization; -using OpenMod.Core.Commands; using OpenMod.Unturned.Commands; -using OpenMod.Unturned.Users; +using SilK.Unturned.Extras.Localization; using System; -namespace Deathmatch.Core.Commands.Loadouts +namespace Deathmatch.Core.Commands.Loadouts.Base { - [Command("deleteloadout", Priority = Priority.Normal)] - [CommandSyntax(" ")] - [CommandDescription("Delete the specified loadout.")] - [CommandActor(typeof(UnturnedUser))] - public class DeleteLoadout : UnturnedCommand + public abstract class RemoveLoadoutCommand : UnturnedCommand + where TLoadoutCategory : LoadoutCategoryBase + where TLoadout : LoadoutBase + where TItem : Item { private readonly ILoadoutManager _loadoutManager; private readonly IStringLocalizer _stringLocalizer; - public DeleteLoadout(ILoadoutManager loadoutManager, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(serviceProvider) + protected RemoveLoadoutCommand(IServiceProvider serviceProvider) : base(serviceProvider) { - _loadoutManager = loadoutManager; - _stringLocalizer = stringLocalizer; + _loadoutManager = serviceProvider.GetRequiredService(); + _stringLocalizer = serviceProvider.GetRequiredService>(); } protected override async UniTask OnExecuteAsync() { - var gameMode = await Context.Parameters.GetAsync(0); - var loadoutTitle = await Context.Parameters.GetAsync(1); + var loadoutTitle = await Context.Parameters.GetAsync(0); - var category = _loadoutManager.GetCategory(gameMode); + var category = _loadoutManager.GetCategory(); if (category == null) + { throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_gamemode"]); + } var loadout = category.GetLoadout(loadoutTitle); if (loadout == null) + { throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_loadout"]); + } category.RemoveLoadout(loadout); - await category.SaveLoadouts(); + await category.Save(); await PrintAsync(_stringLocalizer["commands:delete_loadout:success", new { GameMode = category.Title, Loadout = loadout.Title }]); diff --git a/Deathmatch.Core/Commands/Loadouts/CommandGiveLoadout.cs b/Deathmatch.Core/Commands/Loadouts/CGiveLoadout.cs similarity index 93% rename from Deathmatch.Core/Commands/Loadouts/CommandGiveLoadout.cs rename to Deathmatch.Core/Commands/Loadouts/CGiveLoadout.cs index 01db15a..f42aab6 100644 --- a/Deathmatch.Core/Commands/Loadouts/CommandGiveLoadout.cs +++ b/Deathmatch.Core/Commands/Loadouts/CGiveLoadout.cs @@ -1,6 +1,7 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Loadouts; using Deathmatch.API.Players; +using Deathmatch.Core.Loadouts; using Microsoft.Extensions.Localization; using OpenMod.API.Commands; using OpenMod.API.Prioritization; @@ -8,7 +9,6 @@ using OpenMod.Unturned.Commands; using OpenMod.Unturned.Users; using System; -using Deathmatch.Core.Loadouts; namespace Deathmatch.Core.Commands.Loadouts { @@ -19,13 +19,13 @@ namespace Deathmatch.Core.Commands.Loadouts [CommandSyntax(" ")] [CommandDescription("Give yourself the specified loadout.")] [CommandActor(typeof(UnturnedUser))] - public class GiveLoadout : UnturnedCommand + public class CGiveLoadout : UnturnedCommand { private readonly IGamePlayerManager _playerManager; private readonly ILoadoutManager _loadoutManager; private readonly IStringLocalizer _stringLocalizer; - public GiveLoadout(IGamePlayerManager playerManager, + public CGiveLoadout(IGamePlayerManager playerManager, ILoadoutManager loadoutManager, IStringLocalizer stringLocalizer, IServiceProvider serviceProvider) : base(serviceProvider) @@ -54,7 +54,7 @@ protected override async UniTask OnExecuteAsync() await UniTask.SwitchToMainThread(); - loadout.GiveToPlayer(player); + await loadout.GiveToPlayer(player); await PrintAsync(_stringLocalizer["commands:give_loadout:success", new { GameMode = category.Title, Loadout = loadout.Title }]); diff --git a/Deathmatch.Core/Commands/Loadouts/CommandLoadout.cs b/Deathmatch.Core/Commands/Loadouts/CLoadout.cs similarity index 63% rename from Deathmatch.Core/Commands/Loadouts/CommandLoadout.cs rename to Deathmatch.Core/Commands/Loadouts/CLoadout.cs index b34b1aa..203f9cb 100644 --- a/Deathmatch.Core/Commands/Loadouts/CommandLoadout.cs +++ b/Deathmatch.Core/Commands/Loadouts/CLoadout.cs @@ -5,27 +5,25 @@ using Microsoft.Extensions.Localization; using OpenMod.API.Commands; using OpenMod.API.Permissions; -using OpenMod.API.Prioritization; -using OpenMod.Core.Commands; using OpenMod.Unturned.Commands; using OpenMod.Unturned.Users; using System; namespace Deathmatch.Core.Commands.Loadouts { - [Command("loadout", Priority = Priority.Normal)] - [CommandSyntax(" ")] - [CommandDescription("Select your loadout for the given game mode.")] - [CommandActor(typeof(UnturnedUser))] - public class CommandLoadout : UnturnedCommand + //[Command("loadout", Priority = Priority.Normal)] + //[CommandSyntax(" ")] + //[CommandDescription("Select your loadout for the given game mode.")] + //[CommandActor(typeof(UnturnedUser))] + public abstract class CLoadout : UnturnedCommand { private readonly IGamePlayerManager _playerManager; private readonly ILoadoutManager _loadoutManager; private readonly ILoadoutSelector _loadoutSelector; - private readonly IStringLocalizer _stringLocalizer; + private readonly IStringLocalizer _stringLocalizer; private readonly IPermissionChecker _permissionChecker; - public CommandLoadout(IGamePlayerManager playerManager, + protected CLoadout(IGamePlayerManager playerManager, ILoadoutManager loadoutManager, ILoadoutSelector loadoutSelector, IStringLocalizer stringLocalizer, @@ -46,28 +44,18 @@ protected override async UniTask OnExecuteAsync() var gameMode = await Context.Parameters.GetAsync(0); var loadoutTitle = await Context.Parameters.GetAsync(1); - var category = _loadoutManager.GetCategory(gameMode); + var category = _loadoutManager.GetCategory(gameMode) ?? + throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_gamemode"]); - if (category == null) - { - throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_gamemode"]); - } - - var loadout = category.GetLoadout(loadoutTitle, false); - - if (loadout == null) - { - throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_loadout"]); - } + var loadout = category.GetLoadout(loadoutTitle, false) ?? + throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_loadout"]); - if (loadout.Permission != null && - await _permissionChecker.CheckPermissionAsync(Context.Actor, loadout.Permission) != - PermissionGrantResult.Grant) + if (!await loadout.IsPermitted(player.User)) { throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_permission"]); } - await _loadoutSelector.SetLoadout(player, category.Title, loadout.Title); + _loadoutSelector.SetSelectedLoadout(player, category, loadout); await PrintAsync(_stringLocalizer["commands:loadout:success", new { GameMode = category.Title, Loadout = loadout.Title }]); } diff --git a/Deathmatch.Core/Commands/Loadouts/CommandLoadouts.cs b/Deathmatch.Core/Commands/Loadouts/CLoadouts.cs similarity index 90% rename from Deathmatch.Core/Commands/Loadouts/CommandLoadouts.cs rename to Deathmatch.Core/Commands/Loadouts/CLoadouts.cs index 8389df0..1b648f9 100644 --- a/Deathmatch.Core/Commands/Loadouts/CommandLoadouts.cs +++ b/Deathmatch.Core/Commands/Loadouts/CLoadouts.cs @@ -18,14 +18,14 @@ namespace Deathmatch.Core.Commands.Loadouts [CommandSyntax("[game mode]")] [CommandDescription("View which loadouts you have unlocked.")] [CommandActor(typeof(UnturnedUser))] - public class CommandLoadouts : UnturnedCommand + public class CLoadouts : UnturnedCommand { private readonly IGamePlayerManager _playerManager; private readonly ILoadoutManager _loadoutManager; private readonly IStringLocalizer _stringLocalizer; private readonly IPermissionChecker _permissionChecker; - public CommandLoadouts(IGamePlayerManager playerManager, + public CLoadouts(IGamePlayerManager playerManager, ILoadoutManager loadoutManager, IStringLocalizer stringLocalizer, IPermissionChecker permissionChecker, @@ -43,12 +43,12 @@ private async Task> GetUnlockedLoadoutTitles(IPermissionActor actor foreach (var loadout in category.GetLoadouts()) { - if (loadout.Permission == null || - await _permissionChecker.CheckPermissionAsync(actor, loadout.Permission) == - PermissionGrantResult.Grant) + if (!await loadout.IsPermitted(actor)) { - loadouts.Add(loadout.Title); + continue; } + + loadouts.Add(loadout.Title); } return loadouts; diff --git a/Deathmatch.Core/Commands/Loadouts/CommandCreateLoadout.cs b/Deathmatch.Core/Commands/Loadouts/CommandCreateLoadout.cs deleted file mode 100644 index 9665fbf..0000000 --- a/Deathmatch.Core/Commands/Loadouts/CommandCreateLoadout.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Cysharp.Threading.Tasks; -using Deathmatch.API.Loadouts; -using Deathmatch.API.Players; -using Deathmatch.Core.Loadouts; -using Microsoft.Extensions.Localization; -using OpenMod.API.Commands; -using OpenMod.API.Permissions; -using OpenMod.API.Prioritization; -using OpenMod.Core.Commands; -using OpenMod.Unturned.Commands; -using OpenMod.Unturned.Users; -using System; - -namespace Deathmatch.Core.Commands.Loadouts -{ - [Command("createloadout", Priority = Priority.Normal)] - [CommandAlias("createl")] - [CommandAlias("cloadout")] - [CommandAlias("cl")] - [CommandAlias("addloadout")] - [CommandAlias("addl")] - [CommandAlias("aloadout")] - [CommandAlias("al")] - [CommandSyntax(" ")] - [CommandDescription("Creates a loadout from your current inventory.")] - [CommandActor(typeof(UnturnedUser))] - public class CommandCreateLoadout : UnturnedCommand - { - private readonly IGamePlayerManager _playerManager; - private readonly ILoadoutManager _loadoutManager; - private readonly IStringLocalizer _stringLocalizer; - private readonly IPermissionRegistry _permissionRegistry; - - public CommandCreateLoadout(IGamePlayerManager playerManager, - ILoadoutManager loadoutManager, - IStringLocalizer stringLocalizer, - IPermissionRegistry permissionRegistry, - IServiceProvider serviceProvider) : base(serviceProvider) - { - _playerManager = playerManager; - _loadoutManager = loadoutManager; - _stringLocalizer = stringLocalizer; - _permissionRegistry = permissionRegistry; - } - - protected override async UniTask OnExecuteAsync() - { - var player = _playerManager.GetPlayer((UnturnedUser)Context.Actor); - - var gameMode = await Context.Parameters.GetAsync(0); - var loadoutTitle = await Context.Parameters.GetAsync(1); - - var category = _loadoutManager.GetCategory(gameMode); - - if (category == null) - { - throw new UserFriendlyException(_stringLocalizer["commands:loadout:no_gamemode"]); - } - - var oldLoadout = category.GetLoadout(loadoutTitle); - - var title = loadoutTitle.ToLower(); - var permissionWithoutComponent = "loadouts." + title; - var permission = category.Component.OpenModComponentId + ":" + permissionWithoutComponent; - - var newLoadout = Loadout.FromPlayer(player, loadoutTitle.ToLower(), permission); - - if (oldLoadout != null) - { - category.RemoveLoadout(oldLoadout); - } - - _permissionRegistry.RegisterPermission(category.Component, permissionWithoutComponent); - - category.AddLoadout(newLoadout); - - await category.SaveLoadouts(); - - await PrintAsync(_stringLocalizer[ - "commands:create_loadout:success" + (oldLoadout == null ? "" : "_overwrite"), - new { Loadout = newLoadout.Title, GameMode = category.Title }]); - } - } -} diff --git a/Deathmatch.Core/Configuration/DeathmatchConfig.cs b/Deathmatch.Core/Configuration/DeathmatchConfig.cs new file mode 100644 index 0000000..52f229e --- /dev/null +++ b/Deathmatch.Core/Configuration/DeathmatchConfig.cs @@ -0,0 +1,14 @@ +using System; + +namespace Deathmatch.Core.Configuration +{ + [Serializable] + public class DeathmatchConfig + { + public float MatchInterval { get; set; } = 1800; + + public AutoAnnouncement[] AutoAnnouncements { get; set; } = new AutoAnnouncement[0]; + + public string[] DisabledCommands { get; set; } = new string[0]; + } +} diff --git a/Deathmatch.Core/Deathmatch.Core.csproj b/Deathmatch.Core/Deathmatch.Core.csproj index b50044b..0c7f74a 100644 --- a/Deathmatch.Core/Deathmatch.Core.csproj +++ b/Deathmatch.Core/Deathmatch.Core.csproj @@ -11,8 +11,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Deathmatch.Core/DeathmatchPlugin.cs b/Deathmatch.Core/DeathmatchPlugin.cs index 23ae1ef..471b6c4 100644 --- a/Deathmatch.Core/DeathmatchPlugin.cs +++ b/Deathmatch.Core/DeathmatchPlugin.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using OpenMod.API.Commands; using OpenMod.API.Plugins; using OpenMod.API.Users; using OpenMod.Core.Users; @@ -30,8 +31,6 @@ public class DeathmatchPlugin : OpenModUnturnedPlugin private readonly IMatchExecutor _matchExecutor; private readonly IPreservationManager _preservationManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; private readonly IStringLocalizer _stringLocalizer; private readonly IServiceProvider _serviceProvider; @@ -44,8 +43,6 @@ public DeathmatchPlugin(IMatchManager matchManager, IMatchExecutor matchExecutor, IPreservationManager preservationManager, IUserManager userManager, - ILogger logger, - IConfiguration configuration, IStringLocalizer stringLocalizer, IServiceProvider serviceProvider) : base(serviceProvider) { @@ -54,8 +51,6 @@ public DeathmatchPlugin(IMatchManager matchManager, _matchExecutor = matchExecutor; _preservationManager = preservationManager; _userManager = userManager; - _logger = logger; - _configuration = configuration; _stringLocalizer = stringLocalizer; _serviceProvider = serviceProvider; @@ -103,7 +98,7 @@ protected override async UniTask OnLoadAsync() foreach (var registration in matchRegistrations) { - _logger.LogInformation(_stringLocalizer["logs:registered_match", new { Registration = registration }]); + Logger.LogInformation(_stringLocalizer["logs:registered_match", new { Registration = registration }]); } _cancellationTokenSource = new CancellationTokenSource(); @@ -156,14 +151,14 @@ private async UniTask MatchAutoStartLoop() _loopStarted = true; - var delay = _configuration.GetValue("MatchInterval"); + var delay = Configuration.GetValue("MatchInterval"); if (delay <= 0) { return; } - var announcements = _configuration.GetSection("AutoAnnouncements").Get>() ?? + var announcements = Configuration.GetSection("AutoAnnouncements").Get>() ?? new List(); // Sorts descending @@ -201,7 +196,7 @@ private async UniTask MatchAutoStartLoop() if (registrations.Count == 0) { - _logger.LogCritical(_stringLocalizer["logs:no_registrations"]); + Logger.LogCritical(_stringLocalizer["logs:no_registrations"]); } else { @@ -232,10 +227,23 @@ await _userManager.BroadcastAsync(KnownActorTypes.Player, } } - if (_matchExecutor.CurrentMatch == null) + if (_matchExecutor.CurrentMatch != null) + { + continue; + } + + try { await _matchExecutor.StartMatch(registration); } + catch (UserFriendlyException ex) + { + Logger.LogError($"Unable to automatically start match: {ex.Message}"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Unable to automatically start match."); + } } } } diff --git a/Deathmatch.Core/Helpers/StringHelper.cs b/Deathmatch.Core/Helpers/StringHelper.cs index 1307281..a640d1e 100644 --- a/Deathmatch.Core/Helpers/StringHelper.cs +++ b/Deathmatch.Core/Helpers/StringHelper.cs @@ -10,7 +10,7 @@ public static class StringHelper public static T FindBestMatch(this IEnumerable enumerable, Func termSelector, string searchString) { return enumerable.Where(x => - termSelector(x).IndexOf(searchString, StringComparison.OrdinalIgnoreCase) >= 0) + (termSelector(x)?.IndexOf(searchString, StringComparison.OrdinalIgnoreCase) ?? -1) >= 0) .MinBy(asset => OpenMod.Core.Helpers.StringHelper.LevenshteinDistance(searchString, termSelector(asset))) .FirstOrDefault(); diff --git a/Deathmatch.Core/Items/Item.cs b/Deathmatch.Core/Items/Item.cs index 7bee59b..0a2f3ca 100644 --- a/Deathmatch.Core/Items/Item.cs +++ b/Deathmatch.Core/Items/Item.cs @@ -21,7 +21,7 @@ public Item() { Id = ""; Amount = 1; - Quality = byte.MaxValue; + Quality = 100; State = null; } @@ -57,7 +57,7 @@ public void SetState(byte[]? state) _cachedAsset = Assets.find(EAssetType.ITEM, parsed) as ItemAsset; } - return _cachedAsset ??= Assets.find(EAssetType.ITEM).OfType().FindBestMatch(x => x.itemName, Id); + return _cachedAsset ??= Assets.find(EAssetType.ITEM).OfType().Where(x => x != null).FindBestMatch(x => x.itemName, Id); } public virtual bool GiveToPlayer(IGamePlayer player) @@ -78,9 +78,9 @@ public virtual bool GiveToPlayer(IGamePlayer player) asset.id, Amount, Quality, - State == null + string.IsNullOrWhiteSpace(State) ? asset.getState(EItemOrigin.ADMIN) - : State.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray()); + : State!.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray()); player.Inventory.forceAddItem(item, true); diff --git a/Deathmatch.Core/Loadouts/BasicLoadout.cs b/Deathmatch.Core/Loadouts/BasicLoadout.cs new file mode 100644 index 0000000..b1c6377 --- /dev/null +++ b/Deathmatch.Core/Loadouts/BasicLoadout.cs @@ -0,0 +1,22 @@ +using Deathmatch.Core.Items; +using System; + +namespace Deathmatch.Core.Loadouts +{ + [Serializable] + public class BasicLoadout : LoadoutBase + { + public BasicLoadout() + { + } + + public BasicLoadout(string title, string? permission) : base(title, permission) + { + } + + protected override Item CreateItem(ushort id, byte amount, byte quality, byte[] state) + { + return new(id, amount, quality, state); + } + } +} diff --git a/Deathmatch.Core/Loadouts/Loadout.cs b/Deathmatch.Core/Loadouts/Loadout.cs deleted file mode 100644 index 5d966f2..0000000 --- a/Deathmatch.Core/Loadouts/Loadout.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Deathmatch.API.Players; -using SDG.Unturned; -using System; -using System.Collections.Generic; -using Item = Deathmatch.Core.Items.Item; - -namespace Deathmatch.Core.Loadouts -{ - [Serializable] - public class Loadout : LoadoutBase - { - public List Items { get; set; } = new(); - - public Loadout() : base("", null) - { - } - - public Loadout(string title, string? permission) : base(title, permission) - { - } - - public override IReadOnlyCollection GetItems() - { - return Items.AsReadOnly(); - } - - public static Loadout FromPlayer(IGamePlayer player, string title, string? permission) - { - var loadout = new Loadout(title, permission); - - var c = player.Clothing; - - if (c.backpack != 0) - { - loadout.Items.Add(new Item(c.backpack, 1, c.backpackQuality, c.backpackState)); - } - - if (c.glasses != 0) - { - loadout.Items.Add(new Item(c.glasses, 1, c.glassesQuality, c.glassesState)); - } - - if (c.hat != 0) - { - loadout.Items.Add(new Item(c.hat, 1, c.hatQuality, c.hatState)); - } - - if (c.mask != 0) - { - loadout.Items.Add(new Item(c.mask, 1, c.maskQuality, c.maskState)); - } - - if (c.pants != 0) - { - loadout.Items.Add(new Item(c.pants, 1, c.pantsQuality, c.pantsState)); - } - - if (c.shirt != 0) - { - loadout.Items.Add(new Item(c.shirt, 1, c.shirtQuality, c.shirtState)); - } - - if (c.vest != 0) - { - loadout.Items.Add(new Item(c.vest, 1, c.vestQuality, c.vestState)); - } - - for (byte page = 0; page < PlayerInventory.PAGES - 2; page++) - { - var count = player.Inventory.getItemCount(page); - - for (byte i = 0; i < count; i++) - { - var jar = player.Inventory.getItem(page, i); - - if (jar?.item == null) - { - continue; - } - - loadout.Items.Add(Item.FromUnturnedItem(jar.item)); - } - } - - return loadout; - } - } -} diff --git a/Deathmatch.Core/Loadouts/LoadoutBase.cs b/Deathmatch.Core/Loadouts/LoadoutBase.cs index 1d67bee..32221b9 100644 --- a/Deathmatch.Core/Loadouts/LoadoutBase.cs +++ b/Deathmatch.Core/Loadouts/LoadoutBase.cs @@ -1,38 +1,79 @@ -using Deathmatch.API.Loadouts; +using Cysharp.Threading.Tasks; +using Deathmatch.API.Loadouts; using Deathmatch.API.Players; -using Deathmatch.Core.Items; +using Microsoft.Extensions.DependencyInjection; +using OpenMod.API; +using OpenMod.API.Permissions; +using SDG.Unturned; +using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Item = Deathmatch.Core.Items.Item; namespace Deathmatch.Core.Loadouts { - public abstract class LoadoutBase : ILoadout + [Serializable] + public abstract class LoadoutBase : ILoadout where TItem : Item { + private class LoadoutServices + { + // ReSharper disable MemberCanBePrivate.Local + public readonly IOpenModComponent Component; + public readonly IPermissionChecker PermissionChecker; + public readonly IPermissionRegistry PermissionRegistry; + // ReSharper restore MemberCanBePrivate.Local + + public LoadoutServices(IOpenModComponent component, + IPermissionChecker permissionChecker, + IPermissionRegistry permissionRegistry) + { + Component = component; + PermissionChecker = permissionChecker; + PermissionRegistry = permissionRegistry; + } + + public static LoadoutServices CreateInstance(IServiceProvider serviceProvider) + { + return ActivatorUtilities.CreateInstance(serviceProvider); + } + } + public string Title { get; set; } public string? Permission { get; set; } + public ICollection Items { get; set; } = new List(); + + private LoadoutServices? _services; + + protected LoadoutBase() + { + Title = ""; + Permission = null; + } + protected LoadoutBase(string title, string? permission) { Title = title; Permission = permission; } - public abstract IReadOnlyCollection GetItems(); - - public string? GetPermissionWithoutComponent() + public virtual void ProvideServices(IServiceProvider serviceProvider) { - if (Permission == null) + _services = LoadoutServices.CreateInstance(serviceProvider); + + if (Permission != null) { - return null; + _services.PermissionRegistry.RegisterPermission(_services.Component, Permission, $"Grants access to the '{Title}' loadout."); } - - var index = Permission.IndexOf(':'); - - return index < 0 ? Permission : Permission.Substring(index + 1); } - public void GiveToPlayer(IGamePlayer player) + protected abstract TItem CreateItem(ushort id, byte amount, byte quality, byte[] state); + + public virtual async UniTask GiveToPlayer(IGamePlayer player) { + await UniTask.SwitchToMainThread(); + if (player.IsDead) { return; @@ -41,10 +82,74 @@ public void GiveToPlayer(IGamePlayer player) player.ClearInventory(); player.ClearClothing(); - foreach (var item in GetItems()) + foreach (var item in Items) { item.GiveToPlayer(player); } } + + public async UniTask LoadItemsFromPlayer(IGamePlayer player) + { + await UniTask.SwitchToMainThread(); + + Items.Clear(); + + void AddItem(ushort id, byte amount, byte quality, byte[] state) + { + if (id == 0) + { + return; + } + + var copiedState = new byte[state.Length]; + Array.Copy(state, copiedState, state.Length); + + var item = CreateItem(id, amount, quality, copiedState); + + Items.Add(item); + } + + var c = player.Clothing; + + AddItem(c.backpack, 1, c.backpackQuality, c.backpackState); + AddItem(c.glasses, 1, c.glassesQuality, c.glassesState); + AddItem(c.hat, 1, c.hatQuality, c.hatState); + AddItem(c.mask, 1, c.maskQuality, c.maskState); + AddItem(c.pants, 1, c.pantsQuality, c.pantsState); + AddItem(c.shirt, 1, c.shirtQuality, c.shirtState); + AddItem(c.vest, 1, c.vestQuality, c.vestState); + + for (byte page = 0; page < PlayerInventory.PAGES - 2; page++) + { + var count = player.Inventory.getItemCount(page); + + for (byte i = 0; i < count; i++) + { + var item = player.Inventory.getItem(page, i)?.item; + + if (item == null) + { + continue; + } + + AddItem(item.id, item.amount, item.quality, item.state); + } + } + } + + public async Task IsPermitted(IPermissionActor actor) + { + if (Permission == null) + { + return true; + } + + if (_services == null) + { + return false; + } + + return await _services.PermissionChecker.CheckPermissionAsync(actor, Permission) == PermissionGrantResult.Grant; + } } } diff --git a/Deathmatch.Core/Loadouts/LoadoutCategory.cs b/Deathmatch.Core/Loadouts/LoadoutCategory.cs deleted file mode 100644 index 57419b4..0000000 --- a/Deathmatch.Core/Loadouts/LoadoutCategory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Deathmatch.API.Loadouts; -using OpenMod.API; -using OpenMod.API.Persistence; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Deathmatch.Core.Loadouts -{ - public class LoadoutCategory : ILoadoutCategory - { - public string Title { get; } - - public IReadOnlyCollection Aliases { get; } - - public IOpenModComponent Component { get; } - - protected List Loadouts; - protected readonly IDataStore DataStore; - - protected const string DataStoreKey = "loadouts"; - - public LoadoutCategory(string title, IReadOnlyCollection? aliases, IOpenModComponent component, - IDataStore dataStore, List? loadouts = null) - { - Title = title; - Aliases = aliases ?? new List(); - - Component = component; - DataStore = dataStore; - - Loadouts = loadouts ?? new List(); - } - - public IReadOnlyCollection GetLoadouts() => Loadouts.AsReadOnly(); - - public virtual async Task LoadLoadouts() - { - var loadouts = new List(); - - if (await DataStore.ExistsAsync(DataStoreKey)) - { - loadouts.AddRange(await DataStore.LoadAsync>(DataStoreKey) ?? new List()); - } - - Loadouts = loadouts; - } - - public virtual async Task SaveLoadouts() - { - await DataStore.SaveAsync(DataStoreKey, Loadouts.OfType().ToList()); - } - - public virtual void AddLoadout(ILoadout loadout) - { - if (this.GetLoadout(loadout.Title) != null) - { - throw new ArgumentException("Loadout with given title already exists", nameof(loadout)); - } - - Loadouts.Add(loadout); - } - - public virtual bool RemoveLoadout(ILoadout loadout) - { - return Loadouts.Remove(loadout); - } - } -} diff --git a/Deathmatch.Core/Loadouts/LoadoutCategoryBase.cs b/Deathmatch.Core/Loadouts/LoadoutCategoryBase.cs new file mode 100644 index 0000000..799cb79 --- /dev/null +++ b/Deathmatch.Core/Loadouts/LoadoutCategoryBase.cs @@ -0,0 +1,109 @@ +using Deathmatch.API.Loadouts; +using Deathmatch.Core.Helpers; +using Deathmatch.Core.Items; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MoreLinq; +using OpenMod.API; +using OpenMod.API.Persistence; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Deathmatch.Core.Loadouts +{ + public abstract class LoadoutCategoryBase : ILoadoutCategory + where TItem : Item + where TLoadout : LoadoutBase + { + protected LoadoutCategoryBase(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + DataStore = serviceProvider.GetRequiredService(); + Logger = (ILogger)_serviceProvider.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType())); + + Component = serviceProvider.GetRequiredService(); + + Loadouts = Array.Empty(); + } + + public abstract string Title { get; } + + public abstract IReadOnlyCollection Aliases { get; } + + private readonly IServiceProvider _serviceProvider; + + protected readonly IDataStore DataStore; + protected readonly ILogger Logger; + + protected TLoadout[] Loadouts; + + public IOpenModComponent Component { get; } + + protected virtual string DataStoreKey => "loadouts"; + + ILoadout? ILoadoutCategory.GetDefaultLoadout() => GetDefaultLoadout(); + + IReadOnlyCollection ILoadoutCategory.GetLoadouts() => GetLoadouts(); + + public TLoadout? GetDefaultLoadout() + { + return Loadouts.Length > 0 ? Loadouts.RandomElement() : null; + } + + public IReadOnlyCollection GetLoadouts() + { + return Loadouts; + } + + public void AddLoadout(TLoadout loadout) + { + loadout.ProvideServices(_serviceProvider); + + Loadouts = Loadouts.Append(loadout).ToArray(); + } + + public bool RemoveLoadout(TLoadout loadout) + { + var oldLength = Loadouts.Length; + + Loadouts = Loadouts.Where(x => x != loadout).ToArray(); + + return Loadouts.Length != oldLength; + } + + public virtual async Task Load() + { + if (!await DataStore.ExistsAsync(DataStoreKey)) + { + Loadouts = Array.Empty(); + return; + } + + var loadouts = new List(); + var pendingLoadouts = await DataStore.LoadAsync(DataStoreKey) ?? Array.Empty(); + + foreach (var loadout in pendingLoadouts) + { + try + { + loadout.ProvideServices(_serviceProvider); + + loadouts.Add(loadout); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when loading loadout '{LoadoutTitle}'. Skipping this loadout.", loadout.Title); + } + } + + Loadouts = loadouts.ToArray(); + } + + public virtual async Task Save() + { + await DataStore.SaveAsync(DataStoreKey, Loadouts); + } + } +} diff --git a/Deathmatch.Core/Loadouts/LoadoutCategoryExtensions.cs b/Deathmatch.Core/Loadouts/LoadoutCategoryExtensions.cs new file mode 100644 index 0000000..938c431 --- /dev/null +++ b/Deathmatch.Core/Loadouts/LoadoutCategoryExtensions.cs @@ -0,0 +1,88 @@ +using Deathmatch.API.Loadouts; +using Deathmatch.API.Players; +using Deathmatch.Core.Helpers; +using OpenMod.API.Permissions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Deathmatch.Core.Loadouts +{ + public static class LoadoutCategoryExtensions + { + private static ILoadout? GetLoadoutInternal(ILoadoutCategory loadoutCategory, string title, bool exact = true) + { + var loadouts = loadoutCategory.GetLoadouts(); + + return exact + ? loadouts.FirstOrDefault(x => x.Title.Equals(title, StringComparison.OrdinalIgnoreCase)) + : loadouts.FindBestMatch(x => x.Title, title); + } + + public static ILoadout? GetLoadout(this ILoadoutCategory loadoutCategory, string title, bool exact = true) + { + return GetLoadoutInternal(loadoutCategory, title, exact); + } + + public static TLoadout? GetLoadout( + this ILoadoutCategory loadoutCategory, string title, bool exact = true) where TLoadout : class, ILoadout + { + return (TLoadout?)GetLoadoutInternal(loadoutCategory, title, exact); + } + + private static async Task GetRandomLoadoutInternal(ILoadoutCategory loadoutCategory, IGamePlayer player) + { + foreach (var randomLoadout in loadoutCategory.GetLoadouts().ToList().Shuffle()) + { + if (!await randomLoadout.IsPermitted(player.User)) + { + continue; + } + + return randomLoadout; + } + + return default; + } + + public static async Task GetRandomLoadout(this ILoadoutCategory loadoutCategory, IGamePlayer player) + { + return await GetRandomLoadoutInternal(loadoutCategory, player); + } + + public static async Task GetRandomLoadout( + this ILoadoutCategory loadoutCategory, IGamePlayer player) where TLoadout : class, ILoadout + { + return (TLoadout?)await GetRandomLoadoutInternal(loadoutCategory, player); + } + + private static async Task> GetLoadoutsInternal(ILoadoutCategory loadoutCategory, IPermissionActor actor) + { + var loadouts = new List(); + + foreach (var loadout in loadoutCategory.GetLoadouts()) + { + if (!await loadout.IsPermitted(actor)) + { + continue; + } + + loadouts.Add(loadout); + } + + return loadouts; + } + + public static async Task> GetLoadouts(ILoadoutCategory loadoutCategory, IPermissionActor actor) + { + return await GetLoadoutsInternal(loadoutCategory, actor); + } + + public static async Task> GetLoadouts( + ILoadoutCategory loadoutCategory, IPermissionActor actor) where TLoadout : ILoadout + { + return (await GetLoadoutsInternal(loadoutCategory, actor)).OfType().ToList(); + } + } +} diff --git a/Deathmatch.Core/Loadouts/LoadoutExtensions.cs b/Deathmatch.Core/Loadouts/LoadoutExtensions.cs deleted file mode 100644 index 1303490..0000000 --- a/Deathmatch.Core/Loadouts/LoadoutExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Deathmatch.API.Loadouts; -using Deathmatch.API.Players; -using Deathmatch.Core.Helpers; -using MoreLinq; -using MoreLinq.Extensions; -using OpenMod.API.Permissions; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace Deathmatch.Core.Loadouts -{ - public static class LoadoutExtensions - { - public static ILoadout? GetLoadout(this ILoadoutCategory loadoutCategory, string title, bool exact = true) - { - var loadouts = loadoutCategory.GetLoadouts(); - - return exact - ? loadouts.FirstOrDefault(x => x.Title.Equals(title, StringComparison.OrdinalIgnoreCase)) - : loadouts.FindBestMatch(x => x.Title, title); - } - - public static ILoadoutCategory? GetCategory(this ILoadoutManager loadoutManager, string title, bool exact = true) - { - var categories = loadoutManager.GetCategories(); - - if (exact) - { - return categories.FirstOrDefault(x => x.Title.Equals(title, StringComparison.OrdinalIgnoreCase)) ?? - categories.FirstOrDefault(x => - x.Aliases.Any(y => y.Equals(title, StringComparison.OrdinalIgnoreCase))); - } - - return categories.FindBestMatch(x => x.Title, title) ?? - categories.SelectMany(category => category.Aliases.Select(alias => (alias, category))) - .FindBestMatch(pair => pair.alias, title).category; - } - - public static async Task GetRandomLoadout(this ILoadoutManager loadoutManager, string categoryTitle, - IGamePlayer player, IPermissionChecker? permissionChecker = null) - { - var category = loadoutManager.GetCategory(categoryTitle); - - if (category == null) - { - return null; - } - - if (permissionChecker == null) - { - return category.GetLoadouts().RandomElement(); - } - - foreach (var randomLoadout in category.GetLoadouts().ToList().Shuffle()) - { - if (randomLoadout.Permission == null || - await permissionChecker.CheckPermissionAsync(player.User, randomLoadout.Permission) == - PermissionGrantResult.Grant) - { - return randomLoadout; - } - } - - return null; - } - } -} diff --git a/Deathmatch.Core/Loadouts/LoadoutManager.cs b/Deathmatch.Core/Loadouts/LoadoutManager.cs index 90acbc2..d007a87 100644 --- a/Deathmatch.Core/Loadouts/LoadoutManager.cs +++ b/Deathmatch.Core/Loadouts/LoadoutManager.cs @@ -4,7 +4,6 @@ using OpenMod.API.Prioritization; using System; using System.Collections.Generic; -using System.Linq; namespace Deathmatch.Core.Loadouts { diff --git a/Deathmatch.Core/Loadouts/LoadoutManagerExtensions.cs b/Deathmatch.Core/Loadouts/LoadoutManagerExtensions.cs new file mode 100644 index 0000000..2655dce --- /dev/null +++ b/Deathmatch.Core/Loadouts/LoadoutManagerExtensions.cs @@ -0,0 +1,51 @@ +using Deathmatch.API.Loadouts; +using Deathmatch.Core.Helpers; +using Deathmatch.Core.Items; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Deathmatch.Core.Loadouts +{ + public static class LoadoutManagerExtensions + { + public static ILoadoutCategory? GetCategory(this ILoadoutManager loadoutManager, Type categoryType) + { + var categories = loadoutManager.GetCategories(); + + return categories.FirstOrDefault(categoryType.IsInstanceOfType); + } + + public static ILoadoutCategory? GetCategory(this ILoadoutManager loadoutManager, string title, bool exact = true) + { + var categories = loadoutManager.GetCategories(); + + if (exact) + { + return categories.FirstOrDefault(x => x.Title.Equals(title, StringComparison.OrdinalIgnoreCase)) ?? + categories.FirstOrDefault(x => + x.Aliases.Any(y => y.Equals(title, StringComparison.OrdinalIgnoreCase))); + } + + return categories.FindBestMatch(x => x.Title, title) ?? + categories.SelectMany(category => category.Aliases.Select(alias => (alias, category))) + .FindBestMatch(pair => pair.alias, title).category; + } + + public static TLoadoutCategory? GetCategory(this ILoadoutManager loadoutManager) + where TLoadoutCategory : class, ILoadoutCategory + { + return (TLoadoutCategory?)loadoutManager.GetCategory(typeof(TLoadoutCategory)); + } + + public static async Task LoadAndAddCategory(this ILoadoutManager loadoutManager, + LoadoutCategoryBase loadoutCategory) + where TLoadout : LoadoutBase + where TItem : Item + { + await loadoutCategory.Load(); + + loadoutManager.AddCategory(loadoutCategory); + } + } +} diff --git a/Deathmatch.Core/Loadouts/LoadoutSelection.cs b/Deathmatch.Core/Loadouts/LoadoutSelection.cs deleted file mode 100644 index 113bee0..0000000 --- a/Deathmatch.Core/Loadouts/LoadoutSelection.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Deathmatch.Core.Loadouts -{ - public class LoadoutSelection - { - public string GameMode { get; set; } = ""; - - public string Loadout { get; set; } = ""; - } -} diff --git a/Deathmatch.Core/Loadouts/LoadoutSelector.cs b/Deathmatch.Core/Loadouts/LoadoutSelector.cs index fa45fe9..1b4c288 100644 --- a/Deathmatch.Core/Loadouts/LoadoutSelector.cs +++ b/Deathmatch.Core/Loadouts/LoadoutSelector.cs @@ -1,159 +1,122 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Loadouts; using Deathmatch.API.Players; -using Deathmatch.API.Players.Events; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using OpenMod.API.Ioc; +using OpenMod.API.Persistence; using OpenMod.API.Prioritization; -using OpenMod.API.Users; -using OpenMod.Common.Helpers; using OpenMod.Core.Helpers; +using OpenMod.Core.Plugins.Events; using SilK.Unturned.Extras.Events; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Deathmatch.Core.Loadouts { [ServiceImplementation(Lifetime = ServiceLifetime.Singleton, Priority = Priority.Lowest)] - public sealed class LoadoutSelector : ILoadoutSelector, - IInstanceEventListener, - IInstanceEventListener + public sealed class LoadoutSelector : ILoadoutSelector, IDisposable, + IInstanceEventListener { - private readonly ILoadoutManager _loadoutManager; - private readonly IUserDataStore _userDataStore; - private readonly ILogger _logger; + public class CategorySelections + { + public string CategoryTitle { get; set; } = ""; - private readonly Dictionary> _loadoutSelections; + public Dictionary Selections = new(); + } - public LoadoutSelector(ILoadoutManager loadoutManager, - IUserDataStore userDataStore, - IGamePlayerManager playerManager, - ILogger logger) - { - _loadoutManager = loadoutManager; - _userDataStore = userDataStore; - _logger = logger; + private IDataStore? _dataStore; + private readonly CancellationTokenSource _cts; + private List _categories; - _loadoutSelections = new Dictionary>(); + private bool _isDirty; - AsyncHelper.RunSync(async () => - { - var existingPlayers = playerManager.GetPlayers(); + private const string DataStoreKey = "loadoutselections"; - foreach (var user in existingPlayers) - { - await LoadPlayer(user); - } - }); + public LoadoutSelector() + { + _cts = new(); + _categories = new(); } - public ILoadout? GetLoadout(IGamePlayer player, string category) + public void Dispose() { - var loadoutCategory = _loadoutManager.GetCategory(category); + _cts.Cancel(); + } - if (loadoutCategory == null) + public async UniTask HandleEventAsync(object? sender, PluginLoadedEvent @event) + { + if (@event.Plugin.GetType() != typeof(DeathmatchPlugin)) { - return null; + return; } - var loadoutTitle = _loadoutSelections[player] - .FirstOrDefault(x => x.GameMode.Equals(loadoutCategory.Title, StringComparison.OrdinalIgnoreCase)) - ?.Loadout; + _dataStore = @event.Plugin.DataStore; - return loadoutTitle == null ? null : loadoutCategory.GetLoadout(loadoutTitle); + _categories = await Load(); + + AsyncHelper.Schedule($"{nameof(LoadoutSelector)} - {nameof(SaveLoop)}", SaveLoop); } - public async Task SetLoadout(IGamePlayer player, string category, string loadout) + private async Task SaveLoop() { - var loadoutCategory = _loadoutManager.GetCategory(category); - - if (loadoutCategory == null) return; - - var selections = _loadoutSelections[player]; - - var selection = - selections.FirstOrDefault(x => x.GameMode.Equals(loadoutCategory.Title, StringComparison.OrdinalIgnoreCase)); - - if (selection == null) + try { - selection = new LoadoutSelection() + var cancellationToken = _cts.Token; + + while (!_cts.IsCancellationRequested) { - GameMode = loadoutCategory.Title, - Loadout = loadout - }; + await Task.Delay(60000, cancellationToken); - selections.Add(selection); + if (_dataStore != null && _isDirty) + { + _isDirty = false; + await _dataStore.SaveAsync(DataStoreKey, _categories); + } + } } - else + catch (TaskCanceledException) { - selection.Loadout = loadout; } - - await _userDataStore.SetUserDataAsync(player.User.Id, player.User.Type, LoadoutSelectionsKey, - selections); } - private const string LoadoutSelectionsKey = "LoadoutSelections"; - - private async Task LoadPlayer(IGamePlayer player) + private async Task> Load() { - var userData = - await _userDataStore.GetUserDataAsync(player.User.Id, player.User.Type, - LoadoutSelectionsKey); - - List? selections = null; - - if (userData is List objects) + if (_dataStore == null) { - selections = objects; + throw new Exception("No data store to load spawns"); } - else if (userData is List other) - { - selections = other.Cast().ToList(); - } - - var loaded = new List(); - if (selections != null) - { - loaded.AddRange(selections.OfType()); + return await _dataStore.LoadAsync>(DataStoreKey) ?? new(); + } - loaded.AddRange(selections.OfType>() - .Select(selection => selection.ToObject()).Where(parsed => parsed != null)); - } + public ILoadout? GetSelectedLoadout(IGamePlayer player, ILoadoutCategory category) + { + var categorySelections = _categories.FirstOrDefault(x => x.CategoryTitle.Equals(category.Title)); - if (!_loadoutSelections.ContainsKey(player)) + if (categorySelections == null || !categorySelections.Selections.TryGetValue(player.SteamId.m_SteamID, out var selection)) { - _loadoutSelections.Add(player, loaded); - } - else - { - _loadoutSelections[player] = loaded; + return null; } + + return category.GetLoadout(selection); } - private async Task SavePlayer(IGamePlayer player) + public void SetSelectedLoadout(IGamePlayer player, ILoadoutCategory category, ILoadout loadout) { - if (_loadoutSelections.ContainsKey(player)) - { - await _userDataStore.SetUserDataAsync(player.User.Id, player.User.Type, LoadoutSelectionsKey, - _loadoutSelections[player]); + var categorySelections = _categories.FirstOrDefault(x => x.CategoryTitle.Equals(category.Title)); - _loadoutSelections.Remove(player); + if (categorySelections == null) + { + categorySelections = new CategorySelections {CategoryTitle = category.Title}; + _categories.Add(categorySelections); } - } - public async UniTask HandleEventAsync(object? sender, IGamePlayerConnectedEvent @event) - { - await LoadPlayer(@event.Player); - } + categorySelections.Selections[player.SteamId.m_SteamID] = loadout.Title; - public async UniTask HandleEventAsync(object? sender, IGamePlayerDisconnectedEvent @event) - { - await SavePlayer(@event.Player); + _isDirty = true; } } } diff --git a/Deathmatch.Core/Matches/MatchBase.cs b/Deathmatch.Core/Matches/MatchBase.cs index 71d0009..f404df4 100644 --- a/Deathmatch.Core/Matches/MatchBase.cs +++ b/Deathmatch.Core/Matches/MatchBase.cs @@ -1,19 +1,22 @@ using Autofac; using Cysharp.Threading.Tasks; +using Deathmatch.API.Loadouts; using Deathmatch.API.Matches; using Deathmatch.API.Matches.Registrations; using Deathmatch.API.Players; using Deathmatch.API.Preservation; +using Deathmatch.Core.Grace; +using Deathmatch.Core.Loadouts; using Deathmatch.Core.Matches.Events; -using Deathmatch.Core.Matches.Extensions; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; using Nito.AsyncEx; using OpenMod.API; using OpenMod.API.Eventing; +using OpenMod.API.Permissions; using OpenMod.API.Users; -using OpenMod.Core.Eventing; +using SilK.Unturned.Extras.Configuration; using SilK.Unturned.Extras.Events; using System; using System.Collections.Generic; @@ -23,16 +26,24 @@ namespace Deathmatch.Core.Matches { - public abstract class MatchBase : IMatch, IAsyncDisposable + public abstract class MatchBase : IMatch, IAsyncDisposable + where TConfig : class + where TLoadoutCategory : class, ILoadoutCategory { - protected static Random Rng = new(); + protected Random Rng = new(); protected readonly IOpenModComponent OpenModComponent; - protected readonly IConfiguration Configuration; + protected readonly IConfigurationParser Configuration; protected readonly IStringLocalizer StringLocalizer; protected readonly IPreservationManager PreservationManager; protected readonly IMatchExecutor MatchExecutor; protected readonly IUserManager UserManager; + protected readonly IGraceManager GraceManager; + protected readonly ILoadoutManager LoadoutManager; + protected readonly ILoadoutSelector LoadoutSelector; + protected readonly IPermissionChecker PermissionChecker; + protected readonly ILogger Logger; + protected readonly TLoadoutCategory LoadoutCategory; private readonly IEventBus _eventBus; private readonly IEventSubscriber _eventSubscriber; @@ -55,13 +66,22 @@ public abstract class MatchBase : IMatch, IAsyncDisposable protected MatchBase(IServiceProvider serviceProvider) { OpenModComponent = serviceProvider.GetRequiredService(); - Configuration = serviceProvider.GetRequiredService(); + Configuration = serviceProvider.GetRequiredService>(); StringLocalizer = serviceProvider.GetRequiredService(); PreservationManager = serviceProvider.GetRequiredService(); MatchExecutor = serviceProvider.GetRequiredService(); UserManager = serviceProvider.GetRequiredService(); + GraceManager = serviceProvider.GetRequiredService(); + LoadoutManager = serviceProvider.GetRequiredService(); + LoadoutSelector = serviceProvider.GetRequiredService(); + PermissionChecker = serviceProvider.GetRequiredService(); + LoadoutCategory = LoadoutManager.GetCategory() ?? + throw new Exception($"Cannot find category {nameof(TLoadoutCategory)}"); - _eventBus = serviceProvider.GetRequiredService(); + var loggerType = typeof(ILogger<>).MakeGenericType(GetType()); + Logger = (ILogger)serviceProvider.GetRequiredService(loggerType); + + _eventBus = serviceProvider.GetRequiredService(); _eventSubscriber = serviceProvider.GetRequiredService(); _players = new List(); @@ -100,21 +120,19 @@ public async UniTask StartAsync(IEnumerable players) try { + Logger.LogInformation("Starting match {Title}", Registration.Title); + await OnStartAsync(); await this.AddPlayers(players); + + Status = MatchStatus.InProgress; } catch { - Status = MatchStatus.Exception; + Status = MatchStatus.ExceptionWhenStarting; throw; } - - Status = MatchStatus.InProgress; - - var startedEvent = new MatchStartedEvent(this); - - await _eventBus.EmitAsync(OpenModComponent, this, startedEvent); } public async UniTask EndAsync() @@ -140,6 +158,8 @@ public async UniTask EndAsync() // End match + Logger.LogInformation("Ending match {Title}", Registration.Title); + _cancellationTokenSource.Cancel(); var players = _players.ToList(); @@ -161,7 +181,7 @@ public async UniTask EndAsync() } catch { - Status = MatchStatus.Exception; + Status = MatchStatus.ExceptionWhenEnding; throw; } @@ -190,7 +210,8 @@ public async UniTask AddPlayers(params IGamePlayer[] players) throw new Exception("Cannot add players. Match is ending."); case MatchStatus.Ended: throw new Exception("Cannot add players. Match has ended."); - case MatchStatus.Exception: + case MatchStatus.ExceptionWhenStarting: + case MatchStatus.ExceptionWhenEnding: throw new Exception("Cannot add players. Match is in exception state."); } diff --git a/Deathmatch.Core/Matches/MatchExecutor.cs b/Deathmatch.Core/Matches/MatchExecutor.cs index ae870e1..b3aaa1c 100644 --- a/Deathmatch.Core/Matches/MatchExecutor.cs +++ b/Deathmatch.Core/Matches/MatchExecutor.cs @@ -6,13 +6,12 @@ using Deathmatch.API.Players; using Deathmatch.Core.Helpers; using Deathmatch.Core.Matches.Events; -using Deathmatch.Core.Matches.Extensions; using Deathmatch.Core.Matches.Registrations; using Deathmatch.Core.Players.Events; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Nito.AsyncEx; using OpenMod.API; +using OpenMod.API.Commands; using OpenMod.API.Eventing; using OpenMod.API.Ioc; using OpenMod.API.Plugins; @@ -23,6 +22,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Deathmatch.Core.Matches { @@ -37,8 +37,6 @@ public class MatchExecutor : IMatchExecutor, private readonly IMatchManager _matchManager; private readonly IPluginActivator _pluginActivator; private readonly IStringLocalizerAccessor _stringLocalizer; - private readonly ILogger _logger; - private readonly ILifetimeScope _lifetimeScope; private readonly List _participants; private readonly AsyncLock _matchLock; @@ -47,17 +45,13 @@ public MatchExecutor(IRuntime runtime, IEventBus eventBus, IMatchManager matchManager, IPluginActivator pluginActivator, - IStringLocalizerAccessor stringLocalizer, - ILogger logger, - ILifetimeScope lifetimeScope) + IStringLocalizerAccessor stringLocalizer) { _runtime = runtime; _eventBus = eventBus; _matchManager = matchManager; _pluginActivator = pluginActivator; _stringLocalizer = stringLocalizer; - _logger = logger; - _lifetimeScope = lifetimeScope; _participants = new List(); _matchLock = new AsyncLock(); @@ -65,38 +59,38 @@ public MatchExecutor(IRuntime runtime, public IReadOnlyCollection GetParticipants() => _participants.AsReadOnly(); - public async UniTask AddParticipant(IGamePlayer player) + public async UniTask AddParticipant(IGamePlayer player) { - if (!_participants.Contains(player)) + if (_participants.Contains(player)) { - await UniTask.SwitchToMainThread(); + return false; + } - player.ClearMatchData(); + await UniTask.SwitchToMainThread(); - _participants.Add(player); + player.ClearMatchData(); - if (CurrentMatch != null && !CurrentMatch.Players.Contains(player)) - { - var preEvent = new GamePlayerJoiningMatchEvent(player, CurrentMatch); - await _eventBus.EmitAsync(_runtime, this, preEvent); - if (preEvent.IsCancelled) return; + _participants.Add(player); - await CurrentMatch.AddPlayer(player); + if (CurrentMatch == null || CurrentMatch.Players.Contains(player)) + { + return true; + } - player.CurrentMatch = CurrentMatch; + var joiningEvent = new GamePlayerJoiningMatchEvent(player, CurrentMatch); + await EmitEvent(joiningEvent); - var postEvent = new GamePlayerJoinedMatchEvent(player, CurrentMatch); - await _eventBus.EmitAsync(_runtime, this, postEvent); - } - else - { - await player.PrintMessageAsync(_stringLocalizer.GetInstance()["commands:join:success"]); - } - } - else + if (joiningEvent.IsCancelled) { - await player.PrintMessageAsync(_stringLocalizer.GetInstance()["commands:join:already"]); + return false; } + + await CurrentMatch.AddPlayer(player); + player.CurrentMatch = CurrentMatch; + + await EmitEvent(new GamePlayerJoinedMatchEvent(player, CurrentMatch)); + + return true; } public async UniTask RemoveParticipant(IGamePlayer player) @@ -124,105 +118,122 @@ public async UniTask RemoveParticipant(IGamePlayer player) } else { - await player.PrintMessageAsync(_stringLocalizer.GetInstance()["commands:leave:success"]); + await player.PrintMessageAsync(_stringLocalizer["commands:leave:success"]); } } else { - await player.PrintMessageAsync(_stringLocalizer.GetInstance()["commands:leave:already"]); + await player.PrintMessageAsync(_stringLocalizer["commands:leave:already"]); } } - public async UniTask StartMatch(IMatchRegistration? registration = null) + private void CheckMatchInstance() { - await UniTask.SwitchToThreadPool(); + if (CurrentMatch == null) + { + return; + } - using (await _matchLock.LockAsync()) + if (CurrentMatch.Status == MatchStatus.Initialized || CurrentMatch.Status == MatchStatus.Ended || + CurrentMatch.Status == MatchStatus.ExceptionWhenEnding) { - if (CurrentMatch != null && CurrentMatch.Status != MatchStatus.Initialized) - { - return false; - } + CurrentMatch = null; + } + } - await UniTask.SwitchToThreadPool(); + private IMatchRegistration GetRandomMatchRegistration() + { + var registrations = _matchManager.GetEnabledMatchRegistrations(); - IMatch? match = null; + if (registrations.Count == 0) + { + throw new UserFriendlyException(_stringLocalizer.GetInstance()["errors:no_registrations"]); + } - try - { - if (registration == null) - { - var registrations = _matchManager.GetEnabledMatchRegistrations(); + return registrations.RandomElement(); + } - if (registrations.Count == 0) - { - return false; - } + private ILifetimeScope GetScopeFromRegistration(IMatchRegistration registration) + { + var plugins = _pluginActivator.ActivatedPlugins; - registration = registrations.RandomElement(); - } + var plugin = plugins.FirstOrDefault(x => x.GetType().Assembly == registration.Type.Assembly); - var scope = _lifetimeScope; + return plugin?.LifetimeScope ?? throw new Exception("Could not get match game mode's plugin instance"); + } - // Use the plugin's lifetime scope - var plugin = _pluginActivator.ActivatedPlugins.FirstOrDefault(x => - x.GetType().Assembly == registration.Type.Assembly); + private void BuildMatchScope(ContainerBuilder builder, IMatchRegistration registration) + { + builder.Register(_ => new MatchRegistrationAccessor(registration)) + .AsSelf() + .As(); + + builder.RegisterType(registration.Type) + .AsSelf() + .As() + .SingleInstance() + .OwnedByLifetimeScope(); + } - if (plugin != null) - { - scope = plugin.LifetimeScope; - } + private IMatch CreateMatchInstance(IMatchRegistration registration) + { + // Use the plugin's lifetime scope + var scope = GetScopeFromRegistration(registration); - // Create scope + // Create child scope + var matchScope = scope.BeginLifetimeScopeEx(builder => BuildMatchScope(builder, registration)); - var matchScope = scope.BeginLifetimeScopeEx(builder => - { - builder.Register(_ => new MatchRegistrationAccessor(registration)) - .AsSelf() - .As(); + return (IMatch?)matchScope.Resolve(registration.Type) ?? + throw new Exception($"Unable to create instance of {registration.Type}."); + } - builder.RegisterType(registration.Type) - .AsSelf() - .As() - .SingleInstance() - .OwnedByLifetimeScope(); - }); + private async Task EmitEvent(IEvent @event) + { + await _eventBus.EmitAsync(_runtime, this, @event); + } - match = (IMatch?)matchScope.Resolve(registration.Type); - - if (match == null) - { - throw new Exception($"Unable to create instance of {registration.Type.Name}."); - } + public async UniTask StartMatch(IMatchRegistration? registration = null) + { + await UniTask.SwitchToThreadPool(); - // Emit MatchStartingEvent + using var matchLock = await _matchLock.LockAsync(); - var startingEvent = new MatchStartingEvent(match); + CheckMatchInstance(); - await _eventBus.EmitAsync(_runtime, this, startingEvent); + if (CurrentMatch != null) + { + return false; + } - if (startingEvent.IsCancelled) - { - return false; - } + try + { + registration ??= GetRandomMatchRegistration(); - // Start match + CurrentMatch = CreateMatchInstance(registration); - CurrentMatch = match; + // Emit MatchStartingEvent + var startingEvent = new MatchStartingEvent(CurrentMatch); + await EmitEvent(startingEvent); - await CurrentMatch.StartAsync(_participants); - } - catch (Exception ex) + // If start cancelled + if (startingEvent.IsCancelled) { - _logger.LogError(ex, "Exception occurred when attempting to start match"); - - return false; + throw new UserFriendlyException(_stringLocalizer["errors:match_start_cancelled"]); } - CurrentMatch = match; + // Start match + await CurrentMatch.StartAsync(_participants); + + // Emit MatchStartedEvent + await EmitEvent(new MatchStartedEvent(CurrentMatch)); return true; } + catch + { + CurrentMatch = null; + throw; + } } public UniTask HandleEventAsync(object? sender, IMatchEndedEvent @event) diff --git a/Deathmatch.Core/Matches/MatchManager.cs b/Deathmatch.Core/Matches/MatchManager.cs index 4c241b1..9cc9dff 100644 --- a/Deathmatch.Core/Matches/MatchManager.cs +++ b/Deathmatch.Core/Matches/MatchManager.cs @@ -25,8 +25,9 @@ public MatchManager() public IReadOnlyCollection GetMatchRegistrations() { - return MatchProviders.SelectMany(x => x.GetMatchRegistrations()).OrderBy(x => x.Priority, _priorityComparer) - .ToList().AsReadOnly(); + return MatchProviders.SelectMany(x => x.GetMatchRegistrations()) + .OrderBy(x => x.Priority, _priorityComparer) + .ToList(); } public void AddMatchProvider(IMatchProvider provider) diff --git a/Deathmatch.Core/Players/GamePlayer.cs b/Deathmatch.Core/Players/GamePlayer.cs index c7c1b92..1fe57d7 100644 --- a/Deathmatch.Core/Players/GamePlayer.cs +++ b/Deathmatch.Core/Players/GamePlayer.cs @@ -3,6 +3,7 @@ using OpenMod.Unturned.Users; using SDG.Unturned; using Steamworks; +using System; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; @@ -26,7 +27,7 @@ internal GamePlayer(UnturnedUser user) public bool IsInActiveMatch() { - return CurrentMatch != null && CurrentMatch.Status == MatchStatus.InProgress; + return CurrentMatch is { Status: MatchStatus.InProgress }; } public T? GetMatchData(string key) @@ -65,7 +66,7 @@ public void SetMatchData(string key, T value) public PlayerClothing Clothing => Player.clothing; - private static readonly byte[] EmptyArray = new byte[0]; + private static readonly byte[] EmptyArray = Array.Empty(); public void ClearClothing() => Clothing.updateClothes( 0, 0, EmptyArray, @@ -91,6 +92,9 @@ public void ClearInventory() Inventory.removeItem(page, 0); } } + + Player.equipment.sendSlot(0); + Player.equipment.sendSlot(1); } public PlayerLife Life => Player.life; diff --git a/Deathmatch.Core/Spawns/SpawnDirectory.cs b/Deathmatch.Core/Spawns/SpawnDirectory.cs new file mode 100644 index 0000000..6ad488d --- /dev/null +++ b/Deathmatch.Core/Spawns/SpawnDirectory.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using OpenMod.API.Persistence; +using OpenMod.Core.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Deathmatch.Core.Spawns +{ + public class SpawnDirectory + { + private readonly IDataStore _dataStore; + + private List _spawns = new(); + + public SpawnDirectory(IServiceProvider serviceProvider) + { + _dataStore = serviceProvider.GetRequiredService(); + + AsyncHelper.RunSync(LoadSpawns); + } + + protected virtual string DataStoreKey => "spawns"; + + public IReadOnlyCollection Spawns => _spawns.AsReadOnly(); + + public async Task LoadSpawns() + { + var loadedList = await _dataStore.ExistsAsync(DataStoreKey) ? await _dataStore.LoadAsync>(DataStoreKey) : null; + + _spawns = loadedList ?? new(); + } + + public async Task SaveSpawns(IEnumerable spawns) + { + var playerSpawns = spawns.ToList(); + + await _dataStore.SaveAsync(DataStoreKey, playerSpawns); + + _spawns = playerSpawns; + } + } +} diff --git a/Deathmatch.Core/translations.yaml b/Deathmatch.Core/translations.yaml index 1ea31e0..475b6f3 100644 --- a/Deathmatch.Core/translations.yaml +++ b/Deathmatch.Core/translations.yaml @@ -2,7 +2,7 @@ commands: disabled_during_match: "This command is disabled during matches." dmstart: success: "Successfully started a {Title} match." - failure: "Failed to start match, one may already be running." + match_running: "Failed to start match - a match is running." not_found: "Failed to start match, the specified match does not exist." dmend: success: "Successfully ended match." @@ -11,6 +11,7 @@ commands: join: success: "Successfully joined the match/match pool." already: "You've already joined the match/match pool." + failure: "Unable to join the match/match pool." leave: success: "Successfully left the match/match pool." already: "You're not a part of the match/match pool." @@ -34,6 +35,9 @@ commands: announcements: planned_match: "A game of {Match.Title} will start in {Time}! Make sure to /join!" +errors: + no_registrations: "Match could not be started - no match registrations are enabled." + match_start_cancelled: "Match could not be started - a plugin cancelled the starting." + logs: - registered_match: "Loaded match registration: {Registration.Title} (Enabled: {Registration.Enabled}) - {Registration.Description}" - no_registrations: "No match registrations are loaded. Can't auto start a match." \ No newline at end of file + registered_match: "Loaded match registration: {Registration.Title} (Enabled: {Registration.Enabled}) - {Registration.Description}" \ No newline at end of file diff --git a/FreeForAll/Commands/CommandFFA.cs b/FreeForAll/Commands/CFreeForAll.cs similarity index 69% rename from FreeForAll/Commands/CommandFFA.cs rename to FreeForAll/Commands/CFreeForAll.cs index eab7bff..c61052d 100644 --- a/FreeForAll/Commands/CommandFFA.cs +++ b/FreeForAll/Commands/CFreeForAll.cs @@ -7,12 +7,13 @@ namespace FreeForAll.Commands { [Command("ffa")] - [CommandSyntax("")] + [CommandAlias("freeforall")] + [CommandSyntax("")] [CommandDescription("Manage the Free For All game mode.")] [CommandActor(typeof(UnturnedUser))] - public class CommandFFA : UnturnedCommand + public class CFreeForAll : UnturnedCommand { - public CommandFFA(IServiceProvider serviceProvider) : base(serviceProvider) + public CFreeForAll(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/FreeForAll/Commands/Loadouts/CLoadouts.cs b/FreeForAll/Commands/Loadouts/CLoadouts.cs new file mode 100644 index 0000000..d84e8f4 --- /dev/null +++ b/FreeForAll/Commands/Loadouts/CLoadouts.cs @@ -0,0 +1,24 @@ +using Cysharp.Threading.Tasks; +using OpenMod.Core.Commands; +using OpenMod.Unturned.Commands; +using System; + +namespace FreeForAll.Commands.Loadouts +{ + [Command("loadouts")] + [CommandAlias("loadout")] + [CommandDescription("Manages Team Deathmatch loadouts.")] + [CommandSyntax("<[a]dd/[r]emove> ")] + [CommandParent(typeof(CFreeForAll))] + public class CLoadouts : UnturnedCommand + { + public CLoadouts(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override UniTask OnExecuteAsync() + { + throw new CommandWrongUsageException(Context); + } + } +} \ No newline at end of file diff --git a/FreeForAll/Commands/Loadouts/CLoadoutsAdd.cs b/FreeForAll/Commands/Loadouts/CLoadoutsAdd.cs new file mode 100644 index 0000000..25fd50b --- /dev/null +++ b/FreeForAll/Commands/Loadouts/CLoadoutsAdd.cs @@ -0,0 +1,27 @@ +using Deathmatch.Core.Commands.Loadouts.Base; +using Deathmatch.Core.Items; +using Deathmatch.Core.Loadouts; +using FreeForAll.Loadouts; +using OpenMod.Core.Commands; +using System; + +namespace FreeForAll.Commands.Loadouts +{ + [Command("add")] + [CommandAlias("a")] + [CommandAlias("+")] + [CommandDescription("Adds a new Free For All loadout.")] + [CommandSyntax("<title>")] + [CommandParent(typeof(CLoadouts))] + public class CLoadoutsAdd : AddLoadoutCommand<FFALoadoutCategory, BasicLoadout, Item> + { + public CLoadoutsAdd(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override BasicLoadout CreateLoadout(string title, string permission) + { + return new(title, permission); + } + } +} \ No newline at end of file diff --git a/FreeForAll/Commands/Loadouts/CLoadoutsRemove.cs b/FreeForAll/Commands/Loadouts/CLoadoutsRemove.cs new file mode 100644 index 0000000..a0c7e92 --- /dev/null +++ b/FreeForAll/Commands/Loadouts/CLoadoutsRemove.cs @@ -0,0 +1,23 @@ +using Deathmatch.Core.Commands.Loadouts.Base; +using Deathmatch.Core.Items; +using Deathmatch.Core.Loadouts; +using FreeForAll.Loadouts; +using OpenMod.Core.Commands; +using System; + +namespace FreeForAll.Commands.Loadouts +{ + [Command("remove")] + [CommandAlias("rem")] + [CommandAlias("r")] + [CommandAlias("-")] + [CommandDescription("Removes a Free For All loadout.")] + [CommandSyntax("<title>")] + [CommandParent(typeof(CLoadouts))] + public class CLoadoutsRemove : RemoveLoadoutCommand<FFALoadoutCategory, BasicLoadout, Item> + { + public CLoadoutsRemove(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + } +} \ No newline at end of file diff --git a/FreeForAll/Commands/Spawns/CommandSpawns.cs b/FreeForAll/Commands/Spawns/CSpawns.cs similarity index 72% rename from FreeForAll/Commands/Spawns/CommandSpawns.cs rename to FreeForAll/Commands/Spawns/CSpawns.cs index 06fa676..365ea3e 100644 --- a/FreeForAll/Commands/Spawns/CommandSpawns.cs +++ b/FreeForAll/Commands/Spawns/CSpawns.cs @@ -9,10 +9,10 @@ namespace FreeForAll.Commands.Spawns [CommandAlias("spawn")] [CommandDescription("Manages FFA spawns.")] [CommandSyntax("<[a]dd/[r]emove/[l]ist/[c]lear>")] - [CommandParent(typeof(CommandFFA))] - public class CommandSpawns : UnturnedCommand + [CommandParent(typeof(CFreeForAll))] + public class CSpawns : UnturnedCommand { - public CommandSpawns(IServiceProvider serviceProvider) : base(serviceProvider) + public CSpawns(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/FreeForAll/Commands/Spawns/CSpawnsAction.cs b/FreeForAll/Commands/Spawns/CSpawnsAction.cs new file mode 100644 index 0000000..25f31d5 --- /dev/null +++ b/FreeForAll/Commands/Spawns/CSpawnsAction.cs @@ -0,0 +1,35 @@ +using Cysharp.Threading.Tasks; +using Deathmatch.Core.Spawns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using OpenMod.Unturned.Commands; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FreeForAll.Commands.Spawns +{ + public abstract class CSpawnsAction : UnturnedCommand + { + protected readonly IStringLocalizer StringLocalizer; + protected readonly SpawnDirectory SpawnDirectory; + + protected CSpawnsAction(IServiceProvider serviceProvider) : base(serviceProvider) + { + StringLocalizer = serviceProvider.GetRequiredService<IStringLocalizer>(); + SpawnDirectory = serviceProvider.GetRequiredService<SpawnDirectory>(); + } + + public string SpawnsKey => FreeForAllPlugin.SpawnsKey; + + protected List<PlayerSpawn> GetSpawns() + { + return SpawnDirectory.Spawns.ToList(); + } + + protected async UniTask SaveSpawns(IEnumerable<PlayerSpawn> spawns) + { + await SpawnDirectory.SaveSpawns(spawns); + } + } +} diff --git a/FreeForAll/Commands/Spawns/CommandSpawnsAdd.cs b/FreeForAll/Commands/Spawns/CSpawnsAdd.cs similarity index 55% rename from FreeForAll/Commands/Spawns/CommandSpawnsAdd.cs rename to FreeForAll/Commands/Spawns/CSpawnsAdd.cs index 7f82461..958b955 100644 --- a/FreeForAll/Commands/Spawns/CommandSpawnsAdd.cs +++ b/FreeForAll/Commands/Spawns/CSpawnsAdd.cs @@ -1,10 +1,8 @@ using Cysharp.Threading.Tasks; using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; using OpenMod.Unturned.Users; using System; -using System.Collections.Generic; namespace FreeForAll.Commands.Spawns { @@ -13,24 +11,22 @@ namespace FreeForAll.Commands.Spawns [CommandAlias("+")] [CommandDescription("Add a spawn.")] [CommandSyntax("")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsAdd : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsAdd : CSpawnsAction { - public CommandSpawnsAdd(FreeForAllPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsAdd(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync() { - List<PlayerSpawn> spawns = await LoadSpawnsAsync(); + var spawns = GetSpawns(); var spawn = new PlayerSpawn((UnturnedUser)Context.Actor); spawns.Add(spawn); - await SaveSpawnsAsync(spawns); + await SaveSpawns(spawns); await PrintAsync(StringLocalizer["commands:spawns:add:success"]); } diff --git a/FreeForAll/Commands/Spawns/CSpawnsClear.cs b/FreeForAll/Commands/Spawns/CSpawnsClear.cs new file mode 100644 index 0000000..f875f62 --- /dev/null +++ b/FreeForAll/Commands/Spawns/CSpawnsClear.cs @@ -0,0 +1,26 @@ +using Cysharp.Threading.Tasks; +using Deathmatch.Core.Spawns; +using OpenMod.Core.Commands; +using System; + +namespace FreeForAll.Commands.Spawns +{ + [Command("clear")] + [CommandAlias("c")] + [CommandDescription("Removes all spawns.")] + [CommandSyntax("")] + [CommandParent(typeof(CSpawns))] + public class CSpawnsClear : CSpawnsAction + { + public CSpawnsClear(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override async UniTask OnExecuteAsync() + { + await SaveSpawns(Array.Empty<PlayerSpawn>()); + + await PrintAsync(StringLocalizer["commands:spawns:clear:success"]); + } + } +} \ No newline at end of file diff --git a/FreeForAll/Commands/Spawns/CSpawnsList.cs b/FreeForAll/Commands/Spawns/CSpawnsList.cs new file mode 100644 index 0000000..772d46a --- /dev/null +++ b/FreeForAll/Commands/Spawns/CSpawnsList.cs @@ -0,0 +1,37 @@ +using Cysharp.Threading.Tasks; +using OpenMod.Core.Commands; +using System; + +namespace FreeForAll.Commands.Spawns +{ + [Command("list")] + [CommandAlias("l")] + [CommandDescription("Lists the spawns.")] + [CommandSyntax("")] + [CommandParent(typeof(CSpawns))] + public class CSpawnsList : CSpawnsAction + { + public CSpawnsList(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override async UniTask OnExecuteAsync() + { + var spawns = GetSpawns(); + + var localizedSpawns = new string[spawns.Count]; + + for (var i = 0; i < localizedSpawns.Length; i++) + { + localizedSpawns[i] = StringLocalizer["commands:spawns:list:element", + new { I = i, spawns[i].X, spawns[i].Y, spawns[i].Z }]; + } + + var list = string.Join(StringLocalizer["commands:spawns:list:delimiter"], localizedSpawns); + + var output = StringLocalizer["commands:spawns:list:header", new { List = list }]; + + await PrintAsync(output); + } + } +} \ No newline at end of file diff --git a/FreeForAll/Commands/Spawns/CommandSpawnsRemove.cs b/FreeForAll/Commands/Spawns/CSpawnsRemove.cs similarity index 62% rename from FreeForAll/Commands/Spawns/CommandSpawnsRemove.cs rename to FreeForAll/Commands/Spawns/CSpawnsRemove.cs index 70c265c..7761932 100644 --- a/FreeForAll/Commands/Spawns/CommandSpawnsRemove.cs +++ b/FreeForAll/Commands/Spawns/CSpawnsRemove.cs @@ -1,10 +1,7 @@ using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.API.Commands; using OpenMod.Core.Commands; using System; -using System.Collections.Generic; namespace FreeForAll.Commands.Spawns { @@ -14,18 +11,16 @@ namespace FreeForAll.Commands.Spawns [CommandAlias("-")] [CommandDescription("Remove a spawn.")] [CommandSyntax("<index>")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsRemove : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsRemove : CSpawnsAction { - public CommandSpawnsRemove(FreeForAllPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsRemove(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync() { - int index = await Context.Parameters.GetAsync<int>(0); + var index = await Context.Parameters.GetAsync<int>(0); // A check which we can call before loading from the disk if (index <= 0) @@ -33,7 +28,7 @@ protected override async UniTask OnExecuteAsync() throw new UserFriendlyException(StringLocalizer["commands:spawns:remove:index_out_of_bounds"]); } - List<PlayerSpawn> spawns = await LoadSpawnsAsync(); + var spawns = GetSpawns(); if (index >= spawns.Count) { @@ -42,7 +37,7 @@ protected override async UniTask OnExecuteAsync() spawns.RemoveAt(index); - await SaveSpawnsAsync(spawns); + await SaveSpawns(spawns); await PrintAsync(StringLocalizer["commands:spawns:remove:success"]); } diff --git a/FreeForAll/Commands/Spawns/CommandSpawnsAction.cs b/FreeForAll/Commands/Spawns/CommandSpawnsAction.cs deleted file mode 100644 index 92ed5c7..0000000 --- a/FreeForAll/Commands/Spawns/CommandSpawnsAction.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; -using OpenMod.Unturned.Commands; -using System; -using System.Collections.Generic; - -namespace FreeForAll.Commands.Spawns -{ - public abstract class CommandSpawnsAction : UnturnedCommand - { - protected readonly FreeForAllPlugin Plugin; - protected readonly IStringLocalizer StringLocalizer; - - protected CommandSpawnsAction(FreeForAllPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(serviceProvider) - { - Plugin = plugin; - StringLocalizer = stringLocalizer; - } - - public string SpawnsKey => FreeForAllPlugin.SpawnsKey; - - protected async UniTask<List<PlayerSpawn>> LoadSpawnsAsync() - { - var list = await Plugin.DataStore.ExistsAsync(SpawnsKey) - ? await Plugin.DataStore.LoadAsync<List<PlayerSpawn>>(SpawnsKey) - : null; - - return list ?? new List<PlayerSpawn>(); - } - - protected async UniTask SaveSpawnsAsync(List<PlayerSpawn> spawns) - { - await Plugin.DataStore.SaveAsync(SpawnsKey, spawns); - - await Plugin.ReloadSpawns(); - } - } -} diff --git a/FreeForAll/Commands/Spawns/CommandSpawnsClear.cs b/FreeForAll/Commands/Spawns/CommandSpawnsClear.cs deleted file mode 100644 index 646b179..0000000 --- a/FreeForAll/Commands/Spawns/CommandSpawnsClear.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; -using OpenMod.Core.Commands; -using System; -using System.Collections.Generic; - -namespace FreeForAll.Commands.Spawns -{ - [Command("clear")] - [CommandAlias("c")] - [CommandDescription("Removes all spawns.")] - [CommandSyntax("")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsClear : CommandSpawnsAction - { - public CommandSpawnsClear(FreeForAllPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) - { - } - - protected override async UniTask OnExecuteAsync() - { - List<PlayerSpawn> spawns = new List<PlayerSpawn>(); - - await SaveSpawnsAsync(spawns); - - await PrintAsync(StringLocalizer["commands:spawns:clear:success"]); - } - } -} \ No newline at end of file diff --git a/FreeForAll/Commands/Spawns/CommandSpawnsList.cs b/FreeForAll/Commands/Spawns/CommandSpawnsList.cs deleted file mode 100644 index 30689d6..0000000 --- a/FreeForAll/Commands/Spawns/CommandSpawnsList.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; -using OpenMod.Core.Commands; -using System; -using System.Collections.Generic; - -namespace FreeForAll.Commands.Spawns -{ - [Command("list")] - [CommandAlias("l")] - [CommandDescription("Lists the spawns.")] - [CommandSyntax("")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsList : CommandSpawnsAction - { - public CommandSpawnsList(FreeForAllPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) - { - } - - protected override async UniTask OnExecuteAsync() - { - List<PlayerSpawn> spawns = await LoadSpawnsAsync(); - - string[] localizedSpawns = new string[spawns.Count]; - - for (int i = 0; i < localizedSpawns.Length; i++) - { - localizedSpawns[i] = StringLocalizer["commands:spawns:list:element", - new { I = i, spawns[i].X, spawns[i].Y, spawns[i].Z }]; - } - - string list = string.Join(StringLocalizer["commands:spawns:list:delimiter"], localizedSpawns); - - string output = StringLocalizer["commands:spawns:list:header", new { List = list }]; - - await PrintAsync(output); - } - } -} \ No newline at end of file diff --git a/FreeForAll/Configuration/AutoRespawnConfig.cs b/FreeForAll/Configuration/AutoRespawnConfig.cs new file mode 100644 index 0000000..d92678f --- /dev/null +++ b/FreeForAll/Configuration/AutoRespawnConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace FreeForAll.Configuration +{ + [Serializable] + public class AutoRespawnConfig + { + public bool Enabled { get; set; } = true; + + public float Delay { get; set; } = 0; + } +} diff --git a/FreeForAll/Configuration/FreeForAllConfig.cs b/FreeForAll/Configuration/FreeForAllConfig.cs new file mode 100644 index 0000000..705a06a --- /dev/null +++ b/FreeForAll/Configuration/FreeForAllConfig.cs @@ -0,0 +1,18 @@ +using System; + +namespace FreeForAll.Configuration +{ + [Serializable] + public class FreeForAllConfig + { + public AutoRespawnConfig AutoRespawn { get; set; } = new(); + + public int KillThreshold { get; set; } = 30; + + public float MaxDuration { get; set; } = 600; + + public float GracePeriod { get; set; } = 2; + + public GameRewardsConfig Rewards { get; set; } = new(); + } +} diff --git a/FreeForAll/Configuration/GameRewardsConfig.cs b/FreeForAll/Configuration/GameRewardsConfig.cs new file mode 100644 index 0000000..5e4d1d5 --- /dev/null +++ b/FreeForAll/Configuration/GameRewardsConfig.cs @@ -0,0 +1,19 @@ +using Deathmatch.Core.Items; +using System; + +namespace FreeForAll.Configuration +{ + [Serializable] + public class GameRewardsConfig + { + public int MinimumPlayers { get; set; } = 5; + + public ChanceItem[] Winners { get; set; } = new ChanceItem[0]; + + public ChanceItem[] Losers { get; set; } = new ChanceItem[0]; + + public ChanceItem[] Tied { get; set; } = new ChanceItem[0]; + + public ChanceItem[] All { get; set; } = new ChanceItem[0]; + } +} diff --git a/FreeForAll/Configuration/RewardConfig.cs b/FreeForAll/Configuration/RewardConfig.cs new file mode 100644 index 0000000..01cecee --- /dev/null +++ b/FreeForAll/Configuration/RewardConfig.cs @@ -0,0 +1,14 @@ +using System; + +namespace FreeForAll.Configuration +{ + [Serializable] + public class RewardConfig + { + public string Id { get; set; } = ""; + + public int Amount { get; set; } = 1; + + public int Chance { get; set; } = 1; + } +} diff --git a/FreeForAll/FreeForAll.csproj b/FreeForAll/FreeForAll.csproj index 71e70e5..8b8370e 100644 --- a/FreeForAll/FreeForAll.csproj +++ b/FreeForAll/FreeForAll.csproj @@ -11,8 +11,8 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="OpenMod.Unturned" Version="3.1.2" /> - <PackageReference Include="SilK.Unturned.Extras" Version="1.6.5" /> + <PackageReference Include="OpenMod.Unturned" Version="3.2.2" /> + <PackageReference Include="SilK.Unturned.Extras" Version="1.7.0" /> </ItemGroup> <ItemGroup> diff --git a/FreeForAll/FreeForAllPlugin.cs b/FreeForAll/FreeForAllPlugin.cs index d80a3ee..c072d2b 100644 --- a/FreeForAll/FreeForAllPlugin.cs +++ b/FreeForAll/FreeForAllPlugin.cs @@ -1,16 +1,10 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Loadouts; using Deathmatch.Core.Loadouts; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Configuration; -using OpenMod.API.Permissions; -using OpenMod.API.Persistence; +using FreeForAll.Loadouts; using OpenMod.API.Plugins; using OpenMod.Unturned.Plugins; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; [assembly: PluginMetadata("FreeForAll", DisplayName = "Free For All")] namespace FreeForAll @@ -18,73 +12,20 @@ namespace FreeForAll public class FreeForAllPlugin : OpenModUnturnedPlugin { public const string SpawnsKey = "spawns"; - - private readonly IConfiguration _configuration; + private readonly ILoadoutManager _loadoutManager; - private readonly IDataStore _dataStore; - private readonly IPermissionRegistry _permissionRegistry; - - private readonly List<PlayerSpawn> _spawns; + private readonly IServiceProvider _serviceProvider; - public FreeForAllPlugin(IConfiguration configuration, - ILoadoutManager loadoutManager, - IDataStore dataStore, - IPermissionRegistry permissionRegistry, + public FreeForAllPlugin(ILoadoutManager loadoutManager, IServiceProvider serviceProvider) : base(serviceProvider) { - _configuration = configuration; _loadoutManager = loadoutManager; - _dataStore = dataStore; - _permissionRegistry = permissionRegistry; - - _spawns = new List<PlayerSpawn>(); + _serviceProvider = serviceProvider; } protected override async UniTask OnLoadAsync() { - await ReloadSpawns(); - - var category = - new LoadoutCategory("Free For All", new List<string> { "FreeForAll", "FFA" }, this, _dataStore); - await category.LoadLoadouts(); - - foreach (var loadout in category.GetLoadouts().OfType<Loadout>()) - { - var permission = loadout.GetPermissionWithoutComponent(); - - if (permission != null) - { - _permissionRegistry.RegisterPermission(this, permission); - } - } - - _loadoutManager.AddCategory(category); - } - - protected override UniTask OnUnloadAsync() - { - return UniTask.CompletedTask; + await _loadoutManager.LoadAndAddCategory(new FFALoadoutCategory(_serviceProvider)); } - - public IReadOnlyCollection<PlayerSpawn> Spawns => _spawns.AsReadOnly(); - - public async Task ReloadSpawns() - { - async Task LoadList<T>(string key, List<T> list) - { - var loadedList = await DataStore.ExistsAsync(key) ? await DataStore.LoadAsync<List<T>>(key) : null; - - loadedList ??= new List<T>(); - - list.Clear(); - list.AddRange(loadedList); - } - - await LoadList(SpawnsKey, _spawns); - } - - public IReadOnlyCollection<Loadout> Loadouts => - _configuration.GetSection("Loadouts").Get<List<Loadout>>() ?? - new List<Loadout>(); } } diff --git a/FreeForAll/Loadouts/FFALoadoutCategory.cs b/FreeForAll/Loadouts/FFALoadoutCategory.cs new file mode 100644 index 0000000..6a98e3f --- /dev/null +++ b/FreeForAll/Loadouts/FFALoadoutCategory.cs @@ -0,0 +1,18 @@ +using Deathmatch.Core.Items; +using Deathmatch.Core.Loadouts; +using System; +using System.Collections.Generic; + +namespace FreeForAll.Loadouts +{ + public class FFALoadoutCategory : LoadoutCategoryBase<BasicLoadout, Item> + { + public FFALoadoutCategory(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override string Title => "Free For All"; + + public override IReadOnlyCollection<string> Aliases => new[] {"FreeForAll", "FFA"}; + } +} diff --git a/FreeForAll/Matches/MatchEventListener.cs b/FreeForAll/Matches/MatchEventListener.cs index 589a4c2..11c62dc 100644 --- a/FreeForAll/Matches/MatchEventListener.cs +++ b/FreeForAll/Matches/MatchEventListener.cs @@ -1,6 +1,5 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Matches; -using Deathmatch.Core.Matches.Extensions; using Deathmatch.Core.Players.Extensions; using JetBrains.Annotations; using Microsoft.Extensions.Configuration; diff --git a/FreeForAll/Matches/MatchFFA.cs b/FreeForAll/Matches/MatchFFA.cs index 0103f40..c960ae9 100644 --- a/FreeForAll/Matches/MatchFFA.cs +++ b/FreeForAll/Matches/MatchFFA.cs @@ -3,25 +3,21 @@ using Deathmatch.API.Matches; using Deathmatch.API.Players; using Deathmatch.API.Players.Events; -using Deathmatch.Core.Grace; using Deathmatch.Core.Helpers; using Deathmatch.Core.Items; using Deathmatch.Core.Loadouts; using Deathmatch.Core.Matches; -using Deathmatch.Core.Matches.Extensions; using Deathmatch.Core.Spawns; +using FreeForAll.Configuration; +using FreeForAll.Loadouts; using FreeForAll.Players; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using MoreLinq; using OpenMod.API.Commands; -using OpenMod.API.Permissions; -using OpenMod.API.Plugins; using OpenMod.Core.Users; using OpenMod.UnityEngine.Extensions; using OpenMod.Unturned.Players.Life.Events; using SilK.Unturned.Extras.Events; -using SilK.Unturned.Extras.Plugins; using System; using System.Collections.Generic; using System.Linq; @@ -32,38 +28,20 @@ namespace FreeForAll.Matches [Match("Free For All")] [MatchDescription("A game mode where the first to the kill threshold wins.")] [MatchAlias("FFA")] - public class MatchFFA : MatchBase, + public class MatchFFA : MatchBase<FreeForAllConfig, FFALoadoutCategory>, IInstanceEventListener<UnturnedPlayerDeathEvent>, IInstanceEventListener<IGamePlayerSelectingRespawnEvent>, IInstanceEventListener<UnturnedPlayerSpawnedEvent> { - private readonly IPluginAccessor<FreeForAllPlugin> _pluginAccessor; - private readonly ILogger<MatchFFA> _logger; - private readonly ILoadoutManager _loadoutManager; - private readonly ILoadoutSelector _loadoutSelector; - private readonly IPermissionChecker _permissionChecker; - private readonly IGraceManager _graceManager; - - public MatchFFA( - IPluginAccessor<FreeForAllPlugin> pluginAccessor, - ILogger<MatchFFA> logger, - ILoadoutManager loadoutManager, - ILoadoutSelector loadoutSelector, - IPermissionChecker permissionChecker, - IGraceManager graceManager, - IServiceProvider serviceProvider) : base(serviceProvider) + private readonly SpawnDirectory _spawnDirectory; + + public MatchFFA(IServiceProvider serviceProvider, + SpawnDirectory spawnDirectory) : base(serviceProvider) { - _pluginAccessor = pluginAccessor; - _logger = logger; - _loadoutManager = loadoutManager; - _loadoutSelector = loadoutSelector; - _permissionChecker = permissionChecker; - _graceManager = graceManager; + _spawnDirectory = spawnDirectory; } - public IReadOnlyCollection<PlayerSpawn> GetSpawns() => _pluginAccessor.Instance?.Spawns ?? - throw new PluginNotLoadedException( - typeof(FreeForAllPlugin)); + public IReadOnlyCollection<PlayerSpawn> GetSpawns() => _spawnDirectory.Spawns; public PlayerSpawn GetFurthestSpawn() { @@ -84,18 +62,14 @@ static float TotalMagnitude(Vector3 point, IEnumerable<Vector3> others) public async UniTask<ILoadout?> GetLoadout(IGamePlayer player) { - const string category = "Free For All"; - - var loadout = _loadoutSelector.GetLoadout(player, category); + var loadout = LoadoutSelector.GetSelectedLoadout(player, LoadoutCategory); - if (loadout != null && (loadout.Permission == null || - await _permissionChecker.CheckPermissionAsync(player.User, loadout.Permission) == - PermissionGrantResult.Grant)) + if (loadout != null && await loadout.IsPermitted(player.User)) { return loadout; } - return await _loadoutManager.GetRandomLoadout(category, player, _permissionChecker); + return await LoadoutCategory.GetRandomLoadout(player); } public async UniTask GiveLoadout(IGamePlayer player) @@ -111,7 +85,7 @@ public async UniTask GiveLoadout(IGamePlayer player) } else { - loadout.GiveToPlayer(player); + await loadout.GiveToPlayer(player); } } @@ -119,20 +93,41 @@ public async UniTask GrantGracePeriod(IGamePlayer player) { await UniTask.SwitchToMainThread(); - _graceManager.GrantGracePeriod(player, Configuration.GetValue<float>("GracePeriod", 2)); + GraceManager.GrantGracePeriod(player, Configuration.Instance.GracePeriod); } - public async UniTask SpawnPlayer(IGamePlayer player, PlayerSpawn spawn) + public async UniTask SpawnPlayer(IGamePlayer player, PlayerSpawn? spawn = null) { await UniTask.SwitchToMainThread(); - await GrantGracePeriod(player); + try + { + await GrantGracePeriod(player); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when granting player grace period"); + } - spawn.SpawnPlayer(player); + spawn?.SpawnPlayer(player); - player.Heal(); + try + { + player.Heal(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when healing player"); + } - await GiveLoadout(player); + try + { + await GiveLoadout(player); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when giving player loadout"); + } } protected override async UniTask OnPlayerAdded(IGamePlayer player) @@ -191,7 +186,7 @@ protected override async UniTask OnEndAsync() } catch (Exception ex) { - _logger.LogError(ex, "Error occurred restoring player."); + Logger.LogError(ex, "Error occurred restoring player."); exceptions.Add(ex); } } @@ -204,14 +199,20 @@ protected override async UniTask OnEndAsync() IGamePlayer? winner = null; var maxKills = 0; + Logger.LogDebug("FFA Match Kills:"); + foreach (var player in Players) { - if (player.GetKills() > maxKills) + var playerKills = player.GetKills(); + + Logger.LogDebug("{PlayerId} - {Kills}", player.SteamId, playerKills); + + if (playerKills > maxKills) { winner = player; - maxKills = player.GetKills(); + maxKills = playerKills; } - else if (player.GetKills() == maxKills) + else if (playerKills == maxKills) { winner = null; } @@ -219,27 +220,29 @@ protected override async UniTask OnEndAsync() if (winner != null) { + Logger.LogInformation( + "Player '{PlayerName}' ({PlayerSteamId}) has won the FFA match with {Kills} kills.", + winner.DisplayName, winner.SteamId, maxKills); + await UserManager.BroadcastAsync(KnownActorTypes.Player, - StringLocalizer["announcements:match_end:player_won", new { Winner = winner.User }]); + StringLocalizer["announcements:match_end:player_won", new {Winner = winner.User}]); } else { + Logger.LogInformation("The FFA match has ended in a tie with {Kills} kills."); + await UserManager.BroadcastAsync(KnownActorTypes.Player, StringLocalizer["announcements:match_end:tie"]); } - if (Players.Count >= Configuration.GetValue("Rewards:MinimumPlayers", 5)) + if (Players.Count >= Configuration.Instance.Rewards.MinimumPlayers) { - var winnerRewards = - Configuration.GetSection("Rewards:Winners").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var loserRewards = - Configuration.GetSection("Rewards:Losers").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var tiedRewards = - Configuration.GetSection("Rewards:Tied").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var allRewards = - Configuration.GetSection("Rewards:All").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - - void GiveRewards(IGamePlayer player, List<ChanceItem> items) + var winnerRewards = Configuration.Instance.Rewards.Winners; + var loserRewards = Configuration.Instance.Rewards.Losers; + var tiedRewards = Configuration.Instance.Rewards.Tied; + var allRewards = Configuration.Instance.Rewards.All; + + void GiveRewards(IGamePlayer player, IEnumerable<ChanceItem> items) { foreach (var item in items) { @@ -263,7 +266,7 @@ void GiveRewards(IGamePlayer player, List<ChanceItem> items) private void SetupDelayedEnd() { - var maxDuration = Configuration.GetValue("MaxDuration", 0f); + var maxDuration = Configuration.Instance.MaxDuration; if (maxDuration <= 0) { @@ -288,6 +291,8 @@ public async UniTask HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @ var victim = this.GetPlayer(@event.Player); var killer = this.GetPlayer(@event.Instigator); + Logger.LogDebug("{VictimId} (IsNull: {VictimIsNull}) died to {KillerId} (IsNull: {KillerIsNull})", @event.Player.SteamId, @event.Instigator, victim == null, killer == null); + if (victim == null || killer == null || killer == victim) { return; @@ -297,7 +302,7 @@ public async UniTask HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @ killer.SetKills(kills); - var threshold = Configuration.GetValue("KillThreshold", 30); + var threshold = Configuration.Instance.KillThreshold; if (kills >= threshold) { @@ -333,13 +338,7 @@ public async UniTask HandleEventAsync(object? sender, UnturnedPlayerSpawnedEvent return; } - await UniTask.SwitchToMainThread(); - - player.Heal(); - - await GrantGracePeriod(player); - - await GiveLoadout(player); + await SpawnPlayer(player); } } } diff --git a/FreeForAll/PluginContainerConfigurator.cs b/FreeForAll/PluginContainerConfigurator.cs new file mode 100644 index 0000000..d4cbb97 --- /dev/null +++ b/FreeForAll/PluginContainerConfigurator.cs @@ -0,0 +1,19 @@ +extern alias JetBrainsAnnotations; +using Autofac; +using Deathmatch.Core.Spawns; +using JetBrainsAnnotations::JetBrains.Annotations; +using OpenMod.API.Plugins; + +namespace FreeForAll +{ + [UsedImplicitly] + public class PluginContainerConfigurator : IPluginContainerConfigurator + { + public void ConfigureContainer(IPluginServiceConfigurationContext context) + { + context.ContainerBuilder.RegisterType<SpawnDirectory>() + .AsSelf() + .SingleInstance(); + } + } +} diff --git a/TeamDeathmatch/Commands/CommandTDM.cs b/TeamDeathmatch/Commands/CTeamDeathmatch.cs similarity index 68% rename from TeamDeathmatch/Commands/CommandTDM.cs rename to TeamDeathmatch/Commands/CTeamDeathmatch.cs index 207ac36..5191622 100644 --- a/TeamDeathmatch/Commands/CommandTDM.cs +++ b/TeamDeathmatch/Commands/CTeamDeathmatch.cs @@ -7,12 +7,13 @@ namespace TeamDeathmatch.Commands { [Command("tdm")] - [CommandSyntax("<spawns>")] + [CommandAlias("teamdeathmatch")] + [CommandSyntax("<spawns | loadouts>")] [CommandDescription("Manage the Team Deathmatch game mode.")] [CommandActor(typeof(UnturnedUser))] - public class CommandTDM : UnturnedCommand + public class CTeamDeathmatch : UnturnedCommand { - public CommandTDM(IServiceProvider serviceProvider) : base(serviceProvider) + public CTeamDeathmatch(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/TeamDeathmatch/Commands/Loadouts/CLoadouts.cs b/TeamDeathmatch/Commands/Loadouts/CLoadouts.cs new file mode 100644 index 0000000..42bc8c2 --- /dev/null +++ b/TeamDeathmatch/Commands/Loadouts/CLoadouts.cs @@ -0,0 +1,24 @@ +using Cysharp.Threading.Tasks; +using OpenMod.Core.Commands; +using OpenMod.Unturned.Commands; +using System; + +namespace TeamDeathmatch.Commands.Loadouts +{ + [Command("loadouts")] + [CommandAlias("loadout")] + [CommandDescription("Manages Team Deathmatch loadouts.")] + [CommandSyntax("<[a]dd/[r]emove> <title>")] + [CommandParent(typeof(CTeamDeathmatch))] + public class CLoadouts : UnturnedCommand + { + public CLoadouts(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override UniTask OnExecuteAsync() + { + throw new CommandWrongUsageException(Context); + } + } +} \ No newline at end of file diff --git a/TeamDeathmatch/Commands/Loadouts/CLoadoutsAdd.cs b/TeamDeathmatch/Commands/Loadouts/CLoadoutsAdd.cs new file mode 100644 index 0000000..6afbf37 --- /dev/null +++ b/TeamDeathmatch/Commands/Loadouts/CLoadoutsAdd.cs @@ -0,0 +1,26 @@ +using Deathmatch.Core.Commands.Loadouts.Base; +using OpenMod.Core.Commands; +using System; +using TeamDeathmatch.Items; +using TeamDeathmatch.Loadouts; + +namespace TeamDeathmatch.Commands.Loadouts +{ + [Command("add")] + [CommandAlias("a")] + [CommandAlias("+")] + [CommandDescription("Adds a new Team Deathmatch loadout.")] + [CommandSyntax("<title>")] + [CommandParent(typeof(CLoadouts))] + public class CLoadoutsAdd : AddLoadoutCommand<TDMLoadoutCategory, TeamLoadout, TeamItem> + { + public CLoadoutsAdd(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override TeamLoadout CreateLoadout(string title, string permission) + { + return new(title, permission); + } + } +} \ No newline at end of file diff --git a/TeamDeathmatch/Commands/Loadouts/CLoadoutsRemove.cs b/TeamDeathmatch/Commands/Loadouts/CLoadoutsRemove.cs new file mode 100644 index 0000000..4512e99 --- /dev/null +++ b/TeamDeathmatch/Commands/Loadouts/CLoadoutsRemove.cs @@ -0,0 +1,22 @@ +using Deathmatch.Core.Commands.Loadouts.Base; +using OpenMod.Core.Commands; +using System; +using TeamDeathmatch.Items; +using TeamDeathmatch.Loadouts; + +namespace TeamDeathmatch.Commands.Loadouts +{ + [Command("remove")] + [CommandAlias("rem")] + [CommandAlias("r")] + [CommandAlias("-")] + [CommandDescription("Removes a Team Deathmatch loadout.")] + [CommandSyntax("<title>")] + [CommandParent(typeof(CLoadouts))] + public class CLoadoutsRemove : RemoveLoadoutCommand<TDMLoadoutCategory, TeamLoadout, TeamItem> + { + public CLoadoutsRemove(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + } +} \ No newline at end of file diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawns.cs b/TeamDeathmatch/Commands/Spawns/CSpawns.cs similarity index 73% rename from TeamDeathmatch/Commands/Spawns/CommandSpawns.cs rename to TeamDeathmatch/Commands/Spawns/CSpawns.cs index 98fc2be..7afdc63 100644 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawns.cs +++ b/TeamDeathmatch/Commands/Spawns/CSpawns.cs @@ -9,10 +9,10 @@ namespace TeamDeathmatch.Commands.Spawns [CommandAlias("spawn")] [CommandDescription("Manages TDM spawns.")] [CommandSyntax("<[a]dd/[r]emove/[l]ist/[c]lear> <[r]ed/[b]lue>")] - [CommandParent(typeof(CommandTDM))] - public class CommandSpawns : UnturnedCommand + [CommandParent(typeof(CTeamDeathmatch))] + public class CSpawns : UnturnedCommand { - public CommandSpawns(IServiceProvider serviceProvider) : base(serviceProvider) + public CSpawns(IServiceProvider serviceProvider) : base(serviceProvider) { } diff --git a/TeamDeathmatch/Commands/Spawns/CSpawnsAction.cs b/TeamDeathmatch/Commands/Spawns/CSpawnsAction.cs new file mode 100644 index 0000000..eae272e --- /dev/null +++ b/TeamDeathmatch/Commands/Spawns/CSpawnsAction.cs @@ -0,0 +1,78 @@ +using Cysharp.Threading.Tasks; +using Deathmatch.Core.Spawns; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using OpenMod.API.Commands; +using OpenMod.Unturned.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using TeamDeathmatch.Spawns; +using TeamDeathmatch.Teams; + +namespace TeamDeathmatch.Commands.Spawns +{ + public abstract class CSpawnsAction : UnturnedCommand + { + protected readonly BlueSpawnDirectory BlueSpawnDirectory; + protected readonly RedSpawnDirectory RedSpawnDirectory; + protected readonly IStringLocalizer StringLocalizer; + + protected CSpawnsAction(IServiceProvider serviceProvider) : base(serviceProvider) + { + BlueSpawnDirectory = serviceProvider.GetRequiredService<BlueSpawnDirectory>(); + RedSpawnDirectory = serviceProvider.GetRequiredService<RedSpawnDirectory>(); + StringLocalizer = serviceProvider.GetRequiredService<IStringLocalizer>(); + } + + private SpawnDirectory GetSpawnDirectory(Team team) + { + return team switch + { + Team.Red => BlueSpawnDirectory, + Team.Blue => RedSpawnDirectory, + _ => throw new ArgumentException("Bad team (not red or blue)", nameof(team)) + }; + } + + protected List<PlayerSpawn> GetSpawns(Team team) + { + return GetSpawnDirectory(team).Spawns.ToList(); + } + + protected async UniTask SaveSpawns(Team team, IEnumerable<PlayerSpawn> spawns) + { + var spawnDirectory = GetSpawnDirectory(team); + + await spawnDirectory.SaveSpawns(spawns); + } + + protected override async UniTask OnExecuteAsync() + { + var strTeam = await Context.Parameters.GetAsync<string>(0); + + var team = Team.None; + + switch (strTeam.Trim().ToLower()) + { + case "r": + case "red": + team = Team.Red; + break; + case "b": + case "blue": + team = Team.Blue; + break; + } + + if (team == Team.None) + { + throw new UserFriendlyException(StringLocalizer["commands:spawns:common:unknown_team", new { Team = strTeam }]); + } + + await OnExecuteAsync(team); + } + + protected abstract UniTask OnExecuteAsync(Team team); + } +} diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawnsAdd.cs b/TeamDeathmatch/Commands/Spawns/CSpawnsAdd.cs similarity index 58% rename from TeamDeathmatch/Commands/Spawns/CommandSpawnsAdd.cs rename to TeamDeathmatch/Commands/Spawns/CSpawnsAdd.cs index ed19122..7670379 100644 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawnsAdd.cs +++ b/TeamDeathmatch/Commands/Spawns/CSpawnsAdd.cs @@ -1,10 +1,9 @@ using Cysharp.Threading.Tasks; using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; using OpenMod.Unturned.Users; using System; -using System.Collections.Generic; +using System.Linq; using TeamDeathmatch.Teams; namespace TeamDeathmatch.Commands.Spawns @@ -14,24 +13,22 @@ namespace TeamDeathmatch.Commands.Spawns [CommandAlias("+")] [CommandDescription("Add a spawn.")] [CommandSyntax("<[r]ed/[b]lue>")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsAdd : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsAdd : CSpawnsAction { - public CommandSpawnsAdd(TeamDeathmatchPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsAdd(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync(Team team) { - List<PlayerSpawn> spawns = await LoadSpawnsAsync(team); - + var spawns = GetSpawns(team); + var spawn = new PlayerSpawn((UnturnedUser)Context.Actor); spawns.Add(spawn); - await SaveSpawnsAsync(team, spawns); + await SaveSpawns(team, spawns); await PrintAsync(StringLocalizer["commands:spawns:add:success", new { Team = team.ToString() }]); } diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawnsClear.cs b/TeamDeathmatch/Commands/Spawns/CSpawnsClear.cs similarity index 52% rename from TeamDeathmatch/Commands/Spawns/CommandSpawnsClear.cs rename to TeamDeathmatch/Commands/Spawns/CSpawnsClear.cs index 5180dd9..a84ee0b 100644 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawnsClear.cs +++ b/TeamDeathmatch/Commands/Spawns/CSpawnsClear.cs @@ -1,9 +1,7 @@ using Cysharp.Threading.Tasks; using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; using System; -using System.Collections.Generic; using TeamDeathmatch.Teams; namespace TeamDeathmatch.Commands.Spawns @@ -12,20 +10,16 @@ namespace TeamDeathmatch.Commands.Spawns [CommandAlias("c")] [CommandDescription("Removes all spawns.")] [CommandSyntax("<[r]ed/[b]lue>")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsClear : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsClear : CSpawnsAction { - public CommandSpawnsClear(TeamDeathmatchPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsClear(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync(Team team) { - List<PlayerSpawn> spawns = new List<PlayerSpawn>(); - - await SaveSpawnsAsync(team, spawns); + await SaveSpawns(team, Array.Empty<PlayerSpawn>()); await PrintAsync(StringLocalizer["commands:spawns:clear:success", new { Team = team.ToString() }]); } diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawnsList.cs b/TeamDeathmatch/Commands/Spawns/CSpawnsList.cs similarity index 58% rename from TeamDeathmatch/Commands/Spawns/CommandSpawnsList.cs rename to TeamDeathmatch/Commands/Spawns/CSpawnsList.cs index 3753754..64bf6d3 100644 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawnsList.cs +++ b/TeamDeathmatch/Commands/Spawns/CSpawnsList.cs @@ -1,9 +1,6 @@ using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.Core.Commands; using System; -using System.Collections.Generic; using TeamDeathmatch.Teams; namespace TeamDeathmatch.Commands.Spawns @@ -12,22 +9,20 @@ namespace TeamDeathmatch.Commands.Spawns [CommandAlias("l")] [CommandDescription("Lists the spawns.")] [CommandSyntax("<[r]ed/[b]lue>")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsList : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsList : CSpawnsAction { - public CommandSpawnsList(TeamDeathmatchPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsList(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync(Team team) { - List<PlayerSpawn> spawns = await LoadSpawnsAsync(team); + var spawns = GetSpawns(team); - string[] localizedSpawns = new string[spawns.Count]; + var localizedSpawns = new string[spawns.Count]; - for (int i = 0; i < localizedSpawns.Length; i++) + for (var i = 0; i < localizedSpawns.Length; i++) { localizedSpawns[i] = StringLocalizer["commands:spawns:list:element", new { I = i, spawns[i].X, spawns[i].Y, spawns[i].Z }]; diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawnsRemove.cs b/TeamDeathmatch/Commands/Spawns/CSpawnsRemove.cs similarity index 64% rename from TeamDeathmatch/Commands/Spawns/CommandSpawnsRemove.cs rename to TeamDeathmatch/Commands/Spawns/CSpawnsRemove.cs index f7a1ac9..f6aae7c 100644 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawnsRemove.cs +++ b/TeamDeathmatch/Commands/Spawns/CSpawnsRemove.cs @@ -1,10 +1,7 @@ using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; using OpenMod.API.Commands; using OpenMod.Core.Commands; using System; -using System.Collections.Generic; using TeamDeathmatch.Teams; namespace TeamDeathmatch.Commands.Spawns @@ -15,18 +12,16 @@ namespace TeamDeathmatch.Commands.Spawns [CommandAlias("-")] [CommandDescription("Remove a spawn.")] [CommandSyntax("<[r]ed/[b]lue> <index>")] - [CommandParent(typeof(CommandSpawns))] - public class CommandSpawnsRemove : CommandSpawnsAction + [CommandParent(typeof(CSpawns))] + public class CSpawnsRemove : CSpawnsAction { - public CommandSpawnsRemove(TeamDeathmatchPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(plugin, stringLocalizer, serviceProvider) + public CSpawnsRemove(IServiceProvider serviceProvider) : base(serviceProvider) { } protected override async UniTask OnExecuteAsync(Team team) { - int index = await Context.Parameters.GetAsync<int>(1); + var index = await Context.Parameters.GetAsync<int>(1); // A check which we can call before loading from the disk if (index <= 0) @@ -34,7 +29,7 @@ protected override async UniTask OnExecuteAsync(Team team) throw new UserFriendlyException(StringLocalizer["commands:spawns:remove:index_out_of_bounds"]); } - List<PlayerSpawn> spawns = await LoadSpawnsAsync(team); + var spawns = GetSpawns(team); if (index >= spawns.Count) { @@ -43,7 +38,7 @@ protected override async UniTask OnExecuteAsync(Team team) spawns.RemoveAt(index); - await SaveSpawnsAsync(team, spawns); + await SaveSpawns(team, spawns); await PrintAsync(StringLocalizer["commands:spawns:remove:success", new { Team = team.ToString() }]); } diff --git a/TeamDeathmatch/Commands/Spawns/CommandSpawnsAction.cs b/TeamDeathmatch/Commands/Spawns/CommandSpawnsAction.cs deleted file mode 100644 index db885ee..0000000 --- a/TeamDeathmatch/Commands/Spawns/CommandSpawnsAction.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Cysharp.Threading.Tasks; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Localization; -using OpenMod.API.Commands; -using OpenMod.Unturned.Commands; -using System; -using System.Collections.Generic; -using TeamDeathmatch.Teams; - -namespace TeamDeathmatch.Commands.Spawns -{ - public abstract class CommandSpawnsAction : UnturnedCommand - { - protected readonly TeamDeathmatchPlugin Plugin; - protected readonly IStringLocalizer StringLocalizer; - - protected CommandSpawnsAction(TeamDeathmatchPlugin plugin, - IStringLocalizer stringLocalizer, - IServiceProvider serviceProvider) : base(serviceProvider) - { - Plugin = plugin; - StringLocalizer = stringLocalizer; - } - - private string GetKey(Team team) - { - switch (team) - { - case Team.Red: - return TeamDeathmatchPlugin.RedSpawnsKey; - case Team.Blue: - return TeamDeathmatchPlugin.BlueSpawnsKey; - default: - throw new ArgumentException("Bad team (not red or blue)", nameof(team)); - } - } - - protected async UniTask<List<PlayerSpawn>> LoadSpawnsAsync(Team team) - { - var list = await Plugin.DataStore.ExistsAsync(GetKey(team)) - ? await Plugin.DataStore.LoadAsync<List<PlayerSpawn>>(GetKey(team)) - : null; - - return list ?? new List<PlayerSpawn>(); - } - - protected async UniTask SaveSpawnsAsync(Team team, List<PlayerSpawn> spawns) - { - await Plugin.DataStore.SaveAsync(GetKey(team), spawns); - - await Plugin.ReloadSpawns(); - } - - protected override async UniTask OnExecuteAsync() - { - string strTeam = await Context.Parameters.GetAsync<string>(0); - - Team team = Team.None; - - switch (strTeam.Trim().ToLower()) - { - case "r": - case "red": - team = Team.Red; - break; - case "b": - case "blue": - team = Team.Blue; - break; - } - - if (team == Team.None) - { - throw new UserFriendlyException(StringLocalizer["commands:spawns:common:unknown_team", new { Team = strTeam }]); - } - - await OnExecuteAsync(team); - } - - protected abstract UniTask OnExecuteAsync(Team team); - } -} diff --git a/TeamDeathmatch/Configuration/AutoRespawnConfig.cs b/TeamDeathmatch/Configuration/AutoRespawnConfig.cs new file mode 100644 index 0000000..78cfbc3 --- /dev/null +++ b/TeamDeathmatch/Configuration/AutoRespawnConfig.cs @@ -0,0 +1,12 @@ +using System; + +namespace TeamDeathmatch.Configuration +{ + [Serializable] + public class AutoRespawnConfig + { + public bool Enabled { get; set; } = true; + + public float Delay { get; set; } = 0; + } +} diff --git a/TeamDeathmatch/Configuration/GameRewardsConfig.cs b/TeamDeathmatch/Configuration/GameRewardsConfig.cs new file mode 100644 index 0000000..f3cceea --- /dev/null +++ b/TeamDeathmatch/Configuration/GameRewardsConfig.cs @@ -0,0 +1,19 @@ +using System; +using Deathmatch.Core.Items; + +namespace TeamDeathmatch.Configuration +{ + [Serializable] + public class GameRewardsConfig + { + public int MinimumPlayers { get; set; } = 5; + + public ChanceItem[] Winners { get; set; } = new ChanceItem[0]; + + public ChanceItem[] Losers { get; set; } = new ChanceItem[0]; + + public ChanceItem[] Tied { get; set; } = new ChanceItem[0]; + + public ChanceItem[] All { get; set; } = new ChanceItem[0]; + } +} diff --git a/TeamDeathmatch/Configuration/RewardConfig.cs b/TeamDeathmatch/Configuration/RewardConfig.cs new file mode 100644 index 0000000..4453d96 --- /dev/null +++ b/TeamDeathmatch/Configuration/RewardConfig.cs @@ -0,0 +1,14 @@ +using System; + +namespace TeamDeathmatch.Configuration +{ + [Serializable] + public class RewardConfig + { + public string Id { get; set; } = ""; + + public int Amount { get; set; } = 1; + + public int Chance { get; set; } = 1; + } +} diff --git a/TeamDeathmatch/Configuration/TeamDeathmatchConfiguration.cs b/TeamDeathmatch/Configuration/TeamDeathmatchConfiguration.cs new file mode 100644 index 0000000..779c6ef --- /dev/null +++ b/TeamDeathmatch/Configuration/TeamDeathmatchConfiguration.cs @@ -0,0 +1,20 @@ +using System; + +namespace TeamDeathmatch.Configuration +{ + [Serializable] + public class TeamDeathmatchConfiguration + { + public AutoRespawnConfig AutoRespawn { get; set; } = new(); + + public bool FriendlyFire { get; set; } = false; + + public int KillThreshold { get; set; } = 30; + + public float MaxDuration { get; set; } = 600; + + public float GracePeriod { get; set; } = 2; + + public GameRewardsConfig Rewards { get; set; } = new(); + } +} diff --git a/TeamDeathmatch/Items/TeamItem.cs b/TeamDeathmatch/Items/TeamItem.cs index 796db30..3eee57f 100644 --- a/TeamDeathmatch/Items/TeamItem.cs +++ b/TeamDeathmatch/Items/TeamItem.cs @@ -9,6 +9,14 @@ namespace TeamDeathmatch.Items [Serializable] public class TeamItem : Item { + public TeamItem() + { + } + + public TeamItem(ushort id, byte amount, byte quality, byte[] state) : base(id, amount, quality, state) + { + } + public Team Team { get; set; } public override bool GiveToPlayer(IGamePlayer player) diff --git a/TeamDeathmatch/Loadouts/TDMLoadoutCategory.cs b/TeamDeathmatch/Loadouts/TDMLoadoutCategory.cs new file mode 100644 index 0000000..2a0f182 --- /dev/null +++ b/TeamDeathmatch/Loadouts/TDMLoadoutCategory.cs @@ -0,0 +1,18 @@ +using Deathmatch.Core.Loadouts; +using System; +using System.Collections.Generic; +using TeamDeathmatch.Items; + +namespace TeamDeathmatch.Loadouts +{ + public class TDMLoadoutCategory : LoadoutCategoryBase<TeamLoadout, TeamItem> + { + public TDMLoadoutCategory(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public override string Title => "Team Deathmatch"; + + public override IReadOnlyCollection<string> Aliases => new[] {"TeamDeathmatch", "TDM"}; + } +} diff --git a/TeamDeathmatch/Loadouts/TeamLoadout.cs b/TeamDeathmatch/Loadouts/TeamLoadout.cs index babe670..892d20a 100644 --- a/TeamDeathmatch/Loadouts/TeamLoadout.cs +++ b/TeamDeathmatch/Loadouts/TeamLoadout.cs @@ -1,28 +1,23 @@ -using Deathmatch.Core.Items; -using Deathmatch.Core.Loadouts; +using Deathmatch.Core.Loadouts; using System; -using System.Collections.Generic; using TeamDeathmatch.Items; namespace TeamDeathmatch.Loadouts { [Serializable] - public class TeamLoadout : LoadoutBase + public class TeamLoadout : LoadoutBase<TeamItem> { - public List<TeamItem> Items { get; set; } - - public TeamLoadout() : this("", null) + public TeamLoadout() { } public TeamLoadout(string title, string? permission) : base(title, permission) { - Items = new List<TeamItem>(); } - public override IReadOnlyCollection<Item> GetItems() + protected override TeamItem CreateItem(ushort id, byte amount, byte quality, byte[] state) { - return Items; + return new(id, amount, quality, state); } } } diff --git a/TeamDeathmatch/Loadouts/TeamLoadoutCategory.cs b/TeamDeathmatch/Loadouts/TeamLoadoutCategory.cs deleted file mode 100644 index cab6f29..0000000 --- a/TeamDeathmatch/Loadouts/TeamLoadoutCategory.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Deathmatch.API.Loadouts; -using Deathmatch.Core.Loadouts; -using OpenMod.API; -using OpenMod.API.Persistence; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using TeamDeathmatch.Items; -using TeamDeathmatch.Teams; - -namespace TeamDeathmatch.Loadouts -{ - public class TeamLoadoutCategory : LoadoutCategory - { - public TeamLoadoutCategory(string title, IReadOnlyCollection<string>? aliases, - IOpenModComponent component, IDataStore dataStore, List<ILoadout>? loadouts = null) - : base(title, aliases, component, dataStore, loadouts) - { - } - - public override async Task LoadLoadouts() - { - var loadouts = new List<ILoadout>(); - - if (await DataStore.ExistsAsync(DataStoreKey)) - { - loadouts.AddRange(await DataStore.LoadAsync<List<TeamLoadout>>(DataStoreKey) ?? - new List<TeamLoadout>()); - } - - Loadouts = loadouts; - } - - public override async Task SaveLoadouts() - { - await DataStore.SaveAsync(DataStoreKey, Loadouts.OfType<Loadout>().ToList()); - } - - public override void AddLoadout(ILoadout loadout) - { - if (this.GetLoadout(loadout.Title) != null) - { - throw new ArgumentException("Loadout with given title already exists", nameof(loadout)); - } - - if (loadout is not LoadoutBase knownLoadout) - { - throw new NotSupportedException($"Argument {nameof(loadout)} must implement {typeof(Loadout).FullName}."); - } - - var teamLoadout = new TeamLoadout(loadout.Title, loadout.Permission); - - foreach (var item in knownLoadout.GetItems()) - { - teamLoadout.Items.Add(new TeamItem() - { - Id = item.Id, - Amount = item.Amount, - Quality = item.Quality, - State = item.State, - Team = Team.None - }); - } - } - - public override bool RemoveLoadout(ILoadout loadout) - { - return Loadouts.RemoveAll(x => loadout.Title.Equals(x.Title, StringComparison.OrdinalIgnoreCase)) > 0; - } - } -} diff --git a/TeamDeathmatch/Matches/MatchEventListener.cs b/TeamDeathmatch/Matches/MatchEventListener.cs index f886df6..fe68b2a 100644 --- a/TeamDeathmatch/Matches/MatchEventListener.cs +++ b/TeamDeathmatch/Matches/MatchEventListener.cs @@ -1,22 +1,22 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Matches; -using Deathmatch.Core.Matches.Extensions; using Deathmatch.Core.Players.Extensions; -using Microsoft.Extensions.Configuration; using OpenMod.API.Eventing; using OpenMod.Unturned.Players; using OpenMod.Unturned.Players.Life.Events; +using SilK.Unturned.Extras.Configuration; using System.Threading.Tasks; +using TeamDeathmatch.Configuration; namespace TeamDeathmatch.Matches { public class MatchEventListener : IEventListener<UnturnedPlayerDeathEvent> { private readonly IMatchExecutor _matchExecutor; - private readonly IConfiguration _configuration; + private readonly IConfigurationParser<TeamDeathmatchConfiguration> _configuration; public MatchEventListener(IMatchExecutor matchExecutor, - IConfiguration configuration) + IConfigurationParser<TeamDeathmatchConfiguration> configuration) { _matchExecutor = matchExecutor; _configuration = configuration; @@ -48,12 +48,12 @@ public async Task HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @eve return; } - if (!_configuration.GetValue("AutoRespawn:Enabled", false)) + if (!_configuration.Instance.AutoRespawn.Enabled) { return; } - var delayConfig = _configuration.GetValue<double>("AutoRespawn:Delay", 0); + var delayConfig = _configuration.Instance.AutoRespawn.Delay; async UniTask AutoRespawnPlayer(UnturnedPlayer player, double delay) { diff --git a/TeamDeathmatch/Matches/MatchTDM.cs b/TeamDeathmatch/Matches/MatchTDM.cs index eeec2df..850868c 100644 --- a/TeamDeathmatch/Matches/MatchTDM.cs +++ b/TeamDeathmatch/Matches/MatchTDM.cs @@ -3,27 +3,25 @@ using Deathmatch.API.Matches; using Deathmatch.API.Players; using Deathmatch.API.Players.Events; -using Deathmatch.Core.Grace; using Deathmatch.Core.Helpers; using Deathmatch.Core.Items; using Deathmatch.Core.Loadouts; using Deathmatch.Core.Matches; -using Deathmatch.Core.Matches.Extensions; using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using OpenMod.API.Commands; -using OpenMod.API.Permissions; -using OpenMod.API.Plugins; using OpenMod.Core.Users; using OpenMod.UnityEngine.Extensions; using OpenMod.Unturned.Players.Life.Events; -using SDG.Unturned; using SilK.Unturned.Extras.Events; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using TeamDeathmatch.Configuration; +using TeamDeathmatch.Loadouts; using TeamDeathmatch.Players; +using TeamDeathmatch.Spawns; using TeamDeathmatch.Teams; namespace TeamDeathmatch.Matches @@ -31,16 +29,14 @@ namespace TeamDeathmatch.Matches [Match("Team Deathmatch")] [MatchDescription("A game mode where two teams fight to a kill threshold.")] [MatchAlias("TDM")] - public class MatchTDM : MatchBase, + public class MatchTDM : MatchBase<TeamDeathmatchConfiguration, TDMLoadoutCategory>, IInstanceEventListener<UnturnedPlayerDamagingEvent>, IInstanceEventListener<UnturnedPlayerDeathEvent>, + IInstanceEventListener<UnturnedPlayerSpawnedEvent>, IInstanceEventListener<IGamePlayerSelectingRespawnEvent> { - private readonly IPluginAccessor<TeamDeathmatchPlugin> _pluginAccessor; - private readonly ILoadoutManager _loadoutManager; - private readonly ILoadoutSelector _loadoutSelector; - private readonly IPermissionChecker _permissionChecker; - private readonly IGraceManager _graceManager; + private readonly BlueSpawnDirectory _blueSpawnDirectory; + private readonly RedSpawnDirectory _redSpawnDirectory; private int _redDeaths; private int _blueDeaths; @@ -51,18 +47,12 @@ public class MatchTDM : MatchBase, private readonly List<PlayerSpawn> _redInitialSpawns; private readonly List<PlayerSpawn> _blueInitialSpawns; - public MatchTDM(IPluginAccessor<TeamDeathmatchPlugin> pluginAccessor, - ILoadoutManager loadoutManager, - ILoadoutSelector loadoutSelector, - IPermissionChecker permissionChecker, - IGraceManager graceManager, - IServiceProvider serviceProvider) : base(serviceProvider) + public MatchTDM(IServiceProvider serviceProvider, + BlueSpawnDirectory blueSpawnDirectory, + RedSpawnDirectory redSpawnDirectory) : base(serviceProvider) { - _pluginAccessor = pluginAccessor; - _loadoutManager = loadoutManager; - _loadoutSelector = loadoutSelector; - _permissionChecker = permissionChecker; - _graceManager = graceManager; + _blueSpawnDirectory = blueSpawnDirectory; + _redSpawnDirectory = redSpawnDirectory; _redDeaths = 0; _blueDeaths = 0; @@ -78,8 +68,8 @@ public IReadOnlyCollection<PlayerSpawn> GetSpawns(Team team) { return team switch { - Team.Red => _pluginAccessor.Instance!.RedSpawns, - Team.Blue => _pluginAccessor.Instance!.BlueSpawns, + Team.Red => _redSpawnDirectory.Spawns, + Team.Blue => _blueSpawnDirectory.Spawns, _ => throw new InvalidOperationException("Player has no team") }; } @@ -88,18 +78,14 @@ public IReadOnlyCollection<PlayerSpawn> GetSpawns(Team team) public async Task<ILoadout?> GetLoadout(IGamePlayer player) { - const string category = "Team Deathmatch"; + var loadout = LoadoutSelector.GetSelectedLoadout(player, LoadoutCategory); - var loadout = _loadoutSelector.GetLoadout(player, category); - - if (loadout != null && (loadout.Permission == null || - await _permissionChecker.CheckPermissionAsync(player.User, loadout.Permission) == - PermissionGrantResult.Grant)) + if (loadout != null && await loadout.IsPermitted(player.User)) { return loadout; } - return await _loadoutManager.GetRandomLoadout(category, player, _permissionChecker); + return await LoadoutCategory.GetRandomLoadout(player); } public async UniTask GiveLoadout(IGamePlayer player) @@ -115,21 +101,49 @@ public async UniTask GiveLoadout(IGamePlayer player) } else { - loadout.GiveToPlayer(player); + await loadout.GiveToPlayer(player); } } - public async UniTask SpawnPlayer(IGamePlayer player, PlayerSpawn spawn) + public async UniTask GrantGracePeriod(IGamePlayer player) + { + await UniTask.SwitchToMainThread(); + + GraceManager.GrantGracePeriod(player, Configuration.Instance.GracePeriod); + } + + public async UniTask SpawnPlayer(IGamePlayer player, PlayerSpawn? spawn = null) { await UniTask.SwitchToMainThread(); - spawn.SpawnPlayer(player); + try + { + await GrantGracePeriod(player); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when granting player grace period"); + } - player.Heal(); + spawn?.SpawnPlayer(player); - await GiveLoadout(player); + try + { + player.Heal(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when healing player"); + } - _graceManager.GrantGracePeriod(player, Configuration.GetValue<float>("GracePeriod", 2)); + try + { + await GiveLoadout(player); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error occurred when giving player loadout"); + } } protected override async UniTask OnPlayerAdded(IGamePlayer player) @@ -229,18 +243,14 @@ await UserManager.BroadcastAsync(KnownActorTypes.Player, StringLocalizer["announcements:match_end:tie"]); } - if (Players.Count >= Configuration.GetValue("Rewards:MinimumPlayers", 8)) + if (Players.Count >= Configuration.Instance.Rewards.MinimumPlayers) { - var winnerRewards = - Configuration.GetSection("Rewards:Winners").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var loserRewards = - Configuration.GetSection("Rewards:Losers").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var tiedRewards = - Configuration.GetSection("Rewards:Tied").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - var allRewards = - Configuration.GetSection("Rewards:All").Get<List<ChanceItem>>() ?? new List<ChanceItem>(); - - void GiveRewards(IGamePlayer player, List<ChanceItem> items) + var winnerRewards = Configuration.Instance.Rewards.Winners; + var loserRewards = Configuration.Instance.Rewards.Losers; + var tiedRewards = Configuration.Instance.Rewards.Tied; + var allRewards = Configuration.Instance.Rewards.All; + + void GiveRewards(IGamePlayer player, IEnumerable<ChanceItem> items) { foreach (var item in items) { @@ -270,7 +280,7 @@ void GiveRewards(IGamePlayer player, List<ChanceItem> items) private void SetupDelayedEnd() { - var maxDuration = Configuration.GetValue("MaxDuration", 0f); + var maxDuration = Configuration.Instance.MaxDuration; if (maxDuration <= 0) { @@ -294,7 +304,7 @@ public UniTask HandleEventAsync(object? sender, UnturnedPlayerDamagingEvent @eve if (victim != null && killer != null && victim != killer && victim.GetTeam() == killer.GetTeam() - && !Configuration.GetValue("FriendlyFire", false)) + && !Configuration.Instance.FriendlyFire) { @event.IsCancelled = true; } @@ -313,12 +323,7 @@ public async UniTask HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @ var killer = this.GetPlayer(@event.Instigator); - if (killer == null && @event.DeathCause != EDeathCause.BLEEDING) - { - return; - } - - if (victim == killer) + if (killer == null || victim == killer) { return; } @@ -333,7 +338,7 @@ public async UniTask HandleEventAsync(object? sender, UnturnedPlayerDeathEvent @ break; } - var threshold = Configuration.GetValue("DeathThreshold", 30); + var threshold = Configuration.Instance.KillThreshold; if (_redDeaths >= threshold || _blueDeaths >= threshold) { @@ -359,5 +364,17 @@ public UniTask HandleEventAsync(object? sender, IGamePlayerSelectingRespawnEvent return UniTask.CompletedTask; } + + public async UniTask HandleEventAsync(object? sender, UnturnedPlayerSpawnedEvent @event) + { + var player = this.GetPlayer(@event.Player); + + if (player == null || Status != MatchStatus.InProgress) + { + return; + } + + await SpawnPlayer(player); + } } } diff --git a/TeamDeathmatch/PluginContainerConfigurator.cs b/TeamDeathmatch/PluginContainerConfigurator.cs new file mode 100644 index 0000000..aecc588 --- /dev/null +++ b/TeamDeathmatch/PluginContainerConfigurator.cs @@ -0,0 +1,23 @@ +extern alias JetBrainsAnnotations; +using Autofac; +using JetBrainsAnnotations::JetBrains.Annotations; +using OpenMod.API.Plugins; +using TeamDeathmatch.Spawns; + +namespace TeamDeathmatch +{ + [UsedImplicitly] + public class PluginContainerConfigurator : IPluginContainerConfigurator + { + public void ConfigureContainer(IPluginServiceConfigurationContext context) + { + context.ContainerBuilder.RegisterType<BlueSpawnDirectory>() + .AsSelf() + .SingleInstance(); + + context.ContainerBuilder.RegisterType<RedSpawnDirectory>() + .AsSelf() + .SingleInstance(); + } + } +} diff --git a/TeamDeathmatch/Spawns/BlueSpawnDirectory.cs b/TeamDeathmatch/Spawns/BlueSpawnDirectory.cs new file mode 100644 index 0000000..f0c7697 --- /dev/null +++ b/TeamDeathmatch/Spawns/BlueSpawnDirectory.cs @@ -0,0 +1,14 @@ +using Deathmatch.Core.Spawns; +using System; + +namespace TeamDeathmatch.Spawns +{ + public class BlueSpawnDirectory : SpawnDirectory + { + public BlueSpawnDirectory(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override string DataStoreKey => "spawns.blue"; + } +} diff --git a/TeamDeathmatch/Spawns/RedSpawnDirectory.cs b/TeamDeathmatch/Spawns/RedSpawnDirectory.cs new file mode 100644 index 0000000..884aa12 --- /dev/null +++ b/TeamDeathmatch/Spawns/RedSpawnDirectory.cs @@ -0,0 +1,14 @@ +using Deathmatch.Core.Spawns; +using System; + +namespace TeamDeathmatch.Spawns +{ + public class RedSpawnDirectory : SpawnDirectory + { + public RedSpawnDirectory(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + protected override string DataStoreKey => "spawns.red"; + } +} diff --git a/TeamDeathmatch/TeamDeathmatch.csproj b/TeamDeathmatch/TeamDeathmatch.csproj index 764511b..1a075f6 100644 --- a/TeamDeathmatch/TeamDeathmatch.csproj +++ b/TeamDeathmatch/TeamDeathmatch.csproj @@ -11,8 +11,8 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="OpenMod.Unturned" Version="3.1.2" /> - <PackageReference Include="SilK.Unturned.Extras" Version="1.6.5" /> + <PackageReference Include="OpenMod.Unturned" Version="3.2.2" /> + <PackageReference Include="SilK.Unturned.Extras" Version="1.7.0" /> </ItemGroup> <ItemGroup> diff --git a/TeamDeathmatch/TeamDeathmatchPlugin.cs b/TeamDeathmatch/TeamDeathmatchPlugin.cs index 82a04a2..cebbd3d 100644 --- a/TeamDeathmatch/TeamDeathmatchPlugin.cs +++ b/TeamDeathmatch/TeamDeathmatchPlugin.cs @@ -1,16 +1,9 @@ using Cysharp.Threading.Tasks; using Deathmatch.API.Loadouts; using Deathmatch.Core.Loadouts; -using Deathmatch.Core.Spawns; -using Microsoft.Extensions.Configuration; -using OpenMod.API.Permissions; -using OpenMod.API.Persistence; using OpenMod.API.Plugins; using OpenMod.Unturned.Plugins; using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using TeamDeathmatch.Loadouts; [assembly: PluginMetadata("TeamDeathmatch", DisplayName = "Team Deathmatch")] @@ -18,80 +11,19 @@ namespace TeamDeathmatch { public class TeamDeathmatchPlugin : OpenModUnturnedPlugin { - public const string RedSpawnsKey = "redspawns"; - public const string BlueSpawnsKey = "bluespawns"; - - private readonly IConfiguration _configuration; private readonly ILoadoutManager _loadoutManager; - private readonly IDataStore _dataStore; - private readonly IPermissionRegistry _permissionRegistry; - - private readonly List<PlayerSpawn> _redSpawns; - private readonly List<PlayerSpawn> _blueSpawns; + private readonly IServiceProvider _serviceProvider; - public TeamDeathmatchPlugin(IConfiguration configuration, - ILoadoutManager loadoutManager, - IDataStore dataStore, - IPermissionRegistry permissionRegistry, - IServiceProvider serviceProvider) : base(serviceProvider) + public TeamDeathmatchPlugin(IServiceProvider serviceProvider, + ILoadoutManager loadoutManager) : base(serviceProvider) { - _configuration = configuration; _loadoutManager = loadoutManager; - _dataStore = dataStore; - _permissionRegistry = permissionRegistry; - - _redSpawns = new List<PlayerSpawn>(); - _blueSpawns = new List<PlayerSpawn>(); + _serviceProvider = serviceProvider; } protected override async UniTask OnLoadAsync() { - await ReloadSpawns(); - - var category = new TeamLoadoutCategory("Team Deathmatch", new List<string> {"TeamDeathmatch", "TDM"}, this, - _dataStore); - await category.LoadLoadouts(); - - foreach (var loadout in category.GetLoadouts().OfType<Loadout>()) - { - var permission = loadout.GetPermissionWithoutComponent(); - - if (permission != null) - { - _permissionRegistry.RegisterPermission(this, permission); - } - } - - _loadoutManager.AddCategory(category); - } - - protected override UniTask OnUnloadAsync() - { - return UniTask.CompletedTask; + await _loadoutManager.LoadAndAddCategory(new TDMLoadoutCategory(_serviceProvider)); } - - public IReadOnlyCollection<PlayerSpawn> RedSpawns => _redSpawns.AsReadOnly(); - - public IReadOnlyCollection<PlayerSpawn> BlueSpawns => _blueSpawns.AsReadOnly(); - - public async Task ReloadSpawns() - { - async Task LoadList<T>(string key, List<T> list) - { - var loadedList = await DataStore.ExistsAsync(key) ? await DataStore.LoadAsync<List<T>>(key) : null; - - loadedList ??= new List<T>(); - - list.Clear(); - list.AddRange(loadedList); - } - - await LoadList(RedSpawnsKey, _redSpawns); - await LoadList(BlueSpawnsKey, _blueSpawns); - } - - public IReadOnlyCollection<Loadout> Loadouts => - _configuration.GetSection("Loadouts").Get<List<Loadout>>() ?? - new List<Loadout>(); } } diff --git a/TeamDeathmatch/config.yaml b/TeamDeathmatch/config.yaml index 703d52e..ceb8f25 100644 --- a/TeamDeathmatch/config.yaml +++ b/TeamDeathmatch/config.yaml @@ -33,12 +33,15 @@ Rewards: - Id: Eaglefire Amount: 1 Chance: 0.1 # Represents a 10% chance + Losers: # Only losers will receive these rewards - Id: Tomato Amount: 5 + Tied: # If the game comes to a tie, both teams will receive these rewards - Id: Grilled Cheese Sandwich Amount: 2 + All: # Both teams will receive these rewards in all cases - Id: Police Vest Amount: 1 \ No newline at end of file