Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
using MonkeyLoader.Components;
using MonkeyLoader.Configuration;
using MonkeyLoader.Resonite.DataFeeds;
using System;
using System.Collections.Generic;
using System.Text;

namespace MonkeyLoader.Resonite.Configuration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
using Elements.Quantity;
using FrooxEngine;
using MonkeyLoader.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonkeyLoader.Resonite.Configuration
{
Expand Down Expand Up @@ -50,7 +45,7 @@ public ConfigKeyQuantity(UnitConfiguration defaultConfiguration, UnitConfigurati
}
}

/// <summary>su
/// <summary>
/// Defines the typed definition for a quantified config item.
/// </summary>
/// <typeparam name="T">The type of the config item's value.</typeparam>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
using FrooxEngine;
using MonkeyLoader.Components;
using MonkeyLoader.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonkeyLoader.Resonite.Configuration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Elements.Core;
using EnumerableToolkit;
using FrooxEngine;
using MonkeyLoader.Components;
using MonkeyLoader.Configuration;
using MonkeyLoader.Resonite.DataFeeds;

namespace MonkeyLoader.Resonite.Configuration
{
/// <summary>
/// Represents a config key component, which marks keys that should be
/// enumerated in (nested) subgroups for the <see cref="SettingsDataFeed">settings</see>.
/// </summary>
/// <remarks><para>
/// <see cref="LocaleString"/> keys for the subgroups are generated by appending the
/// subgroup's path up to that point to the parent <see cref="ConfigSection"/>'s
/// <see cref="ConfigSection.FullId">FullId</see>, all joined by periods (<c>.</c>).
/// </para><para>
/// This component can be added to multiple config keys, as it does not keep a reference to them.
/// </para></remarks>
public class ConfigKeySubgroup : IConfigKeySubgroup
{
/// <inheritdoc/>
public int Priority { get; }

/// <inheritdoc/>
public Sequence<string> SubgroupPath { get; }

/// <summary>
/// Creates a new <see cref="IConfigKeySubgroup"/> component, which marks keys that should be
/// enumerated in (nested) subgroups for the <see cref="SettingsDataFeed">settings</see>.
/// </summary>
/// <param name="priority">The priority of this subgroup. Higher comes first.</param>
/// <param name="path">The first segment of the subgroup path.</param>
/// <param name="subgroupPath">The further subgroup path elements.</param>
/// <exception cref="ArgumentException">When any subgroup path element is <see cref="string.IsNullOrWhiteSpace(string?)"><see langword="null"/> or whitespace</see>.</exception>
/// <inheritdoc cref="ConfigKeySubgroup"/>
public ConfigKeySubgroup(int priority, string path, params string[]? subgroupPath)
: this(priority, path.Yield().Concat(subgroupPath ?? []))
{ }

/// <param name="priority">The priority of this subgroup. Higher comes first.</param>
/// <param name="subgroupPath">The path that this subgroup should have. Must have at least one element.</param>
/// <exception cref="NullReferenceException">When the <paramref name="subgroupPath"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">When the <paramref name="subgroupPath"/> doesn't have at least one element.</exception>
/// <inheritdoc cref="ConfigKeySubgroup(int, string, string[])"/>
public ConfigKeySubgroup(int priority, IEnumerable<string> subgroupPath)
{
ArgumentNullException.ThrowIfNull(subgroupPath);

Priority = priority;
SubgroupPath = subgroupPath.ToArray();

ArgumentOutOfRangeException.ThrowIfZero(SubgroupPath.Length, nameof(subgroupPath));

if (SubgroupPath.Any(string.IsNullOrWhiteSpace))
throw new ArgumentException("Subgroup path must not have any null or whitespace elements!");
}

/// <inheritdoc cref="ConfigKeySubgroup(int, string, string[])"/>
public ConfigKeySubgroup(string path, params string[]? subgroupPath)
: this(0, path.Yield().Concat(subgroupPath ?? []))
{ }

void IComponent<IDefiningConfigKey>.Initialize(IDefiningConfigKey entity)
{ }
}

/// <summary>
/// Defines the interface for config key components, which mark keys that should be
/// enumerated in (nested) subgroups for the <see cref="SettingsDataFeed">settings</see>.
/// </summary>
/// <remarks>
/// <see cref="LocaleString"/> keys for the subgroups are generated by appending the
/// subgroup's path up to that point to the parent <see cref="ConfigSection"/>'s
/// <see cref="ConfigSection.FullId">FullId</see>, all joined by periods (<c>.</c>).
/// </remarks>
public interface IConfigKeySubgroup : IConfigKeyComponent<IDefiningConfigKey>, ISubgroupedDataFeedItem, IPrioritizable
{ }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using FrooxEngine;
using System;
using System.Collections.Generic;

namespace MonkeyLoader.Resonite.DataFeeds
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Elements.Core;
using EnumerableToolkit;
using FrooxEngine;
using MonkeyLoader.Meta;

namespace MonkeyLoader.Resonite.DataFeeds
{
/// <summary>
/// Defines the interface for objects that should be enumerated in (nested) subgroups for their <see cref="IDataFeed">data feed</see>.
/// </summary>
/// <remarks>
/// <see cref="LocaleString"/> keys for the subgroups are generated by appending the
/// subgroup's path up to that point to the parent <see cref="IIdentifiable"/>'s
/// <see cref="IIdentifiable.FullId">FullId</see>, all joined by periods (<c>.</c>).
/// </remarks>
public interface ISubgroupedDataFeedItem
{
/// <summary>
/// Gets the path that the subgroup of this object should have.<br/>
/// Multiple segments are generated as nested subgroups.
/// </summary>
/// <inheritdoc cref="ISubgroupedDataFeedItem"/>
public Sequence<string> SubgroupPath { get; }
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
using MonkeyLoader.Meta;
using MonkeyLoader.Patching;
using MonkeyLoader.Resonite.Configuration;
using MonkeyLoader.Resonite.Locale;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace MonkeyLoader.Resonite.DataFeeds.Settings
{
Expand Down Expand Up @@ -57,6 +51,19 @@ private static void GenerateLocaleMessages(FallbackLocaleGenerationEvent eventDa
{
eventData.AddMessage(configKey.GetLocaleKey("Name"), configKey.Id);
eventData.AddMessage(configKey.GetLocaleKey("Description"), configKey.Description ?? "No Description");

if (configKey.Components.TryGet<IConfigKeySubgroup>(out var subgroup))
{
var subgroupKey = configSection.FullId;

foreach (var pathElement in subgroup.SubgroupPath)
{
subgroupKey = $"{subgroupKey}.{pathElement}";

eventData.AddMessage($"{subgroupKey}.Name", pathElement);
eventData.AddMessage($"{subgroupKey}.Description", "No Description");
}
}
}
}
}
Expand Down
149 changes: 149 additions & 0 deletions MonkeyLoader.Resonite.Integration/DataFeeds/SubgroupGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using Elements.Core;
using EnumerableToolkit;
using FrooxEngine;
using MonkeyLoader.Meta;

#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)

namespace MonkeyLoader.Resonite.DataFeeds
{
/// <summary>
/// Represents a helper to generate nested <see cref="DataFeedGroup">subgroups</see>
/// for <see cref="ISubgroupedDataFeedItem">subgrouped data feed items</see>.
/// </summary>
/// <remarks><para>
/// The generator uses internal state to track which subgroups have already been created.<br/>
/// As such, it must be recreated for each enumeration of a <see cref="IDataFeed">data feed</see>.
/// </para><para>
/// <see cref="LocaleString"/> keys for the subgroups are generated by appending the
/// subgroup's path up to that point to the <see cref="BaseIdentifiable">BaseIdentifiable</see>'s
/// <see cref="IIdentifiable.FullId">FullId</see>, all joined by periods (<c>.</c>).
/// </para></remarks>
public sealed class SubgroupGenerator
{
private readonly HashSet<Sequence<string>> _createdSubgroups = [];

/// <summary>
/// Gets the group keys for the outer container that the
/// nested <see cref="DataFeedGroup">subgroups</see> should be placed in.
/// </summary>
public IReadOnlyList<string> BaseGroupKeys { get; }

/// <summary>
/// Gets the base <see cref="IIdentifiable">identifiable</see> that's used to
/// generate the <see cref="LocaleString"/>s for the subgroups.
/// </summary>
/// <remarks>
/// <see cref="LocaleString"/> keys for the subgroups are generated by appending the
/// subgroup's path up to that point to the this <see cref="IIdentifiable">identifiable</see>'s
/// <see cref="IIdentifiable.FullId">FullId</see>, all joined by periods (<c>.</c>).
/// </remarks>
public IIdentifiable BaseIdentifiable { get; }

/// <summary>
/// Gets path of the items being enumerated.
/// </summary>
public IReadOnlyList<string> Path { get; }

/// <summary>
/// Creates a new instance of this helper to generate nested <see cref="DataFeedGroup">subgroups</see>
/// for <see cref="ISubgroupedDataFeedItem">subgrouped data feed items</see>.
/// </summary>
/// <param name="baseIdentifiable">The base <see cref="IIdentifiable">identifiable</see> that's used to generate the <see cref="LocaleString"/>s for the subgroups.</param>
/// <param name="path">The path of the items being enumerated.</param>
/// <param name="groupKeys">The group keys for the outer container that the nested <see cref="DataFeedGroup">subgroups</see> should be placed in.</param>
/// <inheritdoc cref="SubgroupGenerator"/>
public SubgroupGenerator(IIdentifiable baseIdentifiable, IReadOnlyList<string> path, IReadOnlyList<string> groupKeys)
{
BaseIdentifiable = baseIdentifiable;
Path = path;
BaseGroupKeys = groupKeys;
}

/// <summary>
/// Enumerates the missing nested subgroups for the given path.
/// </summary>
/// <remarks>
/// Prefer using any of the <c>GetSubgroupEnumerator</c> overloads,
/// since they also return the required group keys to place items in the final subgroup.
/// </remarks>
/// <inheritdoc cref="GetSubgroupEnumerator(Sequence{string}, int, out IReadOnlyList{string})"/>
public async IAsyncEnumerable<DataFeedItem> EnumerateSubgroupsAsync(Sequence<string> subgroupPath, int priority = 0)
{
await Task.CompletedTask;

if (_createdSubgroups.Contains(subgroupPath))
yield break;

for (var i = 0; i < subgroupPath.Length; ++i)
{
var subgroupPathElements = subgroupPath.Take(i + 1).ToArray();

if (!_createdSubgroups.Add(subgroupPathElements))
continue;

var joinedSubgroupPath = string.Join('.', subgroupPathElements);
var label = BaseIdentifiable.GetLocaleString($"{joinedSubgroupPath}.Name");
var description = BaseIdentifiable.GetLocaleString($"{joinedSubgroupPath}.Description");

var subgroup = new DataFeedGroup();
subgroup.InitBase(subgroupPathElements[i], Path, [.. BaseGroupKeys, .. subgroupPathElements[..i]], label);
subgroup.InitSorting(-priority);
subgroup.InitDescription(description);
yield return subgroup;
}
}

/// <summary>
/// Gets an enumerator for the missing nested subgroups for the subgrouped data feed item's path and
/// returns the group keys necessary to place items in the final subgroup,
/// using the item's <see cref="IPrioritizable.Priority">priority</see> too, if available.
/// </summary>
/// <param name="subgroupedItem">
/// The <see cref="ISubgroupedDataFeedItem">subgrouped data feed item</see> containing the
/// <see cref="ISubgroupedDataFeedItem.SubgroupPath">path</see> of the required nested subgroups.<br/>
/// If this is also <see cref="IPrioritizable"/>, that <see cref="IPrioritizable.Priority">priority</see>
/// will be assigned to the newly generated subgroups; otherwise, zero. Higher comes first.
/// </param>
/// <inheritdoc cref="GetSubgroupEnumerator(ISubgroupedDataFeedItem, int, out IReadOnlyList{string})"/>
public IAsyncEnumerable<DataFeedItem> GetSubgroupEnumerator(ISubgroupedDataFeedItem subgroupedItem, out IReadOnlyList<string> groupKeys)
=> GetSubgroupEnumerator(subgroupedItem, (subgroupedItem as IPrioritizable)?.Priority ?? 0, out groupKeys);

/// <summary>
/// Gets an enumerator for the missing nested subgroups for the subgrouped data feed item's path and
/// returns the group keys necessary to place items in the final subgroup.
/// </summary>
/// <param name="subgroupedItem">
/// The <see cref="ISubgroupedDataFeedItem">subgrouped data feed item</see> containing the
/// <see cref="ISubgroupedDataFeedItem.SubgroupPath">path</see> of the required nested subgroups.
/// </param>
/// <inheritdoc cref="GetSubgroupEnumerator(Sequence{string}, int, out IReadOnlyList{string})"/>
public IAsyncEnumerable<DataFeedItem> GetSubgroupEnumerator(ISubgroupedDataFeedItem subgroupedItem, int priority, out IReadOnlyList<string> groupKeys)
{
groupKeys = [.. BaseGroupKeys, .. subgroupedItem.SubgroupPath];

return EnumerateSubgroupsAsync(subgroupedItem.SubgroupPath, priority);
}

/// <summary>
/// Gets an enumerator for the missing nested subgroups for the given path and
/// returns the group keys necessary to place items in the final subgroup.
/// </summary>
/// <param name="subgroupPath">The path of the required nested subgroups.</param>
/// <param name="priority">The optional priority to assign to the newly created subgroups. Higher comes first.</param>
/// <param name="groupKeys">The group keys necessary to place items in the final subgroup.</param>
/// <returns>A sequence of the missing nested subgroups for the given path. Empty if all subgroups have already been generated.</returns>
public IAsyncEnumerable<DataFeedItem> GetSubgroupEnumerator(Sequence<string> subgroupPath, int priority, out IReadOnlyList<string> groupKeys)
{
groupKeys = [.. BaseGroupKeys, .. subgroupPath];

return EnumerateSubgroupsAsync(subgroupPath, priority);
}

/// <inheritdoc cref="GetSubgroupEnumerator(Sequence{string}, int, out IReadOnlyList{string})"/>
public IAsyncEnumerable<DataFeedItem> GetSubgroupEnumerator(Sequence<string> subgroupPath, out IReadOnlyList<string> groupKeys)
=> GetSubgroupEnumerator(subgroupPath, 0, out groupKeys);
}
}

#pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
Loading