Compare commits

..
22 Commits
Author SHA1 Message Date
Az 344aa046ac Merge branch 'dev' into no-boxing 2026-10-26 16:53:21 -04:00
Az 51397d78a7 color improvements 2026-10-26 16:53:02 -04:00
Az 28770e4cd8 Merge branch 'no-boxing' of https://github.com/All-Of-Us-Mods/MiraAPI into no-boxing 2026-10-26 15:41:52 -04:00
Az e8ea720ca0 always update the backing array 2026-10-26 15:41:47 -04:00
AS 7945173061 Merge branch 'dev' into no-boxing 2026-10-26 05:11:53 -04:00
Az 98a37721aa fix small issue 2026-10-26 04:55:46 -04:00
Az 08fd56754e fix small issue 2026-10-26 04:55:38 -04:00
Az accfd04209 several big changes
- abstract display value behaviour to the option itself
- updated deprecation notices a bit
- enforced some consistency
- moved the notification code up in the call stack into SetValue
- made the number option round floats instead of directly casting, due to casting having a truncation behaviour (0.999 becomes 0, not 1)
- optimised the wiki behaviour
2026-10-26 04:47:07 -04:00
Az 936da28f6a actually cache the HarmonyMethod instance instead 2026-10-26 03:38:29 -04:00
Az 6f94e4ed21 reduce value boxing 2026-10-26 03:35:11 -04:00
Az 181013efec minor code fixes and nitpicks 2026-10-26 00:52:19 -04:00
Atony a8dfbb616c Use monobehaviour instead of il2 obj for use event 2026-10-24 17:01:44 -05:00
Az b79c14ce50 resolve warnings that i am responsible for 2026-10-22 22:41:49 -04:00
Az 17dafadec8 move away from using GetCallingAssembly and GetExecutingAssembly 2026-10-22 22:35:17 -04:00
Az f9274eda09 consolidate code 2026-10-22 22:02:33 -04:00
Az 10daf01a55 facepalm 2026-10-22 21:32:13 -04:00
Az 1daffd7560 code ordering and cleanup 2026-10-22 21:15:57 -04:00
Az 26541dc07d clear warnings 2026-10-22 20:54:33 -04:00
Atony 2a7570f022 Adjust code that gets roles and modifiers list 2026-10-22 02:01:05 -05:00
Atony 71138c7bbe Display current role and modifiers at top 2026-10-22 01:17:21 -05:00
Atony 2993ee2bbc Resync options upon clicking start
This is necessary to prevent some pretty common option desync that tends to happen. If anyone else can optimize it further or move it somewhere else, go for it.
2026-10-21 23:37:46 -05:00
Atony 79bbebd12a Allow modifiers and roles to override chance text 2026-10-21 17:48:33 -05:00
65 changed files with 1543 additions and 759 deletions

No files matched your search

