diff --git a/EXILED/EXILED.sln b/EXILED/EXILED.sln index 3614644a8..55cf8c82b 100644 --- a/EXILED/EXILED.sln +++ b/EXILED/EXILED.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exiled.CreditTags", "Exiled EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exiled.CustomRoles", "Exiled.CustomRoles\Exiled.CustomRoles.csproj", "{417C3309-8B93-4218-A1D1-D4BB7B09BE0F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exiled.CustomUnits", "Exiled.CustomUnits\Exiled.CustomUnits.csproj", "{9C4931B5-FB58-4069-815D-02C9FD8BEC05}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +76,12 @@ Global {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Installer|Any CPU.ActiveCfg = Installer|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Release|Any CPU.Build.0 = Release|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Installer|Any CPU.ActiveCfg = Debug|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Installer|Any CPU.Build.0 = Debug|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C4931B5-FB58-4069-815D-02C9FD8BEC05}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EXILED/Exiled.API/Extensions/StringExtensions.cs b/EXILED/Exiled.API/Extensions/StringExtensions.cs index 37d1a6777..961ed2a43 100644 --- a/EXILED/Exiled.API/Extensions/StringExtensions.cs +++ b/EXILED/Exiled.API/Extensions/StringExtensions.cs @@ -14,6 +14,7 @@ namespace Exiled.API.Extensions using System.Text; using System.Text.RegularExpressions; + using Exiled.API.Features; using Exiled.API.Features.Pools; /// @@ -74,6 +75,21 @@ public static (string commandName, string[] arguments) ExtractCommand(this strin return (extractedArguments[0].ToLower(), extractedArguments.Skip(1).ToArray()); } + /// + /// Parse players from a . + /// + /// Query to be parsed. + /// Separator to be used in the query. + /// An of if any found, otherwise an empty . + public static IEnumerable ParsePlayers(this string query, char separator = '.') + { + foreach (string str in query.Split(separator)) + { + if (Player.TryGet(str, out Player player)) + yield return player; + } + } + /// /// Converts a to snake_case convention. /// diff --git a/EXILED/Exiled.API/Features/Attributes/Config/CustomValidatorAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/CustomValidatorAttribute.cs new file mode 100644 index 000000000..e93cbaed2 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/CustomValidatorAttribute.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System; + + /// + /// A custom validator for config values and base class for all config validators. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class CustomValidatorAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// + public CustomValidatorAttribute(Func validator) + { + Validator = validator; + } + + /// + /// Gets a function that validates an object. + /// + public Func Validator { get; } + + /// + /// Validates an object. + /// + /// Object to validate. + /// true if validation was successful, false otherwise. + public bool Validate(object obj) => Validator(obj); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Config/MaxLengthAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/MaxLengthAttribute.cs new file mode 100644 index 000000000..d29badd0a --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/MaxLengthAttribute.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System.Collections; + using System.Linq; + + /// + /// An attribute that checks if length of a sequence is less than a specified value. + /// + public class MaxLengthAttribute : CustomValidatorAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Maximum length of sequence. + /// Whether check is inclusive or not. + public MaxLengthAttribute(int length, bool inclusive = false) + : base(x => x is IEnumerable enumerable && enumerable.Cast().Count() < (inclusive ? length + 1 : length)) + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Config/MaxValueAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/MaxValueAttribute.cs new file mode 100644 index 000000000..ce21b010b --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/MaxValueAttribute.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System.Collections; + + /// + /// An attribute to check if a value is less than a specified value. + /// + public class MaxValueAttribute : CustomValidatorAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Maximum value. + /// Whether check should be inclusive or not. + public MaxValueAttribute(object maxValue, bool inclusive = true) + : base(x => Comparer.Default.Compare(maxValue, x) > (inclusive ? -1 : 0)) + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Config/MinLengthAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/MinLengthAttribute.cs new file mode 100644 index 000000000..897e7360f --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/MinLengthAttribute.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System.Collections; + using System.Linq; + + /// + /// An attribute that checks if length of a sequence is greater than a specified value. + /// + public class MinLengthAttribute : CustomValidatorAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Maximum length of sequence. + /// Whether check is inclusive or not. + public MinLengthAttribute(int length, bool inclusive = false) + : base(x => x is IEnumerable enumerable && enumerable.Cast().Count() > (inclusive ? length - 1 : length)) + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Config/MinValueAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/MinValueAttribute.cs new file mode 100644 index 000000000..a4f2c738e --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/MinValueAttribute.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System.Collections; + + /// + /// An attribute to check if value is greater than or equal to min value. + /// + public class MinValueAttribute : CustomValidatorAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Minimum value. + /// Whether check should be inclusive or not. + public MinValueAttribute(object minValue, bool inclusive = true) + : base(x => Comparer.Default.Compare(x, minValue) > (inclusive ? -1 : 0)) + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/Config/OneOfAttribute.cs b/EXILED/Exiled.API/Features/Attributes/Config/OneOfAttribute.cs new file mode 100644 index 000000000..ab317dbc7 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/Config/OneOfAttribute.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes.Config +{ + using System; + + /// + /// An attribute to check if a value is one of a possible values. + /// + public class OneOfAttribute : CustomValidatorAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Values. + public OneOfAttribute(params object[] values) + : base(x => Array.IndexOf(values, x) != -1) + { + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Attributes/CustomTeamAttribute.cs b/EXILED/Exiled.API/Features/Attributes/CustomTeamAttribute.cs new file mode 100644 index 000000000..c575c0e98 --- /dev/null +++ b/EXILED/Exiled.API/Features/Attributes/CustomTeamAttribute.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Attributes +{ + using System; + + /// + /// An attribute to easily manage custom teams initialization. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class CustomTeamAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/ConstProperty.cs b/EXILED/Exiled.API/Features/Core/ConstProperty.cs new file mode 100644 index 000000000..66ca11a7b --- /dev/null +++ b/EXILED/Exiled.API/Features/Core/ConstProperty.cs @@ -0,0 +1,202 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Core +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + + using HarmonyLib; + using Mono.Cecil; + + /// + /// A class to easily manipulate const values. + /// + /// Type of constant. + public class ConstProperty + where T : IEquatable + { + /// + /// Gets the list of all . + /// + internal static readonly List> List = new(); + + private static readonly Harmony Harmony = new($"Exiled.API-{typeof(T).Name}"); + + private readonly IEnumerable skipMethods; + private readonly Type type; + private IEnumerable methodsToPatch; + private IEnumerable patchedMethods; + + private T value; + private bool patched; + + /// + /// Initializes a new instance of the class. + /// + /// Constant value. + /// Type to patch. + /// Methods that won't be patched. + internal ConstProperty(T defaultValue, Type type, params string[] skipMethods) + { + DefaultValue = defaultValue; + value = defaultValue; + + this.type = type; + this.skipMethods = skipMethods.Select(x => type.GetMethod(x, AccessTools.all)); + + List.Add(this); + } + + /// + /// Gets the default constant value. + /// + public T DefaultValue { get; } + + /// + /// Gets or sets the new value. + /// + public T Value + { + get => value; + set + { + if (this.value.Equals(value)) + return; + + this.value = value; + + if (!patched) + patchedMethods = Patch(); + else + Repatch(); + } + } + + /// + /// Gets the methods that will be or already patched to replace constant value. + /// + public IEnumerable MethodsToPatch => methodsToPatch ??= GetMethodsToPatch(); + + /// + /// Gets the patched methods. + /// + /// Can be null if no patches were performed. + public IEnumerable PatchedMethods => !patched ? null : patchedMethods; + + /// + /// Patches methods to replace values. + /// + /// A collection of methods that replacing actual methods. + public IEnumerable Patch() + { + foreach (MethodInfo method in MethodsToPatch) + { + MethodInfo returnInfo; + + try + { + returnInfo = Harmony.Patch(method, transpiler: new(typeof(ConstProperty), nameof(Transpiler))); + } + catch (HarmonyException exception) + { + Log.Error(exception); + continue; + } + + yield return returnInfo; + } + + patched = true; + } + + /// + /// Unpatches methods and then patches them again. + /// + public void Repatch() + { + foreach (MethodInfo method in MethodsToPatch) + Harmony.Unpatch(method, PatchedMethods.First(x => x.Name.Contains(method.Name))); + + patchedMethods = Patch(); + } + + /// + /// Gets the methods that should be patched to replace constant value. + /// + /// of . + public IEnumerable GetMethodsToPatch() + { + AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(Path.Combine(Paths.ManagedAssemblies, "Assembly-CSharp.dll")); + + foreach (MethodInfo method in type.GetProperties().Where(x => x.DeclaringType == type && x.GetMethod != null && !skipMethods.Contains(x.GetMethod)).Select(x => x.GetMethod)) + { + if (assembly.MainModule.ImportReference(method).Resolve().Body.Instructions.Any(x => x.Operand is T obj && obj.Equals(DefaultValue))) + yield return method; + } + + foreach (MethodInfo method in type.GetProperties().Where(x => x.DeclaringType == type && x.SetMethod != null && !skipMethods.Contains(x.SetMethod)).Select(x => x.SetMethod)) + { + if (assembly.MainModule.ImportReference(method).Resolve().Body.Instructions.Any(x => x.Operand is T obj && obj.Equals(DefaultValue))) + yield return method; + } + + foreach (MethodInfo method in type.GetMethods().Where(x => x.DeclaringType == type && !skipMethods.Contains(x))) + { + if (assembly.MainModule.ImportReference(method).Resolve().Body.Instructions.Any(x => x.Operand is T obj && obj.Equals(DefaultValue))) + yield return method; + } + } + + /// + /// Gets the for the specified method. + /// + /// Method to patch. + /// Default value to replace with. + /// or null. + internal static ConstProperty Get(MethodInfo method, T defaultValue) + { + List> properties = List.FindAll(x => x.MethodsToPatch.Contains(method)); + + return properties.Count switch + { + 0 => null, + 1 => properties[0], + _ => properties.Find(x => x.DefaultValue.Equals(defaultValue)) + }; + } + + private static IEnumerable Transpiler(IEnumerable instructions, MethodBase original) + { + foreach (CodeInstruction instruction in instructions) + { + if (instruction.operand is not T obj) + { + yield return instruction; + continue; + } + + ConstProperty property = Get((MethodInfo)original, obj); + + if (property == null || !property.DefaultValue.Equals(obj)) + { + yield return instruction; + continue; + } + + if (typeof(T) == typeof(float)) + yield return new(OpCodes.Ldc_R4, obj); + else + yield return new(instruction.opcode, obj); + } + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Plugin.cs b/EXILED/Exiled.API/Features/Plugin.cs index 6153cbeee..13f2c56c8 100644 --- a/EXILED/Exiled.API/Features/Plugin.cs +++ b/EXILED/Exiled.API/Features/Plugin.cs @@ -7,15 +7,15 @@ namespace Exiled.API.Features { - using System.Linq; - -#pragma warning disable SA1402 +#pragma warning disable SA1402 // File may only contain a single type using System; using System.Collections.Generic; + using System.Linq; using System.Reflection; using CommandSystem; using Enums; + using Exiled.API.Features.Pools; using Extensions; using Interfaces; using RemoteAdmin; @@ -99,82 +99,80 @@ public virtual void OnEnabled() /// public virtual void OnRegisteringCommands() { - Dictionary> toRegister = new(); + Dictionary> toRegister = DictionaryPool>.Pool.Get(); + Dictionary parentCommands = DictionaryPool.Pool.Get(); foreach (Type type in Assembly.GetTypes()) { if (type.GetInterface("ICommand") != typeof(ICommand)) continue; - if (!Attribute.IsDefined(type, typeof(CommandHandlerAttribute))) - continue; + ICommand command = (ICommand)Activator.CreateInstance(type); - foreach (CustomAttributeData customAttributeData in type.GetCustomAttributesData()) + foreach (CustomAttributeData attributeData in type.GetCustomAttributesData()) { - try - { - if (customAttributeData.AttributeType != typeof(CommandHandlerAttribute)) - continue; - - Type commandHandlerType = (Type)customAttributeData.ConstructorArguments[0].Value; + if (attributeData.AttributeType == typeof(CommandHandlerAttribute)) + continue; - ICommand command = GetCommand(type) ?? (ICommand)Activator.CreateInstance(type); + Type attribute = (Type)attributeData.ConstructorArguments[0].Value; - if (typeof(ParentCommand).IsAssignableFrom(commandHandlerType)) + try + { + if (typeof(ParentCommand).IsAssignableFrom(attribute)) { - ParentCommand parentCommand = GetCommand(commandHandlerType) as ParentCommand; - - if (parentCommand == null) + if (parentCommands.TryGetValue(attribute, out ParentCommand parentCommand)) { - if (!toRegister.TryGetValue(commandHandlerType, out List list)) - toRegister.Add(commandHandlerType, new() { command }); - else + parentCommand.RegisterCommand(command); + } + else + { + if (toRegister.TryGetValue(attribute, out List list)) list.Add(command); - - continue; + else + toRegister.Add(attribute, new() { command }); } - - parentCommand.RegisterCommand(command); - continue; } - - try + else { - if (commandHandlerType == typeof(RemoteAdminCommandHandler)) + if (attribute == typeof(RemoteAdminCommandHandler)) CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(command); - else if (commandHandlerType == typeof(GameConsoleCommandHandler)) + else if (attribute == typeof(GameConsoleCommandHandler)) GameCore.Console.singleton.ConsoleCommandHandler.RegisterCommand(command); - else if (commandHandlerType == typeof(ClientCommandHandler)) + else if (attribute == typeof(ClientCommandHandler)) QueryProcessor.DotCommandHandler.RegisterCommand(command); } - catch (ArgumentException e) + } + catch (ArgumentException argumentException) + { + if (argumentException.Message.StartsWith("An")) { - if (e.Message.StartsWith("An")) - { - Log.Error($"Command with same name has already registered! Command: {command.Command}"); - } - else - { - Log.Error($"An error has occurred while registering a command: {e}"); - } + Log.Error($"Command with same name has already registered! Command: {command.Command}"); + } + else + { + Log.Error($"An error has occurred while registering a command: {argumentException}"); } - - Commands[commandHandlerType][type] = command; } catch (Exception exception) { Log.Error($"An error has occurred while registering a command: {exception}"); } - } - } - foreach (KeyValuePair> kvp in toRegister) - { - ParentCommand parentCommand = GetCommand(kvp.Key) as ParentCommand; + if (command is ParentCommand) + { + ParentCommand parentCommand = (ParentCommand)command; - foreach (ICommand command in kvp.Value) - parentCommand.RegisterCommand(command); + if (toRegister.TryGetValue(type, out List list)) + { + foreach (ICommand com in list) + parentCommand.RegisterCommand(com); + } + } + } } + + DictionaryPool.Pool.Return(parentCommands); + DictionaryPool>.Pool.Return(toRegister); } /// diff --git a/EXILED/Exiled.API/Features/Spawn/SpawnProperties.cs b/EXILED/Exiled.API/Features/Spawn/SpawnProperties.cs index 4cdf3345b..cc227462d 100644 --- a/EXILED/Exiled.API/Features/Spawn/SpawnProperties.cs +++ b/EXILED/Exiled.API/Features/Spawn/SpawnProperties.cs @@ -7,8 +7,11 @@ namespace Exiled.API.Features.Spawn { + using System; using System.Collections.Generic; + using Exiled.API.Extensions; + /// /// Handles special properties of spawning an item. /// @@ -49,5 +52,52 @@ public class SpawnProperties /// /// How many spawn points there are. public int Count() => DynamicSpawnPoints.Count + StaticSpawnPoints.Count + RoleSpawnPoints.Count + RoomSpawnPoints.Count + LockerSpawnPoints.Count; + + /// + /// Gets a random spawn point from all available. + /// + /// A random spawn point or null if is 0. + public SpawnPoint GetRandomSpawnPoint() + { + List points = new(DynamicSpawnPoints); + points.AddRange(StaticSpawnPoints); + points.AddRange(RoleSpawnPoints); + points.AddRange(RoomSpawnPoints); + points.AddRange(LockerSpawnPoints); + + for (int i = 0; i < 20; i++) + { + SpawnPoint point = points.GetRandomValue(); + + if (point.Chance < UnityEngine.Random.value * 100) + return point; + } + + return points.GetRandomValue(); + } + + /// + /// Gets a random spawn point from all available. + /// + /// A filter to choose a spawn point from. + /// A random spawn point or null if is 0. + public SpawnPoint GetRandomSpawnPoint(Func filter) + { + List points = new(DynamicSpawnPoints); + points.AddRange(StaticSpawnPoints); + points.AddRange(RoleSpawnPoints); + points.AddRange(RoomSpawnPoints); + points.AddRange(LockerSpawnPoints); + + for (int i = 0; i < 20; i++) + { + SpawnPoint point = points.GetRandomValue(); + + if (point.Chance < UnityEngine.Random.value * 100) + return point; + } + + return points.GetRandomValue(filter); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/API/Extensions.cs b/EXILED/Exiled.CustomUnits/API/Extensions.cs new file mode 100644 index 000000000..656433ff3 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/API/Extensions.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.API +{ + using System.Collections.Generic; + + using Exiled.API.Features; + using Exiled.CustomUnits.API.Features; + + /// + /// A class containing extension methods for API. + /// + public static class Extensions + { + /// + /// Gets all custom units from player. + /// + /// Target player. + /// A collection of custom units. + public static IEnumerable GetCustomUnits(this Player player) + { + foreach (CustomUnit? customUnit in CustomUnit.Registered) + { + if (customUnit?.Check(player) ?? false) + yield return customUnit; + } + } + + /// + /// Registers a custom unit. + /// + /// instance. + /// true if successful, false otherwise. + public static bool Register(this CustomUnit customUnit) => customUnit.TryRegister(); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/API/Features/CustomUnit.cs b/EXILED/Exiled.CustomUnits/API/Features/CustomUnit.cs new file mode 100644 index 000000000..b76249657 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/API/Features/CustomUnit.cs @@ -0,0 +1,530 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.API.Features +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using Exiled.API.Enums; + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Attributes; + using Exiled.API.Features.Spawn; + using Exiled.API.Interfaces; + using Exiled.CustomItems.API.Features; + using Exiled.CustomUnits.API.Features.Enums; + using Exiled.Events.EventArgs.Interfaces; + using Exiled.Events.EventArgs.Player; + using Exiled.Events.EventArgs.Server; + using MEC; + using PlayerRoles; + using Respawning; + using YamlDotNet.Serialization; + + /// + /// A generic class for all custom units. + /// + public abstract class CustomUnit + { + private static Dictionary typeLookupTable = new(); + private static Dictionary stringLookupTable = new(); + private static Dictionary idLookupTable = new(); + + /// + /// Gets a list of all registered custom units. + /// + public static HashSet Registered { get; } = new(); + + /// + /// Gets or sets the custom ID of the unit. + /// + public abstract uint Id { get; set; } + + /// + /// Gets or sets the name of this unit. + /// + public abstract string Name { get; set; } + + /// + /// Gets or sets the description of this unit. + /// + public abstract string Description { get; set; } + + /// + /// Gets or sets the to spawn this unit as. + /// + public abstract List Roles { get; set; } + + /// + /// Gets or sets the spawn type for this unit. + /// + public virtual SpawnType SpawnType { get; set; } = SpawnType.None; + + /// + /// Gets all of the players currently set to this unit. + /// + [YamlIgnore] + public HashSet TrackedPlayers { get; } = new(); + + /// + /// Gets or sets the amount of current tickets. + /// + /// Will be -1 if is not . + [YamlIgnore] + public float CurrentTickets { get; set; } = -1f; + + /// + /// Gets or sets the amount of maximum tickets that this unit requires to spawn. + /// + public virtual float MinimumTickets { get; set; } = 50f; + + /// + /// Gets or sets a predicate that checks if an event can add tickets to this unit. + /// + public virtual Func CheckGrant { get; set; } = _ => true; + + /// + /// Gets or sets the amount of maximum tickets that will be subtracted from when this unit spawns. + /// + public virtual float ReduceAmount { get; set; } = 30f; + + /// + /// Gets or sets the spawn chance for this unit. + /// + /// Will be -1 if is not . + public virtual float SpawnChance { get; set; } = -1f; + + /// + /// Gets or sets the starting inventory for the unit. + /// + public virtual List Inventory { get; set; } = new(); + + /// + /// Gets or sets the starting ammo for the unit. + /// + public virtual Dictionary Ammo { get; set; } = new(); + + /// + /// Gets or sets the possible spawn locations for this unit. + /// + public virtual SpawnProperties? SpawnProperties { get; set; } + + /// + /// Gets or sets a value indicating whether players keep their current position when gaining this unit. + /// + public virtual bool KeepPositionOnSpawn { get; set; } + + /// + /// Gets or sets a value indicating whether players keep their current inventory when gaining this unit. + /// + public virtual bool KeepInventoryOnSpawn { get; set; } + + /// + /// Gets or sets an announcement that will be played when unit is spawned. + /// + public virtual string? CassieAnnouncement { get; set; } + + /// + /// Gets or sets a containing custom friendly fire multipliers for every role type. + /// + public virtual Dictionary CustomUnitFFMultiplier { get; set; } = new(); + + /// + /// Gets or sets a of , or that couldn't be damaged by this unit. + /// + public virtual List Friends { get; set; } = new(); + + /// + /// Gets or sets a minimum amount of players that will be spawned. + /// + public virtual int MinimumToSpawn { get; set; } = 1; + + /// + /// Gets or sets a maximum amount of players that will be spawned. + /// + public virtual int MaximumToSpawn { get; set; } = 10; + + /// + /// Gets a by ID. + /// + /// The ID of the unit to get. + /// The unit, or if it doesn't exist. + public static CustomUnit? Get(uint id) + { + if (!idLookupTable.ContainsKey(id)) + idLookupTable.Add(id, Registered?.FirstOrDefault(r => r.Id == id)); + return idLookupTable[id]; + } + + /// + /// Gets a by type. + /// + /// The to get. + /// The unit, or if it doesn't exist. + public static CustomUnit? Get(Type t) + { + if (!typeLookupTable.ContainsKey(t)) + typeLookupTable.Add(t, Registered?.FirstOrDefault(r => r.GetType() == t)); + return typeLookupTable[t]; + } + + /// + /// Gets a by name. + /// + /// The name of the unit to get. + /// The unit, or if it doesn't exist. + public static CustomUnit? Get(string name) + { + if (!stringLookupTable.ContainsKey(name)) + stringLookupTable.Add(name, Registered?.FirstOrDefault(r => r.Name == name)); + return stringLookupTable[name]; + } + + /// + /// Tries to get a by . + /// + /// The ID of the unit to get. + /// The custom unit. + /// True if the unit exists. + public static bool TryGet(uint id, out CustomUnit? customUnit) + { + customUnit = Get(id); + + return customUnit is not null; + } + + /// + /// Tries to get a by name. + /// + /// The name of the unit to get. + /// The custom unit. + /// True if the unit exists. + /// If the name is or an empty string. + public static bool TryGet(string name, out CustomUnit? customUnit) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException(nameof(name)); + + customUnit = uint.TryParse(name, out uint id) ? Get(id) : Get(name); + + return customUnit is not null; + } + + /// + /// Tries to get a by name. + /// + /// The of the unit to get. + /// The custom unit. + /// True if the unit exists. + /// If the name is or an empty string. + public static bool TryGet(Type t, out CustomUnit? customUnit) + { + customUnit = Get(t); + + return customUnit is not null; + } + + /// + /// Register all custom units present in the assembly. + /// + /// Types that will be registered if inheriting . + /// Whether reflection should be skipped. It's used for creating custom unit from a properties. + /// The class where reflection would be used. If null plugin's config will be used instead. + /// Whether types without can be registered. + /// Assembly from which types for registration will be taken if is null. + /// A collection of all registered custom units. + public static IEnumerable RegisterUnits(IEnumerable? targetTypes = null, bool skipReflection = false, object? overrideClass = null, bool ignoreAttributes = false, Assembly? assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + targetTypes ??= assembly.GetTypes(); + + foreach (Type type in targetTypes) + { + if (type.BaseType != typeof(CustomUnit) || (!ignoreAttributes && type.GetCustomAttribute() == null)) + continue; + + CustomUnit? customUnit = null; + + if (!skipReflection && Server.PluginAssemblies.TryGetValue(assembly, out IPlugin plugin)) + { + foreach (PropertyInfo property in overrideClass?.GetType().GetProperties() ?? + plugin.Config.GetType().GetProperties()) + { + if (property.PropertyType != type) + continue; + + customUnit = property.GetValue(overrideClass ?? plugin.Config) as CustomUnit; + } + } + + customUnit ??= (CustomUnit)Activator.CreateInstance(type); + + if (customUnit.TryRegister()) + yield return customUnit; + } + } + + /// + /// Forces a to spawn. + /// + /// Players that will be spawned. + public void Spawn(IEnumerable? players = null) + { + IList playerToSpawn = (players ?? Player.List.Where(x => RespawnManager.Singleton.CheckSpawnable(x.ReferenceHub))).ToArray(); + + if (playerToSpawn.Count < MinimumToSpawn) + return; + + playerToSpawn.ShuffleList(); + + IList units = new UnitRole[] { }; + + foreach (UnitRole unit in Roles) + { + for (int i = 0; i < unit.MaximumAmount; i++) + units.Add(unit); + } + + for (int i = 0; i < Math.Min(MaximumToSpawn, playerToSpawn.Count); i++) + { + UnitRole unit = units.Any(x => x.MustSpawn) ? units.GetRandomValue(x => x.MustSpawn) : units.GetRandomValue(); + + GrantRole(playerToSpawn[i], unit); + units.Remove(unit); + + TrackedPlayers.Add(playerToSpawn[i]); + } + + if (CassieAnnouncement != null) + Cassie.Message(CassieAnnouncement); + } + + /// + /// Grants a unit to a player. + /// + /// Target player. + /// Role to grant. + public void GrantRole(Player player, UnitRole unit) + { + if (unit.CustomRole != null) + unit.CustomRole.AddRole(player); + else if (unit.RoleTypeId != RoleTypeId.None) + player.Role.Set(unit.RoleTypeId, SpawnReason.Respawn, RoleSpawnFlags.All); + else + throw new ArgumentException("Role must have a custom unit or a unit type.", nameof(unit)); + + Timing.CallDelayed(0.5f, () => + { + if (SpawnProperties != null && !KeepPositionOnSpawn) + { + SpawnPoint spawnPoint = SpawnProperties.GetRandomSpawnPoint(); + + player.Position = spawnPoint.Position; + } + + if (!KeepInventoryOnSpawn) + { + player.ClearAmmo(); + player.ClearItems(); + + player.SetAmmo(Ammo); + + foreach (string str in Inventory) + { + if (CustomItem.TryGet(str, out CustomItem? customItem) && customItem != null) + { + customItem.Give(player); + continue; + } + + if (Enum.TryParse(str, out ItemType itemType)) + { + player.AddItem(itemType); + continue; + } + + Log.Warn($"{Name}: {str} is not a valid ItemType or Custom Item name."); + } + } + + player.FriendlyFireMultiplier = CustomUnitFFMultiplier; + + OnGrantedRole(player, unit); + }); + } + + /// + /// Tries to register a . + /// + /// true if successful, false otherwise. + public bool TryRegister() + { + if (!CustomRoles.CustomRoles.Instance.Config.IsEnabled) + return false; + + if (!Registered.Contains(this)) + { + if (Registered.Any(r => r.Id == Id)) + { + Log.Warn($"{Name} has tried to register with the same Role ID as another unit: {Id}. It will not be registered!"); + + return false; + } + + Registered.Add(this); + Init(); + + Log.Debug($"{Name} ({Id}) has been successfully registered."); + + return true; + } + + Log.Warn($"Couldn't register {Name} ({Id}) as it already exists."); + + return false; + } + + /// + /// Checks if is tracked by . + /// + /// Target to check. + /// true if tracked, false otherwise. + public bool Check(Player player) => TrackedPlayers.Contains(player); + + /// + /// Grants specified amount of tickets to unit. + /// + /// Amount of tickets to grant. + public void GrantTickets(float amount) + { + if (SpawnType != SpawnType.Ticket) + return; + + CurrentTickets += amount; + } + + /// + /// Initializes . + /// + protected virtual void Init() + { + Friends.Add(Id.ToString()); + + SubscribeEvents(); + + typeLookupTable[GetType()] = this; + idLookupTable[Id] = this; + stringLookupTable[Name] = this; + } + + /// + /// Destroys . + /// + protected virtual void Destroy() + { + UnsubscribeEvents(); + + typeLookupTable.Remove(GetType()); + idLookupTable.Remove(Id); + stringLookupTable.Remove(Name); + } + + /// + /// Subscribes to events. + /// + protected virtual void SubscribeEvents() + { + Events.Handlers.Server.RespawningTeam += OnInternalRespawningTeam; + Events.Handlers.Player.Shot += OnInternalShot; + Events.Handlers.Player.ChangingRole += OnInternalChangingRole; + } + + /// + /// Unsubscribes from events. + /// + protected virtual void UnsubscribeEvents() + { + Events.Handlers.Server.RespawningTeam -= OnInternalRespawningTeam; + Events.Handlers.Player.Shot -= OnInternalShot; + Events.Handlers.Player.ChangingRole -= OnInternalChangingRole; + } + + /// + /// Fired when method is finished. + /// + /// Player that received the unit. + /// Role that was granted. + protected virtual void OnGrantedRole(Player player, UnitRole unit) + { + } + + /// + /// Fired when unit operative is shooting. + /// + /// instance. + /// Whether or not can be damaged. + protected virtual void OnShot(ShotEventArgs ev, bool canDamage) + { + } + + /// + /// Fired when unit is respawning. + /// + /// Players that will be respawned. + protected virtual void OnRespawning(IEnumerable players) + { + } + + private void OnInternalShot(ShotEventArgs ev) + { + if (ev.Target == null || !Check(ev.Player)) + return; + + bool canDamage = !(Friends.Contains(ev.Target.Role.Type.ToString()) || Friends.Contains(ev.Target.Role.Team.ToString())); + CustomUnit[] targetUnits = ev.Target.GetCustomUnits().ToArray(); + + if (targetUnits.Length > 0 && Friends.Exists(x => Array.Exists(targetUnits, y => y.Id.ToString() == x))) + canDamage = false; + + OnShot(ev, canDamage); + + if (!canDamage) + ev.Damage = 0; + } + + private void OnInternalRespawningTeam(RespawningTeamEventArgs ev) + { + if (SpawnType == SpawnType.None) + return; + + if (SpawnType == SpawnType.Ticket && CurrentTickets >= MinimumTickets) + { + CurrentTickets = Math.Max(0, CurrentTickets - ReduceAmount); + Spawn(ev.Players); + OnRespawning(ev.Players); + } + + if (SpawnType == SpawnType.Chance && UnityEngine.Random.value * 100 <= SpawnChance) + { + Spawn(ev.Players); + OnRespawning(ev.Players); + } + } + + private void OnInternalChangingRole(ChangingRoleEventArgs ev) + { + if (Check(ev.Player)) + { + TrackedPlayers.Remove(ev.Player); + Destroy(); + } + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/API/Features/Enums/SpawnType.cs b/EXILED/Exiled.CustomUnits/API/Features/Enums/SpawnType.cs new file mode 100644 index 000000000..c315095a9 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/API/Features/Enums/SpawnType.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.API.Features.Enums +{ + /// + /// All available spawn types for . + /// + public enum SpawnType + { + /// + /// Unit can be spawned only via command. + /// + None, + + /// + /// Unit can replace another non-custom unit if it has enough tickets. + /// + Ticket, + + /// + /// Unit can replace another custom unit with specified chance. + /// + Chance, + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/API/Features/UnitRole.cs b/EXILED/Exiled.CustomUnits/API/Features/UnitRole.cs new file mode 100644 index 000000000..6442b0cc8 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/API/Features/UnitRole.cs @@ -0,0 +1,72 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.API.Features +{ + using Exiled.CustomRoles.API.Features; + using PlayerRoles; + + /// + /// A wrapper for roles in . + /// + public class UnitRole + { + /// + /// Initializes a new instance of the class. + /// + public UnitRole() + { + RoleTypeId = RoleTypeId.None; + CustomRole = null; + MaximumAmount = 0; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public UnitRole(RoleTypeId roleTypeId, int maximumAmount = 1) + { + RoleTypeId = roleTypeId; + CustomRole = null; + MaximumAmount = maximumAmount; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public UnitRole(CustomRole customRole, int maximumAmount = 1) + { + RoleTypeId = RoleTypeId.None; + CustomRole = customRole; + MaximumAmount = maximumAmount; + } + + /// + /// Gets or sets a game role. + /// + public RoleTypeId RoleTypeId { get; set; } + + /// + /// Gets or sets a custom role. + /// + public CustomRole? CustomRole { get; set; } + + /// + /// Gets or sets a maximum amount of players that can get this role. + /// + public int MaximumAmount { get; set; } + + /// + /// Gets or sets a value indicating whether this role must spawn in wave. + /// + public bool MustSpawn { get; set; } = false; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Commands/Info.cs b/EXILED/Exiled.CustomUnits/Commands/Info.cs new file mode 100644 index 000000000..0f821ecf6 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Commands/Info.cs @@ -0,0 +1,64 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.Commands +{ + using System; + using System.Text; + + using CommandSystem; + using Exiled.API.Features.Pools; + using Exiled.CustomUnits.API.Features; + using Exiled.Permissions.Extensions; + + /// + /// The command to view info about a specific role. + /// + [CommandHandler(typeof(Parent))] + internal sealed class Info : ICommand + { + /// + public string Command { get; } = "info"; + + /// + public string[] Aliases { get; } = { "i" }; + + /// + public string Description { get; } = "Gets more information about the specified custom role."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customunits.info")) + { + response = "Permission Denied, required: customunits.info"; + return false; + } + + if (arguments.Count < 1) + { + response = "info [Custom unit name/Custom unit ID]"; + return false; + } + + if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomUnit.TryGet(id, out CustomUnit? unit)) && !CustomUnit.TryGet(arguments.At(0), out unit)) || unit is null) + { + response = $"{arguments.At(0)} is not a valid custom unit."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("- ").Append(unit.Name) + .Append(" (").Append(unit.Id).Append(")") + .Append("- ").AppendLine(unit.Description).AppendLine(); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Commands/List/List.cs b/EXILED/Exiled.CustomUnits/Commands/List/List.cs new file mode 100644 index 000000000..72960477d --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Commands/List/List.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.Commands.List +{ + using System; + + using CommandSystem; + + /// + /// The command to list all registered roles. + /// + internal sealed class List : ParentCommand + { + /// + public override string Command { get; } = "list"; + + /// + public override string[] Aliases { get; } = { "l" }; + + /// + public override string Description { get; } = "Gets a list of all currently registered custom roles."; + + /// + public override void LoadGeneratedCommands() + { + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + if (arguments.IsEmpty() && TryGetCommand(Registered.Instance.Command, out ICommand command)) + { + command.Execute(arguments, sender, out response); + return true; + } + + response = "Invalid subcommand! Available: registered"; + return false; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Commands/List/Registered.cs b/EXILED/Exiled.CustomUnits/Commands/List/Registered.cs new file mode 100644 index 000000000..4008459c9 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Commands/List/Registered.cs @@ -0,0 +1,67 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.Commands.List +{ + using System; + using System.Linq; + using System.Text; + + using CommandSystem; + using Exiled.API.Features.Pools; + using Exiled.CustomUnits.API.Features; + using Exiled.Permissions.Extensions; + + /// + [CommandHandler(typeof(List))] + internal sealed class Registered : ICommand + { + private Registered() + { + } + + /// + /// Gets the instance of the . + /// + public static Registered Instance { get; } = new(); + + /// + public string Command { get; } = "registered"; + + /// + public string[] Aliases { get; } = { "r" }; + + /// + public string Description { get; } = "Gets a list of registered custom roles."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customunits.list.registered")) + { + response = "Permission Denied, required: customunits.list.registered"; + return false; + } + + if (CustomUnit.Registered.Count == 0) + { + response = "There are no custom units currently on this server."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("[Registered custom units (").Append(CustomUnit.Registered.Count).AppendLine(")]"); + + foreach (CustomUnit unit in CustomUnit.Registered.OrderBy(r => r.Id)) + builder.Append('[').Append(unit.Id).Append(". ").Append(unit.Name).AppendLine("]"); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Commands/Parent.cs b/EXILED/Exiled.CustomUnits/Commands/Parent.cs new file mode 100644 index 000000000..c9fd90cb1 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Commands/Parent.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.Commands +{ + using System; + + using CommandSystem; + + /// + /// The main parent command. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + public class Parent : ParentCommand + { + /// + public override string Command { get; } = "customroles"; + + /// + public override string[] Aliases { get; } = { "cr", "crs" }; + + /// + public override string Description { get; } = string.Empty; + + /// + public override void LoadGeneratedCommands() + { + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: give, info, list"; + return false; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Commands/Spawn.cs b/EXILED/Exiled.CustomUnits/Commands/Spawn.cs new file mode 100644 index 000000000..b325a888f --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Commands/Spawn.cs @@ -0,0 +1,86 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits.Commands +{ + using System; + using System.Collections.Generic; + + using CommandSystem; + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Pools; + using Exiled.CustomUnits.API.Features; + using Exiled.Permissions.Extensions; + + /// + /// A command that spawns a custom unit. + /// + [CommandHandler(typeof(Parent))] + public class Spawn : ICommand + { + /// + public string Command { get; } = "spawn"; + + /// + public string[] Aliases { get; } = { "sp", "force" }; + + /// + public string Description { get; } = "Spawns a unit."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customroles.give")) + { + response = "Permission Denied, required: customroles.give"; + return false; + } + + if (arguments.Count == 0) + { + response = "give [Nickname/PlayerID/UserID/all/*]"; + return false; + } + + if (!CustomUnit.TryGet(arguments.At(0), out CustomUnit? unit) || unit is null) + { + response = $"Custom role {arguments.At(0)} not found!"; + return false; + } + + if (arguments.Count == 1) + { + unit.Spawn(); + + response = $"Unit {unit.Name} ({unit.Id}) spawn forced."; + return false; + } + + List players = ListPool.Pool.Get(arguments.At(1) switch + { + "*" or "all" => Player.List, + _ => arguments.At(0).ParsePlayers(), + }); + + unit.Spawn(players); + + response = $"Forced spawn of {unit.Name} with {players.Count} players."; + ListPool.Pool.Return(players); + return true; + } + catch (Exception e) + { + Log.Error(e); + response = "Error"; + return false; + } + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Config.cs b/EXILED/Exiled.CustomUnits/Config.cs new file mode 100644 index 000000000..f5c84bfdb --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Config.cs @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits +{ + using Exiled.API.Interfaces; + + /// + /// A class that holds the configuration for the custom units. + /// + public sealed class Config : IConfig + { + /// + public bool IsEnabled { get; set; } + + /// + public bool Debug { get; set; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/CustomUnits.cs b/EXILED/Exiled.CustomUnits/CustomUnits.cs new file mode 100644 index 000000000..654b2b453 --- /dev/null +++ b/EXILED/Exiled.CustomUnits/CustomUnits.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomUnits +{ + using Exiled.API.Features; + + /// + /// A class for custom units that implements . + /// + public class CustomUnits : Plugin + { + /// + /// Gets the current instance of . + /// + public static CustomUnits Instance { get; private set; } + + /// + public override void OnEnabled() + { + Instance = this; + + base.OnEnabled(); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomUnits/Exiled.CustomUnits.csproj b/EXILED/Exiled.CustomUnits/Exiled.CustomUnits.csproj new file mode 100644 index 000000000..348d383de --- /dev/null +++ b/EXILED/Exiled.CustomUnits/Exiled.CustomUnits.csproj @@ -0,0 +1,53 @@ + + + + + + + Library + Exiled.CustomUnits + true + Debug;Release;Installer + AnyCPU + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + + + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/"; fi + + + \ No newline at end of file diff --git a/EXILED/Exiled.Loader/ConfigManager.cs b/EXILED/Exiled.Loader/ConfigManager.cs index 30243056c..fe94ce46c 100644 --- a/EXILED/Exiled.Loader/ConfigManager.cs +++ b/EXILED/Exiled.Loader/ConfigManager.cs @@ -11,14 +11,14 @@ namespace Exiled.Loader using System.Collections.Generic; using System.IO; using System.Linq; + using System.Reflection; using API.Enums; using API.Extensions; using API.Interfaces; - using Exiled.API.Features; + using Exiled.API.Features.Attributes.Config; using Exiled.API.Features.Pools; - using YamlDotNet.Core; /// @@ -100,6 +100,7 @@ public static IConfig LoadDefaultConfig(this IPlugin plugin, Dictionary { string rawConfigString = Loader.Serializer.Serialize(rawDeserializedConfig); config = (IConfig)Loader.Deserializer.Deserialize(rawConfigString, plugin.Config.GetType()); + ValidateConfig(config, plugin); plugin.Config.CopyProperties(config); } catch (YamlException yamlException) @@ -111,6 +112,35 @@ public static IConfig LoadDefaultConfig(this IPlugin plugin, Dictionary return config; } + /// + /// Validates config of a plugin. + /// + /// Config to check. + /// Plugin which config will be checked. + public static void ValidateConfig(IConfig config, IPlugin plugin) + { + int success = 0; + + foreach (PropertyInfo property in config.GetType().GetProperties()) + { + IEnumerable attributes = property.GetCustomAttributes(); + + foreach (CustomValidatorAttribute attribute in attributes) + { + if (!attribute.Validate(property.GetValue(config))) + { + Log.Error($"Config validation failed for {property.Name} from plugin {plugin.Name} ({property.GetValue(config)}). Default value will be used instead."); + property.SetValue(config, property.GetValue(plugin.Config)); + continue; + } + + success++; + } + } + + Log.Debug($"{plugin.Name}'s configs have successfully passed {success} validations."); + } + /// /// Loads the config of a plugin using the separated distribution. ///