Interop fixes, readme

This commit is contained in:
Tyfon
2024-09-21 00:36:22 -07:00
parent 82e5e08177
commit 3321d9bb02
2 changed files with 141 additions and 92 deletions

View File

@@ -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<Item> 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<Item> action, ItemUiContext context = null);
public static Task Apply(Func<Item, Task> func, ItemUiContext context = null);
}
```

View File

@@ -8,108 +8,132 @@ using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
namespace UIFixesInterop;
/*
UI Fixes Multi-Select InterOp
/// <summary>
/// Provides access to UI Fixes' multiselect functionality.
/// </summary>
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<Item, Task>. 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;
/// <value><c>Count</c> represents the number of items in the current selection, 0 if UI Fixes is not present.</value>
public int Count
{
get
{
if (!Loaded())
{
return 0;
}
return (int)GetCountMethod.Invoke(null, []);
}
}
/// <value><c>Items</c> is an enumerable list of items in the current selection, empty if UI Fixes is not present</value>
public IEnumerable<Item> Items
{
get
{
if (!Loaded())
{
return [];
}
return (IEnumerable<Item>)GetItemsMethod.Invoke(null, []);
}
}
/// <summary>
/// This method takes an <c>Action</c> 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.
/// </summary>
/// <param name="action">The action to call on each item.</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
public void Apply(Action<Item> 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;
/// <value><c>Count</c> represents the number of items in the current selection, 0 if UI Fixes is not present.</value>
public static int Count
{
return;
}
Func<Item, Task> func = item =>
{
action(item);
return Task.CompletedTask;
};
ApplyMethod.Invoke(null, [func, itemUiContext]);
}
/// <summary>
/// This method takes an <c>Func</c> that returns a <c>Task</c> and calls it *sequentially* on each item in the current selection.
/// Will return a completed task immediately if UI Fixes is not present.
/// </summary>
/// <param name="func">The function to call on each item</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
/// <returns>A <c>Task</c> that will complete when all the function calls are complete.</returns>
public Task Apply(Func<Item, Task> 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[] { });
}
}
/// <value><c>Items</c> is an enumerable list of items in the current selection, empty if UI Fixes is not present.</value>
public static IEnumerable<Item> Items
{
get
{
if (!Loaded())
{
return new Item[] { };
}
return (IEnumerable<Item>)GetItemsMethod.Invoke(null, new object[] { });
}
}
/// <summary>
/// This method takes an <c>Action</c> and calls it *sequentially* on each item in the current selection.
/// Will no-op if UI Fixes is not present.
/// </summary>
/// <param name="action">The action to call on each item.</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
public static void Apply(Action<Item> action, ItemUiContext itemUiContext = null)
{
if (!Loaded())
{
return;
}
Func<Item, Task> func = item =>
{
action(item);
return Task.CompletedTask;
};
ApplyMethod.Invoke(null, new object[] { func, itemUiContext });
}
/// <summary>
/// This method takes an <c>Func</c> that returns a <c>Task</c> and calls it *sequentially* on each item in the current selection.
/// Will return a completed task immediately if UI Fixes is not present.
/// </summary>
/// <param name="func">The function to call on each item</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
/// <returns>A <c>Task</c> that will complete when all the function calls are complete.</returns>
public static Task Apply(Func<Item, Task> 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;
}
}
}