mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-21 13:37:54 +00:00
Compare commits
172
Commits
id-builder
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e894a29a | ||
|
|
94affd8bf3 | ||
|
|
5021afeee7 | ||
|
|
6bfc49af00 | ||
|
|
7a4e891367 | ||
|
|
1667fa4541 | ||
|
|
fc7a1dc831 | ||
|
|
d90d3c0a28 | ||
|
|
6e48446894 | ||
|
|
0224ac95bc | ||
|
|
48ee06990d | ||
|
|
0ddbabadd6 | ||
|
|
12dee96200 | ||
|
|
d8162dcb32 | ||
|
|
5106efe2ac | ||
|
|
dc87c0fc7f | ||
|
|
6364f8eae6 | ||
|
|
3e421909de | ||
|
|
e4cb4a28df | ||
|
|
0f51eee1be | ||
|
|
0b85c9fe96 | ||
|
|
8fa6903a8d | ||
|
|
ca587ebb18 | ||
|
|
e0db03b576 | ||
|
|
aa69fafcf1 | ||
|
|
5e12d1f0d6 | ||
|
|
04b7701b79 | ||
|
|
5c51037503 | ||
|
|
da7162d60d | ||
|
|
602665079c | ||
|
|
39d77a301b | ||
|
|
5e9967606d | ||
|
|
a7585ee2fe | ||
|
|
b074fdc330 | ||
|
|
fb74a688c3 | ||
|
|
7ed3944ad1 | ||
|
|
f08a00ded7 | ||
|
|
f433454fe6 | ||
|
|
a31b9e29b9 | ||
|
|
bcacbad0e6 | ||
|
|
c1978bbb88 | ||
|
|
cac15bf07f | ||
|
|
1db6a80768 | ||
|
|
06ca48c088 | ||
|
|
7b77d0d102 | ||
|
|
901503f762 | ||
|
|
56cc7661c7 | ||
|
|
bceed03ac7 | ||
|
|
144c762ae3 | ||
|
|
5197b90360 | ||
|
|
542bf72ece | ||
|
|
754161d439 | ||
|
|
63c411754c | ||
|
|
5e863747f7 | ||
|
|
a01ff6168c | ||
|
|
75142dbcb8 | ||
|
|
6d40113ea0 | ||
|
|
e3358507bf | ||
|
|
5ec02eec9c | ||
|
|
1e453cf6af | ||
|
|
2748c64db2 | ||
|
|
1027e90447 | ||
|
|
60537fe1a3 | ||
|
|
50312e31c2 | ||
|
|
19dd1bb497 | ||
|
|
51efa74811 | ||
|
|
1c8f7318d3 | ||
|
|
8e4ff7c179 | ||
|
|
afcb4dc180 | ||
|
|
98aa9dc9a8 | ||
|
|
b6414fe5a0 | ||
|
|
6f36aa91a5 | ||
|
|
57471289e1 | ||
|
|
cc4daf5d8b | ||
|
|
3b336fb27c | ||
|
|
9da879fd61 | ||
|
|
503b7cec51 | ||
|
|
348c7c615b | ||
|
|
a07efcd268 | ||
|
|
613ebafa8a | ||
|
|
7349a259a1 | ||
|
|
a9dabe9ff4 | ||
|
|
5d354eff5e | ||
|
|
a6dab42df3 | ||
|
|
67e3a6a7bc | ||
|
|
3982867943 | ||
|
|
d5b08a7145 | ||
|
|
01b172d73e | ||
|
|
bdf9f4c9e8 | ||
|
|
362cfc02ff | ||
|
|
d3bce25ef6 | ||
|
|
317fd243b1 | ||
|
|
b6bddbd0df | ||
|
|
edadd4f350 | ||
|
|
c5431f0666 | ||
|
|
ec4a5f2bae | ||
|
|
abfbd8ee73 | ||
|
|
f0650a9841 | ||
|
|
43f1209c1c | ||
|
|
a90a0c4905 | ||
|
|
6410a0dd42 | ||
|
|
f5a994abc3 | ||
|
|
c4a5c638f7 | ||
|
|
5379f22364 | ||
|
|
cb292308c4 | ||
|
|
ff17c946a4 | ||
|
|
5c0e5473a5 | ||
|
|
b84c5b064c | ||
|
|
ffd42a4a91 | ||
|
|
b94f0aa63b | ||
|
|
5d9c38a503 | ||
|
|
9cefeebe2b | ||
|
|
a88f9b6611 | ||
|
|
034521b7b6 | ||
|
|
77366593f1 | ||
|
|
0903503782 | ||
|
|
a297898e6e | ||
|
|
c5c55745fd | ||
|
|
9e5b470ae6 | ||
|
|
d827e5d1f9 | ||
|
|
b00775e868 | ||
|
|
83722a81c8 | ||
|
|
a1d22b2fac | ||
|
|
eac86ca4eb | ||
|
|
e5c41913cb | ||
|
|
3b4b6e1800 | ||
|
|
7ee4d7da96 | ||
|
|
206b00c11f | ||
|
|
aec2cfcad3 | ||
|
|
0dfb65e9b6 | ||
|
|
3cf1963414 | ||
|
|
6d1557a48d | ||
|
|
5e6645ff3b | ||
|
|
a36f010207 | ||
|
|
7deaf5e963 | ||
|
|
d0138936be | ||
|
|
7cb8246953 | ||
|
|
9fd5b488b8 | ||
|
|
24407580d5 | ||
|
|
15c0002395 | ||
|
|
91c7ea8dda | ||
|
|
3bcc5139f3 | ||
|
|
b912f6ffa3 | ||
|
|
f0503a6d68 | ||
|
|
c2e251e9c6 | ||
|
|
43fa7a0a2a | ||
|
|
a27a45944c | ||
|
|
79dc3280e8 | ||
|
|
5303f09627 | ||
|
|
85f8da54dd | ||
|
|
2e212dee47 | ||
|
|
c2c7c63b9b | ||
|
|
f52c5de4cd | ||
|
|
cd753d554b | ||
|
|
f3ddf8d2ab | ||
|
|
ab7d43f18b | ||
|
|
9f4e519b9e | ||
|
|
27ac201f88 | ||
|
|
a73d1df67e | ||
|
|
24f71de1dd | ||
|
|
3bf066f38c | ||
|
|
44d476dd3a | ||
|
|
c1cd4a7d67 | ||
|
|
5fb60a7082 | ||
|
|
041c39d7ff | ||
|
|
2d4619f2cf | ||
|
|
1b587547f7 | ||
|
|
ffe9194d54 | ||
|
|
3575f308e4 | ||
|
|
0f8d5bc8ca | ||
|
|
1c35e93121 | ||
|
|
1ab662bcf1 |
No files matched your search
@@ -86,3 +86,21 @@ dotnet_diagnostic.SA1133.severity = none
|
||||
|
||||
# IDE0001: Simplify Names
|
||||
dotnet_diagnostic.IDE0001.severity = warning
|
||||
|
||||
# IDE0079: Remove unnecessary suppression
|
||||
dotnet_diagnostic.IDE0079.severity = none
|
||||
|
||||
# S3903: Types should be defined in named namespaces
|
||||
dotnet_diagnostic.S3903.severity = none
|
||||
|
||||
# IDE0045: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0045.severity = none
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0046.severity = none
|
||||
|
||||
# S1244: Floating point numbers should not be tested for equality
|
||||
dotnet_diagnostic.S1244.severity = none
|
||||
|
||||
# S2325: Methods and properties that don't access instance data should be static
|
||||
dotnet_diagnostic.S2325.severity = none
|
||||
@@ -5,6 +5,8 @@ on: ["workflow_dispatch", "push", "pull_request"]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -38,14 +40,21 @@ 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 ${{ secrets.NUGET_API_KEY }}
|
||||
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 }}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
|
||||
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
|
||||
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.8.18" PrivateAssets="all" />
|
||||
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.9.29" PrivateAssets="all" />
|
||||
|
||||
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[*.{cs,vb}]
|
||||
|
||||
# SA1602: Enumeration items should be documented.
|
||||
dotnet_diagnostic.SA1602.severity = none
|
||||
|
||||
# SA1601: Partial elements should be documented
|
||||
dotnet_diagnostic.SA1601.severity = none
|
||||
@@ -1,4 +1,5 @@
|
||||
using MiraAPI.Hud;
|
||||
using MiraAPI.Keybinds;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using UnityEngine;
|
||||
@@ -13,6 +14,8 @@ public class ThinkButton : CustomActionButton
|
||||
public override int MaxUses => 1;
|
||||
public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound;
|
||||
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
|
||||
public override MiraKeybind Keybind => MiraGlobalKeybinds.SecondaryAbility;
|
||||
|
||||
protected override void OnClick()
|
||||
{
|
||||
Button!.OverrideText("button.think.thinking".Translate());
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using MiraAPI.Colors;
|
||||
using MiraAPI.Colors.ColorSpaces;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example;
|
||||
@@ -14,4 +16,9 @@ public static class ExampleColors
|
||||
public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f));
|
||||
|
||||
public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f));
|
||||
|
||||
public static VariableColor Rainbow { get; } = new("color.rainbow", t => new HsbColor(MathUtilities.PingPong(t, 1f, 0.3f), 1f, 1f))
|
||||
{
|
||||
ColorBrightness = CustomColorBrightness.Lighter,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using MiraAPI.Events;
|
||||
using MiraAPI.Events.Mira;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MiraAPI.Keybinds;
|
||||
using MiraAPI.Keybinds;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Example;
|
||||
@@ -6,5 +6,5 @@ namespace MiraAPI.Example;
|
||||
[RegisterCustomKeybinds]
|
||||
public static class ExampleKeybinds
|
||||
{
|
||||
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]);
|
||||
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, ControllerButton.RightStick, [ModifierKey.Shift]);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.Example.Roles;
|
||||
using MiraAPI.Example.Roles;
|
||||
using MiraAPI.Modifiers.Types;
|
||||
using MiraAPI.Translation;
|
||||
using UnityEngine;
|
||||
@@ -28,7 +27,7 @@ public class FreezeModifier : TimedModifier
|
||||
|
||||
if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
|
||||
{
|
||||
Player.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
|
||||
Player.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(Palette.LightBlue));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.Attributes;
|
||||
using MiraAPI.Utilities;
|
||||
@@ -6,33 +6,74 @@ using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example.Options;
|
||||
|
||||
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Demo.")]
|
||||
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Read above.")]
|
||||
[SuppressMessage("Minor Code Smell", "S3400:Methods should not return constants", Justification = "Read above.")]
|
||||
public class ExampleOptions : AbstractOptionGroup
|
||||
{
|
||||
public override string GroupName => "options.exampleOptions1";
|
||||
public override Color GroupColor => Color.green;
|
||||
public override bool ShowInMatchInfoGuide => true;
|
||||
|
||||
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
|
||||
public bool ToggleOpt { get; set; } = false;
|
||||
[ModdedOptionVisibility] // Automatically resolves to using ToggleOpt1Visibility, can also target toggle options
|
||||
public bool ToggleOpt1 { get; set; } = false;
|
||||
|
||||
[ModdedToggleOption("options.exampleOptions1.toggleOpt1")]
|
||||
[ModdedToggleOption("options.exampleOptions1.toggleOpt2")]
|
||||
[ModdedOptionVisibility(nameof(ToggleOpt1))] // ToggleOpt2 will be visible only when ToggleOpt1 is true
|
||||
public bool ToggleOpt2 { get; set; } = true;
|
||||
|
||||
[ModdedToggleOption("Toggle Opt 3")]
|
||||
[ModdedOptionVisibility(nameof(ToggleOpt2))] // ToggleOpt3 will be visible only when ToggleOpt2 is true
|
||||
public bool ToggleOpt3 { get; set; } = true;
|
||||
|
||||
[ModdedNumberOption("options.exampleOptions1.numberOpt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)]
|
||||
[ModdedOptionVisibility(nameof(NumberOptVisibility))] // Can also target static methods
|
||||
public float NumberOpt { get; set; } = 4f;
|
||||
|
||||
[ModdedEnumOption("options.exampleOptions1.bestApi", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
|
||||
public BestApi Opt { get; set; } = BestApi.MiraAPI;
|
||||
[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;
|
||||
|
||||
[ModdedEnumOptionList<ValueState>("Value State {0}", ["None", "Primary", "Secondary"])]
|
||||
public List<ValueState> ValueStates { get; } =
|
||||
[ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
|
||||
public ModdedArray<ValueState> ValueStates { get; set; } = [ValueState.Primary, ValueState.Secondary, ValueState.None, ValueState.None, ValueState.None];
|
||||
|
||||
[ModdedPlayerOption("options.exampleOptions1.players")]
|
||||
public NetworkedPlayerInfo PlayerSelected { get; set; }
|
||||
|
||||
public bool ToggleOpt1Visibility()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool NumberOptVisibility()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Api GetApi(Api currentValue)
|
||||
{
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
public static Api SetApi(Api newValue, Api oldValue)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public static void ApiChanged(Api currentValue)
|
||||
{
|
||||
// Something
|
||||
}
|
||||
}
|
||||
|
||||
public enum BestApi
|
||||
public enum Api
|
||||
{
|
||||
MiraAPI,
|
||||
Mitochondria,
|
||||
|
||||
@@ -7,10 +7,9 @@ namespace MiraAPI.Example.Options;
|
||||
public class ExampleOptions2 : AbstractOptionGroup
|
||||
{
|
||||
public override string GroupName => "options.exampleOptions2";
|
||||
|
||||
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
|
||||
|
||||
public override MenuCategory ParentMenu => MenuCategory.CustomTwo;
|
||||
public override bool ShowInMatchInfoGuide => true;
|
||||
|
||||
public ModdedPlayerOption PlayerOption2 { get; } = new("options.exampleOptions2.youMustChoseAPlayer", false);
|
||||
public ModdedPlayerOption PlayerOption { get; } = new("options.exampleOptions2.ehWhatever");
|
||||
@@ -27,6 +26,8 @@ public class ExampleOptions2 : AbstractOptionGroup
|
||||
public ModdedEnumOption<TestingData> EnumOpt { get; } = new("options.exampleOptions2.enumOpt", 0)
|
||||
{
|
||||
ChangedEvent = x => Logger<ExamplePlugin>.Info($"changed Enum Opt to {x}"),
|
||||
Getter = currentValue => currentValue,
|
||||
Setter = (newValue, oldValue) => newValue,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
Your votes have double the power.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Mayor.TabDescription">You get one extra vote in the meeting!</string>
|
||||
<string name="ApiExample.Role.Crewmate.Mayor.WikiDescription">The Mayor is a Crewmate role that gets two votes in the meeting, instead of the singular vote other Crewmates get.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chamelon">Chamelon</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chamelon.IntroBlurb">Become one with the environment.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chamelon.MedDescription">Fade out of everyone's vision by not moving.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chamelon.TabDescription">Stay invisible while not moving.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chamelon.WikiDescription">The Chamelon is a Crewmate role that slowly becomes invisible while standing still.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chameleon">Chameleon</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chameleon.IntroBlurb">Become one with the environment.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chameleon.MedDescription">Fade out of everyone's vision by not moving.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chameleon.TabDescription">Stay invisible while not moving.</string>
|
||||
<string name="ApiExample.Role.Crewmate.Chameleon.WikiDescription">The Chameleon is a Crewmate role that slowly becomes invisible while standing still.</string>
|
||||
<string name="ApiExample.Role.Outcast.OutcastKiller">Outcast Killer</string>
|
||||
<string name="ApiExample.Role.Outcast.OutcastKiller.IntroBlurb">Kill everyone else.</string>
|
||||
<string name="ApiExample.Role.Outcast.OutcastKiller.MedDescription">Be the last outcast standing.
|
||||
@@ -47,6 +47,8 @@ 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>
|
||||
<string name="options.exampleOptions2.ehWhatever">Eh whatever</string>
|
||||
@@ -73,11 +75,12 @@ Eliminate every other faction.</string>
|
||||
<string name="settings.miraExample.exampleBool2">Example Bool 2</string>
|
||||
<string name="settings.miraExample.slider">Slider</string>
|
||||
<string name="settings.miraExample.exampleNumber">Example Number</string>
|
||||
<string name="settings.miraExample.exampleMultiplier">Example Multplier</string>
|
||||
<string name="settings.miraExample.exampleMultiplier">Example Multiplier</string>
|
||||
<string name="settings.miraExample.exampleEnum">Example Enum</string>
|
||||
<string name="settings.miraExample.button.exampleButton">Example Button</string>
|
||||
<string name="gameover.outcastKiller.win">Outcast Killer Wins!</string>
|
||||
<string name="color.cerulean">Cerulean</string>
|
||||
<string name="color.rose">Rose</string>
|
||||
<string name="color.gold">Gold</string>
|
||||
<string name="color.rainbow">Rainbow</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Colors.ColorSpaces;
|
||||
|
||||
/// <summary>
|
||||
/// A struct that denotes the CMYK (Cyan, Magenta, Yellow, Key) color space.
|
||||
/// </summary>
|
||||
/// <param name="c">The cyan component.</param>
|
||||
/// <param name="m">The magenta component.</param>
|
||||
/// <param name="y">The yellow component.</param>
|
||||
/// <param name="k">The key component.</param>
|
||||
/// <param name="a">The alpha component.</param>
|
||||
[Serializable]
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
|
||||
public record struct CmykColor(float c, float m, float y, float k, float a = 1f)
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicitly converts a <see cref="CmykColor"/> to a Unity <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator Color(CmykColor c)
|
||||
{
|
||||
return ToColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a Unity <see cref="Color"/> to a <see cref="CmykColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator CmykColor(Color c)
|
||||
{
|
||||
return FromColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="CmykColor"/> to a standard Unity RGB <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="cmyk">The CMYK color to convert.</param>
|
||||
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
|
||||
public static Color ToColor(CmykColor cmyk)
|
||||
{
|
||||
var key = 1f - cmyk.k;
|
||||
|
||||
var r = (1f - cmyk.c) * key;
|
||||
var g = (1f - cmyk.m) * key;
|
||||
var b = (1f - cmyk.y) * key;
|
||||
|
||||
return new Color(MathUtilities.Clamp01(r), MathUtilities.Clamp01(g), MathUtilities.Clamp01(b), cmyk.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard Unity RGB <see cref="Color"/> to a <see cref="CmykColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="color">The Unity Color to convert.</param>
|
||||
/// <returns>A <see cref="CmykColor"/> representing the same visual color.</returns>
|
||||
public static CmykColor FromColor(Color color)
|
||||
{
|
||||
var r = color.r;
|
||||
var g = color.g;
|
||||
var b = color.b;
|
||||
|
||||
var k = 1f - MathF.Max(r, MathF.Max(g, b));
|
||||
|
||||
if (k >= 1f)
|
||||
return new CmykColor(0f, 0f, 0f, 1f, color.a);
|
||||
|
||||
var invK = 1f / (1f - k);
|
||||
|
||||
var c = (1f - r - k) * invK;
|
||||
var m = (1f - g - k) * invK;
|
||||
var y = (1f - b - k) * invK;
|
||||
|
||||
return new CmykColor(MathUtilities.Clamp01(c), MathUtilities.Clamp01(m), MathUtilities.Clamp01(y), MathUtilities.Clamp01(k), color.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="CmykColor"/> values.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
|
||||
/// <returns>The interpolated <see cref="CmykColor"/>.</returns>
|
||||
public static CmykColor Lerp(CmykColor a, CmykColor b, float t)
|
||||
{
|
||||
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="CmykColor"/> values without clamping the interpolant.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors.</param>
|
||||
/// <returns>The interpolated <see cref="CmykColor"/>.</returns>
|
||||
public static CmykColor LerpUnclamped(CmykColor a, CmykColor b, float t)
|
||||
{
|
||||
return new CmykColor(
|
||||
MathUtilities.LerpUnclamped(a.c, b.c, t),
|
||||
MathUtilities.LerpUnclamped(a.m, b.m, t),
|
||||
MathUtilities.LerpUnclamped(a.y, b.y, t),
|
||||
MathUtilities.LerpUnclamped(a.k, b.k, t),
|
||||
MathUtilities.LerpUnclamped(a.a, b.a, t));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Colors.ColorSpaces;
|
||||
|
||||
/// <summary>
|
||||
/// A struct that denotes the HSB (Hue, Saturation, Brightness) color space.
|
||||
/// </summary>
|
||||
/// <param name="h">The hue component.</param>
|
||||
/// <param name="s">The saturation component.</param>
|
||||
/// <param name="b">The brightness component.</param>
|
||||
/// <param name="a">The alpha component.</param>
|
||||
[Serializable]
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
|
||||
public record struct HsbColor(float h, float s, float b, float a = 1f)
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicitly converts an <see cref="HsbColor"/> to a Unity <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator Color(HsbColor c)
|
||||
{
|
||||
return ToColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a Unity <see cref="Color"/> to an <see cref="HsbColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator HsbColor(Color c)
|
||||
{
|
||||
return FromColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an <see cref="HsbColor"/> to a standard Unity RGB <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="hsb">The HSB color to convert.</param>
|
||||
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
|
||||
public static Color ToColor(HsbColor hsb)
|
||||
{
|
||||
if (hsb.s <= float.Epsilon)
|
||||
return new Color(MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), hsb.a);
|
||||
|
||||
var c = hsb.b * hsb.s;
|
||||
var hPrime = hsb.h * 6f;
|
||||
var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f));
|
||||
var m = hsb.b - c;
|
||||
|
||||
var (r, g, b) = hPrime switch
|
||||
{
|
||||
< 1f => (c, x, 0f),
|
||||
< 2f => (x, c, 0f),
|
||||
< 3f => (0f, c, x),
|
||||
< 4f => (0f, x, c),
|
||||
< 5f => (x, 0f, c),
|
||||
<= 6f => (c, 0f, x),
|
||||
_ => (0f, 0f, 0f),
|
||||
};
|
||||
|
||||
return new Color(
|
||||
MathUtilities.Clamp01(r + m),
|
||||
MathUtilities.Clamp01(g + m),
|
||||
MathUtilities.Clamp01(b + m),
|
||||
hsb.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard Unity RGB <see cref="Color"/> to an <see cref="HsbColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="color">The Unity Color to convert.</param>
|
||||
/// <returns>An <see cref="HsbColor"/> representing the same visual color.</returns>
|
||||
public static HsbColor FromColor(Color color)
|
||||
{
|
||||
var max = MathF.Max(color.r, MathF.Max(color.g, color.b));
|
||||
var min = MathF.Min(color.r, MathF.Min(color.g, color.b));
|
||||
var delta = max - min;
|
||||
|
||||
var h = 0f;
|
||||
|
||||
if (delta > 0f)
|
||||
{
|
||||
if (max == color.r)
|
||||
h = (color.g - color.b) / delta % 6f;
|
||||
else if (max == color.g)
|
||||
h = (color.b - color.r) / delta + 2f;
|
||||
else
|
||||
h = (color.r - color.g) / delta + 4f;
|
||||
|
||||
h /= 6f;
|
||||
|
||||
if (h < 0f)
|
||||
h += 1f;
|
||||
}
|
||||
|
||||
var s = max == 0f ? 0f : delta / max;
|
||||
var b = max;
|
||||
|
||||
return new HsbColor(h, s, b, color.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="HsbColor"/> values, using the shortest path for the hue.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
|
||||
/// <returns>The interpolated <see cref="HsbColor"/>.</returns>
|
||||
public static HsbColor Lerp(HsbColor a, HsbColor b, float t)
|
||||
{
|
||||
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="HsbColor"/> values without clamping the interpolant.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors.</param>
|
||||
/// <returns>The interpolated <see cref="HsbColor"/>.</returns>
|
||||
public static HsbColor LerpUnclamped(HsbColor a, HsbColor b, float t)
|
||||
{
|
||||
var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f);
|
||||
|
||||
if (hueDifference > 0.5f)
|
||||
hueDifference -= 1f;
|
||||
|
||||
return new HsbColor(
|
||||
MathUtilities.Repeat(a.h + (hueDifference * t), 1f),
|
||||
MathUtilities.LerpUnclamped(a.s, b.s, t),
|
||||
MathUtilities.LerpUnclamped(a.b, b.b, t),
|
||||
MathUtilities.LerpUnclamped(a.a, b.a, t));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Colors.ColorSpaces;
|
||||
|
||||
/// <summary>
|
||||
/// A struct that denotes the HSL (Hue, Saturation, Lightness) color space.
|
||||
/// </summary>
|
||||
/// <param name="h">The hue component.</param>
|
||||
/// <param name="s">The saturation component.</param>
|
||||
/// <param name="l">The lightness component.</param>
|
||||
/// <param name="a">The alpha component.</param>
|
||||
[Serializable]
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
|
||||
public record struct HslColor(float h, float s, float l, float a = 1f)
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicitly converts an <see cref="HslColor"/> to a Unity <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator Color(HslColor c)
|
||||
{
|
||||
return ToColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts a Unity <see cref="Color"/> to an <see cref="HslColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The color being converted.</param>
|
||||
public static implicit operator HslColor(Color c)
|
||||
{
|
||||
return FromColor(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an <see cref="HslColor"/> to a standard Unity RGB <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="hsl">The HSL color to convert.</param>
|
||||
/// <returns>A Unity <see cref="Color"/> representing the same visual color.</returns>
|
||||
public static Color ToColor(HslColor hsl)
|
||||
{
|
||||
if (hsl.s <= float.Epsilon)
|
||||
return new Color(MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), hsl.a);
|
||||
|
||||
var c = (1f - MathF.Abs(2f * hsl.l - 1f)) * hsl.s;
|
||||
var hPrime = hsl.h * 6f;
|
||||
var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f));
|
||||
var m = hsl.l - c / 2f;
|
||||
|
||||
var (r, g, b) = hPrime switch
|
||||
{
|
||||
< 1f => (c, x, 0f),
|
||||
< 2f => (x, c, 0f),
|
||||
< 3f => (0f, c, x),
|
||||
< 4f => (0f, x, c),
|
||||
< 5f => (x, 0f, c),
|
||||
<= 6f => (c, 0f, x),
|
||||
_ => (0f, 0f, 0f),
|
||||
};
|
||||
|
||||
return new Color(MathUtilities.Clamp01(r + m), MathUtilities.Clamp01(g + m), MathUtilities.Clamp01(b + m), hsl.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard Unity RGB <see cref="Color"/> to an <see cref="HslColor"/>.
|
||||
/// </summary>
|
||||
/// <param name="color">The Unity Color to convert.</param>
|
||||
/// <returns>An <see cref="HslColor"/> representing the same visual color.</returns>
|
||||
public static HslColor FromColor(Color color)
|
||||
{
|
||||
var max = MathF.Max(color.r, MathF.Max(color.g, color.b));
|
||||
var min = MathF.Min(color.r, MathF.Min(color.g, color.b));
|
||||
var l = (max + min) / 2f;
|
||||
var delta = max - min;
|
||||
|
||||
var s = delta == 0f ? 0f : delta / (1f - MathF.Abs((2f * l) - 1f));
|
||||
|
||||
var h = 0f;
|
||||
|
||||
if (delta > 0f)
|
||||
{
|
||||
if (max == color.r)
|
||||
h = (color.g - color.b) / delta % 6f;
|
||||
else if (max == color.g)
|
||||
h = (color.b - color.r) / delta + 2f;
|
||||
else
|
||||
h = (color.r - color.g) / delta + 4f;
|
||||
|
||||
h /= 6f;
|
||||
|
||||
if (h < 0f)
|
||||
h += 1f;
|
||||
}
|
||||
|
||||
return new HslColor(h, s, l, color.a);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="HslColor"/> values, using the shortest path for the hue.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors, clamped to a 0 to 1 range.</param>
|
||||
/// <returns>The interpolated <see cref="HslColor"/>.</returns>
|
||||
public static HslColor Lerp(HslColor a, HslColor b, float t)
|
||||
{
|
||||
return LerpUnclamped(a, b, MathUtilities.Clamp01(t));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolates between two <see cref="HslColor"/> values without clamping the interpolant.
|
||||
/// </summary>
|
||||
/// <param name="a">The starting color.</param>
|
||||
/// <param name="b">The ending color.</param>
|
||||
/// <param name="t">The interpolation value between the two colors.</param>
|
||||
/// <returns>The interpolated <see cref="HslColor"/>.</returns>
|
||||
public static HslColor LerpUnclamped(HslColor a, HslColor b, float t)
|
||||
{
|
||||
var hueDifference = MathUtilities.Repeat(b.h - a.h, 1f);
|
||||
|
||||
if (hueDifference > 0.5f)
|
||||
hueDifference -= 1f;
|
||||
|
||||
return new HslColor(
|
||||
MathUtilities.Repeat(a.h + (hueDifference * t), 1f),
|
||||
MathUtilities.LerpUnclamped(a.s, b.s, t),
|
||||
MathUtilities.LerpUnclamped(a.l, b.l, t),
|
||||
MathUtilities.LerpUnclamped(a.a, b.a, t));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using MiraAPI.Translation;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -10,17 +11,37 @@ namespace MiraAPI.Colors;
|
||||
/// <param name="name">The name of the option.</param>
|
||||
/// <param name="mainColor">The main color.</param>
|
||||
/// <param name="shadowColor">The shadow color.</param>
|
||||
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal code.")]
|
||||
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")]
|
||||
public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the main color.
|
||||
/// </summary>
|
||||
public Color32 MainColor { get; set; } = mainColor;
|
||||
public Color32 MainColor
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
InternalMainColor = value;
|
||||
}
|
||||
}
|
||||
= mainColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the shadow color.
|
||||
/// </summary>
|
||||
public Color32 ShadowColor { get; set; } = shadowColor;
|
||||
public Color32 ShadowColor
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
InternalShadowColor = value;
|
||||
}
|
||||
}
|
||||
= shadowColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the color.
|
||||
@@ -32,6 +53,9 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
|
||||
/// </summary>
|
||||
public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
|
||||
|
||||
internal Color InternalMainColor = mainColor;
|
||||
internal Color InternalShadowColor = shadowColor;
|
||||
|
||||
/// <inheritdoc cref="CustomColor"/>
|
||||
public CustomColor(StringNames name, Color32 mainColor)
|
||||
: this(name, mainColor, mainColor.GetShadowColor(60))
|
||||
@@ -56,6 +80,11 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
|
||||
/// </summary>
|
||||
public enum CustomColorBrightness
|
||||
{
|
||||
/// <summary>
|
||||
/// An invalid color.
|
||||
/// </summary>
|
||||
Invalid = -1,
|
||||
|
||||
/// <summary>
|
||||
/// A lighter color.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Colors;
|
||||
|
||||
@@ -15,9 +17,36 @@ public static class PaletteManager
|
||||
public static CustomColor[] RegisteredColors => [.. CustomColors];
|
||||
|
||||
internal static readonly List<CustomColor> CustomColors = [];
|
||||
internal static readonly HashSet<StringNames> VariableColorNames = [];
|
||||
|
||||
private static readonly CustomColorBrightness[] ColorBrightnessMap =
|
||||
[
|
||||
CustomColorBrightness.Darker, // Red
|
||||
CustomColorBrightness.Darker, // Blue
|
||||
CustomColorBrightness.Darker, // Green
|
||||
CustomColorBrightness.Lighter, // Pink
|
||||
CustomColorBrightness.Lighter, // Orange
|
||||
CustomColorBrightness.Lighter, // Yellow
|
||||
CustomColorBrightness.Darker, // Black
|
||||
CustomColorBrightness.Lighter, // White
|
||||
CustomColorBrightness.Darker, // Purple
|
||||
CustomColorBrightness.Darker, // Brown
|
||||
CustomColorBrightness.Lighter, // Cyan
|
||||
CustomColorBrightness.Lighter, // Lime
|
||||
CustomColorBrightness.Darker, // Maroon
|
||||
CustomColorBrightness.Lighter, // Rose
|
||||
CustomColorBrightness.Lighter, // Banana
|
||||
CustomColorBrightness.Darker, // Gray
|
||||
CustomColorBrightness.Darker, // Tan
|
||||
CustomColorBrightness.Lighter, // Coral
|
||||
];
|
||||
|
||||
private static int originalLength;
|
||||
|
||||
internal static void RegisterAllColors()
|
||||
{
|
||||
originalLength = Palette.PlayerColors.Length;
|
||||
|
||||
var colors = CustomColors.Select(x => x.MainColor).ToArray();
|
||||
var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray();
|
||||
var stringNames = CustomColors.Select(x => x.Name).ToArray();
|
||||
@@ -29,4 +58,90 @@ public static class PaletteManager
|
||||
Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors);
|
||||
Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the provided color ID belongs to a variable color.
|
||||
/// </summary>
|
||||
/// <param name="colorId">The id of the color.</param>
|
||||
/// <returns><c>true</c> if the color is a variable color; <c>false</c> otherwise.</returns>
|
||||
public static bool IsVariable(int colorId)
|
||||
{
|
||||
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
|
||||
return false;
|
||||
|
||||
return VariableColorNames.Contains(Palette.ColorNames[colorId]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the provided color ID belongs to a custom color.
|
||||
/// </summary>
|
||||
/// <param name="colorId">The ID of the color.</param>
|
||||
/// <returns><c>true</c> if the color is a custom color; <c>false</c> otherwise.</returns>
|
||||
public static bool IsCustomColor(int colorId)
|
||||
{
|
||||
return (uint)(colorId - originalLength) < (uint)CustomColors.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main color of the provided color ID.
|
||||
/// </summary>
|
||||
/// <param name="colorId">The id of the color.</param>
|
||||
/// <returns>The main color associated with the provided ID.</returns>
|
||||
public static Color GetMainColor(int colorId)
|
||||
{
|
||||
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
|
||||
return Color.white;
|
||||
|
||||
return !TryGetCustomColor(colorId, out var custom)
|
||||
? Palette.PlayerColors[colorId]
|
||||
: (custom is VariableColor variable
|
||||
? variable.EvaluateMainColor(Time.time)
|
||||
: custom.InternalMainColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shadow color of the provided color ID.
|
||||
/// </summary>
|
||||
/// <param name="colorId">The ID of the color.</param>
|
||||
/// <returns>The shadow color associated with the provided ID.</returns>
|
||||
public static Color GetShadowColor(int colorId)
|
||||
{
|
||||
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
|
||||
return Color.white;
|
||||
|
||||
return !TryGetCustomColor(colorId, out var custom)
|
||||
? Palette.ShadowColors[colorId]
|
||||
: (custom is VariableColor variable
|
||||
? variable.EvaluateShadowColor(Time.time)
|
||||
: custom.InternalShadowColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brightness type of the color associated with the provided ID.
|
||||
/// </summary>
|
||||
/// <param name="colorId">The ID of the color.</param>
|
||||
/// <returns>The brightness type of the provided ID.</returns>
|
||||
public static CustomColorBrightness GetBrightness(int colorId)
|
||||
{
|
||||
if ((uint)colorId >= (uint)Palette.ColorNames.Length)
|
||||
return CustomColorBrightness.Invalid;
|
||||
|
||||
return TryGetCustomColor(colorId, out var custom)
|
||||
? custom.ColorBrightness
|
||||
: ColorBrightnessMap[colorId];
|
||||
}
|
||||
|
||||
internal static bool TryGetCustomColor(int colorId, [NotNullWhen(true)] out CustomColor? custom)
|
||||
{
|
||||
var index = colorId - originalLength;
|
||||
|
||||
if ((uint)index >= (uint)CustomColors.Count)
|
||||
{
|
||||
custom = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
custom = CustomColors[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using MiraAPI.LocalSettings;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Colors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a dynamic custom color that changes over time.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the color.</param>
|
||||
/// <param name="mainEvaluator">The function to evaluate the dynamic main color.</param>
|
||||
/// <param name="shadowEvaluator">The function to evaluate the dynamic shadow color.</param>
|
||||
/// <param name="mainColor">The static main color used when dynamic colors are disabled.</param>
|
||||
/// <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))
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate that takes in a time value and returns the dynamic main color.
|
||||
/// </summary>
|
||||
public Func<float, Color> MainEvaluator { get; set; } = mainEvaluator;
|
||||
|
||||
/// <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));
|
||||
|
||||
/// <inheritdoc cref="VariableColor"/>
|
||||
public VariableColor(StringNames name, Func<float, Color> mainEvaluator, Color32? mainColor = null, float defaultTime = 0f)
|
||||
: this(name, mainEvaluator, null, mainColor, null, defaultTime)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="VariableColor"/>
|
||||
public VariableColor(string name, Func<float, Color> mainEvaluator, Color32? mainColor = null, float defaultTime = 0f)
|
||||
: this(name, mainEvaluator, null, mainColor, null, defaultTime)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="VariableColor"/>
|
||||
public VariableColor(string name, Func<float, Color> mainEvaluator, Func<float, Color>? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f)
|
||||
: this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor, defaultTime)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the dynamic main color based on the provided time value.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to evaluate the color against.</param>
|
||||
/// <returns>The dynamically evaluated color, or the static <see cref="CustomColor.MainColor"/> if static variable colors are enabled in the user's settings.</returns>
|
||||
public Color EvaluateMainColor(float time)
|
||||
{
|
||||
return LocalSettingsTabSingleton<MiraApiSettings>.Instance.EnableVariableColors.Value
|
||||
? MainEvaluator(time)
|
||||
: InternalMainColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the dynamic shadow color based on the provided time value.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to evaluate the color against.</param>
|
||||
/// <returns>The dynamically evaluated color, or the static <see cref="CustomColor.ShadowColor"/> if static variable colors are enabled in the user's settings.</returns>
|
||||
public Color EvaluateShadowColor(float time)
|
||||
{
|
||||
return LocalSettingsTabSingleton<MiraApiSettings>.Instance.EnableVariableColors.Value
|
||||
? ShadowEvaluator(time)
|
||||
: InternalShadowColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Il2CppInterop.Runtime.Attributes;
|
||||
using MiraAPI.Colors;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Attributes;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI;
|
||||
|
||||
/// <summary>
|
||||
/// The base class for variable color behaviour on <see cref="SpriteRenderer"/>s.
|
||||
/// </summary>
|
||||
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
|
||||
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity fields.")]
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||
public abstract class BaseVariableColorBehaviour : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="VariableColor"/> to apply.
|
||||
/// </summary>
|
||||
protected VariableColor? color;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current variable color.
|
||||
/// </summary>
|
||||
/// <param name="newColor">The new <see cref="VariableColor"/>.</param>
|
||||
[HideFromIl2Cpp]
|
||||
public void SetColor(VariableColor? newColor)
|
||||
{
|
||||
color = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mono script that adjusts the material colors of a <see cref="Renderer"/> component that it's attached to based on the provided variable color.
|
||||
/// </summary>
|
||||
[RegisterInIl2Cpp]
|
||||
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
|
||||
public sealed class VariableColorBehaviour : BaseVariableColorBehaviour
|
||||
{
|
||||
private Renderer? rend;
|
||||
|
||||
private static readonly Color VisorColor = Palette.VisorColor;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the new <see cref="Renderer"/> target.
|
||||
/// </summary>
|
||||
/// <param name="newRend">The new <see cref="Renderer"/> that the script must target.</param>
|
||||
public void SetRend(Renderer? newRend)
|
||||
{
|
||||
rend = newRend;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rend = GetComponent<Renderer>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (color == null || rend == null)
|
||||
return;
|
||||
|
||||
var time = Time.time;
|
||||
var material = rend.material;
|
||||
|
||||
material.SetColor(ShaderID.VisorColor, VisorColor);
|
||||
|
||||
material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor(time));
|
||||
material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor(time));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mono script that adjusts the graphic colors of a <see cref="SpriteRenderer"/> component that it's attached to based on the provided variable color.
|
||||
/// </summary>
|
||||
[RegisterInIl2Cpp]
|
||||
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
|
||||
public sealed class BasicVariableColorBehaviour : BaseVariableColorBehaviour
|
||||
{
|
||||
private SpriteRenderer? rend;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the new <see cref="SpriteRenderer"/> target.
|
||||
/// </summary>
|
||||
/// <param name="newRend">The new <see cref="SpriteRenderer"/> that the script must target.</param>
|
||||
public void SetRend(SpriteRenderer? newRend)
|
||||
{
|
||||
rend = newRend;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rend = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (color != null && rend != null)
|
||||
rend.color = color.EvaluateMainColor(Time.time);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.Patches;
|
||||
using MiraAPI.Patches.GameModes;
|
||||
using MiraAPI.Patches.Options;
|
||||
using MiraAPI.Translation;
|
||||
@@ -39,7 +40,7 @@ public static class GameModeOption
|
||||
}
|
||||
}
|
||||
|
||||
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("Gamemode");
|
||||
internal static readonly StringNames GamemodeName = MiraLocaleManager.GetOrCreateLocaleString("MiraApi.Gamemode");
|
||||
internal static readonly StringNames CustomName = MiraLocaleManager.GetOrCreateLocaleString("Custom");
|
||||
internal static readonly Dictionary<uint, StringNames> Values = new()
|
||||
{
|
||||
@@ -128,6 +129,9 @@ public static class GameModeOption
|
||||
MenuState.Instance.QueuedRoleMenuRefresh[roleMenu.Key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
GameOptionsMenuPatch.RefreshForGameMode();
|
||||
RoleGuidePatches.RefreshSettingsForGameMode();
|
||||
}
|
||||
|
||||
// TODO: could make Values a dict of AbstractGameMode too
|
||||
@@ -135,7 +139,7 @@ public static class GameModeOption
|
||||
|
||||
[HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.ValueChanged))]
|
||||
[HarmonyPrefix]
|
||||
private static bool ValueChanged(GameOptionsMenu __instance, OptionBehaviour option)
|
||||
private static bool ValueChanged(OptionBehaviour option)
|
||||
{
|
||||
if (!OptionBehaviour.Equals(option))
|
||||
return true;
|
||||
@@ -143,12 +147,6 @@ public static class GameModeOption
|
||||
Info($"Game mode changed to {option.GetInt()}");
|
||||
RpcSyncGamemode(PlayerControl.LocalPlayer, option.GetInt());
|
||||
|
||||
if (GameSettingMenu.Instance && CustomGameModeManager.ActiveMode != null)
|
||||
{
|
||||
GameOptionsMenuPatch.ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, __instance);
|
||||
GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using AmongUs.Data;
|
||||
@@ -462,7 +461,6 @@ public class HideAndSeekMode : AbstractGameMode
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")]
|
||||
public override PlayerBodyTypes GetBodyType(PlayerControl player)
|
||||
{
|
||||
if (player == null || player.Data == null || player.Data.Role == null)
|
||||
|
||||
@@ -3,8 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Il2CppInterop.Runtime.Attributes;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.GameOptions;
|
||||
@@ -26,6 +29,12 @@ public abstract class AbstractOptionGroup
|
||||
/// </summary>
|
||||
public abstract string GroupName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sprite of the group that is displayed in the match info guide.
|
||||
/// </summary>
|
||||
[HideFromIl2Cpp]
|
||||
public virtual LoadableAsset<Sprite> Sprite => MiraAssets.IconSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Optionable type of the group.
|
||||
/// </summary>
|
||||
@@ -47,7 +56,43 @@ public abstract class AbstractOptionGroup
|
||||
/// <summary>
|
||||
/// Gets the function that determines whether the group should be visible or not.
|
||||
/// </summary>
|
||||
public virtual Func<bool> GroupVisible => () => true;
|
||||
public virtual Func<bool> GroupVisible => DelegateUtilities.AlwaysTrue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this group's options are shown in the Match Info Guide.
|
||||
/// </summary>
|
||||
public virtual bool ShowInMatchInfoGuide => ParentMenu is MenuCategory.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name title of the group when it is in the match info guide.
|
||||
/// </summary>
|
||||
public virtual string RoleGuideTitle => GroupName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key used to combine this group with other option groups on one Match Info Guide page.
|
||||
/// Groups are combined only with groups from the same plugin that use the same non-empty key.
|
||||
/// </summary>
|
||||
public virtual string? MatchInfoGuidePageKey => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title of the Match Info Guide page. For groups sharing a page key, the first
|
||||
/// group in menu order supplies the page title.
|
||||
/// </summary>
|
||||
public virtual string MatchInfoGuidePageTitle => RoleGuideTitle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordering priority of this Match Info Guide page. Lower values appear first
|
||||
/// among pages with the same menu category and group priority. Groups sharing a page key
|
||||
/// use the lowest page-order value among those groups.
|
||||
/// </summary>
|
||||
public virtual int MatchInfoGuidePageOrder => int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of columns used to lay out options on this Match Info Guide page.
|
||||
/// For a combined page, groups are placed into these columns. Values are limited to 1-6;
|
||||
/// for groups sharing a page key, the first group in menu order supplies this value.
|
||||
/// </summary>
|
||||
public virtual int MatchInfoGuidePageColumns => 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the group <see cref="Color"/>. This is used to color the group in the options menu.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to set the <see cref="IModdedOption.ChangedEvent"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The method to get the changed event from.</param>
|
||||
/// <param name="holderType">The type the method is in.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type? holderType = null)
|
||||
: ModdedOptionMemberAttribute(methodName, holderType, "Changed")
|
||||
{
|
||||
internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var valueType = option.GetValueType();
|
||||
|
||||
if (!ValidateSignature(method, typeof(void), valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Action<>).MakeGenericType(valueType);
|
||||
return CompileDynamicDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "Changed");
|
||||
}
|
||||
|
||||
internal Func<int, Delegate?>? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Action<>).MakeGenericType(valueType);
|
||||
return CompileDynamicListDelegate(type, method, instanceGetter, typeof(void), [valueType], delegateType, "ListChanged", hasIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to set the <see cref="IModdedOption.Getter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The method to get the getter from.</param>
|
||||
/// <param name="holderType">The type the method is in.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type? holderType = null)
|
||||
: ModdedOptionMemberAttribute(methodName, holderType, "Getter")
|
||||
{
|
||||
internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var valueType = option.GetValueType();
|
||||
|
||||
if (!ValidateSignature(method, valueType, valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
|
||||
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "Getter");
|
||||
}
|
||||
|
||||
internal Func<int, Delegate?>? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Func<,>).MakeGenericType(valueType, valueType);
|
||||
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType], delegateType, "ListGetter", hasIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using MiraAPI.Utilities;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Base attribute for resolving and compiling option delegate members (<see cref="IModdedOption.Visible"/>, <see cref="IModdedOption.Getter"/>, <see cref="IModdedOption.Setter"/>, and <see cref="IModdedOption.ChangedEvent"/>).
|
||||
/// </summary>
|
||||
/// <param name="memberName">The name of the target member, or <see langword="null"/> to use the property name with <paramref name="defaultSuffix"/>.</param>
|
||||
/// <param name="holderType">The type containing the target member, or <see langword="null"/> to use the declaring <see cref="AbstractOptionGroup"/> type.</param>
|
||||
/// <param name="defaultSuffix">The default suffix appended to the property name when <paramref name="memberName"/> is not specified.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public abstract class ModdedOptionMemberAttribute(string? memberName, Type? holderType, string defaultSuffix) : Attribute
|
||||
{
|
||||
private static readonly Dictionary<MethodInfo, Delegate> DelegateCache = [];
|
||||
|
||||
/// <summary>
|
||||
/// A helper class to bind a specific list index to a dynamically compiled delegate.
|
||||
/// </summary>
|
||||
/// <param name="index">The index to bind.</param>
|
||||
public sealed class DelegateIndexClosure(int index)
|
||||
{
|
||||
/// <summary>
|
||||
/// The bound index.
|
||||
/// </summary>
|
||||
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "No need to be private.")]
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||
public readonly int Index = index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the index field so child attributes can emit instructions to read it.
|
||||
/// </summary>
|
||||
protected static readonly FieldInfo IndexClosureField = typeof(DelegateIndexClosure).GetField(nameof(DelegateIndexClosure.Index))!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolved name of the target member.
|
||||
/// </summary>
|
||||
protected string? MemberName { get; private set; } = memberName;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a method matches the expected return type and parameter types.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to validate.</param>
|
||||
/// <param name="expectedReturnType">The expected return type of the method.</param>
|
||||
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
|
||||
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
|
||||
protected bool ValidateSignature(MethodInfo method, Type expectedReturnType, params Type[] expectedParamTypes)
|
||||
{
|
||||
if (method.ReturnType != expectedReturnType)
|
||||
{
|
||||
Error($"{MemberName}'s return type must be {expectedReturnType.Name}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
if (parameters.Length != expectedParamTypes.Length)
|
||||
{
|
||||
Error($"{MemberName} must have exactly {expectedParamTypes.Length} parameter(s).");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < expectedParamTypes.Length; i++)
|
||||
{
|
||||
if (parameters[i].ParameterType == expectedParamTypes[i])
|
||||
continue;
|
||||
|
||||
Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a list method matches the expected return type, base parameter types, and an optional integer index.
|
||||
/// </summary>
|
||||
/// <param name="method">The method to validate.</param>
|
||||
/// <param name="expectedReturnType">The expected return type of the method.</param>
|
||||
/// <param name="hasIndex">When the method returns, a flag that indicates if the method has an index parameter.</param>
|
||||
/// <param name="expectedParamTypes">The expected types of the parameters of the method.</param>
|
||||
/// <returns><c>true</c> if the method is valid; <c>false</c> otherwise.</returns>
|
||||
protected bool ValidateListSignature(MethodInfo method, Type expectedReturnType, out bool hasIndex, params Type[] expectedParamTypes)
|
||||
{
|
||||
hasIndex = false;
|
||||
|
||||
if (method.ReturnType != expectedReturnType)
|
||||
{
|
||||
Error($"{MemberName}'s return type must be {expectedReturnType.Name}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
if (parameters.Length < expectedParamTypes.Length || parameters.Length > expectedParamTypes.Length + 1)
|
||||
{
|
||||
Error($"{MemberName} must have {expectedParamTypes.Length} parameter(s), optionally with an extra trailing int parameter.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < expectedParamTypes.Length; i++)
|
||||
{
|
||||
if (parameters[i].ParameterType == expectedParamTypes[i])
|
||||
continue;
|
||||
|
||||
Error($"{MemberName}'s parameter at index {i} must be {expectedParamTypes[i].Name}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameters.Length == expectedParamTypes.Length + 1)
|
||||
{
|
||||
if (parameters[^1].ParameterType != typeof(int))
|
||||
{
|
||||
Error($"{MemberName}'s final parameter must be an int (the list index).");
|
||||
return false;
|
||||
}
|
||||
|
||||
hasIndex = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the target <see cref="MemberInfo"/> (property, field or method) and its associated singleton instance getter, if applicable.
|
||||
/// </summary>
|
||||
/// <param name="group">The option group instance the property belongs to.</param>
|
||||
/// <param name="property">The option property decorated with this attribute.</param>
|
||||
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target member.</param>
|
||||
/// <param name="instanceGetter">
|
||||
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
|
||||
/// if the resolved member is an instance member on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
|
||||
/// </param>
|
||||
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
|
||||
/// <returns>The resolved <see cref="MemberInfo"/> if found; otherwise, <see langword="null"/>.</returns>
|
||||
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
|
||||
protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
|
||||
{
|
||||
var groupType = group.GetType();
|
||||
var flags = BindingFlags.Static | BindingFlags.Public;
|
||||
|
||||
MemberName ??= property.Name + defaultSuffix;
|
||||
|
||||
type = holderType ?? groupType;
|
||||
instanceGetter = null;
|
||||
|
||||
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
|
||||
{
|
||||
flags |= BindingFlags.Instance;
|
||||
|
||||
if (type == groupType)
|
||||
flags |= BindingFlags.NonPublic;
|
||||
|
||||
instanceGetter = typeof(OptionGroupSingleton<>)
|
||||
.MakeGenericType(type)
|
||||
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!
|
||||
.GetGetMethod();
|
||||
}
|
||||
else if (!type.IsStatic())
|
||||
{
|
||||
flags |= BindingFlags.Instance;
|
||||
|
||||
var canSearchPrivates = type.GetCustomAttribute<SearchPrivateAttribute>() != null;
|
||||
|
||||
if (canSearchPrivates)
|
||||
flags |= BindingFlags.NonPublic;
|
||||
|
||||
var getterFlags = BindingFlags.Static | BindingFlags.Public | (canSearchPrivates ? BindingFlags.NonPublic : BindingFlags.Default);
|
||||
|
||||
foreach (var method in type.GetMethods(getterFlags))
|
||||
{
|
||||
if (method.GetParameters().Length > 0 || (method.GetCustomAttribute<InstanceGetterAttribute>() == null && method.Name != "GetInstance"))
|
||||
continue;
|
||||
|
||||
instanceGetter = method;
|
||||
break;
|
||||
}
|
||||
|
||||
if (instanceGetter == null)
|
||||
{
|
||||
foreach (var prop in type.GetProperties(getterFlags))
|
||||
{
|
||||
if (prop.GetCustomAttribute<InstanceGetterAttribute>() == null && prop.Name != "Instance")
|
||||
continue;
|
||||
|
||||
var getMethod = prop.GetGetMethod(canSearchPrivates);
|
||||
|
||||
if (getMethod == null)
|
||||
continue;
|
||||
|
||||
instanceGetter = getMethod;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceGetter == null)
|
||||
{
|
||||
Error($"No valid static instance getter (method GetInstance or property Instance) found in instanced type {type.Name}. Try renaming the method/property, or decorate it with InstanceGetterAttribute.");
|
||||
notFound = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!instanceGetter.ReturnType.IsAssignableTo(type))
|
||||
{
|
||||
Error($"Found instance getter {instanceGetter.Name}, but the return type did not match what was expected ({type.Name}).");
|
||||
notFound = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (type.GetCustomAttribute<SearchPrivateAttribute>() != null)
|
||||
{
|
||||
flags |= BindingFlags.NonPublic;
|
||||
}
|
||||
|
||||
var member = type.GetMember(MemberName, flags).FirstOrDefault();
|
||||
notFound = true;
|
||||
|
||||
if (member == null)
|
||||
return null;
|
||||
|
||||
notFound = false;
|
||||
|
||||
if (instanceGetter == null)
|
||||
return member;
|
||||
|
||||
bool isStatic;
|
||||
|
||||
if (member is FieldInfo field)
|
||||
{
|
||||
isStatic = field.IsStatic;
|
||||
}
|
||||
else
|
||||
{
|
||||
var method = (member as PropertyInfo)?.GetGetMethod(true) ?? member as MethodInfo;
|
||||
isStatic = method is { IsStatic: true };
|
||||
}
|
||||
|
||||
if (isStatic)
|
||||
instanceGetter = null;
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the target <see cref="MethodInfo"/> and its associated singleton instance getter, if applicable.
|
||||
/// </summary>
|
||||
/// <param name="group">The option group instance the property belongs to.</param>
|
||||
/// <param name="property">The option property decorated with this attribute.</param>
|
||||
/// <param name="type">When this method returns, contains the resolved <see cref="Type"/> expected to hold the target method.</param>
|
||||
/// <param name="instanceGetter">
|
||||
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
|
||||
/// if the resolved method is an instance method on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
|
||||
/// </param>
|
||||
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
|
||||
/// <returns>The resolved <see cref="MethodInfo"/> if found; otherwise, <see langword="null"/>.</returns>
|
||||
protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
|
||||
{
|
||||
return ResolveMember(group, property, out type, out instanceGetter, out notFound) as MethodInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a <see cref="DynamicMethod"/> using IL generation to invoke the resolved static or instance <paramref name="method"/>.
|
||||
/// </summary>
|
||||
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
|
||||
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
|
||||
/// <param name="instanceGetter">
|
||||
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
|
||||
/// or <see langword="null"/> if <paramref name="method"/> is static.
|
||||
/// </param>
|
||||
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
|
||||
/// <param name="paramTypes">The parameter types accepted by the generated delegate.</param>
|
||||
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create (e.g., <see cref="Func{T, TResult}"/> or <see cref="Action{T}"/>).</param>
|
||||
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
|
||||
/// <param name="forwardParameters">Whether to push the delegate parameters onto the stack before invoking <paramref name="method"/>.</param>
|
||||
/// <param name="postProcessCall">An optional callback to emit additional IL instructions before returning.</param>
|
||||
/// <returns>A compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> bound to the generated IL method.</returns>
|
||||
protected static Delegate CompileDynamicDelegate(
|
||||
Type ownerType,
|
||||
MethodInfo method,
|
||||
MethodInfo? instanceGetter,
|
||||
Type returnType,
|
||||
Type[] paramTypes,
|
||||
Type delegateType,
|
||||
string suffix,
|
||||
bool forwardParameters = true,
|
||||
Action<ILGenerator>? postProcessCall = null)
|
||||
{
|
||||
if (postProcessCall == null && DelegateCache.TryGetValue(method, out var cachedDelegate))
|
||||
return cachedDelegate;
|
||||
|
||||
var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}", returnType, paramTypes, ownerType, true);
|
||||
var il = dm.GetILGenerator();
|
||||
|
||||
if (!method.IsStatic && instanceGetter != null)
|
||||
il.Emit(OpCodes.Call, instanceGetter);
|
||||
|
||||
if (forwardParameters)
|
||||
{
|
||||
for (byte i = 0; i < paramTypes.Length; i++)
|
||||
{
|
||||
var loadOp = i switch
|
||||
{
|
||||
0 => OpCodes.Ldarg_0,
|
||||
1 => OpCodes.Ldarg_1,
|
||||
2 => OpCodes.Ldarg_2,
|
||||
3 => OpCodes.Ldarg_3,
|
||||
_ => OpCodes.Ldarg_S,
|
||||
};
|
||||
|
||||
if (loadOp == OpCodes.Ldarg_S)
|
||||
il.Emit(loadOp, i);
|
||||
else
|
||||
il.Emit(loadOp);
|
||||
}
|
||||
}
|
||||
|
||||
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
|
||||
il.Emit(callOp, method);
|
||||
|
||||
postProcessCall?.Invoke(il);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
var compiledDelegate = dm.CreateDelegate(delegateType);
|
||||
|
||||
if (postProcessCall == null)
|
||||
DelegateCache[method] = compiledDelegate;
|
||||
|
||||
return compiledDelegate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a factory function that provides a <see cref="Delegate"/> for each index in an option list.
|
||||
/// If <paramref name="hasIndexParameter"/> is <see langword="false"/> and <paramref name="postProcessListCall"/> is <see langword="null"/>,
|
||||
/// a single <see cref="Delegate"/> is compiled once and reused for all indices.
|
||||
/// </summary>
|
||||
/// <param name="ownerType">The <see cref="Type"/> that owns the generated <see cref="DynamicMethod"/> and holds the target method.</param>
|
||||
/// <param name="method">The target <see cref="MethodInfo"/> to invoke.</param>
|
||||
/// <param name="instanceGetter">
|
||||
/// The static getter <see cref="MethodInfo"/> used to push the target instance onto the evaluation stack for instance methods,
|
||||
/// or <see langword="null"/> if <paramref name="method"/> is static.
|
||||
/// </param>
|
||||
/// <param name="returnType">The return <see cref="Type"/> of the generated delegate (or <see cref="void"/> for actions).</param>
|
||||
/// <param name="baseParamTypes">The parameter types accepted by the generated delegate.</param>
|
||||
/// <param name="delegateType">The concrete delegate <see cref="Type"/> to create.</param>
|
||||
/// <param name="suffix">A descriptive suffix appended to the generated <see cref="DynamicMethod"/> name for debugging and stack traces.</param>
|
||||
/// <param name="hasIndexParameter">Whether the target <paramref name="method"/> takes the list <see langword="int"/> index as its final parameter.</param>
|
||||
/// <param name="postProcessCall">An optional index-independent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
|
||||
/// <param name="postProcessListCall">An optional index-dependent callback to emit additional IL instructions after <paramref name="method"/> is called.</param>
|
||||
/// <returns>A factory function that returns a compiled <see cref="Delegate"/> of type <paramref name="delegateType"/> for a given list index.</returns>
|
||||
protected static Func<int, Delegate> CompileDynamicListDelegate(
|
||||
Type ownerType,
|
||||
MethodInfo method,
|
||||
MethodInfo? instanceGetter,
|
||||
Type returnType,
|
||||
Type[] baseParamTypes,
|
||||
Type delegateType,
|
||||
string suffix,
|
||||
bool hasIndexParameter,
|
||||
Action<ILGenerator>? postProcessCall = null,
|
||||
Action<ILGenerator>? postProcessListCall = null)
|
||||
{
|
||||
if (!hasIndexParameter && postProcessListCall is null)
|
||||
{
|
||||
var sharedDelegate = CompileDynamicDelegate(
|
||||
ownerType,
|
||||
method,
|
||||
instanceGetter,
|
||||
returnType,
|
||||
baseParamTypes,
|
||||
delegateType,
|
||||
suffix,
|
||||
baseParamTypes.Length > 0,
|
||||
postProcessCall);
|
||||
|
||||
return _ => sharedDelegate;
|
||||
}
|
||||
|
||||
var fullParamTypes = new Type[baseParamTypes.Length + 1];
|
||||
fullParamTypes[0] = typeof(DelegateIndexClosure);
|
||||
Array.Copy(baseParamTypes, 0, fullParamTypes, 1, baseParamTypes.Length);
|
||||
|
||||
var dm = new DynamicMethod($"{ownerType.Name}_{method.Name}_{suffix}_List", returnType, fullParamTypes, ownerType, true);
|
||||
var il = dm.GetILGenerator();
|
||||
|
||||
if (!method.IsStatic && instanceGetter != null)
|
||||
il.Emit(OpCodes.Call, instanceGetter);
|
||||
|
||||
for (var i = 0; i < baseParamTypes.Length; i++)
|
||||
{
|
||||
var argPos = (byte)(i + 1);
|
||||
var loadOp = argPos switch
|
||||
{
|
||||
1 => OpCodes.Ldarg_1,
|
||||
2 => OpCodes.Ldarg_2,
|
||||
3 => OpCodes.Ldarg_3,
|
||||
_ => OpCodes.Ldarg_S,
|
||||
};
|
||||
|
||||
if (loadOp == OpCodes.Ldarg_S)
|
||||
il.Emit(loadOp, argPos);
|
||||
else
|
||||
il.Emit(loadOp);
|
||||
}
|
||||
|
||||
if (hasIndexParameter)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldfld, IndexClosureField);
|
||||
}
|
||||
|
||||
var callOp = method.IsStatic ? OpCodes.Call : OpCodes.Callvirt;
|
||||
il.Emit(callOp, method);
|
||||
|
||||
postProcessCall?.Invoke(il);
|
||||
postProcessListCall?.Invoke(il);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
return index => dm.CreateDelegate(delegateType, new DelegateIndexClosure(index));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to set the <see cref="IModdedOption.Setter"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="methodName">The method to get the setter from.</param>
|
||||
/// <param name="holderType">The type the method is in.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type? holderType = null)
|
||||
: ModdedOptionMemberAttribute(methodName, holderType, "Setter")
|
||||
{
|
||||
internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var valueType = option.GetValueType();
|
||||
|
||||
if (!ValidateSignature(method, valueType, valueType, valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
|
||||
return CompileDynamicDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "Setter");
|
||||
}
|
||||
|
||||
internal Func<int, Delegate?>? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
|
||||
{
|
||||
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Method {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType))
|
||||
return null;
|
||||
|
||||
var delegateType = typeof(Func<,,>).MakeGenericType(valueType, valueType, valueType);
|
||||
return CompileDynamicListDelegate(type, method, instanceGetter, valueType, [valueType, valueType], delegateType, "ListSetter", hasIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to set the <see cref="IModdedOption.Visible"/> delegate in a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="memberName">The member to get the visibility function from.</param>
|
||||
/// <param name="holderType">The type the member is in.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, Type? holderType = null)
|
||||
: ModdedOptionMemberAttribute(memberName, holderType, "Visibility")
|
||||
{
|
||||
private static readonly MethodInfo ModdedOptionBoolValueGetter = typeof(ModdedOption<bool>).GetProperty(nameof(ModdedOption<>.Value))!.GetGetMethod()!;
|
||||
private static readonly MethodInfo ListBoolIndexerGetter = typeof(IList<bool>).GetProperty("Item")!.GetGetMethod()!;
|
||||
|
||||
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
|
||||
{
|
||||
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Member {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo? targetMethod;
|
||||
Action<ILGenerator>? postProcess = null;
|
||||
|
||||
if (member is PropertyInfo vProperty)
|
||||
{
|
||||
if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
|
||||
{
|
||||
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
|
||||
}
|
||||
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
|
||||
{
|
||||
Error($"Property {MemberName} is not a bool or a bool ModdedOption.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vProperty.GetIndexParameters().Length > 0)
|
||||
{
|
||||
Error($"Property {MemberName} cannot be an indexer.");
|
||||
return null;
|
||||
}
|
||||
|
||||
targetMethod = vProperty.GetGetMethod(true);
|
||||
|
||||
if (targetMethod is null)
|
||||
{
|
||||
Error($"Property {MemberName} does not have a getter.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (member is MethodInfo vMethod)
|
||||
{
|
||||
if (vMethod.ReturnType != typeof(bool))
|
||||
{
|
||||
Error($"Method {MemberName} does not return a bool.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vMethod.GetParameters().Length > 0)
|
||||
{
|
||||
Error($"Method {MemberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
targetMethod = vMethod;
|
||||
}
|
||||
else if (member is FieldInfo field)
|
||||
{
|
||||
if (field.FieldType != typeof(bool))
|
||||
{
|
||||
Error($"Field {MemberName} is not a bool.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var dynamicMethod = new DynamicMethod($"GetVisibility_{type.Name}_{field.Name}", typeof(bool), Type.EmptyTypes, type, true);
|
||||
var il = dynamicMethod.GetILGenerator();
|
||||
|
||||
if (instanceGetter != null)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Call, instanceGetter);
|
||||
il.Emit(OpCodes.Ldfld, field);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.Emit(OpCodes.Ldsfld, field);
|
||||
}
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
return dynamicMethod.CreateDelegate<Func<bool>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Error($"Member {MemberName} of {type.FullName} is not valid for Visible function.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Func<bool>)CompileDynamicDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "Visibility", false, postProcess);
|
||||
}
|
||||
|
||||
internal Func<int, Delegate?>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
|
||||
{
|
||||
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
|
||||
{
|
||||
if (notFound)
|
||||
Error($"Member {MemberName} does not exist in {type.FullName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo? targetMethod;
|
||||
Action<ILGenerator>? postProcess = null;
|
||||
Action<ILGenerator>? postProcessList = null;
|
||||
|
||||
var hasIndexParam = false;
|
||||
|
||||
if (member is PropertyInfo vProperty)
|
||||
{
|
||||
var isListBool = false;
|
||||
|
||||
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
|
||||
{
|
||||
isListBool = true;
|
||||
postProcessList = il =>
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Ldfld, IndexClosureField);
|
||||
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
|
||||
};
|
||||
}
|
||||
else if (vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
|
||||
{
|
||||
postProcess = il => il.Emit(OpCodes.Callvirt, ModdedOptionBoolValueGetter);
|
||||
}
|
||||
else if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)))
|
||||
{
|
||||
Error($"Property {MemberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexParams = vProperty.GetIndexParameters();
|
||||
|
||||
if (indexParams.Length > 1)
|
||||
{
|
||||
Error($"Indexer {MemberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isListBool && indexParams.Length != 0)
|
||||
{
|
||||
Error($"Property {MemberName} cannot be an indexer of lists of bools.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (indexParams.Length == 1)
|
||||
{
|
||||
if (indexParams[0].ParameterType != typeof(int))
|
||||
{
|
||||
Error($"Indexer {MemberName}'s parameter is not an int.");
|
||||
return null;
|
||||
}
|
||||
|
||||
hasIndexParam = true;
|
||||
}
|
||||
|
||||
targetMethod = vProperty.GetGetMethod(true);
|
||||
|
||||
if (targetMethod is null)
|
||||
{
|
||||
Error($"Property {MemberName} does not have a getter.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (member is MethodInfo vMethod)
|
||||
{
|
||||
if (vMethod.ReturnType != typeof(bool))
|
||||
{
|
||||
Error($"Method {MemberName} does not return a bool.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var paramList = vMethod.GetParameters();
|
||||
|
||||
if (paramList.Length > 1)
|
||||
{
|
||||
Error($"Method {MemberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (paramList.Length == 1)
|
||||
{
|
||||
if (paramList[0].ParameterType != typeof(int))
|
||||
{
|
||||
Error($"Method {MemberName}'s parameter is not an int.");
|
||||
return null;
|
||||
}
|
||||
|
||||
hasIndexParam = true;
|
||||
}
|
||||
|
||||
targetMethod = vMethod;
|
||||
}
|
||||
else if (member is FieldInfo field)
|
||||
{
|
||||
if (field.FieldType.IsAssignableTo(typeof(IList<bool>)))
|
||||
{
|
||||
postProcess = il =>
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Callvirt, ListBoolIndexerGetter);
|
||||
};
|
||||
}
|
||||
else if (field.FieldType != typeof(bool))
|
||||
{
|
||||
Error($"Field {MemberName} is not a bool or a list of bools.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var dynamicMethod = new DynamicMethod($"GetListVisibility_{type.Name}_{field.Name}", typeof(bool), [typeof(int)], type, true);
|
||||
var il = dynamicMethod.GetILGenerator();
|
||||
|
||||
if (instanceGetter != null)
|
||||
{
|
||||
il.Emit(OpCodes.Ldarg_0);
|
||||
il.Emit(OpCodes.Call, instanceGetter);
|
||||
il.Emit(OpCodes.Ldfld, field);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.Emit(OpCodes.Ldsfld, field);
|
||||
}
|
||||
|
||||
postProcess?.Invoke(il);
|
||||
|
||||
il.Emit(OpCodes.Ret);
|
||||
|
||||
var func = dynamicMethod.CreateDelegate<Func<int, bool>>();
|
||||
return index => () => func(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
Error($"Member {MemberName} of {type.FullName} is not valid for Visibility function.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return CompileDynamicListDelegate(type, targetMethod, instanceGetter, typeof(bool), Type.EmptyTypes, typeof(Func<bool>), "ListVisibility", hasIndexParam, postProcess, postProcessList);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute used to mark a method or property as one that returns an instance of the class it's declared in.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
|
||||
public sealed class InstanceGetterAttribute : Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute used to mark a class as one whose reflection search should include private members.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class SearchPrivateAttribute : Attribute;
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Translation;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
@@ -17,7 +16,7 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
|
||||
{
|
||||
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
var opt = new ModdedEnumOption(Title.Translate(), (int)(value ?? 0), enumType, values);
|
||||
var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values);
|
||||
return opt;
|
||||
}
|
||||
|
||||
@@ -33,7 +32,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}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +40,12 @@ public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? va
|
||||
/// Attribute for creating an enum option.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type.</typeparam>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <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)
|
||||
where T : Enum
|
||||
where T : struct, Enum
|
||||
{
|
||||
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
@@ -64,6 +65,6 @@ public class ModdedEnumOptionAttribute<T>(string title, string[]? values = null)
|
||||
{
|
||||
return HolderOption is ModdedEnumOption<T> opt
|
||||
? opt.Value
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType ${typeof(T).FullName} is not a ModdedEnumOption");
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).Name}>");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
@@ -13,45 +12,93 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
|
||||
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="zeroInfinity">Flag indicating whether 0 means infinity.</param>
|
||||
/// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedNumberOptionAttribute(
|
||||
string title,
|
||||
float min,
|
||||
float max,
|
||||
float increment = 1,
|
||||
float increment,
|
||||
string zeroBehavior,
|
||||
string negativeBehavior,
|
||||
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false)
|
||||
bool halfIncrements = false)
|
||||
: ModdedOptionAttribute(title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedNumberOptionAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
public ModdedNumberOptionAttribute(
|
||||
string title,
|
||||
float min,
|
||||
float max,
|
||||
float increment = 1,
|
||||
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false)
|
||||
: this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
|
||||
{
|
||||
}
|
||||
|
||||
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
var final = value switch
|
||||
{
|
||||
null => min + increment,
|
||||
int integer => integer,
|
||||
_ => (float)value,
|
||||
};
|
||||
|
||||
return new ModdedNumberOption(
|
||||
Title.Translate(),
|
||||
(float)(value ?? min + increment),
|
||||
Title,
|
||||
final,
|
||||
min,
|
||||
max,
|
||||
increment,
|
||||
zeroBehavior,
|
||||
negativeBehavior,
|
||||
suffixType,
|
||||
formatString,
|
||||
zeroInfinity);
|
||||
halfIncrements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
var opt = HolderOption as ModdedNumberOption;
|
||||
opt?.SetValue((float)value);
|
||||
if (HolderOption is not ModdedNumberOption opt)
|
||||
return;
|
||||
|
||||
var final = value switch
|
||||
{
|
||||
int integer => integer,
|
||||
_ => (float)value,
|
||||
};
|
||||
opt.SetValue(final);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue()
|
||||
{
|
||||
return HolderOption is ModdedNumberOption opt
|
||||
? (object)opt.Value
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
|
||||
if (HolderOption is not ModdedNumberOption opt)
|
||||
throw new InvalidOperationException($"HolderOption for option \"{Title}\" is not a ModdedNumberOption");
|
||||
|
||||
var value = opt.Value;
|
||||
|
||||
if (ValueType == typeof(int))
|
||||
return (int)Math.Round(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
public abstract class ModdedOptionAttribute(string title, Type? roleType = null, Type? modeType = null) : PropertyOptionAttribute
|
||||
{
|
||||
internal IModdedOption? HolderOption { get; set; }
|
||||
internal Type ValueType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title of the option.
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to modify a property with a <see cref="ModdedOptionAttribute"/> or <see cref="ModdedOptionListAttribute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="holderType">The type the member is in.</param>
|
||||
/// <param name="memberName">The member to get the visibility function from.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ModdedOptionVisibilityAttribute(Type? holderType = null, string? memberName = null) : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedOptionVisibilityAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="memberName">The member to get the visibility function from.</param>
|
||||
public ModdedOptionVisibilityAttribute(string memberName)
|
||||
: this(null, memberName)
|
||||
{
|
||||
}
|
||||
|
||||
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
|
||||
{
|
||||
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
|
||||
{
|
||||
Error($"Member {memberName} does not exist in {type.FullName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (member is PropertyInfo vProperty)
|
||||
{
|
||||
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
|
||||
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)))
|
||||
{
|
||||
Error($"Property {memberName} is not a bool or a bool ModdedOption.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexParams = vProperty.GetIndexParameters();
|
||||
if (indexParams.Length > 0)
|
||||
{
|
||||
Error($"Property {memberName} cannot be an indexer.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return () => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
|
||||
}
|
||||
|
||||
if (member is MethodInfo vMethod)
|
||||
{
|
||||
if (vMethod.ReturnType != typeof(bool))
|
||||
{
|
||||
Error($"Method {memberName} does not return a bool.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var paramList = vMethod.GetParameters();
|
||||
if (paramList.Length > 0)
|
||||
{
|
||||
Error($"Method {memberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return () => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
|
||||
}
|
||||
|
||||
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
|
||||
return null;
|
||||
}
|
||||
|
||||
internal Func<int, bool>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
|
||||
{
|
||||
if (GetVisibilityMember(group, property, out Type type, out var instanceGet) is not { } member)
|
||||
{
|
||||
Error($"Member {memberName} does not exist in {type.FullName}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (member is PropertyInfo vProperty)
|
||||
{
|
||||
if (!vProperty.PropertyType.IsAssignableTo(typeof(bool)) &&
|
||||
!vProperty.PropertyType.IsAssignableTo(typeof(ModdedOption<bool>)) &&
|
||||
!vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
|
||||
{
|
||||
Error($"Property {memberName} is not a bool value or indexer, a list of bools, or a bool ModdedOption.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexParams = vProperty.GetIndexParameters();
|
||||
if (indexParams.Length > 1)
|
||||
{
|
||||
Error($"Indexer {memberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vProperty.PropertyType.IsAssignableTo(typeof(IList<bool>)))
|
||||
{
|
||||
if (indexParams.Length != 0)
|
||||
{
|
||||
Error($"Property {memberName} cannot be an indexer of lists of bools.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return i => ((IList<bool>)vProperty.GetValue(instanceGet?.Invoke())!)[i];
|
||||
}
|
||||
|
||||
if (indexParams.Length == 0)
|
||||
{
|
||||
return _ => (bool)vProperty.GetValue(instanceGet?.Invoke())!;
|
||||
}
|
||||
|
||||
if (indexParams[0].ParameterType != typeof(int))
|
||||
{
|
||||
Error($"Indexer {memberName}'s parameter is not an int.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return i => (bool)vProperty.GetValue(instanceGet?.Invoke(), [i])!;
|
||||
}
|
||||
|
||||
if (member is MethodInfo vMethod)
|
||||
{
|
||||
if (vMethod.ReturnType != typeof(bool))
|
||||
{
|
||||
Error($"Method {memberName} does not return a bool.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var paramList = vMethod.GetParameters();
|
||||
if (paramList.Length > 1)
|
||||
{
|
||||
Error($"Method {memberName} has too many parameters.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (paramList.Length == 0)
|
||||
{
|
||||
return _ => (bool)vMethod.Invoke(instanceGet?.Invoke(), null)!;
|
||||
}
|
||||
|
||||
if (paramList[0].ParameterType != typeof(int))
|
||||
{
|
||||
Error($"Method {memberName}'s parameter is not an int.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return i => (bool)vMethod.Invoke(instanceGet?.Invoke(), [i])!;
|
||||
}
|
||||
|
||||
Error($"Member {memberName} of {type.FullName} is not valid for Visible function.");
|
||||
return null;
|
||||
}
|
||||
|
||||
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
|
||||
private MemberInfo? GetVisibilityMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out Func<object>? instanceGet)
|
||||
{
|
||||
Type groupType = group.GetType();
|
||||
BindingFlags flags = BindingFlags.Static | BindingFlags.Public;
|
||||
|
||||
instanceGet = null;
|
||||
type = holderType ?? groupType;
|
||||
memberName ??= $"{property.Name}Visible";
|
||||
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
|
||||
{
|
||||
flags |= BindingFlags.Instance;
|
||||
|
||||
if (type == groupType)
|
||||
{
|
||||
flags |= BindingFlags.NonPublic;
|
||||
instanceGet = () => group;
|
||||
}
|
||||
else
|
||||
{
|
||||
var instanceProp = typeof(OptionGroupSingleton<>).MakeGenericType(type)
|
||||
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!;
|
||||
instanceGet = () => instanceProp.GetValue(null)!;
|
||||
}
|
||||
}
|
||||
|
||||
MemberInfo? member = type.GetMember(memberName, flags).FirstOrDefault();
|
||||
if (instanceGet != null)
|
||||
{
|
||||
MethodInfo? method = (member as PropertyInfo)?.GetGetMethod()
|
||||
?? member as MethodInfo;
|
||||
if (method != null && method.IsStatic)
|
||||
{
|
||||
instanceGet = null;
|
||||
}
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for a <see cref="ModdedPlayerOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedPlayerOptionAttribute(
|
||||
string title,
|
||||
bool allowNone = true)
|
||||
: ModdedOptionAttribute(title)
|
||||
{
|
||||
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
var option = new ModdedPlayerOption(Title, allowNone);
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue()
|
||||
{
|
||||
if (HolderOption is not ModdedPlayerOption opt)
|
||||
throw new InvalidOperationException($"Holder option for {Title} is not a ModdedPlayerOption.");
|
||||
|
||||
if (ValueType == typeof(int))
|
||||
return opt.Value;
|
||||
|
||||
return opt.SelectedPlayer!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
if (HolderOption is not ModdedPlayerOption opt)
|
||||
return;
|
||||
|
||||
var final = value switch
|
||||
{
|
||||
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
|
||||
_ => (int)value,
|
||||
};
|
||||
opt?.SetValue(final);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for a <see cref="ModdedStringOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="values">An option list of string value.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedStringOptionAttribute(
|
||||
string title,
|
||||
string[] values)
|
||||
: ModdedOptionAttribute(title)
|
||||
{
|
||||
internal override IModdedOption? CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
var option = new ModdedStringOption(Title, (string)(value ?? string.Empty), values);
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue()
|
||||
{
|
||||
return HolderOption is ModdedStringOption opt
|
||||
? opt.Value
|
||||
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedStringOption.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
var opt = HolderOption as ModdedStringOption;
|
||||
opt?.SetValue((string)value);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Translation;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
@@ -9,12 +8,27 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
/// Attribute for a <see cref="ModdedToggleOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="on">What the option displays when toggled on.</param>
|
||||
/// <param name="off">What the option displays when toggled off.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title)
|
||||
public class ModdedToggleOptionAttribute(
|
||||
string title,
|
||||
string on,
|
||||
string off)
|
||||
: ModdedOptionAttribute(title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedToggleOptionAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
public ModdedToggleOptionAttribute(string title)
|
||||
: this(title, "On", "Off")
|
||||
{
|
||||
}
|
||||
|
||||
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
|
||||
{
|
||||
var toggleOpt = new ModdedToggleOption(Title.Translate(), (bool)(value ?? false));
|
||||
var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), on, off);
|
||||
return toggleOpt;
|
||||
}
|
||||
|
||||
@@ -29,7 +43,7 @@ public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(t
|
||||
public override object GetValue()
|
||||
{
|
||||
return HolderOption is ModdedToggleOption opt
|
||||
? (object)opt.Value
|
||||
? opt.Value
|
||||
: throw new InvalidOperationException($"Holder option for {Title} is not a ModdedToggleOption.");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
/// <summary>
|
||||
/// Attribute for creating a list of enum options.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <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)]
|
||||
public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]? values = null)
|
||||
: ModdedOptionListAttribute(title)
|
||||
@@ -31,7 +34,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)}\" with EnumType {enumType.FullName} is not a ModdedEnumOption");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +42,12 @@ public class ModdedEnumOptionListAttribute(string title, Type enumType, string[]
|
||||
/// Attribute for creating a list of enum options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type.</typeparam>
|
||||
/// <param name="title">The option's title.</param>
|
||||
/// <param name="values">The option's possible values.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = null)
|
||||
: ModdedOptionListAttribute(title)
|
||||
where T : Enum
|
||||
where T : struct, Enum
|
||||
{
|
||||
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
|
||||
{
|
||||
@@ -63,6 +68,6 @@ public class ModdedEnumOptionListAttribute<T>(string title, string[]? values = n
|
||||
{
|
||||
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");
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" with EnumType {typeof(T).FullName} is not a ModdedEnumOption<{typeof(T).FullName}>");
|
||||
}
|
||||
}
|
||||
@@ -7,47 +7,102 @@ using MiraAPI.Utilities;
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// A number option attribute for the modded options system.
|
||||
/// Attribute for creating a list of number options.
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
|
||||
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="halfIncrements">Whether increments can be split in half (using Shift).</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedNumberOptionListAttribute(
|
||||
string title,
|
||||
float min,
|
||||
float max,
|
||||
float increment = 1,
|
||||
float increment,
|
||||
string zeroBehavior,
|
||||
string negativeBehavior,
|
||||
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false)
|
||||
bool halfIncrements = false)
|
||||
: ModdedOptionListAttribute(title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedNumberOptionListAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
public ModdedNumberOptionListAttribute(
|
||||
string title,
|
||||
float min,
|
||||
float max,
|
||||
float increment = 1,
|
||||
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false)
|
||||
: this(title, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f)
|
||||
{
|
||||
}
|
||||
|
||||
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
|
||||
{
|
||||
return new ModdedOptionList<ModdedNumberOption>(value.Count, idx =>
|
||||
{
|
||||
var final = value[idx] switch
|
||||
{
|
||||
null => min + increment,
|
||||
int integer => integer,
|
||||
_ => (float)value[idx]!,
|
||||
};
|
||||
|
||||
return new(
|
||||
GetFormattedTitle(idx),
|
||||
(float)(value[idx] ?? min + increment),
|
||||
final,
|
||||
min,
|
||||
max,
|
||||
increment,
|
||||
zeroBehavior,
|
||||
negativeBehavior,
|
||||
suffixType,
|
||||
formatString,
|
||||
zeroInfinity);
|
||||
halfIncrements);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(int idx, object value)
|
||||
{
|
||||
var opt = HolderOptionList?[idx] as ModdedNumberOption;
|
||||
opt?.SetValue((float)value);
|
||||
if (HolderOptionList?[idx] is not ModdedNumberOption opt)
|
||||
return;
|
||||
|
||||
var final = value switch
|
||||
{
|
||||
int integer => integer,
|
||||
_ => (float)value,
|
||||
};
|
||||
opt.SetValue(final);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue(int idx)
|
||||
{
|
||||
return HolderOptionList?[idx] is ModdedNumberOption opt
|
||||
? (object)opt.Value
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
|
||||
if (HolderOptionList?[idx] is not ModdedNumberOption opt)
|
||||
throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedNumberOption");
|
||||
|
||||
var value = opt.Value;
|
||||
|
||||
if (ValueType == typeof(int))
|
||||
return (int)Math.Round(value);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using MiraAPI.Translation;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
@@ -14,8 +15,8 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAttribute
|
||||
{
|
||||
internal IModdedOptionList? HolderOptionList { get; set; }
|
||||
|
||||
internal object? Value { get; set; }
|
||||
internal IList? Value { get; set; }
|
||||
internal Type ValueType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title of the options.
|
||||
@@ -31,16 +32,16 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
|
||||
/// <param name="value">The new values as an object.</param>
|
||||
public override void SetValue(object value)
|
||||
{
|
||||
Value = value;
|
||||
var list = (IList)value;
|
||||
if (list.Count != HolderOptionList!.Count)
|
||||
Value = (IList)value;
|
||||
|
||||
if (Value.Count != HolderOptionList!.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Value set to option list cannot change the list's length.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
for (int i = 0; i < Value.Count; i++)
|
||||
{
|
||||
SetValue(i, list[i]!);
|
||||
SetValue(i, Value[i]!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,18 +78,19 @@ public abstract class ModdedOptionListAttribute(string title) : PropertyOptionAt
|
||||
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "I don't even know if this is worth it.")]
|
||||
protected string GetFormattedTitle(int index)
|
||||
{
|
||||
Match match = Regex.Match(Title, @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
|
||||
var match = Regex.Match(Title.Translate(), @"\{(\d+)(?::[^}]+)?\}", default, Regex.InfiniteMatchTimeout);
|
||||
|
||||
if (match.Success)
|
||||
if (!match.Success)
|
||||
{
|
||||
if (match.Groups.Count == 1 && match.Groups[0].Value == "0")
|
||||
{
|
||||
return string.Format(Title, index);
|
||||
}
|
||||
|
||||
Error("ModdedOptionList Title cannot contain more than the first parameter for formatting.");
|
||||
return Title + index;
|
||||
}
|
||||
|
||||
if (match.Value == "{0}" && !match.NextMatch().Success)
|
||||
{
|
||||
return string.Format(Title, index);
|
||||
}
|
||||
|
||||
Error($"ModdedOptionList Title of {Title} cannot contain more than the first parameter for formatting.");
|
||||
return Title + index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for creating a list of player options.
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedPlayerOptionListAttribute(
|
||||
string title,
|
||||
bool allowNone = true)
|
||||
: ModdedOptionListAttribute(title)
|
||||
{
|
||||
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
|
||||
{
|
||||
return new ModdedOptionList<ModdedPlayerOption>(value.Count, idx =>
|
||||
{
|
||||
return new(GetFormattedTitle(idx), allowNone);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(int idx, object value)
|
||||
{
|
||||
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
|
||||
return;
|
||||
|
||||
var final = value switch
|
||||
{
|
||||
NetworkedPlayerInfo info => info ? opt.Values.IndexOf(info) : (allowNone ? -1 : 0),
|
||||
_ => (int)value,
|
||||
};
|
||||
opt?.SetValue(final);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue(int idx)
|
||||
{
|
||||
if (HolderOptionList?[idx] is not ModdedPlayerOption opt)
|
||||
throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedPlayerOption");
|
||||
|
||||
if (ValueType == typeof(int))
|
||||
return opt.Value;
|
||||
|
||||
return opt.SelectedPlayer!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Translation;
|
||||
|
||||
namespace MiraAPI.GameOptions.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute for creating a list of string options.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="values">An option list of string value.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedStringOptionListAttribute(
|
||||
string title,
|
||||
string[] values)
|
||||
: ModdedOptionListAttribute(title)
|
||||
{
|
||||
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
|
||||
{
|
||||
return new ModdedOptionList<ModdedStringOption>(value.Count, idx =>
|
||||
{
|
||||
return new ModdedStringOption(Title.Translate(), (string)(value[idx] ?? string.Empty), values);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetValue(int idx, object value)
|
||||
{
|
||||
var opt = HolderOptionList?[idx] as ModdedStringOption;
|
||||
opt?.SetValue((string)value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object GetValue(int idx)
|
||||
{
|
||||
return HolderOptionList?[idx] is ModdedStringOption opt
|
||||
? opt.Value
|
||||
: throw new InvalidOperationException($"HolderOption for option \"{GetFormattedTitle(idx)}\" is not a ModdedStringOption");
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,28 @@ namespace MiraAPI.GameOptions.Attributes;
|
||||
/// Attribute for a list of toggle options.
|
||||
/// </summary>
|
||||
/// <param name="title">The option's title.</param>
|
||||
/// <param name="on">What the option displays when toggled on.</param>
|
||||
/// <param name="off">What the option displays when toggled off.</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAttribute(title)
|
||||
public class ModdedToggleOptionListAttribute(
|
||||
string title,
|
||||
string on,
|
||||
string off)
|
||||
: ModdedOptionListAttribute(title)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedToggleOptionListAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The option's title.</param>
|
||||
public ModdedToggleOptionListAttribute(string title)
|
||||
: this(title, "On", "Off")
|
||||
{
|
||||
}
|
||||
|
||||
internal override IModdedOptionList CreateOptionList(IList value, PropertyInfo property)
|
||||
{
|
||||
var optList = new ModdedOptionList<ModdedToggleOption>(
|
||||
value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false)));
|
||||
value.Count, idx => new(GetFormattedTitle(idx), (bool)(value[idx] ?? false), on, off));
|
||||
return optList;
|
||||
}
|
||||
|
||||
@@ -30,7 +45,7 @@ public class ModdedToggleOptionListAttribute(string title) : ModdedOptionListAtt
|
||||
public override object GetValue(int idx)
|
||||
{
|
||||
return HolderOptionList?[idx] is ModdedToggleOption opt
|
||||
? (object)opt.Value
|
||||
? opt.Value
|
||||
: throw new InvalidOperationException($"Holder option for {GetFormattedTitle(idx)} is not a ModdedToggleOption.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
|
||||
internal sealed class CollectionBuilderAttribute(Type builderType, string methodName) : Attribute
|
||||
{
|
||||
public Type BuilderType { get; } = builderType;
|
||||
public string MethodName { get; } = methodName;
|
||||
}
|
||||
@@ -57,6 +57,21 @@ public interface IModdedOption
|
||||
/// </summary>
|
||||
Func<bool> Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a getter event for the option, allowing for dynamic values based on external circumstances.
|
||||
/// </summary>
|
||||
Delegate? Getter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a setter event for the option, allowing for dynamic value changes based on external circumstances.
|
||||
/// </summary>
|
||||
Delegate? Setter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event that is invoked when the value of the option changes.
|
||||
/// </summary>
|
||||
Delegate? ChangedEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the option should be included with presets.
|
||||
/// </summary>
|
||||
@@ -114,6 +129,12 @@ public interface IModdedOption
|
||||
/// </summary>
|
||||
/// <param name="presetConfig">The <see cref="ConfigFile"/> representing the preset configuration.</param>
|
||||
void LoadFromPreset(ConfigFile presetConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the value that the option represents.
|
||||
/// </summary>
|
||||
/// <returns>The type of the option value.</returns>
|
||||
Type GetValueType();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -121,4 +142,24 @@ public interface IModdedOption
|
||||
/// </summary>
|
||||
public interface IModdedOptionList : IReadOnlyList<IModdedOption>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the function to name the keys of the individual options in the configuration file.
|
||||
/// <para/>
|
||||
/// Return <see langword="null"/> to use the default of the property's name with its index appended.
|
||||
/// </summary>
|
||||
Func<int, string>? ConfigName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for list of <typeparamref name="T"/> options.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of <see cref="IModdedOption"/>.</typeparam>
|
||||
public interface IModdedOptionList<out T> : IModdedOptionList
|
||||
where T : IModdedOption
|
||||
{
|
||||
/// <inheritdoc cref="IReadOnlyList{T}.this[int]"/>
|
||||
new T this[int index] { get; }
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
new IEnumerator<T> GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MiraAPI.GameOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Allows an option group to compress noisy option blocks in the wiki "Options" section.
|
||||
/// Implementers can hide specific option keys and replace them with one or more summary lines.
|
||||
/// </summary>
|
||||
public interface ISummarizedOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the option string keys (StringNames) to omit from the wiki options list (e.g., a large set of related options).
|
||||
/// </summary>
|
||||
IReadOnlySet<StringNames> WikiHiddenOptionKeys { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets summary lines to insert when the first hidden option would have appeared.
|
||||
/// Lines should already be formatted as "Title: Value".
|
||||
/// </summary>
|
||||
IEnumerable<string> GetWikiOptionSummaryLines();
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MiraAPI.GameOptions;
|
||||
|
||||
/// <summary>
|
||||
/// A fixed-size array created specifically for modded options that automatically synchronizes with option attributes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the values that the array holds.</typeparam>
|
||||
[CollectionBuilder(typeof(ModdedArrayBuilder), nameof(ModdedArrayBuilder.Create))]
|
||||
public sealed class ModdedArray<T> : IList<T>, IReadOnlyList<T>, IList
|
||||
{
|
||||
private readonly T[] items;
|
||||
private int version;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => items.Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IList.IsFixedSize => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool ICollection.IsSynchronized => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object ICollection.SyncRoot => items.SyncRoot;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with the provided capacity.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The capacity of the list.</param>
|
||||
public ModdedArray(int capacity)
|
||||
{
|
||||
items = new T[capacity];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
|
||||
/// </summary>
|
||||
/// <param name="collection">The initial collection of values to initialise with.</param>
|
||||
public ModdedArray(IEnumerable<T> collection)
|
||||
{
|
||||
items = [.. collection];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
|
||||
/// </summary>
|
||||
/// <param name="span">The initial span of values to initialise with.</param>
|
||||
public ModdedArray(Span<T> span)
|
||||
{
|
||||
items = [.. span];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedArray{T}"/> class with initial values.
|
||||
/// </summary>
|
||||
/// <param name="span">The initial span of values to initialise with.</param>
|
||||
public ModdedArray(ReadOnlySpan<T> span)
|
||||
{
|
||||
items = [.. span];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element at the specified index, syncing with the attribute if registered.
|
||||
/// </summary>
|
||||
/// <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];
|
||||
set
|
||||
{
|
||||
if (ModdedOptionsManager.ListInstanceCache.TryGetValue(this, out var attribute))
|
||||
attribute.SetValue(index, value!);
|
||||
else
|
||||
items[index] = value;
|
||||
|
||||
version++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object? IList.this[int index]
|
||||
{
|
||||
get => this[index];
|
||||
set => this[index] = (T)value!;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return Array.IndexOf(items, item) >= 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
items.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
return Array.IndexOf(items, item);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IList.Contains(object? value)
|
||||
{
|
||||
return ((IList)items).Contains(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IList.IndexOf(object? value)
|
||||
{
|
||||
return ((IList)items).IndexOf(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void ICollection.CopyTo(Array array, int index)
|
||||
{
|
||||
items.CopyTo(array, index);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IList.Add(object? value)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IList.Clear()
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IList.Insert(int index, object? value)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IList.Remove(object? value)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IList.RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Add(T item)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Remove(T item)
|
||||
{
|
||||
throw new NotSupportedException("ModdedArray is of a fixed size.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the elements of a <see cref="ModdedArray{T}"/>.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly ModdedArray<T> array;
|
||||
private readonly int version;
|
||||
|
||||
private int index;
|
||||
private T? current;
|
||||
|
||||
internal Enumerator(ModdedArray<T> moddedArray)
|
||||
{
|
||||
array = moddedArray;
|
||||
version = moddedArray.version;
|
||||
index = 0;
|
||||
current = default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly T Current => current!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly object? IEnumerator.Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index == 0 || index == array.Count + 1)
|
||||
throw new InvalidOperationException("Enumeration has not started or has already finished.");
|
||||
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (version != array.version)
|
||||
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
|
||||
|
||||
if ((uint)index < (uint)array.Count)
|
||||
{
|
||||
current = array[index];
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
index = array.Count + 1;
|
||||
current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Reset()
|
||||
{
|
||||
if (version != array.version)
|
||||
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute.");
|
||||
|
||||
index = 0;
|
||||
current = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a builder for <see cref="ModdedArray{T}"/> to support C# 12 collection expressions.
|
||||
/// </summary>
|
||||
public static class ModdedArrayBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ModdedArray{T}"/> from a span of values.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The the type of the values.</typeparam>
|
||||
/// <param name="values">The values to initialise with.</param>
|
||||
/// <returns>The <see cref="ModdedArray{T}"/> instance initialised with the provided values.</returns>
|
||||
public static ModdedArray<T> Create<T>(ReadOnlySpan<T> values)
|
||||
{
|
||||
return new(values);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BepInEx.Configuration;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.GameModes;
|
||||
@@ -24,9 +25,15 @@ namespace MiraAPI.GameOptions;
|
||||
/// </summary>
|
||||
public static class ModdedOptionsManager
|
||||
{
|
||||
private static readonly Dictionary<PropertyInfo, PropertyOptionAttribute> OptionAttributes = [];
|
||||
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> GetterLookups = [];
|
||||
private static readonly Dictionary<MethodBase, PropertyOptionAttribute> SetterLookups = [];
|
||||
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));
|
||||
|
||||
internal static readonly ConditionalWeakTable<object, ModdedOptionListAttribute> ListInstanceCache = [];
|
||||
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
|
||||
internal static readonly Dictionary<OptionBehaviour, ModdedStringOption> CreatedStringOptions = [];
|
||||
internal static readonly Dictionary<Type, List<AbstractOptionGroup>> GameModeOptionGroups = [];
|
||||
@@ -61,14 +68,14 @@ public static class ModdedOptionsManager
|
||||
|
||||
internal static bool RegisterGroup(Type type)
|
||||
{
|
||||
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
|
||||
if (TypeToGroup.ContainsKey(type))
|
||||
{
|
||||
Error($"Group {type.Name} already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TypeToGroup.ContainsKey(type))
|
||||
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -78,17 +85,17 @@ public static class ModdedOptionsManager
|
||||
|
||||
internal static bool RegisterGroup(Type type, MiraPluginInfo pluginInfo)
|
||||
{
|
||||
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TypeToGroup.ContainsKey(type))
|
||||
{
|
||||
Error($"Group {type.Name} already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Activator.CreateInstance(type) is not AbstractOptionGroup group)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof(AbstractGameMode).IsAssignableFrom(group.OptionableType))
|
||||
{
|
||||
if (GameModeOptionGroups.TryGetValue(group.OptionableType, out var oldList))
|
||||
@@ -116,7 +123,7 @@ public static class ModdedOptionsManager
|
||||
Groups.Add(group);
|
||||
TypeToGroup.Add(type, group);
|
||||
typeof(OptionGroupSingleton<>).MakeGenericType(type)
|
||||
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)! // Message suppression points to here
|
||||
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.SetValue(null, group);
|
||||
}
|
||||
|
||||
@@ -143,7 +150,7 @@ public static class ModdedOptionsManager
|
||||
PropertyInfo property,
|
||||
MiraPluginInfo pluginInfo)
|
||||
{
|
||||
if (OptionAttributes.ContainsKey(property))
|
||||
if (!OptionAttributes.Add(property))
|
||||
{
|
||||
Error($"Property {property.Name} already has an attribute registered.");
|
||||
return;
|
||||
@@ -163,22 +170,40 @@ public static class ModdedOptionsManager
|
||||
return;
|
||||
}
|
||||
|
||||
var setterOriginal = property.GetSetMethod();
|
||||
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
|
||||
var getterOriginal = property.GetGetMethod(true)!;
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
|
||||
GetterLookups[getterOriginal] = attribute;
|
||||
|
||||
var getterOriginal = property.GetGetMethod();
|
||||
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
|
||||
var setterOriginal = property.GetSetMethod(true);
|
||||
|
||||
OptionAttributes.Add(property, attribute);
|
||||
attribute.HolderOption = option;
|
||||
|
||||
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
|
||||
var visibilityFunc = visibilityAttr?.GetVisibility(group, property);
|
||||
if (visibilityFunc != null)
|
||||
if (setterOriginal != null)
|
||||
{
|
||||
option.Visible = visibilityFunc;
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
|
||||
SetterLookups[setterOriginal] = attribute;
|
||||
}
|
||||
|
||||
attribute.HolderOption = option;
|
||||
attribute.ValueType = property.PropertyType;
|
||||
|
||||
var customAttributes = property.GetCustomAttributes(false);
|
||||
|
||||
foreach (var attr in customAttributes)
|
||||
{
|
||||
switch (attr)
|
||||
{
|
||||
case ModdedOptionVisibilityAttribute visibilityAttr:
|
||||
option.Visible = visibilityAttr.GetVisibility(group, property) ?? DelegateUtilities.AlwaysTrue;
|
||||
break;
|
||||
case ModdedOptionGetterAttribute getterAttr:
|
||||
option.Getter = getterAttr.GetGetter(group, property, option);
|
||||
break;
|
||||
case ModdedOptionSetterAttribute setterAttr:
|
||||
option.Setter = setterAttr.GetSetter(group, property, option);
|
||||
break;
|
||||
case ModdedOptionChangedAttribute changedAttr:
|
||||
option.ChangedEvent = changedAttr.GetChanged(group, property, option);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
RegisterOption(option, group, property.Name, pluginInfo);
|
||||
@@ -198,9 +223,11 @@ public static class ModdedOptionsManager
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < optionList.Count; i++)
|
||||
var configName = optionList.ConfigName ?? (i => property.Name + (i + 1));
|
||||
|
||||
for (var i = 0; i < optionList.Count; i++)
|
||||
{
|
||||
RegisterOption(optionList[i], group, property.Name + i, pluginInfo);
|
||||
RegisterOption(optionList[i], group, configName(i), pluginInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +237,7 @@ public static class ModdedOptionsManager
|
||||
PropertyInfo property,
|
||||
MiraPluginInfo pluginInfo)
|
||||
{
|
||||
if (OptionAttributes.ContainsKey(property))
|
||||
if (!OptionAttributes.Add(property))
|
||||
{
|
||||
Error($"Property {property.Name} already has an attribute registered.");
|
||||
return;
|
||||
@@ -245,39 +272,73 @@ public static class ModdedOptionsManager
|
||||
return;
|
||||
}
|
||||
|
||||
var setterOriginal = property.GetSetMethod();
|
||||
var setterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertySetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: new HarmonyMethod(setterPatch));
|
||||
var listType = propertyList.GetType();
|
||||
var valueType = listType.GetGenericArguments()[0];
|
||||
|
||||
var getterOriginal = property.GetGetMethod();
|
||||
var getterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyGetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: new HarmonyMethod(getterPatch));
|
||||
if (!listType.IsGenericType || listType.GetGenericTypeDefinition() != typeof(ModdedArray<>))
|
||||
{
|
||||
Error($"Please use ModdedList<{valueType.Name}> for modded option lists.");
|
||||
return;
|
||||
}
|
||||
|
||||
var listIndex = propertyList.GetType().GetProperty("Item")!;
|
||||
var getterOriginal = property.GetGetMethod(true)!;
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(getterOriginal, prefix: CachedGetterPatch);
|
||||
GetterLookups[getterOriginal] = attribute;
|
||||
|
||||
var listSetterOriginal = listIndex.GetSetMethod();
|
||||
var listSetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListSetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listSetterOriginal, postfix: new HarmonyMethod(listSetterPatch));
|
||||
var setterOriginal = property.GetSetMethod(true);
|
||||
|
||||
var listGetterOriginal = listIndex.GetSetMethod();
|
||||
var listGetterPatch = typeof(ModdedOptionsManager).GetMethod(nameof(PropertyListGetterPatch));
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(listGetterOriginal, prefix: new HarmonyMethod(listGetterPatch));
|
||||
if (setterOriginal != null)
|
||||
{
|
||||
PluginSingleton<MiraApiPlugin>.Instance.Harmony.Patch(setterOriginal, postfix: CachedSetterPatch);
|
||||
SetterLookups[setterOriginal] = attribute;
|
||||
}
|
||||
|
||||
OptionAttributes.Add(property, attribute);
|
||||
attribute.HolderOptionList = optionList;
|
||||
attribute.Value = propertyVal;
|
||||
attribute.Value = propertyList;
|
||||
attribute.ValueType = valueType;
|
||||
|
||||
var visibilityAttr = property.GetCustomAttribute<ModdedOptionVisibilityAttribute>();
|
||||
var visibilityFunc = visibilityAttr?.GetListVisibility(group, property);
|
||||
for (int i = 0; i < optionList.Count; i++)
|
||||
ListInstanceCache.Add(propertyVal, attribute);
|
||||
|
||||
var customAttributes = property.GetCustomAttributes(false);
|
||||
|
||||
Func<int, Delegate?>? visibilityFactory = null, getterFactory = null, setterFactory = null, changedFactory = null;
|
||||
|
||||
foreach (var attr in customAttributes)
|
||||
{
|
||||
switch (attr)
|
||||
{
|
||||
case ModdedOptionVisibilityAttribute visibilityAttr:
|
||||
visibilityFactory = visibilityAttr.GetListVisibility(group, property);
|
||||
break;
|
||||
case ModdedOptionGetterAttribute getterAttr:
|
||||
getterFactory = getterAttr.GetListGetter(group, property, valueType);
|
||||
break;
|
||||
case ModdedOptionSetterAttribute setterAttr:
|
||||
setterFactory = setterAttr.GetListSetter(group, property, valueType);
|
||||
break;
|
||||
case ModdedOptionChangedAttribute changedAttr:
|
||||
changedFactory = changedAttr.GetListChanged(group, property, valueType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < optionList.Count; i++)
|
||||
{
|
||||
var option = optionList[i];
|
||||
if (visibilityFunc != null)
|
||||
{
|
||||
option.Visible = () => visibilityFunc(i);
|
||||
}
|
||||
|
||||
RegisterOption(option, group, property.Name + i, pluginInfo);
|
||||
if (visibilityFactory != null)
|
||||
option.Visible = (Func<bool>)(visibilityFactory(i) ?? DelegateUtilities.AlwaysTrue);
|
||||
|
||||
if (getterFactory != null)
|
||||
option.Getter = getterFactory(i);
|
||||
|
||||
if (setterFactory != null)
|
||||
option.Setter = setterFactory(i);
|
||||
|
||||
if (changedFactory != null)
|
||||
option.ChangedEvent = changedFactory(i);
|
||||
|
||||
RegisterOption(option, group, property.Name + (i + 1), pluginInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,11 +409,16 @@ public static class ModdedOptionsManager
|
||||
/// <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 = "Harmony naming convention.")]
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
|
||||
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
|
||||
{
|
||||
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
|
||||
if (!SetterLookups.TryGetValue(__originalMethod, out var attribute))
|
||||
return;
|
||||
|
||||
attribute.SetValue(value);
|
||||
|
||||
if (attribute is ModdedOptionListAttribute listAttribute && value != null)
|
||||
ListInstanceCache.AddOrUpdate(value, listAttribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -362,43 +428,13 @@ public static class ModdedOptionsManager
|
||||
/// <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 = "Harmony naming convention.")]
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Read above.")]
|
||||
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
|
||||
{
|
||||
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
|
||||
if (!GetterLookups.TryGetValue(__originalMethod, out var attribute))
|
||||
return true;
|
||||
|
||||
__result = attribute.GetValue();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches the setter of a list property to update the value of the option.
|
||||
/// </summary>
|
||||
/// <param name="__instance">The list's instance.</param>
|
||||
/// <param name="index">The index to find in the list.</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 PropertyListSetterPatch(object __instance, int index, object value)
|
||||
{
|
||||
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
|
||||
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
|
||||
attribute.SetValue(index, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patches the getter of a list property to return the value of the option.
|
||||
/// </summary>
|
||||
/// <param name="__instance">The list's instance.</param>
|
||||
/// <param name="index">The index to find in the list.</param>
|
||||
/// <param name="__result">The result of the property getter.</param>
|
||||
/// <returns>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 PropertyListGetterPatch(object __instance, int index, ref object __result)
|
||||
{
|
||||
var attribute = (ModdedOptionListAttribute)OptionAttributes.First(
|
||||
pair => pair.Value is ModdedOptionListAttribute list && ReferenceEquals(list.Value, __instance)).Value;
|
||||
__result = attribute.GetValue(index);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,11 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
|
||||
|
||||
stringOption.SetUpFromData(Data, 20);
|
||||
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
// Changing the button text to make them more distinct from number options
|
||||
stringOption.MinusBtn.ChangeButtonText("<");
|
||||
stringOption.PlusBtn.ChangeButtonText(">");
|
||||
|
||||
// SetUpFromData method doesn't work correctly so we must set the values manually
|
||||
stringOption.Title = StringName;
|
||||
@@ -113,7 +117,7 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The <see cref="Enum"/> type.</typeparam>
|
||||
public class ModdedEnumOption<T> : ModdedOption<T>
|
||||
where T : Enum
|
||||
where T : struct, Enum
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the string values of the <see langword="enum"/>.
|
||||
@@ -130,7 +134,7 @@ public class ModdedEnumOption<T> : ModdedOption<T>
|
||||
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Values = values ?? Enum.GetNames(typeof(T));
|
||||
Values = values ?? Enum.GetNames<T>();
|
||||
|
||||
var data = ScriptableObject.CreateInstance<StringGameSetting>();
|
||||
data.Title = StringName;
|
||||
@@ -149,7 +153,11 @@ public class ModdedEnumOption<T> : ModdedOption<T>
|
||||
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
|
||||
|
||||
stringOption.SetUpFromData(Data, 20);
|
||||
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
// Changing the button text to make them more distinct from number options
|
||||
stringOption.MinusBtn.ChangeButtonText("<");
|
||||
stringOption.PlusBtn.ChangeButtonText(">");
|
||||
|
||||
// SetUpFromData method doesn't work correctly so we must set the values manually
|
||||
stringOption.Title = StringName;
|
||||
@@ -176,13 +184,13 @@ public class ModdedEnumOption<T> : ModdedOption<T>
|
||||
/// <inheritdoc />
|
||||
public override void HandleNetData(byte[] data)
|
||||
{
|
||||
SetValue((T)Enum.Parse(typeof(T), Encoding.Unicode.GetString(data)));
|
||||
SetValue(Enum.Parse<T>(Encoding.Unicode.GetString(data)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
|
||||
{
|
||||
return (T)Enum.Parse(typeof(T), optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
|
||||
return Enum.Parse<T>(optionBehaviour.GetInt().ToString(NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -61,10 +61,10 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="zeroBehavior">Determines what is shown for zero. Options: ∞, #, or a word value (such as Off).</param>
|
||||
/// <param name="negativeBehavior">Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off).</param>
|
||||
/// <param name="zeroBehavior">Determines what is shown for zero.</param>
|
||||
/// <param name="negativeBehavior">Determines what is shown for negative 1.</param>
|
||||
/// <param name="halfIncrements">Whether increments can be split in half.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedNumberOption(
|
||||
string title,
|
||||
@@ -85,14 +85,12 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
Increment = increment;
|
||||
SuffixType = suffixType;
|
||||
ZeroWordValue = zeroBehavior;
|
||||
NegativeWordValue = negativeBehavior;
|
||||
ShiftIncrement = halfIncrements;
|
||||
NegativeWordValue = negativeBehavior;
|
||||
|
||||
Value = Mathf.Clamp(defaultValue, min, max);
|
||||
|
||||
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
|
||||
|
||||
var data = (FloatGameSetting)Data;
|
||||
var data = ScriptableObject.CreateInstance<FloatGameSetting>();
|
||||
data.Type = global::OptionTypes.Float;
|
||||
data.Title = StringName;
|
||||
data.Value = Value;
|
||||
@@ -108,6 +106,8 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
data.ZeroIsInfinity = ZeroInfinity;
|
||||
data.SuffixType = (NumberSuffixes)SuffixType;
|
||||
data.OptionName = FloatOptionNames.Invalid;
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -119,8 +119,8 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="increment">The increment.</param>
|
||||
/// <param name="suffixType">The suffix type.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
|
||||
/// <param name="formatString">Optional format string for the option screen.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedNumberOption(
|
||||
string title,
|
||||
@@ -132,36 +132,8 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false,
|
||||
bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
: this(title, defaultValue, min, max, increment, zeroInfinity ? "∞" : "#", "#", suffixType, formatString, increment == 1f, includeInPreset)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
Increment = increment;
|
||||
SuffixType = suffixType;
|
||||
ZeroWordValue = zeroInfinity ? "∞" : "#";
|
||||
NegativeWordValue = "#";
|
||||
ShiftIncrement = Mathf.Approximately(increment, 1f);
|
||||
|
||||
Value = Mathf.Clamp(defaultValue, min, max);
|
||||
|
||||
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
|
||||
|
||||
var data = (FloatGameSetting)Data;
|
||||
data.Type = global::OptionTypes.Float;
|
||||
data.Title = StringName;
|
||||
data.Value = Value;
|
||||
data.Increment = Increment;
|
||||
data.ValidRange = new FloatRange(Min, Max);
|
||||
data.FormatString = formatString ??
|
||||
(defaultValue.IsInteger() &&
|
||||
Increment.IsInteger() &&
|
||||
Value.IsInteger() &&
|
||||
Min.IsInteger() &&
|
||||
Max.IsInteger() ? "0" : "0.0");
|
||||
|
||||
data.ZeroIsInfinity = ZeroInfinity;
|
||||
data.SuffixType = (NumberSuffixes)SuffixType;
|
||||
data.OptionName = FloatOptionNames.Invalid;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BepInEx.Configuration;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Networking.Rpc;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -43,16 +45,32 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the option.
|
||||
/// </summary>
|
||||
public T Value { get; protected set; }
|
||||
public T Value
|
||||
{
|
||||
get => Getter != null ? Getter(_value) : _value;
|
||||
protected set
|
||||
{
|
||||
if (Setter != null)
|
||||
value = Setter(value, _value);
|
||||
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private T _value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default value of the option.
|
||||
/// </summary>
|
||||
public T DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the event that is invoked when the value of the option changes.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="IModdedOption.Getter"/>
|
||||
public Func<T, T>? Getter { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IModdedOption.Setter"/>
|
||||
public Func<T, T, T>? Setter { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IModdedOption.ChangedEvent"/>
|
||||
public Action<T>? ChangedEvent { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -67,6 +85,33 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// <inheritdoc />
|
||||
public ConfigDefinition? ConfigDefinition { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public AbstractOptionGroup ParentGroup { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
|
||||
|
||||
/// <inheritdoc />
|
||||
Delegate? IModdedOption.Getter
|
||||
{
|
||||
get => Getter;
|
||||
set => Getter = value as Func<T, T>;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
Delegate? IModdedOption.Setter
|
||||
{
|
||||
get => Setter;
|
||||
set => Setter = value as Func<T, T, T>;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
Delegate? IModdedOption.ChangedEvent
|
||||
{
|
||||
get => ChangedEvent;
|
||||
set => ChangedEvent = value as Action<T>;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
|
||||
/// </summary>
|
||||
@@ -80,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
DefaultValue = defaultValue;
|
||||
Value = defaultValue;
|
||||
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
|
||||
Visible = () => true;
|
||||
Visible = DelegateUtilities.AlwaysTrue;
|
||||
IncludeInPreset = includeInPreset;
|
||||
}
|
||||
|
||||
@@ -96,25 +141,25 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// <param name="sendRpc">Whether to send the value to other players.</param>
|
||||
public void SetValue(T newValue, bool sendRpc = true)
|
||||
{
|
||||
var oldVal = Value;
|
||||
var oldVal = _value;
|
||||
Value = newValue;
|
||||
|
||||
if (Value?.Equals(oldVal) == false)
|
||||
if (!EqualityComparer<T>.Default.Equals(_value, oldVal))
|
||||
{
|
||||
ChangedEvent?.Invoke(Value);
|
||||
ChangedEvent?.Invoke(_value);
|
||||
}
|
||||
|
||||
if (sendRpc && AmongUsClient.Instance.AmHost)
|
||||
{
|
||||
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
|
||||
{
|
||||
entry.Value = Value;
|
||||
entry.Value = _value;
|
||||
}
|
||||
|
||||
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
|
||||
}
|
||||
|
||||
OnValueChanged(newValue);
|
||||
OnValueChanged(_value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -149,6 +194,12 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetValueType()
|
||||
{
|
||||
return typeof(T);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract float GetFloatData();
|
||||
|
||||
@@ -188,10 +239,4 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
{
|
||||
return option.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AbstractOptionGroup ParentGroup { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual OptionNotifConfiguration Configuration => new(ParentGroup);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace MiraAPI.GameOptions.OptionTypes;
|
||||
/// Represents a modded option list.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of options.</typeparam>
|
||||
public class ModdedOptionList<T> : IModdedOptionList
|
||||
public class ModdedOptionList<T> : IModdedOptionList<T>
|
||||
where T : IModdedOption
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
@@ -20,6 +20,9 @@ public class ModdedOptionList<T> : IModdedOptionList
|
||||
/// </summary>
|
||||
public IReadOnlyList<T> Options { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Func<int, string>? ConfigName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedOptionList{T}"/> class.
|
||||
/// </summary>
|
||||
@@ -41,17 +44,6 @@ public class ModdedOptionList<T> : IModdedOptionList
|
||||
Options = [.. options];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<IModdedOption> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Options.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indexes the option of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
@@ -59,5 +51,24 @@ public class ModdedOptionList<T> : IModdedOptionList
|
||||
/// <returns>option of type <typeparamref name="T"/>.</returns>
|
||||
public T this[int idx] => Options[idx];
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return Options.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Options.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
|
||||
IEnumerator<IModdedOption> IEnumerable<IModdedOption>.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<IModdedOption>)Options).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IReadOnlyList{T}.this"/>
|
||||
IModdedOption IReadOnlyList<IModdedOption>.this[int index] => this[index];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Il2CppSystem.Collections.Generic;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
@@ -29,6 +29,11 @@ public class ModdedPlayerOption : ModdedOption<int>
|
||||
/// </summary>
|
||||
public bool AllowNone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player selected by this option. May be <c>null</c> if allowed to have <c>None</c> as an option.
|
||||
/// </summary>
|
||||
public NetworkedPlayerInfo? SelectedPlayer => Value == -1 ? null : Values[Value];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
|
||||
/// </summary>
|
||||
@@ -37,15 +42,16 @@ public class ModdedPlayerOption : ModdedOption<int>
|
||||
public ModdedPlayerOption(string title, bool allowNone = true)
|
||||
: base(title, allowNone ? -1 : 0, false)
|
||||
{
|
||||
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
|
||||
var data = (PlayerSelectionGameSetting)Data;
|
||||
Values = [];
|
||||
AllowNone = allowNone;
|
||||
|
||||
Value = DefaultValue;
|
||||
|
||||
var data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.Player;
|
||||
|
||||
Values = new List<NetworkedPlayerInfo>();
|
||||
AllowNone = allowNone;
|
||||
Value = DefaultValue;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -60,17 +66,16 @@ public class ModdedPlayerOption : ModdedOption<int>
|
||||
playerOption.name =
|
||||
$"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
|
||||
|
||||
var filteredList = GetFilteredPlayers();
|
||||
Values = filteredList.ToIl2CppList();
|
||||
Values = GetFilteredPlayers();
|
||||
|
||||
playerOption.SetUpFromData(Data, 20);
|
||||
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
playerOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
playerOption.Title = StringName;
|
||||
playerOption.TitleText.text = Title.Translate();
|
||||
playerOption.Values = Values;
|
||||
playerOption.Values = Values.ToIl2CppList();
|
||||
playerOption.Value = Value;
|
||||
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
|
||||
playerOption.playerIndex = Values.FindIndex(p => p.PlayerId == Value);
|
||||
|
||||
OptionBehaviour = playerOption;
|
||||
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
|
||||
@@ -85,9 +90,16 @@ public class ModdedPlayerOption : ModdedOption<int>
|
||||
/// Returns a player ran through the filter.
|
||||
/// </summary>
|
||||
/// <returns>A list of filtered players.</returns>
|
||||
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
|
||||
public List<NetworkedPlayerInfo> GetFilteredPlayers()
|
||||
{
|
||||
return [.. GameData.Instance.AllPlayers.ToArray().Where(x => PlayerFilter?.Invoke(x) ?? true)];
|
||||
var players = GameData.Instance.AllPlayers.ToArray();
|
||||
|
||||
IEnumerable<NetworkedPlayerInfo> filtered = players;
|
||||
|
||||
if (PlayerFilter != null)
|
||||
filtered = players.Where(PlayerFilter);
|
||||
|
||||
return [.. filtered.OrderBy(x => x.PlayerId)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,6 +25,28 @@ public class ModdedStringOption : ModdedOption<string>
|
||||
/// </summary>
|
||||
public StringNames[] StringNameValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="defaultValue">The default value as a string.</param>
|
||||
/// <param name="values">An option list of string values to use.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Values = values;
|
||||
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
|
||||
|
||||
var data = ScriptableObject.CreateInstance<StringGameSetting>();
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.String;
|
||||
data.Values = StringNameValues;
|
||||
data.Index = GetIndex(Value);
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string's index.
|
||||
/// </summary>
|
||||
@@ -61,61 +83,37 @@ public class ModdedStringOption : ModdedOption<string>
|
||||
StringNameValues = [.. newStrVals];
|
||||
}
|
||||
|
||||
if (Data != null)
|
||||
{
|
||||
var data = (StringGameSetting)Data;
|
||||
data.Values = StringNameValues;
|
||||
|
||||
data.Index = GetIndex(Value);
|
||||
if (OptionBehaviour != null)
|
||||
{
|
||||
var strOpt = OptionBehaviour.TryCast<StringOption>();
|
||||
if (strOpt != null)
|
||||
{
|
||||
strOpt.Values = StringNameValues;
|
||||
strOpt.Value = GetIndex(Value);
|
||||
Warning("StringOption is valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
Error("StringOption is null.");
|
||||
}
|
||||
|
||||
Warning("OptionBehaviour is valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
Error("OptionBehaviour is null.");
|
||||
}
|
||||
|
||||
Warning("Data is valid");
|
||||
}
|
||||
else
|
||||
if (Data == null)
|
||||
{
|
||||
Error("Data is null.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedStringOption"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="defaultValue">The default value as a string.</param>
|
||||
/// <param name="values">An option list of string values to use.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedStringOption(string title, string defaultValue, string[] values, bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Values = values;
|
||||
Data = ScriptableObject.CreateInstance<StringGameSetting>();
|
||||
var data = (StringGameSetting)Data;
|
||||
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.String;
|
||||
StringNameValues = [.. values.Select(MiraLocaleManager.GetOrCreateLocaleString)];
|
||||
data.Values = StringNameValues;
|
||||
|
||||
data.Index = GetIndex(Value);
|
||||
|
||||
Warning("Data is valid");
|
||||
|
||||
if (OptionBehaviour == null)
|
||||
{
|
||||
Error("OptionBehaviour is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
var strOpt = OptionBehaviour.TryCast<StringOption>();
|
||||
|
||||
Warning("OptionBehaviour is valid");
|
||||
|
||||
if (strOpt == null)
|
||||
{
|
||||
Error("StringOption is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
strOpt.Values = StringNameValues;
|
||||
strOpt.Value = GetIndex(Value);
|
||||
Warning("StringOption is valid");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -126,7 +124,11 @@ public class ModdedStringOption : ModdedOption<string>
|
||||
$"{ParentMod!.OptionsTitleText}.StringOption.{TranslationController.Instance.GetString(StringName)}";
|
||||
|
||||
stringOption.SetUpFromData(Data, 20);
|
||||
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
stringOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
// Changing the button text to make them more distinct from number options
|
||||
stringOption.MinusBtn.ChangeButtonText("<");
|
||||
stringOption.PlusBtn.ChangeButtonText(">");
|
||||
|
||||
// SetUpFromData method doesn't work correctly so we must set the values manually
|
||||
stringOption.Title = StringName;
|
||||
|
||||
@@ -11,6 +11,37 @@ namespace MiraAPI.GameOptions.OptionTypes;
|
||||
/// </summary>
|
||||
public class ModdedToggleOption : ModdedOption<bool>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display value when the option is toggled on.
|
||||
/// </summary>
|
||||
public string OnValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display value when the option is toggled off.
|
||||
/// </summary>
|
||||
public string OffValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
/// <param name="on">What the option displays when toggled on.</param>
|
||||
/// <param name="off">What the option displays when toggled off.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedToggleOption(string title, bool defaultValue, string on, string off, bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
OnValue = on;
|
||||
OffValue = off;
|
||||
|
||||
var data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.Checkbox;
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedToggleOption"/> class.
|
||||
/// </summary>
|
||||
@@ -18,13 +49,8 @@ public class ModdedToggleOption : ModdedOption<bool>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset = true)
|
||||
: base(title, defaultValue, includeInPreset)
|
||||
: this(title, defaultValue, "On", "Off", includeInPreset)
|
||||
{
|
||||
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
|
||||
|
||||
var data = (CheckboxGameSetting)Data;
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.Checkbox;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -39,7 +65,7 @@ public class ModdedToggleOption : ModdedOption<bool>
|
||||
toggleOption.Title = StringName;
|
||||
toggleOption.TitleText.text = Title.Translate();
|
||||
toggleOption.CheckMark.enabled = Value;
|
||||
toggleOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
toggleOption.OnValueChanged = (Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
OptionBehaviour = toggleOption;
|
||||
|
||||
@@ -76,7 +102,7 @@ public class ModdedToggleOption : ModdedOption<bool>
|
||||
ModdedOptionsManager.AddSettingsChangeMessage(
|
||||
HudManager.Instance.Notifier,
|
||||
StringName,
|
||||
newValue ? "On" : "Off",
|
||||
(newValue ? OnValue : OffValue).Translate(),
|
||||
Configuration.PopUpTextColor,
|
||||
Configuration.PopUpIconTmp,
|
||||
false);
|
||||
|
||||
@@ -126,7 +126,7 @@ public sealed class HideAndSeekHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
|
||||
currentFinalHideTime = totalFinalHideTime;
|
||||
if (timerBar != null)
|
||||
{
|
||||
Object.Destroy(timerBar);
|
||||
Destroy(timerBar);
|
||||
}
|
||||
|
||||
finalHideAlertSfx = GameManagerCreator.Instance.HideAndSeekManagerPrefab.FinalHideAlertSFX;
|
||||
|
||||
@@ -139,10 +139,7 @@ public abstract class CustomActionButton
|
||||
/// <param name="parent">The parent of the button.</param>
|
||||
public virtual void CreateButton(Transform parent)
|
||||
{
|
||||
if (Button)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Button) return;
|
||||
|
||||
UsesLeft = MaxUses;
|
||||
Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown;
|
||||
@@ -152,19 +149,15 @@ public abstract class CustomActionButton
|
||||
Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent);
|
||||
Button.name = Name + "Button";
|
||||
Button.OverrideText(Name.Translate().ToUpperInvariant());
|
||||
Object.Destroy(Button.glyph.GetComponent<SpriteRenderer>());
|
||||
Object.Destroy(Button.glyph.GetComponent<Image>());
|
||||
|
||||
Button.graphic.sprite = Sprite.LoadAsset();
|
||||
|
||||
Button.SetUsesRemaining(MaxUses);
|
||||
if (MaxUses <= 0)
|
||||
{
|
||||
Button.SetInfiniteUses();
|
||||
}
|
||||
if (MaxUses <= 0) Button.SetInfiniteUses();
|
||||
|
||||
if (TextOutlineColor != Color.clear)
|
||||
{
|
||||
SetTextOutline(TextOutlineColor);
|
||||
}
|
||||
if (TextOutlineColor != Color.clear) SetTextOutline(TextOutlineColor);
|
||||
|
||||
var pb = Button.GetComponent<PassiveButton>();
|
||||
pb.OnClick = new Button.ButtonClickedEvent();
|
||||
@@ -173,10 +166,7 @@ public abstract class CustomActionButton
|
||||
// Invoke the generic button click event.
|
||||
var genericEvent = new MiraButtonClickEvent(this);
|
||||
MiraEventManager.InvokeEvent(genericEvent);
|
||||
if (genericEvent.IsCancelled)
|
||||
{
|
||||
MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
|
||||
}
|
||||
if (genericEvent.IsCancelled) MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
|
||||
|
||||
// Invoke the button click event for specific button.
|
||||
var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
|
||||
@@ -191,68 +181,47 @@ public abstract class CustomActionButton
|
||||
|
||||
if (specificInvoked)
|
||||
{
|
||||
if (!@event.IsCancelled)
|
||||
{
|
||||
ClickHandler();
|
||||
}
|
||||
if (!@event.IsCancelled) ClickHandler();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!genericEvent.IsCancelled)
|
||||
{
|
||||
ClickHandler();
|
||||
}
|
||||
if (!genericEvent.IsCancelled) ClickHandler();
|
||||
}
|
||||
}));
|
||||
|
||||
if (Keybind == null) return;
|
||||
Keybind.OnActivate(() =>
|
||||
{
|
||||
if (!Enabled(PlayerControl.LocalPlayer.Data.Role)) return;
|
||||
Keybind.RemoveOnActivate(OnKeybind);
|
||||
Keybind.OnActivate(OnKeybind);
|
||||
|
||||
// Invoke the generic button click event.
|
||||
var genericEvent = new MiraButtonClickEvent(this);
|
||||
MiraEventManager.InvokeEvent(genericEvent);
|
||||
if (genericEvent.IsCancelled)
|
||||
{
|
||||
MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
|
||||
}
|
||||
|
||||
// Invoke the button click event for specific button.
|
||||
var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
|
||||
var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this, genericEvent)!;
|
||||
var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType);
|
||||
if (@event.IsCancelled)
|
||||
{
|
||||
var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()];
|
||||
var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!;
|
||||
MiraEventManager.InvokeEvent(cancelEvent, cancelEventType);
|
||||
}
|
||||
|
||||
if (specificInvoked)
|
||||
{
|
||||
if (!@event.IsCancelled)
|
||||
{
|
||||
ClickHandler();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!genericEvent.IsCancelled)
|
||||
{
|
||||
ClickHandler();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindIcon =
|
||||
Helpers.CreateKeybindIcon(
|
||||
Button.gameObject,
|
||||
Keybind.CurrentKey,
|
||||
new Vector3(0.4f, 0.45f, -9f));
|
||||
KeybindIcon = Helpers.CreateKeybindIcon(Button.gameObject, Keybind.CurrentKey, new Vector3(0.4f, 0.45f, -9f));
|
||||
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
|
||||
HudManagerPatches.ModdedKeybindIcons.Add(KeybindText);
|
||||
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
|
||||
KeybindText.fontSizeMax = KeybindText.fontSize;
|
||||
KeybindText.fontSizeMin = KeybindText.fontSize * 0.5f;
|
||||
KeybindText.enableAutoSizing = true;
|
||||
Button.usesRemainingSprite.transform.localPosition = new Vector3(-0.341f, 0.45f, -0.1f);
|
||||
UpdateKeybindIcon();
|
||||
}
|
||||
|
||||
private void OnKeybind()
|
||||
{
|
||||
if (Button == null || !Enabled(PlayerControl.LocalPlayer.Data.Role)) return;
|
||||
|
||||
Button.GetComponent<PassiveButton>().OnClick.Invoke();
|
||||
}
|
||||
|
||||
internal void UpdateKeybindIcon()
|
||||
{
|
||||
if (Keybind == null || KeybindText == null) return;
|
||||
|
||||
var text = ActiveInputManager.currentControlType switch
|
||||
{
|
||||
ActiveInputManager.InputType.Keyboard when Keybind.CurrentKey != KeyboardKeyCode.None => Keybind.CurrentKey.ToString(),
|
||||
ActiveInputManager.InputType.Joystick => Keybind.CurrentControllerButton,
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
KeybindText.text = text;
|
||||
KeybindIcon?.SetActive(text.Length > 0 && LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -262,22 +231,13 @@ public abstract class CustomActionButton
|
||||
/// <param name="moveButton">Whether the button's position should change in-game.</param>
|
||||
public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true)
|
||||
{
|
||||
if (!HudManager.InstanceExists || Button == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!HudManager.InstanceExists || Button == null) return;
|
||||
|
||||
Location = location;
|
||||
|
||||
if (!moveButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!moveButton) return;
|
||||
|
||||
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null) return;
|
||||
|
||||
switch (location)
|
||||
{
|
||||
@@ -305,10 +265,7 @@ public abstract class CustomActionButton
|
||||
public virtual void ResetCooldownAndOrEffect()
|
||||
{
|
||||
Timer = Cooldown;
|
||||
if (EffectActive)
|
||||
{
|
||||
OnEffectEnd();
|
||||
}
|
||||
if (EffectActive) OnEffectEnd();
|
||||
|
||||
EffectActive = false;
|
||||
}
|
||||
@@ -328,10 +285,7 @@ public abstract class CustomActionButton
|
||||
/// <param name="sprite">The new <see cref="UnityEngine.Sprite"/> to override with.</param>
|
||||
public virtual void OverrideSprite(Sprite sprite)
|
||||
{
|
||||
if (Button != null)
|
||||
{
|
||||
Button.graphic.sprite = sprite;
|
||||
}
|
||||
if (Button != null) Button.graphic.sprite = sprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -388,10 +342,7 @@ public abstract class CustomActionButton
|
||||
UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue);
|
||||
Button?.SetUsesRemaining(UsesLeft);
|
||||
|
||||
if (Button != null)
|
||||
{
|
||||
Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
|
||||
}
|
||||
if (Button != null) Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -469,8 +420,7 @@ public abstract class CustomActionButton
|
||||
/// <returns>A value that represents whether the button should light up or not.</returns>
|
||||
public virtual bool CanUse()
|
||||
{
|
||||
return PlayerControl.LocalPlayer.moveable &&
|
||||
((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
|
||||
return PlayerControl.LocalPlayer.moveable && ((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -498,10 +448,7 @@ public abstract class CustomActionButton
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanClick())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!CanClick()) return;
|
||||
|
||||
if (LimitedUses)
|
||||
{
|
||||
@@ -530,20 +477,9 @@ public abstract class CustomActionButton
|
||||
/// <param name="playerControl">The local PlayerControl.</param>
|
||||
public virtual void FixedUpdateHandler(PlayerControl playerControl)
|
||||
{
|
||||
if (Keybind != null && KeybindText != null)
|
||||
{
|
||||
KeybindText.text = Keybind.CurrentKey.ToString();
|
||||
KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard &&
|
||||
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value &&
|
||||
Keybind.CurrentKey != KeyboardKeyCode.None);
|
||||
}
|
||||
|
||||
if (Timer >= 0)
|
||||
{
|
||||
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent))
|
||||
{
|
||||
Timer -= Time.deltaTime;
|
||||
}
|
||||
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent)) Timer -= Time.deltaTime;
|
||||
}
|
||||
else if (HasEffect && EffectActive && EffectDuration > 0)
|
||||
{
|
||||
@@ -555,13 +491,9 @@ public abstract class CustomActionButton
|
||||
if (Button)
|
||||
{
|
||||
if (CanUse())
|
||||
{
|
||||
Button!.SetEnabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
Button!.SetDisabled();
|
||||
}
|
||||
|
||||
if (EffectActive && EffectDuration > 0)
|
||||
{
|
||||
@@ -623,10 +555,7 @@ public abstract class CustomActionButton<T> : CustomActionButton
|
||||
public override bool CanUse()
|
||||
{
|
||||
var newTarget = GetTarget();
|
||||
if (newTarget != Target)
|
||||
{
|
||||
SetOutline(false);
|
||||
}
|
||||
if (newTarget != Target) SetOutline(false);
|
||||
|
||||
Target = IsTargetValid(newTarget) ? newTarget : null;
|
||||
SetOutline(true);
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MiraAPI.Patches.Stubs;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
@@ -100,7 +101,6 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu<CustomPaginableMenu.
|
||||
: text.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
[SuppressMessage("Style", "IDE0046:Convert to conditional expression", Justification = "Creates a rather large ternary operation.")]
|
||||
private List<MenuEntry> GetFilteredEntries()
|
||||
{
|
||||
var query = NormalizeForSearch(searchText);
|
||||
@@ -248,8 +248,7 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu<CustomPaginableMenu.
|
||||
|
||||
var wikiClickSound = HudManager.Instance?.MapButton?.ClickSound;
|
||||
|
||||
var searchFocusButton = searchTextbox.gameObject.GetComponent<PassiveButton>()
|
||||
?? searchTextbox.gameObject.AddComponent<PassiveButton>();
|
||||
var searchFocusButton = searchTextbox.gameObject.GetOrAddComponent<PassiveButton>();
|
||||
if (wikiClickSound != null)
|
||||
{
|
||||
searchFocusButton.ClickSound = wikiClickSound;
|
||||
|
||||
@@ -151,7 +151,7 @@ public sealed class MiraHudHelper(nint cppPtr) : MonoBehaviour(cppPtr)
|
||||
var oldPos = settingsButton.transform.localPosition;
|
||||
settingsButton.transform.localPosition = new Vector3(oldPos.x, oldPos.y, -100);
|
||||
var chatButton = instance.Chat.chatButton.gameObject;
|
||||
ClonedChatButton = Object.Instantiate(chatButton, chatButton.transform.parent);
|
||||
ClonedChatButton = Instantiate(chatButton, chatButton.transform.parent);
|
||||
ClonedChatButton.SetActive(false);
|
||||
instance.Chat.chatButtonAspectPosition = ClonedChatButton.GetComponent<AspectPosition>();
|
||||
chatButton.GetComponent<AspectPosition>().Destroy();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
@@ -23,6 +23,11 @@ public class BaseKeybind
|
||||
/// </summary>
|
||||
public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display label of the currently assigned button on the active controller.
|
||||
/// </summary>
|
||||
public string CurrentControllerButton => KeybindUtils.GetControllerButtonByKeybind(this);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler of the keybind. Invoked when the keybind is activated.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
/// <summary>
|
||||
/// Controller buttons used for default keybinds.
|
||||
/// </summary>
|
||||
public enum ControllerButton
|
||||
{
|
||||
/// <summary>
|
||||
/// The bottom face button (Cross on PlayStation, A on Xbox).
|
||||
/// </summary>
|
||||
South = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The right face button (Circle on PlayStation, B on Xbox).
|
||||
/// </summary>
|
||||
East = 5,
|
||||
|
||||
/// <summary>
|
||||
/// The left face button (Square on PlayStation, X on Xbox).
|
||||
/// </summary>
|
||||
West = 7,
|
||||
|
||||
/// <summary>
|
||||
/// The top face button (Triangle on PlayStation, Y on Xbox).
|
||||
/// </summary>
|
||||
North = 8,
|
||||
|
||||
/// <summary>
|
||||
/// The left bumper (L1 on PlayStation, LB on Xbox).
|
||||
/// </summary>
|
||||
LeftBumper = 10,
|
||||
|
||||
/// <summary>
|
||||
/// The right bumper (R1 on PlayStation, RB on Xbox).
|
||||
/// </summary>
|
||||
RightBumper = 12,
|
||||
|
||||
/// <summary>
|
||||
/// The left trigger (L2 on PlayStation, LT on Xbox).
|
||||
/// </summary>
|
||||
LeftTrigger = 11,
|
||||
|
||||
/// <summary>
|
||||
/// The right trigger (R2 on PlayStation, RT on Xbox).
|
||||
/// </summary>
|
||||
RightTrigger = 13,
|
||||
|
||||
/// <summary>
|
||||
/// The left stick button (L3 on PlayStation).
|
||||
/// </summary>
|
||||
LeftStick = 17,
|
||||
|
||||
/// <summary>
|
||||
/// The right stick button (R3 on PlayStation).
|
||||
/// </summary>
|
||||
RightStick = 18,
|
||||
|
||||
/// <summary>
|
||||
/// The back button (Create on PlayStation, View on Xbox).
|
||||
/// </summary>
|
||||
Back = 14,
|
||||
|
||||
/// <summary>
|
||||
/// The start button (Options on PlayStation, Menu on Xbox).
|
||||
/// </summary>
|
||||
Start = 15,
|
||||
|
||||
/// <summary>
|
||||
/// The D-pad up button.
|
||||
/// </summary>
|
||||
DPadUp = 19,
|
||||
|
||||
/// <summary>
|
||||
/// The D-pad right button.
|
||||
/// </summary>
|
||||
DPadRight = 20,
|
||||
|
||||
/// <summary>
|
||||
/// The D-pad down button.
|
||||
/// </summary>
|
||||
DPadDown = 21,
|
||||
|
||||
/// <summary>
|
||||
/// The D-pad left button.
|
||||
/// </summary>
|
||||
DPadLeft = 22,
|
||||
}
|
||||
@@ -33,12 +33,40 @@ public static class KeybindManager
|
||||
continue;
|
||||
}
|
||||
|
||||
keybind.RewiredInputAction = instance.userData.RegisterModBind(
|
||||
keybind.Id,
|
||||
keybind.Name,
|
||||
keybind.SourcePluginName,
|
||||
keybind.DefaultKey,
|
||||
modifiers: keybind.ModifierKeys);
|
||||
keybind.RewiredInputAction = instance.userData.RegisterModBind(keybind.Id, keybind.Name, keybind.SourcePluginName, keybind.DefaultKey, modifiers: keybind.ModifierKeys);
|
||||
|
||||
if (keybind.DefaultControllerButton == null) continue;
|
||||
|
||||
var button = (int)keybind.DefaultControllerButton.Value;
|
||||
var template = instance.dataFiles.GetJoystickTemplate(GamepadTemplate.typeGuid);
|
||||
foreach (var map in instance.userData.joystickMaps)
|
||||
{
|
||||
if (map.categoryId != 1) continue;
|
||||
|
||||
var elementId = button;
|
||||
var elementType = keybind.DefaultControllerButton is ControllerButton.LeftTrigger or ControllerButton.RightTrigger ? ControllerElementType.Axis : ControllerElementType.Button;
|
||||
|
||||
if (map.hardwareGuid != GamepadTemplate.typeGuid)
|
||||
{
|
||||
var controller = template?.joysticks.ToArray().FirstOrDefault(x => x.JoystickGuid == map.hardwareGuid);
|
||||
elementId = controller?.GetJoystickElementId(button) ?? -1;
|
||||
if (elementId < 0) continue;
|
||||
|
||||
var element = instance.dataFiles.GetHardwareJoystickMap(map.hardwareGuid)?.GetElementIdentifier(elementId);
|
||||
if (element == null) continue;
|
||||
|
||||
elementType = element.elementType;
|
||||
}
|
||||
|
||||
map.actionElementMaps.Add(new ActionElementMap
|
||||
{
|
||||
_actionId = keybind.RewiredInputAction.id,
|
||||
_elementIdentifierId = elementId,
|
||||
_elementType = elementType,
|
||||
_axisRange = AxisRange.Positive,
|
||||
_axisContribution = Pole.Positive,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -50,24 +78,66 @@ public static class KeybindManager
|
||||
/// <summary>
|
||||
/// Returns all conflicts where <see cref="MiraKeybind"/>s use the same key.
|
||||
/// </summary>
|
||||
/// <returns>A list of pairs of keybinds that conflict.</returns>
|
||||
/// <returns>The conflicting keybinds grouped by keycode.</returns>
|
||||
public static Dictionary<KeyboardKeyCode, List<MiraKeybind>> GetConflicts()
|
||||
{
|
||||
var conflicts = new Dictionary<KeyboardKeyCode, List<MiraKeybind>>();
|
||||
foreach (var keybind in Keybinds)
|
||||
{
|
||||
var all = GetKeybindsForKey(keybind.CurrentKey, true);
|
||||
var key = keybind.CurrentKey;
|
||||
if (key == KeyboardKeyCode.None) continue;
|
||||
|
||||
var all = GetKeybindsForKey(key, true);
|
||||
if (all.Length > 1)
|
||||
{
|
||||
if (!conflicts.TryGetValue(keybind.CurrentKey, out var group))
|
||||
if (!conflicts.TryGetValue(key, out var group))
|
||||
{
|
||||
conflicts.Add(keybind.CurrentKey, []);
|
||||
group = conflicts[keybind.CurrentKey];
|
||||
group = [];
|
||||
conflicts.Add(key, group);
|
||||
}
|
||||
|
||||
if (!group.Contains(keybind))
|
||||
if (!group.Contains(keybind)) group.Add(keybind);
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns controller bindings shared by exclusive custom keybinds.
|
||||
/// </summary>
|
||||
/// <param name="enabledOnly">Whether to check only active controller maps and available HUD buttons.</param>
|
||||
/// <returns>The conflicting custom bindings.</returns>
|
||||
public static List<ActionElementMap> GetControllerConflicts(bool enabledOnly = false)
|
||||
{
|
||||
var conflicts = new List<ActionElementMap>();
|
||||
var actions = Keybinds.Where(x => x.Exclusive && x.RewiredInputAction != null && (!enabledOnly || KeybindUtils.IsControllerActionActive(x.RewiredInputAction.id))).Select(x => x.RewiredInputAction!.id).ToHashSet();
|
||||
var player = ReInput.players.GetPlayer(0);
|
||||
for (var i = 0; i < player.controllers.joystickCount; i++)
|
||||
{
|
||||
var controller = player.controllers.Joysticks[i];
|
||||
var maps = new CppCollections.List<ControllerMap>(player.controllers.maps.GetMaps(controller).Cast<CppCollections.IEnumerable<ControllerMap>>()).ToArray();
|
||||
foreach (var map in maps)
|
||||
{
|
||||
if (enabledOnly && !map.enabled) continue;
|
||||
|
||||
foreach (var binding in map.GetElementMaps())
|
||||
{
|
||||
group.Add(keybind);
|
||||
if (!binding.enabled || !actions.Contains(binding.actionId)) continue;
|
||||
|
||||
foreach (var otherMap in maps)
|
||||
{
|
||||
if (enabledOnly && !otherMap.enabled) continue;
|
||||
if (!enabledOnly && map.categoryId != otherMap.categoryId && map.categoryId != 0 && otherMap.categoryId != 0) continue;
|
||||
|
||||
foreach (var other in otherMap.GetElementMaps())
|
||||
{
|
||||
if (!other.enabled || binding.actionId == other.actionId || !actions.Contains(other.actionId) || !binding.CheckForAssignmentConflict(other)) continue;
|
||||
|
||||
if (!conflicts.Contains(binding)) conflicts.Add(binding);
|
||||
if (!conflicts.Contains(other)) conflicts.Add(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +149,7 @@ public static class KeybindManager
|
||||
/// Returns all <see cref="MiraKeybind"/>s for a specified keycode.
|
||||
/// </summary>
|
||||
/// <param name="keyCode">The key to look for.</param>
|
||||
/// /// <param name="exclusiveCheck">Should only exclusive keybinds be present.</param>
|
||||
/// <param name="exclusiveCheck">Whether to return only exclusive keybinds.</param>
|
||||
/// <returns>The list of <see cref="MiraKeybind"/>s.</returns>
|
||||
public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false)
|
||||
{
|
||||
@@ -87,9 +157,7 @@ public static class KeybindManager
|
||||
foreach (var keybind in Keybinds)
|
||||
{
|
||||
if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck))
|
||||
{
|
||||
all.Add(keybind);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. all];
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MiraAPI.Hud;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of keybind-related utility classes.
|
||||
/// Collection of keybind-related utilities.
|
||||
/// </summary>
|
||||
public static class KeybindUtils
|
||||
{
|
||||
@@ -23,6 +25,25 @@ public static class KeybindUtils
|
||||
return keybind.RewiredInputAction == null ? KeyboardKeyCode.None : GetKeycodeByActionId(keybind.RewiredInputAction.id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the display label for the currently assigned controller button of a <see cref="BaseKeybind"/>.
|
||||
/// </summary>
|
||||
/// <param name="keybind">The keybind to get the controller button from.</param>
|
||||
/// <returns>The button label, or an empty string if no controller binding is available.</returns>
|
||||
public static string GetControllerButtonByKeybind(BaseKeybind keybind)
|
||||
{
|
||||
var map = keybind.RewiredInputAction == null ? null : GetControllerElementMap(keybind.RewiredInputAction.id);
|
||||
var name = map?.elementIdentifierName ?? string.Empty;
|
||||
return name.ToLowerInvariant() switch
|
||||
{
|
||||
"cross" => "X",
|
||||
"square" => "□",
|
||||
"circle" => "○",
|
||||
"triangle" => "△",
|
||||
_ => name,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the keycode for an action with <see cref="ReInput"/>.
|
||||
/// </summary>
|
||||
@@ -37,23 +58,40 @@ public static class KeybindUtils
|
||||
/// Gets a <see cref="ActionElementMap"/> by id with <see cref="ReInput"/>.
|
||||
/// </summary>
|
||||
/// <param name="actionId">The action ID.</param>
|
||||
/// <returns>The keyboard keycode.</returns>
|
||||
/// <returns>The keyboard binding, or null if the action is unassigned.</returns>
|
||||
public static ActionElementMap? GetActionElementMap(int actionId)
|
||||
{
|
||||
var player = ReInput.players.GetPlayer(0);
|
||||
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the binding for an action on the active controller.
|
||||
/// </summary>
|
||||
/// <param name="actionId">The action ID.</param>
|
||||
/// <returns>The controller binding, or null if no active controller or binding is available.</returns>
|
||||
public static ActionElementMap? GetControllerElementMap(int actionId)
|
||||
{
|
||||
var player = ReInput.players.GetPlayer(0);
|
||||
var controller = player.controllers.GetLastActiveController(ControllerType.Joystick);
|
||||
return controller == null ? null : player.controllers.maps.GetFirstElementMapWithAction(controller, actionId, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="InputAction"/> by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The action identifier name.</param>
|
||||
/// <returns>The keyboard keycode.</returns>
|
||||
/// <param name="id">The action ID.</param>
|
||||
/// <returns>The input action, or null if it is unavailable.</returns>
|
||||
public static InputAction? GetInputActionById(int id)
|
||||
{
|
||||
return RewiredInputManager?.userData?.GetActionById(id);
|
||||
}
|
||||
|
||||
internal static bool IsControllerActionActive(int actionId)
|
||||
{
|
||||
return CustomButtonManager.Buttons.Any(x => x.Keybind?.RewiredInputAction?.id == actionId && x.Button != null && x.Button.isActiveAndEnabled && x.Enabled(PlayerControl.LocalPlayer?.Data?.Role));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Rewired;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
/// <summary>
|
||||
/// Class containing all default <see cref="MiraKeybind"/>s.
|
||||
/// Recommended for cross-mod compatibility.
|
||||
/// Controller bindings are unassigned by default.
|
||||
/// </summary>
|
||||
[RegisterCustomKeybinds]
|
||||
public static class MiraGlobalKeybinds
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
@@ -10,11 +10,7 @@ namespace MiraAPI.Keybinds;
|
||||
/// <param name="defaultKeycode">The default keycode.</param>
|
||||
/// <param name="modifierKeys">Up to 3 optional <see cref="ModifierKey"/>s.</param>
|
||||
/// <param name="exclusive">Is exclusive.</param>
|
||||
public class MiraKeybind(
|
||||
string name,
|
||||
KeyboardKeyCode? defaultKeycode,
|
||||
ModifierKey[]? modifierKeys = null,
|
||||
bool exclusive = true) : BaseKeybind(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'))
|
||||
public class MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ModifierKey[]? modifierKeys = null, bool exclusive = true) : BaseKeybind(name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_'))
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets name of the keybind.
|
||||
@@ -26,6 +22,11 @@ public class MiraKeybind(
|
||||
/// </summary>
|
||||
public KeyboardKeyCode DefaultKey { get; } = defaultKeycode ?? KeyboardKeyCode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default controller button. If <see langword="null"/>, the controller binding is left unassigned.
|
||||
/// </summary>
|
||||
public ControllerButton? DefaultControllerButton { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ModifierKey"/>s assigned to this keybind.
|
||||
/// Due to Rewired limitations, there can only be 3 <see cref="ModifierKey"/>s.
|
||||
@@ -34,11 +35,26 @@ public class MiraKeybind(
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key.
|
||||
/// Active exclusive controller bindings take priority over vanilla actions during gameplay.
|
||||
/// </summary>
|
||||
public bool Exclusive { get; } = exclusive;
|
||||
|
||||
internal string? SourcePluginName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MiraKeybind"/> class with a controller binding.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the keybind.</param>
|
||||
/// <param name="defaultKeycode">The default keycode.</param>
|
||||
/// <param name="defaultControllerButton">The default controller button.</param>
|
||||
/// <param name="modifierKeys">Up to 3 optional <see cref="ModifierKey"/>s for the keyboard binding.</param>
|
||||
/// <param name="exclusive">Is exclusive.</param>
|
||||
public MiraKeybind(string name, KeyboardKeyCode? defaultKeycode, ControllerButton defaultControllerButton, ModifierKey[]? modifierKeys = null, bool exclusive = true)
|
||||
: this(name, defaultKeycode, modifierKeys, exclusive)
|
||||
{
|
||||
DefaultControllerButton = defaultControllerButton;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MiraAPI.Keybinds;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using BepInEx;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Patches;
|
||||
using MiraAPI.Patches.Roles;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Translation;
|
||||
@@ -63,9 +64,34 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
|
||||
private static MiraPluginManager? PluginManager { get; set; }
|
||||
internal Harmony Harmony { get; } = new(Id);
|
||||
|
||||
internal static ConfigEntry<SortingGroups> WikiSortingGroup { get; private set; }
|
||||
internal static ConfigEntry<SortingMethod> WikiSortingMethod { get; private set; }
|
||||
internal static ConfigEntry<SortingFilter> WikiSortingFilter { get; private set; }
|
||||
internal static ConfigEntry<string> DistanceSuffix { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Load()
|
||||
{
|
||||
WikiSortingGroup = Config.Bind(
|
||||
"Wiki",
|
||||
"WikiSortingGroup",
|
||||
SortingGroups.Ungrouped,
|
||||
"Determines how clusters of roles / modifiers are grouped together, if at all.");
|
||||
WikiSortingMethod = Config.Bind(
|
||||
"Wiki",
|
||||
"WikiSortingMethod",
|
||||
SortingMethod.Alphabetical,
|
||||
"Determines if the list of roles / modifiers are organized in ascending or descending order.");
|
||||
WikiSortingFilter = Config.Bind(
|
||||
"Wiki",
|
||||
"WikiSortingFilter",
|
||||
SortingFilter.AllRoles,
|
||||
"Determines what roles / modifiers are displayed at once.");
|
||||
DistanceSuffix = Config.Bind(
|
||||
"Display",
|
||||
"Distance",
|
||||
"unit(s)",
|
||||
"Dictates the unit of distance used in numeric option displays representing a distance.");
|
||||
Harmony.PatchAll();
|
||||
RoleOptionsCollectionPatch.PatchRoleMethods(Harmony);
|
||||
UiResetEvents.Initialize();
|
||||
|
||||
@@ -69,6 +69,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
|
||||
|
||||
OldUiButtonScaleFactor = TopRightButtonsFactorSlider.Value;
|
||||
}
|
||||
else if (configEntry == MatchInfoGuideScaleFactor && MatchInfoGuide.Instance)
|
||||
{
|
||||
RoleGuidePatches.ApplyGuideScale(MatchInfoGuide.Instance, MatchInfoGuideScaleFactor.Value);
|
||||
}
|
||||
else if (configEntry == WikiOnBottomRow)
|
||||
{
|
||||
SetUpButtonPositions();
|
||||
@@ -216,11 +220,24 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
|
||||
[LocalToggleSetting]
|
||||
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.ShowKeybindsOnButtons", true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scale of the match info guide.
|
||||
/// </summary>
|
||||
[LocalSliderSetting(min: 0.5f, max: 1f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
|
||||
public ConfigEntry<float> MatchInfoGuideScaleFactor { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.MatchInfoGuideScaleFactor", 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag indicating whether the wiki is on the bottom row.
|
||||
/// </summary>
|
||||
[LocalToggleSetting]
|
||||
public ConfigEntry<bool> WikiOnBottomRow { get; private set; } = config.Bind("MiraApi.VisualsUi", "MiraApi.WikiOnBottomRow", true);
|
||||
public ConfigEntry<bool> WikiOnBottomRow { get; private set; } = config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiOnBottomRow", true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flag indicating the position for the wiki's tabs.
|
||||
/// </summary>
|
||||
[LocalEnumSetting(names: ["MiraApi.WikiTabPosition.LeftSide", "MiraApi.WikiTabPosition.RightSide", "MiraApi.WikiTabPosition.Below"])]
|
||||
public ConfigEntry<MatchInfoTabPos> WikiTabPosition { get; private set; } =
|
||||
config.Bind("MiraApi.MatchInfoGuide", "MiraApi.WikiTabPosition", MiraApiPlugin.IsMobile ? MatchInfoTabPos.RightSide : MatchInfoTabPos.LeftSide);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether to show the red flash from sabotages.
|
||||
@@ -233,4 +250,31 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
|
||||
/// </summary>
|
||||
[LocalToggleSetting]
|
||||
public ConfigEntry<bool> EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether variable colors will remain static or not.
|
||||
/// </summary>
|
||||
[LocalToggleSetting]
|
||||
public ConfigEntry<bool> EnableVariableColors { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableVariableColors", true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An enum that dictates the location of the match info tab buttons.
|
||||
/// </summary>
|
||||
public enum MatchInfoTabPos
|
||||
{
|
||||
/// <summary>
|
||||
/// The left side.
|
||||
/// </summary>
|
||||
LeftSide,
|
||||
|
||||
/// <summary>
|
||||
/// The right side.
|
||||
/// </summary>
|
||||
RightSide,
|
||||
|
||||
/// <summary>
|
||||
/// Below the menu.
|
||||
/// </summary>
|
||||
Below,
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Il2CppInterop.Runtime.Attributes;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Modifiers;
|
||||
@@ -44,16 +50,98 @@ public abstract class BaseModifier : IOptionable
|
||||
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId))
|
||||
?? throw new InvalidOperationException("Modifier is not registered.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id part used to build the modifier's translation keys. It is recommended to simply call it what the modifier is, as other mods may utilize it.
|
||||
/// </summary>
|
||||
public virtual string IdPart => GetType().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the id part used to build the modifier's translation keys. It is recommended to call it ModGuid.Modifier.Type.
|
||||
/// </summary>
|
||||
public virtual string IdPrefix => GetType().Namespace!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modifier name.
|
||||
/// </summary>
|
||||
public abstract string ModifierName { get; }
|
||||
public virtual string ModifierName => MiraLocaleManager.Get(ModifierNameLocale);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modifier's name id for localization.
|
||||
/// </summary>
|
||||
public virtual string ModifierNameLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the medium description of the modifier. Used in the modifier guide and options menu.
|
||||
/// </summary>
|
||||
public virtual string ModifierMedDescription => MiraLocaleManager.Get(ModifierMedDescriptionLocale, GetDescription());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modifier's medium description id for localization.
|
||||
/// </summary>
|
||||
public virtual string ModifierMedDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "MedDescription");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wiki description of the modifier. Used in the wiki and normally appends the options text as well.
|
||||
/// </summary>
|
||||
public virtual string ModifierWikiDescription => MiraLocaleManager.Get(ModifierWikiDescriptionLocale, GetDescription()) +
|
||||
Helpers.GetOptionsText(GetType());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modifier's wiki description id for localization.
|
||||
/// </summary>
|
||||
public virtual string ModifierWikiDescriptionLocale => MiraLocaleManager.BuildTranslationId(IdPrefix, IdPart, "WikiDescription");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the category of the modifier. Used in the wiki.
|
||||
/// </summary>
|
||||
public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier");
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the modifier is forcibly shown or disabled in the wiki screen.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the modifier is always displayed, otherwise <see langword="false"/> if it is never displayable, or <see langword="null"/> if it is dictated by amount and chance.</returns>
|
||||
public virtual bool? ForceShowModifierOnWiki => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the information to display in the wiki.
|
||||
/// </summary>
|
||||
/// <param name="guide">The guide object.</param>
|
||||
/// <param name="titleText">The title text object.</param>
|
||||
/// <param name="titleIcon">The title's icon.</param>
|
||||
/// <param name="parent">The scroller parent.</param>
|
||||
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
|
||||
public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, SpriteRenderer titleIcon, Scroller parent)
|
||||
{
|
||||
var abilities = WikiAbilities;
|
||||
return Helpers.CreateAdvancedWikiPage(
|
||||
guide,
|
||||
titleText,
|
||||
titleIcon,
|
||||
parent,
|
||||
ModifierNameLocale,
|
||||
$"{ModifierName} ({ModifierCategoryTitle})".TrimStart(),
|
||||
ModifierIcon ?? MiraAssets.IconNeutral.LoadAsset(),
|
||||
ModifierWikiDescription,
|
||||
abilities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of abilities or other similar information to display below the text of a modifier's wiki page.
|
||||
/// </summary>
|
||||
[HideFromIl2Cpp]
|
||||
public virtual List<AdvancedWikiAbilityDescription> WikiAbilities => [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the modifier icon. Useless if <see cref="HideOnUi"/> is <see langword="true"/>.
|
||||
/// </summary>
|
||||
public virtual LoadableAsset<Sprite>? ModifierIcon => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="TMP_SpriteAsset"/> for the Modifier Icon.
|
||||
/// </summary>
|
||||
[HideFromIl2Cpp]
|
||||
public virtual TMP_SpriteAsset IconTmp => null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided.
|
||||
/// </summary>
|
||||
@@ -69,6 +157,11 @@ public abstract class BaseModifier : IOptionable
|
||||
/// </summary>
|
||||
public virtual Color FreeplayFileColor => Color.gray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Color"/> of the modifier, used in the wiki and other UI outside freeplay.
|
||||
/// </summary>
|
||||
public virtual Color GeneralColor => FreeplayFileColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the modifier is unique. If <see langword="true"/>, the player can only have one instance of this modifier.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace MiraAPI.Modifiers.Types;
|
||||
[MiraIgnore]
|
||||
public abstract class GameModifier : BaseModifier
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool? ForceShowModifierOnWiki => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ShowInFreeplay => true;
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ dotnet_diagnostic.SA1611.severity = none
|
||||
# SA1615: Element return value should be documented.
|
||||
dotnet_diagnostic.SA1615.severity = none
|
||||
|
||||
# S125: Sections of code should not be commented out.
|
||||
dotnet_diagnostic.S125.severity = none
|
||||
# SA1602: Enumeration items should be documented.
|
||||
dotnet_diagnostic.SA1602.severity = none
|
||||
@@ -36,7 +36,7 @@ internal static class AmongUsClientSyncPatch
|
||||
GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerLeft))]
|
||||
public static void PlayerLeftPatch(ClientData data, DisconnectReasons reason)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Colors;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Patches.Colors;
|
||||
|
||||
[HarmonyPatch(typeof(PlayerMaterial))]
|
||||
internal static class SetPlayerMaterialPatch
|
||||
{
|
||||
[HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))]
|
||||
public static bool Prefix(int colorId, Renderer rend)
|
||||
{
|
||||
rend.gameObject.GetOrAddComponent<VariableColorBehaviour>().SetColor(
|
||||
PaletteManager.TryGetCustomColor(colorId, out var color) && color is VariableColor variable
|
||||
? variable
|
||||
: null);
|
||||
return !PaletteManager.IsVariable(colorId);
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))]
|
||||
public static void Prefix(Renderer rend)
|
||||
{
|
||||
rend.gameObject.GetOrAddComponent<VariableColorBehaviour>().SetColor(null);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))]
|
||||
internal static class PlayerTabPatch
|
||||
{
|
||||
public static void Postfix(PlayerTab __instance)
|
||||
{
|
||||
for (var i = 0; i < __instance.ColorChips.Count; i++)
|
||||
{
|
||||
__instance.ColorChips[i].Inner.SpriteColor = PaletteManager.GetMainColor(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))]
|
||||
internal static class ChatNotifRainbowPatch
|
||||
{
|
||||
private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout);
|
||||
|
||||
public static void Postfix(ChatNotification __instance)
|
||||
{
|
||||
if (!__instance.gameObject.active)
|
||||
return;
|
||||
|
||||
var colorId = __instance.player.cosmetics.ColorId;
|
||||
|
||||
if (!PaletteManager.IsVariable(colorId))
|
||||
return;
|
||||
|
||||
var color = PaletteManager.GetMainColor(colorId);
|
||||
var str = color.ToHtmlStringRGBA();
|
||||
var nameText = __instance.playerNameText;
|
||||
nameText.text = "<color=#" + str + ">" + RichTags.Replace(nameText.text, string.Empty);
|
||||
nameText.outlineColor = color.GetShadowColor(0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))]
|
||||
internal static class RainbowLobbyInfoPanePatch
|
||||
{
|
||||
[SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")]
|
||||
public static void Postfix(HostInfoPanel __instance)
|
||||
{
|
||||
if (!__instance.gameObject.activeInHierarchy)
|
||||
return;
|
||||
|
||||
var colorId = __instance.player.cosmetics.ColorId;
|
||||
|
||||
if (!PaletteManager.IsVariable(colorId))
|
||||
return;
|
||||
|
||||
var host = GameData.Instance.GetHost();
|
||||
|
||||
if (!host)
|
||||
return;
|
||||
|
||||
var text = PaletteManager.GetMainColor(colorId).ToHtmlStringRGBA();
|
||||
var label = __instance.hostLabel;
|
||||
label.text = TranslationController.Instance.GetString(StringNames.HostNounLabel);
|
||||
|
||||
if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage))
|
||||
{
|
||||
label.text = "<b>" + label.text + "</b>";
|
||||
}
|
||||
|
||||
if (AmongUsClient.Instance.AmHost)
|
||||
{
|
||||
__instance.playerName.text = (string.IsNullOrEmpty(host.PlayerName)
|
||||
? "..."
|
||||
: $"<color=#{text}>{host.PlayerName}</color>")
|
||||
+ " <size=90%><b><font=\"Barlow-BoldItalic SDF\" material=\"Barlow-BoldItalic SDF Outline\">" +
|
||||
TranslationController.Instance.GetString(StringNames.HostYouLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
__instance.playerName.text =
|
||||
(string.IsNullOrEmpty(host.PlayerName) ? "..." : $"<color=#{text}>{host.PlayerName}</color>") +
|
||||
" (" + __instance.player.ColorBlindName + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,18 @@ public static class VanillaButtonPatches
|
||||
{
|
||||
playerTarget = role.Cast<TrackerRole>().currentTarget;
|
||||
}
|
||||
else if (role.Role is RoleTypes.Detective)
|
||||
{
|
||||
playerTarget = role.Cast<DetectiveRole>().currentTarget;
|
||||
}
|
||||
else if (role.Role is RoleTypes.GuardianAngel)
|
||||
{
|
||||
playerTarget = role.Cast<GuardianAngelRole>().currentTarget;
|
||||
}
|
||||
else if (role.Role is RoleTypes.SpiritGuide)
|
||||
{
|
||||
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
|
||||
}
|
||||
else if (role.Role is RoleTypes.Engineer)
|
||||
{
|
||||
ventTarget = role.Cast<EngineerRole>().currentTarget;
|
||||
@@ -53,6 +61,10 @@ public static class VanillaButtonPatches
|
||||
{
|
||||
playerTarget = role.Cast<DetectiveRole>().currentTarget;
|
||||
}
|
||||
else if (role.Role is RoleTypes.SpiritGuide)
|
||||
{
|
||||
playerTarget = role.Cast<SpiritGuideRole>().currentTarget;
|
||||
}
|
||||
|
||||
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget);
|
||||
MiraEventManager.InvokeEvent(genericEvent);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections;
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.GameModes;
|
||||
using MiraAPI.Translation;
|
||||
using Reactor.Utilities;
|
||||
@@ -16,7 +15,7 @@ internal static class HudPatches
|
||||
[HarmonyPostfix, HarmonyPatch(nameof(HudManager.Update))]
|
||||
public static void HudUpdatePatch(HudManager __instance)
|
||||
{
|
||||
if (GameManager.Instance != null && GameManager.Instance.GameHasStarted &&
|
||||
if (GameManager.Instance && GameManager.Instance.GameHasStarted &&
|
||||
CustomGameModeManager.ActiveMode != null)
|
||||
{
|
||||
CustomGameModeManager.ActiveMode.HudUpdate(__instance);
|
||||
@@ -32,7 +31,7 @@ internal static class HudPatches
|
||||
|
||||
private static IEnumerator CoPostHudStart(HudManager instance)
|
||||
{
|
||||
yield return new WaitUntil((Func<bool>)(() => GameManager.Instance != null));
|
||||
yield return new WaitUntil((Il2CppSystem.Func<bool>)(() => GameManager.Instance != null));
|
||||
if (GameManager.Instance.IsHideAndSeek())
|
||||
yield return null;
|
||||
var infoPane = instance.gameObject.transform.FindChild("LobbyInfoPane");
|
||||
@@ -42,7 +41,7 @@ internal static class HudPatches
|
||||
var modelText = modeLabel!.Find("Text_TMP").gameObject;
|
||||
var modelTextClone = Object.Instantiate(modelText, modeLabel);
|
||||
Object.Destroy(modelTextClone.GetComponent<TextTranslatorTMP>());
|
||||
modelTextClone.GetComponent<TextMeshPro>().text = "Gamemode";
|
||||
modelTextClone.GetComponent<TextMeshPro>().text = MiraLocaleManager.Get("MiraApi.Gamemode", "Gamemode");
|
||||
var gmText = modeValue!.Find("GameModeText").gameObject;
|
||||
var gmTextClone = Object.Instantiate(gmText, modeValue);
|
||||
gmText.SetActive(false);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Hud;
|
||||
using MiraAPI.Keybinds;
|
||||
@@ -28,47 +27,29 @@ public static class HudManagerPatches
|
||||
|
||||
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = [];
|
||||
|
||||
[SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "This is internal and will never be used by user code.")]
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "This is internal and will never be used by user code.")]
|
||||
internal static List<TextMeshPro> ModdedKeybindIcons = [];
|
||||
|
||||
public static IEnumerator CoResizeUI()
|
||||
{
|
||||
while (!HudManager.Instance)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
while (!HudManager.Instance) yield return null;
|
||||
|
||||
yield return new WaitForSeconds(0.01f);
|
||||
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
|
||||
}
|
||||
|
||||
private static Transform _storedButtonsParent;
|
||||
|
||||
public static void ResizeUI(float scaleFactor)
|
||||
{
|
||||
if (!_storedButtonsParent)
|
||||
{
|
||||
_storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons");
|
||||
}
|
||||
if (!_storedButtonsParent) _storedButtonsParent = HudManager.Instance.transform.FindChild("Buttons");
|
||||
|
||||
if (_storedButtonsParent)
|
||||
{
|
||||
foreach (var aspect in _storedButtonsParent.GetComponentsInChildren<AspectPosition>(true))
|
||||
{
|
||||
if (!aspect.gameObject)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!aspect.gameObject) continue;
|
||||
|
||||
if (aspect.gameObject.transform.parent.name == "TopRight")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (aspect.gameObject.transform.parent.name == "TopRight") continue;
|
||||
|
||||
if (aspect.gameObject.name.Contains("TopRight"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (aspect.gameObject.name.Contains("TopRight")) continue;
|
||||
|
||||
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
|
||||
aspect.DistanceFromEdge *= new Vector2(scaleFactor, scaleFactor);
|
||||
@@ -78,10 +59,7 @@ public static class HudManagerPatches
|
||||
|
||||
foreach (var button in HudManager.Instance.GetComponentsInChildren<ActionButton>(true))
|
||||
{
|
||||
if (!button.gameObject)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!button.gameObject) continue;
|
||||
|
||||
button.gameObject.SetActive(!button.isActiveAndEnabled);
|
||||
button.gameObject.transform.localScale *= scaleFactor;
|
||||
@@ -91,15 +69,9 @@ public static class HudManagerPatches
|
||||
if (!_storedButtonsParent) return;
|
||||
foreach (var arrange in _storedButtonsParent.GetComponentsInChildren<GridArrange>(true))
|
||||
{
|
||||
if (!arrange.gameObject || !arrange.transform)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!arrange.gameObject || !arrange.transform) continue;
|
||||
|
||||
if (arrange.gameObject.name.Contains("TopRight"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (arrange.gameObject.name.Contains("TopRight")) continue;
|
||||
|
||||
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
|
||||
arrange.CellSize = new Vector2(scaleFactor, scaleFactor);
|
||||
@@ -143,10 +115,7 @@ public static class HudManagerPatches
|
||||
[HarmonyPatch(typeof(ChatController), nameof(ChatController.Close))]
|
||||
public static void TogglePrefix(ChatController __instance)
|
||||
{
|
||||
if (!MiraHudHelper.ClonedChatButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!MiraHudHelper.ClonedChatButton) return;
|
||||
|
||||
__instance.chatButton.transform.localPosition = MiraHudHelper.ClonedChatButton.transform.localPosition + new Vector3(-0.3f, 0);
|
||||
}
|
||||
@@ -179,25 +148,13 @@ public static class HudManagerPatches
|
||||
public static void StartPostfix(HudManager __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponent<MiraHudHelper>();
|
||||
if (Buttons == null)
|
||||
{
|
||||
Buttons = __instance.transform.Find("Buttons");
|
||||
}
|
||||
if (Buttons == null) Buttons = __instance.transform.Find("Buttons");
|
||||
|
||||
if (BottomRight == null)
|
||||
{
|
||||
BottomRight = Buttons.Find("BottomRight");
|
||||
}
|
||||
if (BottomRight == null) BottomRight = Buttons.Find("BottomRight");
|
||||
|
||||
if (BottomLeft == null)
|
||||
{
|
||||
BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
|
||||
}
|
||||
if (BottomLeft == null) BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
|
||||
|
||||
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
|
||||
{
|
||||
t.gameObject.Destroy();
|
||||
}
|
||||
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true)) t.gameObject.Destroy();
|
||||
|
||||
var gridArrange = BottomLeft.GetComponent<GridArrange>();
|
||||
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
|
||||
@@ -209,10 +166,7 @@ public static class HudManagerPatches
|
||||
if (Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone)
|
||||
{
|
||||
var fakeButton = Object.Instantiate(__instance.AbilityButton, BottomLeft.transform);
|
||||
foreach (var renderer in fakeButton.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
renderer.enabled = false;
|
||||
}
|
||||
foreach (var renderer in fakeButton.GetComponentsInChildren<Renderer>()) renderer.enabled = false;
|
||||
|
||||
fakeButton.buttonLabelText.Destroy();
|
||||
fakeButton.cooldownTimerText.Destroy();
|
||||
@@ -222,8 +176,6 @@ public static class HudManagerPatches
|
||||
fakeButton.Destroy();
|
||||
}
|
||||
|
||||
ModdedKeybindIcons = [];
|
||||
|
||||
foreach (var button in CustomButtonManager.CustomButtons)
|
||||
{
|
||||
var location = button.Location switch
|
||||
@@ -233,10 +185,7 @@ public static class HudManagerPatches
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (location is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (location is null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -272,25 +221,21 @@ public static class HudManagerPatches
|
||||
var buttonObj = kvp.Key;
|
||||
var actionId = kvp.Value;
|
||||
|
||||
var key = KeybindUtils.GetKeycodeByActionId(actionId);
|
||||
if (key == KeyboardKeyCode.None)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
|
||||
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
|
||||
if (!MiraApiPlugin.IsMobile)
|
||||
{
|
||||
var comp = buttonObj.GetComponent<ActionButton>();
|
||||
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
|
||||
}
|
||||
|
||||
var key = KeybindUtils.GetKeycodeByActionId(actionId);
|
||||
if (key == KeyboardKeyCode.None) continue;
|
||||
|
||||
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
|
||||
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
|
||||
}
|
||||
|
||||
MiraApiSettings.OldButtonScaleFactor =
|
||||
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
|
||||
MiraApiSettings.OldUiButtonScaleFactor =
|
||||
LocalSettingsTabSingleton<MiraApiSettings>.Instance.TopRightButtonsFactorSlider.Value;
|
||||
MiraApiSettings.OldButtonScaleFactor = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
|
||||
MiraApiSettings.OldUiButtonScaleFactor = LocalSettingsTabSingleton<MiraApiSettings>.Instance.TopRightButtonsFactorSlider.Value;
|
||||
Coroutines.Start(CoResizeUI());
|
||||
}
|
||||
|
||||
@@ -306,10 +251,7 @@ public static class HudManagerPatches
|
||||
public static void SetHudActivePostfix(HudManager __instance, PlayerControl localPlayer, RoleBehaviour role, bool isActive)
|
||||
{
|
||||
__instance.AdminButton.ToggleVisible(isActive && role.IsImpostor && GameOptionsManager.Instance.CurrentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek);
|
||||
if (localPlayer.Data == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (localPlayer.Data == null) return;
|
||||
|
||||
foreach (var button in CustomButtonManager.CustomButtons)
|
||||
{
|
||||
@@ -328,8 +270,7 @@ public static class HudManagerPatches
|
||||
[HarmonyPostfix]
|
||||
public static void UpdatePostfix()
|
||||
{
|
||||
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
|
||||
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
|
||||
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard && LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
|
||||
|
||||
foreach (var btnIcon in vanillaKeybindIcons)
|
||||
{
|
||||
@@ -337,44 +278,19 @@ public static class HudManagerPatches
|
||||
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
|
||||
}
|
||||
|
||||
foreach (var btnIcon in ModdedKeybindIcons)
|
||||
{
|
||||
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
|
||||
}
|
||||
foreach (var button in CustomButtonManager.CustomButtons) button.UpdateKeybindIcon();
|
||||
|
||||
var player = ReInput.players.GetPlayer(0);
|
||||
var keyboard = player.controllers.Keyboard;
|
||||
foreach (var entry in KeybindManager.Keybinds)
|
||||
{
|
||||
var modKeys = entry.ModifierKeys;
|
||||
bool modifierKeysPressed;
|
||||
if (modKeys.Length == 0)
|
||||
{
|
||||
modifierKeysPressed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
modifierKeysPressed = true;
|
||||
foreach (var key in modKeys)
|
||||
{
|
||||
if (keyboard.GetModifierKey(key)) continue;
|
||||
modifierKeysPressed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
|
||||
{
|
||||
if (entry.RewiredInputAction != null && player.GetButtonDown(entry.RewiredInputAction.id))
|
||||
entry.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in KeybindManager.VanillaKeybinds.Values)
|
||||
{
|
||||
if (player.GetButtonDown(entry.Id))
|
||||
{
|
||||
entry.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Keybinds;
|
||||
using Rewired;
|
||||
using Rewired.UI.ControlMapper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -10,53 +11,71 @@ namespace MiraAPI.Patches.Keybinds;
|
||||
[HarmonyPatch(typeof(ControlMapper))]
|
||||
public static class ControlMapperPatches
|
||||
{
|
||||
// Patching this crashes for Epic Games users
|
||||
/*[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(ControlMapper.Start))]
|
||||
private static void StartPrefix(ControlMapper __instance)
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(ControlMapper.Initialize))]
|
||||
private static void InitializePrefix(ControlMapper __instance)
|
||||
{
|
||||
var doneButton = GameObject.Find("DoneButton")?.GetComponent<CustomButton>();
|
||||
if (doneButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
__instance.showControllers = true;
|
||||
__instance.showGlyphs = false;
|
||||
}
|
||||
|
||||
var toggle = Object.Instantiate(doneButton, doneButton.transform.parent);
|
||||
var text = toggle.GetComponentInChildren<TextMeshProUGUI>();
|
||||
var entry = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds;
|
||||
toggle.gameObject.name = "KeybindsVisibleToggle";
|
||||
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
|
||||
toggle.onClick.RemoveAllListeners();
|
||||
toggle.GetComponent<ButtonInfo>().identifier = "KeybindsVisibleToggle";
|
||||
toggle.onClick.AddListener((UnityAction)(() =>
|
||||
{
|
||||
entry.Value = !entry.Value;
|
||||
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
|
||||
}));
|
||||
}*/
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(ControlMapper.CreateLayout))]
|
||||
private static void CreateLayoutPostfix(ControlMapper __instance)
|
||||
{
|
||||
__instance.references.controllerGroup.gameObject.SetActive(false);
|
||||
__instance.references.assignedControllersGroup.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(ControlMapper.GetControllerMap))]
|
||||
private static bool GetControllerMapPrefix(ControlMapper __instance, ControllerType type, ref ControllerMap? __result)
|
||||
{
|
||||
if (type != ControllerType.Joystick) return true;
|
||||
|
||||
__result = __instance.currentPlayer?.controllers.maps.GetFirstMapInCategory(type, __instance.currentJoystickId, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(ControlMapper.HasElementAssignmentConflicts))]
|
||||
private static bool HasElementAssignmentConflictsPrefix(ControlMapper.InputMapping mapping, ref bool __result)
|
||||
{
|
||||
if (mapping == null || mapping.controllerType != ControllerType.Joystick || !KeybindManager.Keybinds.Any(x => x.RewiredInputAction?.id == mapping.fieldInfo.actionId)) return true;
|
||||
|
||||
__result = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(ControlMapper.Update))]
|
||||
private static void UpdatePrefix(ControlMapper __instance)
|
||||
{
|
||||
var conflicts = KeybindManager.GetConflicts();
|
||||
var controllerConflicts = KeybindManager.GetControllerConflicts();
|
||||
foreach (var element in __instance.themedElements)
|
||||
{
|
||||
var info = element.GetComponent<InputFieldInfo>();
|
||||
if (info == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (info == null) continue;
|
||||
|
||||
var key = info.glyphOrText?.actionElementMap?.keyboardKeyCode;
|
||||
if (key == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var map = ReInput.mapping.GetActionElementMap(info.actionElementMapId);
|
||||
var image = element.GetComponent<Image>();
|
||||
if (image == null) continue;
|
||||
|
||||
var conflicts = KeybindManager.GetConflicts();
|
||||
if (conflicts.Any(x => x.Key.ToString() == key.ToString()))
|
||||
var hasConflict = map?.controllerMap.controllerType switch
|
||||
{
|
||||
element.GetComponent<Image>().color = Color.red;
|
||||
ControllerType.Keyboard => conflicts.ContainsKey(map.keyboardKeyCode),
|
||||
ControllerType.Joystick => controllerConflicts.Any(x => x.controllerMap.id == map.controllerMap.id && x.id == map.id),
|
||||
_ => false,
|
||||
};
|
||||
if (hasConflict)
|
||||
{
|
||||
image.color = Color.red;
|
||||
}
|
||||
else if (image.color == Color.red)
|
||||
{
|
||||
image.color = Color.white;
|
||||
element.ApplyTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Keybinds;
|
||||
using Rewired;
|
||||
@@ -10,19 +12,18 @@ public static class KeybindMenuPatch
|
||||
private static bool _registered;
|
||||
|
||||
[HarmonyPrefix]
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private static void AwakePrefix(InputManager_Base __instance)
|
||||
{
|
||||
if (_registered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_registered) return;
|
||||
|
||||
KeybindUtils.RewiredInputManager = __instance;
|
||||
KeybindManager.RewiredInit();
|
||||
_registered = true;
|
||||
|
||||
// Set these here right after input manager initializes
|
||||
KeybindManager.VanillaKeybinds = new()
|
||||
KeybindManager.VanillaKeybinds = new Dictionary<Type, VanillaKeybind>
|
||||
{
|
||||
{ typeof(KillButton), new VanillaKeybind(8) },
|
||||
{ typeof(UseButton), new VanillaKeybind(6) },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using HarmonyLib;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Keybinds;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -10,40 +11,42 @@ public static class OptionsMenuBehaviourPatch
|
||||
private static ButtonRolloverHandler? _remapRollover;
|
||||
private static SpriteRenderer? _remapBackground;
|
||||
|
||||
private static bool Conflicts => KeybindManager.GetConflicts().Count > 0;
|
||||
private static bool Conflicts => KeybindManager.GetConflicts().Count > 0 || KeybindManager.GetControllerConflicts().Count > 0;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(OptionsMenuBehaviour.Open))]
|
||||
private static void OpenPostfix()
|
||||
private static void OpenPostfix(OptionsMenuBehaviour __instance)
|
||||
{
|
||||
_remapRollover = GameObject.Find("Remap Controls")?.GetComponent<ButtonRolloverHandler>()!;
|
||||
try
|
||||
{
|
||||
_remapBackground = _remapRollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
|
||||
_remapBackground.color = Conflicts ? Color.red : Color.white;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored, this normally breaks when on mobile since there's no remap button screen.
|
||||
}
|
||||
_remapRollover = __instance.GetComponentsInChildren<ButtonRolloverHandler>(true).FirstOrDefault(x => x.name == "Remap Controls");
|
||||
_remapBackground = _remapRollover?.transform.FindChild("Background")?.GetComponent<SpriteRenderer>();
|
||||
if (_remapBackground != null) _remapBackground.color = Conflicts ? Color.red : Color.white;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(OptionsMenuBehaviour.Update))]
|
||||
private static bool UpdatePrefix(OptionsMenuBehaviour __instance)
|
||||
{
|
||||
if (MiraApiPlugin.IsMobile || ActiveInputManager.currentControlType != ActiveInputManager.InputType.Joystick) return true;
|
||||
|
||||
if (Input.GetKeyUp(KeyCode.Escape) && !__instance.KeyboardOptions.activeSelf) __instance.Close();
|
||||
|
||||
__instance.KeyboardOptions.SetActive(true);
|
||||
__instance.MouseAndKeyboardOptions.SetActive(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(OptionsMenuBehaviour.Update))]
|
||||
private static void UpdatePostfix()
|
||||
{
|
||||
if (_remapRollover == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_remapRollover == null) return;
|
||||
|
||||
if (_remapBackground == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_remapBackground == null) return;
|
||||
|
||||
_remapRollover.OutColor = Conflicts ? Color.red : Color.white;
|
||||
_remapRollover.UnselectedColor = Conflicts ? Color.red : Color.white;
|
||||
_remapRollover.OverColor = Conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen;
|
||||
var conflicts = Conflicts;
|
||||
_remapRollover.OutColor = conflicts ? Color.red : Color.white;
|
||||
_remapRollover.UnselectedColor = conflicts ? Color.red : Color.white;
|
||||
_remapRollover.OverColor = conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen;
|
||||
if (!conflicts && _remapBackground.color == Color.red) _remapBackground.color = Color.white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Keybinds;
|
||||
using Rewired;
|
||||
|
||||
namespace MiraAPI.Patches.Keybinds;
|
||||
|
||||
[HarmonyPatch(typeof(Player))]
|
||||
public static class PlayerKeybindPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(Player.GetButtonDown), typeof(int))]
|
||||
private static void GetButtonPostfix(Player __instance, int __0, ref bool __result)
|
||||
{
|
||||
if (!__result || __instance.id != 0 || PlayerControl.LocalPlayer == null || ConsoleJoystick.inputState != ConsoleJoystick.ConsoleInputState.Gameplay || ControllerManager.Instance.IsUiControllerActive) return;
|
||||
|
||||
var keybind = KeybindManager.Keybinds.FirstOrDefault(x => x.RewiredInputAction?.id == __0);
|
||||
if (keybind is { Exclusive: false }) return;
|
||||
|
||||
var sources = new CppCollections.List<InputActionSourceData>(__instance.GetCurrentInputSources(__0).Cast<CppCollections.IEnumerable<InputActionSourceData>>()).ToArray();
|
||||
if (sources.Length == 0 || sources.Any(x => x.controllerType != ControllerType.Joystick)) return;
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var claimed = false;
|
||||
foreach (var custom in KeybindManager.Keybinds)
|
||||
{
|
||||
if (custom == keybind) break;
|
||||
if (!custom.Exclusive || custom.RewiredInputAction == null || !KeybindUtils.IsControllerActionActive(custom.RewiredInputAction.id)) continue;
|
||||
|
||||
var inputs = new CppCollections.List<InputActionSourceData>(__instance.GetCurrentInputSources(custom.RewiredInputAction.id).Cast<CppCollections.IEnumerable<InputActionSourceData>>()).ToArray();
|
||||
if (!inputs.Any(x => x.controllerType == ControllerType.Joystick && x.controller.id == source.controller.id && x.actionElementMap.CheckForAssignmentConflict(source.actionElementMap))) continue;
|
||||
|
||||
claimed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!claimed) return;
|
||||
}
|
||||
|
||||
__result = false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(Player.GetButton), typeof(int))]
|
||||
private static void GetButtonHeldPostfix(Player __instance, int __0, ref bool __result)
|
||||
{
|
||||
GetButtonPostfix(__instance, __0, ref __result);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(Player.GetButtonDown), typeof(string))]
|
||||
private static void GetButtonByNamePostfix(Player __instance, string __0, ref bool __result)
|
||||
{
|
||||
if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(Player.GetButton), typeof(string))]
|
||||
private static void GetButtonHeldByNamePostfix(Player __instance, string __0, ref bool __result)
|
||||
{
|
||||
if (__result) GetButtonPostfix(__instance, ReInput.mapping.GetActionId(__0), ref __result);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using MiraAPI.LocalSettings;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
@@ -15,12 +15,18 @@ namespace MiraAPI.Patches.LocalSettings;
|
||||
public static class OptionsMenuPatches
|
||||
{
|
||||
private static readonly Dictionary<int, List<GameObject>> TabButtons = [];
|
||||
private static int currentPage = 1;
|
||||
private static int _currentPage = 1;
|
||||
|
||||
internal static OptionsMenuBehaviour? Instance { get; private set; }
|
||||
|
||||
[SuppressMessage("Critical Code Smell", "S2223:Non-constant static fields should not be visible", Justification = "Internal behaviour that does not need property-level validation.")]
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
|
||||
[SuppressMessage(
|
||||
"Critical Code Smell",
|
||||
"S2223:Non-constant static fields should not be visible",
|
||||
Justification = "Internal behaviour that does not need property-level validation.")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.MaintainabilityRules",
|
||||
"SA1401:Fields should be private",
|
||||
Justification = "Read above.")]
|
||||
internal static BoxCollider2D MaskCollider = null!;
|
||||
|
||||
private static SpriteRenderer? background;
|
||||
@@ -69,7 +75,7 @@ public static class OptionsMenuPatches
|
||||
MaskCollider.isTrigger = true;
|
||||
MaskCollider.enabled = true;
|
||||
|
||||
currentPage = 1;
|
||||
_currentPage = 1;
|
||||
TabButtons.Clear();
|
||||
float yOffset = 0;
|
||||
var i = 0;
|
||||
@@ -83,7 +89,7 @@ public static class OptionsMenuPatches
|
||||
{
|
||||
var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset);
|
||||
button.GetComponent<TabGroup>().Content = tab;
|
||||
button.SetActive(page == currentPage);
|
||||
button.SetActive(page == _currentPage);
|
||||
|
||||
if (!TabButtons.TryGetValue(page, out var list))
|
||||
{
|
||||
@@ -105,20 +111,6 @@ public static class OptionsMenuPatches
|
||||
}
|
||||
}
|
||||
|
||||
static void ChangePage(int increase)
|
||||
{
|
||||
currentPage += increase;
|
||||
if (currentPage > TabButtons.Count)
|
||||
{
|
||||
currentPage = 1;
|
||||
}
|
||||
|
||||
if (currentPage < 1)
|
||||
{
|
||||
currentPage = TabButtons.Count;
|
||||
}
|
||||
}
|
||||
|
||||
// Create page buttons
|
||||
// TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz
|
||||
if (TabButtons.Count <= 1)
|
||||
@@ -126,44 +118,56 @@ public static class OptionsMenuPatches
|
||||
return;
|
||||
}
|
||||
|
||||
var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
|
||||
nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10);
|
||||
nextButton.transform.localScale = new Vector2(0.25f, 0.25f);
|
||||
var nextRend = nextButton.GetComponent<SpriteRenderer>();
|
||||
nextRend.sprite = MiraAssets.NextButton.LoadAsset();
|
||||
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
|
||||
CreatePageButton(__instance, 3.2f, false, 1);
|
||||
CreatePageButton(__instance, 2.9f, true, -1);
|
||||
}
|
||||
|
||||
var nextRollover = nextButton.gameObject.AddComponent<ButtonRolloverHandler>();
|
||||
nextRollover.Target = nextRend;
|
||||
nextRollover.OverColor = Palette.AcceptedGreen;
|
||||
private static void CreatePageButton(OptionsMenuBehaviour menu, float x, bool flip, int direction)
|
||||
{
|
||||
var obj = new GameObject("PageButton") { layer = 5 };
|
||||
obj.transform.SetParent(menu.transform);
|
||||
obj.transform.localPosition = new Vector3(x, 2.6f, -10);
|
||||
obj.transform.localScale = new Vector3(0.25f, 0.25f, 1);
|
||||
|
||||
nextButton.OnClick = new Button.ButtonClickedEvent();
|
||||
nextButton.OnClick.AddListener(
|
||||
var rend = obj.AddComponent<SpriteRenderer>();
|
||||
rend.sprite = MiraAssets.NextButton.LoadAsset();
|
||||
rend.flipX = flip;
|
||||
|
||||
var col = obj.AddComponent<BoxCollider2D>();
|
||||
col.size = rend.sprite.bounds.size;
|
||||
|
||||
var rollover = obj.AddComponent<ButtonRolloverHandler>();
|
||||
rollover.Target = rend;
|
||||
rollover.OverColor = Palette.AcceptedGreen;
|
||||
rollover.OutColor = Color.white;
|
||||
|
||||
var button = obj.AddComponent<PassiveButton>();
|
||||
button.Colliders = new Il2CppReferenceArray<Collider2D>([col]);
|
||||
button.OnClick = new Button.ButtonClickedEvent();
|
||||
button.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
ChangePage(1);
|
||||
ChangePage(direction);
|
||||
UpdatePages();
|
||||
}));
|
||||
button.OnMouseOver = new UnityEvent();
|
||||
button.OnMouseOver.AddListener((UnityAction)(() => rollover.DoMouseOver()));
|
||||
button.OnMouseOut = new UnityEvent();
|
||||
button.OnMouseOut.AddListener((UnityAction)(() => rollover.DoMouseOut()));
|
||||
}
|
||||
|
||||
var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
|
||||
backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10);
|
||||
backButton.transform.localScale = new Vector2(0.25f, 0.25f);
|
||||
var backRend = backButton.GetComponent<SpriteRenderer>();
|
||||
backRend.sprite = MiraAssets.NextButton.LoadAsset();
|
||||
backRend.flipX = true;
|
||||
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
|
||||
private static void ChangePage(int increase)
|
||||
{
|
||||
_currentPage += increase;
|
||||
if (_currentPage > TabButtons.Count)
|
||||
{
|
||||
_currentPage = 1;
|
||||
}
|
||||
|
||||
var backRollover = backButton.gameObject.AddComponent<ButtonRolloverHandler>();
|
||||
backRollover.Target = backRend;
|
||||
backRollover.OverColor = Palette.AcceptedGreen;
|
||||
|
||||
backButton.OnClick = new Button.ButtonClickedEvent();
|
||||
backButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
ChangePage(-1);
|
||||
UpdatePages();
|
||||
}));
|
||||
if (_currentPage < 1)
|
||||
{
|
||||
_currentPage = TabButtons.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -194,7 +198,7 @@ public static class OptionsMenuPatches
|
||||
public static void OpenPostfix()
|
||||
{
|
||||
LocalSettingsManager.AvailableTabs.ForEach(CustomClose);
|
||||
currentPage = 1;
|
||||
_currentPage = 1;
|
||||
UpdatePages();
|
||||
}
|
||||
|
||||
@@ -204,9 +208,7 @@ public static class OptionsMenuPatches
|
||||
{
|
||||
pages.Value?
|
||||
.Where(x => x != null)
|
||||
.Do(
|
||||
x => x?.SetActive(
|
||||
pages.Key == currentPage));
|
||||
.Do(x => x?.SetActive(pages.Key == _currentPage));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(HatsTab))]
|
||||
public static class HatsTabPatches
|
||||
{
|
||||
private static readonly Dictionary<int, string> StoreNames = [];
|
||||
private static SortedList<string, List<HatData>> sortedHats = [];
|
||||
private static int currentPage;
|
||||
|
||||
private static void PreviousPage(HatsTab hatsTab)
|
||||
{
|
||||
currentPage--;
|
||||
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
|
||||
GenerateHats(hatsTab, currentPage);
|
||||
}
|
||||
|
||||
private static void NextPage(HatsTab hatsTab)
|
||||
{
|
||||
currentPage++;
|
||||
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
|
||||
GenerateHats(hatsTab, currentPage);
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(HatsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
|
||||
public static bool OnEnablePrefix(HatsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableHatsExist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
|
||||
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
|
||||
|
||||
if (sortedHats.Count == 0)
|
||||
{
|
||||
var num = 0;
|
||||
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
|
||||
sortedHats = new SortedList<string, List<HatData>>(comparer);
|
||||
foreach (var hat in allHats)
|
||||
{
|
||||
if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
|
||||
sortedHats[hat.StoreName].Add(hat);
|
||||
|
||||
if (!StoreNames.ContainsValue(hat.StoreName))
|
||||
{
|
||||
StoreNames.Add(num, hat.StoreName);
|
||||
num++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
|
||||
|
||||
GenerateHats(__instance, currentPage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
|
||||
[HarmonyPrefix]
|
||||
public static void UpdatePrefix(HatsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableHatsExist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortedHats.Count == 0) return;
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
|
||||
{
|
||||
PreviousPage(__instance);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
|
||||
{
|
||||
NextPage(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator? loadRoutine;
|
||||
private static int hatIndex;
|
||||
|
||||
private static void GenerateHats(HatsTab __instance, int page)
|
||||
{
|
||||
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
|
||||
|
||||
hatIndex = 0;
|
||||
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
var group = sortedHats.Where(x => x.Key == StoreNames[page]);
|
||||
|
||||
foreach (var (groupName, hats) in group)
|
||||
{
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 5f;
|
||||
text.fontSizeMax = 5f;
|
||||
text.fontSizeMin = 0f;
|
||||
var xLerp = __instance.XRange.Lerp(0.5f);
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats));
|
||||
}
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.currentHatIsEquipped = true;
|
||||
}
|
||||
|
||||
private static IEnumerator CoGenerateChips(HatsTab __instance, List<HatData> hats)
|
||||
{
|
||||
__instance.scroller.ScrollToTop();
|
||||
var batchSize = 5;
|
||||
for (var i = 0; i < hats.Count; i += batchSize)
|
||||
{
|
||||
var batch = hats.Skip(i).Take(batchSize).ToList();
|
||||
|
||||
foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf))
|
||||
{
|
||||
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), hat);
|
||||
hatIndex += 1;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
__instance.SetScrollerBounds();
|
||||
yield return new WaitForSeconds(0.01f);
|
||||
}
|
||||
|
||||
__instance.currentHatIsEquipped = true;
|
||||
loadRoutine = null;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = hat.ProductId;
|
||||
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
|
||||
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
|
||||
}
|
||||
else
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(hat)));
|
||||
}
|
||||
|
||||
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
|
||||
colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = hat;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
public static class InventoryUtility
|
||||
{
|
||||
public static void CreateNextBackButtons<T>(T tab, Action<T> previousPage, Action<T> nextPage)
|
||||
where T : InventoryTab
|
||||
{
|
||||
var title = tab.transform.FindChild("Text");
|
||||
title.localPosition = new Vector3(.9123f, -.23f, -55);
|
||||
title.GetComponent<TextMeshPro>().alignment = TextAlignmentOptions.Center;
|
||||
|
||||
tab.scroller.Hitbox.transform.localScale -= new Vector3(0, 0.1f, 0);
|
||||
tab.scroller.Hitbox.transform.localPosition -= new Vector3(0, 0.1f, 0);
|
||||
|
||||
var leftButtonElement = Object.Instantiate(PlayerCustomizationMenu.Instance.BackButton.gameObject, tab.transform);
|
||||
var leftSpriteRenderer = leftButtonElement.GetComponent<SpriteRenderer>();
|
||||
var leftButton = leftButtonElement.GetComponent<PassiveButton>();
|
||||
|
||||
leftButtonElement.transform.localPosition = new Vector3(-1.1f, -0.2f, -55);
|
||||
leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
|
||||
leftSpriteRenderer.flipX = true;
|
||||
|
||||
leftButton.OnMouseOver = new UnityEvent();
|
||||
leftButton.OnMouseOver.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
|
||||
leftButton.OnMouseOut = new UnityEvent();
|
||||
leftButton.OnMouseOut.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
|
||||
|
||||
leftButton.OnClick = new Button.ButtonClickedEvent();
|
||||
leftButton.OnClick.AddListener((Action)(() => previousPage(tab)));
|
||||
|
||||
var rightButtonElement = Object.Instantiate(leftButtonElement, tab.transform);
|
||||
var rightSpriteRenderer = rightButtonElement.GetComponent<SpriteRenderer>();
|
||||
var rightButton = rightButtonElement.GetComponent<PassiveButton>();
|
||||
|
||||
rightButtonElement.transform.localPosition = new Vector3(2.8f, -0.2f, -55);
|
||||
rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
|
||||
rightSpriteRenderer.flipX = false;
|
||||
|
||||
rightButton.OnMouseOver = new UnityEvent();
|
||||
rightButton.OnMouseOver.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
|
||||
rightButton.OnMouseOut = new UnityEvent();
|
||||
rightButton.OnMouseOut.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
|
||||
|
||||
rightButton.OnClick = new Button.ButtonClickedEvent();
|
||||
rightButton.OnClick.AddListener((Action)(() => nextPage(tab)));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.LocalSettings;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -23,10 +22,9 @@ public static class MainMenuManagerPatches
|
||||
[HarmonyPostfix]
|
||||
public static void AwakePostfix()
|
||||
{
|
||||
var requiredVersion = new Version(2026, 6, 5);
|
||||
var requiredVersion = new Version(2026, 9, 29);
|
||||
var version = Version.Parse(Application.version);
|
||||
NeedsDeepDestroy = version >= requiredVersion;
|
||||
AddressablesLoader.LoadAll();
|
||||
Coroutines.Start(SetFps());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using Innersloth.Assets;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(NameplatesTab))]
|
||||
public static class NameplatesTabPatches
|
||||
{
|
||||
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
|
||||
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
|
||||
private static int currentPage;
|
||||
|
||||
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
|
||||
{
|
||||
foreach (var (key, visor) in data)
|
||||
{
|
||||
if (!SortedNameplates.ContainsKey(key)) SortedNameplates.Add(key, []);
|
||||
SortedNameplates[key].Add(visor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviousPage(NameplatesTab tab)
|
||||
{
|
||||
currentPage--;
|
||||
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
private static void NextPage(NameplatesTab tab)
|
||||
{
|
||||
currentPage++;
|
||||
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(NameplatesTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableNameplatesExist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
|
||||
|
||||
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
|
||||
|
||||
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
|
||||
|
||||
GenerateHats(__instance, currentPage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))]
|
||||
[HarmonyPrefix]
|
||||
|
||||
public static void UpdatePrefix(NameplatesTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableNameplatesExist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
|
||||
{
|
||||
PreviousPage(__instance);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
|
||||
{
|
||||
NextPage(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateHats(NameplatesTab __instance, int page)
|
||||
{
|
||||
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
var hatIndex = 0;
|
||||
|
||||
var (groupName, nameplates) = SortedNameplates.ToArray()[page];
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 5f;
|
||||
text.fontSizeMax = 5f;
|
||||
text.fontSizeMin = 0f;
|
||||
var xLerp = __instance.XRange.Lerp(0.5f);
|
||||
var yLerp = __instance.YStart - __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 2;
|
||||
foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
|
||||
{
|
||||
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentNameplateIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = namePlate.ProductId;
|
||||
|
||||
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
|
||||
colorChip.Button.OnMouseOut.AddListener(
|
||||
(Action)(() => __instance.SelectNameplate(
|
||||
HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
|
||||
}
|
||||
else
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
|
||||
}
|
||||
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = namePlate.ProdId;
|
||||
|
||||
__instance.StartCoroutine(__instance.CoLoadAssetAsync<NamePlateViewData>(namePlate.GetAssetReference(), (Action<NamePlateViewData>?)LoadNameplate));
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Tag = namePlate;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
return;
|
||||
|
||||
void LoadNameplate(NamePlateViewData viewData)
|
||||
{
|
||||
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewData.Image;
|
||||
|
||||
// (colorChip as NameplateChip).image.sprite = viewData?.Image;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(SkinsTab))]
|
||||
public static class SkinsTabPatches
|
||||
{
|
||||
private static SortedList<string, List<SkinData>> sortedSkins = [];
|
||||
private static int currentPage;
|
||||
|
||||
private static void PreviousPage(SkinsTab tab)
|
||||
{
|
||||
currentPage--;
|
||||
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
private static void NextPage(SkinsTab tab)
|
||||
{
|
||||
currentPage++;
|
||||
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
|
||||
public static bool OnEnablePrefix(SkinsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableSkinsExist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
|
||||
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
|
||||
|
||||
if (sortedSkins.Count == 0)
|
||||
{
|
||||
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
|
||||
sortedSkins = new SortedList<string, List<SkinData>>(comparer);
|
||||
foreach (var skin in allSkins)
|
||||
{
|
||||
if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
|
||||
sortedSkins[skin.StoreName].Add(skin);
|
||||
}
|
||||
}
|
||||
|
||||
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
|
||||
|
||||
GenerateHats(__instance, currentPage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))]
|
||||
[HarmonyPrefix]
|
||||
|
||||
public static void UpdatePrefix(SkinsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableSkinsExist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
|
||||
{
|
||||
PreviousPage(__instance);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
|
||||
{
|
||||
NextPage(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateHats(SkinsTab __instance, int page)
|
||||
{
|
||||
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
var hatIndex = 0;
|
||||
|
||||
var (groupName, skins) = sortedSkins.ToArray()[page];
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 5f;
|
||||
text.fontSizeMax = 5f;
|
||||
text.fontSizeMin = 0f;
|
||||
var xLerp = __instance.XRange.Lerp(0.5f);
|
||||
var yLerp = __instance.YStart - __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
|
||||
{
|
||||
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), skin);
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentSkinIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = skin.ProductId;
|
||||
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
|
||||
colorChip.Button.OnMouseOut.AddListener(
|
||||
(Action)(() =>
|
||||
__instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
|
||||
}
|
||||
else
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectSkin(skin)));
|
||||
}
|
||||
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = skin.ProductId;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
|
||||
skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = skin;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(VisorsTab))]
|
||||
public static class VisorsTabPatches
|
||||
{
|
||||
[SuppressMessage("Style", "IDE0028:Collection initialization can be simplified", Justification = "Preview feature.")]
|
||||
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
|
||||
private static int currentPage;
|
||||
|
||||
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
|
||||
{
|
||||
foreach (var (key, visor) in data)
|
||||
{
|
||||
if (!SortedVisors.ContainsKey(key)) SortedVisors.Add(key, []);
|
||||
SortedVisors[key].Add(visor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviousPage(VisorsTab tab)
|
||||
{
|
||||
currentPage--;
|
||||
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
private static void NextPage(VisorsTab tab)
|
||||
{
|
||||
currentPage++;
|
||||
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
|
||||
GenerateHats(tab, currentPage);
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(VisorsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableVisorsExist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
|
||||
|
||||
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(HatManager.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
|
||||
|
||||
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
|
||||
|
||||
GenerateHats(__instance, currentPage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))]
|
||||
[HarmonyPrefix]
|
||||
|
||||
public static void UpdatePrefix(VisorsTab __instance)
|
||||
{
|
||||
if (!AddressablesLoader.AddressableVisorsExist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
|
||||
{
|
||||
PreviousPage(__instance);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
|
||||
{
|
||||
NextPage(__instance);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateHats(VisorsTab __instance, int page)
|
||||
{
|
||||
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.DeepDestroy(false));
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
var hatIndex = 0;
|
||||
|
||||
var (groupName, visors) = SortedVisors.ToArray()[page];
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 5f;
|
||||
text.fontSizeMax = 5f;
|
||||
text.fontSizeMin = 0f;
|
||||
var xLerp = __instance.XRange.Lerp(0.5f);
|
||||
var yLerp = __instance.YStart - __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
|
||||
{
|
||||
var hatXPosition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (Justification: Intended.)
|
||||
var hatYPosition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXPosition, hatYPosition), visor);
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentVisorIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = visor.ProductId;
|
||||
|
||||
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
|
||||
colorChip.Button.OnMouseOut.AddListener(
|
||||
(Action)(() =>
|
||||
__instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
|
||||
}
|
||||
else
|
||||
{
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectVisor(visor)));
|
||||
}
|
||||
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = visor.ProductId;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
|
||||
visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = visor;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BepInEx.Unity.IL2CPP.Utils.Collections;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using Il2CppSystem.Collections.Generic;
|
||||
using MiraAPI.GameModes;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Modifiers;
|
||||
@@ -25,12 +25,12 @@ namespace MiraAPI.Patches.Options;
|
||||
[HarmonyPatch(typeof(GameOptionsMenu))]
|
||||
internal static class GameOptionsMenuPatch
|
||||
{
|
||||
private static readonly List<CategoryHeaderMasked> VanillaHeaders = new();
|
||||
private static readonly List<OptionBehaviour> VanillaOptions = new();
|
||||
private static readonly List<OptionBehaviour> MainOptions = new();
|
||||
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<AbstractOptionGroup>> GameModeGroups = [];
|
||||
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<OptionBehaviour>> GameModeOptions = [];
|
||||
private static readonly System.Collections.Generic.Dictionary<AbstractGameMode, System.Collections.Generic.List<CategoryHeaderMasked>> GameModeHeaders = [];
|
||||
private static readonly List<CategoryHeaderMasked> VanillaHeaders = [];
|
||||
private static readonly List<OptionBehaviour> VanillaOptions = [];
|
||||
private static readonly List<OptionBehaviour> MainOptions = [];
|
||||
private static readonly Dictionary<AbstractGameMode, List<AbstractOptionGroup>> GameModeGroups = [];
|
||||
private static readonly Dictionary<AbstractGameMode, List<OptionBehaviour>> GameModeOptions = [];
|
||||
private static readonly Dictionary<AbstractGameMode, List<CategoryHeaderMasked>> GameModeHeaders = [];
|
||||
private static TextMeshPro _gamemodeDescription = null!;
|
||||
private static SpriteRenderer _modIcon = null!;
|
||||
|
||||
@@ -38,7 +38,8 @@ internal static class GameOptionsMenuPatch
|
||||
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
|
||||
public static bool InitPatch(GameOptionsMenu __instance)
|
||||
{
|
||||
__instance.Children ??= new List<OptionBehaviour>();
|
||||
Helpers.CacheAdvancedSettingsRowPrefab(__instance.numberOptionOrigin);
|
||||
__instance.Children ??= new();
|
||||
__instance.Children.Clear();
|
||||
|
||||
if (MenuState.Instance.CurrentModIdx == 0)
|
||||
@@ -187,6 +188,66 @@ internal static class GameOptionsMenuPatch
|
||||
instance.scrollBar.SetYBoundsMax(-num - 1.65f);
|
||||
}
|
||||
|
||||
internal static void RefreshForGameMode()
|
||||
{
|
||||
if (CustomGameModeManager.ActiveMode == null ||
|
||||
!GameSettingMenu.Instance ||
|
||||
!GameSettingMenu.Instance.GameSettingsTab ||
|
||||
!MenuState.Instance ||
|
||||
MenuState.Instance.CurrentModIdx != 0 ||
|
||||
GameModeGroups.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToggleGamemodeOptions(CustomGameModeManager.ActiveMode, GameSettingMenu.Instance.GameSettingsTab);
|
||||
GameSettingMenu.Instance.RoleSettingsButton.gameObject.SetActive(
|
||||
CustomGameModeManager.ActiveMode.ShowNormalRoleSettings);
|
||||
}
|
||||
|
||||
internal static List<KeyValuePair<string, string>> GetActiveGameSettingsRows(Func<bool, string> formatBool)
|
||||
{
|
||||
var menu = GameSettingMenu.Instance ? GameSettingMenu.Instance.GameSettingsTab : null;
|
||||
if (!menu || menu!.Children == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var rows = new List<KeyValuePair<string, string>>();
|
||||
foreach (var option in menu.Children)
|
||||
{
|
||||
if (!option || !option.gameObject.activeInHierarchy ||
|
||||
option == menu.MapPicker ||
|
||||
(GameModeOption.OptionBehaviour && option == GameModeOption.OptionBehaviour) ||
|
||||
!option.Data)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var title = option switch
|
||||
{
|
||||
ToggleOption toggle => toggle.TitleText.text,
|
||||
NumberOption number => number.TitleText.text,
|
||||
StringOption str => str.TitleText.text,
|
||||
PlayerOption player => player.TitleText.text,
|
||||
_ => TranslationController.Instance.GetString(option.Data.Title),
|
||||
};
|
||||
|
||||
var value = option switch
|
||||
{
|
||||
ToggleOption toggle => formatBool(toggle.GetBool()),
|
||||
NumberOption number => number.ValueText.text,
|
||||
StringOption str => str.ValueText.text,
|
||||
PlayerOption player => player.ValueText.text,
|
||||
_ => option.Data.GetValueString(option.GetFloat()),
|
||||
};
|
||||
|
||||
rows.Add(new KeyValuePair<string, string>(title, value));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static void CreateSettings(GameOptionsMenu instance, Transform container)
|
||||
{
|
||||
var num = 0.713f;
|
||||
@@ -324,8 +385,8 @@ internal static class GameOptionsMenuPatch
|
||||
.ToList();
|
||||
GameModeGroups.Add(mode, [.. filteredGroups]);
|
||||
|
||||
var optionBehaviours = new System.Collections.Generic.List<OptionBehaviour>();
|
||||
var categoryHeaders = new System.Collections.Generic.List<CategoryHeaderMasked>();
|
||||
var optionBehaviours = new List<OptionBehaviour>();
|
||||
var categoryHeaders = new List<CategoryHeaderMasked>();
|
||||
var newNum = mode.ShowNormalGameSettings ? num : num - additionalVanillaScrollNum;
|
||||
foreach (var group in filteredGroups)
|
||||
{
|
||||
@@ -406,7 +467,7 @@ internal static class GameOptionsMenuPatch
|
||||
__instance.scrollBar.SetYBoundsMax(-num - 1.65f);
|
||||
}
|
||||
|
||||
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref System.Collections.Generic.List<OptionBehaviour> optionBehaviours, ref System.Collections.Generic.List<CategoryHeaderMasked> categoryHeaders)
|
||||
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, ref float num, ref List<OptionBehaviour> optionBehaviours, ref List<CategoryHeaderMasked> categoryHeaders)
|
||||
{
|
||||
var categoryHeaderMasked = Object.Instantiate(
|
||||
menu.categoryHeaderOrigin,
|
||||
@@ -618,6 +679,8 @@ internal static class GameOptionsMenuPatch
|
||||
{
|
||||
menu.MapPicker.gameObject.SetActive(false);
|
||||
var mod = MenuState.Instance.CurrentMod;
|
||||
var container = MenuState.Instance.CurrentContainer.transform;
|
||||
PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset);
|
||||
|
||||
var filteredGroups = MenuState.Instance.CurrentMenu switch
|
||||
{
|
||||
@@ -643,15 +706,13 @@ internal static class GameOptionsMenuPatch
|
||||
{
|
||||
foreach (var group in filteredGroups)
|
||||
{
|
||||
yield return CoCreateGroup(menu, group);
|
||||
yield return CoCreateGroup(menu, group, container, defaultPreset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group)
|
||||
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset)
|
||||
{
|
||||
var container = MenuState.Instance.CurrentContainer.transform;
|
||||
|
||||
group.Ready = false;
|
||||
if (group.Header == null || !group.Header)
|
||||
{
|
||||
@@ -713,12 +774,6 @@ internal static class GameOptionsMenuPatch
|
||||
menu.playerOptionOrigin,
|
||||
container));
|
||||
|
||||
OptionPreset? defaultPreset = null;
|
||||
if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset))
|
||||
{
|
||||
defaultPreset = preset;
|
||||
}
|
||||
|
||||
yield return options.CoLoopWithBudget(newOpt =>
|
||||
{
|
||||
newOpt.SetClickMask(menu.ButtonClickMask);
|
||||
@@ -765,73 +820,53 @@ internal static class GameOptionsMenuPatch
|
||||
}
|
||||
|
||||
menu.Children.Add(newOpt);
|
||||
var resetBtn = new GameObject("ResetOption")
|
||||
if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) &&
|
||||
newOpt is ToggleOption or NumberOption or StringOption)
|
||||
{
|
||||
transform =
|
||||
var resetBtn = new GameObject("ResetOption")
|
||||
{
|
||||
parent = newOpt.transform,
|
||||
localScale = new(.5f, .5f, 1),
|
||||
localPosition = new Vector3(-3.1f, 0f, -2f),
|
||||
},
|
||||
layer = LayerMask.NameToLayer("UI"),
|
||||
};
|
||||
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
|
||||
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
|
||||
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
|
||||
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
|
||||
? Color.white
|
||||
: group.GroupColor.FindAlternateColor();
|
||||
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
|
||||
resetBoxCol.size = new Vector2(1f, 1f);
|
||||
resetBoxCol.offset = new Vector2(0, 0);
|
||||
var passiveButton = resetBtn.AddComponent<PassiveButton>();
|
||||
passiveButton.OnClick = new Button.ButtonClickedEvent();
|
||||
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
|
||||
passiveButton.OnMouseOver = new UnityEvent();
|
||||
passiveButton.OnMouseOver.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
|
||||
? group.GroupColor
|
||||
: MiraAssets.AcceptedTeal;
|
||||
}));
|
||||
passiveButton.OnMouseOut = new UnityEvent();
|
||||
passiveButton.OnMouseOut.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
|
||||
? Color.white
|
||||
: group.GroupColor.FindAlternateColor();
|
||||
}));
|
||||
if (newOpt is ToggleOption toggleOpt)
|
||||
{
|
||||
transform =
|
||||
{
|
||||
parent = newOpt.transform,
|
||||
localScale = new(.5f, .5f, 1),
|
||||
localPosition = new Vector3(-3.1f, 0f, -2f),
|
||||
},
|
||||
layer = LayerMask.NameToLayer("UI"),
|
||||
};
|
||||
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
|
||||
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
|
||||
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
|
||||
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
|
||||
? Color.white
|
||||
: group.GroupColor.FindAlternateColor();
|
||||
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
|
||||
resetBoxCol.size = new Vector2(1f, 1f);
|
||||
resetBoxCol.offset = new Vector2(0, 0);
|
||||
var passiveButton = resetBtn.AddComponent<PassiveButton>();
|
||||
passiveButton.OnClick = new Button.ButtonClickedEvent();
|
||||
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
|
||||
passiveButton.OnMouseOver = new UnityEvent();
|
||||
passiveButton.OnMouseOver.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
|
||||
? group.GroupColor
|
||||
: MiraAssets.AcceptedTeal;
|
||||
}));
|
||||
passiveButton.OnMouseOut = new UnityEvent();
|
||||
passiveButton.OnMouseOut.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
|
||||
? Color.white
|
||||
: group.GroupColor.FindAlternateColor();
|
||||
}));
|
||||
passiveButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
defaultPreset!.ResetOption(toggleOpt);
|
||||
defaultPreset.ResetOption(newOpt);
|
||||
}));
|
||||
}
|
||||
else if (newOpt is NumberOption numOpt)
|
||||
{
|
||||
passiveButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
defaultPreset!.ResetOption(numOpt);
|
||||
}));
|
||||
}
|
||||
else if (newOpt is StringOption strOpt)
|
||||
{
|
||||
passiveButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
defaultPreset!.ResetOption(strOpt);
|
||||
}));
|
||||
}
|
||||
|
||||
if (!defaultPreset!.IsOptionInPreset(newOpt))
|
||||
{
|
||||
resetBtn.Destroy();
|
||||
}
|
||||
|
||||
newOpt.Initialize();
|
||||
newOpt.gameObject.SetActive(false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
@@ -13,6 +14,11 @@ internal static class GameSettingMenuPatches
|
||||
[HarmonyPatch(nameof(GameSettingMenu.OnEnable))]
|
||||
public static void OnEnablePrefix(GameSettingMenu __instance)
|
||||
{
|
||||
if (__instance.GameSettingsTab && __instance.GameSettingsTab.numberOptionOrigin)
|
||||
{
|
||||
Helpers.CacheAdvancedSettingsRowPrefab(__instance.GameSettingsTab.numberOptionOrigin);
|
||||
}
|
||||
|
||||
// Add Menu State object
|
||||
if (!__instance.gameObject.GetComponent<MenuState>())
|
||||
{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Translation;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
using Object = Il2CppSystem.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
@@ -34,6 +32,11 @@ public static class OptionsPatches
|
||||
__instance.roleChance = 0;
|
||||
}
|
||||
|
||||
if (__instance.roleChance == 0)
|
||||
{
|
||||
__instance.roleMaxCount = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
@@ -60,6 +63,11 @@ public static class OptionsPatches
|
||||
__instance.roleChance = 100;
|
||||
}
|
||||
|
||||
if (__instance.roleChance == 0)
|
||||
{
|
||||
__instance.roleMaxCount = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
@@ -84,6 +92,11 @@ public static class OptionsPatches
|
||||
__instance.roleMaxCount = 0;
|
||||
}
|
||||
|
||||
if (__instance.roleMaxCount == 0)
|
||||
{
|
||||
__instance.roleChance = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
@@ -108,6 +121,11 @@ public static class OptionsPatches
|
||||
__instance.roleMaxCount = __instance.role.MaxCount;
|
||||
}
|
||||
|
||||
if (__instance.roleMaxCount == 0)
|
||||
{
|
||||
__instance.roleChance = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
@@ -165,7 +183,7 @@ public static class OptionsPatches
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -314,8 +332,8 @@ public static class OptionsPatches
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>()).Translate();
|
||||
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title).Translate();
|
||||
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public static class PlayerOptionPatches
|
||||
{
|
||||
filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
|
||||
|
||||
moddedPlrOpt.Values = filteredPlayers;
|
||||
__instance.Values = filteredPlayers.ToIl2CppList();
|
||||
moddedPlrOpt.Values = __instance.Values;
|
||||
__instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
|
||||
__instance.SetValueText();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.GameOptions;
|
||||
@@ -24,7 +23,6 @@ public static class SettingPatches
|
||||
/// <returns><see langword="false"/> to skip original method.</returns>
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(FloatGameSetting), nameof(FloatGameSetting.GetValueString))]
|
||||
[SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Warning cascades into forcing the entire tree to be ternary operators.")]
|
||||
public static bool ValueStringPatch(
|
||||
FloatGameSetting __instance,
|
||||
ref string __result,
|
||||
@@ -48,14 +46,14 @@ public static class SettingPatches
|
||||
}
|
||||
else
|
||||
{
|
||||
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
|
||||
result = suffix switch
|
||||
{
|
||||
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
|
||||
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
|
||||
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
|
||||
_ => TranslationController.Instance.GetString(
|
||||
StringNames.GameSecondsAbbrev,
|
||||
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
|
||||
MiraNumberSuffixes.None => part,
|
||||
MiraNumberSuffixes.Multiplier => part + "x",
|
||||
MiraNumberSuffixes.Percent => part + "%",
|
||||
MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
|
||||
_ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -65,14 +63,14 @@ public static class SettingPatches
|
||||
}
|
||||
else
|
||||
{
|
||||
var part = value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo);
|
||||
result = suffix switch
|
||||
{
|
||||
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
|
||||
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
|
||||
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
|
||||
_ => TranslationController.Instance.GetString(
|
||||
StringNames.GameSecondsAbbrev,
|
||||
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
|
||||
MiraNumberSuffixes.None => part,
|
||||
MiraNumberSuffixes.Multiplier => part + "x",
|
||||
MiraNumberSuffixes.Percent => part + "%",
|
||||
MiraNumberSuffixes.Distance => string.Concat(part, " ", MiraApiPlugin.DistanceSuffix.Value),
|
||||
_ => TranslationController.Instance.GetString(StringNames.GameSecondsAbbrev, part),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2086
-172
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,4 @@
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.Roles;
|
||||
using UnityEngine;
|
||||
@@ -31,13 +30,13 @@ public static class KillButtonPatches
|
||||
|
||||
if (__instance.currentTarget && __instance.currentTarget != target)
|
||||
{
|
||||
__instance.currentTarget.cosmetics.SetOutline(false, new Nullable<Color>(Color.clear));
|
||||
__instance.currentTarget.cosmetics.SetOutline(false, new Il2CppSystem.Nullable<Color>(Color.clear));
|
||||
}
|
||||
|
||||
__instance.currentTarget = target;
|
||||
if (__instance.currentTarget)
|
||||
{
|
||||
__instance.currentTarget.cosmetics.SetOutline(true, new Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
|
||||
__instance.currentTarget.cosmetics.SetOutline(true, new Il2CppSystem.Nullable<Color>(customRole.Configuration.KillButtonOutlineColor));
|
||||
__instance.SetEnabled();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AmongUs.GameOptions;
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem.Collections.Generic;
|
||||
using MiraAPI.Roles;
|
||||
|
||||
namespace MiraAPI.Patches.Roles;
|
||||
@@ -14,7 +14,7 @@ public static class LogicRoleSelectionHnsPatch
|
||||
[HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))]
|
||||
public static bool AssignRolesForTeam(
|
||||
LogicRoleSelectionHnS __instance,
|
||||
List<NetworkedPlayerInfo> players,
|
||||
Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players,
|
||||
IGameOptions opts,
|
||||
RoleTeamTypes team,
|
||||
int teamMax,
|
||||
@@ -58,7 +58,7 @@ public static class LogicRoleSelectionHnsPatch
|
||||
|
||||
case RoleTeamTypes.Impostor:
|
||||
Info($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
|
||||
var newImpostors = new List<NetworkedPlayerInfo>();
|
||||
var newImpostors = new Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo>();
|
||||
|
||||
// Specified Seeker
|
||||
if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
|
||||
@@ -113,7 +113,7 @@ public static class LogicRoleSelectionHnsPatch
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void AssignRolesFromList(List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
|
||||
public static void AssignRolesFromList(Il2CppSystem.Collections.Generic.List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
|
||||
{
|
||||
while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax)
|
||||
{
|
||||
@@ -128,7 +128,7 @@ public static class LogicRoleSelectionHnsPatch
|
||||
}
|
||||
|
||||
private static void AddGuaranteedRoles(
|
||||
System.Collections.Generic.IEnumerable<RoleBehaviour> guaranteedRoles,
|
||||
IEnumerable<RoleBehaviour> guaranteedRoles,
|
||||
IGameOptions opts,
|
||||
List<RoleTypes> list)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ public static class LogicRoleSelectionHnsPatch
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPotentialRoles(System.Collections.Generic.IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
|
||||
private static void AddPotentialRoles(IEnumerable<RoleBehaviour> source, IGameOptions opts, List<RoleTypes> list)
|
||||
{
|
||||
var roleOptions = opts.RoleOptions;
|
||||
var potentialRoles = source.Where(x => !x.IsDead).Select(role => new RoleManager.RoleAssignmentData(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using HarmonyLib;
|
||||
using Hazel;
|
||||
using Il2CppSystem.Text;
|
||||
|
||||
namespace MiraAPI.Patches.Stubs;
|
||||
|
||||
@@ -40,14 +39,14 @@ public static class RoleBehaviourStubs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub method for <see cref="RoleBehaviour.AppendTaskHint(StringBuilder)"/>.
|
||||
/// Stub method for <see cref="RoleBehaviour.AppendTaskHint(Il2CppSystem.Text.StringBuilder)"/>.
|
||||
/// </summary>
|
||||
/// <param name="instance">The <see cref="RoleBehaviour"/> object.</param>
|
||||
/// <param name="taskStringBuilder">The <see cref="StringBuilder"/> to append the task hint to.</param>
|
||||
/// <param name="taskStringBuilder">The <see cref="Il2CppSystem.Text.StringBuilder"/> to append the task hint to.</param>
|
||||
[HarmonyReversePatch]
|
||||
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(StringBuilder))]
|
||||
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint), typeof(Il2CppSystem.Text.StringBuilder))]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder)
|
||||
public static void AppendTaskHint(RoleBehaviour instance, Il2CppSystem.Text.StringBuilder taskStringBuilder)
|
||||
{
|
||||
// nothing needed
|
||||
}
|
||||
|
||||
@@ -313,9 +313,12 @@ public sealed class MiraPluginManager
|
||||
|
||||
foreach (var property in type.GetProperties())
|
||||
{
|
||||
if (property.GetCustomAttribute<MiraIgnoreAttribute>() != null)
|
||||
continue;
|
||||
|
||||
if (property.GetMethod?.IsStatic == true)
|
||||
{
|
||||
Error($"Option property {property.Name} in {type.Name} must not be static.");
|
||||
Error($"Non-ignored option property {property.Name} in {type.Name} must not be static.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -325,6 +328,12 @@ public sealed class MiraPluginManager
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
|
||||
{
|
||||
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
var attribute = property.GetCustomAttribute<ModdedOptionAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
@@ -332,11 +341,6 @@ public sealed class MiraPluginManager
|
||||
continue;
|
||||
}
|
||||
|
||||
if (property.PropertyType.IsAssignableTo(typeof(IModdedOptionList)))
|
||||
{
|
||||
ModdedOptionsManager.RegisterPropertyOptionList(type, property, pluginInfo);
|
||||
}
|
||||
|
||||
var listAttr = property.GetCustomAttribute<ModdedOptionListAttribute>();
|
||||
if (listAttr != null)
|
||||
{
|
||||
@@ -404,7 +408,7 @@ public sealed class MiraPluginManager
|
||||
|
||||
foreach (var property in type.GetProperties())
|
||||
{
|
||||
if (property.PropertyType != typeof(CustomColor))
|
||||
if (!property.PropertyType.IsAssignableTo(typeof(CustomColor)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -416,6 +420,9 @@ public sealed class MiraPluginManager
|
||||
}
|
||||
|
||||
PaletteManager.CustomColors.Add(color);
|
||||
|
||||
if (color is VariableColor)
|
||||
PaletteManager.VariableColorNames.Add(color.Name);
|
||||
}
|
||||
|
||||
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
|
||||
|
||||
@@ -10,9 +10,6 @@ namespace MiraAPI.Presets;
|
||||
/// <summary>
|
||||
/// Represents a preset of game options that can be applied to the game.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
|
||||
/// </remarks>
|
||||
/// <param name="name">The name of the preset.</param>
|
||||
/// <param name="plugin">The plugin associated with the preset.</param>
|
||||
/// <param name="presetConfig">The configuration file for the preset.</param>
|
||||
@@ -70,7 +67,12 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
|
||||
/// <param name="baseOption">The option to reset.</param>
|
||||
public void ResetOption(OptionBehaviour baseOption)
|
||||
{
|
||||
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
|
||||
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
|
||||
if (selectedOpt == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedOpt.LoadFromPreset(PresetConfig);
|
||||
/*ModdedOptionsManager.SyncAllOptions();
|
||||
|
||||
@@ -84,7 +86,7 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
|
||||
/// <returns>The value of whether the option is in the preset.</returns>
|
||||
public bool IsOptionInPreset(OptionBehaviour baseOption)
|
||||
{
|
||||
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
|
||||
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
|
||||
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
|
||||
return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,14 @@ using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// TODO: for the love of fucking god please add a namespace on the asset bundle AND here - Atony.
|
||||
/*
|
||||
namespace MiraAPI.Presets;
|
||||
*/
|
||||
|
||||
[RegisterInIl2Cpp]
|
||||
[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity convention.")]
|
||||
[SuppressMessage("Design", "CA1050:Declare types in namespaces", Justification = "Unity script.")]
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member (Justification: Unity fields; ignore.)
|
||||
public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
|
||||
{
|
||||
|
||||
@@ -2,21 +2,29 @@
|
||||
<resources>
|
||||
<string name="MiraApi">Mira API</string>
|
||||
<string name="MiraApi.VisualsUi">Visuals/UI</string>
|
||||
<string name="MiraApi.MatchInfoGuide">Match Info Guide</string>
|
||||
<string name="MiraApi.Accessibility">Accessibility</string>
|
||||
<string name="MiraApi.MiraApiBrand">MiraAPI</string>
|
||||
<string name="MiraApi.ShowModifiersHudOnLeftSide">Show Modifiers HUD on Left Side</string>
|
||||
<string name="MiraApi.ShowKeybindsOnButtons">Show Keybinds on Buttons</string>
|
||||
<string name="MiraApi.ButtonScaleFactor">Button Scale Factor</string>
|
||||
<string name="MiraApi.TopRightButtonScaleFactor">Top-Right UI Scale Factor</string>
|
||||
<string name="MiraApi.MatchInfoGuideScaleFactor">Match Info Guide Scale Factor</string>
|
||||
<string name="MiraApi.WikiTabPosition">Tab Buttons Grid Placement</string>
|
||||
<string name="MiraApi.WikiTabPosition.LeftSide">Left-Side</string>
|
||||
<string name="MiraApi.WikiTabPosition.RightSide">Right-Side</string>
|
||||
<string name="MiraApi.WikiTabPosition.Below">Below UI</string>
|
||||
<string name="MiraApi.MaxFps">Max FPS</string>
|
||||
<string name="MiraApi.WikiOnBottomRow">Display Wiki on Bottom Row</string>
|
||||
<string name="MiraApi.WikiOnBottomRow">Display Button on Bottom Row</string>
|
||||
<string name="MiraApi.EnableSabotageFlashes">Enable Sabotage Flashes</string>
|
||||
<string name="MiraApi.EnableSabotageBlare">Enable Sabotage Blare</string>
|
||||
<string name="MiraApi.EnableVariableColors">Enable Variable Color Changes</string>
|
||||
<string name="MiraApi.ReturnToRoleSettings">RETURN TO ROLE SETTINGS</string>
|
||||
<string name="MiraApi.CustomCategory1">Custom Category 1</string>
|
||||
<string name="MiraApi.CustomCategory2">Custom Category 2</string>
|
||||
<string name="MiraApi.ModifierMenuDescription">Configure modifiers and their settings here!</string>
|
||||
<string name="MiraApi.CustomOptionMenuDescription">Apply game settings for this mod!</string>
|
||||
<string name="MiraApi.Gamemode">Gamemode</string>
|
||||
<string name="MiraApi.Gamemode.Classic">Classic</string>
|
||||
<string name="MiraApi.Gamemode.Classic.Description">The classic Among Us experience!
|
||||
Find the Impostors or deceive the Crew.</string>
|
||||
@@ -43,4 +51,32 @@ Outlive the timer or eliminate the crew.</string>
|
||||
<string name="MiraApi.ModifierDisplay.Page">Page</string>
|
||||
<string name="MiraApi.Page">Page</string>
|
||||
<string name="MiraApi.Main">Main</string>
|
||||
<string name="MiraApi.AbilityType.Basic">Ability</string>
|
||||
<string name="MiraApi.AbilityType.Indirect">Indirect Ability</string>
|
||||
<string name="MiraApi.AbilityType.Interaction">Interaction Ability</string>
|
||||
<string name="MiraApi.AbilityType.Radius">Radius Ability</string>
|
||||
<string name="MiraApi.AbilityType.Menu">Menu Ability</string>
|
||||
<string name="MiraApi.AbilityType.Meeting">Meeting Ability</string>
|
||||
<string name="MiraApi.AbilityType.SelectiveMeeting">Selective Meeting Ability</string>
|
||||
<string name="MiraApi.AbilityType.MeetingMenu">Meeting Menu Ability</string>
|
||||
<string name="Options">Options</string>
|
||||
<string name="MiraApi.Wiki.PlayersTab">Player List</string>
|
||||
<string name="MiraApi.Wiki.SettingsTab">Game Settings</string>
|
||||
<string name="MiraApi.Wiki.SearchPlaceholder">Search...</string>
|
||||
<string name="MiraApi.Wiki.FilterLabel">Filter</string>
|
||||
<string name="MiraApi.Wiki.GroupingLabel">Grouping</string>
|
||||
<string name="MiraApi.Wiki.OrderLabel">Order</string>
|
||||
<string name="MiraApi.Wiki.VanillaSettings">Vanilla</string>
|
||||
<string name="MiraApi.Wiki.SearchResults">Search Results</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>
|
||||
<string name="Inactive">Inactive</string>
|
||||
<string name="Activated">Activated</string>
|
||||
<string name="Deactivated">Deactivated</string>
|
||||
</resources>
|
||||
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user