diff --git a/.github/workflows/programs.yml b/.github/workflows/programs.yml index 06b90c544d..2035c05cb6 100644 --- a/.github/workflows/programs.yml +++ b/.github/workflows/programs.yml @@ -32,7 +32,7 @@ jobs: run: dotnet publish ${{ matrix.proj_name }} -r ${{ matrix.target }} -c release -o builds/${{ matrix.target }} --self-contained true - name: Upload ${{ matrix.proj_name }}@${{ matrix.target }} build - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ${{ matrix.proj_name }}-${{ matrix.target }} path: builds/${{ matrix.target }} diff --git a/EXILED.props b/EXILED.props index 399e8b0c07..1c659c98cf 100644 --- a/EXILED.props +++ b/EXILED.props @@ -15,7 +15,7 @@ - 9.0.0-beta.2 + 9.0.0-beta.3 false diff --git a/Exiled.API/Extensions/ItemExtensions.cs b/Exiled.API/Extensions/ItemExtensions.cs index bae5b06c6c..ef96fe510b 100644 --- a/Exiled.API/Extensions/ItemExtensions.cs +++ b/Exiled.API/Extensions/ItemExtensions.cs @@ -268,7 +268,7 @@ public static IEnumerable GetAttachmentIdentifiers(this Fi if (type.GetBaseCode() > code) code = type.GetBaseCode(); - if (!Firearm.ItemTypeToFirearmInstance.TryGetValue(type, out Firearm firearm)) + if (Item.Create(type.GetItemType()) is not Firearm firearm) throw new ArgumentException($"Couldn't find a Firearm instance matching the ItemType value: {type}."); firearm.Base.ApplyAttachmentsCode(code, true); diff --git a/Exiled.API/Extensions/ReflectionExtensions.cs b/Exiled.API/Extensions/ReflectionExtensions.cs index 1725f5c9e5..64c3f8fc3c 100644 --- a/Exiled.API/Extensions/ReflectionExtensions.cs +++ b/Exiled.API/Extensions/ReflectionExtensions.cs @@ -8,10 +8,10 @@ namespace Exiled.API.Extensions { using System; + using System.Collections.Generic; using System.Reflection; using HarmonyLib; - using LiteNetLib.Utils; /// @@ -51,11 +51,13 @@ public static void InvokeStaticEvent(this Type type, string eventName, object[] } /// - /// Copy all properties from the source class to the target one. + /// Copies all properties from the source object to the target object, performing a deep copy if necessary. /// - /// The target object. + /// The target object to copy properties to. /// The source object to copy properties from. - public static void CopyProperties(this object target, object source) + /// Indicates whether to perform a deep copy of properties that are of class type. + /// Thrown when the target and source types do not match. + public static void CopyProperties(this object target, object source, bool deepCopy = false) { Type type = target.GetType(); @@ -63,7 +65,82 @@ public static void CopyProperties(this object target, object source) throw new InvalidTypeException("Target and source type mismatch!"); foreach (PropertyInfo sourceProperty in type.GetProperties()) - type.GetProperty(sourceProperty.Name)?.SetValue(target, sourceProperty.GetValue(source, null), null); + { + if (sourceProperty.CanWrite) + { + object value = sourceProperty.GetValue(source, null); + + if (deepCopy && value is not null && sourceProperty.PropertyType.IsClass && + sourceProperty.PropertyType != typeof(string)) + { + object targetValue = Activator.CreateInstance(sourceProperty.PropertyType); + CopyProperties(targetValue, value, true); + sourceProperty.SetValue(target, targetValue, null); + } + else + { + sourceProperty.SetValue(target, value, null); + } + } + } + } + + /// + /// Removes the generic type suffix (e.g., '`1' from `List`1`) from a type name if it exists. + /// + /// The name of the type, which may include a generic suffix. + /// The type name without the generic suffix if it was present; otherwise, returns the original type name. + public static string RemoveGenericSuffix(this string typeName) + { + int indexOfBacktick = typeName.IndexOf('`'); + return indexOfBacktick >= 0 ? typeName.Substring(0, indexOfBacktick) : typeName; + } + + /// + /// Gets the first non-generic base type of a given type. + /// + /// The type for which to find the first non-generic base type. + /// The first non-generic base type, or null if none is found. + public static Type GetFirstNonGenericBaseType(this Type type) + { + Type baseType = type.BaseType; + + while (baseType != null && baseType.IsGenericType) + baseType = baseType.BaseType; + + return baseType; + } + + /// + /// Retrieves the names and values of all properties of an object based on the specified binding flags. + /// + /// The object whose properties are to be retrieved. + /// Optional. Specifies the binding flags to use for retrieving properties. Default is and . + /// A dictionary containing property names as keys and their respective values as values. + public static Dictionary GetPropertiesWithValue(this object obj, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public) + { + Dictionary propertyValues = new(); + + obj.GetType().GetProperties(bindingFlags) + .ForEach(property => propertyValues.Add(property.Name, property.GetValue(obj, null))); + + return propertyValues; + } + + /// + /// Retrieves the names and values of all fields of an object based on the specified binding flags. + /// + /// The object whose fields are to be retrieved. + /// Optional. Specifies the binding flags to use for retrieving fields. Default is and . + /// A dictionary containing field names as keys and their respective values as values. + public static Dictionary GetFieldsWithValue(this object obj, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public) + { + Dictionary propertyValues = new(); + + obj.GetType().GetFields(bindingFlags) + .ForEach(field => propertyValues.Add(field.Name, field.GetValue(obj))); + + return propertyValues; } } } \ No newline at end of file diff --git a/Exiled.API/Extensions/StringExtensions.cs b/Exiled.API/Extensions/StringExtensions.cs index 9d21a014d6..2b380e9fcf 100644 --- a/Exiled.API/Extensions/StringExtensions.cs +++ b/Exiled.API/Extensions/StringExtensions.cs @@ -92,6 +92,13 @@ public static string ToSnakeCase(this string str, bool shouldReplaceSpecialChars return shouldReplaceSpecialChars ? Regex.Replace(snakeCaseString, @"[^0-9a-zA-Z_]+", string.Empty) : snakeCaseString; } + /// + /// Converts a to kebab case convention. + /// + /// Input string. + /// A string converted to kebab case. + public static string ToKebabCase(this string input) => Regex.Replace(input, "([a-z])([A-Z])", "$1_$2").ToLower(); + /// /// Converts a from snake_case convention. /// @@ -204,5 +211,23 @@ public static string GetHashedUserId(this string userId) byte[] hash = Sha256.ComputeHash(textData); return BitConverter.ToString(hash).Replace("-", string.Empty); } + + /// + /// Encrypts a value using SHA-256 and returns a hexadecimal hash string. + /// + /// The value to encrypt. + /// A hexadecimal hash string of the encrypted value. + public static string GetHashedValue(this string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + byte[] hashBytes = Sha256.ComputeHash(bytes); + + StringBuilder hashStringBuilder = new(); + + foreach (byte b in hashBytes) + hashStringBuilder.Append(b.ToString("x2")); + + return hashStringBuilder.ToString(); + } } } \ No newline at end of file diff --git a/Exiled.API/Features/Attributes/ConfigAttribute.cs b/Exiled.API/Features/Attributes/ConfigAttribute.cs index 95c731b7cc..79410d80dc 100644 --- a/Exiled.API/Features/Attributes/ConfigAttribute.cs +++ b/Exiled.API/Features/Attributes/ConfigAttribute.cs @@ -10,7 +10,7 @@ namespace Exiled.API.Features.Attributes using System; /// - /// This attribute determines whether the class which is being applied to should be treated as . + /// This attribute determines whether the class which is being applied to should be treated as . /// [AttributeUsage(AttributeTargets.Class)] public class ConfigAttribute : Attribute @@ -18,21 +18,31 @@ public class ConfigAttribute : Attribute /// /// Initializes a new instance of the class. /// - public ConfigAttribute() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public ConfigAttribute(string folder, string name, bool isParent = false) + /// + /// The folder where the config file is stored. + /// This value is used to determine the path of the configuration file. + /// + /// + /// The name of the configuration file. + /// This is the unique name used to identify the configuration file. + /// + /// + /// A value indicating whether this configuration acts as a parent config. + /// If , this config will manage child configurations. + /// + /// + /// A value indicating whether this configuration is stand-alone and should not manage or be managed from other configurations. + /// + public ConfigAttribute( + string folder = null, + string name = null, + bool isParent = false, + bool isStandAlone = false) { Folder = folder; Name = name; IsParent = isParent; + IsStandAlone = isStandAlone; } /// @@ -46,8 +56,13 @@ public ConfigAttribute(string folder, string name, bool isParent = false) public string Name { get; } /// - /// Gets a value indicating whether the class on which this attribute is being applied to should be treated as parent . + /// Gets a value indicating whether the class on which this attribute is being applied to should be treated as parent . /// public bool IsParent { get; } + + /// + /// Gets a value indicating whether the config is individual. + /// + public bool IsStandAlone { get; } } } \ No newline at end of file diff --git a/Exiled.API/Features/ConfigSubsystem.cs b/Exiled.API/Features/ConfigSubsystem.cs new file mode 100644 index 0000000000..fbc17c8f7f --- /dev/null +++ b/Exiled.API/Features/ConfigSubsystem.cs @@ -0,0 +1,587 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features +{ +#nullable enable + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + + using Exiled.API.Extensions; + using Exiled.API.Features.Attributes; + using Exiled.API.Features.Core; + using Exiled.API.Features.Serialization; + using Exiled.API.Features.Serialization.CustomConverters; + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.NodeDeserializers; + + using ColorConverter = Serialization.CustomConverters.ColorConverter; + using UnderscoredNamingConvention = Serialization.UnderscoredNamingConvention; + + /// + /// The base class that handles the config subsystem. + /// + public sealed class ConfigSubsystem : TypeCastObject + { + /// + internal static readonly List ConfigsValue = new(); + + private static readonly Dictionary Cache = new(); + private static readonly List MainConfigsValue = new(); + private static bool isLoaded = false; + + private readonly HashSet data = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The config object. + public ConfigSubsystem(object obj) => Base = obj; + + /// + /// Gets or sets the serializer for configs and translations. + /// + public static ISerializer Serializer { get; set; } = GetDefaultSerializerBuilder().Build(); + + /// + /// Gets or sets the deserializer for configs and translations. + /// + public static IDeserializer Deserializer { get; set; } = GetDefaultDeserializerBuilder().Build(); + + /// + /// Gets or sets the JSON compatible serializer. + /// + public static ISerializer JsonSerializer { get; set; } = new SerializerBuilder() + .JsonCompatible() + .Build(); + + /// + /// Gets a containing all the . + /// + public static IReadOnlyCollection List => ConfigsValue; + + /// + /// Gets a containing all the subconfigs. + /// + public IEnumerable Subconfigs => data; + + /// + /// Gets the base config instance. + /// + public object? Base { get; private set; } + + /// + /// Gets or sets the config's folder. + /// + public string? Folder { get; set; } + + /// + /// Gets or sets the config's name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets a value indicating whether the config does not rely on any path. + /// + public bool IsStandAlone { get; set; } + + /// + /// Gets the absolute path. + /// + public string? AbsolutePath + { + get + { + bool isObjectInitialized = Folder is not null && Name is not null; + return !IsStandAlone ? + isObjectInitialized ? Path.Combine(Paths.Configs, Path.Combine(Folder, Name)) : null : + isObjectInitialized ? Path.Combine(Folder, Name) : null; + } + } + + /// + /// Gets the default serializer builder. + /// + /// The default serializer builder. + public static SerializerBuilder GetDefaultSerializerBuilder() => new SerializerBuilder() + .WithTypeConverter(new VectorsConverter()) + .WithTypeConverter(new ColorConverter()) + .WithTypeConverter(new AttachmentIdentifiersConverter()) + .WithTypeConverter(new EnumClassConverter()) + .WithTypeConverter(new PrivateConstructorConverter()) + .WithEventEmitter(eventEmitter => new TypeAssigningEventEmitter(eventEmitter)) + .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) + .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreFields() + .DisableAliases(); + + /// + /// Gets the default deserializer builder. + /// + /// The default deserializer builder. + public static DeserializerBuilder GetDefaultDeserializerBuilder() => new DeserializerBuilder() + .WithTypeConverter(new VectorsConverter()) + .WithTypeConverter(new ColorConverter()) + .WithTypeConverter(new AttachmentIdentifiersConverter()) + .WithTypeConverter(new EnumClassConverter()) + .WithTypeConverter(new PrivateConstructorConverter()) + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithNodeDeserializer(inner => new ValidatingNodeDeserializer(inner), deserializer => deserializer.InsteadOf()) + .WithDuplicateKeyChecking() + .IgnoreFields() + .IgnoreUnmatchedProperties(); + + /// + /// Gets a instance given the specified folder. + /// + /// The folder of the config to look for. + /// The corresponding instance or if not found. + public static ConfigSubsystem Get(string folder) => List.FirstOrDefault(cfg => cfg.Folder == folder) ?? throw new InvalidOperationException(); + + /// + /// Gets a instance given the specified . + /// + /// The of the config to look for. + /// Whether a new config should be generated, if not found. + /// The corresponding instance or if not found. + public static ConfigSubsystem? Get(Type type, bool generateNew = false) + { + ConfigSubsystem? config = ConfigsValue.FirstOrDefault(config => config?.Base?.GetType() == type); + + if (config == null && generateNew) + config = GenerateNew(type); + + return config; + } + + /// + /// Gets a instance given the specified type . + /// + /// The type of the config to look for. + /// Whether a new config should be generated, if not found. + /// The corresponding instance or if not found. + public static ConfigSubsystem? Get(bool generateNew = false) + where T : class => Get(typeof(T), generateNew); + + /// + /// Generates a new config for the specified . + /// + /// The of the config. + /// The generated config. + public static ConfigSubsystem? GenerateNew(Type type) => Load(type, type.GetCustomAttribute()); + + /// + /// Generates a new config of type . + /// + /// The type of the config. + /// The generated config. + public static ConfigSubsystem? GenerateNew() + where T : class => GenerateNew(typeof(T)); + + /// + /// Loads all configs. + /// + /// The assemblies to load the configs from. + public static void LoadAll(IEnumerable toLoad) + { + if (!isLoaded) + { + isLoaded = true; + MainConfigsValue.Clear(); + ConfigsValue.Clear(); + Cache.Clear(); + } + + void LoadFromAssembly(Assembly asm) => + asm.GetTypes() + .Where(t => t.IsClass && !t.IsInterface && !t.IsAbstract) + .ToList() + .ForEach(t => + { + ConfigAttribute attribute = t.GetCustomAttribute(); + if (attribute is null) + return; + + Load(t, attribute); + }); + + if (toLoad is not null && !toLoad.IsEmpty()) + { + toLoad.ForEach(LoadFromAssembly); + return; + } + + LoadFromAssembly(Assembly.GetCallingAssembly()); + } + + /// + /// Loads a config from a . + /// + /// The config type. + /// The config data. + /// The object. + public static ConfigSubsystem? Load(Type type, ConfigAttribute? attribute) + { + try + { + attribute ??= type.GetCustomAttribute(); + if (attribute is null) + return null; + + ConstructorInfo? constructor = type.GetConstructor(Type.EmptyTypes); + object? target = constructor is not null ? + constructor.Invoke(null)! + : Array.Find( + type.GetProperties(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public), + property => property.PropertyType == type)?.GetValue(null)!; + + if (target is null) + { + Log.Error($"{type.FullName} is a valid config, but it cannot be instantiated!" + + $"It either doesn't have a public default constructor without any arguments or a static property of the {type.FullName} type!"); + + return null; + } + + ConfigSubsystem wrapper = new(target); + + if (string.IsNullOrEmpty(wrapper.Folder)) + { + if (string.IsNullOrEmpty(attribute.Folder)) + { + Log.Warn($"The folder of the object of type {target.GetType()} ({wrapper.Name}) has not been set. It's not possible to determine the parent config which it belongs to, hence it won't be read."); + + return null; + } + + wrapper.Folder = attribute.Folder; + } + + if (string.IsNullOrEmpty(wrapper.Name)) + { + if (string.IsNullOrEmpty(attribute.Name)) + { + wrapper.Name = target.GetType().Name; + Log.Warn($"The config's name of the object of type {target.GetType()} has not been set. The object's type name ({target.GetType().Name}) will be used instead."); + } + else + { + wrapper.Name = attribute.Name; + } + } + + ConfigsValue.Add(wrapper); + if (!wrapper.Name!.Contains(".yml")) + wrapper.Name += ".yml"; + + if (wrapper.IsStandAlone) + { + Directory.CreateDirectory(wrapper.Folder); + + Load(wrapper, wrapper.AbsolutePath); + wrapper.data.Add(wrapper); + MainConfigsValue.Add(wrapper); + + return wrapper; + } + + string path = Path.Combine(Paths.Configs, wrapper.Folder); + if (attribute.IsParent) + { + Directory.CreateDirectory(path); + + Load(wrapper, wrapper.AbsolutePath!); + wrapper.data.Add(wrapper); + MainConfigsValue.Add(wrapper); + + Dictionary localCache = new(Cache); + foreach (KeyValuePair elem in localCache) + LoadFromCache(elem.Key); + + return wrapper; + } + + Cache.Add(wrapper, wrapper.AbsolutePath!); + if (!Directory.Exists(path) || MainConfigsValue.All(cfg => cfg.Folder != wrapper.Folder)) + return wrapper; + + LoadFromCache(wrapper); + + if (!ConfigsValue.Contains(wrapper)) + ConfigsValue.Add(wrapper); + + return wrapper; + } + catch (ReflectionTypeLoadException reflectionTypeLoadException) + { + Log.Error($"Error while initializing config {Assembly.GetCallingAssembly().GetName().Name} (at {Assembly.GetCallingAssembly().Location})! {reflectionTypeLoadException}"); + + foreach (Exception? loaderException in reflectionTypeLoadException.LoaderExceptions) + { + Log.Error(loaderException); + } + } + catch (Exception exception) + { + Log.Error($"Error while initializing config {Assembly.GetCallingAssembly().GetName().Name} (at {Assembly.GetCallingAssembly().Location})! {exception}"); + } + + return null; + } + + /// + /// Loads a config from the cached configs. + /// + /// The config to load. + public static void LoadFromCache(ConfigSubsystem config) + { + if (!Cache.TryGetValue(config, out string path)) + return; + + foreach (ConfigSubsystem cfg in MainConfigsValue) + { + if (string.IsNullOrEmpty(cfg.Folder) || cfg.Folder != config.Folder) + continue; + + cfg.data.Add(config); + } + + Load(config, path); + Cache.Remove(config); + } + + /// + /// Loads a config. + /// + /// The config to load. + /// The config's path. + public static void Load(ConfigSubsystem config, string? path = null) + { + path ??= config.AbsolutePath; + if (File.Exists(path)) + { + config.Base = Deserializer.Deserialize(File.ReadAllText(path ?? throw new ArgumentNullException(nameof(path))), config.Base!.GetType())!; + File.WriteAllText(path, Serializer.Serialize(config.Base!)); + return; + } + + File.WriteAllText(path ?? throw new ArgumentNullException(nameof(path)), Serializer.Serialize(config.Base!)); + } + + /// + /// Loads a dynamic configuration for the specified from the given path. + /// This method generates a new type at runtime that mirrors the , + /// including all serializable properties, and applies attributes like + /// and if specified. + /// + /// The for which to generate the dynamic configuration at runtime. + /// The folder from which to load the configuration. + /// The name of the configuration file. + /// A instance for the dynamically generated configuration, or if not found. + public static ConfigSubsystem? LoadDynamic(Type sourceType, string folder, string name) + { + CustomAttributeBuilder configAttribute = + DynamicTypeGenerator.BuildAttribute( + typeof(ConfigAttribute), new object[] { folder, name, false, true }); + + return Get( + sourceType.GenerateDynamicTypeWithConstructorFromExistingType( + sourceType.Name + "Config", new[] { configAttribute }, true, new[] { typeof(YamlIgnoreAttribute) }), true); + } + + /// + /// Gets the path of the specified data object of type . + /// + /// The type of the data to be read. + /// The corresponding data's path or if not found. + public static string? GetPath() + where T : class + { + object? config = Get(); + return config is null || config is not ConfigSubsystem configBase ? null : configBase.AbsolutePath; + } + + /// + public static string Serialize(object? graph) => Serializer.Serialize(graph); + + /// + public static string Serialize(object? graph, Type type) => Serializer.Serialize(graph, type); + + /// + public static object? Deserialize(string input) => Deserializer.Deserialize(input); + + /// + public static object? Deserialize(string input, Type type) => Deserializer.Deserialize(input, type); + + /// + public static T Deserialize(string input) => Deserializer.Deserialize(input); + + /// + /// Converts a dictionary of key-value pairs into a YAML formatted string. + /// + /// The dictionary containing keys and values to be converted to YAML format. + /// A YAML formatted string representing the dictionary contents. + /// + /// The keys in the dictionary must be of type , and the values can be of any object type. + /// This method ensures compatibility with JSON format before converting to YAML. + /// + public static string ConvertDictionaryToYaml(Dictionary dictionary) + { + Dictionary convertedDictionary = dictionary.ToDictionary( + kvp => kvp.Key.ToString(), + kvp => kvp.Value); + + return JsonSerializer.Serialize(convertedDictionary); + } + +#pragma warning disable CS8603 // Possible null reference return. +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + /// + public override TObject Cast() => Base as TObject; + + /// + public override bool Cast(out TObject param) + { + if (Base is not TObject cast) + { + param = default; + return false; + } + + param = cast; + return true; + } +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. +#pragma warning restore CS8603 // Possible null reference return. + + /// + /// Reads a property of type . + /// + /// The type of the property to be read. + /// The name of the property to be read. + /// The corresponding instance or if not found. + public T? Read(string name) + { + PropertyInfo? propertyInfo = Base?.GetType().GetProperty(name); + + if (propertyInfo is null || propertyInfo.GetCustomAttribute() != null) + return default; + + object? value = propertyInfo.GetValue(Base); + + if (value is null && !typeof(T).IsClass && Nullable.GetUnderlyingType(typeof(T)) == null) + return default; + + try + { + return (T)value!; + } + catch (InvalidCastException) + { + Log.Error($"Failed to read the property '{name}' because its type is incompatible with the expected type '{typeof(T).Name}'."); + return default; + } + } + + /// + /// Reads a data object of type . + /// + /// The type of the data to be read. + /// The corresponding instance or if not found. + public T? ReadDataObject() + where T : class => data.FirstOrDefault(data => data.Base!.GetType() == typeof(T)) as T; + + /// + /// Reads a data object of type and retrieves a property value of type . + /// + /// The type of the data object to be read. + /// The type of the property value to be retrieved. + /// The name of the property to be read. + /// The corresponding instance or if not found or if the type is incompatible. + /// + /// This method searches for a data object of type within the data collection, + /// and then attempts to read a property value of type from it. + /// + public TValue? ReadDataObjectValue(string name) + where TSource : class => data.FirstOrDefault(data => data.Base!.GetType() == typeof(TSource)).Read(name); + + /// + /// Updates the value of a specified property in the config of type and writes the updated config to a file. + /// + /// The type of the data to be written. This should be a class type that represents the configuration. + /// The name of the property within the config object whose value is to be modified. + /// The new value to assign to the specified property. + /// Thrown when the file path is null. + /// + /// This method reads the current configuration of type using the method. + /// It then updates the specified property with the new value. The updated configuration is serialized and written to a file. + /// The file path is determined by the method. + /// The method also updates the current instance with the deserialized configuration data from the file. + /// + public void Write(string name, object value) + where T : class + { + T? param = Read(name); + if (param is null) + return; + + string? path = GetPath(); + + PropertyInfo? propertyInfo = param.GetType().GetProperty(name); + if (propertyInfo is null || propertyInfo.GetCustomAttribute() is not null) + return; + + propertyInfo.SetValue(param, value); + + if (path is null) + throw new ArgumentNullException(nameof(path)); + + File.WriteAllText(path, Serializer.Serialize(param)); + this.CopyProperties(Deserializer.Deserialize(File.ReadAllText(path), Base!.GetType())); + } + + /// + /// Writes a new value to a specified property of a data object of type . + /// + /// The type of the data object to be modified. + /// The type of the new value to be written. + /// The name of the property to be modified. + /// The new value to be written to the property. + /// + /// This method searches for a data object of type within the data collection, + /// and then writes the specified value to the property of the data object. + /// After modifying the property, it serializes the data object to a file and updates the current instance + /// with the deserialized properties from the file. + /// + public void WriteDataObjectValue(string name, TValue value) + where TConfig : class + { + ConfigSubsystem? dataObject = data.FirstOrDefault(d => d.Base!.GetType() == typeof(TConfig)); + if (dataObject is null) + return; + + string? path = GetPath(); + + PropertyInfo? propertyInfo = dataObject.GetType().GetProperty(name); + if (propertyInfo is null || propertyInfo.GetCustomAttribute() is not null) + return; + + propertyInfo.SetValue(dataObject, value); + + if (path is null) + throw new ArgumentNullException(nameof(path)); + + File.WriteAllText(path, Serializer.Serialize(dataObject)); + this.CopyProperties(Deserializer.Deserialize(File.ReadAllText(path), Base!.GetType())); + } + } +} \ No newline at end of file diff --git a/Exiled.API/Features/Core/DynamicTypeGenerator.cs b/Exiled.API/Features/Core/DynamicTypeGenerator.cs new file mode 100644 index 0000000000..d0fb051c3e --- /dev/null +++ b/Exiled.API/Features/Core/DynamicTypeGenerator.cs @@ -0,0 +1,416 @@ +// ----------------------------------------------------------------------- +// +// 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.Linq; + using System.Reflection; + using System.Reflection.Emit; + + /// + /// Provides methods for dynamically generating types at runtime. + /// + public static class DynamicTypeGenerator + { + /// + /// Generates a dynamic type with a default constructor based on the provided base type. + /// + /// The base type to derive the new type from. + /// Optional name for the generated type. + /// Optional array containing all type attributes the class must implement to be dynamically generated. + /// Optional value indicating whether only read and write properties should be collected. + /// Optional array containing all type attributes the property must implement to be dynamically generated. + /// The dynamically generated type. + public static Type GenerateDynamicTypeWithConstructorFromExistingType( + this Type baseType, + string typeName = null, + CustomAttributeBuilder[] classAttributes = null, + bool rwOnly = true, + Type[] attributes = null) + { + AssemblyName assemblyName = new($"ExiledDynamic_{Guid.NewGuid()}"); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); + + string fullName = string.IsNullOrEmpty(typeName) ? baseType.Name + "Dynamic" : typeName; + TypeBuilder typeBuilder = moduleBuilder.AddClass(fullName, baseType); + + if (classAttributes is not null && !classAttributes.IsEmpty()) + classAttributes.ForEach(att => typeBuilder.SetCustomAttribute(att)); + + typeBuilder.AddConstructor(); + + void GenerateFieldAndProperty(PropertyInfo property) + { + if ((!rwOnly && property.CanRead && !property.CanWrite) || + (rwOnly && property.CanRead && property.CanWrite)) + { + typeBuilder.AddField(property.Name, property.PropertyType); + typeBuilder.AddProperty(property.Name, property.PropertyType); + } + } + + if (attributes is null || attributes.IsEmpty()) + { + foreach (PropertyInfo property in baseType.GetProperties()) + GenerateFieldAndProperty(property); + } + else + { + foreach (PropertyInfo property in baseType.GetProperties()) + { + if (property.GetCustomAttributes().Any(att => attributes.Contains(att.GetType()))) + continue; + + GenerateFieldAndProperty(property); + } + } + + return typeBuilder.CreateType(); + } + + /// + /// Generates a dynamic type with a constructor, allowing external actions to modify the type before creation. + /// + /// The name of the generated type. + /// The types of the constructor parameters. + /// A list of actions to modify the type before it is created. + /// The dynamically generated type with a constructor. + public static Type GenerateDynamicTypeWithConstructor( + string typeName, + Type[] constructorParams = null, + List> preCreateActions = null) + { + AssemblyName assemblyName = new($"ExiledDynamic_{Guid.NewGuid()}"); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); + + TypeBuilder typeBuilder = moduleBuilder.AddClass(typeName); + + typeBuilder.AddConstructor(constructorParams); + + preCreateActions?.ForEach(action => action(typeBuilder)); + + return typeBuilder.CreateType(); + } + + /// + /// Generates a dynamic type with custom attributes and external actions to modify the type before creation. + /// + /// The name of the generated type. + /// Optional array containing attributes to apply on the generated type. + /// Optional dictionary specifying attributes for each property. + /// A dictionary where the key is the field name and the value is the field type. + /// A list of actions to modify the type before it is created. + /// The dynamically generated type with custom class and property attributes. + public static Type GenerateDynamicTypeWithAttributes( + string typeName, + CustomAttributeBuilder[] classAttributes = null, + Dictionary propertyAttributes = null, + Dictionary fields = null, + List> preCreateActions = null) + { + AssemblyName assemblyName = new($"ExiledDynamic_{Guid.NewGuid()}"); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); + + TypeBuilder typeBuilder = moduleBuilder.AddClass(typeName); + + if (classAttributes != null && classAttributes.Length > 0) + { + foreach (CustomAttributeBuilder att in classAttributes) + typeBuilder.SetCustomAttribute(att); + } + + foreach (KeyValuePair field in fields) + { + typeBuilder.AddField(field.Key, field.Value); + typeBuilder.AddProperty(field.Key, field.Value); + + if (propertyAttributes != null && propertyAttributes.TryGetValue(field.Key, out CustomAttributeBuilder[] attrs)) + { + foreach (CustomAttributeBuilder attr in attrs) + typeBuilder.SetCustomAttribute(attr); + } + } + + typeBuilder.AddConstructor(); + + preCreateActions?.ForEach(action => action(typeBuilder)); + + return typeBuilder.CreateType(); + } + + /// + /// Adds a new class to the module builder. + /// + /// The module builder to add the class to. + /// The name of the new class. + /// Optional base type for the new class. + /// The newly created type builder. + public static TypeBuilder AddClass(this ModuleBuilder moduleBuilder, string className, Type parentType = null) => + moduleBuilder.DefineType( + className, + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout, + parentType ?? typeof(object)); + + /// + /// Adds a new class to the type builder. + /// + /// The type builder to add the class to. + /// The name of the new class. + /// Optional base type for the new class. + /// The newly created type builder. + public static TypeBuilder AddClass(this TypeBuilder typeBuilder, string className, Type parentType = null) => + ((ModuleBuilder)typeBuilder.Module).AddClass(className, parentType); + + /// + /// Adds a new struct to the type builder. + /// + /// The type builder to add the struct to. + /// The name of the new struct. + /// The newly created type builder. + public static TypeBuilder AddStruct(this TypeBuilder typeBuilder, string structName) + { + return ((ModuleBuilder)typeBuilder.Module).DefineType( + structName, + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SequentialLayout | TypeAttributes.BeforeFieldInit, + typeof(ValueType)); + } + + /// + /// Adds a new field to the type builder. + /// + /// The type builder to add the field to. + /// The name of the new field. + /// The type of the new field. + /// The newly created field builder. + public static FieldBuilder AddField(this TypeBuilder typeBuilder, string fieldName, Type fieldType) => + typeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Public); + + /// + /// Adds a new method to the type builder with custom IL generation. + /// + /// The type builder to add the method to. + /// The name of the new method. + /// The return type of the method. + /// The types of the method parameters. + /// Action to generate the IL for the method. + /// The newly created method builder. + public static MethodBuilder AddMethod( + this TypeBuilder typeBuilder, + string methodName, + Type returnType, + Type[] parameterTypes, + Action ilGeneratorAction) + { + MethodBuilder methodBuilder = typeBuilder.DefineMethod( + methodName, + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, + returnType, + parameterTypes); + + ILGenerator ilGenerator = methodBuilder.GetILGenerator(); + ilGeneratorAction(ilGenerator); + ilGenerator.Emit(OpCodes.Ret); + + return methodBuilder; + } + + /// + /// Adds a new field to the type builder with custom field attributes. + /// + /// The type builder to add the field to. + /// The name of the new field. + /// The type of the new field. + /// Custom attributes for the field. + /// The newly created field builder. + public static FieldBuilder AddField(this TypeBuilder typeBuilder, string fieldName, Type fieldType, FieldAttributes fieldAttributes = FieldAttributes.Public) => + typeBuilder.DefineField(fieldName, fieldType, fieldAttributes); + + /// + /// Adds a new property to the type builder. + /// + /// The type builder to add the property to. + /// The name of the new property. + /// The type of the new property. + public static void AddProperty(this TypeBuilder typeBuilder, string propertyName, Type propertyType) + { + FieldBuilder fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); + PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); + + const MethodAttributes methodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; + + MethodBuilder getterMethod = typeBuilder.DefineMethod("get_" + propertyName, methodAttributes, propertyType, Type.EmptyTypes); + ILGenerator getterIL = getterMethod.GetILGenerator(); + getterIL.Emit(OpCodes.Ldarg_0); + getterIL.Emit(OpCodes.Ldfld, fieldBuilder); + getterIL.Emit(OpCodes.Ret); + propertyBuilder.SetGetMethod(getterMethod); + + MethodBuilder setterMethod = typeBuilder.DefineMethod("set_" + propertyName, methodAttributes, null, new[] { propertyType }); + ILGenerator setterIL = setterMethod.GetILGenerator(); + setterIL.Emit(OpCodes.Ldarg_0); + setterIL.Emit(OpCodes.Ldarg_1); + setterIL.Emit(OpCodes.Stfld, fieldBuilder); + setterIL.Emit(OpCodes.Ret); + propertyBuilder.SetSetMethod(setterMethod); + } + + /// + /// Adds a default constructor to the type builder. + /// + /// The type builder to add the constructor to. + /// The types of the constructor parameters. + public static void AddConstructor(this TypeBuilder typeBuilder, Type[] parameterTypes = null) + { + ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + Type.EmptyTypes); + + ILGenerator constructorIL = constructorBuilder.GetILGenerator(); + + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); + + if (parameterTypes is not null && !parameterTypes.IsEmpty()) + { + for (int i = 0; i < parameterTypes.Length; i++) + { + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg, i + 1); + FieldInfo field = typeBuilder.DefineField($"_param{i}", parameterTypes[i], FieldAttributes.Private); + constructorIL.Emit(OpCodes.Stfld, field); + } + } + + constructorIL.Emit(OpCodes.Ret); + } + + /// + /// Adds a new enum to the module builder. + /// + /// The module builder to add the enum to. + /// The name of the new enum. + /// The underlying type of the enum. + /// The newly created type builder for the enum. + /// Thrown when the underlying type is not valid. + public static TypeBuilder AddEnum(this ModuleBuilder moduleBuilder, string enumName, Type underlyingType) + { + if (!underlyingType.IsEnum) + throw new ArgumentException("Underlying type must be an enum."); + + if (underlyingType != typeof(byte) && + underlyingType != typeof(sbyte) && + underlyingType != typeof(short) && + underlyingType != typeof(ushort) && + underlyingType != typeof(int) && + underlyingType != typeof(uint) && + underlyingType != typeof(long) && + underlyingType != typeof(ulong)) + { + throw new ArgumentException("Underlying type must be one of the valid enum underlying types."); + } + + TypeBuilder typeBuilder = moduleBuilder.DefineType( + enumName, + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.SpecialName | TypeAttributes.Serializable, + typeof(Enum)); + + typeBuilder.DefineField( + "value__", + underlyingType, + FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName); + + return typeBuilder; + } + + /// + /// Adds a new enum to the type builder. + /// + /// The type builder to add the enum to. + /// The name of the new enum. + /// The underlying type of the enum. + /// The newly created type builder for the enum. + /// Thrown when the underlying type is not valid. + public static TypeBuilder AddEnum(this TypeBuilder typeBuilder, string enumName, Type underlyingType) => + ((ModuleBuilder)typeBuilder.Module).AddEnum(enumName, underlyingType); + + /// + /// Adds a custom attribute to the specified field. + /// + /// The FieldBuilder to add the attribute to. + /// The type of the attribute to add. + /// Optional. Arguments to pass to the attribute's constructor. + /// Optional. Named property values to set on the attribute. + /// Thrown when a matching constructor for the attribute is not found. + public static void AddAttribute( + this FieldBuilder fieldBuilder, + Type attributeType, + object[] constructorArguments = null, + (string Name, object Value)[] namedArguments = null) => + fieldBuilder.SetCustomAttribute(BuildAttribute(attributeType, constructorArguments, namedArguments)); + + /// + /// Adds a custom attribute to the specified property. + /// + /// The PropertyBuilder to add the attribute to. + /// The type of the attribute to add. + /// Optional. Arguments to pass to the attribute's constructor. + /// Optional. Named property values to set on the attribute. + /// Thrown when a matching constructor for the attribute is not found. + public static void AddAttribute( + this PropertyBuilder propertyBuilder, + Type attributeType, + object[] constructorArguments = null, + (string Name, object Value)[] namedArguments = null) => + propertyBuilder.SetCustomAttribute(BuildAttribute(attributeType, constructorArguments, namedArguments)); + + /// + /// Adds a custom attribute to the specified method. + /// + /// The MethodBuilder to add the attribute to. + /// The type of the attribute to add. + /// Optional. Arguments to pass to the attribute's constructor. + /// Optional. Named property values to set on the attribute. + /// Thrown when a matching constructor for the attribute is not found. + public static void AddAttribute( + this MethodBuilder methodBuilder, + Type attributeType, + object[] constructorArguments = null, + (string Name, object Value)[] namedArguments = null) => + methodBuilder.SetCustomAttribute(BuildAttribute(attributeType, constructorArguments, namedArguments)); + + /// + /// Builds a custom attribute for the specified using the provided constructor and named arguments. + /// + /// The of the attribute to build. + /// Optional constructor arguments to be passed to the attribute constructor. + /// Optional named arguments to set as properties on the attribute. + /// A instance representing the custom attribute. + /// Thrown when a matching constructor cannot be found for the attribute type. + public static CustomAttributeBuilder BuildAttribute( + Type attributeType, + object[] constructorArguments = null, + (string Name, object Value)[] namedArguments = null) + { + ConstructorInfo constructorInfo = attributeType.GetConstructor(constructorArguments?.Select(a => a.GetType()).ToArray()); + + if (constructorInfo == null) + throw new ArgumentException("Cannot find a matching constructor for the attribute."); + + return new( + constructorInfo, + constructorArguments ?? Array.Empty(), + namedArguments?.Select(na => attributeType.GetProperty(na.Name)).ToArray() ?? Array.Empty(), + namedArguments?.Select(na => na.Value).ToArray() ?? Array.Empty()); + } + } +} \ No newline at end of file diff --git a/Exiled.API/Features/Core/EActor.cs b/Exiled.API/Features/Core/EActor.cs index 7d8b7fe023..e834aa10f9 100644 --- a/Exiled.API/Features/Core/EActor.cs +++ b/Exiled.API/Features/Core/EActor.cs @@ -43,7 +43,6 @@ public abstract class EActor : EObject, IEntity, IWorldSpace protected EActor() : base() { - IsEditable = true; fixedTickRate = DEFAULT_FIXED_TICK_RATE; } @@ -164,6 +163,7 @@ public T AddComponent(string name = "") return null; componentsInChildren.Add(component); + component.OnAdded(); return component.Cast(out T param) ? param : throw new InvalidCastException("The provided EActor cannot be cast to the specified type."); } @@ -177,6 +177,7 @@ public T AddComponent(Type type, string name = "") return null; componentsInChildren.Add(component); + component.OnAdded(); return component.Cast(out T param) ? param : throw new InvalidCastException("The provided EActor cannot be cast to the specified type."); } @@ -193,6 +194,7 @@ public T AddComponent(EActor actor, string name = "") AttachTo(Base, actor); componentsInChildren.Add(actor); + actor.OnAdded(); return actor.Cast(out T param) ? param : throw new InvalidCastException("The provided EActor cannot be cast to the specified type."); } @@ -205,6 +207,8 @@ public EActor AddComponent(Type type, string name = "") return null; componentsInChildren.Add(component); + component.OnAdded(); + return component; } @@ -219,6 +223,7 @@ public EActor AddComponent(EActor actor, string name = "") AttachTo(Base, actor); componentsInChildren.Add(actor); + actor.OnAdded(); return actor; } @@ -257,68 +262,61 @@ public IEnumerable AddComponents(IEnumerable types) public T RemoveComponent(string name = "") where T : EActor { - T comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(out comp)) + if (!TryGetComponent(out T comp)) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } - foreach (EActor actor in GetComponents()) + foreach (T actor in GetComponents()) { if (actor.Name != name) continue; - comp = actor.Cast(); + RemoveComponent_Internal(actor); + return actor; } - return comp; + return default; } /// public T RemoveComponent(EActor actor, string name = "") where T : EActor { - T comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(out comp) || comp != actor) + if (!TryGetComponent(out T comp) || comp != actor) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } - foreach (EActor component in GetComponents()) + foreach (T component in GetComponents()) { if (component.Name != name && component == actor) continue; - comp = component.Cast(); + RemoveComponent_Internal(component); + return component; } - return comp; + return default; } /// public EActor RemoveComponent(Type type, string name = "") { - EActor comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(type, out comp)) + if (!TryGetComponent(type, out EActor comp)) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } @@ -327,10 +325,11 @@ public EActor RemoveComponent(Type type, string name = "") if (actor.Name != name) continue; - comp = actor; + RemoveComponent_Internal(actor); + return actor; } - return comp; + return default; } /// @@ -341,8 +340,7 @@ public EActor RemoveComponent(EActor actor, string name = "") if (string.IsNullOrEmpty(name)) { - actor.Base = null; - componentsInChildren.Remove(actor); + RemoveComponent_Internal(actor); return actor; } @@ -351,10 +349,11 @@ public EActor RemoveComponent(EActor actor, string name = "") if (component != actor || actor.Name != name) continue; - actor = component; + RemoveComponent_Internal(actor); + return actor; } - return actor; + return default; } /// @@ -469,6 +468,26 @@ public void ComponentInitialize() Timing.CallDelayed(fixedTickRate * 2, () => serverTick = Timing.RunCoroutine(ServerTick())); } + /// + internal void OnAdded_Internal() => OnAdded(); + + /// + internal void OnRemoved_Internal() => OnRemoved(); + + /// + /// Called when the is added to an entity. + /// + protected virtual void OnAdded() + { + } + + /// + /// Called when the is removed from an entity. + /// + protected virtual void OnRemoved() + { + } + /// /// Fired after the instance is created. /// @@ -523,6 +542,9 @@ protected override void OnBeginDestroy() { base.OnBeginDestroy(); + foreach (EActor component in ComponentsInChildren) + component.Destroy(); + HashSetPool.Pool.Return(componentsInChildren); Timing.KillCoroutines(serverTick); @@ -538,5 +560,12 @@ private IEnumerator ServerTick() Tick(); } } + + private void RemoveComponent_Internal(EActor actor) + { + actor.Base = null; + componentsInChildren.Remove(actor); + actor.OnRemoved(); + } } } \ No newline at end of file diff --git a/Exiled.API/Features/Core/EObject.cs b/Exiled.API/Features/Core/EObject.cs index a0af7ac800..eba898fb4d 100644 --- a/Exiled.API/Features/Core/EObject.cs +++ b/Exiled.API/Features/Core/EObject.cs @@ -11,6 +11,7 @@ namespace Exiled.API.Features.Core using System.Collections.Generic; using System.Linq; using System.Reflection; + using System.Threading; using Exiled.API.Extensions; using Exiled.API.Features.Core.Attributes; @@ -24,6 +25,8 @@ namespace Exiled.API.Features.Core public abstract class EObject : TypeCastObject { private static readonly Dictionary> RegisteredTypesValue = new(); + private static int lastInstanceId; + private int instanceId; private bool destroyedValue; private bool searchForHostObjectIfNull; private CoroutineHandle addHostObjectInternalHandle; @@ -36,6 +39,7 @@ protected EObject() { IsEditable = true; InternalObjects.Add(this); + instanceId = Interlocked.Increment(ref lastInstanceId); } /// @@ -357,20 +361,17 @@ public static Type GetObjectTypeFromRegisteredTypes(Type type, string name) public static EObject CreateDefaultSubobject(Type type, params object[] parameters) { const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; - EObject @object = Activator.CreateInstance(type, flags, null, null, null) as EObject; - - if (@object is not null && Player.DEFAULT_ROLE_BEHAVIOUR is not null && type.BaseType == Player.DEFAULT_ROLE_BEHAVIOUR) - { - @object.Base = parameters[0] as GameObject; - @object.Cast().ComponentInitialize(); - } // Do not use implicit bool conversion as @object may be null - if (@object != null) + if (Activator.CreateInstance(type, flags, null, null, null) is EObject @object) { if (type.GetCustomAttribute() is not null && GetObjectTypeFromRegisteredTypes(type) == null) RegisterObjectType(type, type.Name); + if (parameters is not null && parameters.Any() && parameters[0] is GameObject go) + @object.Base = go; + + @object.Cast().ComponentInitialize(); return @object; } @@ -865,10 +866,9 @@ public override int GetHashCode() { unchecked { - int hash = 23; - hash = (hash * 29) + Base.GetHashCode(); - hash = (hash * 29) + Name.GetHashCode(); - hash = (hash * 29) + Tag.GetHashCode(); + int hash = 17; + hash = (hash * 23) + instanceId.GetHashCode(); + hash = (hash * 23) + (Name?.GetHashCode() ?? 0); return hash; } } diff --git a/Exiled.API/Features/Core/GameEntity.cs b/Exiled.API/Features/Core/GameEntity.cs index aa84602d36..e4a51679cd 100644 --- a/Exiled.API/Features/Core/GameEntity.cs +++ b/Exiled.API/Features/Core/GameEntity.cs @@ -40,14 +40,16 @@ public abstract class GameEntity : TypeCastObject, IEntity, IWorldSp protected GameEntity(GameObject gameObject) { GameObject = gameObject; - - // List.Add(this); } /// /// Finalizes an instance of the class. /// - ~GameEntity() => List.Remove(this); + ~GameEntity() + { + foreach (EActor component in ComponentsInChildren) + component.Destroy(); + } /// /// Gets all active instances. @@ -156,6 +158,7 @@ public T AddComponent(string name = "") return null; componentsInChildren.Add(component); + return component; } @@ -168,6 +171,7 @@ public EActor AddComponent(Type type, string name = "") return null; componentsInChildren.Add(component); + return component; } @@ -178,11 +182,15 @@ public T AddComponent(EActor actor, string name = "") if (!actor) throw new NullReferenceException("The provided EActor is null."); + if (componentsInChildren.Contains(actor)) + return actor.Cast(); + if (!string.IsNullOrEmpty(name)) actor.Name = name; EActor.AttachTo(GameObject, actor); componentsInChildren.Add(actor); + actor.OnAdded_Internal(); return actor.Cast(out T param) ? param : throw new InvalidCastException("The provided EActor cannot be cast to the specified type."); } @@ -206,11 +214,15 @@ public EActor AddComponent(EActor actor, string name = "") if (!actor) throw new NullReferenceException("The provided EActor is null."); + if (componentsInChildren.Contains(actor)) + return actor; + if (!string.IsNullOrEmpty(name)) actor.Name = name; EActor.AttachTo(GameObject, actor); componentsInChildren.Add(actor); + actor.OnAdded_Internal(); return actor; } @@ -245,19 +257,16 @@ public IEnumerable AddComponents(IEnumerable types) yield return AddComponent(type); } - /// + /// public T RemoveComponent(string name = "") where T : EActor { - T comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(out comp)) + if (!TryGetComponent(out T comp)) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } @@ -266,25 +275,23 @@ public T RemoveComponent(string name = "") if (actor.Name != name) continue; - comp = actor.Cast(); + RemoveComponent_Internal(actor); + return actor; } - return comp; + return default; } /// public T RemoveComponent(EActor actor, string name = "") where T : EActor { - T comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(out comp) || comp != actor) + if (!TryGetComponent(out T comp) || comp != actor) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } @@ -293,24 +300,22 @@ public T RemoveComponent(EActor actor, string name = "") if (component.Name != name && component == actor) continue; - comp = component.Cast(); + RemoveComponent_Internal(component); + return component; } - return comp; + return default; } /// public EActor RemoveComponent(Type type, string name = "") { - EActor comp = null; - if (string.IsNullOrEmpty(name)) { - if (!TryGetComponent(type, out comp)) + if (!TryGetComponent(type, out EActor comp)) return null; - comp.Base = null; - componentsInChildren.Remove(comp); + RemoveComponent_Internal(comp); return comp; } @@ -319,10 +324,11 @@ public EActor RemoveComponent(Type type, string name = "") if (actor.Name != name) continue; - comp = actor; + RemoveComponent_Internal(actor); + return actor; } - return comp; + return default; } /// @@ -333,8 +339,7 @@ public EActor RemoveComponent(EActor actor, string name = "") if (string.IsNullOrEmpty(name)) { - actor.Base = null; - componentsInChildren.Remove(actor); + RemoveComponent_Internal(actor); return actor; } @@ -343,7 +348,7 @@ public EActor RemoveComponent(EActor actor, string name = "") if (component != actor || actor.Name != name) continue; - actor = component; + RemoveComponent_Internal(actor); } return actor; @@ -376,15 +381,19 @@ public IEnumerable RemoveComponentOfType(Type type, string name = "") public void RemoveComponents(IEnumerable types) => types.ForEach(type => RemoveComponent(type)); /// - public void RemoveComponents(IEnumerable actors) => actors.ForEach(actor => RemoveComponent(actor)); + public void RemoveComponents(IEnumerable actors) => actors.ToList().ForEach(actor => RemoveComponent(actor)); /// public void RemoveComponents(IEnumerable actors) - where T : EActor => actors.ForEach(actor => RemoveComponent(actor)); + where T : EActor => actors.ToList().ForEach(actor => RemoveComponent(actor)); /// - public void RemoveComponents(IEnumerable types) - where T : EActor => types.ForEach(type => RemoveComponent(type)); + public void RemoveComponents(IEnumerable actors) + where T : EActor => actors.ToList().ForEach(actor => + { + if (actor.GetType() == typeof(T)) + RemoveComponent(actor); + }); /// public T GetComponent() @@ -426,5 +435,12 @@ public bool HasComponent(bool depthInheritance = false) => depthInheritance public bool HasComponent(Type type, bool depthInheritance = false) => depthInheritance ? componentsInChildren.Any(type.IsInstanceOfType) : componentsInChildren.Any(comp => type == comp.GetType()); + + private void RemoveComponent_Internal(EActor actor) + { + actor.Base = null; + componentsInChildren.Remove(actor); + actor.OnRemoved_Internal(); + } } } \ No newline at end of file diff --git a/Exiled.API/Features/Core/Generic/EBehaviour.cs b/Exiled.API/Features/Core/Generic/EBehaviour.cs index a2b3fd611e..8b0cc346b1 100644 --- a/Exiled.API/Features/Core/Generic/EBehaviour.cs +++ b/Exiled.API/Features/Core/Generic/EBehaviour.cs @@ -78,7 +78,7 @@ protected override void PostInitialize() FindOwner(); - if (!Owner && DisposeOnNullOwner) + if (Owner is null && DisposeOnNullOwner) { Destroy(); return; @@ -112,6 +112,15 @@ protected override void OnEndPlay() return; } + /// + protected override void OnAdded() + { + base.OnAdded(); + + if (Owner is null) + FindOwner(); + } + /// /// Checks if the specified owner is not null and matches the stored owner. /// @@ -121,6 +130,6 @@ protected override void OnEndPlay() /// This method verifies if the provided owner is not null and matches the stored owner. ///
It is typically used to ensure that the owner being checked is valid and corresponds to the expected owner for the current context. /// - protected virtual bool Check(T owner) => owner && Owner == owner; + protected virtual bool Check(T owner) => owner is not null && Owner is not null && Owner == owner; } } \ No newline at end of file diff --git a/Exiled.API/Features/Core/Generic/RepNotify.cs b/Exiled.API/Features/Core/Generic/RepNotify.cs index 65c1e64d4f..e06a7e3623 100644 --- a/Exiled.API/Features/Core/Generic/RepNotify.cs +++ b/Exiled.API/Features/Core/Generic/RepNotify.cs @@ -38,7 +38,7 @@ public RepNotify() /// Gets or sets the which handles all the delegates fired before replicating the . ///
[DynamicEventDispatcher] - public TDynamicEventDispatcher> ReplicatingReferenceDispatcher { get; set; } + public TDynamicEventDispatcher> ReplicatingReferenceDispatcher { get; set; } = new(); /// public virtual bool Replicates { get; set; } diff --git a/Exiled.API/Features/Core/Generic/StaticActor.cs b/Exiled.API/Features/Core/Generic/StaticActor.cs index 36c25bf629..537649dce8 100644 --- a/Exiled.API/Features/Core/Generic/StaticActor.cs +++ b/Exiled.API/Features/Core/Generic/StaticActor.cs @@ -28,6 +28,10 @@ namespace Exiled.API.Features.Core.Generic public abstract class StaticActor : EActor where T : EActor { +#pragma warning disable SA1309 + private static T __fastCall; +#pragma warning restore SA1309 + /// /// Gets a value indicating whether the method has already been called by Unity. /// @@ -49,6 +53,9 @@ public abstract class StaticActor : EActor /// The existing instance, or if not found. public static T FindExistingInstance() { + if (__fastCall) + return __fastCall; + T[] existingInstances = FindActiveObjectsOfType(); return existingInstances?.Length == 0 ? null : existingInstances[0]; } @@ -62,6 +69,7 @@ public static T CreateNewInstance() EObject @object = CreateDefaultSubobject(); @object.Name = "__" + typeof(T).Name + " (StaticActor)"; @object.SearchForHostObjectIfNull = true; + __fastCall = @object.Cast(); return @object.Cast(); } @@ -86,7 +94,6 @@ protected override void PostInitialize() if (!IsInitialized) { - Log.Debug($"Start() StaticActor with type {GetType().Name} in the Actor {Name}"); PostInitialize_Static(); IsInitialized = true; } @@ -100,6 +107,7 @@ protected override void OnBeginPlay() if (IsStarted) return; + SubscribeEvents(); BeginPlay_Static(); IsStarted = true; } @@ -108,6 +116,7 @@ protected override void OnBeginPlay() protected override void OnEndPlay() { IsDestroyed = true; + UnsubscribeEvents(); EndPlay_Static(); } diff --git a/Exiled.API/Features/Core/StateMachine/StateController.cs b/Exiled.API/Features/Core/StateMachine/StateController.cs index 7f490eb0dd..8aa98d4b2c 100644 --- a/Exiled.API/Features/Core/StateMachine/StateController.cs +++ b/Exiled.API/Features/Core/StateMachine/StateController.cs @@ -53,13 +53,13 @@ public State CurrentState /// Gets or sets the which handles all the delegates fired when entering a new state. ///
[DynamicEventDispatcher] - public TDynamicEventDispatcher BeginStateMulticastDispatcher { get; set; } + public TDynamicEventDispatcher BeginStateMulticastDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired when exiting the current state. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher EndStateMulticastDispatcher { get; set; } + public TDynamicEventDispatcher EndStateMulticastDispatcher { get; set; } = new(); /// /// Fired every tick from the current state. diff --git a/Exiled.API/Features/Core/StaticActor.cs b/Exiled.API/Features/Core/StaticActor.cs index 501b10b4fa..005add990a 100644 --- a/Exiled.API/Features/Core/StaticActor.cs +++ b/Exiled.API/Features/Core/StaticActor.cs @@ -8,8 +8,10 @@ namespace Exiled.API.Features.Core { using System; + using System.Collections.Generic; using Exiled.API.Features; + using Exiled.API.Features.Core.Generic; /// /// This is a generic Singleton implementation for components. @@ -27,6 +29,10 @@ namespace Exiled.API.Features.Core /// public abstract class StaticActor : EActor { +#pragma warning disable SA1309 + private static readonly Dictionary __fastCall = new(); +#pragma warning restore SA1309 + /// /// Gets a value indicating whether the method has already been called by Unity. /// @@ -54,11 +60,13 @@ public static StaticActor CreateNewInstance() /// The created or already existing instance. public static StaticActor CreateNewInstance(Type type) { - EActor @object = CreateDefaultSubobject(type); + EObject @object = CreateDefaultSubobject(type); @object.Name = "__" + type.Name + " (StaticActor)"; @object.SearchForHostObjectIfNull = true; - @object.ComponentInitialize(); - return @object.Cast(); + StaticActor actor = @object.Cast(); + actor.ComponentInitialize(); + __fastCall[type] = actor; + return actor; } /// @@ -69,11 +77,15 @@ public static StaticActor CreateNewInstance(Type type) public static T Get() where T : StaticActor, new() { + if (__fastCall.TryGetValue(typeof(T), out StaticActor staticActor)) + return staticActor.Cast(); + foreach (StaticActor actor in FindActiveObjectsOfType()) { - if (!actor.Cast(out StaticActor staticActor) || staticActor.GetType() != typeof(T)) + if (actor.GetType() != typeof(T)) continue; + __fastCall[typeof(T)] = actor.Cast(); return actor.Cast(); } @@ -89,11 +101,15 @@ public static T Get() public static T Get(Type type) where T : StaticActor { + if (__fastCall.TryGetValue(typeof(T), out StaticActor staticActor)) + return staticActor.Cast(); + foreach (StaticActor actor in FindActiveObjectsOfType()) { - if (!actor.Cast(out StaticActor staticActor) || staticActor.GetType() != type) + if (actor.GetType() != type) continue; + __fastCall[typeof(T)] = actor.Cast(); return actor.Cast(); } @@ -107,11 +123,15 @@ public static T Get(Type type) /// The corresponding . public static StaticActor Get(Type type) { + if (__fastCall.TryGetValue(type, out StaticActor staticActor)) + return staticActor; + foreach (StaticActor actor in FindActiveObjectsOfType()) { - if (!actor.Cast(out StaticActor staticActor) || staticActor.GetType() != type) + if (actor.GetType() != type) continue; + __fastCall[type] = actor; return actor; } @@ -133,7 +153,6 @@ protected override void PostInitialize() if (IsInitialized) return; - Log.Debug($"Start() StaticActor with type {GetType().Name} in the Actor {Name}"); PostInitialize_Static(); IsInitialized = true; } diff --git a/Exiled.API/Features/Core/TypeCastObject.cs b/Exiled.API/Features/Core/TypeCastObject.cs index 9ede587fcc..66522d17a9 100644 --- a/Exiled.API/Features/Core/TypeCastObject.cs +++ b/Exiled.API/Features/Core/TypeCastObject.cs @@ -24,11 +24,11 @@ protected TypeCastObject() } /// - public TObject Cast() + public virtual TObject Cast() where TObject : class, T => this as TObject; /// - public bool Cast(out TObject param) + public virtual bool Cast(out TObject param) where TObject : class, T { if (this is not TObject cast) diff --git a/Exiled.API/Features/EConfig.cs b/Exiled.API/Features/EConfig.cs deleted file mode 100644 index 873abb6c11..0000000000 --- a/Exiled.API/Features/EConfig.cs +++ /dev/null @@ -1,370 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.API.Features -{ -#nullable enable - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - - using Exiled.API.Extensions; - using Exiled.API.Features.Attributes; - using Exiled.API.Features.Core; - using Exiled.API.Features.Serialization; - using Exiled.API.Features.Serialization.CustomConverters; - using YamlDotNet.Serialization; - using YamlDotNet.Serialization.NodeDeserializers; - - using ColorConverter = Serialization.CustomConverters.ColorConverter; - using UnderscoredNamingConvention = Serialization.UnderscoredNamingConvention; - - /// - /// The base class that handles the config subsystem. - /// - public sealed class EConfig : TypeCastObject - { - /// - internal static readonly List ConfigsValue = new(); - - private static readonly Dictionary Cache = new(); - private static readonly List MainConfigsValue = new(); - - private readonly HashSet data = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The config object. - public EConfig(object obj) => Base = obj; - - /// - /// Gets or sets the serializer for configs and translations. - /// - public static ISerializer Serializer { get; set; } = new SerializerBuilder() - .WithTypeConverter(new VectorsConverter()) - .WithTypeConverter(new ColorConverter()) - .WithTypeConverter(new AttachmentIdentifiersConverter()) - .WithTypeConverter(new EnumClassConverter()) - .WithTypeConverter(new PrivateConstructorConverter()) - .WithEventEmitter(eventEmitter => new TypeAssigningEventEmitter(eventEmitter)) - .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) - .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreFields() - .DisableAliases() - .Build(); - - /// - /// Gets or sets the deserializer for configs and translations. - /// - public static IDeserializer Deserializer { get; set; } = new DeserializerBuilder() - .WithTypeConverter(new VectorsConverter()) - .WithTypeConverter(new ColorConverter()) - .WithTypeConverter(new AttachmentIdentifiersConverter()) - .WithTypeConverter(new EnumClassConverter()) - .WithTypeConverter(new PrivateConstructorConverter()) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithNodeDeserializer(inner => new ValidatingNodeDeserializer(inner), deserializer => deserializer.InsteadOf()) - .WithDuplicateKeyChecking() - .IgnoreFields() - .IgnoreUnmatchedProperties() - .Build(); - - /// - /// Gets a containing all the . - /// - public static IReadOnlyCollection List => ConfigsValue; - - /// - /// Gets a containing all the subconfigs. - /// - public IEnumerable Subconfigs => data; - - /// - /// Gets the base config instance. - /// - public object? Base { get; private set; } - - /// - /// Gets or sets the config's folder. - /// - public string? Folder { get; set; } - - /// - /// Gets or sets the config's name. - /// - public string? Name { get; set; } - - /// - /// Gets the absolute path. - /// - public string? AbsolutePath => Folder is not null && Name is not null ? Path.Combine(Paths.Configs, Path.Combine(Folder, Name)) : null; - - /// - /// Gets a instance given the specified type . - /// - /// The type of the config to look for. - /// Whether a new config should be generated, if not found. - /// The corresponding instance or if not found. - public static EConfig? Get(bool generateNew = false) - where T : class - { - EConfig? config = ConfigsValue.FirstOrDefault(config => config?.Base?.GetType() == typeof(T)); - - if (!config && generateNew) - config = GenerateNew(); - - return config; - } - - /// - /// Gets a instance given the specified folder. - /// - /// The folder of the config to look for. - /// The corresponding instance or if not found. - public static EConfig Get(string folder) => List.FirstOrDefault(cfg => cfg.Folder == folder) ?? throw new InvalidOperationException(); - - /// - /// Generates a new config of type . - /// - /// The type of the config. - /// The generated config. - public static EConfig? GenerateNew() - where T : class - { - EConfig? config = Get(); - if (config is not null) - return config; - - return Load(typeof(T), typeof(T).GetCustomAttribute()); - } - - /// - /// Loads all configs. - /// - public static void LoadAll() - { - MainConfigsValue.Clear(); - ConfigsValue.Clear(); - Cache.Clear(); - - Assembly.GetCallingAssembly() - .GetTypes() - .Where(t => t.IsClass && !t.IsInterface && !t.IsAbstract) - .ToList() - .ForEach(t => Load(t)); - } - - /// - /// Loads a config from a . - /// - /// The config type. - /// The config data. - /// The object. - public static EConfig? Load(Type type, ConfigAttribute? attribute = null) - { - try - { - attribute ??= type.GetCustomAttribute(); - if (attribute is null) - return null; - - ConstructorInfo? constructor = type.GetConstructor(Type.EmptyTypes); - object? config = constructor is not null ? - constructor.Invoke(null)! - : Array.Find( - type.GetProperties(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public), - property => property.PropertyType == type)?.GetValue(null)!; - - if (config is null) - { - Log.Error($"{type.FullName} is a valid config, but it cannot be instantiated!" + - $"It either doesn't have a public default constructor without any arguments or a static property of the {type.FullName} type!"); - - return null; - } - - EConfig wrapper = new(config); - if (string.IsNullOrEmpty(wrapper.Folder)) - { - if (string.IsNullOrEmpty(attribute.Folder)) - { - Log.Warn($"The folder of the object of type {config.GetType()} ({wrapper.Name}) has not been set. It's not possible to determine the parent config which it belongs to, hence it won't be read."); - - return null; - } - - wrapper.Folder = attribute.Folder; - } - - if (string.IsNullOrEmpty(wrapper.Name)) - { - if (string.IsNullOrEmpty(attribute.Name)) - { - wrapper.Name = config.GetType().Name; - Log.Warn($"The config's name of the object of type {config.GetType()} has not been set. The object's type name ({config.GetType().Name}) will be used instead."); - } - else - { - wrapper.Name = attribute.Name; - } - } - - ConfigsValue.Add(wrapper); - if (!wrapper.Name!.Contains(".yml")) - wrapper.Name += ".yml"; - - if (wrapper.Folder is not null) - { - string path = Path.Combine(Paths.Configs, wrapper.Folder); - if (attribute.IsParent) - { - if (!Directory.Exists(Path.Combine(path))) - Directory.CreateDirectory(path); - - Load(wrapper, wrapper.AbsolutePath!); - wrapper.data.Add(wrapper); - MainConfigsValue.Add(wrapper); - - Dictionary localCache = new(Cache); - foreach (KeyValuePair elem in localCache) - LoadFromCache(elem.Key); - - return wrapper; - } - - Cache.Add(wrapper, wrapper.AbsolutePath!); - if (!Directory.Exists(path) || MainConfigsValue.All(cfg => cfg.Folder != wrapper.Folder)) - return wrapper; - } - - LoadFromCache(wrapper); - - if (!ConfigsValue.Contains(wrapper)) - ConfigsValue.Add(wrapper); - - return wrapper; - } - catch (ReflectionTypeLoadException reflectionTypeLoadException) - { - Log.Error($"Error while initializing config {Assembly.GetCallingAssembly().GetName().Name} (at {Assembly.GetCallingAssembly().Location})! {reflectionTypeLoadException}"); - - foreach (Exception? loaderException in reflectionTypeLoadException.LoaderExceptions) - { - Log.Error(loaderException); - } - } - catch (Exception exception) - { - Log.Error($"Error while initializing config {Assembly.GetCallingAssembly().GetName().Name} (at {Assembly.GetCallingAssembly().Location})! {exception}"); - } - - return null; - } - - /// - /// Loads a config from the cached configs. - /// - /// The config to load. - public static void LoadFromCache(EConfig config) - { - if (!Cache.TryGetValue(config, out string path)) - return; - - foreach (EConfig cfg in MainConfigsValue) - { - if (string.IsNullOrEmpty(cfg.Folder) || cfg.Folder != config.Folder) - continue; - - cfg.data.Add(config); - } - - Load(config, path); - Cache.Remove(config); - } - - /// - /// Loads a config. - /// - /// The config to load. - /// The config's path. - public static void Load(EConfig config, string? path = null) - { - path ??= config.AbsolutePath; - if (File.Exists(path)) - { - config.Base = Deserializer.Deserialize(File.ReadAllText(path ?? throw new ArgumentNullException(nameof(path))), config.Base!.GetType())!; - File.WriteAllText(path, Serializer.Serialize(config.Base!)); - return; - } - - File.WriteAllText(path ?? throw new ArgumentNullException(nameof(path)), Serializer.Serialize(config.Base!)); - } - - /// - /// Gets the path of the specified data object of type . - /// - /// The type of the data to be read. - /// The corresponding data's path or if not found. - public static string? GetPath() - where T : class - { - object? config = Get(); - return config is null || config is not EConfig configBase ? null : configBase.AbsolutePath; - } - - /// - /// Reads a data object of type . - /// - /// The type of the data to be read. - /// The corresponding instance or if not found. - public T? Read() - where T : class - { - EConfig? t = Get(); - - if (t is null) - return default; - - return t.data.FirstOrDefault(data => data.Base!.GetType() == typeof(T)) as T ?? default; - } - - /// - /// Reads the base data object of type . - /// - /// The type of the data to be read. - /// The corresponding instance or if not found. - public T? BaseAs() => Base is not T t ? default : t; - - /// - /// Writes a new value contained in the specified config of type . - /// - /// The type of the data to be written. - /// The name of the parameter to be modified. - /// The new value to be written. - public void Write(string name, object value) - where T : class - { - T? param = Read(); - if (param is null) - return; - - string? path = GetPath(); - PropertyInfo? propertyInfo = param.GetType().GetProperty(name); - - if (propertyInfo is null) - return; - - propertyInfo.SetValue(param, value); - File.WriteAllText(path ?? throw new InvalidOperationException(), Serializer.Serialize(param)); - this.CopyProperties(Deserializer.Deserialize(File.ReadAllText(path), GetType())); - } - } -} \ No newline at end of file diff --git a/Exiled.API/Features/GlobalPatchProcessor.cs b/Exiled.API/Features/GlobalPatchProcessor.cs index dee1ceda16..2c5922f70f 100644 --- a/Exiled.API/Features/GlobalPatchProcessor.cs +++ b/Exiled.API/Features/GlobalPatchProcessor.cs @@ -57,7 +57,9 @@ public static void PatchAll(Harmony harmony, out int failedPatch) } } - Log.Debug("Events patched by attributes successfully!"); +#if DEBUG + Log.DebugWithContext("Events patched by attributes successfully!"); +#endif } /// @@ -86,7 +88,7 @@ public static Harmony PatchAll(string id = "", string groupId = null) } if (string.IsNullOrEmpty(patchGroup.GroupId)) - throw new ArgumentNullException("GroupId"); + throw new ArgumentNullException(nameof(groupId)); if (string.IsNullOrEmpty(groupId) || patchGroup.GroupId != groupId) continue; @@ -111,15 +113,13 @@ public static Harmony PatchAll(string id = "", string groupId = null) } #if DEBUG - MethodBase callee = new StackTrace().GetFrame(1).GetMethod(); - Log.Debug($"Patching completed. Requested by: ({callee.DeclaringType.Name}::{callee.Name})"); + Log.DebugWithContext($"Patching completed."); #endif return harmony; } catch (Exception ex) { - MethodBase callee = new StackTrace().GetFrame(1).GetMethod(); - Log.Error($"Callee ({callee.DeclaringType.Name}::{callee.Name}) Patching failed!, " + ex); + Log.ErrorWithContext($"Patching failed!\n" + ex); } return null; @@ -136,7 +136,7 @@ public static void UnpatchAll(string id = "", string groupId = null) Harmony harmony = new(id); foreach (MethodBase methodBase in Harmony.GetAllPatchedMethods().ToList()) { - PatchProcessor processor = harmony.CreateProcessor(methodBase); + harmony.CreateProcessor(methodBase); Patches patchInfo = Harmony.GetPatchInfo(methodBase); if (!patchInfo.Owners.Contains(id)) @@ -147,7 +147,7 @@ public static void UnpatchAll(string id = "", string groupId = null) goto Unpatch; if (string.IsNullOrEmpty(patchGroup.GroupId)) - throw new ArgumentNullException("GroupId"); + throw new ArgumentNullException(nameof(groupId)); if (string.IsNullOrEmpty(groupId) || patchGroup.GroupId != groupId) continue; diff --git a/Exiled.API/Features/Input/InputActionComponent.cs b/Exiled.API/Features/Input/InputActionComponent.cs index 1288b44c2e..80021b8097 100644 --- a/Exiled.API/Features/Input/InputActionComponent.cs +++ b/Exiled.API/Features/Input/InputActionComponent.cs @@ -28,7 +28,7 @@ public class InputActionComponent : EBehaviour /// Gets or sets the which handles all delegates to be fired before processing an action. /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher ProcessingActionDispatcher { get; set; } + public static TDynamicEventDispatcher ProcessingActionDispatcher { get; set; } = new(); /// /// Gets or sets the amount of key presses. diff --git a/Exiled.API/Features/Items/Firearm.cs b/Exiled.API/Features/Items/Firearm.cs index 1b0c62d0f0..41a48341a1 100644 --- a/Exiled.API/Features/Items/Firearm.cs +++ b/Exiled.API/Features/Items/Firearm.cs @@ -32,11 +32,6 @@ namespace Exiled.API.Features.Items /// public class Firearm : Item, IWrapper { - /// - /// A of which contains all the existing firearms based on all the s. - /// - internal static readonly Dictionary ItemTypeToFirearmInstance = new(); - /// /// Gets a which contains all the base codes expressed in and . /// diff --git a/Exiled.API/Features/Items/Item.cs b/Exiled.API/Features/Items/Item.cs index 9223b1bedc..d21666d07f 100644 --- a/Exiled.API/Features/Items/Item.cs +++ b/Exiled.API/Features/Items/Item.cs @@ -7,6 +7,7 @@ namespace Exiled.API.Features.Items { + using System; using System.Collections.Generic; using System.Linq; diff --git a/Exiled.API/Features/Log.cs b/Exiled.API/Features/Log.cs index e2360af37e..e08c6a65a9 100644 --- a/Exiled.API/Features/Log.cs +++ b/Exiled.API/Features/Log.cs @@ -72,6 +72,11 @@ public static class Log /// Represents serialization context, used for de/serialize operations and issues. /// public const string CONTEXT_SERIALIZATION = "SERIALIZATION"; + + /// + /// Represents deployent context, used for deployment and un/registration operations and issues. + /// + public const string CONTEXT_DEPLOYMENT = "DEPLOYMENT"; #pragma warning restore /// @@ -390,13 +395,18 @@ public static void AssertWithContext(bool condition, object message, string cont private static string GetContextInfo() { + StackFrame sourceStackFrame = new(1, true); StackFrame stackFrame = new(2, true); + MethodBase sourceMethod = sourceStackFrame.GetMethod(); MethodBase method = stackFrame.GetMethod(); + string sourceMethodName = sourceMethod?.Name ?? "UnknownMethod"; string methodName = method?.Name ?? "UnknownMethod"; + string sourceClassName = sourceMethod?.DeclaringType?.Name ?? "UnknownClass"; string className = method?.DeclaringType?.Name ?? "UnknownClass"; + int sourceLineNumber = sourceStackFrame.GetFileLineNumber(); int lineNumber = stackFrame.GetFileLineNumber(); - return $"[{className}::{methodName} at line {lineNumber}]"; + return $"{sourceClassName}::{sourceMethodName} (line {sourceLineNumber}) called {className}::{methodName} (line {lineNumber})"; } } } \ No newline at end of file diff --git a/Exiled.API/Features/Map.cs b/Exiled.API/Features/Map.cs index 998b3ad13d..22be75d24c 100644 --- a/Exiled.API/Features/Map.cs +++ b/Exiled.API/Features/Map.cs @@ -381,7 +381,6 @@ internal static void ClearCache() Ragdoll.BasicRagdollToRagdoll.Clear(); - Items.Firearm.ItemTypeToFirearmInstance.Clear(); Items.Firearm.BaseCodesValue.Clear(); Items.Firearm.AvailableAttachmentsValue.Clear(); diff --git a/Exiled.API/Features/Pickups/Pickup.cs b/Exiled.API/Features/Pickups/Pickup.cs index 86b86c24e5..b54bbdf4c9 100644 --- a/Exiled.API/Features/Pickups/Pickup.cs +++ b/Exiled.API/Features/Pickups/Pickup.cs @@ -7,6 +7,7 @@ namespace Exiled.API.Features.Pickups { + using System; using System.Collections.Generic; using System.Linq; @@ -77,7 +78,7 @@ internal Pickup(ItemType type) { ItemBase itemBase = type.GetItemBase(); - Base = Object.Instantiate(itemBase.PickupDropModel); + Base = UnityEngine.Object.Instantiate(itemBase.PickupDropModel); GameObject = Base.gameObject; PickupSyncInfo psi = new() diff --git a/Exiled.API/Features/Roles/Scp106Role.cs b/Exiled.API/Features/Roles/Scp106Role.cs index ffda53fbae..6e5797bbba 100644 --- a/Exiled.API/Features/Roles/Scp106Role.cs +++ b/Exiled.API/Features/Roles/Scp106Role.cs @@ -204,8 +204,6 @@ public bool SinkholeState /// public float SinkholeSpeedMultiplier => SinkholeController.SpeedMultiplier; - // TODO: ReAdd Setter but before making an propper way to overwrite NW constant only when the propperty has been used - /// /// Gets or sets how mush cost the Ability Stalk will cost per tick when being stationary. /// diff --git a/Exiled.API/Features/Serialization/CustomConverters/DynamicTypeConverter.cs b/Exiled.API/Features/Serialization/CustomConverters/DynamicTypeConverter.cs new file mode 100644 index 0000000000..06eb85822f --- /dev/null +++ b/Exiled.API/Features/Serialization/CustomConverters/DynamicTypeConverter.cs @@ -0,0 +1,58 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Serialization.CustomConverters +{ + using System; + + using Exiled.API.Features; + using YamlDotNet.Core; + using YamlDotNet.Serialization; + + /// + /// A type converter that dynamically handles the (de)serialization process for a specified type. + /// This converter allows custom (de)serialization logic based on the type's compatibility with the YAML deserializer. + /// + /// The type that this converter will handle during the (de)serialization process. + public class DynamicTypeConverter : IYamlTypeConverter + where T : class + { + private readonly bool shouldDeserialize; + + /// + /// Initializes a new instance of the class. + /// + /// Whether the entry should be deserialized. + public DynamicTypeConverter(bool inShouldDeserialize = true) + { + shouldDeserialize = inShouldDeserialize; + } + + /// + public bool Accepts(Type type) + { + return typeof(T).IsAssignableFrom(type); + } + + /// + public object ReadYaml(IParser parser, Type type) + { + if (shouldDeserialize) + { + return ConfigSubsystem.Deserializer.Deserialize(parser, type); + } + else + { + parser.SkipThisAndNestedEvents(); + return null; + } + } + + /// + public void WriteYaml(IEmitter emitter, object value, Type type) => ConfigSubsystem.Serializer.Serialize(emitter, value, type); + } +} \ No newline at end of file diff --git a/Exiled.API/Features/Serialization/CustomConverters/EnumClassConverter.cs b/Exiled.API/Features/Serialization/CustomConverters/EnumClassConverter.cs index b3554d657b..17dab63959 100644 --- a/Exiled.API/Features/Serialization/CustomConverters/EnumClassConverter.cs +++ b/Exiled.API/Features/Serialization/CustomConverters/EnumClassConverter.cs @@ -65,7 +65,7 @@ public object ReadYaml(IParser parser, Type type) /// The YAML emitter used to write the data. /// The object to serialize. /// The type of the object to serialize. - public void WriteYaml(IEmitter emitter, object value, Type type) => EConfig.Serializer.Serialize(emitter, value); + public void WriteYaml(IEmitter emitter, object value, Type type) => ConfigSubsystem.Serializer.Serialize(emitter, value); private void ApplyYamlData(object instance, Dictionary yamlData) { diff --git a/Exiled.API/Features/Spawn/SpawnProperties.cs b/Exiled.API/Features/Spawn/SpawnProperties.cs index d219791f52..98289d667a 100644 --- a/Exiled.API/Features/Spawn/SpawnProperties.cs +++ b/Exiled.API/Features/Spawn/SpawnProperties.cs @@ -8,6 +8,9 @@ namespace Exiled.API.Features.Spawn { using System.Collections.Generic; + using System.ComponentModel; + + using YamlDotNet.Serialization; /// /// Handles special properties of spawning an entity. @@ -17,32 +20,38 @@ public class SpawnProperties /// /// Gets or sets a value indicating how many items can be spawned when the round starts. /// + [Description("Indicates how many items can be spawned when the round starts.")] public uint Limit { get; set; } /// /// Gets or sets a of possible dynamic spawn points. /// + [Description("A list of possible dynamic spawn points.")] public List DynamicSpawnPoints { get; set; } = new(); /// /// Gets or sets a of possible static spawn points. /// + [Description("A list of possible static spawn points.")] public List StaticSpawnPoints { get; set; } = new(); /// /// Gets or sets a of possible role-based spawn points. /// + [Description("A list of possible role-based spawn points.")] public List RoleSpawnPoints { get; set; } = new(); /// /// Gets a value indicating whether spawn points count is zero. /// + [YamlIgnore] public bool IsEmpty => Length == 0; /// /// Gets the amount of spawn points in this instance. /// /// The amount of existing spawn points. + [YamlIgnore] public int Length => DynamicSpawnPoints.Count + StaticSpawnPoints.Count + RoleSpawnPoints.Count; } } \ No newline at end of file diff --git a/Exiled.API/Features/VirtualAssemblies/Generics/VirtualPlugin.cs b/Exiled.API/Features/VirtualAssemblies/Generics/VirtualPlugin.cs index 4a7161943f..f04ef7c3b7 100644 --- a/Exiled.API/Features/VirtualAssemblies/Generics/VirtualPlugin.cs +++ b/Exiled.API/Features/VirtualAssemblies/Generics/VirtualPlugin.cs @@ -13,6 +13,6 @@ public abstract class VirtualPlugin : VirtualPlugin where TConfig : class { /// - public override EConfig Config { get; protected set; } = EConfig.Get(true); + public override ConfigSubsystem Config { get; protected set; } = ConfigSubsystem.Get(true); } } diff --git a/Exiled.API/Features/VirtualAssemblies/VirtualPlugin.cs b/Exiled.API/Features/VirtualAssemblies/VirtualPlugin.cs index cec6c97657..c35fb0f1d0 100644 --- a/Exiled.API/Features/VirtualAssemblies/VirtualPlugin.cs +++ b/Exiled.API/Features/VirtualAssemblies/VirtualPlugin.cs @@ -36,19 +36,19 @@ public abstract class VirtualPlugin : TypeCastObject /// Gets or sets the which handles all the delegates fired before enabling a . /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher EnablingVirtualPluginDispatcher { get; set; } + public static TDynamicEventDispatcher EnablingVirtualPluginDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before disabling a . /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher DisablingVirtualPluginDispatcher { get; set; } + public static TDynamicEventDispatcher DisablingVirtualPluginDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before reloading a . /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher ReloadingVirtualPluginDispatcher { get; set; } + public static TDynamicEventDispatcher ReloadingVirtualPluginDispatcher { get; set; } = new(); /// /// Gets a containing all registered instances. @@ -56,9 +56,9 @@ public abstract class VirtualPlugin : TypeCastObject public static IEnumerable List => Registered; /// - /// Gets or sets the object. + /// Gets or sets the object. /// - public abstract EConfig Config { get; protected set; } + public abstract ConfigSubsystem Config { get; protected set; } /// /// Gets the plugin's master branch name. @@ -207,7 +207,7 @@ public bool ReloadConfig() if (!Config) return false; - EConfig.Load(Config); + ConfigSubsystem.Load(Config); return true; } diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/Attach.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/Attach.cs new file mode 100644 index 0000000000..e09d938ba1 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/Attach.cs @@ -0,0 +1,115 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomEscapes; + using Exiled.Permissions.Extensions; + + /// + /// The command to attach a custom escape to a player(s). + /// + internal sealed class Attach : ICommand + { + private Attach() + { + } + + /// + /// Gets the instance. + /// + public static Attach Instance { get; } = new(); + + /// + public string Command { get; } = "attach"; + + /// + public string[] Aliases { get; } = { "a" }; + + /// + public string Description { get; } = "Attach the specified custom escape to a player(s)."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customescapes.attach")) + { + response = "Permission denied, customescapes.attach is required."; + return false; + } + + if (arguments.Count < 2) + { + response = "attach [Nickname / PlayerID / UserID / all / *]"; + return false; + } + + if (!CustomEscape.TryGet(arguments.At(0), out CustomEscape escape) && (!uint.TryParse(arguments.At(0), out uint id) || !CustomEscape.TryGet(id, out escape)) && escape is null) + { + response = $"Custom escape {arguments.At(0)} not found!"; + return false; + } + + if (arguments.Count == 2) + { + Player player = Player.Get(arguments.At(1)); + + if (player is null) + { + response = "Player not found."; + return false; + } + + escape.Attach(player); + response = $"{escape.Name} ({escape.Id}) has been attached to {player.Nickname}."; + return true; + } + + string identifier = string.Join(" ", arguments.Skip(1)); + + switch (identifier) + { + case "*": + case "all": + List players = ListPool.Pool.Get(Player.List).ToList(); + + foreach (Player ply in players) + escape.Attach(ply); + + response = $"{escape.Name} ({escape.Id}) attached to all players."; + ListPool.Pool.Return(players); + return true; + default: + if (Player.Get(identifier) is null) + { + response = $"Unable to find the player: {identifier}"; + return false; + } + + escape.Attach(Player.Get(identifier)); + response = $"{escape.Name} ({escape.Id}) has been attached to {Player.Get(identifier).Nickname}."; + return true; + } + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/Detach.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/Detach.cs new file mode 100644 index 0000000000..858d9b9244 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/Detach.cs @@ -0,0 +1,117 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomEscapes; + using Exiled.Permissions.Extensions; + + /// + /// The command to detach a custom escape from a player(s). + /// + internal sealed class Detach : ICommand + { + private Detach() + { + } + + /// + /// Gets the instance. + /// + public static Detach Instance { get; } = new(); + + /// + public string Command { get; } = "detach"; + + /// + public string[] Aliases { get; } = { "d" }; + + /// + public string Description { get; } = "Detaches the specified custom escape from a player(s)."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customescapes.detach")) + { + response = "Permission denied, customescapes.detach is required."; + return false; + } + + if (arguments.Count < 2) + { + response = "detach [Nickname / PlayerID / UserID / all / *]"; + return false; + } + + if (!CustomEscape.TryGet(arguments.At(0), out CustomEscape escape) && + (!uint.TryParse(arguments.At(0), out uint id) || + !CustomEscape.TryGet(id, out escape)) && escape is null) + { + response = $"Custom escape {arguments.At(0)} not found!"; + return false; + } + + if (arguments.Count == 2) + { + Player player = Player.Get(arguments.At(1)); + + if (player is null) + { + response = "Player not found."; + return false; + } + + escape.Detach(player); + response = $"{escape.Name} ({escape.Id}) has been detached from {player.Nickname}."; + return true; + } + + string identifier = string.Join(" ", arguments.Skip(1)); + + switch (identifier) + { + case "*": + case "all": + List players = ListPool.Pool.Get(Player.List).ToList(); + + foreach (Player ply in players) + escape.Detach(ply); + + response = $"{escape.Name} ({escape.Id}) detached from all players."; + ListPool.Pool.Return(players); + return true; + default: + if (Player.Get(identifier) is null) + { + response = $"Unable to find the player: {identifier}"; + return false; + } + + escape.Detach(Player.Get(identifier)); + response = $"{escape.Name} ({escape.Id}) has been detached from {Player.Get(identifier).Nickname}."; + return true; + } + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/Info.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/Info.cs new file mode 100644 index 0000000000..236e2fef19 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/Info.cs @@ -0,0 +1,92 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes +{ + using System; + using System.Collections.Generic; + using System.Text; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomEscapes; + using Exiled.Permissions.Extensions; + + /// + /// The command to retrieve a specific custom escape information. + /// + internal sealed class Info : ICommand + { + private Info() + { + } + + /// + /// Gets the command instance. + /// + public static Info Instance { get; } = new(); + + /// + public string Command { get; } = "info"; + + /// + public string[] Aliases { get; } = { "i" }; + + /// + public string Description { get; } = "Gets the information of the specified custom escape."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customescapes.info")) + { + response = "Permission denied, customescapes.info is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "info [Custom Escape ID]"; + return false; + } + + if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomEscape.TryGet(id, out CustomEscape escape)) && !CustomEscape.TryGet(arguments.At(0), out escape)) || escape is null) + { + response = $"{arguments.At(0)} is not a valid Custom Escape."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("- ").Append(escape.Name) + .Append(" (").Append(escape.Id).Append(")") + .AppendLine("- Is Enabled: ").Append(escape.IsEnabled) + .AppendLine("- Attached To: "); + + foreach (KeyValuePair managers in CustomEscape.Manager) + { + if (managers.Value != escape) + continue; + + builder.Append(" - ").Append(managers.Key.Nickname).Append(" (").Append(managers.Key.UserId).Append(")").AppendLine(); + } + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/List/List.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/List/List.cs new file mode 100644 index 0000000000..63f22da071 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/List/List.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes.List +{ + using System; + + using CommandSystem; + + /// + /// The command to list all registered custom escapes. + /// + internal sealed class List : ParentCommand + { + private List() + { + LoadGeneratedCommands(); + } + + /// + /// Gets the command instance. + /// + public static List Instance { get; } = new(); + + /// + 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 escapes."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Registered.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: registered."; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/List/Registered.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/List/Registered.cs new file mode 100644 index 0000000000..2a6e723367 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/List/Registered.cs @@ -0,0 +1,66 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes.List +{ + using System; + using System.Linq; + using System.Text; + + using CommandSystem; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomEscapes; + using Exiled.Permissions.Extensions; + + /// + internal sealed class Registered : ICommand + { + private Registered() + { + } + + /// + /// Gets the command instance. + /// + 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 escapes."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customescapes.list.registered")) + { + response = "Permission denied, customescapes.list.registered is required."; + return false; + } + + if (!CustomEscape.List.Any()) + { + response = "There are no custom escapes currently on this server."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("[Registered Custom Escapes (").Append(CustomEscape.List.Count()).AppendLine(")]"); + + foreach (CustomEscape escape in CustomEscape.List.OrderBy(r => r.Id)) + builder.Append('[').Append(escape.Id).Append(". ").Append(escape.Name).AppendLine("]"); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomEscapes/Parent.cs b/Exiled.CustomModules/API/Commands/CustomEscapes/Parent.cs new file mode 100644 index 0000000000..a1901530d7 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomEscapes/Parent.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomEscapes +{ + using System; + + using CommandSystem; + + /// + /// The main parent command for custom escapes. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + internal sealed class Parent : ParentCommand + { + /// + /// Initializes a new instance of the class. + /// + public Parent() + { + LoadGeneratedCommands(); + } + + /// + public override string Command { get; } = "customescapes"; + + /// + public override string[] Aliases { get; } = { "ce", "ces", "escapes", "esc" }; + + /// + public override string Description { get; } = "Commands for managing custom escapes."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Attach.Instance); + RegisterCommand(Detach.Instance); + RegisterCommand(Info.Instance); + RegisterCommand(List.List.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: attach, detach, info, list"; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomItem/Give.cs b/Exiled.CustomModules/API/Commands/CustomItem/Give.cs index 0d68c1bf49..9fa8c278c5 100644 --- a/Exiled.CustomModules/API/Commands/CustomItem/Give.cs +++ b/Exiled.CustomModules/API/Commands/CustomItem/Give.cs @@ -46,17 +46,19 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!sender.CheckPermission("customitems.give")) { - response = "Permission Denied, required: customitems.give"; + response = "Permission denied, customitems.give is required."; return false; } if (arguments.Count == 0) { - response = "give [Nickname/PlayerID/UserID/all/*]"; + response = "give [Nickname / PlayerID / UserID / all / *]"; return false; } - if (!CustomItem.TryGet(arguments.At(0), out CustomItem item) && (!uint.TryParse(arguments.At(0), out uint id) || !CustomItem.TryGet(id, out item)) && item is null) + if (!CustomItem.TryGet(arguments.At(0), out CustomItem item) && + (!uint.TryParse(arguments.At(0), out uint id) || + !CustomItem.TryGet(id, out item)) && item is null) { response = $"Custom item {arguments.At(0)} not found!"; return false; @@ -93,7 +95,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s foreach (Player ply in eligiblePlayers) item?.Give(ply); - response = $"Custom item {item?.Name} given to all players who can receive them ({eligiblePlayers.Count} players)"; + response = $"Custom item {item?.Name} given to all players who can receive them ({eligiblePlayers.Count} players)."; return true; default: if (Player.Get(identifier) is not { } player) diff --git a/Exiled.CustomModules/API/Commands/CustomItem/Info.cs b/Exiled.CustomModules/API/Commands/CustomItem/Info.cs index 5960d9145e..edc5663a9d 100644 --- a/Exiled.CustomModules/API/Commands/CustomItem/Info.cs +++ b/Exiled.CustomModules/API/Commands/CustomItem/Info.cs @@ -45,17 +45,18 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!sender.CheckPermission("customitems.info")) { - response = "Permission Denied, required: customitems.info"; + response = "Permission denied, customitems.info is required."; return false; } if (arguments.Count < 1) { - response = "info [Custom item name/Custom item ID]"; + response = "info "; return false; } - if (!(uint.TryParse(arguments.At(0), out uint id) && CustomItem.TryGet(id, out CustomItem item)) && + if (!(uint.TryParse(arguments.At(0), out uint id) && + CustomItem.TryGet(id, out CustomItem item)) && !CustomItem.TryGet(arguments.At(0), out item)) { response = $"{arguments.At(0)} is not a valid custom item."; diff --git a/Exiled.CustomModules/API/Commands/CustomItem/Parent.cs b/Exiled.CustomModules/API/Commands/CustomItem/Parent.cs index 70074ae8dd..d302f1f046 100644 --- a/Exiled.CustomModules/API/Commands/CustomItem/Parent.cs +++ b/Exiled.CustomModules/API/Commands/CustomItem/Parent.cs @@ -12,7 +12,7 @@ namespace Exiled.CustomModules.API.Commands.CustomItem using CommandSystem; /// - /// The main parent command for custom items.. + /// The main parent command for custom items. /// [CommandHandler(typeof(RemoteAdminCommandHandler))] [CommandHandler(typeof(GameConsoleCommandHandler))] @@ -33,7 +33,7 @@ public Parent() public override string[] Aliases { get; } = { "ci", "cis" }; /// - public override string Description { get; } = string.Empty; + public override string Description { get; } = "Commands for managing custom items."; /// public override void LoadGeneratedCommands() diff --git a/Exiled.CustomModules/API/Commands/CustomItem/Spawn.cs b/Exiled.CustomModules/API/Commands/CustomItem/Spawn.cs index 240d8be3fe..56db65ab82 100644 --- a/Exiled.CustomModules/API/Commands/CustomItem/Spawn.cs +++ b/Exiled.CustomModules/API/Commands/CustomItem/Spawn.cs @@ -39,24 +39,28 @@ private Spawn() public string[] Aliases { get; } = { "sp" }; /// - public string Description { get; } = "Spawn an item at the specified Spawn Location, coordinates, or at the designated player's feet."; + public string Description { get; } = "Spawns an item at the specified spawn location, coordinates, or at the designated player's position."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission("customitems.spawn")) { - response = "Permission Denied, required: customitems.spawn"; + response = "Permission denied, customitems.spawn is required."; return false; } if (arguments.Count < 2) { - response = "spawn [Custom item name] [Location name]\nspawn [Custom item name] [Nickname/PlayerID/UserID]\nspawn [Custom item name] [X] [Y] [Z]"; + response = "spawn [Location name]" + + "\nspawn [Nickname / PlayerID / UserID]" + + "\nspawn [X] [Y] [Z]"; return false; } - if (!CustomItem.TryGet(arguments.At(0), out CustomItem item)) + if (!(uint.TryParse(arguments.At(0), out uint id) && + CustomItem.TryGet(id, out CustomItem item)) && + !CustomItem.TryGet(arguments.At(0), out item)) { response = $" {arguments.At(0)} is not a valid custom item."; return false; @@ -84,7 +88,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!float.TryParse(arguments.At(1), out float x) || !float.TryParse(arguments.At(2), out float y) || !float.TryParse(arguments.At(3), out float z)) { - response = "Invalid coordinates selected."; + response = "Invalid coordinates."; return false; } diff --git a/Exiled.CustomModules/API/Commands/CustomRoles/Give.cs b/Exiled.CustomModules/API/Commands/CustomRoles/Give.cs index 224e15b997..5796827ca8 100644 --- a/Exiled.CustomModules/API/Commands/CustomRoles/Give.cs +++ b/Exiled.CustomModules/API/Commands/CustomRoles/Give.cs @@ -48,13 +48,13 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!sender.CheckPermission("customroles.give")) { - response = "Permission Denied, required: customroles.give"; + response = "Permission denied, customroles.give is required."; return false; } if (arguments.Count < 2) { - response = "give Custom role ID> [Nickname/PlayerID/UserID/all/*]"; + response = "give [Nickname / PlayerID / UserID / all / *]"; return false; } @@ -70,7 +70,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s if (player is null) { - response = "Player not found"; + response = "Player not found."; return false; } diff --git a/Exiled.CustomModules/API/Commands/CustomRoles/Info.cs b/Exiled.CustomModules/API/Commands/CustomRoles/Info.cs index aa45373fef..b2ecf16ae5 100644 --- a/Exiled.CustomModules/API/Commands/CustomRoles/Info.cs +++ b/Exiled.CustomModules/API/Commands/CustomRoles/Info.cs @@ -44,13 +44,13 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!sender.CheckPermission("customroles.info")) { - response = "Permission Denied, required: customroles.info"; + response = "Permission denied, customroles.info is required."; return false; } if (arguments.Count < 1) { - response = "info [Custom role name/Custom role ID]"; + response = "info "; return false; } diff --git a/Exiled.CustomModules/API/Commands/CustomRoles/List/Registered.cs b/Exiled.CustomModules/API/Commands/CustomRoles/List/Registered.cs index 6c3f251114..046956a066 100644 --- a/Exiled.CustomModules/API/Commands/CustomRoles/List/Registered.cs +++ b/Exiled.CustomModules/API/Commands/CustomRoles/List/Registered.cs @@ -42,7 +42,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { if (!sender.CheckPermission("customroles.list.registered")) { - response = "Permission Denied, required: customroles.list.registered"; + response = "Permission denied, customroles.list.registered is required."; return false; } diff --git a/Exiled.CustomModules/API/Commands/CustomRoles/Parent.cs b/Exiled.CustomModules/API/Commands/CustomRoles/Parent.cs index b7ffdef3a6..e725ca9b2b 100644 --- a/Exiled.CustomModules/API/Commands/CustomRoles/Parent.cs +++ b/Exiled.CustomModules/API/Commands/CustomRoles/Parent.cs @@ -12,7 +12,7 @@ namespace Exiled.CustomModules.API.Commands.CustomRoles using CommandSystem; /// - /// The main parent command for customroles. + /// The main parent command for custom roles. /// [CommandHandler(typeof(RemoteAdminCommandHandler))] [CommandHandler(typeof(GameConsoleCommandHandler))] @@ -33,7 +33,7 @@ public Parent() public override string[] Aliases { get; } = { "cr", "crs" }; /// - public override string Description { get; } = string.Empty; + public override string Description { get; } = "Commands for managing custom roles."; /// public override void LoadGeneratedCommands() diff --git a/Exiled.CustomModules/API/Commands/CustomTeams/Info.cs b/Exiled.CustomModules/API/Commands/CustomTeams/Info.cs new file mode 100644 index 0000000000..79178a49f7 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomTeams/Info.cs @@ -0,0 +1,85 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomTeams +{ + using System; + using System.Text; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.Permissions.Extensions; + + /// + /// The command to retrieve a specific custom team information. + /// + internal sealed class Info : ICommand + { + private Info() + { + } + + /// + /// Gets the command instance. + /// + public static Info Instance { get; } = new(); + + /// + public string Command { get; } = "info"; + + /// + public string[] Aliases { get; } = { "i" }; + + /// + public string Description { get; } = "Gets the information of the specified custom team."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customteams.info")) + { + response = "Permission denied, customteams.info is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "info "; + return false; + } + + if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomTeam.TryGet(id, out CustomTeam team)) && !CustomTeam.TryGet(arguments.At(0), out team)) || team is null) + { + response = $"{arguments.At(0)} is not a valid custom team."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("- ").Append(team.Name) + .Append(" (").Append(team.Id).Append(")") + .AppendLine("- Is Enabled: ").Append(team.IsEnabled) + .AppendLine("- Probability: ").Append(team.Probability) + .AppendLine("- Display Color: ").Append(team.DisplayColor) + .AppendLine("- Size: ").Append(team.Size); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomTeams/List/List.cs b/Exiled.CustomModules/API/Commands/CustomTeams/List/List.cs new file mode 100644 index 0000000000..681feceb54 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomTeams/List/List.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomTeams.List +{ + using System; + + using CommandSystem; + + /// + /// The command to list all registered custom teams. + /// + internal sealed class List : ParentCommand + { + private List() + { + LoadGeneratedCommands(); + } + + /// + /// Gets the command instance. + /// + public static List Instance { get; } = new(); + + /// + 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 teams."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Registered.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: registered."; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomTeams/List/Registered.cs b/Exiled.CustomModules/API/Commands/CustomTeams/List/Registered.cs new file mode 100644 index 0000000000..5dc4d33871 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomTeams/List/Registered.cs @@ -0,0 +1,66 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomTeams.List +{ + using System; + using System.Linq; + using System.Text; + + using CommandSystem; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.Permissions.Extensions; + + /// + internal sealed class Registered : ICommand + { + private Registered() + { + } + + /// + /// Gets the command instance. + /// + 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 teams."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customteams.list.registered")) + { + response = "Permission denied, customteams.list.registered is required."; + return false; + } + + if (!CustomTeam.List.Any()) + { + response = "There are no custom teams currently on this server."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("[Registered Custom Teams (").Append(CustomTeam.List.Count()).AppendLine(")]"); + + foreach (CustomTeam team in CustomTeam.List.OrderBy(r => r.Id)) + builder.Append('[').Append(team.Id).Append(". ").Append(team.Name).AppendLine("]"); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomTeams/Parent.cs b/Exiled.CustomModules/API/Commands/CustomTeams/Parent.cs new file mode 100644 index 0000000000..9b7e5a88a0 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomTeams/Parent.cs @@ -0,0 +1,53 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomTeams +{ + using System; + + using CommandSystem; + + /// + /// The main parent command for custom teams. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + internal sealed class Parent : ParentCommand + { + /// + /// Initializes a new instance of the class. + /// + public Parent() + { + LoadGeneratedCommands(); + } + + /// + public override string Command { get; } = "customteams"; + + /// + public override string[] Aliases { get; } = { "cts" }; + + /// + public override string Description { get; } = "Commands for managing custom teams."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Spawn.Instance); + RegisterCommand(Info.Instance); + RegisterCommand(List.List.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: spawn, info, list"; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/CustomTeams/Spawn.cs b/Exiled.CustomModules/API/Commands/CustomTeams/Spawn.cs new file mode 100644 index 0000000000..88cf7fd832 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/CustomTeams/Spawn.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.CustomTeams +{ + using System; + using System.Linq; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.Permissions.Extensions; + using PlayerRoles; + + /// + /// The command to spawn a custom team. + /// + internal sealed class Spawn : ICommand + { + private Spawn() + { + } + + /// + /// Gets the instance. + /// + public static Spawn Instance { get; } = new(); + + /// + public string Command { get; } = "spawn"; + + /// + public string[] Aliases { get; } = { "s" }; + + /// + public string Description { get; } = "Spawns the specified custom team."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("customteams.spawn")) + { + response = "Permission denied, customteams.spawn is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "spawn "; + return false; + } + + if (!CustomTeam.TryGet(arguments.At(0), out CustomTeam team) && (!uint.TryParse(arguments.At(0), out uint id) || !CustomTeam.TryGet(id, out team)) && team is null) + { + response = $"Custom team {arguments.At(0)} not found!"; + return false; + } + + if (!Player.Get(Team.Dead).Any()) + { + response = "There are no dead players to spawn the custom team."; + return false; + } + + team.Respawn(true); + + response = $"Custom team {team.Name} has been spawned."; + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/End.cs b/Exiled.CustomModules/API/Commands/Gamemodes/End.cs new file mode 100644 index 0000000000..2d5cc3f886 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/End.cs @@ -0,0 +1,72 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes +{ + using System; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.CustomModules.API.Features; + using Exiled.Permissions.Extensions; + + /// + /// The command to end a gamemode. + /// + internal sealed class End : ICommand + { + private End() + { + } + + /// + /// Gets the command instance. + /// + public static End Instance { get; } = new(); + + /// + public string Command { get; } = "end"; + + /// + public string[] Aliases { get; } = { "e" }; + + /// + public string Description { get; } = "Ends the running gamemode."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("gamemodes.end")) + { + response = "Permission denied, gamemodes.end is required."; + return false; + } + + World world = World.Get(); + + if (world.GameState) + { + world.GameState.End(true); + + response = "The gamemode has been ended."; + return true; + } + + response = "There is no gamemode running."; + return false; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/Enqueue.cs b/Exiled.CustomModules/API/Commands/Gamemodes/Enqueue.cs new file mode 100644 index 0000000000..ce0f52d20f --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/Enqueue.cs @@ -0,0 +1,79 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes +{ + using System; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.CustomModules.API.Features; + using Exiled.CustomModules.API.Features.CustomGameModes; + using Exiled.Permissions.Extensions; + + /// + /// The command to enqueue gamemodes. + /// + internal sealed class Enqueue : ICommand + { + private Enqueue() + { + } + + /// + /// Gets the command instance. + /// + public static Enqueue Instance { get; } = new(); + + /// + public string Command { get; } = "enqueue"; + + /// + public string[] Aliases { get; } = { "eq" }; + + /// + public string Description { get; } = "Enqueues the specified gamemode."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("gamemodes.enqueue")) + { + response = "Permission denied, gamemodes.enqueue is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "Usage: enqueue "; + return false; + } + + if (CustomGameMode.TryGet(arguments.At(0), out CustomGameMode gameMode) && + (!uint.TryParse(arguments.At(0), out uint id) || + !CustomGameMode.TryGet(id, out gameMode)) && gameMode is null) + { + response = $"Custom gamemode {arguments.At(0)} not found!"; + return false; + } + + World.Get().EnqueueGameMode(gameMode); + + response = $"Enqueued: <{gameMode.Id}> {gameMode.Name}"; + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/Info.cs b/Exiled.CustomModules/API/Commands/Gamemodes/Info.cs new file mode 100644 index 0000000000..f4e3110c11 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/Info.cs @@ -0,0 +1,82 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes +{ + using System; + using System.Text; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomGameModes; + using Exiled.Permissions.Extensions; + + /// + /// The command to retrieve a specific gamemode information. + /// + internal sealed class Info : ICommand + { + private Info() + { + } + + /// + /// Gets the command instance. + /// + public static Info Instance { get; } = new(); + + /// + public string Command { get; } = "info"; + + /// + public string[] Aliases { get; } = { "i" }; + + /// + public string Description { get; } = "Gets the information of the specified gamemode."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("gamemodes.info")) + { + response = "Permission denied, gamemodes.info is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "info "; + return false; + } + + if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomGameMode.TryGet(id, out CustomGameMode gameMode)) && !CustomGameMode.TryGet(arguments.At(0), out gameMode)) || gameMode is null) + { + response = $"{arguments.At(0)} is not a valid custom gamemode."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + builder.Append("- ").Append(gameMode.Name) + .Append(" (").Append(gameMode.Id).Append(")") + .AppendLine("- Is Enabled: ").Append(gameMode.IsEnabled) + .AppendLine("- Can Start Automatically: ").Append(gameMode.CanStartAuto); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/List/List.cs b/Exiled.CustomModules/API/Commands/Gamemodes/List/List.cs new file mode 100644 index 0000000000..e675c01714 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/List/List.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes.List +{ + using System; + + using CommandSystem; + + /// + /// The command to list all registered gamemodes. + /// + internal sealed class List : ParentCommand + { + private List() + { + LoadGeneratedCommands(); + } + + /// + /// Gets the command instance. + /// + public static List Instance { get; } = new(); + + /// + public override string Command { get; } = "list"; + + /// + public override string[] Aliases { get; } = { "l" }; + + /// + public override string Description { get; } = "Gets a list of all currently registered gamemodes."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Registered.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: registered."; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/List/Registered.cs b/Exiled.CustomModules/API/Commands/Gamemodes/List/Registered.cs new file mode 100644 index 0000000000..2e21cc3bb4 --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/List/Registered.cs @@ -0,0 +1,66 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes.List +{ + using System; + using System.Linq; + using System.Text; + + using CommandSystem; + using Exiled.API.Features.Core.Generic.Pools; + using Exiled.CustomModules.API.Features.CustomGameModes; + using Exiled.Permissions.Extensions; + + /// + internal sealed class Registered : ICommand + { + private Registered() + { + } + + /// + /// Gets the command instance. + /// + 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 gamemodes."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("gamemodes.list.registered")) + { + response = "Permission denied, gamemodes.list.registered is required."; + return false; + } + + if (!CustomGameMode.List.Any()) + { + response = "There are no gamemodes currently on this server."; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("[Registered gamemodes (").Append(CustomGameMode.List.Count()).AppendLine(")]"); + + foreach (CustomGameMode gameMode in CustomGameMode.List.OrderBy(r => r.Id)) + builder.Append('[').Append(gameMode.Id).Append(". ").Append(gameMode.Name).AppendLine("]"); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/Parent.cs b/Exiled.CustomModules/API/Commands/Gamemodes/Parent.cs new file mode 100644 index 0000000000..18c1dc01ce --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/Parent.cs @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes +{ + using System; + + using CommandSystem; + + /// + /// The main parent command for custom gamemodes. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + internal sealed class Parent : ParentCommand + { + /// + /// Initializes a new instance of the class. + /// + public Parent() + { + LoadGeneratedCommands(); + } + + /// + public override string Command { get; } = "customgamemodes"; + + /// + public override string[] Aliases { get; } = { "gm", "cgm", "cgmds" }; + + /// + public override string Description { get; } = "Commands for managing custom gamemodes."; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Start.Instance); + RegisterCommand(End.Instance); + RegisterCommand(Info.Instance); + RegisterCommand(Enqueue.Instance); + RegisterCommand(List.List.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Invalid subcommand! Available: start, end, info, enqueue, list"; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Commands/Gamemodes/Start.cs b/Exiled.CustomModules/API/Commands/Gamemodes/Start.cs new file mode 100644 index 0000000000..849acef55b --- /dev/null +++ b/Exiled.CustomModules/API/Commands/Gamemodes/Start.cs @@ -0,0 +1,77 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Commands.GameModes +{ + using System; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.CustomModules.API.Features; + using Exiled.CustomModules.API.Features.CustomGameModes; + using Exiled.Permissions.Extensions; + + /// + /// The command to start a gamemode. + /// + internal sealed class Start : ICommand + { + private Start() + { + } + + /// + /// Gets the command instance. + /// + public static Start Instance { get; } = new(); + + /// + public string Command { get; } = "start"; + + /// + public string[] Aliases { get; } = { "s" }; + + /// + public string Description { get; } = "Starts the specified custom gamemode."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + if (!sender.CheckPermission("gamemodes.start")) + { + response = "Permission denied, gamemodes.start is required."; + return false; + } + + if (arguments.Count < 1) + { + response = "start "; + return false; + } + + if (CustomGameMode.TryGet(arguments.At(0), out CustomGameMode gameMode) && (!uint.TryParse(arguments.At(0), out uint id) || !CustomGameMode.TryGet(id, out gameMode)) && gameMode is null) + { + response = $"Custom gamemode {arguments.At(0)} not found!"; + return false; + } + + World.Get().Start(gameMode, true); + + response = $"Started: <{gameMode.Id}> {gameMode.Name}."; + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = "An error occurred when executing the command, check server console for more details."; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/AbilityTracker.cs b/Exiled.CustomModules/API/Features/CustomAbilities/AbilityTracker.cs deleted file mode 100644 index dbea404321..0000000000 --- a/Exiled.CustomModules/API/Features/CustomAbilities/AbilityTracker.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.CustomAbilities -{ - using Exiled.CustomModules.API.Features.Generic; - - /// - /// The actor which handles all tracking-related tasks for item abilities. - /// - public class AbilityTracker : TrackerBase - { - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/CustomAbility.cs b/Exiled.CustomModules/API/Features/CustomAbilities/CustomAbility.cs index 5df8885d06..5f96903c74 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/CustomAbility.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/CustomAbility.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reflection; @@ -128,31 +129,36 @@ public abstract class CustomAbility : CustomModule, ICustomAbility [YamlIgnore] public abstract Type BehaviourComponent { get; } - /// - /// Gets the ability's settings. - /// - public virtual AbilitySettings Settings { get; } = AbilitySettings.Default; - /// /// Gets or sets the 's name. /// + [Description("The name of the custom ability.")] public override string Name { get; set; } /// /// Gets or sets the 's id. /// + [Description("The id of the custom ability.")] public override uint Id { get; set; } /// /// Gets or sets a value indicating whether the ability is enabled. /// + [Description("Indicates whether the ability is enabled.")] public override bool IsEnabled { get; set; } /// /// Gets or sets the description of the ability. /// + [Description("The description of the custom ability.")] public virtual string Description { get; set; } + /// + /// Gets the ability's settings. + /// + [Description("The settings for the custom ability.")] + public virtual AbilitySettings Settings { get; } = AbilitySettings.Default; + /// /// Gets the reflected generic type. /// @@ -487,16 +493,15 @@ public static void RemoveRange(T entity, IEnumerable ids) /// /// Enables all the custom abilities present in the assembly. /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom abilities present in the assembly. - /// - /// The assembly to enable the abilities from. - public static void EnableAll(Assembly assembly) + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. + /// + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. + /// + public static int EnableAll(Assembly assembly = null) { - if (!CustomModules.Instance.Config.Modules.Contains(UUModuleType.CustomAbilities.Name)) - throw new Exception("ModuleType::CustomAbilities must be enabled in order to load any custom abilities"); + assembly ??= Assembly.GetCallingAssembly(); List> customAbilities = new(); foreach (Type type in assembly.GetTypes()) @@ -515,19 +520,25 @@ public static void EnableAll(Assembly assembly) customAbilities.Add(customAbility); } - if (customAbilities.Count != Registered.Count) - Log.Info($"{customAbilities.Count()} custom abilities have been successfully registered!"); + return customAbilities.Count; } /// /// Disables all the custom abilities present in the assembly. /// - public static void DisableAll() + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. + /// + /// This method dynamically disables all module instances found in the calling assembly that were + /// previously registered. + /// + public static int DisableAll(Assembly assembly = null) { - List> customAbilities = new(); - customAbilities.AddRange(UnorderedRegistered.Where(ability => ability.TryUnregister())); + assembly ??= Assembly.GetCallingAssembly(); - Log.Info($"{customAbilities.Count()} custom abilities have been successfully unregistered!"); + List> customAbilities = new(); + customAbilities.AddRange(UnorderedRegistered.Where(ability => ability.GetType().Assembly == assembly && ability.TryUnregister())); + return customAbilities.Count; } /// diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Item/ItemAbility.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Item/ItemAbility.cs index a64d5d5ded..387d239217 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Item/ItemAbility.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Item/ItemAbility.cs @@ -37,6 +37,11 @@ public abstract class ItemAbility : CustomAbility /// public static IEnumerable Owners => Manager.Keys.ToHashSet(); + /// + /// Gets the . + /// + protected static TrackerBase Tracker { get; } = TrackerBase.Get(); + /// /// Gets a given the specified id. /// @@ -103,33 +108,33 @@ public abstract class ItemAbility : CustomAbility /// The type to search for. /// The found , if not registered. /// if a was found; otherwise, . - public static bool TryGet(Type type, out ItemAbility customAbility) => customAbility = Get(type.GetType()); + public static bool TryGet(Type type, out ItemAbility customAbility) => customAbility = Get(type); /// public static new bool Add(Item entity, out TAbility param) where TAbility : ItemAbility => CustomAbility.Add(entity, out param); /// - public static new bool Add(Item entity, Type type) => CustomAbility.Add(entity, type) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Item entity, Type type) => CustomAbility.Add(entity, type) && Tracker.AddOrTrack(entity); /// - public static new bool Add(Item entity, string name) => CustomAbility.Add(entity, name) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Item entity, string name) => CustomAbility.Add(entity, name) && Tracker.AddOrTrack(entity); /// - public static new bool Add(Item entity, uint id) => CustomAbility.Add(entity, id) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Item entity, uint id) => CustomAbility.Add(entity, id) && Tracker.AddOrTrack(entity); /// public static new void Add(Item entity, IEnumerable types) { CustomAbility.Add(entity, types); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// public static new void Add(Item entity, IEnumerable names) { CustomAbility.Add(entity, names); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// @@ -146,7 +151,7 @@ public abstract class ItemAbility : CustomAbility if (!entity.TryGetComponent(itemAbility.BehaviourComponent, out EActor component) || component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, type); } @@ -158,7 +163,7 @@ public abstract class ItemAbility : CustomAbility component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, name); } @@ -170,14 +175,14 @@ public abstract class ItemAbility : CustomAbility component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, id); } /// public static new void RemoveAll(Item entity) { - StaticActor.Get().Remove(entity); + Tracker.Remove(entity); CustomAbility.RemoveAll(entity); } @@ -213,7 +218,7 @@ public abstract class ItemAbility : CustomAbility { base.Add(entity); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// @@ -222,7 +227,7 @@ public abstract class ItemAbility : CustomAbility if (!entity.TryGetComponent(BehaviourComponent, out EActor component) || component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return base.Remove(entity); } diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Pickup/PickupAbility.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Pickup/PickupAbility.cs index eb36301e68..6ed3543cba 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Pickup/PickupAbility.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Pickup/PickupAbility.cs @@ -8,7 +8,6 @@ namespace Exiled.CustomModules.API.Features.PickupAbilities { using System; - using System.Collections.Generic; using System.Linq; @@ -38,6 +37,11 @@ public abstract class PickupAbility : CustomAbility /// public static IEnumerable Owners => Manager.Keys.ToHashSet(); + /// + /// Gets the . + /// + protected static TrackerBase Tracker { get; } = TrackerBase.Get(); + /// /// Gets a given the specified id. /// @@ -104,33 +108,33 @@ public abstract class PickupAbility : CustomAbility /// The type to search for. /// The found , if not registered. /// if a was found; otherwise, . - public static bool TryGet(Type type, out PickupAbility customAbility) => customAbility = Get(type.GetType()); + public static bool TryGet(Type type, out PickupAbility customAbility) => customAbility = Get(type); /// public static new bool Add(Pickup entity, out TAbility param) where TAbility : PickupAbility => CustomAbility.Add(entity, out param); /// - public static new bool Add(Pickup entity, Type type) => CustomAbility.Add(entity, type) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Pickup entity, Type type) => CustomAbility.Add(entity, type) && Tracker.AddOrTrack(entity); /// - public static new bool Add(Pickup entity, string name) => CustomAbility.Add(entity, name) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Pickup entity, string name) => CustomAbility.Add(entity, name) && Tracker.AddOrTrack(entity); /// - public static new bool Add(Pickup entity, uint id) => CustomAbility.Add(entity, id) && StaticActor.Get().AddOrTrack(entity); + public static new bool Add(Pickup entity, uint id) => CustomAbility.Add(entity, id) && Tracker.AddOrTrack(entity); /// public static new void Add(Pickup entity, IEnumerable types) { CustomAbility.Add(entity, types); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// public static new void Add(Pickup entity, IEnumerable names) { CustomAbility.Add(entity, names); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// @@ -147,7 +151,7 @@ public abstract class PickupAbility : CustomAbility if (!entity.TryGetComponent(pickupAbility.BehaviourComponent, out EActor component) || component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, type); } @@ -159,7 +163,7 @@ public abstract class PickupAbility : CustomAbility component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, name); } @@ -171,14 +175,14 @@ public abstract class PickupAbility : CustomAbility component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return CustomAbility.Remove(entity, id); } /// public static new void RemoveAll(Pickup entity) { - StaticActor.Get().Remove(entity); + Tracker.Remove(entity); CustomAbility.RemoveAll(entity); } @@ -214,7 +218,7 @@ public abstract class PickupAbility : CustomAbility { base.Add(entity); - StaticActor.Get().AddOrTrack(entity); + Tracker.AddOrTrack(entity); } /// @@ -223,7 +227,7 @@ public abstract class PickupAbility : CustomAbility if (!entity.TryGetComponent(BehaviourComponent, out EActor component) || component is not IAbilityBehaviour behaviour) return false; - StaticActor.Get().Remove(entity, behaviour); + Tracker.Remove(entity, behaviour); return base.Remove(entity); } diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Player/Scp079AbilityBehaviour.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Player/Scp079AbilityBehaviour.cs index a22cd726d0..e122277928 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Player/Scp079AbilityBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Player/Scp079AbilityBehaviour.cs @@ -50,7 +50,7 @@ public abstract class Scp079AbilityBehaviour : UnlockableAbilityBehaviour, ISele /// Gets or sets the which handles all the delegates fired before SCP-079 gains experience. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnGainedExperienceDispatcher { get; set; } + public TDynamicEventDispatcher OnGainedExperienceDispatcher { get; set; } = new(); /// protected override void FindOwner() diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/AbilitySettings.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/AbilitySettings.cs index f702028edd..f1435d11b4 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/AbilitySettings.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/AbilitySettings.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities.Settings { using Exiled.API.Features.Core; using Exiled.API.Features.Core.Interfaces; + using YamlDotNet.Serialization; /// /// Represents the base class for player-specific ability behaviors. @@ -18,6 +19,7 @@ public class AbilitySettings : TypeCastObject, IAdditivePropert /// /// Gets the default values. /// + [YamlIgnore] public static AbilitySettings Default { get; } = new(); } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/ActiveAbilitySettings.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/ActiveAbilitySettings.cs index 23b248e255..1a4935471a 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/ActiveAbilitySettings.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/ActiveAbilitySettings.cs @@ -7,6 +7,8 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities.Settings { + using System.ComponentModel; + using Exiled.API.Features; /// @@ -17,51 +19,61 @@ public class ActiveAbilitySettings : AbilitySettings /// /// Gets or sets a value indicating whether should be used with this ability. /// + [Description("Indicates whether the AbilityInputComponent should be used with this ability.")] public virtual bool UseAbilityInputComponent { get; set; } /// /// Gets or sets the required cooldown before using the ability again. /// + [Description("The required cooldown time in seconds before the ability can be used again.")] public virtual float Cooldown { get; set; } /// /// Gets or sets a value indicating whether the cooldown should be forced when the ability is added, making it already usable. /// + [Description("Indicates whether the cooldown should be forced when the ability is added, making it immediately usable.")] public virtual bool ForceCooldownOnAdded { get; set; } /// /// Gets or sets the time to wait before the ability is activated. /// + [Description("The time to wait before the ability is activated.")] public virtual float WindupTime { get; set; } /// /// Gets or sets the duration of the ability. /// + [Description("The duration of the ability's effect.")] public virtual float Duration { get; set; } /// /// Gets or sets the message to display when the ability is used. /// + [Description("The message to display when the ability is activated.")] public virtual TextDisplay Activated { get; set; } /// - /// Gets or sets the message to display when the ability activation is denied regardless any conditions. + /// Gets or sets the message to display when the ability activation is denied regardless of any conditions. /// + [Description("The message to display when the ability activation is denied regardless of any conditions.")] public virtual TextDisplay CannotBeUsed { get; set; } /// - /// Gets or sets the message to display when the ability activation in on cooldown. + /// Gets or sets the message to display when the ability activation is on cooldown. /// + [Description("The message to display when the ability is on cooldown.")] public virtual TextDisplay OnCooldown { get; set; } /// /// Gets or sets the message to display when the ability is expired. /// + [Description("The message to display when the ability is expired.")] public virtual TextDisplay Expired { get; set; } /// /// Gets or sets the message to display when the ability is ready. /// + [Description("The message to display when the ability is ready for use.")] public virtual TextDisplay OnReady { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/LevelAbilitySettings.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/LevelAbilitySettings.cs index 9e0f9ea6f0..8016d1fe39 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/LevelAbilitySettings.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/LevelAbilitySettings.cs @@ -7,6 +7,8 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities.Settings { + using System.ComponentModel; + using Exiled.API.Features; /// @@ -17,26 +19,31 @@ public class LevelAbilitySettings : ActiveAbilitySettings /// /// Gets or sets the default level the ability should start from. /// + [Description("The default level at which the ability starts.")] public virtual byte DefaultLevel { get; set; } /// - /// Gets or sets the maxiumum level the ability cannot exceed. + /// Gets or sets the maximum level the ability cannot exceed. /// + [Description("The highest level that the ability can reach.")] public virtual byte MaxLevel { get; set; } /// /// Gets or sets the message to display when the ability returns to a previous level. /// + [Description("The message shown when the ability reverts to a previous level.")] public virtual TextDisplay PreviousLevel { get; set; } /// /// Gets or sets the message to display when the ability reaches a new level. /// + [Description("The message shown when the ability advances to a new level.")] public virtual TextDisplay NextLevel { get; set; } /// - /// Gets or sets the message to display when the ability reached the maximum level. + /// Gets or sets the message to display when the ability has reached the maximum level. /// + [Description("The message shown when the ability reaches its maximum level.")] public virtual TextDisplay MaxLevelReached { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/UnlockableAbilitySettings.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/UnlockableAbilitySettings.cs index 25cb542429..b83ea095ab 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Settings/UnlockableAbilitySettings.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Settings/UnlockableAbilitySettings.cs @@ -7,6 +7,8 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities.Settings { + using System.ComponentModel; + using Exiled.API.Features; /// @@ -17,6 +19,7 @@ public class UnlockableAbilitySettings : LevelAbilitySettings /// /// Gets or sets the message to display when the ability is unlocked. /// + [Description("The message to display when the ability is unlocked.")] public virtual TextDisplay Unlocked { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Types/AbilityBehaviourBase.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Types/AbilityBehaviourBase.cs index ff3e6acda8..ffa2c5ca2b 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Types/AbilityBehaviourBase.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Types/AbilityBehaviourBase.cs @@ -14,6 +14,7 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities using Exiled.API.Features.Core.Interfaces; using Exiled.API.Features.DynamicEvents; using Exiled.CustomModules.API.Features.CustomAbilities.Settings; + using Exiled.CustomModules.API.Features.Generic; /// /// Represents the base class for ability behaviors associated with a specific entity type. @@ -22,6 +23,8 @@ namespace Exiled.CustomModules.API.Features.CustomAbilities public abstract class AbilityBehaviourBase : ModuleBehaviour, IAbilityBehaviour, IAdditiveSettings where TEntity : GameEntity { + private AbilitySettings settings; + /// /// Gets the relative . /// @@ -36,35 +39,32 @@ public abstract class AbilityBehaviourBase : ModuleBehaviour, /// /// Gets or sets the . /// - public AbilitySettings Settings { get; set; } + public AbilitySettings Settings + { + get => settings ??= CustomAbility.Settings; + set => settings = value; + } /// - public virtual void AdjustAdditivePipe() + public override ModulePointer Config { - ImplementConfigs(); + get => config ??= CustomAbility.Config; + protected set => config = value; + } - if (CustomAbility.TryGet(GetType(), out CustomAbility customAbility) && - customAbility.Settings is AbilitySettings settings) - { + /// + public virtual void AdjustAdditivePipe() + { + if (CustomAbility.TryGet(GetType(), out CustomAbility customAbility)) CustomAbility = customAbility; - if (Config is null) - Settings = settings; - } - - if (customAbility is null || Settings is null) + if (CustomAbility is null || Settings is null || Config is null) { Log.Error($"Custom ability ({GetType().Name}) has invalid configuration."); Destroy(); } - } - /// - protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue( - typeof(AbilitySettings).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : this, - propertyInfo.GetValue(Config, null)); + ImplementConfigs(); } /// diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Types/ActiveAbilityBehaviour.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Types/ActiveAbilityBehaviour.cs index d4201fbea5..2d32a2cf52 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Types/ActiveAbilityBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Types/ActiveAbilityBehaviour.cs @@ -32,26 +32,26 @@ public abstract class ActiveAbilityBehaviour : AbilityBehaviourBase activates the ability. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnActivatingDispatcher { get; set; } + public TDynamicEventDispatcher OnActivatingDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after a /// activates the ability. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnActivatedDispatcher { get; set; } + public TDynamicEventDispatcher OnActivatedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after the ability expires. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnExpiredDispatcher { get; set; } + public TDynamicEventDispatcher OnExpiredDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after the ability is ready. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnReadyDispatcher { get; set; } + public TDynamicEventDispatcher OnReadyDispatcher { get; set; } = new(); /// /// Gets or sets the . diff --git a/Exiled.CustomModules/API/Features/CustomAbilities/Types/LevelAbilityBehaviour.cs b/Exiled.CustomModules/API/Features/CustomAbilities/Types/LevelAbilityBehaviour.cs index ec15e21272..b969704d3d 100644 --- a/Exiled.CustomModules/API/Features/CustomAbilities/Types/LevelAbilityBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomAbilities/Types/LevelAbilityBehaviour.cs @@ -26,25 +26,25 @@ public abstract class LevelAbilityBehaviour : ActiveAbilityBehaviour which handles all the delegates fired after the ability's level is changed. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnLevelChangedDispatcher { get; set; } + public TDynamicEventDispatcher OnLevelChangedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after the ability's level is added. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnLevelAddedDispatcher { get; set; } + public TDynamicEventDispatcher OnLevelAddedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after the ability's level is removed. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnLevelRemovedDispatcher { get; set; } + public TDynamicEventDispatcher OnLevelRemovedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired after the ability's max level has been reached. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OnMaxLevelReachedDispatcher { get; set; } + public TDynamicEventDispatcher OnMaxLevelReachedDispatcher { get; set; } = new(); /// /// Gets or sets the . diff --git a/Exiled.CustomModules/API/Features/CustomEscapes/CustomEscape.cs b/Exiled.CustomModules/API/Features/CustomEscapes/CustomEscape.cs index 6ae1d0763c..0c2ddb65ab 100644 --- a/Exiled.CustomModules/API/Features/CustomEscapes/CustomEscape.cs +++ b/Exiled.CustomModules/API/Features/CustomEscapes/CustomEscape.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomEscapes { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reflection; @@ -63,35 +64,40 @@ public abstract class CustomEscape : CustomModule, IAdditiveBehaviour [YamlIgnore] public override ModulePointer Config { get; set; } + /// + /// Gets the 's . + /// + [YamlIgnore] + public virtual Type BehaviourComponent { get; } + /// /// Gets or sets the 's name. /// + [Description("The name of the custom escape.")] public override string Name { get; set; } /// - /// Gets or sets or sets the 's id. + /// Gets or sets the 's id. /// + [Description("The id of the custom escape.")] public override uint Id { get; set; } /// /// Gets or sets a value indicating whether the is enabled. /// + [Description("Indicates whether the custom escape is enabled.")] public override bool IsEnabled { get; set; } /// - /// Gets the 's . - /// - [YamlIgnore] - public virtual Type BehaviourComponent { get; } - - /// - /// Gets or sets all 's to be displayed based on the relative . + /// Gets or sets all s to be displayed based on the relative . /// + [Description("A dictionary of hints to be displayed based on the scenario type.")] public virtual Dictionary Scenarios { get; set; } = new(); /// /// Gets or sets a of containing all escape settings. /// + [Description("A list of escape settings, including the default settings.")] public virtual List Settings { get; set; } = new() { EscapeSettings.Default, }; /// @@ -181,16 +187,15 @@ public static CustomEscape Get(Type type) => /// /// Enables all the custom escapes present in the assembly. /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom escapes present in the assembly. - /// - /// The assembly to enable the escapes from. - public static void EnableAll(Assembly assembly) + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. + /// + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. + /// + public static int EnableAll(Assembly assembly = null) { - if (!CustomModules.Instance.Config.Modules.Contains(UUModuleType.CustomEscapes.Name)) - throw new Exception("ModuleType::CustomEscapes must be enabled in order to load any custom escapes"); + assembly ??= Assembly.GetCallingAssembly(); List customEscapes = new(); foreach (Type type in assembly.GetTypes()) @@ -209,19 +214,25 @@ public static void EnableAll(Assembly assembly) customEscapes.Add(customEscape); } - if (customEscapes.Count() != List.Count()) - Log.Info($"{customEscapes.Count()} custom escapes have been successfully registered!"); + return customEscapes.Count; } /// /// Disables all the custom escapes present in the assembly. /// - public static void DisableAll() + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. + /// + /// This method dynamically disables all module instances found in the calling assembly that were + /// previously registered. + /// + public static int DisableAll(Assembly assembly = null) { - List customEscapes = new(); - customEscapes.AddRange(List.Where(customEscape => customEscape.TryUnregister())); + assembly ??= Assembly.GetCallingAssembly(); - Log.Info($"{customEscapes.Count()} custom escapes have been successfully unregistered!"); + List customEscapes = new(); + customEscapes.AddRange(List.Where(customEscape => customEscape.GetType().Assembly == assembly && customEscape.TryUnregister())); + return customEscapes.Count; } /// diff --git a/Exiled.CustomModules/API/Features/CustomEscapes/EscapeBehaviour.cs b/Exiled.CustomModules/API/Features/CustomEscapes/EscapeBehaviour.cs index 527d5bd1dd..35328e94a4 100644 --- a/Exiled.CustomModules/API/Features/CustomEscapes/EscapeBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomEscapes/EscapeBehaviour.cs @@ -17,17 +17,20 @@ namespace Exiled.CustomModules.API.Features.CustomEscapes using Exiled.API.Features.DynamicEvents; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.CustomModules.API.Features.Generic; using PlayerRoles; /// /// Represents the base class for custom escape behaviors. /// /// - /// This class extends and implements . + /// This class extends and implements . ///
It serves as the foundation for creating custom escape behaviors associated with in-game player actions. ///
public abstract class EscapeBehaviour : ModuleBehaviour, IAdditiveSettingsCollection { + private List settings; + /// /// Gets the relative . /// @@ -36,7 +39,18 @@ public abstract class EscapeBehaviour : ModuleBehaviour, IAdditiveSettin /// /// Gets or sets a of containing all escape's settings. /// - public virtual List Settings { get; set; } + public virtual List Settings + { + get => settings ??= CustomEscape.Settings; + set => settings = value; + } + + /// + public override ModulePointer Config + { + get => config ??= CustomEscape.Config; + protected set => config = value; + } /// /// Gets the current escape scenario. @@ -47,41 +61,33 @@ public abstract class EscapeBehaviour : ModuleBehaviour, IAdditiveSettin /// Gets or sets the handling all bound delegates to be fired before escaping. /// [DynamicEventDispatcher] - protected TDynamicEventDispatcher EscapingDispatcher { get; set; } + protected TDynamicEventDispatcher EscapingDispatcher { get; set; } = new(); /// /// Gets or sets the handling all bound delegates to be fired after escaping. /// [DynamicEventDispatcher] - protected TDynamicEventDispatcher EscapedDispatcher { get; set; } + protected TDynamicEventDispatcher EscapedDispatcher { get; set; } = new(); /// public virtual void AdjustAdditivePipe() { - ImplementConfigs(); - CustomRole customRole = Owner.Cast().CustomRole; - if (CustomEscape.TryGet(GetType(), out CustomEscape customEscape) && customEscape.Settings is List settings) + + if (CustomEscape.TryGet(GetType(), out CustomEscape customEscape)) { CustomEscape = customEscape; - - if (Config is null) - Settings = customRole && !customRole.EscapeSettings.IsEmpty() ? customRole.EscapeSettings : settings; + Settings = customRole is not null && !customRole.EscapeSettings.IsEmpty() ? + customRole.EscapeSettings : CustomEscape.Settings; } - if (CustomEscape is null || Settings is null) + if (CustomEscape is null || Settings is null || Config is null) { Log.Error($"Custom escape ({GetType().Name}) has invalid configuration."); Destroy(); } - } - /// - protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue( - typeof(List).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : this, - propertyInfo.GetValue(Config, null)); + ImplementConfigs(); } /// diff --git a/Exiled.CustomModules/API/Features/CustomGamemodes/CustomGameMode.cs b/Exiled.CustomModules/API/Features/CustomGamemodes/CustomGameMode.cs index 314c0c10e0..11a39471b7 100644 --- a/Exiled.CustomModules/API/Features/CustomGamemodes/CustomGameMode.cs +++ b/Exiled.CustomModules/API/Features/CustomGamemodes/CustomGameMode.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomGameModes { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reflection; @@ -74,28 +75,38 @@ public abstract class CustomGameMode : CustomModule, IAdditiveBehaviours [YamlIgnore] public static IEnumerable List => Registered; - /// - [YamlIgnore] - public override ModulePointer Config { get; set; } - - /// + /// + /// Gets or sets the name. + /// + [Description("The name of the game mode.")] public override string Name { get; set; } - /// + /// + /// Gets or sets the 's id. + /// + [Description("The id of the game mode.")] public override uint Id { get; set; } - /// + /// + /// Gets or sets a value indicating whether the is enabled. + /// + [Description("Indicates whether the game mode is enabled.")] public override bool IsEnabled { get; set; } - /// - [YamlIgnore] - public virtual Type[] BehaviourComponents { get; } - /// /// Gets or sets the . /// + [Description("The settings for the game mode.")] public virtual GameModeSettings Settings { get; set; } + /// + [YamlIgnore] + public override ModulePointer Config { get; set; } + + /// + [YamlIgnore] + public virtual Type[] BehaviourComponents { get; } + /// /// Gets a value indicating whether the game mode can start automatically based on the configured probability, if automatic. /// @@ -203,16 +214,15 @@ public static CustomGameMode Get(Type type) => /// /// Enables all the custom game modes present in the assembly. /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom game modes present in the assembly. - /// - /// The assembly to enable the game modes from. - public static void EnableAll(Assembly assembly) + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. + /// + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. + /// + public static int EnableAll(Assembly assembly = null) { - if (!CustomModules.Instance.Config.Modules.Contains(UUModuleType.CustomGameModes.Name)) - throw new Exception("ModuleType::CustomGameModes must be enabled in order to load any custom game modes"); + assembly ??= Assembly.GetCallingAssembly(); List customGameModes = new(); foreach (Type type in assembly.GetTypes()) @@ -238,19 +248,25 @@ public static void EnableAll(Assembly assembly) customGameModes.Add(customGameMode); } - if (customGameModes.Count() != Registered.Count) - Log.Info($"{customGameModes.Count()} custom game modes have been successfully registered!"); + return customGameModes.Count; } /// /// Disables all the custom game modes present in the assembly. /// - public static void DisableAll() + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. + /// + /// This method dynamically disables all module instances found in the calling assembly that were + /// previously registered. + /// + public static int DisableAll(Assembly assembly = null) { - List customGameModes = new(); - customGameModes.AddRange(List.Where(customGameMode => customGameMode.TryUnregister())); + assembly ??= Assembly.GetCallingAssembly(); - Log.Info($"{customGameModes.Count()} custom game modes have been successfully unregistered!"); + List customGameModes = new(); + customGameModes.AddRange(List.Where(customGameMode => customGameMode.GetType().Assembly == assembly && customGameMode.TryUnregister())); + return customGameModes.Count; } /// diff --git a/Exiled.CustomModules/API/Features/CustomGamemodes/GameModeSettings.cs b/Exiled.CustomModules/API/Features/CustomGamemodes/GameModeSettings.cs index 0f562870d0..de338dedd6 100644 --- a/Exiled.CustomModules/API/Features/CustomGamemodes/GameModeSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomGamemodes/GameModeSettings.cs @@ -7,6 +7,8 @@ namespace Exiled.CustomModules.API.Features.CustomGameModes { + using System.ComponentModel; + using Exiled.API.Enums; using Exiled.API.Features.Core; using Exiled.API.Features.Core.Interfaces; @@ -27,6 +29,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// The game mode will start automatically if the specified probability condition is met and the minimum player requirement () is satisfied. /// + [Description("Indicates whether the game mode operates automatically, managed by the World.")] public virtual bool Automatic { get; set; } /// @@ -37,91 +40,109 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// If the specified probability condition is met and the minimum player requirement () is satisfied, the game mode will activate automatically. /// + [Description("The probability condition for automatic activation of the game mode.")] public virtual float AutomaticProbability { get; set; } /// /// Gets or sets the minimum amount of players to start the game mode. /// + [Description("The minimum number of players required to start the game mode.")] public virtual uint MinimumPlayers { get; set; } /// /// Gets or sets the maximum allowed amount of players managed by the game mode. /// + [Description("The maximum number of players that the game mode can manage.")] public virtual uint MaximumPlayers { get; set; } /// /// Gets or sets a value indicating whether the exceeding players should be rejected. /// + [Description("Indicates whether players exceeding the maximum allowed number should be rejected.")] public virtual bool RejectExceedingPlayers { get; set; } /// /// Gets or sets the message to be displayed when a player is rejected due to exceeding amount of players. /// + [Description("The message displayed when a player is rejected due to exceeding the allowed player count.")] public virtual string RejectExceedingMessage { get; set; } /// /// Gets or sets a value indicating whether the players can respawn. /// + [Description("Indicates whether players are allowed to respawn.")] public virtual bool IsRespawnEnabled { get; set; } /// /// Gets or sets the respawn time for individual players. /// + [Description("The respawn time for individual players.")] public virtual float RespawnTime { get; set; } /// /// Gets or sets a value indicating whether teams can regularly respawn. /// + [Description("Indicates whether teams are allowed to respawn regularly.")] public virtual bool IsTeamRespawnEnabled { get; set; } /// /// Gets or sets the respawn time for individual teams. /// + [Description("The respawn time for individual teams.")] public virtual int TeamRespawnTime { get; set; } /// /// Gets or sets a value indicating whether custom ending conditions should be used over predefined conditions. /// + [Description("Indicates whether custom ending conditions should override predefined conditions.")] public virtual bool UseCustomEndingConditions { get; set; } /// - /// Gets or sets a value indicating whether server should be restarted when the game mode ends. + /// Gets or sets a value indicating whether the server should be restarted when the game mode ends. /// + [Description("Indicates whether the server should be restarted when the game mode ends.")] public virtual bool RestartRoundOnEnd { get; set; } /// /// Gets or sets the amount of time to await before restarting the server. /// + [Description("The time to wait before restarting the server.")] public virtual float RestartWindupTime { get; set; } /// /// Gets or sets a [] containing all zones that should be permanently locked. /// + [Description("An array of zones that should be permanently locked.")] public virtual ZoneType[] LockedZones { get; set; } /// /// Gets or sets a [] containing all doors that should be permanently locked. /// + [Description("An array of doors that should be permanently locked.")] public virtual DoorType[] LockedDoors { get; set; } /// /// Gets or sets a [] containing all elevators that should be permanently locked. /// + [Description("An array of elevators that should be permanently locked.")] public virtual ElevatorType[] LockedElevators { get; set; } /// /// Gets or sets a value indicating whether the decontamination should be enabled. /// + [Description("Indicates whether decontamination should be enabled.")] public virtual bool IsDecontaminationEnabled { get; set; } /// /// Gets or sets a value indicating whether the Alpha Warhead is enabled. /// + [Description("Indicates whether the Alpha Warhead is enabled.")] public virtual bool IsWarheadEnabled { get; set; } /// /// Gets or sets a value indicating whether the Alpha Warhead interactions are allowed. /// + [Description("Indicates whether interactions with the Alpha Warhead are allowed.")] public virtual bool IsWarheadInteractable { get; set; } /// @@ -129,6 +150,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope /// /// must be set to . /// + [Description("The time in seconds after which the Alpha Warhead will automatically start, if enabled.")] public virtual float AutoWarheadTime { get; set; } /// @@ -138,6 +160,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of spawnable roles. If specified, only these roles can spawn.")] public virtual RoleTypeId[] SpawnableRoles { get; set; } /// @@ -147,6 +170,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of spawnable custom roles. If specified, only these custom roles can spawn.")] public virtual uint[] SpawnableCustomRoles { get; set; } /// @@ -156,6 +180,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of spawnable teams. If specified, only these teams can spawn.")] public virtual SpawnableTeamType[] SpawnableTeams { get; set; } /// @@ -165,6 +190,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of spawnable custom teams. If specified, only these custom teams can spawn.")] public virtual uint[] SpawnableCustomTeams { get; set; } /// @@ -174,6 +200,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of non-spawnable roles. If specified, these roles cannot spawn.")] public virtual RoleTypeId[] NonSpawnableRoles { get; set; } /// @@ -183,6 +210,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of non-spawnable custom roles. If specified, these custom roles cannot spawn.")] public virtual uint[] NonSpawnableCustomRoles { get; set; } /// @@ -192,6 +220,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of non-spawnable custom teams. If specified, these custom teams cannot spawn.")] public virtual SpawnableTeamType[] NonSpawnableTeams { get; set; } /// @@ -201,6 +230,7 @@ public class GameModeSettings : TypeCastObject, IAdditivePrope ///
/// It's highly recommended to not use it along with . ///
+ [Description("An array of non-spawnable custom teams. If specified, these custom teams cannot spawn.")] public virtual uint[] NonSpawnableCustomTeams { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomGamemodes/GameState.cs b/Exiled.CustomModules/API/Features/CustomGamemodes/GameState.cs index c20a6931b1..ac5ca5958d 100644 --- a/Exiled.CustomModules/API/Features/CustomGamemodes/GameState.cs +++ b/Exiled.CustomModules/API/Features/CustomGamemodes/GameState.cs @@ -11,7 +11,6 @@ namespace Exiled.CustomModules.API.Features.CustomGameModes using System.Collections.Generic; using System.Diagnostics; using System.Linq; - using System.Reflection; using Exiled.API.Extensions; using Exiled.API.Features; @@ -19,9 +18,9 @@ namespace Exiled.CustomModules.API.Features.CustomGameModes using Exiled.API.Features.Core.Interfaces; using Exiled.API.Features.Doors; using Exiled.API.Features.Roles; - using Exiled.API.Interfaces; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.CustomModules.API.Features.Generic; using Exiled.CustomModules.Events.EventArgs.CustomRoles; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Server; @@ -44,6 +43,8 @@ public abstract class GameState : EActor, IAdditiveSettings { private readonly List playerStates = new(); private Type cachedPlayerStateType; + private GameModeSettings settings; + private ModulePointer config; /// /// Gets the relative . @@ -53,17 +54,26 @@ public abstract class GameState : EActor, IAdditiveSettings /// /// Gets the associated to the current . /// - public Type PlayerStateComponent => cachedPlayerStateType ??= CustomGameMode.BehaviourComponents.FirstOrDefault(t => typeof(PlayerState).IsAssignableFrom(t)); + public Type PlayerStateComponent => cachedPlayerStateType ??= + CustomGameMode.BehaviourComponents.FirstOrDefault(t => typeof(PlayerState).IsAssignableFrom(t)); /// /// Gets or sets the settings associated with the custom game mode. /// - public GameModeSettings Settings { get; set; } + public GameModeSettings Settings + { + get => settings ??= CustomGameMode.Settings; + set => settings = value; + } /// - /// Gets or sets the 's config. + /// Gets or sets the game state's configs. /// - public EConfig Config { get; set; } + public virtual ModulePointer Config + { + get => config ??= CustomGameMode.Config; + protected set => config = value; + } /// /// Gets all instances. @@ -93,27 +103,12 @@ public abstract class GameState : EActor, IAdditiveSettings /// public virtual void AdjustAdditivePipe() { - if (Config is not null && Config.GetType().GetInterfaces().Contains(typeof(IConfig))) - { - Type inType = GetType(); - foreach (PropertyInfo propertyInfo in Config.GetType().GetProperties()) - { - PropertyInfo targetInfo = inType.GetProperty(propertyInfo.Name); - targetInfo?.SetValue( - typeof(GameModeSettings).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : this, - propertyInfo.GetValue(Config, null)); - } - } - - if (CustomGameMode.TryGet(GetType(), out CustomGameMode customGameMode) && customGameMode.Settings is GameModeSettings settings) - { + if (CustomGameMode.TryGet(GetType(), out CustomGameMode customGameMode)) CustomGameMode = customGameMode; - if (Config is null) - Settings = settings; - } + ModuleBehaviour.ImplementConfigs_DefaultImplementation(this, Config); - if (CustomGameMode is null || Settings is null) + if (CustomGameMode is null || Settings is null || Config is null) { Log.Error($"Custom game mode ({GetType().Name}) has invalid configuration."); Destroy(); diff --git a/Exiled.CustomModules/API/Features/CustomGamemodes/PlayerState.cs b/Exiled.CustomModules/API/Features/CustomGamemodes/PlayerState.cs index c7a939267d..3aa71cfa78 100644 --- a/Exiled.CustomModules/API/Features/CustomGamemodes/PlayerState.cs +++ b/Exiled.CustomModules/API/Features/CustomGamemodes/PlayerState.cs @@ -9,17 +9,20 @@ namespace Exiled.CustomModules.API.Features.CustomGameModes { using System; using System.Diagnostics; + using System.IO; using Exiled.API.Features; using Exiled.API.Features.Attributes; using Exiled.API.Features.DynamicEvents; + using Exiled.CustomModules.API.Features.Generic; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Warhead; using MEC; using PlayerRoles; + using YamlDotNet.Serialization; /// - /// Represents the state of an individual player within the custom game mode, derived from . + /// Represents the state of an individual player within the custom game mode, derived from . /// /// /// @@ -38,23 +41,36 @@ public abstract class PlayerState : ModuleBehaviour /// Gets or sets the which handles all delegates to be fired after the has been deployed. /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher DeployedDispatcher { get; set; } + public static TDynamicEventDispatcher DeployedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all delegates to be fired after the has been activated. /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher ActivatedDispatcher { get; set; } + public static TDynamicEventDispatcher ActivatedDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all delegates to be fired after the has been deactivated. /// [DynamicEventDispatcher] - public static TDynamicEventDispatcher DeactivatedDispatcher { get; set; } + public static TDynamicEventDispatcher DeactivatedDispatcher { get; set; } = new(); + + /// + /// Gets or sets a value indicating whether the is respawnable. + /// + public virtual bool IsRespawnable { get; set; } + + /// + /// Gets the . + /// + [YamlIgnore] + public virtual new ConfigSubsystem Config => + ConfigSubsystem.LoadDynamic(GetType(), World.Get().GameState.CustomGameMode.ChildPath, $"{GetType().Name}-Config"); /// /// Gets or sets a value indicating whether the can behave regularly. /// + [YamlIgnore] public virtual bool IsActive { get => isActive; @@ -75,34 +91,34 @@ public virtual bool IsActive } } - /// - /// Gets or sets a value indicating whether the is respawnable. - /// - public virtual bool IsRespawnable { get; set; } - /// /// Gets the respawn time for individual players. /// + [YamlIgnore] public float RespawnTime => World.Get().GameState.Settings.RespawnTime; /// /// Gets or sets the score. /// + [YamlIgnore] public int Score { get; protected set; } /// /// Gets or sets the last time the player died. /// + [YamlIgnore] public DateTime LastDeath { get; protected set; } /// /// Gets a value indicating whether the player is ready to respawn. /// + [YamlIgnore] public virtual bool CanRespawn => DateTime.Now > LastDeath + TimeSpan.FromSeconds(RespawnTime); /// /// Gets or sets the remaining respawn time. /// + [YamlIgnore] public virtual float RemainingRespawnTime { get => (float)Math.Max(0f, (LastDeath + TimeSpan.FromSeconds(RespawnTime) - DateTime.Now).TotalSeconds); @@ -166,6 +182,8 @@ protected override void PostInitialize() { base.PostInitialize(); + ImplementConfigs_DefaultImplementation(this, Config); + World.Get().GameState.AddPlayerState(this); } diff --git a/Exiled.CustomModules/API/Features/CustomItems/CustomItem.cs b/Exiled.CustomModules/API/Features/CustomItems/CustomItem.cs index 6733756142..bde381789f 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/CustomItem.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/CustomItem.cs @@ -9,6 +9,8 @@ namespace Exiled.CustomModules.API.Features.CustomItems { using System; using System.Collections.Generic; + using System.ComponentModel; + using System.IO; using System.Linq; using System.Reflection; @@ -20,10 +22,13 @@ namespace Exiled.CustomModules.API.Features.CustomItems using Exiled.API.Features.Items; using Exiled.API.Features.Lockers; using Exiled.API.Features.Pickups; + using Exiled.API.Features.Serialization.CustomConverters; using Exiled.API.Features.Spawn; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features.Attributes; using Exiled.CustomModules.API.Features.CustomItems.Items; + using Exiled.CustomModules.API.Features.CustomItems.Pickups; + using MEC; using UnityEngine; using YamlDotNet.Serialization; @@ -39,13 +44,7 @@ public abstract class CustomItem : CustomModule, IAdditiveBehaviour private static readonly Dictionary BehaviourLookupTable = new(); private static readonly Dictionary IdLookupTable = new(); private static readonly Dictionary NameLookupTable = new(); - - /// - /// Gets all tracked behaviours. - /// -#pragma warning disable SA1202 // Elements should be ordered by access - internal static readonly Dictionary TrackedBehaviours = new(); -#pragma warning restore SA1202 // Elements should be ordered by access + private static readonly Dictionary SettingsTypeLookupTable = new(); /// /// Gets a which contains all registered 's. @@ -90,37 +89,44 @@ public abstract class CustomItem : CustomModule, IAdditiveBehaviour /// /// Gets or sets the 's name. /// + [Description("The name of the custom item.")] public override string Name { get; set; } /// /// Gets or sets the 's id. /// + [Description("The id of the custom item.")] public override uint Id { get; set; } /// - /// Gets or sets a value indicating whether the is enabled. + /// Gets or sets the 's description. /// - public override bool IsEnabled { get; set; } + [Description("The description of the custom item.")] + public virtual string Description { get; set; } /// - /// Gets or sets the 's description. + /// Gets or sets a value indicating whether the is enabled. /// - public virtual string Description { get; set; } + [Description("Indicates whether the custom item is enabled.")] + public override bool IsEnabled { get; set; } /// /// Gets or sets the 's . /// + [Description("The type of the custom item.")] public virtual ItemType ItemType { get; set; } /// /// Gets or sets the 's . /// + [Description("The category of the custom item.")] public virtual ItemCategory ItemCategory { get; set; } /// /// Gets or sets the . /// - public virtual Settings Settings { get; set; } + [Description("The settings of the custom item.")] + public virtual SettingsBase Settings { get; set; } /// /// Gets a value indicating whether the is registered. @@ -140,6 +146,14 @@ public abstract class CustomItem : CustomModule, IAdditiveBehaviour [YamlIgnore] public IEnumerable Items => ItemsValue.Where(x => x.Value.Id == Id).Select(x => x.Key); + /// + /// Gets the deserializer specifically made for custom items. + /// + protected static IDeserializer CustomItemDeserializer { get; } = + ConfigSubsystem.GetDefaultDeserializerBuilder() + .WithTypeConverter(new DynamicTypeConverter()) + .Build(); + /// /// Gets a based on the provided id or . /// @@ -168,7 +182,7 @@ public abstract class CustomItem : CustomModule, IAdditiveBehaviour /// The retrieved instance if found and enabled; otherwise, . public static CustomItem Get(Type type) => typeof(CustomItem).IsAssignableFrom(type) ? TypeLookupTable[type] : - typeof(ItemBehaviour).IsAssignableFrom(type) ? BehaviourLookupTable[type] : null; + typeof(ICustomItemBehaviour).IsAssignableFrom(type) ? BehaviourLookupTable[type] : null; /// /// Retrieves a instance based on the specified instance. @@ -393,24 +407,16 @@ public static bool TryGive(Player player, Type type, bool displayMessage = true) /// /// Enables all the custom items present in the assembly. /// + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. /// - /// This method dynamically enables all custom items found in the calling assembly. Custom items - /// must be marked with the to be considered for enabling. - /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom items present in the assembly. - /// - /// The assembly to enable the items from. - /// - /// This method dynamically enables all custom items found in the calling assembly. Custom items - /// must be marked with the to be considered for enabling. + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. /// - public static void EnableAll(Assembly assembly) + public static int EnableAll(Assembly assembly = null) { - if (!CustomModules.Instance.Config.Modules.Contains(UUModuleType.CustomItems.Name)) - throw new Exception("ModuleType::CustomItems must be enabled in order to load any custom items"); + assembly ??= Assembly.GetCallingAssembly(); + AddSettingsDerivedTypes(assembly); List customItems = new(); foreach (Type type in assembly.GetTypes()) @@ -429,23 +435,25 @@ public static void EnableAll(Assembly assembly) customItems.Add(customItem); } - if (customItems.Count != Registered.Count) - Log.Info($"{customItems.Count} custom items have been successfully registered!"); + return customItems.Count; } /// /// Disables all the custom items present in the assembly. /// + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. /// - /// This method dynamically disables all custom items found in the calling assembly that were + /// This method dynamically disables all module instances found in the calling assembly that were /// previously registered. /// - public static void DisableAll() + public static int DisableAll(Assembly assembly = null) { - List customItems = new(); - customItems.AddRange(Registered.Where(customItem => customItem.TryUnregister())); + assembly ??= Assembly.GetCallingAssembly(); - Log.Info($"{customItems.Count} custom items have been successfully unregistered!"); + List customItems = new(); + customItems.AddRange(Registered.Where(customItem => customItem.GetType().Assembly == assembly && customItem.TryUnregister())); + return customItems.Count; } /// @@ -501,17 +509,28 @@ public static void DisableAll() /// The of the spawned . public virtual Pickup Spawn(Vector3 position, Item item, Player previousOwner = null) { - item.AddComponent(BehaviourComponent); + TrackerBase tracker = TrackerBase.Get(); + + if (typeof(ItemBehaviour).IsAssignableFrom(BehaviourComponent)) + { + item.AddComponent(BehaviourComponent); + tracker.AddOrTrack(item); + } + Pickup pickup = item.CreatePickup(position); + if (typeof(PickupBehaviour).IsAssignableFrom(BehaviourComponent)) + { + pickup.AddComponent(BehaviourComponent); + tracker.AddOrTrack(pickup); + } - ItemTracker tracker = StaticActor.Get(); - tracker.AddOrTrack(item, pickup); - tracker.Restore(pickup, item); + if (Settings is not SettingsBase settings) + throw new InvalidCastException("Settings is not set to an instance of a derived SettingsBase type."); - pickup.Scale = Settings.Scale; + pickup.Scale = settings.Scale; - if (Settings.Weight != -1) - pickup.Weight = Settings.Weight; + if (settings.Weight != -1f) + pickup.Weight = settings.Weight; if (previousOwner) pickup.PreviousOwner = previousOwner; @@ -581,7 +600,7 @@ public virtual uint Spawn(IEnumerable spawnPoints, uint limit) /// public virtual void SpawnAll() { - if (Settings is null || Settings.SpawnProperties is not SpawnProperties spawnProperties) + if (Settings is null || Settings is not SettingsBase settings || settings.SpawnProperties is not SpawnProperties spawnProperties) return; // This will go over each spawn property type (static, dynamic and role) to try and spawn the item. @@ -609,10 +628,23 @@ public virtual void Give(Player player, Item item, bool displayMessage = true) { try { - item.AddComponent(BehaviourComponent); - StaticActor.Get().AddOrTrack(item); - player.AddItem(item); - ItemsValue.Add(item, this); + if (typeof(ItemBehaviour).IsAssignableFrom(BehaviourComponent)) + { + item.AddComponent(BehaviourComponent); + TrackerBase.Get().AddOrTrack(item); + player.AddItem(item); + ItemsValue.Add(item, this); + return; + } + + if (typeof(PickupBehaviour).IsAssignableFrom(BehaviourComponent)) + { + Pickup pickup = item.CreatePickup(Vector3.zero); + pickup.AddComponent(BehaviourComponent); + TrackerBase.Get().AddOrTrack(pickup); + player.AddItem(pickup); + PickupValue.Add(pickup, this); + } } catch (Exception e) { @@ -643,40 +675,47 @@ public virtual void Give(Player player, Item item, bool displayMessage = true) /// if the was registered; otherwise, . protected override bool TryRegister(Assembly assembly, ModuleIdentifierAttribute attribute = null) { - if (!Registered.Contains(this)) + if (Registered.Contains(this)) { - if (attribute is not null && Id == 0) - { - if (attribute.Id != 0) - Id = attribute.Id; - else - throw new ArgumentException($"Unable to register {Name}. The ID 0 is reserved for special use."); - } + Log.Warn($"Unable to register {Name}. Item already exists."); - CustomItem duplicate = Registered.FirstOrDefault(x => x.Id == Id || x.Name == Name || x.BehaviourComponent == BehaviourComponent); - if (duplicate) - { - Log.Warn($"Unable to register {Name}. Another item with the same ID, Name or Behaviour Component already exists: {duplicate.Name}"); - - return false; - } + return false; + } - EObject.RegisterObjectType(BehaviourComponent, Name, assembly); - Registered.Add(this); + if (!typeof(EActor).IsAssignableFrom(BehaviourComponent) || BehaviourComponent.GetInterfaces() + .All(i => !typeof(ICustomItemBehaviour).IsAssignableFrom(i))) + { + Log.Error($"Unable to register {Name}. Behaviour Component must implement EActor and ICustomItemBehaviour."); + return false; + } - base.TryRegister(assembly, attribute); + if (attribute is not null && Id == 0) + { + if (attribute.Id != 0) + Id = attribute.Id; + else + throw new ArgumentException($"Unable to register {Name}. The ID 0 is reserved for special use."); + } - TypeLookupTable.TryAdd(GetType(), this); - BehaviourLookupTable.TryAdd(BehaviourComponent, this); - IdLookupTable.TryAdd(Id, this); - NameLookupTable.TryAdd(Name, this); + CustomItem duplicate = Registered.FirstOrDefault(x => x.Id == Id || x.Name == Name || x.BehaviourComponent == BehaviourComponent); + if (duplicate) + { + Log.Warn($"Unable to register {Name}. Another item with the same ID, Name or Behaviour Component already exists: {duplicate.Name}"); - return true; + return false; } - Log.Warn($"Unable to register {Name}. Item already exists."); + EObject.RegisterObjectType(BehaviourComponent, Name, assembly); + Registered.Add(this); - return false; + base.TryRegister(assembly, attribute); + + TypeLookupTable.TryAdd(GetType(), this); + BehaviourLookupTable.TryAdd(BehaviourComponent, this); + IdLookupTable.TryAdd(Id, this); + NameLookupTable.TryAdd(Name, this); + + return true; } /// @@ -687,7 +726,7 @@ protected override bool TryUnregister() { if (!Registered.Contains(this)) { - Log.Debug($"Unable to unregister {Name}. Item is not yet registered."); + Log.Warn($"Unable to unregister {Name}. Item is not yet registered."); return false; } @@ -704,5 +743,57 @@ protected override bool TryUnregister() return true; } + + /// + protected override void DeserializeModule_Implementation() => Timing.RunCoroutine(EnqueueDeserialization_Internal()); + + private static void AddSettingsDerivedTypes(Assembly assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); + + foreach (Type t in assembly.GetTypes()) + { + if (t.IsAbstract || !typeof(SettingsBase).IsAssignableFrom(t)) + continue; + + try + { + SettingsTypeLookupTable[t.Name] = t; + } + catch + { + continue; + } + } + } + + private IEnumerator EnqueueDeserialization_Internal() + { + yield return Timing.WaitUntilTrue(() => !SettingsTypeLookupTable.IsEmpty()); + + string settingsPropertyName = nameof(Settings).ToKebabCase(); + string settingsTypePropertyName = nameof(Settings.SettingsType).ToKebabCase(); + + Dictionary deserializedModule = CustomItemDeserializer.Deserialize>(File.ReadAllText(FilePath)); + + object settings = null; + if (deserializedModule.TryGetValue(settingsPropertyName, out object value)) + { + settings = value; + deserializedModule.Remove(settingsPropertyName); + } + + string settingsTypeName = (string)(settings as Dictionary)[settingsTypePropertyName]; + string rawCustomItem = ConfigSubsystem.ConvertDictionaryToYaml(deserializedModule); + string serializedSettings = ConfigSubsystem.Serializer.Serialize(settings); + + CustomItem deserializedCustomItem = ModuleDeserializer.Deserialize(rawCustomItem, GetType()) as CustomItem; + SettingsBase settingsBase = ConfigSubsystem.Deserializer.Deserialize(serializedSettings, SettingsTypeLookupTable[settingsTypeName]) as SettingsBase; + deserializedCustomItem.Settings = settingsBase; + + CopyProperties(deserializedCustomItem); + + Config = ModuleDeserializer.Deserialize(File.ReadAllText(PointerPath), ModulePointer.Get(this, GetType().Assembly).GetType()) as ModulePointer; + } } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Generic/CustomItem.cs b/Exiled.CustomModules/API/Features/CustomItems/Generic/CustomItem.cs index 79f924ab30..36ed130eb3 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Generic/CustomItem.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Generic/CustomItem.cs @@ -13,7 +13,7 @@ namespace Exiled.CustomModules.API.Features.CustomItems /// public abstract class CustomItem : CustomItem - where T : ItemBehaviour + where T : ICustomItemBehaviour { /// public override Type BehaviourComponent => typeof(T); diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemTracker.cs b/Exiled.CustomModules/API/Features/CustomItems/ICustomItemBehaviour.cs similarity index 50% rename from Exiled.CustomModules/API/Features/CustomItems/Items/ItemTracker.cs rename to Exiled.CustomModules/API/Features/CustomItems/ICustomItemBehaviour.cs index 6c0c4cdd9a..9aaf8a1769 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemTracker.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/ICustomItemBehaviour.cs @@ -1,18 +1,18 @@ // ----------------------------------------------------------------------- -// +// // Copyright (c) Exiled Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. // // ----------------------------------------------------------------------- -namespace Exiled.CustomModules.API.Features.CustomItems.Items +namespace Exiled.CustomModules.API.Features.CustomItems { - using Exiled.CustomModules.API.Features.Generic; + using Exiled.CustomModules.API.Interfaces; /// - /// The actor which handles all tracking-related tasks for items. + /// Represents a marker interface for custom item behaviors. /// - public class ItemTracker : TrackerBase + public interface ICustomItemBehaviour : ITrackable { } -} \ No newline at end of file +} diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Armors/ArmorSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Armors/ArmorSettings.cs index e65ae8161a..6fa2f0d680 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Armors/ArmorSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Armors/ArmorSettings.cs @@ -7,46 +7,29 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Armors { - using System; using System.ComponentModel; - using Exiled.API.Extensions; - using Exiled.CustomModules.API.Features.CustomItems.Items; - /// /// A tool to easily setup armors. /// - public class ArmorSettings : ItemSettings + public class ArmorSettings : Settings { - /// - public override ItemType ItemType - { - get => base.ItemType; - set - { - if (!value.IsArmor() && value != ItemType.None) - throw new ArgumentOutOfRangeException($"{nameof(Type)}", value, "Invalid armor type."); - - base.ItemType = value; - } - } - /// /// Gets or sets how much faster stamina will drain when wearing this armor. /// - [Description("The value must be above 1 and below 2")] + [Description("Indicates how much faster stamina will drain when wearing this armor. The value must be above 1 and below 2")] public virtual float StaminaUseMultiplier { get; set; } = 1.15f; /// /// Gets or sets how strong the helmet on the armor is. /// - [Description("The value must be above 0 and below 100")] + [Description("Indicates how strong the helmet on the armor is. The value must be above 0 and below 100")] public virtual int HelmetEfficacy { get; set; } = 80; /// /// Gets or sets how strong the vest on the armor is. /// - [Description("The value must be above 0 and below 100")] + [Description("Indicates how strong the vest on the armor is. The value must be above 0 and below 100")] public virtual int VestEfficacy { get; set; } = 80; } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyBehaviour.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyBehaviour.cs index 2dbb560494..fa783b6faa 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyBehaviour.cs @@ -60,7 +60,7 @@ protected override void PostInitialize() Destroy(); } - CandySettings.SelectedText = new($"Custom candies in this bag:\n{string.Join("\n", TrackedCandies.Select(x => x++))}"); + CandySettings.SelectedText = new($"Custom candies in this bag:\n{string.Join("\n", TrackedCandies.Select(x => ++x))}"); TrackedCandies = new(); } @@ -92,9 +92,9 @@ protected override void UnsubscribeEvents() } /// - protected override void OnAcquired(Player player, Item item, bool displayMessage = true) + protected override void OnAcquired(bool displayMessage = true) { - base.OnAcquired(player, item, displayMessage); + base.OnAcquired(displayMessage); if (Scp330.Candies.Count == 0) { @@ -122,7 +122,7 @@ private protected void OnEatingScp330Internal(EatingScp330EventArgs ev) return; ev.Candy = new BaseCandy(CandySettings, ApplyEffects); - ev.Player.ShowTextDisplay(CandySettings.EatenCustomCandyMessage); + ev.Player.ShowTextDisplay(CandySettings.EatenCustomCandyText); OnEatingCandy(ev); } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandySettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandySettings.cs index 6aa399adb9..7b6474b1c0 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandySettings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandySettings.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) Exiled Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. @@ -7,7 +7,7 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Candies { - using System; + using System.ComponentModel; using Exiled.API.Features; using InventorySystem.Items.Usables.Scp330; @@ -16,29 +16,18 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Candies /// /// A tool to easily setup candies. /// - public class CandySettings : ItemSettings + public class CandySettings : Settings { - /// - public override ItemType ItemType - { - get => base.ItemType; - set - { - if (value != ItemType.SCP330) - throw new ArgumentOutOfRangeException(nameof(Type), value, "ItemType must be ItemType.SCP330"); - - base.ItemType = value; - } - } - /// /// Gets or sets a of a custom candy. /// + [Description("The CandyKindID of a custom candy.")] public virtual CandyKindID CandyType { get; set; } /// - /// Gets or sets chance that player would get a custom candy. + /// Gets or sets the chance of getting a custom candy. /// + [Description("The chance of getting a custom candy.")] public override float Weight { get => base.Weight; @@ -46,16 +35,15 @@ public override float Weight } /// - /// Gets or sets the that will be displayed when player ate custom candy.. + /// Gets or sets the to be displayed when a player ate the custom candy. /// - public virtual TextDisplay EatenCustomCandyMessage { get; set; } + [Description("The TextDisplay to be displayed when a player ate the custom candy.")] + public virtual TextDisplay EatenCustomCandyText { get; set; } /// /// Gets or sets a that will be displayed when player has received custom candy. /// - public virtual TextDisplay ReceivedCustomCandyMessage { get; set; } - - /// - public override TextDisplay SelectedText { get; set; } + [Description("The TextDisplay to be displayed when a player has received a custom candy.")] + public virtual TextDisplay ReceivedCustomCandyText { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyTracker.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyTracker.cs index cc7f2fc394..c88c219490 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyTracker.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/CandyTracker.cs @@ -11,6 +11,7 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Candies using System.Linq; using Exiled.API.Features.Items; + using Exiled.CustomModules.API.Features.Generic; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Scp330; using UnityEngine; @@ -18,7 +19,7 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Candies /// /// A custom tracker for candies. /// - public class CandyTracker : ItemTracker + public class CandyTracker : TrackerBase { /// internal void OnInteractingScp330(InteractingScp330EventArgs ev) diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/Patches/DroppingCandy.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/Patches/DroppingCandy.cs index d0165359ab..c0b807fe79 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/Patches/DroppingCandy.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Candies/Patches/DroppingCandy.cs @@ -53,12 +53,12 @@ private static IEnumerable Transpiler(IEnumerable().AddOrTrack(pickup); + TrackerBase.Get().AddOrTrack(pickup); } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Explosives/GrenadeSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Explosives/GrenadeSettings.cs index 1831e3babf..9cc671c05d 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Explosives/GrenadeSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Explosives/GrenadeSettings.cs @@ -7,37 +7,23 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Explosives { - using System; - - using Exiled.API.Extensions; - using Exiled.CustomModules.API.Features.CustomItems.Items; + using System.ComponentModel; /// /// A tool to easily setup grenades. /// - public class GrenadeSettings : ItemSettings + public class GrenadeSettings : Settings { - /// - public override ItemType ItemType - { - get => base.ItemType; - set - { - if (!value.IsThrowable() && value != ItemType.None) - throw new ArgumentOutOfRangeException($"{nameof(Type)}", value, "Invalid grenade type."); - - base.ItemType = value; - } - } - /// /// Gets or sets a value indicating whether the grenade should explode immediately when contacting any surface. /// + [Description("Indicates whether the grenade should explode immediately when contacting any surface.")] public virtual bool ExplodeOnCollision { get; set; } /// /// Gets or sets a value indicating how long the grenade's fuse time should be. /// + [Description("Indicates how long the grenade's fuse time should be.")] public virtual float FuseTime { get; set; } } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/Firearms/FirearmSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/Firearms/FirearmSettings.cs index 814472836e..c35c89fc96 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/Firearms/FirearmSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/Firearms/FirearmSettings.cs @@ -7,38 +7,23 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items.Firearms { - using System; + using System.ComponentModel; using CameraShaking; - - using Exiled.API.Extensions; using Exiled.CustomModules.API.Enums; - using Exiled.CustomModules.API.Features.CustomItems.Items; using InventorySystem.Items.Firearms.Attachments; /// /// A tool to easily setup firearms. /// - public class FirearmSettings : ItemSettings + public class FirearmSettings : SettingsBase { - /// - public override ItemType ItemType - { - get => base.ItemType; - set - { - if (!value.IsWeapon(false) && value != ItemType.None) - throw new ArgumentOutOfRangeException($"{nameof(Type)}", value, "Invalid weapon type."); - - base.ItemType = value; - } - } - /// /// Gets or sets a value indicating whether the custom reload logic should be used. /// /// It adds support for non- items. /// + [Description("Indicates whether the custom reload logic should be used. It adds support for non-AmmoType items.")] public virtual bool OverrideReload { get; set; } /// @@ -48,6 +33,9 @@ public override ItemType ItemType ///
/// is required to be set to in case of non- items. ///
+ [Description( + "The firearm's ammo type. This property cannot be used along with CustomAmmoType." + + "OverrideReload is required to be set to true in case of not AmmoType items.")] public virtual ItemType AmmoType { get; set; } /// @@ -57,31 +45,37 @@ public override ItemType ItemType ///
/// is required to be set to . ///
+ [Description("The firearm's ammo type. This property cannot be used along with AmmoType. OverrideReload is required to be set to true.")] public virtual uint CustomAmmoType { get; set; } /// /// Gets or sets the firearm's attachments. /// + [Description("The firearm's attachments.")] public virtual AttachmentName[] Attachments { get; set; } = { }; /// /// Gets or sets the . /// + [Description("The firearm's firing mode.")] public virtual FiringMode FiringMode { get; set; } /// /// Gets or sets the firearm's damage. /// + [Description("The firearm's damage.")] public virtual float Damage { get; set; } /// /// Gets or sets the firearm's max ammo. /// + [Description("The firearm's max ammo.")] public virtual byte MaxAmmo { get; set; } /// /// Gets or sets the size of the firearm's clip. /// + [Description("The firearm's clip size.")] public virtual byte ClipSize { get; set; } /// @@ -89,6 +83,7 @@ public override ItemType ItemType /// /// is required to be set to . /// + [Description("The firearm's chamber size. OverrideReload is required to be set to true.")] public virtual int ChamberSize { get; set; } /// @@ -98,6 +93,7 @@ public override ItemType ItemType ///
/// Automatic firearms are not supported. ///
+ [Description("The firearm's fire rate. Only decreasing is supported by non-automatic firearms; automatic firearms are not supported.")] public virtual byte FireRate { get; set; } /// @@ -105,16 +101,19 @@ public override ItemType ItemType /// /// Only firearms with will be affected. /// + [Description("The firearm's burst length. Only firearms with Burst fire mode will be affected.")] public virtual byte BurstLength { get; set; } /// /// Gets or sets the . /// + [Description("The firearm's recoil.")] public virtual RecoilSettings RecoilSettings { get; set; } /// /// Gets or sets a value indicating whether friendly fire is allowed with this firearm on FF-enabled servers. /// + [Description("Indicates whether friendly fire is allowed with this firearm on FF-enabled servers.")] public virtual bool AllowFriendlyFire { get; set; } } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/IItemBehaviour.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/IItemBehaviour.cs index 224071ef5a..d5cf93a50c 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/IItemBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/IItemBehaviour.cs @@ -7,12 +7,10 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items { - using Exiled.CustomModules.API.Interfaces; - /// /// Represents a marker interface for custom item behaviors. /// - public interface IItemBehaviour : ITrackable + public interface IItemBehaviour : ICustomItemBehaviour { } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemBehaviour.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/ItemBehaviour.cs index 95f5661bda..263a27d04b 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Items/ItemBehaviour.cs @@ -14,11 +14,11 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items using Exiled.API.Extensions; using Exiled.API.Features; using Exiled.API.Features.Attributes; - using Exiled.API.Features.Core; using Exiled.API.Features.Core.Interfaces; using Exiled.API.Features.DynamicEvents; using Exiled.API.Features.Items; using Exiled.API.Features.Pickups; + using Exiled.CustomModules.API.Features.Generic; using Exiled.CustomModules.Events.EventArgs.CustomItems; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Scp914; @@ -29,64 +29,68 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Items /// Represents the base class for custom item behaviors. ///
/// - /// This class extends and implements and . + /// This class extends and implements and . ///
It provides a foundation for creating custom behaviors associated with in-game items. ///
- public abstract class ItemBehaviour : ModuleBehaviour, IItemBehaviour, IAdditiveSettings + public abstract class ItemBehaviour : ModuleBehaviour, IItemBehaviour, IAdditiveSettings { + private static TrackerBase tracker; + private ushort serial; + private SettingsBase settings; + /// /// Gets or sets the which handles all the delegates fired before owner of the item changes role. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OwnerChangingRoleDispatcher { get; set; } + public TDynamicEventDispatcher OwnerChangingRoleDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item dies. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OwnerDyingDispatcher { get; set; } + public TDynamicEventDispatcher OwnerDyingDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item escapes. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OwnerEscapingDispatcher { get; set; } + public TDynamicEventDispatcher OwnerEscapingDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item gets handcuffed. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher OwnerHandcuffingDispatcher { get; set; } + public TDynamicEventDispatcher OwnerHandcuffingDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item drops it. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher DroppingItemDispatcher { get; set; } + public TDynamicEventDispatcher DroppingItemDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item picks it up. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher PickingUpItemDispatcher { get; set; } + public TDynamicEventDispatcher PickingUpItemDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item changes it. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher ChangingItemDispatcher { get; set; } + public TDynamicEventDispatcher ChangingItemDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item upgrades it. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher UpgradingPickupDispatcher { get; set; } + public TDynamicEventDispatcher UpgradingPickupDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before owner of the item upgrades it through his inventory. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher UpgradingItemDispatcher { get; set; } + public TDynamicEventDispatcher UpgradingItemDispatcher { get; set; } = new(); /// public override bool DisposeOnNullOwner { get; protected set; } = false; @@ -97,37 +101,42 @@ public abstract class ItemBehaviour : ModuleBehaviour, IItemBehaviour, IAd public CustomItem CustomItem { get; private set; } /// - public ItemSettings Settings { get; set; } + public SettingsBase Settings + { + get => settings ??= CustomItem.Settings; + set => settings = value; + } + + /// + public override ModulePointer Config + { + get => config ??= CustomItem.Config; + protected set => config = value; + } /// /// Gets the item's owner. /// - public Player ItemOwner => Owner.Owner; + public Player ItemOwner { get; private set; } + + /// + /// Gets the . + /// + protected static TrackerBase Tracker => tracker ??= TrackerBase.Get(); /// public virtual void AdjustAdditivePipe() { - ImplementConfigs(); - - if (CustomItem.TryGet(GetType(), out CustomItem customItem) && customItem.Settings is ItemSettings itemSettings) - { + if (CustomItem.TryGet(GetType(), out CustomItem customItem)) CustomItem = customItem; - Settings = itemSettings; - } - if (CustomItem is null || Settings is null) + if (CustomItem is null || Settings is null || Config is null) { Log.Error($"Custom item ({GetType().Name}) has invalid configuration."); Destroy(); } - } - /// - protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue( - typeof(ItemSettings).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : this, - propertyInfo.GetValue(Config, null)); + ImplementConfigs(); } /// @@ -149,12 +158,17 @@ protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targ /// if the specified pickup is being tracked and associated with this item; otherwise, . /// /// - /// This method ensures that the provided pickup is being tracked by the + /// This method ensures that the provided pickup is being tracked by the /// and the tracked values associated with the pickup contain this item instance. /// protected virtual bool Check(Pickup pickup) => - pickup && StaticActor.Get() is ItemTracker itemTracker && - itemTracker.IsTracked(pickup) && itemTracker.GetTrackedValues(pickup).Contains(this); + pickup is not null && pickup.Serial == serial && Tracker.IsTracked(pickup) && + Tracker.GetTrackedValues(pickup).Any(c => c.GetHashCode() == GetHashCode()); + + /// + protected override bool Check(Item item) => + item is not null && serial == item.Serial && Tracker.IsTracked(item) && + Tracker.GetTrackedValues(item).Any(c => c.GetHashCode() == GetHashCode()); /// protected override void PostInitialize() @@ -162,13 +176,31 @@ protected override void PostInitialize() base.PostInitialize(); AdjustAdditivePipe(); + + FixedTickRate = 1f; + CanEverTick = true; } - /// + /// protected override void OnBeginPlay() { base.OnBeginPlay(); - this.SubscribeEvents(); + + SubscribeEvents(); + + serial = Owner.Serial; + ItemOwner = Owner.Owner; + + OnAcquired(Settings.ShowPickedUpTextOnItemGiven); + } + + /// + protected override void OnRemoved() + { + base.OnRemoved(); + + Owner = null; + ItemOwner = null; } /// @@ -178,10 +210,11 @@ protected override void SubscribeEvents() Exiled.Events.Handlers.Player.Dying += OnInternalOwnerDying; Exiled.Events.Handlers.Player.DroppingItem += OnInternalDropping; - Exiled.Events.Handlers.Player.ChangingItem += OnInternalChanging; + Exiled.Events.Handlers.Player.ChangingItem += OnInternalChangingItem; Exiled.Events.Handlers.Player.Escaping += OnInternalOwnerEscaping; Exiled.Events.Handlers.Player.PickingUpItem += OnInternalPickingUp; Exiled.Events.Handlers.Player.ItemAdded += OnInternalItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved += OnInternalItemRemoved; Exiled.Events.Handlers.Scp914.UpgradingPickup += OnInternalUpgradingPickup; Exiled.Events.Handlers.Player.Handcuffing += OnInternalOwnerHandcuffing; Exiled.Events.Handlers.Player.ChangingRole += OnInternalOwnerChangingRole; @@ -195,10 +228,11 @@ protected override void UnsubscribeEvents() Exiled.Events.Handlers.Player.Dying -= OnInternalOwnerDying; Exiled.Events.Handlers.Player.DroppingItem -= OnInternalDropping; - Exiled.Events.Handlers.Player.ChangingItem -= OnInternalChanging; + Exiled.Events.Handlers.Player.ChangingItem -= OnInternalChangingItem; Exiled.Events.Handlers.Player.Escaping -= OnInternalOwnerEscaping; Exiled.Events.Handlers.Player.PickingUpItem -= OnInternalPickingUp; Exiled.Events.Handlers.Player.ItemAdded -= OnInternalItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved -= OnInternalItemRemoved; Exiled.Events.Handlers.Scp914.UpgradingPickup -= OnInternalUpgradingPickup; Exiled.Events.Handlers.Player.Handcuffing -= OnInternalOwnerHandcuffing; Exiled.Events.Handlers.Player.ChangingRole -= OnInternalOwnerChangingRole; @@ -247,7 +281,7 @@ protected override void UnsubscribeEvents() /// . protected virtual void OnChangingItem(ChangingItemEventArgs ev) { - if (Settings.ShouldMessageOnGban) + if (Settings.NotifyItemToSpectators) { foreach (Player player in Player.Get(RoleTypeId.Spectator)) { @@ -256,7 +290,7 @@ protected virtual void OnChangingItem(ChangingItemEventArgs ev) } } - ShowSelectedMessage(ev.Player); + ShowSelectedMessage(); ChangingItemDispatcher.InvokeAll(ev); } @@ -272,30 +306,26 @@ protected virtual void OnChangingItem(ChangingItemEventArgs ev) /// /// Called anytime the item enters a player's inventory by any means. /// - /// The acquiring the item. - /// The being acquired. /// Whether the pickup hint should be displayed. - protected virtual void OnAcquired(Player player, Item item, bool displayMessage = true) + protected virtual void OnAcquired(bool displayMessage = true) { if (displayMessage) - ShowPickedUpMessage(player); + ShowPickedUpMessage(); } /// /// Shows a message to the player upon picking up a custom item. /// - /// The who will be shown the message. - protected virtual void ShowPickedUpMessage(Player player) => player.ShowTextDisplay(Settings.PickedUpText); + protected virtual void ShowPickedUpMessage() => ItemOwner.ShowTextDisplay(Settings.PickedUpText); /// /// Shows a message to the player upon selecting a custom item. /// - /// The who will be shown the message. - protected virtual void ShowSelectedMessage(Player player) + protected virtual void ShowSelectedMessage() { string text = Settings.SelectedText.Content.Replace("{item}", CustomItem.Name).Replace("{description}", CustomItem.Description); TextDisplay textDisplay = new(text, Settings.SelectedText.Duration, Settings.SelectedText.CanBeDisplayed, Settings.SelectedText.Channel); - player.ShowTextDisplay(textDisplay); + ItemOwner.ShowTextDisplay(textDisplay); } private void OnInternalOwnerChangingRole(ChangingRoleEventArgs ev) @@ -303,11 +333,8 @@ private void OnInternalOwnerChangingRole(ChangingRoleEventArgs ev) if (ev.Reason.Equals(SpawnReason.Escaped)) return; - foreach (Item item in ev.Player.Items.ToList()) + foreach (Item item in ev.Player.Items.ToList().Where(Check)) { - if (!Check(item)) - continue; - OnOwnerChangingRole(new(item, CustomItem, this, ev)); CustomItem.Spawn(ev.Player, item, ev.Player); @@ -322,11 +349,8 @@ private void OnInternalOwnerDying(DyingEventArgs ev) if (ItemOwner is null || ev.Player != ItemOwner) return; - foreach (Item item in ev.Player.Items.ToList()) + foreach (Item item in ev.Player.Items.ToList().Where(Check)) { - if (!Check(item)) - continue; - OnOwnerDying(new(item, CustomItem, this, ev)); if (!ev.IsAllowed) @@ -346,11 +370,8 @@ private void OnInternalOwnerEscaping(EscapingEventArgs ev) if (ItemOwner is null || ev.Player != ItemOwner) return; - foreach (Item item in ev.Player.Items.ToList()) + foreach (Item item in ev.Player.Items.ToList().Where(Check)) { - if (!Check(item)) - continue; - OnOwnerEscaping(new(item, CustomItem, this, ev)); if (!ev.IsAllowed) @@ -367,11 +388,8 @@ private void OnInternalOwnerEscaping(EscapingEventArgs ev) private void OnInternalOwnerHandcuffing(HandcuffingEventArgs ev) { - foreach (Item item in ev.Target.Items.ToList()) + foreach (Item item in ev.Target.Items.ToList().Where(Check)) { - if (!Check(item)) - continue; - OnOwnerHandcuffing(new(item, CustomItem, this, ev)); if (!ev.IsAllowed) @@ -384,7 +402,7 @@ private void OnInternalOwnerHandcuffing(HandcuffingEventArgs ev) private void OnInternalDropping(DroppingItemEventArgs ev) { - if (!Check(ev.Item) || !Check(Owner)) + if (!Check(ev.Item) || !Check(ItemOwner)) return; OnDropping(ev); @@ -400,13 +418,23 @@ private void OnInternalPickingUp(PickingUpItemEventArgs ev) private void OnInternalItemAdded(ItemAddedEventArgs ev) { - if (!Check(ev.Pickup)) + if (!Check(ev.Item)) + return; + + Owner = ev.Item; + ItemOwner = ev.Player; + OnAcquired(); + } + + private void OnInternalItemRemoved(ItemRemovedEventArgs ev) + { + if (!Check(ev.Item)) return; - OnAcquired(ev.Player, ev.Item, true); + Owner = null; } - private void OnInternalChanging(ChangingItemEventArgs ev) + private void OnInternalChangingItem(ChangingItemEventArgs ev) { if (!Check(ev.Item)) { diff --git a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Items/ItemSettings.cs deleted file mode 100644 index 8a8167d1c2..0000000000 --- a/Exiled.CustomModules/API/Features/CustomItems/Items/ItemSettings.cs +++ /dev/null @@ -1,61 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.CustomItems.Items -{ - using Exiled.API.Features; - using Exiled.API.Features.Spawn; - - using UnityEngine; - - /// - /// A tool to easily setup items. - /// - public class ItemSettings : Settings - { - /// - /// Gets the default values. - /// It refers to the base-game item behavior. - /// - public static ItemSettings Default { get; } = new(); - - /// - /// Gets or sets the custom item's . - /// - public override ItemType ItemType { get; set; } - - /// - /// Gets or sets the . - /// - public override SpawnProperties SpawnProperties { get; set; } = new(); - - /// - /// Gets or sets the weight of the item. - /// - public override float Weight { get; set; } = -1f; - - /// - /// Gets or sets the scale of the item. - /// - public override Vector3 Scale { get; set; } = Vector3.one; - - /// - /// Gets or sets a value indicating whether or not this item causes things to happen that may be considered hacks, and thus be shown to global moderators as being present in a player's inventory when they gban them. - /// - public virtual bool ShouldMessageOnGban { get; set; } - - /// - /// Gets or sets the to be displayed when the item has been picked up. - /// - public override TextDisplay PickedUpText { get; set; } - - /// - /// Gets or sets the to be displayed when the item has been selected. - /// - public virtual TextDisplay SelectedText { get; set; } - } -} diff --git a/Exiled.CustomModules/API/Features/CustomItems/Pickups/Ammos/AmmoSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Pickups/Ammos/AmmoSettings.cs index 0044869841..4b1c12eb73 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Pickups/Ammos/AmmoSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Pickups/Ammos/AmmoSettings.cs @@ -7,21 +7,23 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Pickups.Ammos { - using Exiled.CustomModules.API.Features.CustomItems.Pickups; + using System.ComponentModel; /// /// A tool to easily setup ammos. /// - public class AmmoSettings : PickupSettings + public class AmmoSettings : SettingsBase { /// /// Gets or sets the sizes of the ammo box. /// + [Description("The sizes of the ammo box.")] public virtual ushort[] BoxSizes { get; set; } = { }; /// /// Gets or sets the maximum allowed amount of ammo. /// + [Description("The maximum allowed amount of ammo.")] public virtual ushort MaxUnits { get; set; } } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Pickups/IPickupBehaviour.cs b/Exiled.CustomModules/API/Features/CustomItems/Pickups/IPickupBehaviour.cs index 9dacc50bc2..89468553c6 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Pickups/IPickupBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Pickups/IPickupBehaviour.cs @@ -7,12 +7,10 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Pickups { - using Exiled.CustomModules.API.Interfaces; - /// /// Represents a marker interface for custom pickup behaviors. /// - public interface IPickupBehaviour : ITrackable + public interface IPickupBehaviour : ICustomItemBehaviour { } } diff --git a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupBehaviour.cs b/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupBehaviour.cs index 4ea2fec711..1d7b077540 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupBehaviour.cs @@ -12,11 +12,10 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Pickups using Exiled.API.Features; using Exiled.API.Features.Attributes; - using Exiled.API.Features.Core; using Exiled.API.Features.Core.Interfaces; using Exiled.API.Features.DynamicEvents; using Exiled.API.Features.Pickups; - using Exiled.CustomModules.API.Features.CustomItems.Items; + using Exiled.CustomModules.API.Features.Generic; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Scp914; @@ -27,13 +26,17 @@ namespace Exiled.CustomModules.API.Features.CustomItems.Pickups /// This class extends and implements and . ///
It provides a foundation for creating custom behaviors associated with in-game pickup. /// - public abstract class PickupBehaviour : ModuleBehaviour, IPickupBehaviour, IAdditiveSettings + public abstract class PickupBehaviour : ModuleBehaviour, IPickupBehaviour, IAdditiveSettings { + private static TrackerBase tracker; + private ushort serial; + private SettingsBase settings; + /// /// Gets or sets the which handles all the delegates fired before the pickup is gets picked up. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher PickingUpItemDispatcher { get; set; } + public TDynamicEventDispatcher PickingUpItemDispatcher { get; set; } = new(); /// public override bool DisposeOnNullOwner { get; protected set; } = false; @@ -44,32 +47,37 @@ public abstract class PickupBehaviour : ModuleBehaviour, IPickupBehaviou public CustomItem CustomItem { get; private set; } /// - public PickupSettings Settings { get; set; } + public SettingsBase Settings + { + get => settings ??= CustomItem.Settings; + set => settings = value; + } /// - public virtual void AdjustAdditivePipe() + public override ModulePointer Config { - ImplementConfigs(); + get => config ??= CustomItem.Config; + protected set => config = value; + } - if (CustomItem.TryGet(GetType(), out CustomItem customItem) && customItem.Settings is PickupSettings pickupSettings) - { + /// + /// Gets the . + /// + protected static TrackerBase Tracker => tracker ??= TrackerBase.Get(); + + /// + public virtual void AdjustAdditivePipe() + { + if (CustomItem.TryGet(GetType(), out CustomItem customItem)) CustomItem = customItem; - Settings = pickupSettings; - } - if (CustomItem is null || Settings is null) + if (CustomItem is null || Settings is null || Config is null) { Log.Error($"Custom pickup ({GetType().Name}) has invalid configuration."); Destroy(); } - } - /// - protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue( - typeof(ItemSettings).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : this, - propertyInfo.GetValue(Config, null)); + ImplementConfigs(); } /// @@ -80,12 +88,12 @@ protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targ /// if the specified pickup is being tracked and associated with this item; otherwise, . /// /// - /// This method ensures that the provided pickup is being tracked by the + /// This method ensures that the provided pickup is being tracked by the /// and the tracked values associated with the pickup contain this item instance. /// protected override bool Check(Pickup pickup) => - base.Check(pickup) && StaticActor.Get() is PickupTracker pickupTracker && - pickupTracker.IsTracked(pickup) && pickupTracker.GetTrackedValues(pickup).Contains(this); + pickup is not null && serial == pickup.Serial && Tracker.IsTracked(pickup) && + Tracker.GetTrackedValues(pickup).Any(c => c.GetHashCode() == GetHashCode()); /// protected override void PostInitialize() @@ -93,6 +101,19 @@ protected override void PostInitialize() base.PostInitialize(); AdjustAdditivePipe(); + + FixedTickRate = 1f; + CanEverTick = true; + } + + /// + protected override void OnBeginPlay() + { + base.OnBeginPlay(); + + SubscribeEvents(); + + serial = Owner.Serial; } /// @@ -101,7 +122,8 @@ protected override void SubscribeEvents() base.SubscribeEvents(); Exiled.Events.Handlers.Player.PickingUpItem += OnInternalPickingUp; - Exiled.Events.Handlers.Player.AddingItem += OnInternalAddingItem; + Exiled.Events.Handlers.Player.ItemAdded += OnInternalItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved += OnInternalItemRemoved; Exiled.Events.Handlers.Scp914.UpgradingPickup += OnInternalUpgradingPickup; } @@ -111,7 +133,8 @@ protected override void UnsubscribeEvents() base.UnsubscribeEvents(); Exiled.Events.Handlers.Player.PickingUpItem -= OnInternalPickingUp; - Exiled.Events.Handlers.Player.AddingItem -= OnInternalAddingItem; + Exiled.Events.Handlers.Player.ItemAdded -= OnInternalItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved -= OnInternalItemRemoved; Exiled.Events.Handlers.Scp914.UpgradingPickup -= OnInternalUpgradingPickup; } @@ -140,22 +163,27 @@ protected virtual void OnAcquired(Player player, bool displayMessage = true) private void OnInternalPickingUp(PickingUpItemEventArgs ev) { - if (!Check(ev.Pickup) || ev.Player.Items.Count >= 8) + if (!Check(ev.Pickup)) return; OnPickingUp(ev); + } - if (!ev.IsAllowed) + private void OnInternalItemAdded(ItemAddedEventArgs ev) + { + if (!Check(ev.Pickup)) return; + + Owner = null; + OnAcquired(ev.Player); } - private void OnInternalAddingItem(AddingItemEventArgs ev) + private void OnInternalItemRemoved(ItemRemovedEventArgs ev) { if (!Check(ev.Pickup)) return; - ev.IsAllowed = false; - OnAcquired(ev.Player, true); + Owner = ev.Pickup; } private void OnInternalUpgradingPickup(UpgradingPickupEventArgs ev) diff --git a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupSettings.cs b/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupSettings.cs deleted file mode 100644 index 4f991238ec..0000000000 --- a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupSettings.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.CustomItems.Pickups -{ - using Exiled.API.Features; - using Exiled.API.Features.Spawn; - using UnityEngine; - - /// - /// A tool to easily setup pickups. - /// - public class PickupSettings : Settings - { - /// - /// Gets the default values. - /// It refers to the base-game pickup behavior. - /// - public static PickupSettings Default { get; } = new(); - - /// - /// Gets or sets the custom pickup's . - /// - public override ItemType ItemType { get; set; } - - /// - /// Gets or sets the . - /// - public override SpawnProperties SpawnProperties { get; set; } = new(); - - /// - /// Gets or sets the weight of the pickup. - /// - public override float Weight { get; set; } = -1f; - - /// - /// Gets or sets the scale of the pickup. - /// - public override Vector3 Scale { get; set; } = Vector3.one; - - /// - /// Gets or sets the to be displayed when the pickup has been picked up. - /// - public override TextDisplay PickedUpText { get; set; } - } -} diff --git a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupTracker.cs b/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupTracker.cs deleted file mode 100644 index e27e81951f..0000000000 --- a/Exiled.CustomModules/API/Features/CustomItems/Pickups/PickupTracker.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.CustomItems.Pickups -{ - using Exiled.CustomModules.API.Features.Generic; - - /// - /// The actor which handles all tracking-related tasks for pickups. - /// - public class PickupTracker : TrackerBase - { - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomItems/Settings.cs b/Exiled.CustomModules/API/Features/CustomItems/Settings.cs index 2eac740f16..1540ebf045 100644 --- a/Exiled.CustomModules/API/Features/CustomItems/Settings.cs +++ b/Exiled.CustomModules/API/Features/CustomItems/Settings.cs @@ -7,41 +7,10 @@ namespace Exiled.CustomModules.API.Features.CustomItems { - using Exiled.API.Features; - using Exiled.API.Features.Core; - using Exiled.API.Features.Core.Interfaces; - using Exiled.API.Features.Spawn; - - using UnityEngine; - /// - /// Defines the contract for config features related to custom entities. + /// Defines the contract for config features related to custom items. /// - public class Settings : TypeCastObject, IAdditiveProperty + public class Settings : SettingsBase { - /// - /// Gets or sets the custom entity's . - /// - public virtual ItemType ItemType { get; set; } - - /// - /// Gets or sets the . - /// - public virtual SpawnProperties SpawnProperties { get; set; } - - /// - /// Gets or sets the weight of the entity. - /// - public virtual float Weight { get; set; } - - /// - /// Gets or sets the scale of the entity. - /// - public virtual Vector3 Scale { get; set; } - - /// - /// Gets or sets the to be displayed when the entity has been picked up. - /// - public virtual TextDisplay PickedUpText { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomItems/SettingsBase.cs b/Exiled.CustomModules/API/Features/CustomItems/SettingsBase.cs new file mode 100644 index 0000000000..9a0c9fb7cc --- /dev/null +++ b/Exiled.CustomModules/API/Features/CustomItems/SettingsBase.cs @@ -0,0 +1,78 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Features.CustomItems +{ + using System.ComponentModel; + + using Exiled.API.Features; + using Exiled.API.Features.Core; + using Exiled.API.Features.Core.Interfaces; + using Exiled.API.Features.Spawn; + using UnityEngine; + + /// + /// Defines the contract for config features related to custom entities. + /// + public abstract class SettingsBase : TypeCastObject, IAdditiveProperty + { + /// + /// Initializes a new instance of the class. + /// + public SettingsBase() => SettingsType = GetType().Name; + + /// + /// Gets or sets the settings type. + ///
This value is automatically set.
+ ///
+ [Description("The relative settings type. This value is automatically set and shouldn't be modified.")] + public virtual string SettingsType { get; set; } + + /// + /// Gets or sets the . + /// + [Description("The spawn properties.")] + public virtual SpawnProperties SpawnProperties { get; set; } = new(); + + /// + /// Gets or sets the weight of the entity. + /// + [Description("The weight of the entity.")] + public virtual float Weight { get; set; } = -1f; + + /// + /// Gets or sets the scale of the entity. + /// + [Description("The scale of the entity.")] + public virtual Vector3 Scale { get; set; } = Vector3.one; + + /// + /// Gets or sets a value indicating whether the text to be displayed when the item has been picked up + /// should be displayed when the item is given through any commands. + /// + [Description("Indicates whether the text to be displayed when the item has been picked up should be displayed when the item is given through any commands.")] + public bool ShowPickedUpTextOnItemGiven { get; set; } + + /// + /// Gets or sets the to be displayed when the item has been picked up. + /// + [Description("The TextDisplay to be displayed when the item has been picked up.")] + public virtual TextDisplay PickedUpText { get; set; } = new(); + + /// + /// Gets or sets the to be displayed when the item has been selected. + /// + [Description("The TextDisplay to be displayed when the item has been selected.")] + public virtual TextDisplay SelectedText { get; set; } = new(); + + /// + /// Gets or sets a value indicating whether the item's name should be displayed to spectators when spectating the owner of the item. + /// + [Description("Indicates whether the item's name should be displayed to spectators when spectating the owner of the item.")] + public virtual bool NotifyItemToSpectators { get; set; } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomModule.cs b/Exiled.CustomModules/API/Features/CustomModule.cs index a2444933c5..75fdd36657 100644 --- a/Exiled.CustomModules/API/Features/CustomModule.cs +++ b/Exiled.CustomModules/API/Features/CustomModule.cs @@ -18,8 +18,6 @@ namespace Exiled.CustomModules.API.Features using Exiled.API.Features.Attributes; using Exiled.API.Features.Core; using Exiled.API.Features.DynamicEvents; - using Exiled.API.Features.Serialization; - using Exiled.API.Features.Serialization.CustomConverters; using Exiled.API.Interfaces; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features.Attributes; @@ -37,8 +35,6 @@ public abstract class CustomModule : TypeCastObject, IEquatable Loader = new(); - private string serializableParentPath; private string serializableChildPath; private string serializableFilePath; @@ -93,7 +89,7 @@ internal string ParentName { // Type-Module if (string.IsNullOrEmpty(serializableParentName)) - serializableParentName = $"{GetType().Name}-Module"; + serializableParentName = $"{GetType().BaseType.Name.TrimEnd('`', '1')}-Module"; return serializableParentName; } @@ -206,42 +202,24 @@ internal string PointerPath // /Configs/CustomModules/ParentName/ChildName/PointerName if (string.IsNullOrEmpty(modulePointerPath)) modulePointerPath = Path.Combine(Paths.Configs, CUSTOM_MODULES_FOLDER, ParentName, ChildName, PointerName); - return modulePointerPath; } } + /// + /// Gets the base module type name. + /// + internal string BaseModuleTypeName => GetType().BaseType.Name.RemoveGenericSuffix(); + /// /// Gets the serializer for custom modules. /// - private static ISerializer ModuleSerializer { get; } = new SerializerBuilder() - .WithTypeConverter(new VectorsConverter()) - .WithTypeConverter(new ColorConverter()) - .WithTypeConverter(new AttachmentIdentifiersConverter()) - .WithTypeConverter(new EnumClassConverter()) - .WithTypeConverter(new PrivateConstructorConverter()) - .WithEventEmitter(eventEmitter => new TypeAssigningEventEmitter(eventEmitter)) - .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) - .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .IgnoreFields() - .DisableAliases() - .Build(); + protected static ISerializer ModuleSerializer { get; } = ConfigSubsystem.GetDefaultSerializerBuilder().Build(); /// /// Gets the deserializer for custom modules. /// - private static IDeserializer ModuleDeserializer { get; } = new DeserializerBuilder() - .WithTypeConverter(new VectorsConverter()) - .WithTypeConverter(new ColorConverter()) - .WithTypeConverter(new AttachmentIdentifiersConverter()) - .WithTypeConverter(new EnumClassConverter()) - .WithTypeConverter(new PrivateConstructorConverter()) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithDuplicateKeyChecking() - .IgnoreFields() - .IgnoreUnmatchedProperties() - .Build(); + protected static IDeserializer ModuleDeserializer { get; } = ConfigSubsystem.GetDefaultDeserializerBuilder().Build(); /// /// Compares two operands: and . @@ -332,14 +310,16 @@ UUModuleType FindClosestModuleType(Type t, IEnumerable source) foreach (Type type in assembly.GetTypes()) { - if (type.IsAbstract || (type.BaseType != typeof(CustomModule) && !type.IsSubclassOf(typeof(CustomModule)))) + if (type.BaseType != typeof(CustomModule)) continue; IEnumerable rhMethods = type.GetMethods(ModuleInfo.SIGNATURE_BINDINGS) .Where(m => { ParameterInfo[] mParams = m.GetParameters(); - return (m.Name is ModuleInfo.ENABLE_ALL_CALLBACK && mParams.Any(p => p.ParameterType == typeof(Assembly))) || m.Name is ModuleInfo.DISABLE_ALL_CALLBACK; + return (m.Name is ModuleInfo.ENABLE_ALL_CALLBACK && + mParams.Any(p => p.ParameterType == typeof(Assembly))) || + m.Name is ModuleInfo.DISABLE_ALL_CALLBACK; }); MethodInfo enableAll = rhMethods.FirstOrDefault(m => m.Name is ModuleInfo.ENABLE_ALL_CALLBACK); @@ -357,9 +337,8 @@ UUModuleType FindClosestModuleType(Type t, IEnumerable source) continue; } - Action enableAllAction = Delegate.CreateDelegate(typeof(Action), enableAll) as Action; - Action disableAllAction = Delegate.CreateDelegate(typeof(Action), disableAll) as Action; - + Func enableAllAction = Delegate.CreateDelegate(typeof(Func), enableAll) as Func; + Func disableAllAction = Delegate.CreateDelegate(typeof(Func), disableAll) as Func; ModuleInfo moduleInfo = new() { Type = type, @@ -369,8 +348,10 @@ UUModuleType FindClosestModuleType(Type t, IEnumerable source) ModuleType = FindClosestModuleType(type, moduleTypeValuesInfo), }; + if (!CustomModules.IsModuleLoaded(moduleInfo.ModuleType)) + continue; + ModuleInfo.AllModules.Add(moduleInfo); - CustomModules.Instance.RegistrationHandler.OnModuleEnabled(moduleInfo); if (!shouldBeEnabled) continue; @@ -441,82 +422,67 @@ public override bool Equals(object obj) /// Serializes the current module to a file specified by . /// /// Thrown when is null. - public void SerializeModule() + public virtual void SerializeModule() { if (string.IsNullOrEmpty(Name)) { - Log.Error($"{GetType().Name}::Name property was not defined. A module must define a name, or it won't load."); + Log.Error($"[{BaseModuleTypeName}] {GetType().Name}::Name property was not defined. A module must define a name, or it won't load."); return; } Directory.CreateDirectory(ParentPath); Directory.CreateDirectory(ChildPath); - if (File.Exists(FilePath) && File.Exists(PointerPath)) - { - File.WriteAllText(FilePath, ModuleSerializer.Serialize(this)); - File.WriteAllText(PointerPath, ModuleSerializer.Serialize(Config)); - return; - } - - File.WriteAllText(FilePath ?? throw new ArgumentNullException(nameof(FilePath)), ModuleSerializer.Serialize(this)); - File.WriteAllText(PointerPath ?? throw new ArgumentNullException(nameof(PointerPath)), ModuleSerializer.Serialize(Config)); + SerializeModule_Implementation(); } /// /// Deserializes the module from the file specified by . /// - public void DeserializeModule() + public virtual void DeserializeModule() { if (string.IsNullOrEmpty(Name)) { - Log.ErrorWithContext($"{GetType().Name}::Name property was not defined. A module must define a name, or it won't load.", Log.CONTEXT_CRITICAL); + Log.ErrorWithContext($"[{BaseModuleTypeName}] {GetType().Name}::Name property was not defined. A module must define a name, or it won't load.", Log.CONTEXT_CRITICAL); return; } DynamicEventManager.CreateFromTypeInstance(this); + bool @override = !File.Exists(FilePath) || !File.Exists(PointerPath); + if (!File.Exists(FilePath)) - { - Log.Info($"{GetType().Name} module configuration not found. Creating a new configuration file."); + Log.Info($"[{BaseModuleTypeName}] {Name} configuration not found. Creating a new configuration file."); - if (File.Exists(PointerPath)) - { - try - { - Config = ModuleDeserializer.Deserialize(File.ReadAllText(PointerPath)); - } - catch - { - Config = ModulePointer.Get(this); - } - } - else - { - Config = ModulePointer.Get(this); - } + if (!File.Exists(PointerPath)) + { + Log.Info($"[{BaseModuleTypeName}] {Name} pointer not found. Creating a new pointer file."); + Config = ModulePointer.Get(this, GetType().Assembly); + } + if (@override) SerializeModule(); - return; - } - CustomModule deserializedModule = ModuleDeserializer.Deserialize(File.ReadAllText(FilePath), GetType()) as CustomModule; - CopyProperties(deserializedModule); + DeserializeModule_Implementation(); + } - foreach (string file in Directory.GetFiles(ChildPath)) - { - if (file == FilePath) - continue; + /// + /// Defines the actual process of serialization for the module. + /// + protected virtual void SerializeModule_Implementation() + { + File.WriteAllText(FilePath, ModuleSerializer.Serialize(this)); + File.WriteAllText(PointerPath, ModuleSerializer.Serialize(Config)); + } - try - { - Config = ModuleDeserializer.Deserialize(File.ReadAllText(file)); - } - catch - { - continue; - } - } + /// + /// Defines the actual process of deserialization for the module. + /// + protected virtual void DeserializeModule_Implementation() + { + CustomModule deserializedModule = ModuleDeserializer.Deserialize(File.ReadAllText(FilePath), GetType()) as CustomModule; + CopyProperties(deserializedModule); + Config = ModuleDeserializer.Deserialize(File.ReadAllText(PointerPath), ModulePointer.Get(this, GetType().Assembly).GetType()) as ModulePointer; } /// @@ -542,21 +508,11 @@ protected virtual bool TryUnregister() return true; } - private static void AutomaticModulesLoaderState(bool shouldLoad) - { - Config config = CustomModules.Instance.Config; - if (config.UseAutomaticModulesLoader) - { - foreach (IPlugin plugin in Exiled.Loader.Loader.Plugins) - { - ModuleInfo.AllModules - .Where(moduleInfo => config.Modules.Any(m => m == moduleInfo.ModuleType.Name)) - .ForEach(mod => mod.InvokeCallback(shouldLoad ? ModuleInfo.ENABLE_ALL_CALLBACK : ModuleInfo.DISABLE_ALL_CALLBACK, plugin.Assembly)); - } - } - } - - private void CopyProperties(CustomModule source) + /// + /// Copies all properties from a source object to the current instance. + /// + /// The object to copy the properties from. + protected void CopyProperties(CustomModule source) { if (source is null) throw new NullReferenceException("Source is null. Was the custom module deserialized?"); @@ -567,5 +523,27 @@ private void CopyProperties(CustomModule source) property.SetValue(this, property.GetValue(source)); } } + + private static void AutomaticModulesLoaderState(bool shouldLoad) + { + Config config = CustomModules.Instance.Config; + if (config.UseAutomaticModulesLoader) + { + foreach (ModuleInfo moduleInfo in ModuleInfo.AllModules) + { + foreach (IPlugin plugin in Loader.Loader.Plugins) + { + try + { + moduleInfo.InvokeCallback(shouldLoad ? ModuleInfo.ENABLE_ALL_CALLBACK : ModuleInfo.DISABLE_ALL_CALLBACK, plugin.Assembly); + } + catch + { + continue; + } + } + } + } + } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomModuleDeserializer.cs b/Exiled.CustomModules/API/Features/CustomModuleDeserializer.cs deleted file mode 100644 index dec7a59662..0000000000 --- a/Exiled.CustomModules/API/Features/CustomModuleDeserializer.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features -{ - using System; - using System.Linq; - - using Exiled.CustomModules.API.Features.Deserializers; - using YamlDotNet.Core; - using YamlDotNet.Serialization; - - /// - public class CustomModuleDeserializer : INodeDeserializer - { - /// - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value) - { - value = null; - - if (ParserContext.Delegates.IsEmpty()) - ModuleParser.InstantiateModuleParsers(); - - bool parserStatus = false; - foreach (ParserContext.ModuleDelegate moduleDelegate in ParserContext.Delegates.TakeWhile(_ => !parserStatus)) - parserStatus = moduleDelegate.Invoke(new ParserContext(parser, expectedType, nestedObjectDeserializer), out value); - - return parserStatus; - } - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomRoles/CustomRole.cs b/Exiled.CustomModules/API/Features/CustomRoles/CustomRole.cs index b68c3e0484..a48e8f4391 100644 --- a/Exiled.CustomModules/API/Features/CustomRoles/CustomRole.cs +++ b/Exiled.CustomModules/API/Features/CustomRoles/CustomRole.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomRoles { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reflection; @@ -23,7 +24,6 @@ namespace Exiled.CustomModules.API.Features.CustomRoles using Exiled.CustomModules.API.Features.Attributes; using Exiled.CustomModules.API.Features.CustomEscapes; using Exiled.CustomModules.Events.EventArgs.CustomRoles; - using MEC; using PlayerRoles; using Respawning; using YamlDotNet.Serialization; @@ -92,31 +92,37 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// Gets or sets the 's name. /// + [Description("The name of the custom role.")] public override string Name { get; set; } /// /// Gets or sets the 's id. /// + [Description("The id of the custom role.")] public override uint Id { get; set; } /// /// Gets or sets the 's description. /// + [Description("The description of the custom role.")] public virtual string Description { get; set; } /// /// Gets or sets a value indicating whether the is enabled. /// + [Description("Indicates whether the custom role is enabled.")] public override bool IsEnabled { get; set; } /// /// Gets or sets the 's . /// + [Description("The custom role's RoleTypeId.")] public virtual RoleTypeId Role { get; set; } /// /// Gets or sets the relative spawn chance of the . /// + [Description("The custom role's spawn chance.")] public virtual int Probability { get; set; } /// @@ -125,6 +131,7 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// This property specifies the required alive team to be eligible for spawning in the . /// + [Description("The custom role's required team to spawn.")] public virtual Team RequiredTeamToSpawn { get; set; } = Team.Dead; /// @@ -133,6 +140,7 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// This property specifies the required role type for players to be eligible for spawning in the . /// + [Description("The custom role's required RoleTypeId to spawn.")] public virtual RoleTypeId RequiredRoleToSpawn { get; set; } = RoleTypeId.None; /// @@ -141,6 +149,7 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// This property specifies the required alive custom team to be eligible for spawning in the . /// + [Description("The custom role's required custom team to spawn.")] public virtual uint RequiredCustomTeamToSpawn { get; set; } /// @@ -149,21 +158,19 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// This property specifies the required custom role for players to be eligible for spawning in the . /// + [Description("The custom role's required custom role to spawn.")] public virtual uint RequiredCustomRoleToSpawn { get; set; } - /// - /// Gets or sets the . - /// - public virtual RoleSettings Settings { get; set; } = RoleSettings.Default; - /// /// Gets or sets the . /// + [Description("The escape settings for the custom role.")] public virtual List EscapeSettings { get; set; } = new(); /// /// Gets or sets a value representing the maximum instances of the that can be automatically assigned. /// + [Description("The maximum number of instances for the custom role.")] public virtual int MaxInstances { get; set; } /// @@ -172,31 +179,43 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// This property specifies the teams the belongs to. /// + [Description("The required teams for the custom role to win.")] public virtual Team[] TeamsOwnership { get; set; } = { }; /// /// Gets or sets the from which to retrieve players for assigning the . /// + [Description("The spawnable team type for assigning players to the custom role.")] public virtual SpawnableTeamType AssignFromTeam { get; set; } = SpawnableTeamType.None; /// /// Gets or sets the from which to retrieve players for assigning the . /// + [Description("The role type ID for assigning players to the custom role.")] public virtual RoleTypeId AssignFromRole { get; set; } /// - /// Gets or sets all roles to override, preventing the specified roles to spawn. + /// Gets or sets all roles to override, preventing the specified roles from spawning. /// + [Description("All roles to override and prevent from spawning.")] public virtual RoleTypeId[] OverrideScps { get; set; } /// /// Gets or sets a value indicating whether the should be treated as a separate team unit. /// + [Description("Indicates whether the custom role should be treated as a separate team unit.")] public virtual bool IsTeamUnit { get; set; } + /// + /// Gets or sets the . + /// + [Description("The role settings associated with the custom role.")] + public virtual RoleSettings Settings { get; set; } = RoleSettings.Default; + /// /// Gets or sets a value indicating whether the should be considered an SCP. /// + [Description("Indicates whether the custom role should be considered an SCP.")] public virtual bool IsScp { get; set; } /// @@ -215,6 +234,7 @@ public abstract class CustomRole : CustomModule, IAdditiveBehaviour /// /// Gets all the instances of this in the global context. /// + [YamlIgnore] public int GlobalInstances { get; private set; } /// @@ -554,7 +574,7 @@ public static bool Spawn(Pawn player, string name, bool preservePosition = false if (!TryGet(name, out CustomRole customRole)) return false; - Spawn(player, customRole, preservePosition, spawnReason, roleSpawnFlags); + customRole.Spawn(player, preservePosition, spawnReason, roleSpawnFlags); return true; } @@ -634,27 +654,16 @@ public static void Remove(IEnumerable players) /// /// Enables all the custom roles present in the assembly. /// + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. /// - /// This method dynamically enables all custom roles found in the calling assembly. Custom roles - /// must be marked with the to be considered for enabling. If - /// a custom role is enabled successfully, it is added to the returned list. + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom roles present in the assembly. - /// - /// The assembly to enable the roles from. - /// - /// This method dynamically enables all custom roles found in the calling assembly. Custom roles - /// must be marked with the to be considered for enabling. - /// - public static void EnableAll(Assembly assembly) + public static int EnableAll(Assembly assembly = null) { - if (CustomModules.Instance.Config.Modules is null || !CustomModules.Instance.Config.Modules.Contains("CustomRoles")) - throw new Exception("ModuleType::CustomRoles must be enabled in order to load any custom roles"); + assembly ??= Assembly.GetCallingAssembly(); - Player.DEFAULT_ROLE_BEHAVIOUR = typeof(RoleBehaviour); List customRoles = new(); foreach (Type type in assembly.GetTypes()) { @@ -672,23 +681,24 @@ public static void EnableAll(Assembly assembly) customRoles.Add(customRole); } - if (customRoles.Count != Registered.Count) - Log.Info($"{customRoles.Count} custom roles have been successfully registered!"); + return customRoles.Count; } /// /// Disables all the custom roles present in the assembly. /// + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. /// - /// This method dynamically disables all custom roles found in the calling assembly that were + /// This method dynamically disables all module instances found in the calling assembly that were /// previously registered. /// - public static void DisableAll() + public static int DisableAll(Assembly assembly = null) { + assembly ??= Assembly.GetCallingAssembly(); List customRoles = new(); - customRoles.AddRange(Registered.Where(customRole => customRole.TryUnregister())); - - Log.Info($"{customRoles.Count} custom roles have been successfully unregistered!"); + customRoles.AddRange(Registered.Where(customRole => customRole.GetType().Assembly == assembly && customRole.TryUnregister())); + return customRoles.Count; } /// @@ -728,7 +738,7 @@ public bool Spawn(Pawn player, bool preservePosition = false, SpawnReason spawnR return role.Spawn(player, preservePosition, spawnReason, roleSpawnFlags); object prevRole = player.CustomRole ? player.CustomRole.Id : player.Role.Type; - player.AddComponent(BehaviourComponent); + player.AddComponent(BehaviourComponent, $"ECS-{Name}"); PlayersValue.Remove(player); PlayersValue.Add(player, this); Instances += 1; @@ -807,6 +817,12 @@ protected override bool TryRegister(Assembly assembly, ModuleIdentifierAttribute return false; } + if (!typeof(RoleBehaviour).IsAssignableFrom(BehaviourComponent)) + { + Log.Error($"Unable to register {Name}. Behaviour Component must implement RoleBehaviour."); + return false; + } + if (attribute is not null && Id == 0) { if (attribute.Id != 0) @@ -860,21 +876,5 @@ protected override bool TryUnregister() return true; } - - private void ForceSpawn_Internal(Pawn player, bool preservePosition, SpawnReason spawnReason = null, RoleSpawnFlags roleSpawnFlags = RoleSpawnFlags.All) - { - Instances += 1; - RoleBehaviour roleBehaviour = EObject.CreateDefaultSubobject(BehaviourComponent, $"ECS-{Name}").Cast(); - roleBehaviour.Settings.PreservePosition = preservePosition; - - spawnReason ??= SpawnReason.ForceClass; - if (roleBehaviour.Settings.SpawnReason != spawnReason) - roleBehaviour.Settings.SpawnReason = spawnReason; - - if (roleSpawnFlags != roleBehaviour.Settings.SpawnFlags) - roleBehaviour.Settings.SpawnFlags = roleSpawnFlags; - - EActor ea = player.AddComponent(roleBehaviour); - } } } diff --git a/Exiled.CustomModules/API/Features/CustomRoles/CustomTeam.cs b/Exiled.CustomModules/API/Features/CustomRoles/CustomTeam.cs index ed15c39d84..2fe3660473 100644 --- a/Exiled.CustomModules/API/Features/CustomRoles/CustomTeam.cs +++ b/Exiled.CustomModules/API/Features/CustomRoles/CustomTeam.cs @@ -9,6 +9,7 @@ namespace Exiled.CustomModules.API.Features.CustomRoles { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reflection; @@ -68,16 +69,19 @@ public abstract class CustomTeam : CustomModule /// /// Gets or sets the name of the . /// + [Description("The name of the custom team.")] public override string Name { get; set; } /// /// Gets or sets the 's id. /// + [Description("The custom team's id.")] public override uint Id { get; set; } /// /// Gets or sets a value indicating whether the is enabled. /// + [Description("Indicates whether the custom team is enabled.")] public override bool IsEnabled { get; set; } /// @@ -86,6 +90,7 @@ public abstract class CustomTeam : CustomModule /// /// The display name is used to represent the in user interfaces and other visual contexts. /// + [Description("The display name of the custom team.")] public virtual string DisplayName { get; set; } /// @@ -94,6 +99,7 @@ public abstract class CustomTeam : CustomModule /// /// The display color is the visual representation of the name color in user interfaces and other visual contexts. /// + [Description("The display color of the custom team's name.")] public virtual string DisplayColor { get; set; } /// @@ -102,88 +108,79 @@ public abstract class CustomTeam : CustomModule /// /// The size indicates the maximum number of players that can be part of this . /// + [Description("The maximum size of the custom team.")] public virtual int Size { get; set; } /// /// Gets or sets a collection of ids representing all custom roles offered as units. /// - /// - /// This property provides access to a curated collection of objects, encapsulating all available custom role within the context of units. - ///
The collection is designed to be both queried and modified as needed to accommodate dynamic scenarios within the game architecture. - ///
+ [Description("The collection of custom role ids offered as units.")] public virtual IEnumerable Units { get; set; } = new uint[] { }; /// - /// Gets or sets the minimum amount time after which any team will be allowed to spawn. + /// Gets or sets the minimum amount of time after which any team will be allowed to spawn. /// + [Description("The minimum time before the team can spawn.")] public virtual float MinNextSequenceTime { get; set; } = GameCore.ConfigFile.ServerConfig.GetFloat("minimum_MTF_time_to_spawn", 280f); /// - /// Gets or sets the maximum amount time after which any team will be spawned. + /// Gets or sets the maximum amount of time after which any team will be spawned. /// + [Description("The maximum time before the team can spawn.")] public virtual float MaxNextSequenceTime { get; set; } = GameCore.ConfigFile.ServerConfig.GetFloat("maximum_MTF_time_to_spawn", 350f); /// /// Gets or sets the relative spawn probability of the . /// + [Description("The spawn probability of the custom team.")] public virtual int Probability { get; set; } /// /// Gets or sets the which is being spawned from. /// + [Description("The spawnable team types from which the custom team can be spawned.")] public virtual SpawnableTeamType[] SpawnableFromTeams { get; set; } /// /// Gets or sets a value indicating whether the is configured to use respawn tickets. /// - /// - /// If set to true, the utilizes a ticket system for player respawns. - /// + [Description("Indicates whether the custom team uses respawn tickets.")] public virtual bool UseTickets { get; set; } /// /// Gets or sets the current number of respawn tickets available for the . /// - /// - /// This property represents the remaining number of respawn tickets that can be used by the . - /// + [Description("The number of respawn tickets available for the custom team.")] public virtual uint Tickets { get; set; } /// /// Gets or sets the required that players must belong to in order to allow the to spawn. /// - /// - /// This property specifies the required alive team to be eligible for spawning in the . - /// + [Description("The required team for spawning the custom team.")] public virtual Team RequiredTeamToSpawn { get; set; } = Team.Dead; /// /// Gets or sets the required that players must have to allow the to spawn. /// - /// - /// This property specifies the required role type for players to be eligible for spawning in the . - /// + [Description("The required role type for spawning the custom team.")] public virtual RoleTypeId RequiredRoleToSpawn { get; set; } = RoleTypeId.None; /// /// Gets or sets the required custom team that players must belong to in order to allow the to spawn. /// - /// - /// This property specifies the required alive custom team to be eligible for spawning in the . - /// + [Description("The required custom team for spawning the custom team.")] public virtual uint RequiredCustomTeamToSpawn { get; set; } /// /// Gets or sets the required that players must have to allow the to spawn. /// - /// - /// This property specifies the required custom role for players to be eligible for spawning in the . - /// + [Description("The required custom role for spawning the custom team.")] public virtual uint RequiredCustomRoleToSpawn { get; set; } /// /// Gets or sets the teams the belongs to. /// + [Description("The teams that the custom team belongs to.")] public virtual Team[] TeamsOwnership { get; set; } = { }; /// @@ -455,16 +452,15 @@ public static bool TrySpawn(int amount, uint id) /// /// Enables all the custom teams present in the assembly. /// - public static void EnableAll() => EnableAll(Assembly.GetCallingAssembly()); - - /// - /// Enables all the custom teams present in the assembly. - /// - /// The assembly to enable the teams from. - public static void EnableAll(Assembly assembly) + /// The assembly to enable the module instances from. + /// The amount of enabled module instances. + /// + /// This method dynamically enables all module instances found in the calling assembly that were + /// not previously registered. + /// + public static int EnableAll(Assembly assembly = null) { - if (!CustomModules.Instance.Config.Modules.Contains(UUModuleType.CustomTeams.Name)) - throw new Exception("ModuleType::CustomTeams must be enabled in order to load any custom teams"); + assembly ??= Assembly.GetCallingAssembly(); List customTeams = new(); foreach (Type type in assembly.GetTypes()) @@ -483,19 +479,25 @@ public static void EnableAll(Assembly assembly) customTeams.Add(customTeam); } - if (customTeams.Count() != Registered.Count()) - Log.SendRaw($"{customTeams.Count()} custom teams have been successfully registered!", ConsoleColor.Cyan); + return customTeams.Count; } /// /// Disables all the custom teams present in the assembly. /// - public static void DisableAll() + /// The assembly to disable the module instances from. + /// The amount of disabled module instances. + /// + /// This method dynamically disables all module instances found in the calling assembly that were + /// previously registered. + /// + public static int DisableAll(Assembly assembly = null) { - List customTeams = new(); - customTeams.AddRange(Registered.Where(customTeam => customTeam.TryUnregister())); + assembly ??= Assembly.GetCallingAssembly(); - Log.SendRaw($"{customTeams.Count()} custom teams have been successfully unregistered!", ConsoleColor.Cyan); + List customTeams = new(); + customTeams.AddRange(Registered.Where(customTeam => customTeam.GetType().Assembly == assembly && customTeam.TryUnregister())); + return customTeams.Count; } /// diff --git a/Exiled.CustomModules/API/Features/CustomRoles/RoleBehaviour.cs b/Exiled.CustomModules/API/Features/CustomRoles/RoleBehaviour.cs index 39a9fbd90f..9205df9613 100644 --- a/Exiled.CustomModules/API/Features/CustomRoles/RoleBehaviour.cs +++ b/Exiled.CustomModules/API/Features/CustomRoles/RoleBehaviour.cs @@ -24,6 +24,7 @@ namespace Exiled.CustomModules.API.Features.CustomRoles using Exiled.API.Features.Spawn; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features.CustomEscapes; + using Exiled.CustomModules.API.Features.Generic; using Exiled.CustomModules.API.Features.Inventory; using Exiled.Events.EventArgs.Map; using Exiled.Events.EventArgs.Player; @@ -34,7 +35,7 @@ namespace Exiled.CustomModules.API.Features.CustomRoles /// Represents the base class for custom role behaviors. /// /// - /// This class extends and implements . + /// This class extends and implements . ///
It provides a foundation for creating custom behaviors associated with in-game player roles. ///
public class RoleBehaviour : ModuleBehaviour, IAdditiveSettings @@ -45,6 +46,7 @@ public class RoleBehaviour : ModuleBehaviour, IAdditiveSettings /// Gets a of containing all players to be spawned without affecting their current position (static). @@ -64,7 +66,18 @@ public class RoleBehaviour : ModuleBehaviour, IAdditiveSettings /// Gets or sets the . ///
- public virtual RoleSettings Settings { get; set; } + public RoleSettings Settings + { + get => settings ??= CustomRole.Settings; + set => settings = value; + } + + /// + public override ModulePointer Config + { + get => config ??= CustomRole.Config; + protected set => config = value; + } /// /// Gets a random spawn point based on existing settings. @@ -154,12 +167,12 @@ protected virtual RoleTypeId FakeAppearance /// /// Gets or sets the handling all bound delegates to be fired before escaping. /// - protected TDynamicEventDispatcher EscapingEventDispatcher { get; set; } + protected TDynamicEventDispatcher EscapingEventDispatcher { get; set; } = new(); /// /// Gets or sets the handling all bound delegates to be fired after escaping. /// - protected TDynamicEventDispatcher EscapedEventDispatcher { get; set; } + protected TDynamicEventDispatcher EscapedEventDispatcher { get; set; } = new(); /// /// Gets a value indicating whether the specified is allowed. @@ -214,26 +227,21 @@ public virtual bool EvaluateEndingConditions() /// public virtual void AdjustAdditivePipe() { - ImplementConfigs(); - if (CustomTeam.TryGet(Owner.Cast(), out CustomTeam customTeam)) CustomTeam = customTeam; - if (CustomRole.TryGet(GetType(), out CustomRole customRole) && customRole.Settings is RoleSettings settings) - { + if (CustomRole.TryGet(GetType(), out CustomRole customRole)) CustomRole = customRole; - if (customRole is null || customRole.Config is null) - Settings = settings; - } - - if (CustomRole is null || Settings is null) + if (CustomRole is null || Settings is null || Config is null) { Log.Error($"Custom role ({GetType().Name}) has invalid configuration."); Destroy(); return; } + ImplementConfigs(); + Owner.UniqueRole = CustomRole.Name; // TODO: Owner.TryAddCustomRoleFriendlyFire(Name, Settings.FriendlyFireMultiplier); @@ -244,16 +252,6 @@ public virtual void AdjustAdditivePipe() } } - /// - protected override void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue( - typeof(RoleSettings).IsAssignableFrom(targetInfo.DeclaringType) ? Settings : - typeof(InventoryManager).IsAssignableFrom(targetInfo.DeclaringType) ? Inventory : - this, - propertyInfo.GetValue(Config, null)); - } - /// protected override void PostInitialize() { @@ -355,9 +353,9 @@ protected override void PostInitialize() protected override void OnBeginPlay() { base.OnBeginPlay(); + if (!Owner) { - Log.WarnWithContext("Owner is null"); Destroy(); return; } diff --git a/Exiled.CustomModules/API/Features/CustomRoles/RoleSettings.cs b/Exiled.CustomModules/API/Features/CustomRoles/RoleSettings.cs index 24a1c47ea1..e9614ebf51 100644 --- a/Exiled.CustomModules/API/Features/CustomRoles/RoleSettings.cs +++ b/Exiled.CustomModules/API/Features/CustomRoles/RoleSettings.cs @@ -8,6 +8,7 @@ namespace Exiled.CustomModules.API.Features.CustomRoles { using System.Collections.Generic; + using System.ComponentModel; using Exiled.API.Enums; using Exiled.API.Features; @@ -54,231 +55,276 @@ public class RoleSettings : TypeCastObject, IAdditiveProperty /// /// Gets or sets a value indicating whether the player's role is dynamic. /// + [Description("Indicates whether the player's role is dynamic.")] public virtual bool IsRoleDynamic { get; set; } = false; /// /// Gets or sets a value indicating whether the player's role should use the specified only. /// + [Description("Indicates whether the player's role should use the specified Role only.")] public virtual bool UseDefaultRoleOnly { get; set; } = true; /// /// Gets or sets a for the console message given to players when they receive a role. /// + [Description("The console message given to players when they receive a role.")] public virtual string ConsoleMessage { get; set; } = "You have spawned as {role}!"; /// /// Gets or sets the scale. /// + [Description("The scale of the player.")] public virtual float Scale { get; set; } = 1f; /// /// Gets or sets the initial health. /// + [Description("The initial health of the player.")] public virtual float Health { get; set; } = 100f; /// /// Gets or sets the max health. /// + [Description("The maximum health of the player.")] public virtual int MaxHealth { get; set; } = 100; /// /// Gets or sets the initial artificial health. /// + [Description("The initial artificial health of the player.")] public virtual float ArtificialHealth { get; set; } = 0f; /// /// Gets or sets the max artificial health. /// + [Description("The maximum artificial health of the player.")] public virtual float MaxArtificialHealth { get; set; } = 100f; /// /// Gets or sets the text to be displayed as soon as the role is assigned. /// + [Description("The text to be displayed as soon as the role is assigned.")] public virtual TextDisplay SpawnedText { get; set; } /// /// Gets or sets the . /// + [Description("The spawn properties for the role.")] public virtual SpawnProperties SpawnProperties { get; set; } = new(); /// /// Gets or sets the unique defining the role to be assigned. /// + [Description("The unique role identifier to be assigned.")] public virtual RoleTypeId UniqueRole { get; set; } /// /// Gets or sets a value indicating whether the assignment should maintain the player's current position. /// + [Description("Indicates whether the role assignment should maintain the player's current position.")] public virtual bool PreservePosition { get; set; } /// /// Gets or sets the . /// + [Description("The flags indicating special conditions for role spawning.")] public virtual RoleSpawnFlags SpawnFlags { get; set; } = RoleSpawnFlags.All; /// - /// Gets or sets the . + /// Gets or sets the . /// + [Description("The reason for the role change.")] public virtual RoleChangeReason SpawnReason { get; set; } = RoleChangeReason.RemoteAdmin; /// /// Gets or sets a value indicating whether the assignment should maintain the player's current inventory. /// + [Description("Indicates whether the role assignment should maintain the player's current inventory.")] public virtual bool PreserveInventory { get; set; } /// /// Gets or sets a [] containing all dynamic roles. /// Dynamic roles are specific roles that, if assigned, do not result in the removal of the from the player. /// + [Description("Array of dynamic roles that, if assigned, do not result in removal of the custom role.")] public virtual RoleTypeId[] DynamicRoles { get; set; } = new RoleTypeId[] { }; /// /// Gets or sets a [] containing all the ignored damage types. /// + [Description("Array of damage types that are ignored.")] public virtual DamageType[] IgnoredDamageTypes { get; set; } = new DamageType[] { }; /// /// Gets or sets a [] containing all the allowed damage types. /// + [Description("Array of damage types that are allowed.")] public virtual DamageType[] AllowedDamageTypes { get; set; } = new DamageType[] { }; /// /// Gets or sets a [] containing all doors that can be bypassed. /// + [Description("Array of door types that can be bypassed.")] public virtual DoorType[] BypassableDoors { get; set; } = new DoorType[] { }; /// /// Gets or sets a containing cached and their which is cached Role with FF multiplier. /// + [Description("Dictionary containing cached roles with their friendly fire multiplier.")] public virtual Dictionary FriendlyFireMultiplier { get; set; } = new(); /// /// Gets or sets a value indicating whether SCPs can be hurt. /// + [Description("Indicates whether SCPs can be hurt.")] public virtual bool CanHurtScps { get; set; } = true; /// /// Gets or sets a value indicating whether SCPs can hurt the owner. /// + [Description("Indicates whether SCPs can hurt the owner.")] public virtual bool CanBeHurtByScps { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can enter pocket dimension. /// + [Description("Indicates whether the owner can enter the pocket dimension.")] public virtual bool CanEnterPocketDimension { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can use intercom. /// + [Description("Indicates whether the owner can use the intercom.")] public virtual bool CanUseIntercom { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can use the voicechat. /// + [Description("Indicates whether the owner can use voice chat.")] public virtual bool CanUseVoiceChat { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can place blood. /// + [Description("Indicates whether the owner can place blood.")] public virtual bool CanPlaceBlood { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can be handcuffed. /// + [Description("Indicates whether the owner can be handcuffed.")] public virtual bool CanBeHandcuffed { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can use elevators. /// + [Description("Indicates whether the owner can use elevators.")] public virtual bool CanUseElevators { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can bypass checkpoints. /// + [Description("Indicates whether the owner can bypass checkpoints.")] public virtual bool CanBypassCheckpoints { get; set; } = false; /// /// Gets or sets a value indicating whether the owner can activate warhead. /// + [Description("Indicates whether the owner can activate the warhead.")] public virtual bool CanActivateWarhead { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can activate workstations. /// + [Description("Indicates whether the owner can activate workstations.")] public virtual bool CanActivateWorkstations { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can activate generators. /// + [Description("Indicates whether the owner can activate generators.")] public virtual bool CanActivateGenerators { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can pickup items. /// + [Description("Indicates whether the owner can pick up items.")] public virtual bool CanPickupItems { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can drop items. /// + [Description("Indicates whether the owner can drop items.")] public virtual bool CanDropItems { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can select items from their inventory. /// + [Description("Indicates whether the owner can select items from their inventory.")] public virtual bool CanSelectItems { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can look at Scp173. /// + [Description("Indicates whether the owner can look at SCP-173.")] public virtual bool DoesLookingAffectScp173 { get; set; } = true; /// /// Gets or sets a value indicating whether the owner can trigger Scp096. /// + [Description("Indicates whether the owner can trigger SCP-096.")] public virtual bool DoesLookingAffectScp096 { get; set; } = true; /// /// Gets or sets the custom info. /// + [Description("Custom information related to the owner.")] public virtual string CustomInfo { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether the should be hidden. /// + [Description("Indicates whether the PlayerInfoArea should be hidden.")] public virtual bool HideInfoArea { get; set; } = false; /// /// Gets or sets a value indicating whether the C.A.S.S.I.E death announcement should be played when the owner dies. /// + [Description("Indicates whether the C.A.S.S.I.E death announcement should be played when the owner dies.")] public virtual bool IsDeathAnnouncementEnabled { get; set; } = false; /// /// Gets or sets the C.A.S.S.I.E announcement to be played when the owner dies from an unhandled or unknown termination cause. /// + [Description("The C.A.S.S.I.E announcement to be played when the owner dies from an unhandled or unknown cause.")] public virtual string UnknownTerminationCauseAnnouncement { get; set; } = string.Empty; /// /// Gets or sets a containing all the C.A.S.S.I.E announcements /// to be played when the owner gets killed by a player with the corresponding . /// + [Description("Dictionary containing announcements for when the owner is killed by a player with a specific RoleTypeId.")] public virtual Dictionary KilledByRoleAnnouncements { get; set; } = new(); /// /// Gets or sets a containing all the C.A.S.S.I.E announcements /// to be played when the owner gets killed by a player with the corresponding . /// + [Description("Dictionary containing announcements for when the owner is killed by a player with a specific custom role.")] public virtual Dictionary KilledByCustomRoleAnnouncements { get; set; } = new(); /// /// Gets or sets a containing all the C.A.S.S.I.E announcements /// to be played when the owner gets killed by a player belonging to the corresponding . /// + [Description("Dictionary containing announcements for when the owner is killed by a player from a specific team.")] public virtual Dictionary KilledByTeamAnnouncements { get; set; } = new(); /// /// Gets or sets a containing all the C.A.S.S.I.E announcements /// to be played when the owner gets killed by a player belonging to the corresponding . /// + [Description("Dictionary containing announcements for when the owner is killed by a player from a specific custom team.")] public virtual Dictionary KilledByCustomTeamAnnouncements { get; set; } = new(); } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/CustomRoles/SummaryInfo.cs b/Exiled.CustomModules/API/Features/CustomRoles/SummaryInfo.cs index 324e621636..44a6e2462d 100644 --- a/Exiled.CustomModules/API/Features/CustomRoles/SummaryInfo.cs +++ b/Exiled.CustomModules/API/Features/CustomRoles/SummaryInfo.cs @@ -46,7 +46,7 @@ public static SummaryInfo GetSummary() foreach (Player alive in Player.List) { - if (alive is not Pawn pawn || Round.IgnoredPlayers.Contains(alive)) + if (alive is not Pawn || Round.IgnoredPlayers.Contains(alive)) continue; switch (RoleExtensions.GetTeam(alive.Role.Type)) @@ -62,6 +62,10 @@ public static SummaryInfo GetSummary() case Team.SCPs: ++summary.Anomalies; break; + case Team.Dead: + break; + case Team.OtherAlive: + break; default: ++summary.Neutral; break; @@ -78,7 +82,7 @@ public void Update() { foreach (Player alive in Player.List) { - if (alive is not Pawn pawn || Round.IgnoredPlayers.Contains(alive)) + if (alive is not Pawn || Round.IgnoredPlayers.Contains(alive)) continue; switch (RoleExtensions.GetTeam(alive.Role.Type)) @@ -94,6 +98,10 @@ public void Update() case Team.SCPs: ++Anomalies; break; + case Team.Dead: + break; + case Team.OtherAlive: + break; default: ++Neutral; break; diff --git a/Exiled.CustomModules/API/Features/Deserializers/Inheritables/AdditivePropertyDeserializer.cs b/Exiled.CustomModules/API/Features/Deserializers/Inheritables/AdditivePropertyDeserializer.cs deleted file mode 100644 index 227267f433..0000000000 --- a/Exiled.CustomModules/API/Features/Deserializers/Inheritables/AdditivePropertyDeserializer.cs +++ /dev/null @@ -1,57 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.Deserializers.Inheritables -{ - using System; - using System.Reflection; - - using Exiled.API.Features.Core.Interfaces; - using Exiled.CustomModules.API.Features.CustomRoles; - using YamlDotNet.Core; - using YamlDotNet.Core.Events; - - /// - /// The deserializer for Role Settings. - /// - public class AdditivePropertyDeserializer : ModuleParser - { - /// - public override ParserContext.ModuleDelegate Delegate { get; set; } = Deserialize; - - /// - /// The actual deserializer. - /// - /// The context. - /// The output object (if successful). - /// A bool stating if it was successful or not. - public static bool Deserialize(in ParserContext ctx, out object value) - { - IAdditiveProperty additiveProperty = Activator.CreateInstance(ctx.ExpectedType) as IAdditiveProperty; - ctx.Parser.Consume(); - - while (ctx.Parser.TryConsume(out Scalar scalar)) - { - PropertyInfo property = typeof(RoleSettings).GetProperty(scalar.Value, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (property != null) - { - object propertyValue = ctx.NestedObjectDeserializer(ctx.Parser, property.PropertyType); - property.SetValue(additiveProperty, propertyValue); - } - else - { - // If the property does not exist, skip the scalar value - ctx.Parser.SkipThisAndNestedEvents(); - } - } - - ctx.Parser.Consume(); - value = additiveProperty; - return true; - } - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/Deserializers/ModuleParser.cs b/Exiled.CustomModules/API/Features/Deserializers/ModuleParser.cs deleted file mode 100644 index 2145e919f8..0000000000 --- a/Exiled.CustomModules/API/Features/Deserializers/ModuleParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.Deserializers -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - - using Exiled.API.Features; - using JetBrains.Annotations; - - /// - /// An inheritable class that declares an additional deserializer. - /// - public abstract class ModuleParser - { - /// - /// Initializes a new instance of the class. - /// - protected ModuleParser() - { - ParserContext.Delegates.Add(Delegate); - } - - /// - /// Gets or sets the delegate for parsing. - /// - [NotNull] - public abstract ParserContext.ModuleDelegate Delegate { get; set; } - - /// - /// Registers all module parsers. - /// - public static void InstantiateModuleParsers() - { - Log.Debug("Registering Custom Module Deserializers:"); - - // Get the current assembly - Assembly assembly = typeof(ModuleParser).Assembly; - - // Get all types that inherit from ModuleParser - IEnumerable moduleParserTypes = assembly.GetTypes() - .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(ModuleParser))); - - // Instantiate each type with no parameters - foreach (Type type in moduleParserTypes) - { - Log.Debug(type.Name); - Activator.CreateInstance(type); - } - } - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/Deserializers/ParserContext.cs b/Exiled.CustomModules/API/Features/Deserializers/ParserContext.cs deleted file mode 100644 index da728ccd38..0000000000 --- a/Exiled.CustomModules/API/Features/Deserializers/ParserContext.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.Deserializers -{ - using System; - using System.Collections.Generic; - - using YamlDotNet.Core; - -#pragma warning disable SA1401 - - /// - /// A context for deserializer parsing. - /// - public class ParserContext - { - /// - /// A list of functions that should be checked when deserializing a module. - /// - public static readonly List Delegates = new(); - - /// - /// The parser. - /// - public readonly IParser Parser; - - /// - /// The expected type. - /// - public readonly Type ExpectedType; - - /// - /// The fallback deserializer. - /// - public readonly Func NestedObjectDeserializer; - - /// - /// Initializes a new instance of the class. - /// - /// The Parser. - /// The type expected. - /// The fallback deserializer. - public ParserContext( - IParser parser, - Type expectedType, - Func nestedObjectDeserializer) - { - Parser = parser; - ExpectedType = expectedType; - NestedObjectDeserializer = nestedObjectDeserializer; - } - - /// - /// A delegate returning bool retaining to a module. - /// - /// The parser context. - /// The output object if successful. - /// A bool stating if parsing was successful or not. - public delegate bool ModuleDelegate(in ParserContext input, out object output); - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/Generic/ModuleBehaviour.cs b/Exiled.CustomModules/API/Features/Generic/ModuleBehaviour.cs new file mode 100644 index 0000000000..a0041771f2 --- /dev/null +++ b/Exiled.CustomModules/API/Features/Generic/ModuleBehaviour.cs @@ -0,0 +1,144 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Features.Generic +{ + using System; + using System.Reflection; + + using Exiled.API.Features.Core; + using Exiled.API.Features.Core.Generic; + + /// + /// Represents a marker class for a module's pointer. + /// + /// The type of the entity to which the module behaviour is applied. + /// + /// This abstract class serves as a foundation for user-defined module behaviours that can be applied to entities (such as playable characters) + /// to extend and customize their functionality through custom modules. It provides a modular and extensible architecture for enhancing gameplay elements. + /// + public abstract class ModuleBehaviour : EBehaviour + where TEntity : GameEntity + { +#pragma warning disable SA1310 +#pragma warning disable SA1401 + /// + /// The binding flags in use to implement configs through reflection. + /// + internal const BindingFlags CONFIG_IMPLEMENTATION_BINDING_FLAGS = + BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | + BindingFlags.Instance | BindingFlags.DeclaredOnly; + + /// + /// The behaviour's config. + /// + protected ModulePointer config; +#pragma warning restore SA1401 +#pragma warning restore SA1310 + + /// + /// Gets or sets the behaviour's config. + /// + public virtual ModulePointer Config + { + get => config; + protected set => config = value; + } + + /// + /// Implements the behaviour's configs by copying properties from the config object to the current instance. + /// + /// The instance of the object to implement the configs to. + /// The or any config object to be implemented. + public static void ImplementConfigs_DefaultImplementation(object instance, object config) + { + Type inType = instance.GetType(); + + foreach (PropertyInfo propertyInfo in config.GetType().GetProperties(CONFIG_IMPLEMENTATION_BINDING_FLAGS)) + { + PropertyInfo targetProperty = inType.GetProperty(propertyInfo.Name, CONFIG_IMPLEMENTATION_BINDING_FLAGS); + + if (targetProperty is null || targetProperty.PropertyType != propertyInfo.PropertyType) + continue; + + if (targetProperty.PropertyType.IsClass && targetProperty.PropertyType != typeof(string)) + { + object targetInstance = targetProperty.GetValue(instance); + + if (targetInstance is not null) + ApplyConfig_DefaultImplementation(propertyInfo.GetValue(config), targetInstance); + } + else + { + if (targetProperty.CanWrite) + targetProperty.SetValue(instance, propertyInfo.GetValue(config)); + } + } + } + + /// + /// Applies a configuration property value from the source property to the target property. + /// + /// The source property value from the config object. + /// The target instance where the config should be applied. + public static void ApplyConfig_DefaultImplementation(object source, object target) + { + if (source is null || target is null) + return; + + Type sourceType = source.GetType(); + Type targetType = target.GetType(); + + foreach (PropertyInfo sourceProperty in sourceType.GetProperties(CONFIG_IMPLEMENTATION_BINDING_FLAGS)) + { + PropertyInfo targetProperty = targetType.GetProperty(sourceProperty.Name); + + if (targetProperty is null || !targetProperty.CanWrite || targetProperty.PropertyType != sourceProperty.PropertyType) + continue; + + object value = sourceProperty.GetValue(source); + + if (value is not null && targetProperty.PropertyType.IsClass && targetProperty.PropertyType != typeof(string)) + { + object targetInstance = targetProperty.GetValue(target); + + if (targetInstance is null) + { + targetInstance = Activator.CreateInstance(targetProperty.PropertyType); + targetProperty.SetValue(target, targetInstance); + } + + ApplyConfig_DefaultImplementation(value, targetInstance); + } + else + { + targetProperty.SetValue(target, value); + } + } + } + + /// + /// Implements the behaviour's configs by copying properties from the config object to the current instance, + /// stopping at the current class type (including its base classes up to the current type). + /// + protected virtual void ImplementConfigs() + { + if (Config is null) + return; + + ImplementConfigs_DefaultImplementation(this, Config); + } + + /// + /// Applies the configuration from the source property to the target property by copying the value. + /// Handles nested objects and primitive types appropriately. + /// + /// The source property from which to copy the value. + /// The target property to which the value will be copied. + protected virtual void ApplyConfig(object source, object target) => ApplyConfig_DefaultImplementation(source, target); + } +} diff --git a/Exiled.CustomModules/API/Features/Generic/TrackerBase.cs b/Exiled.CustomModules/API/Features/Generic/TrackerBase.cs deleted file mode 100644 index 6a7f1b8c05..0000000000 --- a/Exiled.CustomModules/API/Features/Generic/TrackerBase.cs +++ /dev/null @@ -1,467 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features.Generic -{ - using System.Collections.Generic; - using System.Linq; - - using Exiled.API.Extensions; - using Exiled.API.Features.Attributes; - using Exiled.API.Features.Core; - using Exiled.API.Features.DynamicEvents; - using Exiled.API.Features.Items; - using Exiled.API.Features.Pickups; - using Exiled.CustomModules.API.Interfaces; - using Exiled.CustomModules.Events.EventArgs.Tracking; - using Exiled.Events.EventArgs.Map; - using Exiled.Events.EventArgs.Player; - - /// - /// The actor which handles all tracking-related tasks for items. - /// - /// The type of the . - public class TrackerBase : StaticActor - where T : ITrackable - { - /// - /// Gets or sets the which handles all the delegates fired when an item is added. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher ItemAddedDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when an item is removed. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher ItemRemovedDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when an item is restored. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher ItemRestoredDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when an item tracking is modified. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher ItemTrackingModifiedDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when a pickup is added. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher PickupAddedDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when a pickup is removed. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher PickupRemovedDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when a pickup is restored. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher PickupRestoredDispatcher { get; set; } - - /// - /// Gets or sets the which handles all the delegates fired when a pickup tracking is modified. - /// - [DynamicEventDispatcher] - public TDynamicEventDispatcher PickupTrackingModifiedDispatcher { get; set; } - - /// - /// Gets a containing all serials and their corresponding items. - /// - private Dictionary> TrackedItemSerials { get; } = new(); - - /// - /// Gets a containing all serials and their corresponding items. - /// - private Dictionary> TrackedPickupSerials { get; } = new(); - - /// - /// Adds or tracks the trackables of an item based on its serial. - /// - /// The whose trackables are to be added or tracked. - /// if the item was added or tracked successfully; otherwise, . - public virtual bool AddOrTrack(Item item) - { - if (!item) - return false; - - IEnumerable trackableBehaviours = item.GetComponents(); - if (trackableBehaviours.IsEmpty()) - return false; - - if (TrackedItemSerials.ContainsKey(item.Serial)) - { - IEnumerable previousTrackableItems = TrackedItemSerials[item.Serial]; - TrackedItemSerials[item.Serial].AddRange(trackableBehaviours.Cast()); - ItemTrackingModifiedEventArgs ev = new(item, previousTrackableItems.Cast(), TrackedItemSerials[item.Serial].Cast()); - ItemTrackingModifiedDispatcher.InvokeAll(ev); - - return true; - } - - TrackedItemSerials.Add(item.Serial, new HashSet()); - TrackedItemSerials[item.Serial].AddRange(trackableBehaviours.Cast()); - - ItemAddedDispatcher.InvokeAll(item); - - return true; - } - - /// - /// Adds or tracks the trackables of a pickup based on its serial. - /// - /// The whose trackables are to be added or tracked. - /// if the pickup was added or tracked successfully; otherwise, . - public virtual bool AddOrTrack(Pickup pickup) - { - if (!pickup) - return false; - - IEnumerable trackableBehaviours = pickup.GetComponents(); - if (trackableBehaviours.IsEmpty()) - return false; - - if (TrackedPickupSerials.ContainsKey(pickup.Serial)) - { - IEnumerable previousTrackableItems = TrackedPickupSerials[pickup.Serial]; - TrackedPickupSerials[pickup.Serial].AddRange(trackableBehaviours.Cast()); - PickupTrackingModifiedEventArgs ev = new(pickup, previousTrackableItems.Cast(), TrackedPickupSerials[pickup.Serial].Cast()); - PickupTrackingModifiedDispatcher.InvokeAll(ev); - - return true; - } - - TrackedPickupSerials.Add(pickup.Serial, new HashSet()); - TrackedPickupSerials[pickup.Serial].AddRange(trackableBehaviours.Cast()); - - PickupAddedDispatcher.InvokeAll(pickup); - - return true; - } - - /// - /// Adds or tracks the trackables of both pickups and items based on their serial. - /// - /// The objects whose trackables are to be added or tracked. - public virtual void AddOrTrack(params object[] objects) - { - foreach (object @object in objects) - { - if (@object is Pickup pickup) - { - AddOrTrack(pickup); - continue; - } - - if (@object is Item item) - AddOrTrack(item); - } - } - - /// - /// Restores all trackables from a pickup which is being tracked. - /// - /// The serial to restore all trackables from. - /// The item to reapply the trackables to. - /// if the pickup was restored successfully; otherwise, . - public virtual bool Restore(ushort serial, Item item) - { - if (!item || !TrackedItemSerials.TryGetValue(serial, out HashSet itemSerial)) - return false; - - foreach (T behaviour in itemSerial) - { - if (behaviour is not EActor component) - continue; - - item.AddComponent(component); - } - - ItemRestoredDispatcher.InvokeAll(item); - - return true; - } - - /// - /// Restores all trackables from a pickup which is being tracked. - /// - /// The whose trackables are to be restored. - /// if the pickup was restored successfully; otherwise, . - public virtual bool Restore(Pickup pickup) - { - if (!pickup || !TrackedPickupSerials.TryGetValue(pickup.Serial, out HashSet serial)) - return false; - - foreach (T behaviour in serial) - { - if (behaviour is not EActor component) - continue; - - pickup.AddComponent(component); - } - - PickupRestoredDispatcher.InvokeAll(pickup); - - return true; - } - - /// - /// Restores all trackables from a pickup which is being tracked. - /// - /// The whose trackables are to be restored. - /// The whose trackables are to be transfered. - /// if the pickup was restored successfully; otherwise, . - public virtual bool Restore(Pickup pickup, Item item) - { - if (!pickup || !item || !TrackedPickupSerials.ContainsKey(pickup.Serial) || - !TrackedItemSerials.TryGetValue(item.Serial, out HashSet serial)) - return false; - - foreach (T behaviour in serial) - { - if (behaviour is not EActor component) - continue; - - pickup.AddComponent(component); - } - - PickupRestoredDispatcher.InvokeAll(pickup); - - return true; - } - - /// - /// Restores all trackables from a item which is being tracked. - /// - /// The whose trackables are to be restored. - /// The whose trackables are to be transfered. - /// if the item was restored successfully; otherwise, . - public virtual bool Restore(Item item, Pickup pickup) - { - if (!pickup || !item || !TrackedPickupSerials.ContainsKey(pickup.Serial) || - !TrackedItemSerials.ContainsKey(item.Serial)) - return false; - - foreach (T behaviour in TrackedPickupSerials[pickup.Serial]) - { - if (behaviour is not EActor component) - continue; - - pickup.AddComponent(component); - } - - PickupRestoredDispatcher.InvokeAll(pickup); - - return true; - } - - /// - /// Removes an item and all its trackables from the tracking. - /// - /// The item to be removed. - public virtual void Remove(Item item) - { - if (TrackedItemSerials.ContainsKey(item.Serial)) - { - TrackedItemSerials.Remove(item.Serial); - - item.GetComponents().ForEach(c => - { - if (c is EActor actor) - actor.Destroy(); - }); - - ItemRemovedDispatcher.InvokeAll(item.Serial); - } - } - - /// - /// Removes an ability from the tracking. - /// - /// The item owning the ability. - /// The to be removed. - public virtual void Remove(Item item, T behaviour) - { - if (TrackedItemSerials.ContainsKey(item.Serial)) - { - IEnumerable previousTrackedItems = TrackedItemSerials[item.Serial]; - TrackedItemSerials[item.Serial].Remove(behaviour); - item.GetComponent(behaviour.GetType()).Destroy(); - ItemTrackingModifiedEventArgs ev = new(item, previousTrackedItems.Cast(), TrackedItemSerials[item.Serial].Cast()); - ItemTrackingModifiedDispatcher.InvokeAll(ev); - } - } - - /// - /// Removes a pickup and all its trackables from the tracking. - /// - /// The pickup to be removed. - public virtual void Remove(Pickup pickup) - { - if (TrackedPickupSerials.ContainsKey(pickup.Serial)) - { - TrackedPickupSerials.Remove(pickup.Serial); - - pickup.GetComponents().ForEach(c => - { - if (c is EActor actor) - actor.Destroy(); - }); - - PickupRemovedDispatcher.InvokeAll(pickup.Serial); - } - } - - /// - /// Removes an ability from the tracking. - /// - /// The pickup owning the ability. - /// The to be removed. - public virtual void Remove(Pickup pickup, T behaviour) - { - if (TrackedPickupSerials.ContainsKey(pickup.Serial)) - { - IEnumerable previousTrackableItems = TrackedPickupSerials[pickup.Serial].Cast(); - TrackedPickupSerials[pickup.Serial].Remove(behaviour); - pickup.GetComponent(behaviour.GetType()).Destroy(); - PickupTrackingModifiedEventArgs ev = new(pickup, previousTrackableItems.Cast(), TrackedPickupSerials[pickup.Serial].Cast()); - PickupTrackingModifiedDispatcher.InvokeAll(ev); - } - } - - /// - /// Removes an item or pickup with the specified serial number from the tracking. - /// - /// The serial number of the item or pickup to be removed. - /// The of containing all items to be removed. - public virtual void Remove(ushort serial, IEnumerable behaviours) - { - if (TrackedItemSerials.ContainsKey(serial)) - { - IEnumerable previousTrackableItems = TrackedItemSerials[serial].Cast(); - - foreach (T behaviour in behaviours) - TrackedItemSerials[serial].Remove(behaviour); - - ItemTrackingModifiedEventArgs ev = new(Item.Get(serial), previousTrackableItems.Cast(), TrackedItemSerials[serial].Cast()); - ItemTrackingModifiedDispatcher.InvokeAll(ev); - } - - if (TrackedPickupSerials.ContainsKey(serial)) - { - IEnumerable previousTrackableItems = TrackedPickupSerials[serial].Cast(); - - foreach (T behaviour in behaviours) - TrackedPickupSerials[serial].Remove(behaviour); - - PickupTrackingModifiedEventArgs ev = new(Pickup.Get(serial), previousTrackableItems.Cast(), TrackedPickupSerials[serial].Cast()); - PickupTrackingModifiedDispatcher.InvokeAll(ev); - } - } - - /// - /// Checks if an item is being tracked. - /// - /// The to check. - /// if the item is being tracked; otherwise, . - public virtual bool IsTracked(Item item) => TrackedItemSerials.ContainsKey(item.Serial); - - /// - /// Checks if a pickup is being tracked. - /// - /// The to check. - /// if the pickup is being tracked; otherwise, . - public virtual bool IsTracked(Pickup pickup) => TrackedPickupSerials.ContainsKey(pickup.Serial); - - /// - /// Gets the tracked values associated with the specified item. - /// - /// The to retrieve tracked values from. - /// - /// An containing the tracked values associated with the item. - /// If the item is not tracked, returns an empty collection. - /// - public virtual IEnumerable GetTrackedValues(Item item) => !IsTracked(item) ? Enumerable.Empty() : TrackedItemSerials[item.Serial]; - - /// - /// Gets the tracked values associated with the specified pickup. - /// - /// The to retrieve tracked values from. - /// - /// An containing the tracked values associated with the pickup. - /// If the pickup is not tracked, returns an empty collection. - /// - public virtual IEnumerable GetTrackedValues(Pickup pickup) => !IsTracked(pickup) ? Enumerable.Empty() : TrackedPickupSerials[pickup.Serial]; - - /// - /// Handles the event when a player is dropping an item. - /// - /// The containing information about the dropping item. - internal void OnDroppingItem(DroppingItemEventArgs ev) => AddOrTrack(ev.Item); - - /// - /// Handles the event when an item is dropped. - /// - /// The containing information about the dropped item. - internal void OnDroppedItem(DroppedItemEventArgs ev) => Restore(ev.Pickup); - - /// - /// Handles the event when an item is added. - /// - /// The containing information about the added item. - internal void OnItemAdded(ItemAddedEventArgs ev) => Restore(ev.Pickup, ev.Item); - - /// - /// Handles the event when a tracked item or pickup is removed from a player's inventory. - /// - /// The containing information about the removed item. - internal void OnItemRemoved(ItemRemovedEventArgs ev) - { - if (ev.Pickup) - return; - - Remove(ev.Item); - } - - /// - /// Handles the event when a tracked item or pickup is destroyed. - /// - /// The containing information about the destroyed pickup. - internal void OnPickupDestroyed(PickupDestroyedEventArgs ev) => Remove(ev.Pickup); - - /// - protected override void SubscribeEvents() - { - base.SubscribeEvents(); - - Exiled.Events.Handlers.Player.DroppingItem += OnDroppingItem; - Exiled.Events.Handlers.Player.DroppedItem += OnDroppedItem; - Exiled.Events.Handlers.Player.ItemAdded += OnItemAdded; - Exiled.Events.Handlers.Player.ItemRemoved += OnItemRemoved; - Exiled.Events.Handlers.Map.PickupDestroyed += OnPickupDestroyed; - } - - /// - protected override void UnsubscribeEvents() - { - base.UnsubscribeEvents(); - - Exiled.Events.Handlers.Player.DroppingItem -= OnDroppingItem; - Exiled.Events.Handlers.Player.DroppedItem -= OnDroppedItem; - Exiled.Events.Handlers.Player.ItemAdded -= OnItemAdded; - Exiled.Events.Handlers.Player.ItemRemoved -= OnItemRemoved; - Exiled.Events.Handlers.Map.PickupDestroyed -= OnPickupDestroyed; - } - } -} \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/Inventory/InventoryManager.cs b/Exiled.CustomModules/API/Features/Inventory/InventoryManager.cs index 5af8df102a..0599cb66df 100644 --- a/Exiled.CustomModules/API/Features/Inventory/InventoryManager.cs +++ b/Exiled.CustomModules/API/Features/Inventory/InventoryManager.cs @@ -8,6 +8,7 @@ namespace Exiled.CustomModules.API.Features.Inventory { using System.Collections.Generic; + using System.ComponentModel; using Exiled.API.Enums; @@ -43,21 +44,26 @@ public InventoryManager( } /// + [Description("The list of items to be given.")] public List Items { get; set; } = new(); /// + [Description("The list of custom items to be given.")] public List CustomItems { get; set; } = new(); /// + [Description("The ammo box settings to be applied.")] public Dictionary AmmoBox { get; set; } = new(); /// + [Description("The custom ammo box settings to be applied.")] public Dictionary CustomAmmoBox { get; set; } = new(); /// /// Gets or sets the probability associated with this inventory slot. ///
Useful for inventory tweaks involving one or more probability values.
///
+ [Description("The probability associated with this inventory slot. Useful for inventory tweaks involving one or more probability values.")] public float Chance { get; set; } } } \ No newline at end of file diff --git a/Exiled.CustomModules/API/Features/ModuleBehaviour.cs b/Exiled.CustomModules/API/Features/ModuleBehaviour.cs deleted file mode 100644 index efb7a43f23..0000000000 --- a/Exiled.CustomModules/API/Features/ModuleBehaviour.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomModules.API.Features -{ - using System; - using System.Reflection; - - using Exiled.API.Features.Core; - using Exiled.API.Features.Core.Generic; - using UnityEngine; - - /// - /// Represents a marker class for a module's pointer. - /// - /// The type of the entity to which the module behaviour is applied. - /// - /// This abstract class serves as a foundation for user-defined module behaviours that can be applied to entities (such as playable characters) - /// to extend and customize their functionality through custom modules. It provides a modular and extensible architecture for enhancing gameplay elements. - /// - public abstract class ModuleBehaviour : EBehaviour - where TEntity : GameEntity - { - /// - /// Gets or sets the behaviour's configs. - /// - public virtual ModulePointer Config { get; set; } - - /// - /// Implements the behaviour's configs by copying properties from the config object to the current instance. - /// - protected virtual void ImplementConfigs() - { - if (Config is null) - return; - - Type inType = GetType(); - foreach (PropertyInfo propertyInfo in Config.GetType().GetProperties()) - ApplyConfig(propertyInfo, inType.GetProperty(propertyInfo.Name)); - } - - /// - /// Applies a configuration property value from the source property to the target property. - /// - /// The source property from the config object. - /// The target property in the current instance. - protected virtual void ApplyConfig(PropertyInfo propertyInfo, PropertyInfo targetInfo) - { - targetInfo?.SetValue(this, propertyInfo.GetValue(Config, null)); - } - } -} diff --git a/Exiled.CustomModules/API/Features/ModuleInfo.cs b/Exiled.CustomModules/API/Features/ModuleInfo.cs index 841011ae41..8f571bdddf 100644 --- a/Exiled.CustomModules/API/Features/ModuleInfo.cs +++ b/Exiled.CustomModules/API/Features/ModuleInfo.cs @@ -12,6 +12,7 @@ namespace Exiled.CustomModules.API.Features using System.Linq; using System.Reflection; + using Exiled.API.Features; using Exiled.CustomModules.API.Enums; /// @@ -62,18 +63,30 @@ public struct ModuleInfo /// /// Callback method for enabling all instances of the module. /// - public Action EnableAll_Callback; + public Func EnableAll_Callback; /// /// Callback method for disabling all instances of the module. /// - public Action DisableAll_Callback; + public Func DisableAll_Callback; #pragma warning restore SA1310 /// /// Gets the module 's name. /// - public string Name => Type.Name; + public readonly string Name => Type.Name; + + /// + /// Gets the assembly which is defined the module in. + /// + public readonly Assembly Assembly => Type.Assembly; + + /// + /// Gets all instances of all defined types in the specified . + /// + /// The assembly look for. + /// All instances of all defined types in the . + public static IEnumerable Get(Assembly assembly) => AllModules.Where(m => m.Assembly == assembly); /// /// Gets a instance based on the module type or name. @@ -101,22 +114,38 @@ public struct ModuleInfo /// /// The name of the callback to invoke ("EnableAll" or "DisableAll"). /// The assembly to pass to the callback method. - public void InvokeCallback(string name, Assembly assembly = null) + public void InvokeCallback(string name, Assembly assembly) { + if (assembly is null) + return; + bool isEnableAllCallback = string.Equals(name, ENABLE_ALL_CALLBACK, StringComparison.CurrentCultureIgnoreCase); if (!isEnableAllCallback && !string.Equals(name, DISABLE_ALL_CALLBACK, StringComparison.CurrentCultureIgnoreCase)) return; - IsCurrentlyLoaded = isEnableAllCallback && !IsCurrentlyLoaded; - if (IsCurrentlyLoaded) + if (CustomModules.Instance.Config.Modules is null || !CustomModules.Instance.Config.Modules.Contains(ModuleType.Name)) + throw new Exception($"ModuleType::{ModuleType.Name} must be enabled in order to load any {Type.Name} instances."); + + if (!IsCurrentlyLoaded && isEnableAllCallback) { + IsCurrentlyLoaded = true; CustomModule.OnEnabled.InvokeAll(this); - EnableAll_Callback(assembly ?? Assembly.GetCallingAssembly()); + } + + if (IsCurrentlyLoaded) + { + int enabledInstancesCount = EnableAll_Callback(assembly); + if (enabledInstancesCount > 0) + Log.Info($"{assembly.GetName().Name} deployed {enabledInstancesCount} {Type.Name} {(enabledInstancesCount > 1 ? "instances" : "instance")}."); + return; } CustomModule.OnDisabled.InvokeAll(this); - DisableAll_Callback(); + + int disabledInstancesCount = DisableAll_Callback(assembly); + if (disabledInstancesCount > 0) + Log.Info($"{assembly.GetName().Name} disabled {disabledInstancesCount} {Type.Name} {(disabledInstancesCount > 1 ? "instances" : "instance")}."); } } } diff --git a/Exiled.CustomModules/API/Features/ModulePointer.cs b/Exiled.CustomModules/API/Features/ModulePointer.cs index 59e2469b92..edb0fbc3a4 100644 --- a/Exiled.CustomModules/API/Features/ModulePointer.cs +++ b/Exiled.CustomModules/API/Features/ModulePointer.cs @@ -8,6 +8,7 @@ namespace Exiled.CustomModules.API.Features { using System; + using System.ComponentModel; using System.Reflection; using Exiled.API.Features.Core; @@ -21,8 +22,15 @@ public abstract class ModulePointer : TypeCastObject /// /// Gets or sets the module's id the is pointing to. /// + [Description("The module's id the module pointer is pointing to.")] public abstract uint Id { get; set; } + /// + /// Gets or sets the module type which the module pointer is pointing to. + /// + [Description("The module type which the module pointer is pointing to.")] + public virtual string ModuleTypeIndicator { get; set; } + /// /// Gets the module pointer for the specified custom module and assembly. /// @@ -32,7 +40,12 @@ public abstract class ModulePointer : TypeCastObject public static ModulePointer Get(CustomModule customModule, Assembly assembly = null) { assembly ??= Assembly.GetCallingAssembly(); - Type customModuleType = customModule.GetType(); + + Type customModuleType = customModule.GetType().BaseType; + + Type baseModuleType = customModuleType.IsGenericType ? customModuleType.BaseType : customModuleType; + if (baseModuleType == typeof(CustomModule)) + baseModuleType = customModuleType; foreach (Type type in assembly.GetTypes()) { @@ -40,41 +53,29 @@ public static ModulePointer Get(CustomModule customModule, Assembly assembly = n if (moduleIdentifier == null) continue; - bool isPointing = moduleIdentifier.Id > 0 && moduleIdentifier.Id == customModule.Id; if (typeof(ModulePointer).IsAssignableFrom(type)) { - ModulePointer modulePointer; - if (type.IsGenericTypeDefinition) - { - Type constructedType = type.MakeGenericType(customModuleType); - modulePointer = Activator.CreateInstance(constructedType) as ModulePointer; - } - else - { - modulePointer = Activator.CreateInstance(type) as ModulePointer; - } - - if (isPointing) - { - modulePointer.Id = moduleIdentifier.Id; - return modulePointer; - } + Type constructedType = null; + + if (type.BaseType.IsGenericType) + constructedType = type.BaseType.GetGenericArguments()[0]; + + if (constructedType != baseModuleType) + continue; + + ModulePointer modulePointer = Activator.CreateInstance(type) as ModulePointer; if (modulePointer.Id != customModule.Id) continue; + if (string.IsNullOrEmpty(modulePointer.ModuleTypeIndicator)) + modulePointer.ModuleTypeIndicator = constructedType.Name; + return modulePointer; } } return null; } - - /// - /// Gets the module pointer for the specified custom module from the calling assembly. - /// - /// The custom module to get the pointer for. - /// The module pointer for the specified custom module, or null if not found. - public static ModulePointer Get(CustomModule customModule) => Get(customModule, Assembly.GetCallingAssembly()); } } diff --git a/Exiled.CustomModules/API/Features/Pawn.cs b/Exiled.CustomModules/API/Features/Pawn.cs index 846858c4a2..c7c5213ac5 100644 --- a/Exiled.CustomModules/API/Features/Pawn.cs +++ b/Exiled.CustomModules/API/Features/Pawn.cs @@ -23,6 +23,7 @@ namespace Exiled.CustomModules.API.Features using Exiled.CustomModules.API.Features.CustomItems.Items; using Exiled.CustomModules.API.Features.CustomItems.Pickups.Ammos; using Exiled.CustomModules.API.Features.CustomRoles; + using Exiled.CustomModules.API.Features.Generic; using Exiled.CustomModules.API.Features.PlayerAbilities; using Exiled.CustomModules.Events.EventArgs.CustomAbilities; using PlayerRoles; @@ -73,7 +74,7 @@ public Pawn(GameObject gameObject) } /// - /// Gets all pawn's 's. + /// Gets all pawn's 's. /// public IEnumerable> ModuleBehaviours => GetComponents>(); diff --git a/Exiled.CustomModules/API/Features/RespawnManager.cs b/Exiled.CustomModules/API/Features/RespawnManager.cs index 72ad9f9c44..fed0ef5a4d 100644 --- a/Exiled.CustomModules/API/Features/RespawnManager.cs +++ b/Exiled.CustomModules/API/Features/RespawnManager.cs @@ -34,7 +34,7 @@ public class RespawnManager : StaticActor /// Gets or sets the which handles all delegates to be fired when selecting the next known team. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher SelectingCustomTeamRespawnDispatcher { get; set; } + public TDynamicEventDispatcher SelectingCustomTeamRespawnDispatcher { get; set; } = new(); /// /// Gets or sets the next known team. diff --git a/Exiled.CustomModules/API/Features/RoleAssigner.cs b/Exiled.CustomModules/API/Features/RoleAssigner.cs index 40aa5949eb..22c175602c 100644 --- a/Exiled.CustomModules/API/Features/RoleAssigner.cs +++ b/Exiled.CustomModules/API/Features/RoleAssigner.cs @@ -34,13 +34,13 @@ public class RoleAssigner : StaticActor /// Gets or sets the which handles all the delegates fired before assigning human roles. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher AssigningHumanCustomRolesDispatcher { get; set; } + public TDynamicEventDispatcher AssigningHumanCustomRolesDispatcher { get; set; } = new(); /// /// Gets or sets the which handles all the delegates fired before assigning SCP roles. /// [DynamicEventDispatcher] - public TDynamicEventDispatcher AssigningScpCustomRolesDispatcher { get; set; } + public TDynamicEventDispatcher AssigningScpCustomRolesDispatcher { get; set; } = new(); /// /// Gets or sets all enqueued SCPs. diff --git a/Exiled.CustomModules/API/Features/TrackablesGC.cs b/Exiled.CustomModules/API/Features/TrackablesGC.cs new file mode 100644 index 0000000000..d70f426e3e --- /dev/null +++ b/Exiled.CustomModules/API/Features/TrackablesGC.cs @@ -0,0 +1,125 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Features +{ + using System.Collections.Generic; + + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Core; + using Exiled.API.Features.Core.Generic; + using Exiled.API.Features.Items; + using Exiled.API.Features.Pickups; + using Exiled.CustomModules.API.Interfaces; + + /// + /// Represents a class responsible for handling the garbage collection of objects. + /// It maintains a collection of tracked objects identified by their unique serial numbers, + /// and handles allocation, collection, and sweeping of these objects. + /// + public sealed class TrackablesGC : StaticActor + { +#pragma warning disable SA1309 + /// + /// Stores the serial numbers of allocated objects. + /// + private HashSet allocated = new(); + + /// + /// Indicates whether the garbage collection process is paused. + /// + private bool isCollectionPaused; +#pragma warning restore SA1309 + + /// + /// Gets the current count of tracked objects. + /// + public int TrackedCount => allocated.Count; + + /// + /// Allocates a new serial number for tracking. + /// + /// The serial number to allocate. + /// if allocation is successful; otherwise, . + public bool Allocate(ushort serial) => allocated.Add(serial); + + /// + /// Frees the allocated object associated with the specified serial number. + /// + /// The serial number of the object to be freed. + /// + /// if the object was successfully freed (i.e., the serial number was found and removed from the allocated set); + /// otherwise, . + /// + public bool Free(ushort serial) => allocated.Remove(serial); + + /// + /// Attempts to collect and remove an object based on its serial number. + /// + /// The serial number of the object to collect. + /// if the object is collected successfully; if collection is paused or the object is not found. + public bool Collect(ushort serial) + { + if (isCollectionPaused || !IsAllocated(serial)) + return false; + + Item item = Item.Get(serial); + if (item is not null && item.Owner is not null && item.Owner == Server.Host) + { + Sweep(item, serial); + return true; + } + + Pickup pickup = Pickup.Get(serial); + if (pickup is not null) + { + Sweep(pickup, serial); + return true; + } + + return false; + } + + /// + /// Sweeps an object, removing it from tracking and its associated components. + /// + /// The to sweep. + /// The serial number of the object. + /// The serial number if the sweep is successful, or 0 if the object is not found. + public ushort Sweep(GameEntity entity, ushort serial) + { + if (!entity.Is(out Item _) && !entity.Is(out Pickup _)) + return 0; + + allocated.Remove(serial); + return serial; + } + + /// + /// Triggers the garbage collection process for all tracked objects. + /// + public void TriggerCollection() => allocated.ForEach(fc => Collect(fc)); + + /// + /// Pauses the garbage collection process. + /// + public void PauseCollection() => isCollectionPaused = true; + + /// + /// Resumes the garbage collection process if it was paused. + /// + public void ResumeCollection() => isCollectionPaused = false; + + /// + /// Checks whether a specific object is currently allocated by its serial number. + /// + /// The serial number to check. + /// if the object is currently allocated by its serial number; otherwise, . + public bool IsAllocated(ushort serial) => allocated.Contains(serial); + } +} diff --git a/Exiled.CustomModules/API/Features/TrackerBase.cs b/Exiled.CustomModules/API/Features/TrackerBase.cs new file mode 100644 index 0000000000..32e9c6a762 --- /dev/null +++ b/Exiled.CustomModules/API/Features/TrackerBase.cs @@ -0,0 +1,608 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.API.Features +{ + using System.Collections.Generic; + using System.Linq; + + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Attributes; + using Exiled.API.Features.Core; + using Exiled.API.Features.Core.Generic; + using Exiled.API.Features.DynamicEvents; + using Exiled.API.Features.Items; + using Exiled.API.Features.Pickups; + using Exiled.CustomModules.API.Features.CustomAbilities; + using Exiled.CustomModules.API.Features.CustomItems.Items; + using Exiled.CustomModules.API.Features.CustomItems.Pickups; + using Exiled.CustomModules.API.Interfaces; + using Exiled.CustomModules.Events.EventArgs.Tracking; + using Exiled.Events.EventArgs.Map; + using Exiled.Events.EventArgs.Player; + using MEC; + + /// + /// The actor which handles all tracking-related tasks for items. + /// + public class TrackerBase : StaticActor + { + /// + /// Gets or sets the which handles all the delegates fired when an item is added. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher ItemAddedDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when an item is removed. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher ItemRemovedDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when an item is restored. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher ItemRestoredDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when an item tracking is modified. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher ItemTrackingModifiedDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when a pickup is added. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher PickupAddedDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when a pickup is removed. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher PickupRemovedDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when a pickup is restored. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher PickupRestoredDispatcher { get; set; } = new(); + + /// + /// Gets or sets the which handles all the delegates fired when a pickup tracking is modified. + /// + [DynamicEventDispatcher] + public TDynamicEventDispatcher PickupTrackingModifiedDispatcher { get; set; } = new(); + + /// + /// Gets a containing all serials and their corresponding items. + /// + private Dictionary> TrackedSerials { get; } = new(); + + /// + /// Adds or tracks the trackables of an item based on its serial. + /// + /// The whose trackables are to be added or tracked. + /// if the item was added or tracked successfully; otherwise, . + public virtual bool AddOrTrack(Item item) + { + if (!item) + return false; + + IEnumerable trackableBehaviours = item.GetComponents() ?? Enumerable.Empty(); + if (trackableBehaviours.IsEmpty()) + return false; + + if (TrackedSerials.TryGetValue(item.Serial, out HashSet components)) + { + IEnumerable previousTrackableItems = components; + TrackedSerials[item.Serial].AddRange(trackableBehaviours.Cast()); + ItemTrackingModifiedEventArgs ev = new(item, previousTrackableItems.Cast(), components.Cast()); + ItemTrackingModifiedDispatcher.InvokeAll(ev); + return true; + } + + TrackedSerials.Add(item.Serial, new HashSet(trackableBehaviours.Cast())); + ItemAddedDispatcher.InvokeAll(item); + + return true; + } + + /// + /// Adds or tracks the trackables of a pickup based on its serial. + /// + /// The whose trackables are to be added or tracked. + /// if the pickup was added or tracked successfully; otherwise, . + public virtual bool AddOrTrack(Pickup pickup) + { + if (!pickup) + return false; + + IEnumerable trackableBehaviours = pickup.GetComponents(); + if (trackableBehaviours.IsEmpty()) + return false; + + if (TrackedSerials.TryGetValue(pickup.Serial, out HashSet components)) + { + IEnumerable previousTrackableItems = components.ToList(); + components.AddRange(trackableBehaviours.Cast()); + PickupTrackingModifiedEventArgs ev = new(pickup, previousTrackableItems.Cast(), components.Cast()); + PickupTrackingModifiedDispatcher.InvokeAll(ev); + return true; + } + + TrackedSerials.Add(pickup.Serial, new HashSet(trackableBehaviours.Cast())); + PickupAddedDispatcher.InvokeAll(pickup); + + return true; + } + + /// + /// Adds or tracks the trackables of both pickups and items based on their serial. + /// + /// The objects whose trackables are to be added or tracked. + public virtual void AddOrTrack(params object[] objects) + { + foreach (object @object in objects) + { + if (@object is Pickup pickup) + { + AddOrTrack(pickup); + continue; + } + + if (@object is Item item) + AddOrTrack(item); + } + } + + /// + /// Restores all trackables from a pickup which is being tracked. + /// + /// The serial to restore all trackables from. + /// The item to reapply the trackables to. + /// if the pickup was restored successfully; otherwise, . + public virtual bool Restore(ushort serial, Item item) + { + if (!item || !TrackedSerials.TryGetValue(serial, out HashSet components)) + return false; + + bool wasRestored = false; + foreach (ITrackable behaviour in components) + { + if (behaviour is not(IItemBehaviour and IAbilityBehaviour)) + continue; + + EActor component = behaviour as EActor; + if (component is not null) + { + wasRestored = true; + item.AddComponent(component); + } + } + + if (wasRestored) + ItemRestoredDispatcher.InvokeAll(item); + + return wasRestored; + } + + /// + /// Restores all trackables from a pickup which is being tracked. + /// + /// The whose trackables are to be restored. + /// if the pickup was restored successfully; otherwise, . + public virtual bool Restore(Pickup pickup) + { + if (!pickup || !TrackedSerials.TryGetValue(pickup.Serial, out HashSet components)) + return false; + + bool wasRestored = false; + foreach (ITrackable behaviour in components) + { + if (behaviour is not(IPickupBehaviour and IAbilityBehaviour)) + continue; + + EActor component = behaviour as EActor; + if (component is not null) + { + pickup.AddComponent(component); + wasRestored = true; + } + } + + if (wasRestored) + PickupRestoredDispatcher.InvokeAll(pickup); + + return wasRestored; + } + + /// + /// Restores all trackables from a pickup which is being tracked. + /// + /// The whose trackables are to be restored. + /// The whose trackables are to be transfered. + /// if the pickup was restored successfully; otherwise, . + public virtual bool Restore(Pickup pickup, Item item) + { + if (!IsTracked(pickup) || !IsTracked(item)) + return false; + + bool wasRestored = false; + EActor component = null; + foreach (ITrackable behaviour in TrackedSerials[pickup.Serial].ToList()) + { + if (behaviour is not(IItemBehaviour and IAbilityBehaviour)) + { + component = behaviour as EActor; + + if (component is not null) + pickup.RemoveComponent(component); + + continue; + } + + if (component is not null) + { + item.AddComponent(component); + wasRestored = true; + } + } + + if (wasRestored) + ItemRestoredDispatcher.InvokeAll(item); + + return wasRestored; + } + + /// + /// Restores all trackables from a item which is being tracked. + /// + /// The whose trackables are to be restored. + /// The whose trackables are to be transfered. + /// if the item was restored successfully; otherwise, . + public virtual bool Restore(Item item, Pickup pickup) + { + if (!IsTracked(pickup) || !IsTracked(item)) + return false; + + bool wasRestored = false; + EActor component = null; + foreach (ITrackable behaviour in TrackedSerials[item.Serial].ToList()) + { + if (behaviour is not(IPickupBehaviour and IAbilityBehaviour)) + { + component = behaviour as EActor; + + if (component is not null) + item.RemoveComponent(behaviour as EActor); + + continue; + } + + if (component is not null) + { + pickup.AddComponent(component); + wasRestored = true; + } + } + + if (wasRestored) + PickupRestoredDispatcher.InvokeAll(pickup); + + return wasRestored; + } + + /// + /// Removes an item and all its trackables from the tracking. + /// + /// The item to be removed. + /// A value indicating whether the behaviours should be destroyed. + public virtual void Remove(Item item, bool destroy = false) + { + if (!TrackedSerials.TryGetValue(item.Serial, out HashSet components)) + return; + + if (destroy) + { + Remove(item.Serial, true); + } + else + { + components.ForEach(c => + { + if (c is EActor actor) + item.RemoveComponent(actor); + }); + + TrackedSerials.Remove(item.Serial); + ItemRemovedDispatcher.InvokeAll(item.Serial); + } + } + + /// + /// Removes an item and their relative trackable from the tracking. + /// + /// The item to be removed. + /// The to be removed. + /// A value indicating whether the behaviour should be destroyed. + public virtual void Remove(Item item, ITrackable behaviour, bool destroy = false) + { + if (!TrackedSerials.TryGetValue(item.Serial, out HashSet components)) + return; + + IEnumerable previousTrackedItems = components.ToList(); + components.Remove(behaviour); + + if (destroy) + item.GetComponent(behaviour.GetType()).Destroy(); + else + item.RemoveComponent(behaviour.GetType()); + + ItemTrackingModifiedEventArgs ev = new(item, previousTrackedItems.Cast(), components.Cast()); + ItemTrackingModifiedDispatcher.InvokeAll(ev); + } + + /// + /// Removes a pickup and all its trackables from the tracking. + /// + /// The pickup to be removed. + /// A value indicating whether the behaviours should be destroyed. + public virtual void Remove(Pickup pickup, bool destroy = false) + { + ushort serial = pickup.Serial; + + if (!TrackedSerials.ContainsKey(serial)) + return; + + if (destroy) + { + Remove(serial, true); + } + else + { + TrackedSerials[serial].ForEach(c => + { + if (c is EActor actor) + { + pickup.RemoveComponent(actor); + } + }); + + TrackedSerials.Remove(serial); + PickupRemovedDispatcher.InvokeAll(serial); + } + } + + /// + /// Removes a serial and all its trackables from the tracking. + /// + /// The serial to be removed. + /// A value indicating whether the behaviours should be destroyed. + public virtual void Remove(ushort serial, bool destroy = false) + { + if (!TrackedSerials.ContainsKey(serial)) + return; + + if (destroy) + { + TrackedSerials[serial].ForEach(c => + { + if (c is EActor actor) + actor.Destroy(); + }); + + TrackedSerials.Remove(serial); + PickupRemovedDispatcher.InvokeAll(serial); + } + else + { + Pickup pickup = Pickup.Get(serial); + if (pickup is not null) + { + Remove(pickup, destroy); + return; + } + + Item item = Item.Get(serial); + if (item is not null && item.Owner is not null && item.Owner != Server.Host) + { + Remove(item, destroy); + return; + } + } + } + + /// + /// Removes an ability from the tracking. + /// + /// The pickup owning the ability. + /// The to be removed. + /// A value indicating whether the behaviour should be destroyed. + public virtual void Remove(Pickup pickup, ITrackable behaviour, bool destroy = false) + { + if (behaviour is not EActor component || !TrackedSerials.TryGetValue(pickup.Serial, out HashSet components)) + return; + + IEnumerable previousTrackableItems = components.ToList(); + components.Remove(behaviour); + + if (destroy) + component.Destroy(); + else + pickup.RemoveComponent(component.GetType()); + + PickupTrackingModifiedEventArgs ev = new(pickup, previousTrackableItems.Cast(), components); + PickupTrackingModifiedDispatcher.InvokeAll(ev); + } + + /// + /// Removes an item or pickup with the specified serial number from the tracking. + /// + /// The serial number of the item or pickup to be removed. + /// The containing all items to be removed. + public virtual void Remove(ushort serial, IEnumerable behaviours) + { + if (!TrackedSerials.TryGetValue(serial, out HashSet components)) + return; + + IEnumerable RemoveBehaviours() + { + IEnumerable previousTrackableItems = components.ToList(); + + foreach (ITrackable behaviour in behaviours) + components.Remove(behaviour); + + return previousTrackableItems; + } + + Item item = Item.Get(serial); + if (item is not null && item.Owner is not null && item.Owner != Server.Host) + { + IEnumerable previousTrackableItems = RemoveBehaviours(); + ItemTrackingModifiedEventArgs ev = new(item, previousTrackableItems, components); + ItemTrackingModifiedDispatcher.InvokeAll(ev); + return; + } + + Pickup pickup = Pickup.Get(serial); + if (pickup is not null) + { + IEnumerable previousTrackableItems = RemoveBehaviours(); + PickupTrackingModifiedEventArgs ev = new(Pickup.Get(serial), previousTrackableItems, components); + PickupTrackingModifiedDispatcher.InvokeAll(ev); + } + } + + /// + /// Checks if a serial is being tracked. + /// + /// The serial to check. + /// if the serial is being tracked; otherwise, . + public virtual bool IsTracked(ushort serial) => TrackedSerials.ContainsKey(serial); + + /// + /// Checks if an item is being tracked. + /// + /// The to check. + /// if the item is being tracked; otherwise, . + public virtual bool IsTracked(Item item) => item && TrackedSerials.ContainsKey(item.Serial); + + /// + /// Checks if a pickup is being tracked. + /// + /// The to check. + /// if the pickup is being tracked; otherwise, . + public virtual bool IsTracked(Pickup pickup) => pickup && TrackedSerials.ContainsKey(pickup.Serial); + + /// + /// Gets the tracked values associated with the specified serial. + /// + /// The serial to retrieve tracked values from. + /// + /// An containing the tracked values associated with the serial. + /// If the serial is not tracked, returns an empty collection. + /// + public virtual IEnumerable GetTrackedValues(ushort serial) => !IsTracked(serial) ? Enumerable.Empty() : TrackedSerials[serial]; + + /// + /// Gets the tracked values associated with the specified item. + /// + /// The to retrieve tracked values from. + /// + /// An containing the tracked values associated with the item. + /// If the item is not tracked, returns an empty collection. + /// + public virtual IEnumerable GetTrackedValues(Item item) => !IsTracked(item) ? Enumerable.Empty() : TrackedSerials[item.Serial]; + + /// + /// Gets the tracked values associated with the specified pickup. + /// + /// The to retrieve tracked values from. + /// + /// An containing the tracked values associated with the pickup. + /// If the pickup is not tracked, returns an empty collection. + /// + public virtual IEnumerable GetTrackedValues(Pickup pickup) => !IsTracked(pickup) ? Enumerable.Empty() : TrackedSerials[pickup.Serial]; + + /// + /// Handles the event when an item is added. + /// + /// The containing information about the added item. + internal void OnItemAdded(ItemAddedEventArgs ev) + { + if (ev.Item.Serial == 0) + return; + + TrackablesGC.Get().Free(ev.Item.Serial); + } + + /// + /// Handles the event when a tracked item or pickup is removed from a player's inventory. + /// + /// The containing information about the removed item. + internal void OnItemRemoved(ItemRemovedEventArgs ev) + { + if (ev.Pickup is not null) + return; + + Remove(ev.Item, true); + } + + /// + /// Handles the event when a tracked item or pickup is destroyed. + /// + /// The containing information about the destroyed pickup. + internal void OnPickupAdded(PickupAddedEventArgs ev) + { + if (ev.Pickup.Serial == 0) + return; + + TrackablesGC.Get().Allocate(ev.Pickup.Serial); + } + + /// + /// Handles the event when a tracked item or pickup is destroyed. + /// + /// The containing information about the destroyed pickup. + internal void OnPickupDestroyed(PickupDestroyedEventArgs ev) + { + if (ev.Pickup.Serial == 0) + return; + + Timing.CallDelayed(0.5f, () => + { + if (TrackablesGC.Get().Collect(ev.Pickup.Serial)) + Remove(ev.Pickup, true); + }); + } + + /// + protected override void SubscribeEvents() + { + base.SubscribeEvents(); + + Exiled.Events.Handlers.Player.ItemAdded += OnItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved += OnItemRemoved; + Exiled.Events.Handlers.Map.PickupAdded += OnPickupAdded; + Exiled.Events.Handlers.Map.PickupDestroyed += OnPickupDestroyed; + } + + /// + protected override void UnsubscribeEvents() + { + base.UnsubscribeEvents(); + + Exiled.Events.Handlers.Player.ItemAdded -= OnItemAdded; + Exiled.Events.Handlers.Player.ItemRemoved -= OnItemRemoved; + Exiled.Events.Handlers.Map.PickupAdded -= OnPickupAdded; + Exiled.Events.Handlers.Map.PickupDestroyed -= OnPickupDestroyed; + } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/CustomModules.cs b/Exiled.CustomModules/CustomModules.cs index a82214692c..5f2e44b0ef 100644 --- a/Exiled.CustomModules/CustomModules.cs +++ b/Exiled.CustomModules/CustomModules.cs @@ -13,12 +13,20 @@ namespace Exiled.CustomModules using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features; using Exiled.CustomModules.EventHandlers; + using MEC; /// /// Handles all custom role API functions. /// public class CustomModules : Plugin { + /// + /// The delay to be applied in order to execute any dispatch operation. + /// +#pragma warning disable SA1310 // Field names should not contain underscore + private const float DISPATCH_OPERATION_DELAY = 0.5f; +#pragma warning restore SA1310 // Field names should not contain underscore + /// /// Gets a static reference to the plugin's instance. /// @@ -56,7 +64,7 @@ public override void OnEnabled() base.OnEnabled(); - CustomModule.LoadAll(); + Timing.CallDelayed(DISPATCH_OPERATION_DELAY, CustomModule.LoadAll); } /// @@ -65,6 +73,8 @@ public override void OnDisabled() base.OnDisabled(); CustomModule.UnloadAll(); + + Instance = null; } /// @@ -76,6 +86,8 @@ protected override void SubscribeEvents() Exiled.Events.Handlers.Player.ChangingItem += PlayerHandler.OnChangingItem; Exiled.Events.Handlers.Server.RoundStarted += ServerHandler.OnRoundStarted; + CustomModule.OnEnabled += RegistrationHandler.OnModuleEnabled; + CustomModule.OnDisabled += RegistrationHandler.OnModuleDisabled; DynamicEventManager.CreateFromTypeInstance(RegistrationHandler); } @@ -85,6 +97,8 @@ protected override void UnsubscribeEvents() { Exiled.Events.Handlers.Player.ChangingItem -= PlayerHandler.OnChangingItem; Exiled.Events.Handlers.Server.RoundStarted -= ServerHandler.OnRoundStarted; + CustomModule.OnEnabled -= RegistrationHandler.OnModuleEnabled; + CustomModule.OnDisabled -= RegistrationHandler.OnModuleDisabled; DynamicEventManager.DestroyFromTypeInstance(RegistrationHandler); diff --git a/Exiled.CustomModules/EventHandlers/PlayerHandler.cs b/Exiled.CustomModules/EventHandlers/PlayerHandler.cs index 9f67f45306..a5af2e94fe 100644 --- a/Exiled.CustomModules/EventHandlers/PlayerHandler.cs +++ b/Exiled.CustomModules/EventHandlers/PlayerHandler.cs @@ -12,7 +12,6 @@ namespace Exiled.CustomModules.EventHandlers using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features; using Exiled.CustomModules.API.Features.CustomItems; - using Exiled.CustomModules.API.Features.CustomItems.Items; using Exiled.Events.EventArgs.Player; /// @@ -28,7 +27,7 @@ public void OnChangingItem(ChangingItemEventArgs ev) if (!ev.IsAllowed || !CustomItemsModuleInfo.IsCurrentlyLoaded) return; - if (CustomItem.TryGet(ev.Item, out CustomItem customItem) && customItem.Settings.Cast().ShouldMessageOnGban) + if (CustomItem.TryGet(ev.Item, out CustomItem customItem) && customItem.Settings.NotifyItemToSpectators) SpectatorCustomNickname(ev.Player, $"{ev.Player.CustomName} (CustomItem: {customItem.Name})"); else if (ev.Player && ev.Player.Cast(out Pawn pawn) && pawn.CurrentItem) SpectatorCustomNickname(ev.Player, ev.Player.HasCustomName ? ev.Player.CustomName : string.Empty); diff --git a/Exiled.CustomModules/EventHandlers/RegistrationHandler.cs b/Exiled.CustomModules/EventHandlers/RegistrationHandler.cs index 99b0b0c8ba..5a26bb665b 100644 --- a/Exiled.CustomModules/EventHandlers/RegistrationHandler.cs +++ b/Exiled.CustomModules/EventHandlers/RegistrationHandler.cs @@ -11,17 +11,14 @@ namespace Exiled.CustomModules.EventHandlers using Exiled.API.Features.Core; using Exiled.CustomModules.API.Enums; using Exiled.CustomModules.API.Features; - using Exiled.CustomModules.API.Features.CustomAbilities; using Exiled.CustomModules.API.Features.CustomItems; - using Exiled.CustomModules.API.Features.CustomItems.Items; - using Exiled.CustomModules.API.Features.CustomItems.Pickups; /// /// Handles the all the module's registration. /// internal class RegistrationHandler { - private Config config; + private readonly Config config; /// /// Initializes a new instance of the class. @@ -35,6 +32,11 @@ internal class RegistrationHandler /// The module which is being enabled. internal void OnModuleEnabled(ModuleInfo moduleInfo) { + if (moduleInfo.Type.IsGenericType && moduleInfo.Type.BaseType != typeof(CustomModule)) + return; + + Log.InfoWithContext($"Module '{moduleInfo.Name}' has been deployed.", Log.CONTEXT_DEPLOYMENT); + if (moduleInfo.ModuleType.Name == UUModuleType.CustomRoles.Name && config.UseDefaultRoleAssigner) { StaticActor.Get(); @@ -55,15 +57,14 @@ internal void OnModuleEnabled(ModuleInfo moduleInfo) if (moduleInfo.ModuleType.Name == UUModuleType.CustomAbilities.Name) { - StaticActor.Get(); + TrackerBase.Get(); return; } if (moduleInfo.ModuleType.Name == UUModuleType.CustomItems.Name) { GlobalPatchProcessor.PatchAll("exiled.customitems.patch", nameof(CustomItem)); - StaticActor.Get(); - StaticActor.Get(); + TrackerBase.Get(); } } @@ -73,6 +74,8 @@ internal void OnModuleEnabled(ModuleInfo moduleInfo) /// The module which is being disabled. internal void OnModuleDisabled(ModuleInfo moduleInfo) { + Log.InfoWithContext($"Module '{moduleInfo.Name}' has been disabled.", Log.CONTEXT_DEPLOYMENT); + if (moduleInfo.ModuleType.Name == UUModuleType.CustomRoles.Name && config.UseDefaultRoleAssigner) { StaticActor.Get()?.Destroy(); @@ -87,21 +90,21 @@ internal void OnModuleDisabled(ModuleInfo moduleInfo) if (moduleInfo.ModuleType.Name == UUModuleType.CustomGameModes.Name) { - World.Get().Destroy(); + World.Get()?.Destroy(); return; } if (moduleInfo.ModuleType.Name == UUModuleType.CustomAbilities.Name) { - StaticActor.Get()?.Destroy(); + TrackerBase.Get()?.Destroy(); return; } if (moduleInfo.ModuleType.Name == UUModuleType.CustomItems.Name) { GlobalPatchProcessor.UnpatchAll("exiled.customitems.unpatch", nameof(CustomItem)); - StaticActor.Get()?.Destroy(); - StaticActor.Get()?.Destroy(); + TrackerBase.Get()?.Destroy(); + return; } } } diff --git a/Exiled.CustomModules/Events/EventArgs/CustomItems/UpgradingEventArgs.cs b/Exiled.CustomModules/Events/EventArgs/CustomItems/UpgradingEventArgs.cs index 969bae59e9..6c8a4dd849 100644 --- a/Exiled.CustomModules/Events/EventArgs/CustomItems/UpgradingEventArgs.cs +++ b/Exiled.CustomModules/Events/EventArgs/CustomItems/UpgradingEventArgs.cs @@ -31,7 +31,7 @@ public class UpgradingEventArgs : UpgradingPickupEventArgs, ICustomPickupEvent /// /// public UpgradingEventArgs(Pickup pickup, CustomItem customItem, ItemBehaviour itemBehaviour, Vector3 newPos, Scp914KnobSetting knobSetting, bool isAllowed = true) - : base(pickup.Base, newPos, knobSetting, Exiled.API.Features.Scp914.GetProcessor(customItem.Settings.ItemType)) + : base(pickup.Base, newPos, knobSetting, Exiled.API.Features.Scp914.GetProcessor(customItem.ItemType)) { IsAllowed = isAllowed; CustomItem = customItem; diff --git a/Exiled.CustomModules/Events/EventArgs/Modules/ModuleTypeEnabledEventArgs.cs b/Exiled.CustomModules/Events/EventArgs/Modules/ModuleTypeEnabledEventArgs.cs new file mode 100644 index 0000000000..e6e1b8d91d --- /dev/null +++ b/Exiled.CustomModules/Events/EventArgs/Modules/ModuleTypeEnabledEventArgs.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomModules.Events.EventArgs.Modules +{ + using Exiled.CustomModules.API.Features; + using Exiled.Events.EventArgs.Interfaces; + + /// + /// Contains all information after enabling a module type. + /// + public class ModuleTypeEnabledEventArgs : IExiledEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + public ModuleTypeEnabledEventArgs(ModuleInfo moduleInfo) => ModuleInfo = moduleInfo; + + /// + /// Gets the . + /// + public ModuleInfo ModuleInfo { get; } + } +} \ No newline at end of file diff --git a/Exiled.CustomModules/Exiled.CustomModules.csproj b/Exiled.CustomModules/Exiled.CustomModules.csproj index d64b1fcce8..16d5af2c19 100644 --- a/Exiled.CustomModules/Exiled.CustomModules.csproj +++ b/Exiled.CustomModules/Exiled.CustomModules.csproj @@ -20,6 +20,7 @@ + diff --git a/Exiled.CustomModules/Patches/PlayerInventorySee.cs b/Exiled.CustomModules/Patches/PlayerInventorySee.cs index a20d1ac2b9..fcd0de58ee 100644 --- a/Exiled.CustomModules/Patches/PlayerInventorySee.cs +++ b/Exiled.CustomModules/Patches/PlayerInventorySee.cs @@ -36,7 +36,7 @@ private static IEnumerable Transpiler(IEnumerable (i.opcode == OpCodes.Ldfld) && ((FieldInfo)i.operand == Field(typeof(ItemBase), nameof(ItemBase.ItemTypeId)))) + offset; Label continueLabel = generator.DefineLabel(); diff --git a/Exiled.Events/Events.cs b/Exiled.Events/Events.cs index 9002c1852f..ed5b939b28 100644 --- a/Exiled.Events/Events.cs +++ b/Exiled.Events/Events.cs @@ -83,6 +83,7 @@ public override void OnEnabled() RagdollManager.OnRagdollRemoved += Handlers.Internal.RagdollList.OnRemovedRagdoll; ItemPickupBase.OnPickupAdded += Handlers.Internal.PickupEvent.OnSpawnedPickup; ItemPickupBase.OnPickupDestroyed += Handlers.Internal.PickupEvent.OnRemovedPickup; + ServerConsole.ReloadServerName(); EventManager.RegisterEvents(this); diff --git a/Exiled.Events/Handlers/Internal/MapGenerated.cs b/Exiled.Events/Handlers/Internal/MapGenerated.cs index ed6532ad18..413b33aad8 100644 --- a/Exiled.Events/Handlers/Internal/MapGenerated.cs +++ b/Exiled.Events/Handlers/Internal/MapGenerated.cs @@ -74,8 +74,6 @@ private static void GenerateAttachments() if (Item.Create(firearmType.GetItemType()) is not Firearm firearm) continue; - Firearm.ItemTypeToFirearmInstance.Add(firearmType, firearm); - List attachmentIdentifiers = ListPool.Pool.Get(); HashSet attachmentsSlots = HashSetPool.Pool.Get(); @@ -88,6 +86,8 @@ private static void GenerateAttachments() code *= 2U; } + firearm.Destroy(); + uint baseCode = 0; attachmentsSlots diff --git a/Exiled.Example/Config.cs b/Exiled.Example/Config.cs index eb50a7e083..d0d936ca5b 100644 --- a/Exiled.Example/Config.cs +++ b/Exiled.Example/Config.cs @@ -16,6 +16,6 @@ public sealed class Config : IConfig public bool IsEnabled { get; set; } = true; /// - public bool Debug { get; set; } = false; + public bool Debug { get; set; } = true; } } \ No newline at end of file diff --git a/Exiled.Example/TestItem/CustomItemType.cs b/Exiled.Example/TestItem/CustomItemType.cs new file mode 100644 index 0000000000..7f4ac86d6b --- /dev/null +++ b/Exiled.Example/TestItem/CustomItemType.cs @@ -0,0 +1,25 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestItem +{ + using Exiled.CustomModules.API.Enums; + + /// + public class CustomItemType : UUCustomItemType + { + /// + /// Initializes a new custom item id for test item. + /// + public static readonly CustomItemType TestItem = new(); + + /// + /// Initializes a new custom item id for test pickup. + /// + public static readonly CustomItemType TestPickup = new(); + } +} \ No newline at end of file diff --git a/Exiled.Example/TestItem/TestItem.cs b/Exiled.Example/TestItem/TestItem.cs new file mode 100644 index 0000000000..e562a1d1ac --- /dev/null +++ b/Exiled.Example/TestItem/TestItem.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestItem +{ + using Exiled.CustomModules.API.Features.Attributes; + using Exiled.CustomModules.API.Features.CustomItems; + + /// + [ModuleIdentifier] + public class TestItem : CustomItem + { + /// + public override string Name { get; set; } = "TestItem"; + + /// + public override uint Id { get; set; } = CustomItemType.TestItem; + + /// + public override bool IsEnabled { get; set; } = true; + + /// + public override string Description { get; set; } = "Custom item for testing purposes."; + + /// + public override ItemType ItemType { get; set; } = ItemType.Coin; + + /// + public override SettingsBase Settings { get; set; } = new Settings() + { + PickedUpText = new("You picked up a test item!"), + }; + } +} \ No newline at end of file diff --git a/Exiled.Example/TestItem/TestItemBehaviour.cs b/Exiled.Example/TestItem/TestItemBehaviour.cs new file mode 100644 index 0000000000..8d39ee4d37 --- /dev/null +++ b/Exiled.Example/TestItem/TestItemBehaviour.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestItem +{ + using Exiled.API.Features; + using Exiled.CustomModules.API.Features.CustomItems.Items; + using Exiled.Events.EventArgs.Player; + + /// + public class TestItemBehaviour : ItemBehaviour + { + /// + protected override void OnPickingUp(PickingUpItemEventArgs ev) + { + base.OnPickingUp(ev); + + Log.ErrorWithContext("Test Item is being picked up."); + } + + /// + protected override void OnAcquired(bool displayMessage = true) + { + base.OnAcquired(displayMessage); + + Log.ErrorWithContext("Test Item is was picked up."); + } + + /// + protected override void OnDropping(DroppingItemEventArgs ev) + { + base.OnDropping(ev); + + Log.ErrorWithContext("Test Item is being dropped."); + } + } +} \ No newline at end of file diff --git a/Exiled.Example/TestItem/TestItemConfig.cs b/Exiled.Example/TestItem/TestItemConfig.cs new file mode 100644 index 0000000000..2ea0158cc4 --- /dev/null +++ b/Exiled.Example/TestItem/TestItemConfig.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestItem +{ + using Exiled.CustomModules.API.Features.Attributes; + using Exiled.CustomModules.API.Features.CustomItems; + using Exiled.CustomModules.API.Features.Generic; + + /// + [ModuleIdentifier] + public class TestItemConfig : ModulePointer + { + /// + public override uint Id { get; set; } = CustomItemType.TestItem; + + /// + /// Gets or sets a string value. + /// + public string ValueString { get; set; } = "Value"; + } +} \ No newline at end of file diff --git a/Exiled.Example/TestPickup/TestPickup.cs b/Exiled.Example/TestPickup/TestPickup.cs new file mode 100644 index 0000000000..794756cd78 --- /dev/null +++ b/Exiled.Example/TestPickup/TestPickup.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestPickup +{ + using Exiled.API.Enums; + using Exiled.CustomModules.API.Features.Attributes; + using Exiled.CustomModules.API.Features.CustomItems; + using Exiled.Example.TestItem; + + /// + [ModuleIdentifier] + public class TestPickup : CustomItem + { + /// + public override string Name { get; set; } = "TestPickup"; + + /// + public override uint Id { get; set; } = CustomItemType.TestPickup; + + /// + public override bool IsEnabled { get; set; } = true; + + /// + public override string Description { get; set; } = "Custom pickup for testing purposes."; + + /// + public override ItemType ItemType { get; set; } = ItemType.Medkit; + + /// + public override SettingsBase Settings { get; set; } = new Settings() + { + PickedUpText = new("You picked up a test pickup!", 5, channel: TextChannelType.Broadcast), + }; + } +} \ No newline at end of file diff --git a/Exiled.Example/TestPickup/TestPickupBehaviour.cs b/Exiled.Example/TestPickup/TestPickupBehaviour.cs new file mode 100644 index 0000000000..627251dbe2 --- /dev/null +++ b/Exiled.Example/TestPickup/TestPickupBehaviour.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestPickup +{ + using Exiled.API.Features; + using Exiled.CustomModules.API.Features.CustomItems.Pickups; + using Exiled.Events.EventArgs.Player; + + /// + public class TestPickupBehaviour : PickupBehaviour + { + /// + protected override void OnPickingUp(PickingUpItemEventArgs ev) + { + base.OnPickingUp(ev); + + Log.InfoWithContext("Test Pickup is being picked up."); + } + + /// + protected override void OnAcquired(Player player, bool displayMessage = true) + { + base.OnAcquired(player, displayMessage); + + Log.InfoWithContext("Test Pickup has been picked up."); + + player.Heal(10f, true); + } + } +} \ No newline at end of file diff --git a/Exiled.Example/TestPickup/TestPickupConfig.cs b/Exiled.Example/TestPickup/TestPickupConfig.cs new file mode 100644 index 0000000000..8260f8fe95 --- /dev/null +++ b/Exiled.Example/TestPickup/TestPickupConfig.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestPickup +{ + using Exiled.CustomModules.API.Features.Attributes; + using Exiled.CustomModules.API.Features.CustomItems; + using Exiled.CustomModules.API.Features.Generic; + using Exiled.Example.TestItem; + + /// + [ModuleIdentifier] + public class TestPickupConfig : ModulePointer + { + /// + public override uint Id { get; set; } = CustomItemType.TestPickup; + + /// + /// Gets or sets a string value. + /// + public string ValueString { get; set; } = "Value"; + } +} \ No newline at end of file diff --git a/Exiled.Example/TestRole/CustomRoleType.cs b/Exiled.Example/TestRole/CustomRoleType.cs index 67fe24e309..f7c6a6503f 100644 --- a/Exiled.Example/TestRole/CustomRoleType.cs +++ b/Exiled.Example/TestRole/CustomRoleType.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) Exiled Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. @@ -17,6 +17,6 @@ public class CustomRoleType : UUCustomRoleType /// /// Initializes a new custom role id. /// - public static readonly CustomRoleType Scp999 = new(); + public static readonly CustomRoleType TestRole = new(); } } \ No newline at end of file diff --git a/Exiled.Example/TestRole/Scp999Behaviour.cs b/Exiled.Example/TestRole/Scp999Behaviour.cs deleted file mode 100644 index 4e5e443ed8..0000000000 --- a/Exiled.Example/TestRole/Scp999Behaviour.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) Exiled Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Example.TestRole -{ - using Exiled.CustomModules.API.Features.CustomRoles; - - /// - public class Scp999Behaviour : RoleBehaviour - { - /// - protected override void PostInitialize() - { - base.PostInitialize(); - } - } -} \ No newline at end of file diff --git a/Exiled.Example/TestRole/Scp999Role.cs b/Exiled.Example/TestRole/TestRole.cs similarity index 77% rename from Exiled.Example/TestRole/Scp999Role.cs rename to Exiled.Example/TestRole/TestRole.cs index 883110626e..8b42da381d 100644 --- a/Exiled.Example/TestRole/Scp999Role.cs +++ b/Exiled.Example/TestRole/TestRole.cs @@ -1,5 +1,5 @@ -// ----------------------------------------------------------------------- -// +// ----------------------------------------------------------------------- +// // Copyright (c) Exiled Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. // @@ -14,19 +14,19 @@ namespace Exiled.Example.TestRole /// [ModuleIdentifier] - public class Scp999Role : CustomRole + public class TestRole : CustomRole { /// - public override string Name { get; set; } = "SCP-999"; + public override string Name { get; set; } = "TestRole"; /// - public override uint Id { get; set; } = CustomRoleType.Scp999; + public override uint Id { get; set; } = CustomRoleType.TestRole; /// public override bool IsEnabled { get; set; } = true; /// - public override string Description { get; set; } = "SCP-999"; + public override string Description { get; set; } = "Custom role for testing purposes."; /// public override RoleTypeId Role { get; set; } = RoleTypeId.ClassD; @@ -55,9 +55,9 @@ public class Scp999Role : CustomRole Health = 300, MaxHealth = 400, Scale = 0.90f, - CustomInfo = "SCP-999", + CustomInfo = "Test Role", - SpawnedText = new("You've been spawned as SCP-999", 10, channel: TextChannelType.Broadcast), + SpawnedText = new("You've been spawned as Test Role", 10, channel: TextChannelType.Broadcast), PreservePosition = true, diff --git a/Exiled.Example/TestRole/TestRoleBehaviour.cs b/Exiled.Example/TestRole/TestRoleBehaviour.cs new file mode 100644 index 0000000000..65c6f8f130 --- /dev/null +++ b/Exiled.Example/TestRole/TestRoleBehaviour.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Example.TestRole +{ + using Exiled.API.Features; + using Exiled.CustomModules.API.Features.CustomRoles; + + /// + public class TestRoleBehaviour : RoleBehaviour + { + /// + /// Gets or sets a integer value. + /// + public int Value { get; set; } + + /// + protected override void OnBeginPlay() + { + base.OnBeginPlay(); + + Log.Info($"Value: {Value}"); + } + } +} \ No newline at end of file diff --git a/Exiled.Example/TestRole/Scp999Config.cs b/Exiled.Example/TestRole/TestRoleConfig.cs similarity index 54% rename from Exiled.Example/TestRole/Scp999Config.cs rename to Exiled.Example/TestRole/TestRoleConfig.cs index 44cd93c69a..d212718673 100644 --- a/Exiled.Example/TestRole/Scp999Config.cs +++ b/Exiled.Example/TestRole/TestRoleConfig.cs @@ -1,5 +1,5 @@ -// ----------------------------------------------------------------------- -// +// ----------------------------------------------------------------------- +// // Copyright (c) Exiled Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. // @@ -13,9 +13,14 @@ namespace Exiled.Example.TestRole /// [ModuleIdentifier] - public class Scp999Config : ModulePointer + public class TestRoleConfig : ModulePointer { /// - public override uint Id { get; set; } = CustomRoleType.Scp999; + public override uint Id { get; set; } = CustomRoleType.TestRole; + + /// + /// Gets or sets a integer value. + /// + public int Value { get; set; } = 10; } } \ No newline at end of file diff --git a/Exiled.Loader/ConfigManager.cs b/Exiled.Loader/ConfigManager.cs index 57fe2878a0..8e96e6b22f 100644 --- a/Exiled.Loader/ConfigManager.cs +++ b/Exiled.Loader/ConfigManager.cs @@ -21,13 +21,18 @@ namespace Exiled.Loader using Exiled.API.Features.Core.Generic.Pools; using YamlDotNet.Core; - using Serialization = API.Features.EConfig; + using Serialization = API.Features.ConfigSubsystem; /// /// Used to handle plugin configs. /// public static class ConfigManager { + /// + /// Implements the . + /// + public static void LoadConfigSubsystem() => Serialization.LoadAll(Loader.Plugins.Select(asm => asm.Assembly)); + /// /// Loads all the plugin configs. /// diff --git a/Exiled.Loader/Loader.cs b/Exiled.Loader/Loader.cs index 6a653fbc38..2af4cb01ec 100644 --- a/Exiled.Loader/Loader.cs +++ b/Exiled.Loader/Loader.cs @@ -416,8 +416,8 @@ public IEnumerator Run(Assembly[] dependencies = null) LoadDependencies(); LoadPlugins(); - EConfig.LoadAll(); ConfigManager.Reload(); + ConfigManager.LoadConfigSubsystem(); TranslationManager.Reload(); EnablePlugins(); diff --git a/Exiled.Loader/TranslationManager.cs b/Exiled.Loader/TranslationManager.cs index f225cad05b..04f430ed1a 100644 --- a/Exiled.Loader/TranslationManager.cs +++ b/Exiled.Loader/TranslationManager.cs @@ -21,7 +21,7 @@ namespace Exiled.Loader using YamlDotNet.Core; - using Serialization = API.Features.EConfig; + using Serialization = API.Features.ConfigSubsystem; /// /// Used to handle plugin translations.