diff --git a/README.md b/README.md index e03a7a1..e64a565 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,28 @@ Fixing bugs that BSG won't or can't - Skips "You can return to this later" warnings when not transferring all items - "Receive All" button no longer shows up when there is nothing to receive + +## Interop + +UI Fixes offers interop with other mods that want to use the multi-select functionality, _without_ taking a hard dependency on `Tyfon.UIFixes.dll`. + +To do this, simply download and add [MultiSelectInterop.cs](src/Multiselect/MultiSelectInterop.cs) to your client project. It will take care of testing if UI Fixes is present and, using reflection, interoping with the mod. + +MultiSelectInterop exposes a small static surface to give you access to the multi-selection. + +```cs +public static class MultiSelect +{ + // Returns the number of items in the current selection + public static int Count { get; } + + // Returns the items in the current selection + public static IEnumerable Items { get; } + + // Executes an operation on each item in the selection, sequentially + // Passing an ItemUiContext is optional as it will use ItemUiContext.Instance if needed + // The second overload takes an async operation and returns a task representing the aggregate. + public static void Apply(Action action, ItemUiContext context = null); + public static Task Apply(Func func, ItemUiContext context = null); +} +``` diff --git a/src/Multiselect/MultiSelectInterop.cs b/src/Multiselect/MultiSelectInterop.cs index c374ce1..43d1b6c 100644 --- a/src/Multiselect/MultiSelectInterop.cs +++ b/src/Multiselect/MultiSelectInterop.cs @@ -8,108 +8,132 @@ using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -namespace UIFixesInterop; +/* +UI Fixes Multi-Select InterOp -/// -/// Provides access to UI Fixes' multiselect functionality. -/// -internal class MultiSelect +First, add the following attribute to your plugin class: + +[BepInDependency("Tyfon.UIFixes", BepInDependency.DependencyFlags.SoftDependency)] + +This will ensure UI Fixes is loaded already when your code is run. It will fail gracefully if UI Fixes is missing. + +Second, add this file to your project. Use the below UIFixesInterop.MultiSelect static methods, no explicit initialization required. + +Some things to keep in mind: +- While you can use MultiSelect.Items to get the items, this should only be used for reading purproses. If you need to +execute an operation on the items, I strongly suggest using the provided MultiSelect.Apply() method. + +- Apply() will execute the provided operation on each item, sequentially (sorted by grid order), maximum of one operation per frame. +It does this because strange bugs manifest if you try to do more than one thing in a single frame. + +- If the operation you are passing to Apply() does anything async, use the overload that takes a Func. It will wait +until each operation is over before doing the next. This is especially important if an operation could be affected by the preceding one, +for example in a quick-move where the avaiable space changes. It's also required if you are doing anything in-raid that will trigger +an animation, as starting the next one before it is complete will likely cancel the first. +*/ +namespace UIFixesInterop { - private static readonly Version RequiredVersion = new(2, 5); - - private static bool? UIFixesLoaded; - - private static Type MultiSelectType; - private static MethodInfo GetCountMethod; - private static MethodInfo GetItemsMethod; - private static MethodInfo ApplyMethod; - - /// Count represents the number of items in the current selection, 0 if UI Fixes is not present. - public int Count - { - get - { - if (!Loaded()) - { - return 0; - } - - return (int)GetCountMethod.Invoke(null, []); - } - } - - /// Items is an enumerable list of items in the current selection, empty if UI Fixes is not present - public IEnumerable Items - { - get - { - if (!Loaded()) - { - return []; - } - - return (IEnumerable)GetItemsMethod.Invoke(null, []); - } - } - /// - /// This method takes an Action and calls it *sequentially* on each item in the current selection. - /// Will no-op if UI Fixes is not present. + /// Provides access to UI Fixes' multiselect functionality. /// - /// The action to call on each item. - /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. - public void Apply(Action action, ItemUiContext itemUiContext = null) + internal static class MultiSelect { - if (!Loaded()) + private static readonly Version RequiredVersion = new Version(2, 5); + + private static bool? UIFixesLoaded; + + private static Type MultiSelectType; + private static MethodInfo GetCountMethod; + private static MethodInfo GetItemsMethod; + private static MethodInfo ApplyMethod; + + /// Count represents the number of items in the current selection, 0 if UI Fixes is not present. + public static int Count { - return; - } - - Func func = item => - { - action(item); - return Task.CompletedTask; - }; - - ApplyMethod.Invoke(null, [func, itemUiContext]); - } - - /// - /// This method takes an Func that returns a Task and calls it *sequentially* on each item in the current selection. - /// Will return a completed task immediately if UI Fixes is not present. - /// - /// The function to call on each item - /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. - /// A Task that will complete when all the function calls are complete. - public Task Apply(Func func, ItemUiContext itemUiContext = null) - { - if (!Loaded()) - { - return Task.CompletedTask; - } - - return (Task)ApplyMethod.Invoke(null, [func, itemUiContext]); - } - - private bool Loaded() - { - if (!UIFixesLoaded.HasValue) - { - bool present = Chainloader.PluginInfos.TryGetValue("Tyfon.UIFixes", out PluginInfo pluginInfo); - UIFixesLoaded = present && pluginInfo.Metadata.Version >= RequiredVersion; - - if (UIFixesLoaded.Value) + get { - MultiSelectType = Type.GetType("UIFixes.MultiSelectController, UIFixes"); - if (MultiSelectType != null) + if (!Loaded()) { - GetCountMethod = AccessTools.Method(MultiSelectType, "GetCount"); - GetItemsMethod = AccessTools.Method(MultiSelectType, "GetItems"); - ApplyMethod = AccessTools.Method(MultiSelectType, "Apply"); + return 0; + } + + return (int)GetCountMethod.Invoke(null, new object[] { }); + } + } + + /// Items is an enumerable list of items in the current selection, empty if UI Fixes is not present. + public static IEnumerable Items + { + get + { + if (!Loaded()) + { + return new Item[] { }; + } + + return (IEnumerable)GetItemsMethod.Invoke(null, new object[] { }); + } + } + + /// + /// This method takes an Action and calls it *sequentially* on each item in the current selection. + /// Will no-op if UI Fixes is not present. + /// + /// The action to call on each item. + /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. + public static void Apply(Action action, ItemUiContext itemUiContext = null) + { + if (!Loaded()) + { + return; + } + + Func func = item => + { + action(item); + return Task.CompletedTask; + }; + + ApplyMethod.Invoke(null, new object[] { func, itemUiContext }); + } + + /// + /// This method takes an Func that returns a Task and calls it *sequentially* on each item in the current selection. + /// Will return a completed task immediately if UI Fixes is not present. + /// + /// The function to call on each item + /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. + /// A Task that will complete when all the function calls are complete. + public static Task Apply(Func func, ItemUiContext itemUiContext = null) + { + if (!Loaded()) + { + return Task.CompletedTask; + } + + return (Task)ApplyMethod.Invoke(null, new object[] { func, itemUiContext }); + } + + private static bool Loaded() + { + if (!UIFixesLoaded.HasValue) + { + bool present = Chainloader.PluginInfos.TryGetValue("Tyfon.UIFixes", out PluginInfo pluginInfo); + UIFixesLoaded = present && pluginInfo.Metadata.Version >= RequiredVersion; + + if (UIFixesLoaded.Value) + { + MultiSelectType = Type.GetType("UIFixes.MultiSelectController, Tyfon.UIFixes"); + if (MultiSelectType != null) + { + GetCountMethod = AccessTools.Method(MultiSelectType, "GetCount"); + GetItemsMethod = AccessTools.Method(MultiSelectType, "GetItems"); + ApplyMethod = AccessTools.Method(MultiSelectType, "Apply"); + } } } - } - return UIFixesLoaded.Value; + return UIFixesLoaded.Value; + } } } \ No newline at end of file