+3
View File
@@ -104,3 +104,6 @@ dotnet_diagnostic.S1244.severity = none
# S2325: Methods and properties that don't access instance data should be static
dotnet_diagnostic.S2325.severity = none
# IDE0005: Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = warning
+3 -12
View File
@@ -5,8 +5,6 @@ on: ["workflow_dispatch", "push", "pull_request"]
jobs:
build:
runs-on: ubuntu-22.04
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
with:
@@ -40,21 +38,14 @@ jobs:
with:
name: AllOfUs.MiraAPI.nupkg
path: MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg
- uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
with:
draft: true
files: MiraAPI/bin/Release/net6.0/MiraAPI.dll
- name: NuGet login
if: github.ref_type == 'tag'
uses: NuGet/login@v1
id: login
with:
user: xtracube
- name: Push NuGet package
if: github.ref_type == 'tag'
run: |
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }}
dotnet nuget push MiraAPI/bin/Release/AllOfUs.MiraAPI.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
+1 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.5.0</VersionPrefix>
<VersionPrefix>0.6.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
+9 -6
View File
@@ -1,14 +1,17 @@
using MiraAPI.Utilities.Assets;
using System.Reflection;
using MiraAPI.Utilities.Assets;
namespace MiraAPI.Example;
public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
public static LoadableResourceAsset CallMeetingButton { get; } = new("MiraAPI.Example.Resources.CallMeeting.png");
private static readonly Assembly _assembly = typeof(ExampleAssets).Assembly;
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png", _assembly);
public static LoadableResourceAsset CallMeetingButton { get; } = new("MiraAPI.Example.Resources.CallMeeting.png", _assembly);
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png");
public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg");
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png", _assembly);
public static LoadableResourceAsset SharpshootButton { get; } = new("MiraAPI.Example.Resources.SharpshootButton.png", _assembly);
public static LoadableResourceAsset Banner { get; } = new("MiraAPI.Example.Resources.FortniteBanner.jpeg", _assembly);
}
+2 -2
View File
@@ -31,7 +31,7 @@ public static class ExampleEventHandlers
MiraEventManager.RegisterEventHandler<CompleteTaskEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}");
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType}");
});
// You can also unregister event handlers using the handle returned from RegisterEventHandler.
@@ -89,7 +89,7 @@ public static class ExampleEventHandlers
[RegisterEvent]
public static void UpdateSystemEventHandler(UpdateSystemEvent @event)
{
Logger<ExamplePlugin>.Error(@event.SystemType.ToString());
Logger<ExamplePlugin>.Error(@event.SystemType);
}
// Example event handler
+1 -1
View File
@@ -19,7 +19,7 @@ public partial class ExamplePlugin : BasePlugin, IMiraPlugin
{
public ExamplePlugin()
{
MiraLocaleManager.Register("mira.example");
MiraLocaleManager.Register("mira.example", typeof(ExamplePlugin).Assembly);
}
public Harmony Harmony { get; } = new(Id);
+2 -6
View File
@@ -30,16 +30,12 @@ public class ExampleOptions : AbstractOptionGroup
[ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Distance)]
public int NumberOpt2 { get; set; } = 4; // Can also be int!
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(Api), ["Mira API", "Mitochondria", "Reactor"])]
public Api BestApi { get; set; } = Api.MiraAPI;
[ModdedEnumOption<Api>("options.exampleOptions1.worstApi", ["Mira API", "Mitochondria", "Reactor"])]
// These three can target both instance and static methods
[ModdedOptionGetter(nameof(GetApi))] // Can also automatically resolve to {Option Name}Getter
[ModdedOptionSetter(nameof(SetApi))] // Can also automatically resolve to {Option Name}Setter
[ModdedOptionChanged(nameof(ApiChanged))] // Can also automatically resolve to {Option Name}Changed
public Api WorstApi { get; set; } = Api.MiraAPI;
[ModdedEnumOption<Api>("options.exampleOptions1.bestApi", ["Mira API", "Mitochondria", "Reactor"])]
public Api BestApi { get; set; } = Api.MiraAPI;
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
public ModdedArray<ValueState> ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
@@ -47,7 +47,6 @@ Eliminate every other faction.</string>
<string name="options.exampleOptions1.toggleOpt2">Toggle Opt 2</string>
<string name="options.exampleOptions1.numberOpt">Number Opt</string>
<string name="options.exampleOptions1.bestApi">Best API</string>
<string name="options.exampleOptions1.worstApi">Worst API</string>
<string name="options.exampleOptions1.players">Choose A Player</string>
<string name="options.exampleOptions2">Example Options 2</string>
<string name="options.exampleOptions2.youMustChoseAPlayer">YOU MUST CHOSE A PLAYER</string>
+2 -2
View File
@@ -58,13 +58,13 @@ public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColo
/// <inheritdoc cref="CustomColor"/>
public CustomColor(StringNames name, Color32 mainColor)
: this(name, mainColor, mainColor.GetShadowColor(60))
: this(name, mainColor, mainColor.DarkenAbsolute(60))
{
}
/// <inheritdoc cref="CustomColor"/>
public CustomColor(string name, Color32 mainColor)
: this(name, mainColor, mainColor.GetShadowColor(60))
: this(name, mainColor, mainColor.DarkenAbsolute(60))
{
}
+2 -2
View File
@@ -16,7 +16,7 @@ namespace MiraAPI.Colors;
/// <param name="shadowColor">The static shadow color used when dynamic colors are disabled.</param>
/// <param name="defaultTime">The default time that is used to automatically fill in the static colors when the latter is not provided.</param>
public sealed class VariableColor(StringNames name, Func<float, Color> mainEvaluator, Func<float, Color>? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f)
: CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60))
: CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).DarkenAbsolute(60))
{
/// <summary>
/// Gets or sets a delegate that takes in a time value and returns the dynamic main color.
@@ -26,7 +26,7 @@ public sealed class VariableColor(StringNames name, Func<float, Color> mainEvalu
/// <summary>
/// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color.
/// </summary>
public Func<float, Color> ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f));
public Func<float, Color> ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).DarkenAbsolute(0.24f));
/// <inheritdoc cref="VariableColor"/>
public VariableColor(StringNames name, Func<float, Color> mainEvaluator, Color32? mainColor = null, float defaultTime = 0f)
+2 -2
View File
@@ -35,7 +35,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
Error($"Error invoking event handler for {typeof(T).Name}: {ex}");
}
}
@@ -65,7 +65,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
Error($"Error invoking event handler for {type.Name}: {ex}");
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ public abstract class AbstractOptionGroup
/// Gets a value indicating whether the group should be shown in the modifiers menu.
/// </summary>
// Completed: make this not a boolean
[Obsolete("Use ParentMenu instead.", true)]
[Obsolete("Use ParentMenu instead. This will be removed in a future version.", true)]
[SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Retained.")]
public virtual bool ShowInModifiersMenu => false;
@@ -0,0 +1,20 @@
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Provides unboxed access to option values to prevent memory allocations.
/// </summary>
/// <typeparam name="T">The type of the value that the option carries.</typeparam>
public interface ITypedOption<T>
{
/// <summary>
/// Gets the unboxed value of the option.
/// </summary>
/// <returns>The unboxed option value.</returns>
T GetValueUnboxed();
/// <summary>
/// Sets the unboxed value of the option.
/// </summary>
/// <param name="value">The new value to set.</param>
void SetValueUnboxed(T value);
}
@@ -11,6 +11,7 @@ namespace MiraAPI.GameOptions.Attributes;
/// <param name="enumType">The <see cref="Enum"/> type.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
[Obsolete("Use ModdedEnumOptionAttribute<T> instead. This will be removed in a future version.")]
public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionAttribute(title)
{
@@ -32,7 +33,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
{
return HolderOption is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedEnumOption with enum type {enumType.FullName}");
}
}
@@ -44,7 +45,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
: ModdedOptionAttribute(title)
: ModdedOptionAttribute(title), ITypedOption<T>
where T : struct, Enum
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
@@ -56,15 +57,26 @@ public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedEnumOption<T>;
opt?.SetValue((T)value);
SetValueUnboxed((T)value);
}
/// <inheritdoc />
public void SetValueUnboxed(T value)
{
(HolderOption as ModdedEnumOption<T>)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue()
{
return GetValueUnboxed();
}
/// <inheritdoc />
public T GetValueUnboxed()
{
return HolderOption is ModdedEnumOption<T> opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).Name}>");
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedEnumOption<{typeof(T).Name}>");
}
}
@@ -28,7 +28,7 @@ public class ModdedNumberOptionAttribute(
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool halfIncrements = false)
: ModdedOptionAttribute(title)
: ModdedOptionAttribute(title), ITypedOption<int>, ITypedOption<float>
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionAttribute"/> class.
@@ -88,6 +88,18 @@ public class ModdedNumberOptionAttribute(
opt.SetValue(final);
}
/// <inheritdoc />
public void SetValueUnboxed(float value)
{
(HolderOption as ModdedNumberOption)?.SetValue(value);
}
/// <inheritdoc />
void ITypedOption<int>.SetValueUnboxed(int value)
{
(HolderOption as ModdedNumberOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue()
{
@@ -97,8 +109,22 @@ public class ModdedNumberOptionAttribute(
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return (int)MathF.Round(value);
return value;
}
/// <inheritdoc />
public float GetValueUnboxed()
{
return HolderOption is ModdedNumberOption opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
}
/// <inheritdoc />
int ITypedOption<int>.GetValueUnboxed()
{
return (int)Math.Round(GetValueUnboxed());
}
}
@@ -13,7 +13,7 @@ namespace MiraAPI.GameOptions.Attributes;
public class ModdedPlayerOptionAttribute(
string title,
bool allowNone = true)
: ModdedOptionAttribute(title)
: ModdedOptionAttribute(title), ITypedOption<int>, ITypedOption<NetworkedPlayerInfo?>
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
@@ -33,6 +33,22 @@ public class ModdedPlayerOptionAttribute(
return opt.SelectedPlayer!;
}
/// <inheritdoc />
public NetworkedPlayerInfo? GetValueUnboxed()
{
return HolderOption is ModdedPlayerOption opt
? opt.SelectedPlayer
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption.");
}
/// <inheritdoc />
int ITypedOption<int>.GetValueUnboxed()
{
return HolderOption is ModdedPlayerOption opt
? opt.Value
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption.");
}
/// <inheritdoc />
public override void SetValue(object value)
{
@@ -41,9 +57,32 @@ public class ModdedPlayerOptionAttribute(
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : -1,
_ => (int)value,
};
if (final == -1 && !allowNone)
throw new ArgumentException("Cannot assign -1 or null to a ModdedPlayerOption that does not allow none as an option.");
opt?.SetValue(final);
}
/// <inheritdoc />
public void SetValueUnboxed(NetworkedPlayerInfo? value)
{
if (!value && !allowNone)
throw new ArgumentException("Cannot assign null to a ModdedPlayerOption that does not allow none as an option.");
if (HolderOption is ModdedPlayerOption opt)
opt.SetValue(value ? opt.Values.IndexOf(value!) : -1);
}
/// <inheritdoc />
void ITypedOption<int>.SetValueUnboxed(int value)
{
if (value == -1 && !allowNone)
throw new ArgumentException("Cannot assign -1 to a ModdedPlayerOption that does not allow none as an option.");
(HolderOption as ModdedPlayerOption)?.SetValue(value);
}
}
@@ -13,7 +13,7 @@ namespace MiraAPI.GameOptions.Attributes;
public class ModdedStringOptionAttribute(
string title,
string[] values)
: ModdedOptionAttribute(title)
: ModdedOptionAttribute(title), ITypedOption<string>
{
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
{
@@ -23,6 +23,12 @@ public class ModdedStringOptionAttribute(
/// <inheritdoc />
public override object GetValue()
{
return GetValueUnboxed();
}
/// <inheritdoc />
public string GetValueUnboxed()
{
return HolderOption is ModdedStringOption opt
? opt.Value
@@ -32,7 +38,12 @@ public class ModdedStringOptionAttribute(
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedStringOption;
opt?.SetValue((string)value);
SetValueUnboxed((string)value);
}
/// <inheritdoc />
public void SetValueUnboxed(string value)
{
(HolderOption as ModdedStringOption)?.SetValue(value);
}
}
@@ -15,7 +15,7 @@ public class ModdedToggleOptionAttribute(
string title,
string on,
string off)
: ModdedOptionAttribute(title)
: ModdedOptionAttribute(title), ITypedOption<bool>
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionAttribute"/> class.
@@ -35,12 +35,23 @@ public class ModdedToggleOptionAttribute(
/// <inheritdoc />
public override void SetValue(object value)
{
var opt = HolderOption as ModdedToggleOption;
opt?.SetValue((bool)value);
SetValueUnboxed((bool)value);
}
/// <inheritdoc />
public void SetValueUnboxed(bool value)
{
(HolderOption as ModdedToggleOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue()
{
return GetValueUnboxed();
}
/// <inheritdoc />
public bool GetValueUnboxed()
{
return HolderOption is ModdedToggleOption opt
? opt.Value
@@ -0,0 +1,22 @@
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// Provides unboxed access to option list values to prevent memory allocations.
/// </summary>
/// <typeparam name="T">The type of the value that the option list carries.</typeparam>
public interface ITypedOptionList<T>
{
/// <summary>
/// Gets the unboxed value of the option at the specified index.
/// </summary>
/// <param name="idx">The index of the option.</param>
/// <returns>The option at the provided index.</returns>
T GetValueUnboxed(int idx);
/// <summary>
/// Sets the unboxed value of the option at the specified index.
/// </summary>
/// <param name="idx">The index of the option.</param>
/// <param name="value">The new value of the option to set.</param>
void SetValueUnboxed(int idx, T value);
}
@@ -12,6 +12,7 @@ namespace MiraAPI.GameOptions.Attributes;
/// <param name="enumType">The <see cref="Enum"/> type.</param>
/// <param name="values">An option list of string values to use in place of the <see langword="enum"/> name.</param>
[AttributeUsage(AttributeTargets.Property)]
[Obsolete("Use ModdedEnumOptionListAttribute<T> instead. This will be removed in a future version.")]
public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionListAttribute(title)
{
@@ -34,7 +35,7 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
{
return HolderOptionList?[idx] is ModdedEnumOption opt
? Enum.ToObject(enumType, opt.Value)
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedEnumOption with enum type {enumType.FullName}");
}
}
@@ -46,7 +47,7 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
/// <param name="values">The option's possible values.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null)
: ModdedOptionListAttribute(title)
: ModdedOptionListAttribute(title), ITypedOptionList<T>
where T : struct, Enum
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
@@ -59,15 +60,26 @@ public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = n
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedEnumOption<T>;
opt?.SetValue((T)value);
SetValueUnboxed(idx, (T)value);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, T value)
{
(HolderOptionList?[idx] as ModdedEnumOption<T>)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return GetValueUnboxed(idx);
}
/// <inheritdoc />
public T GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedEnumOption<T> opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).FullName}>");
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedEnumOption<{typeof(T).FullName}>");
}
}
@@ -29,7 +29,7 @@ public class ModdedNumberOptionListAttribute(
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool halfIncrements = false)
: ModdedOptionListAttribute(title)
: ModdedOptionListAttribute(title), ITypedOptionList<int>, ITypedOptionList<float>
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOptionListAttribute"/> class.
@@ -92,6 +92,18 @@ public class ModdedNumberOptionListAttribute(
opt.SetValue(final);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, float value)
{
(HolderOptionList?[idx] as ModdedNumberOption)?.SetValue(value);
}
/// <inheritdoc />
void ITypedOptionList<int>.SetValueUnboxed(int idx, int value)
{
(HolderOptionList?[idx] as ModdedNumberOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
@@ -101,8 +113,22 @@ public class ModdedNumberOptionListAttribute(
var value = opt.Value;
if (ValueType == typeof(int))
return (int)Math.Round(value);
return (int)MathF.Round(value);
return value;
}
/// <inheritdoc />
public float GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedNumberOption opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
}
/// <inheritdoc />
int ITypedOptionList<int>.GetValueUnboxed(int idx)
{
return (int)Math.Round(GetValueUnboxed(idx));
}
}
@@ -1,9 +1,8 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using MiraAPI.Translation;
namespace MiraAPI.GameOptions.Attributes;
@@ -75,10 +74,9 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
/// </summary>
/// <param name="index">The option's index.</param>
/// <returns>The formatted option's title.</returns>
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")]
protected string GetFormattedTitle(int index)
{
var match = Regex.Match(Title.Translate(), @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
var match = Regex.Match(Title, @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
if (!match.Success)
{
@@ -87,7 +85,7 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
if (match.Value == "{0}" && !match.NextMatch().Success)
{
return string.Format(Title, index);
return string.Format(CultureInfo.InvariantCulture, Title, index);
}
Error($"ModdedOptionList Title of {Title} cannot contain more than the first parameter for formatting.");
@@ -14,7 +14,7 @@ namespace MiraAPI.GameOptions.Attributes;
public class ModdedPlayerOptionListAttribute(
string title,
bool allowNone = true)
: ModdedOptionListAttribute(title)
: ModdedOptionListAttribute(title), ITypedOptionList<int>, ITypedOptionList<NetworkedPlayerInfo?>
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
@@ -32,12 +32,35 @@ public class ModdedPlayerOptionListAttribute(
var final = value switch
{
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : -1,
_ => (int)value,
};
if (final == -1 && !allowNone)
throw new ArgumentException("Cannot assign -1 or null to a ModdedPlayerOption that does not allow none as an option.");
opt?.SetValue(final);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, NetworkedPlayerInfo? value)
{
if (!value && !allowNone)
throw new ArgumentException("Cannot assign null to a ModdedPlayerOption that does not allow none as an option.");
if (HolderOptionList?[idx] is ModdedPlayerOption opt)
opt.SetValue(value ? opt.Values.IndexOf(value!) : -1);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, int value)
{
if (value == -1 && !allowNone)
throw new ArgumentException("Cannot assign -1 to a ModdedPlayerOption that does not allow none as an option.");
(HolderOptionList?[idx] as ModdedPlayerOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
@@ -49,4 +72,20 @@ public class ModdedPlayerOptionListAttribute(
return opt.SelectedPlayer!;
}
/// <inheritdoc />
public NetworkedPlayerInfo? GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedPlayerOption opt
? opt.SelectedPlayer
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption");
}
/// <inheritdoc />
int ITypedOptionList<int>.GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedPlayerOption opt
? opt.Value
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption");
}
}
@@ -15,7 +15,7 @@ namespace MiraAPI.GameOptions.Attributes;
public class ModdedStringOptionListAttribute(
string title,
string[] values)
: ModdedOptionListAttribute(title)
: ModdedOptionListAttribute(title), ITypedOptionList<string>
{
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
{
@@ -28,12 +28,23 @@ public class ModdedStringOptionListAttribute(
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedStringOption;
opt?.SetValue((string)value);
SetValueUnboxed(idx, (string)value);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, string value)
{
(HolderOptionList?[idx] as ModdedStringOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return GetValueUnboxed(idx);
}
/// <inheritdoc />
public string GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedStringOption opt
? opt.Value
@@ -16,7 +16,7 @@ public class ModdedToggleOptionListAttribute(
string title,
string on,
string off)
: ModdedOptionListAttribute(title)
: ModdedOptionListAttribute(title), ITypedOptionList<bool>
{
/// <summary>
/// Initializes a new instance of the <see cref="ModdedToggleOptionListAttribute"/> class.
@@ -37,12 +37,23 @@ public class ModdedToggleOptionListAttribute(
/// <inheritdoc />
public override void SetValue(int idx, object value)
{
var opt = HolderOptionList?[idx] as ModdedToggleOption;
opt?.SetValue((bool)value);
SetValueUnboxed(idx, (bool)value);
}
/// <inheritdoc />
public void SetValueUnboxed(int idx, bool value)
{
(HolderOptionList?[idx] as ModdedToggleOption)?.SetValue(value);
}
/// <inheritdoc />
public override object GetValue(int idx)
{
return GetValueUnboxed(idx);
}
/// <inheritdoc />
public bool GetValueUnboxed(int idx)
{
return HolderOptionList?[idx] is ModdedToggleOption opt
? opt.Value
+11
View File
@@ -77,6 +77,11 @@ public interface IModdedOption
/// </summary>
bool IncludeInPreset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should show a notification, toggled on by an overall option synchronization.
/// </summary>
public bool ShowNotification { get; set; }
/// <summary>
/// Gets or sets the <see cref="BepInEx.Configuration.ConfigDefinition"/> for the option, used for BepInEx configuration.
/// </summary>
@@ -93,6 +98,12 @@ public interface IModdedOption
/// <returns>The created <see cref="global::OptionBehaviour"/> object.</returns>
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
/// <summary>
/// Gets the translated display value of the toggle option.
/// </summary>
/// <returns>The display value.</returns>
string GetDisplayValue();
/// <summary>
/// Gets the value as a <see langword="float"/>.
/// </summary>
+29 -9
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.GameOptions;
@@ -72,15 +73,34 @@ public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
/// <param name="index">The index of the value.</param>
public T this[int index]
{
get => ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute)
? (T)attribute.GetValue(index)
: items[index];
get
{
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
{
items[index] = attribute is ITypedOptionList<T> typedAttr
? typedAttr.GetValueUnboxed(index)
: (T)attribute.GetValue(index);
}
return items[index];
}
set
{
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
attribute.SetValue(index, value!);
{
if (attribute is ITypedOptionList<T> typedAttr)
typedAttr.SetValueUnboxed(index, value);
else
attribute.SetValue(index, value!);
items[index] = attribute is ITypedOptionList<T> typedAttr2
? typedAttr2.GetValueUnboxed(index)
: (T)attribute.GetValue(index);
}
else
{
items[index] = value;
}
version++;
}
@@ -178,31 +198,31 @@ public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
}
/// <inheritdoc/>
public void Insert(int index, T item)
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void RemoveAt(int index)
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Add(T item)
void ICollection<T>.Add(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public void Clear()
void ICollection<T>.Clear()
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
/// <inheritdoc/>
public bool Remove(T item)
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException("ModdedArray is of a fixed size.");
}
+90 -11
View File
@@ -30,8 +30,14 @@ public static class ModdedOptionsManager
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
private static readonly HashSet<PropertyInfo> OptionAttributes = [];
private static readonly HarmonyMethod CachedGetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertyGetterPatch));
private static readonly HarmonyMethod CachedSetterPatch = new(typeof(ModdedOptionsManager), nameof(PropertySetterPatch));
private static readonly MethodInfo OpenGetterPatch = typeof(ModdedOptionsManager)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == nameof(PropertyGetterPatch) && m.IsGenericMethodDefinition);
private static readonly MethodInfo OpenSetterPatch = typeof(ModdedOptionsManager)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == nameof(PropertySetterPatch) && m.IsGenericMethodDefinition);
private static readonly Dictionary<Type, HarmonyMethod> ConcretisedGetters = [];
private static readonly Dictionary<Type, HarmonyMethod> ConcretisedSetters = [];
internal static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
@@ -170,20 +176,34 @@ public static class ModdedOptionsManager
return;
}
var propType = property.PropertyType;
if (!ConcretisedGetters.TryGetValue(propType, out var concreteGetter))
{
concreteGetter = new(OpenGetterPatch.MakeGenericMethod(propType));
ConcretisedGetters[propType] = concreteGetter;
}
var getterOriginal = property.GetGetMethod(true)!;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: concreteGetter);
GetterLookups[getterOriginal] = attribute;
var setterOriginal = property.GetSetMethod(true);
if (setterOriginal != null)
{
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
if (!ConcretisedSetters.TryGetValue(propType, out var concreteSetter))
{
concreteSetter = new(OpenSetterPatch.MakeGenericMethod(propType));
ConcretisedSetters[propType] = concreteSetter;
}
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: concreteSetter);
SetterLookups[setterOriginal] = attribute;
}
attribute.HolderOption = option;
attribute.ValueType = property.PropertyType;
attribute.ValueType = propType;
var customAttributes = property.GetCustomAttributes(false);
@@ -281,15 +301,27 @@ public static class ModdedOptionsManager
return;
}
if (!ConcretisedGetters.TryGetValue(listType, out var concreteGetter))
{
concreteGetter = new(OpenGetterPatch.MakeGenericMethod(listType));
ConcretisedGetters[listType] = concreteGetter;
}
var getterOriginal = property.GetGetMethod(true)!;
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: concreteGetter);
GetterLookups[getterOriginal] = attribute;
var setterOriginal = property.GetSetMethod(true);
if (setterOriginal != null)
{
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
if (!ConcretisedSetters.TryGetValue(listType, out var concreteSetter))
{
concreteSetter = new(OpenSetterPatch.MakeGenericMethod(listType));
ConcretisedSetters[listType] = concreteSetter;
}
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: concreteSetter);
SetterLookups[setterOriginal] = attribute;
}
@@ -327,7 +359,7 @@ public static class ModdedOptionsManager
var option = optionList[i];
if (visibilityFactory != null)
option.Visible = (Func<bool>)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue);
option.Visible = visibilityFactory(i) as Func<bool> ?? DelegateUtilities.AlwaysTrue;
if (getterFactory != null)
option.Getter = getterFactory(i);
@@ -365,11 +397,11 @@ public static class ModdedOptionsManager
while (chunks.Count > 0)
{
Rpc<SyncOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, chunks.Dequeue());
Rpc<SyncOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, new MiraOptionNetData(false, chunks.Dequeue()));
}
}
internal static void HandleSyncOptions(NetData[] data)
internal static void HandleSyncOptions(bool showNotifs, NetData[] data)
{
// necessary to disable then re-enable this setting
// we don't know how other plugins handle their configs
@@ -388,6 +420,7 @@ public static class ModdedOptionsManager
continue;
}
option.ShowNotification = showNotifs;
option.HandleNetData(netData.Data);
}
@@ -397,7 +430,7 @@ public static class ModdedOptionsManager
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
}
if (LobbyInfoPane.Instance)
if (LobbyInfoPane.InstanceExists)
{
LobbyInfoPane.Instance.RefreshPane();
}
@@ -410,6 +443,7 @@ public static class ModdedOptionsManager
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
[Obsolete("Use PropertySetterPatch<T> instead. This will be removed in a future version.")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
if (!SetterLookups.TryGetValue(__originalMethod, out var attribute))
@@ -421,6 +455,28 @@ public static class ModdedOptionsManager
ListInstanceCache.AddOrUpdate(value, listAttribute);
}
/// <summary>
/// Patches the setter of a property to update the value of the option.
/// </summary>
/// <typeparam name="T">The type of the option value.</typeparam>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention.")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static void PropertySetterPatch<T>(MethodBase __originalMethod, T value)
{
if (!SetterLookups.TryGetValue(__originalMethod, out var attribute))
return;
if (attribute is ITypedOption<T> typedAttribute)
typedAttribute.SetValueUnboxed(value);
else
attribute.SetValue(value!);
if (attribute is ModdedOptionListAttribute listAttribute && value is not null)
ListInstanceCache.AddOrUpdate(value, listAttribute);
}
/// <summary>
/// Patches the getter of a property to return the value of the option.
/// </summary>
@@ -429,6 +485,7 @@ public static class ModdedOptionsManager
/// <returns><see langword="false"/> so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
[Obsolete("Use PropertyGetterPatch<T> instead. This will be removed in a future version.")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
if (!GetterLookups.TryGetValue(__originalMethod, out var attribute))
@@ -437,4 +494,26 @@ public static class ModdedOptionsManager
__result = attribute.GetValue();
return false;
}
/// <summary>
/// Patches the getter of a property to return the value of the option.
/// </summary>
/// <typeparam name="T">The type of the option value.</typeparam>
/// <param name="__originalMethod">The original getter method.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns><see langword="false"/> so the original getter gets skipped.</returns>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
public static bool PropertyGetterPatch<T>(MethodBase __originalMethod, ref T __result)
{
if (!GetterLookups.TryGetValue(__originalMethod, out var attribute))
return true;
if (attribute is ITypedOption<T> typedAttribute)
__result = typedAttribute.GetValueUnboxed();
else
__result = (T)attribute.GetValue();
return false;
}
}
@@ -13,6 +13,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// An option for selecting an <see langword="enum"/> value.
/// </summary>
[Obsolete("Use ModdedEnumOption<T> instead. This will be removed in a future version.")]
public class ModdedEnumOption : ModdedOption<int>
{
/// <summary>
@@ -93,19 +94,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is StringOption opt)
if (OptionBehaviour && OptionBehaviour is StringOption opt)
{
opt.Value = newValue;
}
@@ -196,19 +185,7 @@ public class ModdedEnumOption<T> : ModdedOption<T>
/// <inheritdoc />
protected override void OnValueChanged(T newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is StringOption opt)
if (OptionBehaviour && OptionBehaviour is StringOption opt)
{
opt.Value = Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo);
}
@@ -1,4 +1,5 @@
using System;
using System.Text.RegularExpressions;
using AmongUs.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Utilities;
@@ -52,6 +53,8 @@ public class ModdedNumberOption : ModdedOption<float>
/// </summary>
public bool ShiftIncrement { get; }
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOption"/> class.
/// </summary>
@@ -155,7 +158,7 @@ public class ModdedNumberOption : ModdedOption<float>
optionComponent.ZeroValue = ZeroWordValue;
numberOption.SetUpFromData(Data, 20);
numberOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
numberOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
numberOption.Title = StringName;
numberOption.Value = Value;
@@ -171,6 +174,22 @@ public class ModdedNumberOption : ModdedOption<float>
return numberOption;
}
/// <inheritdoc />
public override string GetDisplayValue()
{
if (this is { NegativeWordValue: not "#", Value: -1 })
{
return NegativeWordValue;
}
if (this is { ZeroWordValue: not "#", Value: 0 })
{
return ZeroWordValue;
}
return TrailingZeroRegex.Replace(Data.GetValueString(Value), string.Empty);
}
/// <inheritdoc />
public override float GetFloatData()
{
@@ -198,21 +217,18 @@ public class ModdedNumberOption : ModdedOption<float>
/// <inheritdoc />
protected override void OnValueChanged(float newValue)
{
Value = Mathf.Clamp(newValue, Min, Max);
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (OptionBehaviour is NumberOption opt)
if (OptionBehaviour && OptionBehaviour is NumberOption opt)
{
opt.Value = Value;
}
}
/// <inheritdoc />
protected override float ModifyValue(float newValue, float oldValue)
{
return Math.Clamp(newValue, Min, Max);
}
/// <summary>
/// Implicitly converts the option to an <see langword="int"/>.
/// </summary>
@@ -220,6 +236,6 @@ public class ModdedNumberOption : ModdedOption<float>
/// <returns>Integer value.</returns>
public static implicit operator int(ModdedNumberOption option)
{
return (int)option.Value;
return (int)Math.Round(option.Value);
}
}
@@ -50,6 +50,8 @@ public abstract class ModdedOption<T> : IModdedOption
get => Getter != null ? Getter(_value) : _value;
protected set
{
value = ModifyValue(value, _value);
if (Setter != null)
value = Setter(value, _value);
@@ -79,6 +81,9 @@ public abstract class ModdedOption<T> : IModdedOption
/// <inheritdoc />
public bool IncludeInPreset { get; set; }
/// <inheritdoc />
public bool ShowNotification { get; set; } = true;
/// <inheritdoc />
public OptionBehaviour? OptionBehaviour { get; protected set; }
@@ -156,10 +161,22 @@ public abstract class ModdedOption<T> : IModdedOption
entry.Value = _value;
}
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, new MiraOptionNetData(true, [GetNetData()]), true);
}
if (ShowNotification)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
GetDisplayValue(),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
}
OnValueChanged(_value);
ShowNotification = true;
}
/// <inheritdoc />
@@ -209,12 +226,29 @@ public abstract class ModdedOption<T> : IModdedOption
/// <inheritdoc />
public abstract void HandleNetData(byte[] data);
/// <inheritdoc />
public virtual string GetDisplayValue()
{
return Data.GetValueString(GetFloatData());
}
/// <summary>
/// Handles the value changed event.
/// </summary>
/// <param name="newValue">The new value.</param>
protected abstract void OnValueChanged(T newValue);
/// <summary>
/// Modifies the incoming value with respect to the previous value. This is invoked before the <see cref="ModdedOption{T}.Setter"/> (if it's set).
/// </summary>
/// <param name="newValue">The new value.</param>
/// <param name="oldValue">The old value.</param>
/// <returns>The modified value.</returns>
protected virtual T ModifyValue(T newValue, T oldValue)
{
return newValue;
}
/// <summary>
/// Gets the value from the option behaviour.
/// </summary>
@@ -138,19 +138,7 @@ public class ModdedPlayerOption : ModdedOption<int>
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is PlayerOption opt)
if (OptionBehaviour && OptionBehaviour is PlayerOption opt)
{
opt.Value = newValue;
}
@@ -168,13 +168,7 @@ public class ModdedStringOption : ModdedOption<string>
/// <inheritdoc />
protected override void OnValueChanged(string newValue)
{
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(GetIndex(newValue)), false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is StringOption opt)
if (OptionBehaviour && OptionBehaviour is StringOption opt)
{
opt.Value = GetIndex(newValue);
}
@@ -72,6 +72,18 @@ public class ModdedToggleOption : ModdedOption<bool>
return toggleOption;
}
/// <inheritdoc />
public override string GetDisplayValue()
{
return Value
? (OnValue == "On"
? TranslationController.Instance.GetString(StringNames.SettingsOn)
: OnValue.Translate())
: (OffValue == "Off"
? TranslationController.Instance.GetString(StringNames.SettingsOff)
: OffValue.Translate());
}
/// <inheritdoc />
public override float GetFloatData()
{
@@ -99,19 +111,7 @@ public class ModdedToggleOption : ModdedOption<bool>
/// <inheritdoc />
protected override void OnValueChanged(bool newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
(newValue ? OnValue : OffValue).Translate(),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is ToggleOption toggleOpt)
if (OptionBehaviour && OptionBehaviour is ToggleOption toggleOpt)
{
toggleOpt.CheckMark.enabled = newValue;
}
+1 -1
View File
@@ -102,7 +102,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
PluginManager = new MiraPluginManager();
PluginManager.Initialize(this, this);
MiraLocaleManager.Register("mira.api");
MiraLocaleManager.Register("mira.api", typeof(MiraApiPlugin).Assembly);
LocalizationManager.Register(new MiraLocalizationProvider());
IL2CPPChainloader.Instance.Finished +=
+43
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
@@ -96,6 +97,48 @@ public abstract class BaseModifier : IOptionable
/// </summary>
public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier");
/// <summary>
/// Gets a value indicating whether to exclude the "amount" from the displayed amount text within the role guide.
/// </summary>
public virtual bool ShowModifierAsUnique => false;
/// <summary>
/// Gets the displayed amount and chance of the modifier. Used in the role guide.
/// By default, 0 amount or chance results in a complete 0 amount and chance, so override if necessary.
/// </summary>
/// <param name="amount">The amount of the modifier.</param>
/// <param name="chance">The chance of the modifier.</param>
/// <param name="source">The mod source of the modifier.</param>
/// <returns>The <see cref="string"/> result.</returns>
public virtual string GetAmountText(int amount, int chance, string source)
{
if (amount == 0)
{
chance = 0;
}
if (ShowModifierAsUnique)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0}% ({1})",
chance.ToString(CultureInfo.InvariantCulture),
source);
}
if (chance == 0)
{
amount = 0;
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
chance.ToString(CultureInfo.InvariantCulture),
source);
}
/// <summary>
/// Gets whether the modifier is forcibly shown or disabled in the wiki screen.
/// </summary>
+4 -4
View File
@@ -77,7 +77,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while deactivating modifier {modifier.ModifierName}: {e}");
}
Modifiers.Remove(modifier);
@@ -95,7 +95,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while activating modifier {modifier.ModifierName}: {e}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
@@ -122,7 +122,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e}");
}
}
@@ -153,7 +153,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while updating modifier {modifier.ModifierName}: {e}");
}
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ public abstract class TimedModifier : BaseModifier
/// <returns>A string with the time remaining.</returns>
public override string GetDescription()
{
return $"Remaining Time: {Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s";
return $"Remaining Time: {MathF.Round(Duration - TimeRemaining, 0)}s/{Duration}s";
}
/// <summary>
+2 -2
View File
@@ -571,7 +571,7 @@ public static class CustomMurderRpc
}
catch (Exception e)
{
Error($"Error with kill animation: {e.ToString()}");
Error($"Error with kill animation: {e}");
}
}
@@ -705,7 +705,7 @@ public static class CustomMurderRpc
}
catch (Exception e)
{
Error($"Error with kill animation: {e.ToString()}");
Error($"Error with kill animation: {e}");
}
}
+13 -10
View File
@@ -9,29 +9,32 @@ namespace MiraAPI.Networking;
[RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)]
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.)
// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.)
internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, MiraOptionNetData>(plugin, id)
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[]? data)
public override void Write(MessageWriter writer, MiraOptionNetData data)
{
if (data == null)
/*if (data == null)
{
writer.WritePacked(0U);
return;
}
}*/
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
var info = data.Data;
writer.Write(data.ShouldShowNotifications);
writer.WritePacked((uint)info.Length);
foreach (var netData in info)
{
writer.WritePacked(netData.Id);
writer.WriteBytesAndSize(netData.Data);
}
}
public override NetData[] Read(MessageReader reader)
public override MiraOptionNetData Read(MessageReader reader)
{
var shouldShowNotif = reader.ReadBoolean();
var length = reader.ReadPackedUInt32();
var data = new NetData[length];
for (var i = 0; i < length; i++)
@@ -41,16 +44,16 @@ internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCust
data[i] = new NetData(dataId, bytes);
}
return data;
return new MiraOptionNetData(shouldShowNotif, data);
}
public override void Handle(PlayerControl playerControl, NetData[]? data)
public override void Handle(PlayerControl playerControl, MiraOptionNetData data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
ModdedOptionsManager.HandleSyncOptions(data ?? []);
ModdedOptionsManager.HandleSyncOptions(data.ShouldShowNotifications, data.Data ?? []);
}
}
+19 -10
View File
@@ -5,32 +5,41 @@ using Reactor.Networking.Rpc;
namespace MiraAPI.Networking;
internal readonly struct MiraOptionNetData(bool showNotif, NetData[] data)
{
public bool ShouldShowNotifications { get; } = showNotif;
public NetData[] Data { get; } = data;
}
[RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)]
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.)
// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.)
internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, MiraOptionNetData>(plugin, id)
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[]? data)
public override void Write(MessageWriter writer, MiraOptionNetData data)
{
if (data == null)
/*if (data == null)
{
writer.WritePacked(0U);
return;
}
}*/
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
var info = data.Data;
writer.Write(data.ShouldShowNotifications);
writer.WritePacked((uint)info.Length);
foreach (var netData in info)
{
writer.WritePacked(netData.Id);
writer.WriteBytesAndSize(netData.Data);
}
}
public override NetData[] Read(MessageReader reader)
public override MiraOptionNetData Read(MessageReader reader)
{
var shouldShowNotif = reader.ReadBoolean();
var length = reader.ReadPackedUInt32();
var data = new NetData[length];
for (var i = 0; i < length; i++)
@@ -40,16 +49,16 @@ internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : Player
data[i] = new NetData(dataId, bytes);
}
return data;
return new MiraOptionNetData(shouldShowNotif, data);
}
public override void Handle(PlayerControl playerControl, NetData[]? data)
public override void Handle(PlayerControl playerControl, MiraOptionNetData data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
CustomRoleManager.HandleSyncRoleOptions(data ?? []);
CustomRoleManager.HandleSyncRoleOptions(data.ShouldShowNotifications, data.Data ?? []);
}
}
@@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using AmongUs.Data;
using HarmonyLib;
@@ -42,7 +41,7 @@ internal static class PlayerTabPatch
}
[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))]
internal static class ChatNotifRainbowPatch
internal static class ChatNotifVariablePatch
{
private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout);
@@ -60,14 +59,13 @@ internal static class ChatNotifRainbowPatch
var str = color.ToHtmlStringRGBA();
var nameText = __instance.playerNameText;
nameText.text = "<color=#" + str + ">" + RichTags.Replace(nameText.text, string.Empty);
nameText.outlineColor = color.GetShadowColor(0.5f);
nameText.outlineColor = color.DarkenAbsolute(0.5f);
}
}
[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))]
internal static class RainbowLobbyInfoPanePatch
internal static class VariableLobbyInfoPanePatch
{
[SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")]
public static void Postfix(HostInfoPanel __instance)
{
if (!__instance.gameObject.activeInHierarchy)
+2 -1
View File
@@ -3,6 +3,7 @@ using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Usables;
using UnityEngine;
namespace MiraAPI.Patches.Events;
@@ -27,7 +28,7 @@ public static class CanUsePatches
[HarmonyPriority(Priority.Last)]
[HarmonyPrefix]
public static bool CanUsePatch(Il2CppSystem.Object __instance, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse)
public static bool CanUsePatch(MonoBehaviour __instance, [HarmonyArgument(1)] out bool canUse, [HarmonyArgument(2)] out bool couldUse)
{
canUse = couldUse = false;
+2 -1
View File
@@ -3,6 +3,7 @@ using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Usables;
using UnityEngine;
namespace MiraAPI.Patches.Events;
@@ -25,7 +26,7 @@ public static class UsePatches
[HarmonyPriority(Priority.Last)]
[HarmonyPrefix]
public static bool UsePatch(Il2CppSystem.Object __instance)
public static bool UsePatch(MonoBehaviour __instance)
{
var @event = new PlayerUseEvent(__instance.Cast<IUsable>());
MiraEventManager.InvokeEvent(@event);
+4 -2
View File
@@ -186,9 +186,11 @@ public static class TaskAdderPatches
systemTypes = SystemTypes.UpperEngine;
}
if (!folders.TryGetValue(systemTypes.ToString(), out var taskFolder3))
var systemType = systemTypes.ToString();
if (!folders.TryGetValue(systemType, out var taskFolder3))
{
taskFolder3 = folders[systemTypes.ToString()] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
taskFolder3 = folders[systemType] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
taskFolder3.SetFolderColor(TaskFolder.FolderColor.Tan);
taskFolder3.gameObject.SetActive(false);
taskFolder3.FolderName = systemTypes == SystemTypes.UpperEngine
@@ -1,5 +1,7 @@
using HarmonyLib;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
namespace MiraAPI.Patches.GameModes;
@@ -55,4 +57,17 @@ internal static class GameStartManagerPatches
__instance.MinPlayers = 1;
}
}
[HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.BeginGame))]
public static void Prefix(GameStartManager __instance)
{
if (!AmongUsClient.Instance.AmHost)
{
return;
}
ModdedOptionsManager.SyncAllOptions();
CustomRoleManager.SyncAllRoleSettings();
GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value);
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ public static class HudManagerPatches
public static IEnumerator CoResizeUI()
{
while (!HudManager.Instance) yield return null;
while (!HudManager.InstanceExists) yield return null;
yield return new WaitForSeconds(0.01f);
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
@@ -464,7 +464,7 @@ public static class LobbyViewPanePatches
if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader))
{
var veryDarkColor = group.Color.DarkenColor(.35f);
var veryDarkColor = group.Color.DarkenRelative(.35f);
categoryHeaderRoleVariant.Title.color = veryDarkColor;
categoryHeaderRoleVariant.Background.color = group.Color;
}
@@ -495,7 +495,7 @@ public static class RoleSettingMenuPatches
if (AmongUsClient.Instance.AmHost)
{
Rpc<SyncRoleOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [role.GetNetData()], true);
Rpc<SyncRoleOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, new MiraOptionNetData(true, [role.GetNetData()]), true);
}
GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;
+102 -22
View File
@@ -16,6 +16,7 @@ using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using MiraAPI.Utilities.Components;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -754,7 +755,8 @@ public static class RoleGuidePatches
modifier.ModifierCategoryTitle,
modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(),
null,
modifier));
modifier,
modifier.TypeId));
}
}
@@ -790,7 +792,8 @@ public static class RoleGuidePatches
roleBehaviour.GetCategoryTitle(),
modId,
roleBehaviour,
null));
null,
(uint)roleBehaviour.Role));
}
_roleEntriesMode = activeModeType;
@@ -928,7 +931,7 @@ public static class RoleGuidePatches
}
}
private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier)
private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier, uint typeId)
{
public MatchInfoRolePanel? Panel { get; set; }
public string Title { get; } = title;
@@ -936,6 +939,7 @@ public static class RoleGuidePatches
public string ModId { get; } = modId;
public RoleBehaviour? Role { get; } = role;
public GameModifier? Modifier { get; } = modifier;
public uint TypeId { get; } = typeId;
public int Amount { get; set; }
public int Chance { get; set; }
public float Likelihood { get; set; }
@@ -1157,7 +1161,9 @@ public static class RoleGuidePatches
entry.Visible = entry.CanShow && MatchesSearch(entry, searchText);
if (entry.Panel)
{
SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId);
dynamic entryType = entry.Modifier != null ? entry.Modifier : entry.Role!;
var text = GetPanelCountText(entryType, entry.Amount, entry.Chance, entry.ModId);
SetPanelCount(entry.Panel!, text);
if (!entry.Visible && entry.Panel!.gameObject.activeSelf)
{
entry.Panel.gameObject.SetActive(false);
@@ -1182,16 +1188,58 @@ public static class RoleGuidePatches
return shown;
}
public static IEnumerable<GameModifier> GetActiveModifiers()
{
return PlayerControl.LocalPlayer.GetModifiers<GameModifier>();
}
public static IEnumerable<RoleBehaviour> GetActiveRoles()
{
IEnumerable<RoleBehaviour> comparisons = [];
var curRole = PlayerControl.LocalPlayer.Data.Role;
comparisons = comparisons.AddItem(curRole);
if (PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && PlayerControl.LocalPlayer.Data.IsDead &&
!comparisons.Any(x => x.Role == PlayerControl.LocalPlayer.Data.RoleWhenAlive.Value))
{
comparisons = comparisons.AddItem(RoleManager.Instance.GetRole(PlayerControl.LocalPlayer.Data.RoleWhenAlive.Value));
}
return comparisons;
}
public static List<uint> GetActiveIds()
{
var activeModifiers = GetActiveModifiers();
List<uint> comparisons = activeModifiers.Select(x => x.TypeId).ToList();
comparisons.AddRange(GetActiveRoles().Select(x => (uint)x.Role));
return comparisons;
}
private static List<DetailedPanel> SortEntries(List<DetailedPanel> entries, string searchText)
{
var activeModifiers = PlayerControl.LocalPlayer.GetModifiers<GameModifier>().Select(x => x.TypeId);
var roles = GetActiveRoles().Select(x => (uint)x.Role).ToList();
List<uint> comparisons = activeModifiers.ToList();
comparisons.AddRange(roles);
var comparer = new BasicGuideComparer(comparisons);
var sorted = entries.Where(p => p.Visible)
.OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(p =>
p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase));
return [.. _sortMethod is SortingMethod.Alphabetical
p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.ThenBy(x => x.Modifier != null ? x.Modifier.TypeId : (uint)x.Role!.Role, comparer);
return
[
.. _sortMethod is SortingMethod.Alphabetical
? sorted.ThenBy(p => p.GetSortKey(_sortGrouping))
: sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))];
: sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))
];
}
private static bool MatchesSearch(DetailedPanel entry, string searchText)
@@ -1651,10 +1699,15 @@ public static class RoleGuidePatches
public static Sprite GetRandomRoleIcon()
{
var aliveRole = PlayerControl.LocalPlayer.Data?.Role;
if (aliveRole != null && aliveRole.RoleIconSolid != null)
var roles = GetActiveRoles().Shuffle().ToList();
foreach (var role in roles)
{
return aliveRole.RoleIconSolid;
if (role.RoleIconSolid == null)
{
continue;
}
return role.RoleIconSolid;
}
return GetRandomFactionIcon();
@@ -1793,21 +1846,22 @@ public static class RoleGuidePatches
return false;
}
__instance.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}%",
numPerGame.ToString(CultureInfo.InvariantCulture),
chancePerGame);
if (role is ICustomRole customRole)
{
__instance.roleName.text = customRole.RoleName;
__instance.roleDescription.text =
$"<size=60%>{role.GetCategoryTitle()}</size>\n" + customRole.RoleMedDescription;
__instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset();
__instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})";
var text = GetCustomRolePanelCountText(customRole, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(__instance, text);
}
else
{
__instance.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}%",
numPerGame.ToString(CultureInfo.InvariantCulture),
chancePerGame);
__instance.roleName.text = role.NiceName;
__instance.roleDescription.text = $"<size=60%>{role.GetCategoryTitle()}</size>\n" + role.BlurbMed;
__instance.roleIcon.sprite = role.RoleIconColor;
@@ -1823,9 +1877,20 @@ public static class RoleGuidePatches
return false;
}
private static void SetPanelCount(MatchInfoRolePanel panel, int amount, int chance, string modId)
// NOTE: This is specifically meant to be a different method name as to not have the game check this method and the GetPanelCountText method made for roles overall.
private static string GetCustomRolePanelCountText(ICustomRole custom, int amount, int chance, string modId)
{
panel.roleCount.text = string.Format(
return custom.GetAmountText(amount, chance, modId);
}
private static string GetPanelCountText(RoleBehaviour role, int amount, int chance, string modId)
{
if (role is ICustomRole custom)
{
return custom.GetAmountText(amount, chance, modId);
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
@@ -1833,6 +1898,18 @@ public static class RoleGuidePatches
modId);
}
private static string GetPanelCountText(BaseModifier modifier, int amount, int chance, string modId)
{
return modifier.GetAmountText(amount, chance, modId);
}
private static void SetPanelCount(MatchInfoRolePanel panel, string amountChanceText)
{
panel.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
amountChanceText);
}
public static void SetRolePanel(
this MatchInfoRolePanel instance,
RoleBehaviour role,
@@ -1845,14 +1922,16 @@ public static class RoleGuidePatches
instance.roleDescription.text =
$"<size=60%>{role.GetCategoryTitle()}</size>\n" + customRole.RoleMedDescription;
instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset();
SetPanelCount(instance, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
var text = GetCustomRolePanelCountText(customRole, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(instance, text);
}
else
{
instance.roleName.text = role.NiceName;
instance.roleDescription.text = $"<size=60%>{role.GetCategoryTitle()}</size>\n" + role.BlurbMed;
instance.roleIcon.sprite = role.RoleIconColor;
SetPanelCount(instance, numPerGame, chancePerGame, "AU");
var text = GetPanelCountText(role, numPerGame, chancePerGame, "AU");
SetPanelCount(instance, text);
}
instance.roleIcon.SetSizeLimit(0.13f);
@@ -1873,7 +1952,8 @@ public static class RoleGuidePatches
instance.roleDescription.text =
$"<size=60%>{modifier.ModifierCategoryTitle}</size>\n" + modifier.ModifierMedDescription;
instance.roleIcon.sprite = modifier.ModifierIcon?.LoadAsset();
SetPanelCount(instance, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName());
var text = GetPanelCountText(modifier, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(instance, text);
instance.roleIcon.SetSizeLimit(0.13f);
instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
@@ -2006,7 +2086,7 @@ public static class RoleGuidePatches
vanillaEntries = BuildVanillaSettingsEntries(guide);
}
_settingsPages = GuideOptionPages.BuildPages(guide.GetBoolString);
_settingsPages = GuideOptionPages.BuildPages();
if (_settingsPage > _settingsPages.Count)
{
_settingsPage = 0;
+33 -39
View File
@@ -25,6 +25,26 @@ public class MiraPluginInfo
PluginId = info.Metadata.GUID;
}
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
/// </summary>
public string PluginId { get; }
/// <summary>
/// Gets the plugin's instance as an <see cref="IMiraPlugin"/>.
/// </summary>
public IMiraPlugin MiraPlugin { get; }
/// <summary>
/// Gets the plugin's BepInEx metadata.
/// </summary>
public PluginInfo PluginInfo { get; }
/// <summary>
/// Gets the plugin's <see cref="ConfigFile"/>.
/// </summary>
public ConfigFile PluginConfig { get; }
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of this <see cref="IMiraPlugin"/>'s <see cref="BaseModifier"/>s. This is not safe for <see cref="BaseModifier"/>s with constructors.
/// </summary>
@@ -45,15 +65,6 @@ public class MiraPluginInfo
/// </summary>
public ReadOnlyDictionary<ushort, RoleBehaviour> Roles { get; private set; } = null!;
/// <summary>
/// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with.
/// </summary>
/// <returns>Read only dictionary of IDs and Game Modes.</returns>
public ReadOnlyDictionary<uint, AbstractGameMode> GetModes()
{
return new ReadOnlyDictionary<uint, AbstractGameMode>(GameModes);
}
/// <summary>
/// Gets a <see cref="IReadOnlyCollection{T}"/> of this <see cref="IMiraPlugin"/>'s <see cref="CustomActionButton"/>.
/// </summary>
@@ -74,6 +85,15 @@ public class MiraPluginInfo
/// </summary>
public IReadOnlyCollection<OptionPreset> Presets { get; internal set; } = null!;
/// <summary>
/// Gets a read only dictionary of Game Mode IDs and the AbstractGameMode object they are associated with.
/// </summary>
/// <returns>Read only dictionary of IDs and Game Modes.</returns>
public ReadOnlyDictionary<uint, AbstractGameMode> GetModes()
{
return new ReadOnlyDictionary<uint, AbstractGameMode>(GameModes);
}
internal void SavePublicCollections()
{
Presets = [.. InternalPresets];
@@ -87,39 +107,13 @@ public class MiraPluginInfo
}
internal List<OptionPreset> InternalPresets { get; } = [];
internal List<AbstractOptionGroup> InternalOptionGroups { get; } = [];
internal List<IModdedOption> InternalOptions { get; } = [];
internal List<BaseModifier> InternalModifiers { get; } = [];
internal List<CustomActionButton> InternalButtons { get; } = [];
internal List<AbstractOptionGroup> InternalOptionGroups { get; } = [];
internal List<MeetingActionButton> InternalMeetingButtons { get; } = [];
internal List<TargetedMeetingButton> InternalTargetedMeetingButtons { get; } = [];
internal Dictionary<uint, AbstractGameMode> GameModes { get; } = [];
internal Dictionary<ushort, RoleBehaviour> InternalRoles { get; } = [];
internal List<CustomActionButton> InternalButtons { get; } = [];
internal List<TargetedMeetingButton> InternalTargetedMeetingButtons { get; } = [];
internal List<MeetingActionButton> InternalMeetingButtons { get; } = [];
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
/// </summary>
public string PluginId { get; }
/// <summary>
/// Gets the plugin's instance as an <see cref="IMiraPlugin"/>.
/// </summary>
public IMiraPlugin MiraPlugin { get; }
/// <summary>
/// Gets the plugin's BepInEx metadata.
/// </summary>
public PluginInfo PluginInfo { get; }
/// <summary>
/// Gets the plugin's <see cref="ConfigFile"/>.
/// </summary>
public ConfigFile PluginConfig { get; }
}
+103 -193
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
@@ -24,7 +25,6 @@ using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
namespace MiraAPI.PluginLoading;
@@ -49,100 +49,14 @@ public sealed class MiraPluginManager
Instance = this;
CustomGameModeManager.RegisterDefaultMode();
CustomGameModeManager.GetAndSetGameMode();
if (IL2CPPChainloader.Instance.Plugins.TryGetValue(MiraApiPlugin.Id, out var newPlugin))
{
var assembly = Assembly.GetCallingAssembly();
var info = new MiraPluginInfo(apiPlugin, newPlugin);
var roles = new List<Type>();
var info = RegisterPlugin(typeof(MiraPluginManager).Assembly, apiPlugin, newPlugin, miraApiPlugin, true);
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
{
if (type.IsAbstract || type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
{
continue;
}
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
if (!method.IsStatic)
{
Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterLocalTabs(type, miraApiPlugin))
{
continue;
}
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
if (RegisterGameModeAttribute(type, info))
{
continue;
}
RegisterColorClasses(type);
RegisterKeybinds(type, miraApiPlugin);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
MiraApiConfig = info.PluginConfig;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
MiraApiPluginInfo = info;
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Info($"Registering Mira API as mod.");
}
@@ -153,110 +67,14 @@ public sealed class MiraPluginManager
return;
}
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
{
continue;
}
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
if (!method.IsStatic)
{
Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterLocalTabs(type, plugin))
{
continue;
}
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
if (RegisterMeetingAbility(type, info))
{
continue;
}
if (RegisterTargetedMeetingAbility(type, info))
{
continue;
}
if (RegisterGameModeAttribute(type, info))
{
continue;
}
RegisterColorClasses(type);
RegisterKeybinds(type, plugin);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
RegisterPlugin(assembly, miraPlugin, pluginInfo, plugin, false);
Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () =>
{
PaletteManager.RegisterAllColors();
// Save all buttons into a read-only collection for easy access
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
CustomButtonManager.EventTypes = new ReadOnlyDictionary<Type, Type>(CustomButtonManager.ButtonEventTypes);
@@ -269,9 +87,101 @@ public sealed class MiraPluginManager
ModifierManager.Modifiers = new ReadOnlyCollection<BaseModifier>(ModifierManager.InternalModifiers);
};
}
RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton<MiraApiPlugin>.Instance);
RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton<MiraApiPlugin>.Instance);
private MiraPluginInfo RegisterPlugin(Assembly assembly, IMiraPlugin plugin, PluginInfo pluginInfo, BasePlugin basePlugin, bool isMira)
{
var info = new MiraPluginInfo(plugin, pluginInfo);
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
ProcessAssemblyTypes(assembly, info, basePlugin, roles, isMira);
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
return info;
}
private static void ProcessAssemblyTypes(Assembly assembly, MiraPluginInfo info, BasePlugin basePlugin, List<Type> roles, bool isMira)
{
foreach (var type in assembly.GetTypes())
{
if ((isMira && type.IsAbstract) || type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
continue;
RegisterEvents(type);
if (RegisterModifier(type, info))
continue;
if (RegisterOptions(type, info))
continue;
if (RegisterLocalTabs(type, basePlugin))
continue;
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
continue;
if (RegisterGameOver(type))
continue;
if (RegisterMeetingAbility(type, info))
continue;
if (RegisterTargetedMeetingAbility(type, info))
continue;
if (RegisterGameModeAttribute(type, info))
continue;
RegisterColorClasses(type);
RegisterKeybinds(type, basePlugin);
}
}
private static void RegisterEvents(Type type)
{
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
continue;
if (!method.IsStatic)
{
Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
}
/// <summary>
@@ -358,7 +268,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Error($"Failed to register options for {type.Name}: {e.ToString()}");
Error($"Failed to register options for {type.Name}: {e}");
}
return false;
@@ -543,7 +453,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Error($"Failed to register options for {type.Name}: {e.ToString()}");
Error($"Failed to register options for {type.Name}: {e}");
}
return false;
-2
View File
@@ -71,8 +71,6 @@ Outlive the timer or eliminate the crew.</string>
<string name="MiraApi.Wiki.PreviousPage">Previous</string>
<string name="MiraApi.Wiki.NextPage">Next</string>
<string name="MiraApi.Wiki.NoResultsFound">No Results Found</string>
<string name="On">On</string>
<string name="Off">Off</string>
<string name="Enabled">Enabled</string>
<string name="Disabled">Disabled</string>
<string name="Active">Active</string>
+6 -3
View File
@@ -280,11 +280,11 @@ public static class CustomRoleManager
while (data.Count > 0)
{
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, data.Dequeue());
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, new MiraOptionNetData(false, data.Dequeue()));
}
}
internal static void HandleSyncRoleOptions(NetData[] data)
internal static void HandleSyncRoleOptions(bool showNotifs, NetData[] data)
{
// necessary to disable then re-enable this setting
// we don't know how other plugins handle their configs
@@ -312,7 +312,10 @@ public static class CustomRoleManager
var num = BitConverter.ToInt32(netData.Data, 0);
var chance = BitConverter.ToInt32(netData.Data, 4);
HudManager.Instance.Notifier.AddRoleSettingsChangeMessage(role.StringName, num, chance, role.TeamType, false);
if (showNotifs)
{
HudManager.Instance.Notifier.AddRoleSettingsChangeMessage(role.StringName, num, chance, role.TeamType, false);
}
try
{
+43
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using BepInEx.Configuration;
using Il2CppInterop.Runtime.Attributes;
@@ -92,6 +93,48 @@ public interface ICustomRole : IOptionable
_ => MiraLocaleManager.Get("MiraApi.RoleTeam.Other"),
};
/// <summary>
/// Gets a value indicating whether to exclude the "amount" from the displayed amount text within the role guide.
/// </summary>
public virtual bool ShowRoleAsUnique => Configuration.MaxRoleCount == 1;
/// <summary>
/// Gets the displayed amount and chance of the role. Used in the role guide.
/// By default, 0 amount or chance results in a complete 0 amount and chance, so override if necessary.
/// </summary>
/// <param name="amount">The amount of the role.</param>
/// <param name="chance">The chance of the role.</param>
/// <param name="source">The mod source of the role.</param>
/// <returns>The <see cref="string"/> result.</returns>
public virtual string GetAmountText(int amount, int chance, string source)
{
if (amount == 0)
{
chance = 0;
}
if (ShowRoleAsUnique)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0}% ({1})",
chance.ToString(CultureInfo.InvariantCulture),
source);
}
if (chance == 0)
{
amount = 0;
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
chance.ToString(CultureInfo.InvariantCulture),
source);
}
/// <summary>
/// Gets the <see cref="Color"/> of the role.
/// </summary>
+37 -11
View File
@@ -72,6 +72,7 @@ public static class MiraLocaleManager
/// </summary>
public static Dictionary<MiraLanguage, string> LangCultureList { get; } = new()
{
// Vanilla
{ MiraLanguage.English, "en-US" },
{ MiraLanguage.Latam, "es-419" },
{ MiraLanguage.Brazilian, "pt-BR" },
@@ -88,15 +89,17 @@ public static class MiraLocaleManager
{ MiraLanguage.SChinese, "zh-CN" },
{ MiraLanguage.TChinese, "zh-TW" },
{ MiraLanguage.Irish, "ga-IE" },
{ MiraLanguage.Polish, "pl-PL" }, // Custom
{ MiraLanguage.Turkish, "tr-TR" }, // Custom
{ MiraLanguage.Swedish, "sv-SE" }, // Custom
{ MiraLanguage.Lithuanian, "lt-LT" }, // Custom
{ MiraLanguage.Czech, "cs-CZ" }, // Custom
{ MiraLanguage.LChinese, "lzh-CN" }, // Custom
{ MiraLanguage.Greek, "el-GR" }, // Custom
{ MiraLanguage.Arabic, "arb" }, // Custom
{ MiraLanguage.Hebrew, "heb" }, // Custom
// Custom
{ MiraLanguage.Polish, "pl-PL" },
{ MiraLanguage.Turkish, "tr-TR" },
{ MiraLanguage.Swedish, "sv-SE" },
{ MiraLanguage.Lithuanian, "lt-LT" },
{ MiraLanguage.Czech, "cs-CZ" },
{ MiraLanguage.LChinese, "lzh-CN" },
{ MiraLanguage.Greek, "el-GR" },
{ MiraLanguage.Arabic, "arb" },
{ MiraLanguage.Hebrew, "heb" },
};
/// <summary>
@@ -123,26 +126,49 @@ public static class MiraLocaleManager
/// Call once per mod during plugin Load().
/// </summary>
/// <param name="modGuid">The mod GUID (e.g., "mira.example").</param>
[Obsolete("Pass typeof(YourPlugin).Assembly directly to Register for better performance. This will be removed in a future version.")]
public static void Register(string modGuid)
{
var callingAssembly = Assembly.GetCallingAssembly();
LoadInternalStrings(callingAssembly, callingAssembly.GetName().Name!, modGuid);
}
/// <summary>
/// Registers translations for a mod.
/// Call once per mod during plugin Load().
/// </summary>
/// <param name="modGuid">The mod GUID (e.g., "mira.example").</param>
/// <param name="assembly">The <see cref="Assembly"/> to load the strings from.</param>
public static void Register(string modGuid, Assembly assembly)
{
LoadInternalStrings(assembly, assembly.GetName().Name!, modGuid);
}
/// <summary>
/// Registers translations for a mod.
/// Call once per mod during plugin Load().
/// </summary>
/// <param name="modGuid">The mod GUID (e.g., "mira.example").</param>
/// <param name="internalName">The mod's root namespace (e.g., "mira.api").</param>
[Obsolete("Pass typeof(YourPlugin).Assembly directly to Register for better performance. This will be removed in a future version.")]
public static void Register(string modGuid, string internalName)
{
var callingAssembly = Assembly.GetCallingAssembly();
LoadInternalStrings(callingAssembly, internalName, modGuid);
}
/// <summary>
/// Registers translations for a mod.
/// Call once per mod during plugin Load().
/// </summary>
/// <param name="modGuid">The mod GUID (e.g., "mira.example").</param>
/// <param name="internalName">The mod's root namespace (e.g., "mira.api").</param>
/// <param name="assembly">The <see cref="Assembly"/> to load the strings from.</param>
public static void Register(string modGuid, string internalName, Assembly assembly)
{
LoadInternalStrings(assembly, internalName, modGuid);
}
/// <summary>
/// Translates a key into the current language.
/// </summary>
@@ -1,3 +1,4 @@
using System;
using System.Reflection;
using UnityEngine;
@@ -6,17 +7,39 @@ namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading .WAV audio assets from the DLL's embedded resources.
/// </summary>
/// <param name="path">The path of the wave file.</param>
public class LoadableAudioResourceAsset(string path) : LoadableAsset<AudioClip>
public class LoadableAudioResourceAsset : LoadableAsset<AudioClip>
{
private readonly Assembly _assembly = Assembly.GetCallingAssembly();
private readonly string _path;
private readonly Assembly _assembly;
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAudioResourceAsset"/> class with the provided resource path and the assembly containing the asset.
/// </summary>
/// <param name="path">The path of the wave file.</param>
/// <param name="assembly">The <see cref="Assembly"/> containing the asset.</param>
public LoadableAudioResourceAsset(string path, Assembly assembly)
{
_path = path;
_assembly = assembly;
}
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAudioResourceAsset"/> class with the provided resource path.
/// </summary>
/// <param name="path">The path of the wave file.</param>
[Obsolete("Pass typeof(YourPlugin).Assembly directly to the constructor for better performance. This will be removed in a future version.")]
public LoadableAudioResourceAsset(string path)
{
_path = path;
_assembly = Assembly.GetCallingAssembly();
}
/// <inheritdoc />
public override AudioClip LoadAsset()
{
if (!LoadedAsset)
{
LoadedAsset = AudioTools.LoadAudioFromResources(path, _assembly);
LoadedAsset = AudioTools.LoadAudioFromResources(_path, _assembly);
}
return LoadedAsset!;
@@ -1,4 +1,5 @@
using System.Reflection;
using System;
using System.Reflection;
using UnityEngine;
namespace MiraAPI.Utilities.Assets;
@@ -6,18 +7,44 @@ namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading assets from embedded resources.
/// </summary>
/// <param name="path">The path to the embedded resource.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
public class LoadableResourceAsset(string path, float pixelsPerUnit = 100f) : LoadableAsset<Sprite>
public class LoadableResourceAsset : LoadableAsset<Sprite>
{
private readonly Assembly _assembly = Assembly.GetCallingAssembly();
private readonly string _path;
private readonly float _pixelsPerUnit;
private readonly Assembly _assembly;
/// <summary>
/// Initializes a new instance of the <see cref="LoadableResourceAsset"/> class with the provided resource path and the assembly containing the asset.
/// </summary>
/// <param name="path">The path to the embedded resource.</param>
/// <param name="assembly">The <see cref="Assembly"/> containing the asset.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
public LoadableResourceAsset(string path, Assembly assembly, float pixelsPerUnit = 100f)
{
_path = path;
_assembly = assembly;
_pixelsPerUnit = pixelsPerUnit;
}
/// <summary>
/// Initializes a new instance of the <see cref="LoadableResourceAsset"/> class with the provided resource path and the assembly containing the asset.
/// </summary>
/// <param name="path">The path to the embedded resource.</param>
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
[Obsolete("Pass typeof(YourPlugin).Assembly directly to the constructor for better performance. This will be removed in a future version.")]
public LoadableResourceAsset(string path, float pixelsPerUnit = 100f)
{
_path = path;
_pixelsPerUnit = pixelsPerUnit;
_assembly = Assembly.GetCallingAssembly();
}
/// <inheritdoc />
public override Sprite LoadAsset()
{
if (!LoadedAsset)
{
LoadedAsset = SpriteTools.LoadSpriteFromPath(path, _assembly, pixelsPerUnit);
LoadedAsset = SpriteTools.LoadSpriteFromPath(_path, _assembly, _pixelsPerUnit);
}
return LoadedAsset!;
+69 -66
View File
@@ -1,4 +1,5 @@
using Reactor.Utilities;
using System.Reflection;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Utilities.Assets;
@@ -8,11 +9,13 @@ namespace MiraAPI.Utilities.Assets;
/// </summary>
public static class MiraAssets
{
private static readonly Assembly _assembly = typeof(MiraAssets).Assembly;
static MiraAssets()
{
var boxTex = SpriteTools.LoadTextureFromResourcePath(
"MiraAPI.Resources.RoundedBox.png",
System.Reflection.Assembly.GetExecutingAssembly());
_assembly);
var boxSprite = Sprite.Create(
boxTex,
new Rect(0, 0, boxTex.width, boxTex.height),
@@ -58,7 +61,7 @@ public static class MiraAssets
/// <summary>
/// Gets the empty <see cref="Sprite"/> asset.
/// </summary>
public static LoadableResourceAsset Empty { get; } = new("MiraAPI.Resources.Empty.png");
public static LoadableResourceAsset Empty { get; } = new("MiraAPI.Resources.Empty.png", _assembly);
/// <summary>
/// Gets the RoundedBox <see cref="Sprite"/>, which is a rounded rectangle used for UI elements.
@@ -68,312 +71,312 @@ public static class MiraAssets
/// <summary>
/// Gets the Next Button sprite.
/// </summary>
public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png");
public static LoadableResourceAsset NextButton { get; } = new("MiraAPI.Resources.NextButton.png", _assembly);
/// <summary>
/// Gets the Mira settings icon.
/// </summary>
public static LoadableResourceAsset SettingsIcon { get; } = new("MiraAPI.Resources.Settings.png");
public static LoadableResourceAsset SettingsIcon { get; } = new("MiraAPI.Resources.Settings.png", _assembly);
/// <summary>
/// Gets the highlighted Next Button sprite.
/// </summary>
public static LoadableResourceAsset NextButtonActive { get; } = new("MiraAPI.Resources.NextButtonActive.png");
public static LoadableResourceAsset NextButtonActive { get; } = new("MiraAPI.Resources.NextButtonActive.png", _assembly);
/// <summary>
/// Gets the Cog icon used in Role Settings Menu.
/// </summary>
public static LoadableResourceAsset Cog { get; } = new("MiraAPI.Resources.Cog.png");
public static LoadableResourceAsset Cog { get; } = new("MiraAPI.Resources.Cog.png", _assembly);
/// <summary>
/// Gets the Checkmark Box used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset CheckmarkBox { get; } = new("MiraAPI.Resources.CheckMarkBox.png");
public static LoadableResourceAsset CheckmarkBox { get; } = new("MiraAPI.Resources.CheckMarkBox.png", _assembly);
/// <summary>
/// Gets the Reset Box used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset ResetButton { get; } = new("MiraAPI.Resources.ResetButton.png");
public static LoadableResourceAsset ResetButton { get; } = new("MiraAPI.Resources.ResetButton.png", _assembly);
/// <summary>
/// Gets the Checkmark used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset Checkmark { get; } = new("MiraAPI.Resources.Checkmark.png");
public static LoadableResourceAsset Checkmark { get; } = new("MiraAPI.Resources.Checkmark.png", _assembly);
/// <summary>
/// Gets the white CategoryHeader used in the Settings Menu.
/// </summary>
public static LoadableResourceAsset CategoryHeader { get; } = new("MiraAPI.Resources.CategoryHeader.png");
public static LoadableResourceAsset CategoryHeader { get; } = new("MiraAPI.Resources.CategoryHeader.png", _assembly);
/// <summary>
/// Gets the sprite used for the Keybind icons.
/// </summary>
public static LoadableResourceAsset KeybindButton { get; } = new("MiraAPI.Resources.KeybindButton.png");
public static LoadableResourceAsset KeybindButton { get; } = new("MiraAPI.Resources.KeybindButton.png", _assembly);
/// <summary>
/// Gets the sprite used for crewmate role files in TaskAdderGame.
/// </summary>
public static LoadableResourceAsset CrewmateFile { get; } = new("MiraAPI.Resources.CrewmateFile.png");
public static LoadableResourceAsset CrewmateFile { get; } = new("MiraAPI.Resources.CrewmateFile.png", _assembly);
/// <summary>
/// Gets the sprite used for impostor role files in TaskAdderGame.
/// </summary>
public static LoadableResourceAsset ImpostorFile { get; } = new("MiraAPI.Resources.ImpostorFile.png");
public static LoadableResourceAsset ImpostorFile { get; } = new("MiraAPI.Resources.ImpostorFile.png", _assembly);
/// <summary>
/// Gets the sprite used for custom team role files in TaskAdderGame.
/// </summary>
public static LoadableResourceAsset CustomTeamFile { get; } = new("MiraAPI.Resources.CustomFile.png");
public static LoadableResourceAsset CustomTeamFile { get; } = new("MiraAPI.Resources.CustomFile.png", _assembly);
/// <summary>
/// Gets the sprite used for modifier file in TaskAdderGame.
/// </summary>
public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png");
public static LoadableResourceAsset ModifierFile { get; } = new("MiraAPI.Resources.ModifierFile.png", _assembly);
/// <summary>
/// Gets the sprite used for the Classic gamemode.
/// </summary>
public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png");
public static LoadableResourceAsset ClassicGamemodeIcon { get; } = new("MiraAPI.Resources.ClassicGamemodeIcon.png", _assembly);
/// <summary>
/// Gets the sprite used for the Hide n Seek gamemode.
/// </summary>
public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png");
public static LoadableResourceAsset HnSGamemodeIcon { get; } = new("MiraAPI.Resources.HnSGamemodeIcon.png", _assembly);
/// <summary>
/// Gets the sprite used for timed modifier file in TaskAdderGame.
/// </summary>
public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png");
public static LoadableResourceAsset TimedModifierFile { get; } = new("MiraAPI.Resources.TimedModifierFile.png", _assembly);
/// <summary>
/// Gets the sprite used for the new chat button while hovering.
/// </summary>
public static LoadableResourceAsset ChatHoverSprite { get; } = new("MiraAPI.Resources.NormalChatHover.png");
public static LoadableResourceAsset ChatHoverSprite { get; } = new("MiraAPI.Resources.NormalChatHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the new chat button while hovering.
/// </summary>
public static LoadableResourceAsset ChatIdleSprite { get; } = new("MiraAPI.Resources.NormalChatIdle.png");
public static LoadableResourceAsset ChatIdleSprite { get; } = new("MiraAPI.Resources.NormalChatIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the new chat button while hovering.
/// </summary>
public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png");
public static LoadableResourceAsset ChatOpenSprite { get; } = new("MiraAPI.Resources.NormalChatOpen.png", _assembly);
/// <summary>
/// Gets the sprite used for players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonHover.png");
public static LoadableResourceAsset WikiPlayersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonIdle.png");
public static LoadableResourceAsset WikiPlayersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the players button while hovering.
/// </summary>
public static LoadableResourceAsset WikiPlayersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonOpen.png");
public static LoadableResourceAsset WikiPlayersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarPlayersButtonOpen.png", _assembly);
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonHover.png");
public static LoadableResourceAsset WikiSettingsButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonIdle.png");
public static LoadableResourceAsset WikiSettingsButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the settings button while hovering.
/// </summary>
public static LoadableResourceAsset WikiSettingsButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonOpen.png");
public static LoadableResourceAsset WikiSettingsButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarSettingsButtonOpen.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonHover.png");
public static LoadableResourceAsset WikiRolesButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonIdle.png");
public static LoadableResourceAsset WikiRolesButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png");
public static LoadableResourceAsset WikiRolesButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarRolesButtonOpen.png", _assembly);
/// <summary>
/// Gets the sprite used for the modifiers button while hovering.
/// </summary>
public static LoadableResourceAsset WikiModifiersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonHover.png");
public static LoadableResourceAsset WikiModifiersButtonHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the modifiers button while hovering.
/// </summary>
public static LoadableResourceAsset WikiModifiersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonIdle.png");
public static LoadableResourceAsset WikiModifiersButtonIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the modifiers button while hovering.
/// </summary>
public static LoadableResourceAsset WikiModifiersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonOpen.png");
public static LoadableResourceAsset WikiModifiersButtonOpenSprite { get; } = new("MiraAPI.Resources.Wiki.SidebarModifiersButtonOpen.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupCountHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountHover.png");
public static LoadableResourceAsset GroupCountHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupCountIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountIdle.png");
public static LoadableResourceAsset GroupCountIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupCountIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupFactionHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionHover.png");
public static LoadableResourceAsset GroupFactionHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupFactionIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionIdle.png");
public static LoadableResourceAsset GroupFactionIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupFactionIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupNoneHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneHover.png");
public static LoadableResourceAsset GroupNoneHoverSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset GroupNoneIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneIdle.png");
public static LoadableResourceAsset GroupNoneIdleSprite { get; } = new("MiraAPI.Resources.Wiki.GroupNoneIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchClearHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearHover.png");
public static LoadableResourceAsset SearchClearHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchClearIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearIdle.png");
public static LoadableResourceAsset SearchClearIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchClearIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchIconHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconHover.png");
public static LoadableResourceAsset SearchIconHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SearchIconIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconIdle.png");
public static LoadableResourceAsset SearchIconIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SearchIconIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingAzHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzHover.png");
public static LoadableResourceAsset SortingAzHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingAzIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzIdle.png");
public static LoadableResourceAsset SortingAzIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingAzIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingZaHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaHover.png");
public static LoadableResourceAsset SortingZaHoverSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset SortingZaIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaIdle.png");
public static LoadableResourceAsset SortingZaIdleSprite { get; } = new("MiraAPI.Resources.Wiki.SortingZaIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleAllHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllHover.png");
public static LoadableResourceAsset VisibleAllHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleAllIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllIdle.png");
public static LoadableResourceAsset VisibleAllIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleAllIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOffHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffHover.png");
public static LoadableResourceAsset VisibleOffHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOffIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffIdle.png");
public static LoadableResourceAsset VisibleOffIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOffIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOnHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnHover.png");
public static LoadableResourceAsset VisibleOnHoverSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnHover.png", _assembly);
/// <summary>
/// Gets the sprite used for the roles button while hovering.
/// </summary>
public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png");
public static LoadableResourceAsset VisibleOnIdleSprite { get; } = new("MiraAPI.Resources.Wiki.VisibleOnIdle.png", _assembly);
/// <summary>
/// Gets the sprite used for crewmates.
/// </summary>
public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png");
public static LoadableResourceAsset IconCrewmate { get; } = new("MiraAPI.Resources.Wiki.IconCrewmate.png", _assembly);
/// <summary>
/// Gets the sprite used for impostors.
/// </summary>
public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png");
public static LoadableResourceAsset IconImpostor { get; } = new("MiraAPI.Resources.Wiki.IconImpostor.png", _assembly);
/// <summary>
/// Gets the sprite used for neutrals.
/// </summary>
public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png");
public static LoadableResourceAsset IconNeutral { get; } = new("MiraAPI.Resources.Wiki.IconNeutral.png", _assembly);
/// <summary>
/// Gets the sprite used for the player list.
/// </summary>
public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png");
public static LoadableResourceAsset IconPlayerList { get; } = new("MiraAPI.Resources.Wiki.IconPlayerList.png", _assembly);
/// <summary>
/// Gets the sprite used for settings.
/// </summary>
public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png");
public static LoadableResourceAsset IconSettings { get; } = new("MiraAPI.Resources.Wiki.IconSettings.png", _assembly);
/// <summary>
/// Gets the sprite used for terminology.
/// </summary>
public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png");
public static LoadableResourceAsset IconTerminology { get; } = new("MiraAPI.Resources.Wiki.IconTerminology.png", _assembly);
/// <summary>
/// Gets the Page switching sprite.
/// </summary>
public static LoadableResourceAsset PageLeftHover { get; } = new("MiraAPI.Resources.Wiki.PageLeftHover.png");
public static LoadableResourceAsset PageLeftHover { get; } = new("MiraAPI.Resources.Wiki.PageLeftHover.png", _assembly);
/// <summary>
/// Gets the Page switching sprite.
/// </summary>
public static LoadableResourceAsset PageLeftIdle { get; } = new("MiraAPI.Resources.Wiki.PageLeftIdle.png");
public static LoadableResourceAsset PageLeftIdle { get; } = new("MiraAPI.Resources.Wiki.PageLeftIdle.png", _assembly);
/// <summary>
/// Gets the Page switching sprite.
/// </summary>
public static LoadableResourceAsset PageRightHover { get; } = new("MiraAPI.Resources.Wiki.PageRightHover.png");
public static LoadableResourceAsset PageRightHover { get; } = new("MiraAPI.Resources.Wiki.PageRightHover.png", _assembly);
/// <summary>
/// Gets the Page switching sprite.
/// </summary>
public static LoadableResourceAsset PageRightIdle { get; } = new("MiraAPI.Resources.Wiki.PageRightIdle.png");
public static LoadableResourceAsset PageRightIdle { get; } = new("MiraAPI.Resources.Wiki.PageRightIdle.png", _assembly);
/// <summary>
/// Gets the sprite used below the tab icons in the wiki.
/// </summary>
public static LoadableResourceAsset TabButtonContainer { get; } = new("MiraAPI.Resources.Wiki.TabButtonContainer.png");
public static LoadableResourceAsset TabButtonContainer { get; } = new("MiraAPI.Resources.Wiki.TabButtonContainer.png", _assembly);
internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png");
internal static LoadableResourceAsset BlankSprite { get; } = new("MiraAPI.Resources.BlankSprite.png", _assembly);
}
+31
View File
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Utilities;
/// <summary>
/// A comparer used to sort role/modifier IDs based on which ones are currently active.
/// </summary>
/// <param name="activeIds">The collection of currently active IDs.</param>
public class BasicGuideComparer(IEnumerable<uint> activeIds) : IComparer<uint>
{
/// <inheritdoc/>
public int Compare(uint x, uint y)
{
var xActive = activeIds.Contains(x);
var yActive = activeIds.Contains(y);
if (xActive && !yActive)
{
return -1;
}
if (!xActive && yActive)
{
return 1;
}
return 0;
}
}
+297 -11
View File
@@ -1,12 +1,26 @@
using UnityEngine;
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace MiraAPI.Utilities;
/// <summary>
/// Extension methods for the <see cref="Color"/> struct.
/// Extension methods for the <see cref="Color"/> and <see cref="Color32"/> structs.
/// </summary>
public static class ColorExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float ToFloat(this byte @byte)
{
return @byte / 255f;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ToByte(this float @float)
{
return (byte)MathF.Round(@float * 255f);
}
/// <summary>
/// Gets the relative luminance of a <see cref="Color"/> (WCAG 2.1 specification). 0 is black, 1 is white.
/// </summary>
@@ -19,6 +33,18 @@ public static class ColorExtensions
(0.0722f * Linearize(color.b));
}
/// <summary>
/// Gets the relative luminance of a <see cref="Color32"/> (WCAG 2.1 specification). 0 is black, 1 is white.
/// </summary>
/// <param name="color"><see cref="Color32"/> to calculate luminance for.</param>
/// <returns>Luminance value between 0 and 1.</returns>
public static float GetRelativeLuminance(this Color32 color)
{
return (0.2126f * Linearize(color.r.ToFloat())) +
(0.7152f * Linearize(color.g.ToFloat())) +
(0.0722f * Linearize(color.b.ToFloat()));
}
/// <summary>
/// Linearizes an sRGB component into linear space.
/// </summary>
@@ -26,7 +52,7 @@ public static class ColorExtensions
/// <returns>The linearized result.</returns>
public static float Linearize(float c)
{
return c <= 0.03928f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f);
return c <= 0.03928f ? c / 12.92f : MathF.Pow((c + 0.055f) / 1.055f, 2.4f);
}
/// <summary>
@@ -39,11 +65,27 @@ public static class ColorExtensions
{
var luminanceA = colorA.GetRelativeLuminance();
var luminanceB = colorB.GetRelativeLuminance();
return (Mathf.Max(luminanceA, luminanceB) + 0.05f) / (Mathf.Min(luminanceA, luminanceB) + 0.05f);
return (Math.Max(luminanceA, luminanceB) + 0.05f) / (Math.Min(luminanceA, luminanceB) + 0.05f);
}
/// <summary>
/// Generates an accessible alternate <see cref="Color"/> with sufficient contrast.
/// Calculates the contrast ratio between two <see cref="Color32"/>s.
/// </summary>
/// <param name="colorA">First <see cref="Color32"/>.</param>
/// <param name="colorB">Second <see cref="Color32"/>.</param>
/// <returns>Contrast ratio between the two <see cref="Color32"/>s.</returns>
public static float GetContrastRatio(this Color32 colorA, Color32 colorB)
{
var luminanceA = colorA.GetRelativeLuminance();
var luminanceB = colorB.GetRelativeLuminance();
return (Math.Max(luminanceA, luminanceB) + 0.05f) / (Math.Min(luminanceA, luminanceB) + 0.05f);
}
/// <summary>
/// Generates an accessible alternate <see cref="Color"/> by shifting towards black or white
/// based on which extreme offers the highest potential contrast from the original color.
/// </summary>
/// <param name="color">Base <see cref="Color"/>.</param>
/// <param name="desiredRatio">Target contrast ratio (default 4.5f for AA).</param>
@@ -57,10 +99,120 @@ public static class ColorExtensions
}
/// <summary>
/// Finds the <see cref="Color"/> with the minimum contrast ratio to the target <see cref="Color"/>.
/// Generates an accessible alternate <see cref="Color32"/> with sufficient contrast.
/// </summary>
/// <param name="color">Base <see cref="Color32"/>.</param>
/// <param name="desiredRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>Alternate <see cref="Color32"/> with sufficient contrast.</returns>
public static Color32 FindAlternateColor(this Color32 color, float desiredRatio = 4.5f)
{
var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat());
var result = colorF.FindAlternateColor(desiredRatio);
return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a);
}
/// <summary>
/// Darkens a <see cref="Color"/> towards black until it reaches the target WCAG contrast ratio against a background.
/// </summary>
/// <param name="color">The foreground <see cref="Color"/> to modify.</param>
/// <param name="background">The background <see cref="Color"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A darkened <see cref="Color"/> meeting the contrast ratio, or black if impossible.</returns>
public static Color DarkenToWCAG(this Color color, Color background, float targetRatio = 4.5f)
{
return color.GetContrastRatio(background) >= targetRatio
? color
: FindMinimumContrastColor(color, Color.black, targetRatio);
}
/// <summary>
/// Darkens a <see cref="Color32"/> towards black until it reaches the target WCAG contrast ratio against a background.
/// </summary>
/// <param name="color">The foreground <see cref="Color32"/> to modify.</param>
/// <param name="background">The background <see cref="Color32"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A darkened <see cref="Color32"/> meeting the contrast ratio, or black if impossible.</returns>
public static Color32 DarkenToWCAG(this Color32 color, Color32 background, float targetRatio = 4.5f)
{
var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat());
var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat());
var result = colorF.DarkenToWCAG(bgF, targetRatio);
return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a);
}
/// <summary>
/// Lightens a <see cref="Color"/> towards white until it reaches the target WCAG contrast ratio against a background.
/// </summary>
/// <param name="color">The foreground <see cref="Color"/> to modify.</param>
/// <param name="background">The background <see cref="Color"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A lightened <see cref="Color"/> meeting the contrast ratio, or white if impossible.</returns>
public static Color LightenToWCAG(this Color color, Color background, float targetRatio = 4.5f)
{
return color.GetContrastRatio(background) >= targetRatio
? color
: FindMinimumContrastColor(color, Color.white, targetRatio);
}
/// <summary>
/// Lightens a <see cref="Color32"/> towards white until it reaches the target WCAG contrast ratio against a background.
/// </summary>
/// <param name="color">The foreground <see cref="Color32"/> to modify.</param>
/// <param name="background">The background <see cref="Color32"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A lightened <see cref="Color32"/> meeting the contrast ratio, or white if impossible.</returns>
public static Color32 LightenToWCAG(this Color32 color, Color32 background, float targetRatio = 4.5f)
{
var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat());
var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat());
var result = colorF.LightenToWCAG(bgF, targetRatio);
return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a);
}
/// <summary>
/// Automatically decides whether to lighten or darken the <see cref="Color"/> to achieve WCAG compliance
/// based on which extreme offers the highest potential contrast against the provided background.
/// </summary>
/// <param name="color">The foreground <see cref="Color"/> to modify.</param>
/// <param name="background">The background <see cref="Color"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A modified <see cref="Color"/> meeting the WCAG contrast requirement.</returns>
public static Color EnsureWCAGContrast(this Color color, Color background, float targetRatio = 4.5f)
{
if (color.GetContrastRatio(background) >= targetRatio)
return color;
var blackContrast = Color.black.GetContrastRatio(background);
var whiteContrast = Color.white.GetContrastRatio(background);
return blackContrast > whiteContrast
? color.DarkenToWCAG(background, targetRatio)
: color.LightenToWCAG(background, targetRatio);
}
/// <summary>
/// Automatically decides whether to lighten or darken the <see cref="Color32"/> to achieve WCAG compliance.
/// </summary>
/// <param name="color">The foreground <see cref="Color32"/> to modify.</param>
/// <param name="background">The background <see cref="Color32"/> it sits on.</param>
/// <param name="targetRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>A modified <see cref="Color32"/> meeting the WCAG contrast requirement.</returns>
public static Color32 EnsureWCAGContrast(this Color32 color, Color32 background, float targetRatio = 4.5f)
{
var colorF = new Color(color.r.ToFloat(), color.g.ToFloat(), color.b.ToFloat(), color.a.ToFloat());
var bgF = new Color(background.r.ToFloat(), background.g.ToFloat(), background.b.ToFloat(), background.a.ToFloat());
var result = colorF.EnsureWCAGContrast(bgF, targetRatio);
return new Color32(result.r.ToByte(), result.g.ToByte(), result.b.ToByte(), color.a);
}
/// <summary>
/// Finds the <see cref="Color"/> with the minimum contrast ratio to the target <see cref="Color"/> using binary search.
/// </summary>
/// <param name="baseColor">The base <see cref="Color"/> to start from.</param>
/// <param name="targetColor">The target <see cref="Color"/> to compare against.</param>
/// <param name="targetColor">The target <see cref="Color"/> to approach (usually white or black).</param>
/// <param name="desiredRatio">The desired contrast ratio.</param>
/// <returns>The <see cref="Color"/> with the minimum contrast ratio to the target <see cref="Color"/>.</returns>
public static Color FindMinimumContrastColor(Color baseColor, Color targetColor, float desiredRatio)
@@ -72,18 +224,28 @@ public static class ColorExtensions
? desiredRatio * (baseLuminance + 0.05f) - 0.05f
: (baseLuminance + 0.05f) / desiredRatio - 0.05f;
if (isLightening && targetLuminance > 1f) return Color.white;
if (!isLightening && targetLuminance < 0f) return Color.black;
if (isLightening && targetLuminance > 1f)
return Color.white;
if (!isLightening && targetLuminance < 0f)
return Color.black;
float low = 0f, high = 1f;
var bestColor = targetColor;
for (var i = 0; i < 10; i++)
{
var t = (low + high) / 2f;
var testColor = Color.Lerp(baseColor, targetColor, t);
var testColor = new Color(
baseColor.r + (targetColor.r - baseColor.r) * t,
baseColor.g + (targetColor.g - baseColor.g) * t,
baseColor.b + (targetColor.b - baseColor.b) * t,
baseColor.a);
var testLuminance = testColor.GetRelativeLuminance();
if ((isLightening && testLuminance >= targetLuminance) || (!isLightening && testLuminance <= targetLuminance))
if (isLightening ? testLuminance >= targetLuminance : testLuminance <= targetLuminance)
{
high = t;
bestColor = testColor;
@@ -96,4 +258,128 @@ public static class ColorExtensions
return bestColor;
}
/// <summary>
/// Subtracts a flat amount from the <see cref="Color"/>'s RGB channels.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">The flat amount to subtract (typically 0.0 to 1.0).</param>
/// <returns>A uniformly darkened <see cref="Color"/>.</returns>
public static Color DarkenAbsolute(this Color color, float amount)
{
return new Color(
Math.Max(0f, color.r - amount),
Math.Max(0f, color.g - amount),
Math.Max(0f, color.b - amount),
color.a);
}
/// <summary>
/// Subtracts a flat byte amount from the <see cref="Color32"/>'s RGB channels.
/// </summary>
/// <param name="color">The original <see cref="Color32"/>.</param>
/// <param name="amount">The flat amount to subtract (0 to 255).</param>
/// <returns>A uniformly darkened <see cref="Color32"/>.</returns>
public static Color32 DarkenAbsolute(this Color32 color, byte amount)
{
return new Color32(
(byte)Math.Max(0, color.r - amount),
(byte)Math.Max(0, color.g - amount),
(byte)Math.Max(0, color.b - amount),
color.a);
}
/// <summary>
/// Adds a flat amount to the <see cref="Color"/>'s RGB channels.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">The flat amount to add (typically 0.0 to 1.0).</param>
/// <returns>A uniformly lightened <see cref="Color"/>.</returns>
public static Color LightenAbsolute(this Color color, float amount)
{
return new Color(
Math.Min(1f, color.r + amount),
Math.Min(1f, color.g + amount),
Math.Min(1f, color.b + amount),
color.a);
}
/// <summary>
/// Adds a flat byte amount to the <see cref="Color32"/>'s RGB channels.
/// </summary>
/// <param name="color">The original <see cref="Color32"/>.</param>
/// <param name="amount">The flat amount to add (0 to 255).</param>
/// <returns>A uniformly lightened <see cref="Color32"/>.</returns>
public static Color32 LightenAbsolute(this Color32 color, byte amount)
{
return new Color32(
(byte)Math.Min(255, color.r + amount),
(byte)Math.Min(255, color.g + amount),
(byte)Math.Min(255, color.b + amount),
color.a);
}
/// <summary>
/// Darkens the <see cref="Color"/> multiplicatively, preserving the ratio between RGB channels to maintain hue.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="percentage">The percentage to darken by (e.g., 0.2 for 20% darker).</param>
/// <returns>A ratio-preserved darkened <see cref="Color"/>.</returns>
public static Color DarkenRelative(this Color color, float percentage)
{
var multiplier = Math.Max(0f, Math.Min(1f, 1f - percentage));
return new Color(
color.r * multiplier,
color.g * multiplier,
color.b * multiplier,
color.a);
}
/// <summary>
/// Darkens the <see cref="Color32"/> multiplicatively, preserving the ratio between RGB channels.
/// </summary>
/// <param name="color">The original <see cref="Color32"/>.</param>
/// <param name="percentage">The percentage to darken by (e.g., 0.2 for 20% darker).</param>
/// <returns>A ratio-preserved darkened <see cref="Color32"/>.</returns>
public static Color32 DarkenRelative(this Color32 color, float percentage)
{
var multiplier = Math.Max(0f, Math.Min(1f, 1f - percentage));
return new Color32(
(color.r * multiplier).ToByte(),
(color.g * multiplier).ToByte(),
(color.b * multiplier).ToByte(),
color.a);
}
/// <summary>
/// Lightens the <see cref="Color"/> multiplicatively, preserving the ratio between RGB channels.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="percentage">The percentage to lighten by (e.g., 0.2 for 20% lighter).</param>
/// <returns>A ratio-preserved lightened <see cref="Color"/>.</returns>
public static Color LightenRelative(this Color color, float percentage)
{
var multiplier = 1f + percentage;
return new Color(
Math.Min(1f, color.r * multiplier),
Math.Min(1f, color.g * multiplier),
Math.Min(1f, color.b * multiplier),
color.a);
}
/// <summary>
/// Lightens the <see cref="Color32"/> multiplicatively, preserving the ratio between RGB channels.
/// </summary>
/// <param name="color">The original <see cref="Color32"/>.</param>
/// <param name="percentage">The percentage to lighten by (e.g., 0.2 for 20% lighter).</param>
/// <returns>A ratio-preserved lightened <see cref="Color32"/>.</returns>
public static Color32 LightenRelative(this Color32 color, float percentage)
{
var multiplier = 1f + percentage;
return new Color32(
Math.Min(255f, color.r * 255f * multiplier / 255f).ToByte(),
Math.Min(255f, color.g * 255f * multiplier / 255f).ToByte(),
Math.Min(255f, color.b * 255f * multiplier / 255f).ToByte(),
color.a);
}
}
@@ -2,7 +2,7 @@ using System;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Utilities;
namespace MiraAPI.Utilities.Components;
/// <summary>
/// A spinning ring used to indicate that content is loading.
+61 -25
View File
@@ -26,6 +26,8 @@ namespace MiraAPI.Utilities;
/// </summary>
public static class Extensions
{
private static readonly Regex DisplayRegex = new(@"([^\^])([A-Z][a-z$])", default, Regex.InfiniteMatchTimeout);
internal static NetData GetNetData(this ICustomRole role)
{
var count = role.GetCount();
@@ -492,14 +494,25 @@ public static class Extensions
}
/// <summary>
/// Gets a proper string for an <see cref="Enum"/>. (with spaces).
/// Gets a proper string for an <see cref="Enum"/> (with spaces).
/// </summary>
/// <param name="enum">The <see cref="Enum"/> you would like to change.</param>
/// <returns>A proper string for the <see cref="Enum"/>.</returns>
/// <returns>A proper string for the provided <see cref="Enum"/>.</returns>
public static string ToDisplayString(this Enum @enum)
{
var regex = new Regex(@"([^\^])([A-Z][a-z$])", default, Regex.InfiniteMatchTimeout);
return regex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}");
return DisplayRegex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}");
}
/// <summary>
/// Gets a proper string for a(n) <typeparamref name="T"/> value (with spaces).
/// </summary>
/// <typeparam name="T">The type of the enum value.</typeparam>
/// <param name="enum">The <typeparamref name="T"/> enum value you would like to change.</param>
/// <returns>A proper string for the provided <typeparamref name="T"/> value.</returns>
public static string ToDisplayString<T>(this T @enum)
where T : struct, Enum
{
return DisplayRegex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}");
}
/// <summary>
@@ -526,17 +539,46 @@ public static class Extensions
/// <returns>The distance between the types.</returns>
public static int GetInheritanceDistance(Type from, Type to)
{
if (!from.IsAssignableFrom(to)) return int.MaxValue;
if (from == null || to == null)
throw new ArgumentNullException(from == null ? nameof(from) : nameof(to));
var type = from;
var distance = 0;
while (type != null && type != to)
if (from == to)
return 0;
if (!to.IsGenericTypeDefinition && !to.IsAssignableFrom(from))
return int.MaxValue;
var queue = new Queue<(Type Type, int Distance)>();
var visited = new HashSet<Type>();
queue.Enqueue((from, 0));
visited.Add(from);
while (queue.Count > 0)
{
type = type.BaseType;
distance++;
var (current, dist) = queue.Dequeue();
if (current == to || (current.IsGenericType && current.GetGenericTypeDefinition() == to))
return dist;
if (current.BaseType != null && visited.Add(current.BaseType))
queue.Enqueue((current.BaseType, dist + 1));
var allInterfaces = current.GetInterfaces();
var baseInterfaces = current.BaseType?.GetInterfaces() ?? Type.EmptyTypes;
var directInterfaces = allInterfaces.Except(baseInterfaces).ToList();
var inheritedInterfaces = directInterfaces.SelectMany(i => i.GetInterfaces());
var immediateInterfaces = directInterfaces.Except(inheritedInterfaces);
foreach (var iFace in immediateInterfaces)
{
if (visited.Add(iFace))
queue.Enqueue((iFace, dist + 1));
}
}
return type == to ? distance : int.MaxValue;
return to.IsAssignableFrom(from) ? int.MaxValue - 1 : int.MaxValue;
}
/// <summary>
@@ -565,14 +607,10 @@ public static class Extensions
/// <param name="color">The original <see cref="Color32"/>.</param>
/// <param name="darknessAmount">A darkness amount between 0 and 255.</param>
/// <returns>The darkened <see cref="Color32"/>.</returns>
[Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")]
public static Color32 GetShadowColor(this Color32 color, byte darknessAmount)
{
return
new Color32(
(byte)Mathf.Clamp(color.r - darknessAmount, 0, 255),
(byte)Mathf.Clamp(color.g - darknessAmount, 0, 255),
(byte)Mathf.Clamp(color.b - darknessAmount, 0, 255),
color.a);
return color.DarkenAbsolute(darknessAmount);
}
/// <summary>
@@ -581,14 +619,10 @@ public static class Extensions
/// <param name="color">The original <see cref="Color"/>.</param>
/// <param name="darknessAmount">A darkness amount between 0 and 1.</param>
/// <returns>The darkened <see cref="Color"/>.</returns>
[Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")]
public static Color GetShadowColor(this Color color, float darknessAmount)
{
return
new Color(
MathUtilities.Clamp01(color.r - darknessAmount),
MathUtilities.Clamp01(color.g - darknessAmount),
MathUtilities.Clamp01(color.b - darknessAmount),
color.a);
return color.DarkenAbsolute(darknessAmount);
}
/// <summary>
@@ -677,9 +711,10 @@ public static class Extensions
/// <param name="color">The original <see cref="Color"/>.</param>
/// <param name="amount">A <see langword="float"/> amount between 0 and 1.</param>
/// <returns>The darkened <see cref="Color"/>.</returns>
[Obsolete("Use ColorExtensions.DarkenAbsolute instead. This will be removed in a future version.")]
public static Color DarkenColor(this Color color, float amount = 0.45f)
{
return new Color(color.r - amount, color.g - amount, color.b - amount);
return color.DarkenAbsolute(amount);
}
/// <summary>
@@ -688,9 +723,10 @@ public static class Extensions
/// <param name="color">The original <see cref="Color"/>.</param>
/// <param name="amount">A <see langword="float"/> amount between 0.0 and 1.0.</param>
/// <returns>The lightened <see cref="Color"/>.</returns>
[Obsolete("Use ColorExtensions.LightenAbsolute instead. This will be removed in a future version.")]
public static Color LightenColor(this Color color, float amount = 0.45f)
{
return new Color(color.r + amount, color.g + amount, color.b + amount);
return color.LightenAbsolute(amount);
}
/// <summary>
+41 -78
View File
@@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Utilities;
@@ -57,9 +54,7 @@ internal sealed class GuideOptionPage(
internal static class GuideOptionPages
{
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
public static List<GuideOptionPage> BuildPages(Func<bool, string> formatBool)
public static List<GuideOptionPage> BuildPages()
{
var pages = new List<GuideOptionPage>();
var combinedPages = new Dictionary<string, GuideOptionPage>(StringComparer.OrdinalIgnoreCase);
@@ -95,48 +90,49 @@ internal static class GuideOptionPages
item.Group,
groupName,
item.ModAbbreviation,
modSearchTerms,
formatBool);
modSearchTerms);
if (entry != null)
{
entries.Add(entry);
}
}
if (entries.Count > 0)
if (entries.Count <= 0)
{
var pageKey = item.Group.MatchInfoGuidePageKey?.Trim();
var combinedKey = string.IsNullOrEmpty(pageKey) ? null : $"{item.Plugin.PluginId}\0{pageKey}";
if (combinedKey != null && combinedPages.TryGetValue(combinedKey, out var combinedPage))
continue;
}
var pageKey = item.Group.MatchInfoGuidePageKey?.Trim();
var combinedKey = string.IsNullOrEmpty(pageKey) ? null : $"{item.Plugin.PluginId}\0{pageKey}";
if (combinedKey != null && combinedPages.TryGetValue(combinedKey, out var combinedPage))
{
combinedPage.Groups.Add(item.Group);
combinedPage.Entries.AddRange(entries);
combinedPage.ModSearchTerms.UnionWith(modSearchTerms);
combinedPage.Order = Math.Min(combinedPage.Order, item.Group.MatchInfoGuidePageOrder);
combinedPage.MenuOrder = Math.Min(
combinedPage.MenuOrder,
GetMenuOrder(item.Group.ParentMenu));
combinedPage.GroupPriority = Math.Min(
combinedPage.GroupPriority,
item.Group.GroupPriority);
}
else
{
var page = new GuideOptionPage(
item.Group,
item.Group.MatchInfoGuidePageTitle,
item.Group.MatchInfoGuidePageOrder,
GetMenuOrder(item.Group.ParentMenu),
item.Group.GroupPriority,
Mathf.Clamp(item.Group.MatchInfoGuidePageColumns, 1, 6),
item.ModAbbreviation,
modSearchTerms,
entries);
pages.Add(page);
if (combinedKey != null)
{
combinedPage.Groups.Add(item.Group);
combinedPage.Entries.AddRange(entries);
combinedPage.ModSearchTerms.UnionWith(modSearchTerms);
combinedPage.Order = Math.Min(combinedPage.Order, item.Group.MatchInfoGuidePageOrder);
combinedPage.MenuOrder = Math.Min(
combinedPage.MenuOrder,
GetMenuOrder(item.Group.ParentMenu));
combinedPage.GroupPriority = Math.Min(
combinedPage.GroupPriority,
item.Group.GroupPriority);
}
else
{
var page = new GuideOptionPage(
item.Group,
item.Group.MatchInfoGuidePageTitle,
item.Group.MatchInfoGuidePageOrder,
GetMenuOrder(item.Group.ParentMenu),
item.Group.GroupPriority,
Mathf.Clamp(item.Group.MatchInfoGuidePageColumns, 1, 6),
item.ModAbbreviation,
modSearchTerms,
entries);
pages.Add(page);
if (combinedKey != null)
{
combinedPages.Add(combinedKey, page);
}
combinedPages.Add(combinedKey, page);
}
}
}
@@ -269,7 +265,7 @@ internal static class GuideOptionPages
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Warning($"GroupVisible threw for {group.GetType().Name}: {e.Message}");
Warning($"GroupVisible threw for {group.GetType().Name}: {e.Message}");
return false;
}
}
@@ -282,7 +278,7 @@ internal static class GuideOptionPages
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Warning($"Visible threw for option '{option.Title}': {e.Message}");
Warning($"Visible threw for option '{option.Title}': {e.Message}");
return false;
}
}
@@ -292,51 +288,18 @@ internal static class GuideOptionPages
AbstractOptionGroup group,
string groupName,
string modAbbreviation,
IReadOnlyList<string> modSearchTerms,
Func<bool, string> formatBool)
IReadOnlyList<string> modSearchTerms)
{
try
{
var title = TranslationController.Instance.GetString(option.StringName);
var value = FormatValue(option, formatBool);
var value = option.GetDisplayValue();
return new GuideOptionEntry(title, value, group, groupName, modAbbreviation, modSearchTerms);
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Warning($"Could not format option '{option.Title}' for the guide: {e.Message}");
Warning($"Could not format option '{option.Title}' for the guide: {e.Message}");
return null;
}
}
private static string FormatValue(IModdedOption option, Func<bool, string> formatBool)
{
switch (option)
{
case ModdedToggleOption toggle:
return formatBool(toggle.Value);
case ModdedEnumOption enumOption:
return MiraLocaleManager.Get(
enumOption.Values[enumOption.Value],
enumOption.Values[enumOption.Value]);
case ModdedNumberOption number:
if (number is { NegativeWordValue: not "#", Value: -1 })
{
return number.NegativeWordValue;
}
if (number is { ZeroWordValue: not "#", Value: 0 })
{
return number.ZeroWordValue;
}
return TrailingZeroRegex.Replace(number.Data.GetValueString(number.Value), string.Empty);
default:
return option.Data.Type == OptionTypes.Checkbox
? formatBool(Math.Abs(option.GetFloatData() - 1f) < 0.0001f)
: option.Data.GetValueString(option.GetFloatData());
}
}
}
+35 -95
View File
@@ -5,13 +5,12 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Translation;
@@ -20,7 +19,6 @@ using Rewired;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using MethodBase = System.Reflection.MethodBase;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
@@ -34,7 +32,6 @@ internal readonly record struct AdvancedSettingsRow(string Title, string Value,
public static class Helpers
{
internal const float MatchInfoGuideBaseScale = 1.3f;
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
private static NumberOption? _advancedSettingsRowPrefab;
/// <summary>
@@ -57,111 +54,54 @@ public static class Helpers
public static string GetOptionsText(Type classType)
{
var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType);
var options = group?.Children;
if (group == null)
{
return string.Empty;
}
var headerAdded = false;
var options = group.Children;
var builder = new StringBuilder();
var summaryProvider = group as ISummarizedOptions;
var hiddenKeys = summaryProvider?.WikiHiddenOptionKeys;
if (options == null)
void EnsureHeader()
{
return string.Empty;
if (headerAdded)
return;
builder.AppendLine(MiraApiPlugin.Culture, $"\n<size=50%> \n</size><b><color=#29D7B5>{MiraLocaleManager.Get("Options")}</color></b>");
headerAdded = true;
}
if (summaryProvider != null)
{
foreach (var line in summaryProvider.GetWikiOptionSummaryLines())
{
if (string.IsNullOrWhiteSpace(line))
continue;
EnsureHeader();
builder.AppendLine(line);
}
}
var lines = new List<string>();
var insertedSummary = false;
foreach (var option in options)
{
StringNames? key = option switch
{
ModdedToggleOption t => t.StringName,
ModdedEnumOption e => e.StringName,
ModdedNumberOption n => n.StringName,
_ => null,
};
var key = option.StringName;
if (!insertedSummary && summaryProvider != null && hiddenKeys != null &&
key.HasValue && hiddenKeys.Contains(key.Value))
{
foreach (var line in summaryProvider.GetWikiOptionSummaryLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
lines.Add(line);
}
}
insertedSummary = true;
}
if (!option.Visible())
{
if (hiddenKeys?.Contains(key) == true || !option.Visible())
continue;
}
if (key.HasValue && hiddenKeys?.Contains(key.Value) == true)
{
continue;
}
EnsureHeader();
switch (option)
{
case ModdedToggleOption toggleOption:
lines.Add(
TranslationController.Instance.GetString(toggleOption.StringName) + ": " +
toggleOption.Value);
break;
/*case ModdedMultiSelectOption<Enum> enumOption:
if (!enumOption.Visible())
{
continue;
}
builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]);
break;*/
case ModdedEnumOption enumOption:
lines.Add(
TranslationController.Instance.GetString(enumOption.StringName) + ": " +
MiraLocaleManager.Get(
enumOption.Values[enumOption.Value],
enumOption.Values[enumOption.Value]));
break;
case ModdedNumberOption numberOption:
var optionStr = TrailingZeroRegex.Replace(
numberOption.Data.GetValueString(numberOption.Value),
string.Empty);
var title = TranslationController.Instance.GetString(numberOption.StringName);
if (numberOption is { NegativeWordValue: not "#", Value: -1 })
{
lines.Add(title + $": {numberOption.NegativeWordValue}");
}
else if (numberOption is { ZeroWordValue: not "#", Value: 0 })
{
lines.Add(title + $": {numberOption.ZeroWordValue}");
}
else
{
lines.Add(title + ": " + optionStr);
}
break;
}
builder.Append(TranslationController.Instance.GetString(key))
.Append(": ")
.AppendLine(option.GetDisplayValue());
}
if (lines.Count == 0)
{
return string.Empty;
}
var builder = new StringBuilder();
builder.AppendLine(
MiraApiPlugin.Culture,
$"\n<size=50%> \n</size><b><color=#29D7B5>{MiraLocaleManager.Get("Options")}</color></b>");
foreach (var line in lines)
{
builder.AppendLine(line);
}
return builder.ToString();
return builder.Length == 0 ? string.Empty : builder.ToString();
}
/// <summary>