diff --git a/MonkeyLoader/Components/Entity.cs b/MonkeyLoader/Components/Entity.cs index d2676eb..c4a1477 100644 --- a/MonkeyLoader/Components/Entity.cs +++ b/MonkeyLoader/Components/Entity.cs @@ -1,4 +1,5 @@ -using System; +using MonkeyLoader.Meta.Tagging; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -12,7 +13,7 @@ namespace MonkeyLoader.Components /// that instances can belong to. /// /// The type of the entity. - public abstract class Entity : IEntity + public abstract class Entity : IEntity, ITaggable where TEntity : Entity { /// @@ -20,6 +21,9 @@ public abstract class Entity : IEntity TEntity IEntity.Self => (TEntity)this; + /// + public TagCollection Tags { get; } = []; + /// /// Creates a new entity instance, using this as the /// Entity for the diff --git a/MonkeyLoader/Configuration/ConfigAccessLevelTag.cs b/MonkeyLoader/Configuration/ConfigAccessLevelTag.cs new file mode 100644 index 0000000..dc4c129 --- /dev/null +++ b/MonkeyLoader/Configuration/ConfigAccessLevelTag.cs @@ -0,0 +1,50 @@ +using MonkeyLoader.Meta.Tagging; + +namespace MonkeyLoader.Configuration +{ + /// + /// Defines the possible access levels for config sections or their items. + /// + public enum ConfigAccessLevel + { + /// + /// The default level of access.
+ /// This config section or item should always be shown to users. + ///
+ Regular, + + /// + /// Access for advanced users.
+ /// This config section or item should only be shown to advanced users. + ///
+ Advanced, + + /// + /// Internal access of the mod only.
+ /// This config section or item should never be shown to users. + ///
+ Internal + } + + /// + /// Defines the tag. + /// + /// + /// This tag should be used for config sections or their items to define when or how they can be accessed. + /// + public sealed class ConfigAccessLevelTag : DataTag + { + /// + public override string Description => "Tags a config section or its items with an access level."; + + /// + public override string Id => nameof(ConfigAccessLevel); + + /// + /// Creates a new instance of this tag with the given access . + /// + /// The access level to store. + public ConfigAccessLevelTag(ConfigAccessLevel level) : base(level) + { } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Configuration/ConfigKeyComponent.cs b/MonkeyLoader/Configuration/ConfigKeyComponent.cs index 4b4adf8..fc3d313 100644 --- a/MonkeyLoader/Configuration/ConfigKeyComponent.cs +++ b/MonkeyLoader/Configuration/ConfigKeyComponent.cs @@ -1,4 +1,5 @@ using MonkeyLoader.Components; +using MonkeyLoader.Meta.Tagging; namespace MonkeyLoader.Configuration { @@ -17,6 +18,9 @@ public static class ConfigKeyComponent public static void Add(this IEntity configKey, IConfigKeyComponent component) where TKey : class, IDefiningConfigKey, IEntity => configKey.Components.Add(component); + + public static void Add(this IDefiningConfigKey configKey, ITag tag) + => configKey.Tags.Add(tag); } /// diff --git a/MonkeyLoader/Configuration/DefiningConfigKey.cs b/MonkeyLoader/Configuration/DefiningConfigKey.cs index 00d05bf..9808b03 100644 --- a/MonkeyLoader/Configuration/DefiningConfigKey.cs +++ b/MonkeyLoader/Configuration/DefiningConfigKey.cs @@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis; using MonkeyLoader.Meta; using EnumerableToolkit; +using MonkeyLoader.Meta.Tagging; namespace MonkeyLoader.Configuration { @@ -80,7 +81,7 @@ public bool HasChanges public string Id => AsUntyped.Id; /// - public bool InternalAccessOnly { get; } + public bool InternalAccessOnly => (Tags[nameof(ConfigAccessLevel)].OfType().FirstOrDefault()?.Data ?? ConfigAccessLevel.Regular) is ConfigAccessLevel.Internal; /// public bool IsDefiningKey => true; @@ -89,10 +90,10 @@ public bool HasChanges IIdentifiable INestedIdentifiable.Parent => Section; - /// /// /// Add a priority component to this config key or set a value during initialization. /// + /// public int Priority { get => Components.TryGet(out var priorityComponent) ? priorityComponent.Priority : 0; @@ -164,7 +165,8 @@ public DefiningConfigKey(string id, string? description = null, Func? compute if (valueValidator is not null) Components.Add(new ConfigKeyValidator(valueValidator)); - InternalAccessOnly = internalAccessOnly; + if (internalAccessOnly) + Tags.Add(new ConfigAccessLevelTag(ConfigAccessLevel.Internal)); _canAlwaysHaveChanges = !ValueType.IsValueType && !(typeof(INotifyPropertyChanged).IsAssignableFrom(ValueType) || typeof(INotifyCollectionChanged).IsAssignableFrom(ValueType)); @@ -437,7 +439,7 @@ event ConfigKeyChangedEventHandler? IDefiningConfigKey.Changed /// Defines the definition for a config item. /// public interface IDefiningConfigKey : ITypedConfigKey, IEntity, - INestedIdentifiable, IPrioritizable + INestedIdentifiable, IPrioritizable, ITaggable { /// /// Gets the config this item belongs to. diff --git a/MonkeyLoader/Meta/IMonkey.cs b/MonkeyLoader/Meta/IMonkey.cs index fc7932f..6bea3ff 100644 --- a/MonkeyLoader/Meta/IMonkey.cs +++ b/MonkeyLoader/Meta/IMonkey.cs @@ -1,6 +1,7 @@ using HarmonyLib; using MonkeyLoader.Configuration; using MonkeyLoader.Logging; +using MonkeyLoader.Meta.Tagging; using MonkeyLoader.Patching; using System; using System.Collections.Generic; @@ -35,7 +36,7 @@ public interface IEarlyMonkey : IMonkey /// Defines the interface for all (early) /// monkeys. /// - public interface IMonkey : IRun, IShutdown, IComparable, INestedIdentifiable, IAuthorable + public interface IMonkey : IRun, IShutdown, IComparable, INestedIdentifiable, IAuthorable, ITaggable { /// /// Gets the name of the assembly this monkey is defined in. @@ -68,7 +69,7 @@ public interface IMonkey : IRun, IShutdown, IComparable, INestedIdentif public bool Enabled { get; set; } /// - /// Gets the this monkey's toggle + /// Gets the this monkey's toggle /// if it can be disabled. /// /// The toggle config item if this monkey can be disabled; otherwise, null. diff --git a/MonkeyLoader/Meta/Mod.cs b/MonkeyLoader/Meta/Mod.cs index ffe550c..77a1513 100644 --- a/MonkeyLoader/Meta/Mod.cs +++ b/MonkeyLoader/Meta/Mod.cs @@ -21,7 +21,6 @@ namespace MonkeyLoader.Meta public abstract partial class Mod : IConfigOwner, IShutdown, ILoadedNuGetPackage, IComparable, INestedIdentifiableOwner, INestedIdentifiableOwner, IIdentifiableOwner, IIdentifiableOwner, IAuthorable - { /// /// The file extension for mods' assemblies. diff --git a/MonkeyLoader/Meta/MonkeyTogglesConfigSection.cs b/MonkeyLoader/Meta/MonkeyTogglesConfigSection.cs index 1262664..df7452e 100644 --- a/MonkeyLoader/Meta/MonkeyTogglesConfigSection.cs +++ b/MonkeyLoader/Meta/MonkeyTogglesConfigSection.cs @@ -12,7 +12,7 @@ namespace MonkeyLoader.Meta /// public sealed class MonkeyTogglesConfigSection : ExpandoConfigSection { - private readonly Dictionary> _togglesByMonkey = new(); + private readonly Dictionary> _togglesByMonkey = []; /// public override string Description => "Contains toggles for the Monkeys of a mod which support disabling."; @@ -44,8 +44,11 @@ internal MonkeyTogglesConfigSection(Mod mod) public static ITypedConfigKey GetTemplateKey(IMonkey monkey) => new ConfigKey(monkey.Id); + /// + /// Gets or creates the toggle config item for the given (early) monkey. + /// /// - /// The default for toggles created with this method is true. + /// The default for toggles created with this method is always true. /// /// public IDefiningConfigKey GetToggle(IMonkey monkey) diff --git a/MonkeyLoader/Meta/Tagging/DataTag.cs b/MonkeyLoader/Meta/Tagging/DataTag.cs new file mode 100644 index 0000000..9d37bc4 --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/DataTag.cs @@ -0,0 +1,27 @@ +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Implements an abstract base class for any s. + /// + /// The type of data associated with this tag type. + public abstract class DataTag : Tag, IDataTag + { + /// + public T Data { get; } + + object? IDataTag.Data => Data; + + /// + /// Creates a new instance of this tag type storing the given . + /// + /// The data associated with this instance. + protected DataTag(T data) + { + Data = data; + } + + /// + public override string ToString() + => $"Data Tag: {Id} - {(Data is null ? "null" : Data.ToString())}"; + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/GenericTag.cs b/MonkeyLoader/Meta/Tagging/GenericTag.cs new file mode 100644 index 0000000..a30af10 --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/GenericTag.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Represents a generic tag that can have any Id and Data. + /// + /// + public sealed class GenericTag : DataTag + { + private static readonly Dictionary _descriptionsById = new(StringComparer.OrdinalIgnoreCase); + + /// + public override string Description { get; } + + /// + public override string Id { get; } + + /// + /// Creates a new generic tag instance with the given and . + /// + /// The unique identifier of this type of tag. + /// The data associated with this instance. + public GenericTag(string id, T data) : base(data) + { + Id = id; + + if (!_descriptionsById.TryGetValue(id, out var description)) + { + description = $"GenericTag<{typeof(T).CompactDescription()}> with id: {id}"; + _descriptionsById.Add(id.ToLower(), description); + } + + Description = description; + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/IDataTag.cs b/MonkeyLoader/Meta/Tagging/IDataTag.cs new file mode 100644 index 0000000..5724121 --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/IDataTag.cs @@ -0,0 +1,24 @@ +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Defines the non-generic interface for the data tags of s. + /// + public interface IDataTag : ITag + { + /// + /// Gets the boxed data associated with this tag instance. + /// + public object? Data { get; } + } + + /// + /// Defines the generic interface for the data tags of s. + /// + public interface IDataTag : IDataTag + { + /// + /// Gets the data associated with this tag instance. + /// + public new T Data { get; } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/ITag.cs b/MonkeyLoader/Meta/Tagging/ITag.cs new file mode 100644 index 0000000..218500e --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/ITag.cs @@ -0,0 +1,29 @@ +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Defines the interface for the presence tags of s. + /// + public interface ITag + { + /// + /// Gets the description for this type of tag. + /// + public string Description { get; } + + /// + /// Gets the unique identifier of this type of tag. + /// + /// + /// Case should be ignored when using this. + /// + public string Id { get; } + + /// + /// Gets the name for this type of tag. + /// + /// + /// Implementations may default to the Id. + /// + public string Name { get; } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/ITaggable.cs b/MonkeyLoader/Meta/Tagging/ITaggable.cs new file mode 100644 index 0000000..05340f8 --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/ITaggable.cs @@ -0,0 +1,13 @@ +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Defines the interface for object that support tagging using s. + /// + public interface ITaggable + { + /// + /// Gets the tags of this object. + /// + public TagCollection Tags { get; } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/Tag.cs b/MonkeyLoader/Meta/Tagging/Tag.cs new file mode 100644 index 0000000..87a119e --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/Tag.cs @@ -0,0 +1,24 @@ +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Implements an abstract base class for any s. + /// + public abstract class Tag : ITag + { + /// + public abstract string Description { get; } + + /// + public abstract string Id { get; } + + /// + /// By default: The Id of this tag. + /// + /// + public virtual string Name => Id; + + /// + public override string ToString() + => $"Presence Tag: {Id}"; + } +} \ No newline at end of file diff --git a/MonkeyLoader/Meta/Tagging/TagCollection.cs b/MonkeyLoader/Meta/Tagging/TagCollection.cs new file mode 100644 index 0000000..fd5941d --- /dev/null +++ b/MonkeyLoader/Meta/Tagging/TagCollection.cs @@ -0,0 +1,145 @@ +using EnumerableToolkit; +using NuGet.Packaging; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MonkeyLoader.Meta.Tagging +{ + /// + /// Stores an 's s. + /// + public sealed class TagCollection : IEnumerable>> + { + private readonly Dictionary> _tagsById = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the total number of s stored in this collection. + /// + public int Count => _tagsById.Values.Sum(static tagSet => tagSet.Count); + + /// + /// Gets the number of distinct Ids of s stored in this collection. + /// + public int IdCount => _tagsById.Count; + + /// + /// Gets the distinct Ids of s stored in this collection. + /// + public IEnumerable Ids => _tagsById.Keys; + + /// + /// Gets all s stored in this collection. + /// + public IEnumerable Instances => _tagsById.Values.SelectMany(static set => set); + + /// + /// Gets or sets the tags associated with the given . + /// + /// + /// This will return an empty enumerable instead of throwing + /// when there is no with the given . + /// + /// The id of the tags to get or replace. Case is ignored. + /// All tags with the given . + public IEnumerable this[string id] + { + get => _tagsById.TryGetValue(id, out var tagSet) ? tagSet.AsSafeEnumerable() : []; + + set + { + if (!value.Any()) + { + _tagsById.Remove(id); + return; + } + + var tagSet = GetOrCreateTagSetById(id); + tagSet.Clear(); + tagSet.AddRange(value); + } + } + + /// + /// Ensures that the given is included in this collection. + /// + /// The tag to include in this collection. + /// true if the was added; false if it was already present. + public bool Add(ITag tag) + => GetOrCreateTagSetById(tag.Id).Add(tag); + + /// + /// Ensures that the given are included in this collection. + /// + /// The tags to include in this collection. + public void AddRange(IEnumerable tags) + { + foreach (var tag in tags) + Add(tag); + } + + /// + /// Tests whether any s with the given are stored in this collection. + /// + /// The id of the tags to check for. Case is ignored. + /// true if this collections stores any s with the given ; otherwise, false. + public bool AnyWithId(string id) + => _tagsById.ContainsKey(id); + + /// + /// Removes all s from this collection. + /// + public void Clear() + => _tagsById.Clear(); + + /// + /// Tests whether the given is stored in this collection. + /// + /// The tag to check for. + /// true if the given is stored in this collection; otherwise, false. + public bool Contains(ITag tag) + { + if (!_tagsById.TryGetValue(tag.Id, out var tagSet)) + return false; + + return tagSet.Contains(tag); + } + + /// + public IEnumerator>> GetEnumerator() + => _tagsById.Select(static tagPair => new KeyValuePair>(tagPair.Key, tagPair.Value.AsSafeEnumerable())).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Ensures that the given is not stored in this collection. + /// + /// The tag to exclude from this collection. + /// true if the tag was removed; false if it wasn't stored in this collection. + public bool Remove(ITag tag) + { + if (!_tagsById.TryGetValue(tag.Id, out var tagSet)) + return false; + + var removed = tagSet.Remove(tag); + + if (tagSet.Count is 0) + _tagsById.Remove(tag.Id); + + return removed; + } + + private HashSet GetOrCreateTagSetById(string id) + { + if (!_tagsById.TryGetValue(id, out var tagSet)) + { + tagSet = []; + _tagsById.Add(id, tagSet); + } + + return tagSet; + } + } +} \ No newline at end of file diff --git a/MonkeyLoader/Patching/MonkeyBase.cs b/MonkeyLoader/Patching/MonkeyBase.cs index 6512123..fd4f747 100644 --- a/MonkeyLoader/Patching/MonkeyBase.cs +++ b/MonkeyLoader/Patching/MonkeyBase.cs @@ -3,6 +3,7 @@ using MonkeyLoader.Configuration; using MonkeyLoader.Logging; using MonkeyLoader.Meta; +using MonkeyLoader.Meta.Tagging; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -148,6 +149,9 @@ internal set /// public bool ShutdownRan { get; private set; } = false; + /// + public TagCollection Tags { get; } = []; + /// public Type Type { get; }