mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-21 13:37:54 +00:00
Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
575f7a78a1 | ||
|
|
bf7edb30ab | ||
|
|
b4527014b7 | ||
|
|
277391ef1c | ||
|
|
6b3038f397 | ||
|
|
4e1db039a0 | ||
|
|
62bee24789 | ||
|
|
2156c80467 | ||
|
|
0ff60071c2 | ||
|
|
de2f3f0d95 | ||
|
|
05f132c506 | ||
|
|
ab54c44434 | ||
|
|
bf652b68c3 | ||
|
|
00377c7926 | ||
|
|
55555604f3 | ||
|
|
de77a9fa92 | ||
|
|
eba2a037d3 | ||
|
|
bc7f772de9 | ||
|
|
f43191c4f2 | ||
|
|
56d4e399d9 | ||
|
|
0d41ebbc13 | ||
|
|
9388ff487a | ||
|
|
720491e5cf | ||
|
|
9884df356e | ||
|
|
7ec4addacc | ||
|
|
bc9bc0ce24 | ||
|
|
aff1c311fa | ||
|
|
0d2a9d9700 | ||
|
|
669c541279 | ||
|
|
2e5f9639b3 | ||
|
|
e6d1f440c8 | ||
|
|
2ac8bdc240 | ||
|
|
d5d99001ea | ||
|
|
5628b79814 | ||
|
|
72a44f65f1 | ||
|
|
db6a336a63 | ||
|
|
90b23ec2ca | ||
|
|
20e927208c | ||
|
|
13ef3bdd49 | ||
|
|
b3ff6755bb | ||
|
|
67604600cd | ||
|
|
6e35de6185 | ||
|
|
a26ec51c7d | ||
|
|
3f6ae4cc41 | ||
|
|
15a2cdcd5b | ||
|
|
cf0bd6ec38 | ||
|
|
130236e899 | ||
|
|
3b7b247ac0 | ||
|
|
52de9a22de | ||
|
|
372a9e3aa9 | ||
|
|
10a9dcfa34 | ||
|
|
d46fdb0185 | ||
|
|
a361afbcbf | ||
|
|
24cdac0243 | ||
|
|
5fe75843ad | ||
|
|
a0436dcbba | ||
|
|
35be381a77 | ||
|
|
1298beb517 | ||
|
|
d41c19a176 | ||
|
|
476f4400ad | ||
|
|
2b46d90570 | ||
|
|
ae4aaf7af9 | ||
|
|
757b9436ca | ||
|
|
7ea10afffb | ||
|
|
0ab80c42f9 | ||
|
|
bc144568f4 | ||
|
|
07f402e659 | ||
|
|
1a6aed539a | ||
|
|
023cdaa470 | ||
|
|
98dd5ca056 | ||
|
|
b94472ee07 | ||
|
|
f8056c29da | ||
|
|
8d4f2baf1e | ||
|
|
11ba3dae64 | ||
|
|
86145f94f4 | ||
|
|
e0da386567 | ||
|
|
6f1da2de9f | ||
|
|
b85358e304 | ||
|
|
2c901bb9b9 | ||
|
|
68f40a96b7 | ||
|
|
a96ffc42f2 | ||
|
|
0907b14893 | ||
|
|
32a44d646d | ||
|
|
8f590b94a8 | ||
|
|
2e769d79f2 | ||
|
|
41305ad0e2 | ||
|
|
2418e17af6 | ||
|
|
6fb0afe14e | ||
|
|
ab0063dd99 | ||
|
|
a35bf60870 | ||
|
|
692b9d7180 | ||
|
|
b99efaa420 | ||
|
|
2ecfa267d6 | ||
|
|
9afe821b0f | ||
|
|
a8d076656d | ||
|
|
429b64fc03 | ||
|
|
bf5dadc6a0 | ||
|
|
b2137230da | ||
|
|
9e8c05f21d | ||
|
|
2f56c95b39 | ||
|
|
38f72c42a3 | ||
|
|
5b608f5b3a | ||
|
|
2248726450 | ||
|
|
c60537cbd1 | ||
|
|
f5b4bdd1d4 | ||
|
|
cd6c4f5390 | ||
|
|
5cc29d903a |
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Reactor" Version="2.3.1" />
|
||||
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.697" Private="false" ExcludeAssets="runtime;native" />
|
||||
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.3.25" PrivateAssets="all" />
|
||||
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
|
||||
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.4.15" 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" />
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using BepInEx;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.PluginLoading;
|
||||
using Reactor;
|
||||
using Reactor.Networking;
|
||||
using Reactor.Networking.Attributes;
|
||||
|
||||
namespace MiraAPI.Example.Extension;
|
||||
|
||||
[BepInAutoPlugin("mira.example.extension", "MiraExampleExtensionMod")]
|
||||
[BepInProcess("Among Us.exe")]
|
||||
[BepInDependency(ReactorPlugin.Id)]
|
||||
[BepInDependency(MiraApiPlugin.Id)]
|
||||
[BepInDependency(ExamplePlugin.Id)]
|
||||
[ReactorModFlags(ModFlags.RequireOnAllClients)]
|
||||
public partial class ExampleExtensionPlugin : BasePlugin, IMiraExtension
|
||||
{
|
||||
public Harmony Harmony { get; } = new(Id);
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
Harmony.PatchAll();
|
||||
}
|
||||
|
||||
public Type GetBasePluginType()
|
||||
{
|
||||
return typeof(ExamplePlugin);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Version>1.0.0</Version>
|
||||
<Description>Example extension for Mira API</Description>
|
||||
<Authors>Angxl, XtraCube</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
|
||||
<ProjectReference Include="..\MiraAPI.Example\MiraAPI.Example.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\AmongUs.props" />
|
||||
</Project>
|
||||
@@ -1,16 +0,0 @@
|
||||
using MiraAPI.Example.Extension.Options;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Modifiers.Types;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Modifiers;
|
||||
|
||||
public class ExtensionModifier : GameModifier
|
||||
{
|
||||
public override string ModifierName => "Extension Modifier";
|
||||
|
||||
public override string GetDescription() => "This is from the extension mod!";
|
||||
|
||||
public override int GetAssignmentChance() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierChance;
|
||||
|
||||
public override int GetAmountPerGame() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierLimit;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using MiraAPI.Example.Roles;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.Attributes;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Options;
|
||||
|
||||
public class ExtendedFreezerOptions : AbstractOptionGroup<FreezerRole>
|
||||
{
|
||||
public override string GroupName => "Extended Freezer Role Options";
|
||||
|
||||
[ModdedNumberOption("Freeze Uses", 1, 5)]
|
||||
public float FreezeUses { get; set; } = 1;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using MiraAPI.Example.Extension.Modifiers;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.Attributes;
|
||||
using MiraAPI.Utilities;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Options;
|
||||
|
||||
public class ExtensionModifierOptions : AbstractOptionGroup<ExtensionModifier>
|
||||
{
|
||||
public override string GroupName => "Extension Modifier Options";
|
||||
|
||||
[ModdedNumberOption("Modifier Chance", 0, 100, 10, MiraNumberSuffixes.Percent)]
|
||||
public float ModifierChance { get; set; } = 100f;
|
||||
|
||||
[ModdedNumberOption("Modifier Limit", 0, 10)]
|
||||
public float ModifierLimit { get; set; } = 0;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Example.Buttons.Freezer;
|
||||
using MiraAPI.Example.Extension.Options;
|
||||
using MiraAPI.GameOptions;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Patches;
|
||||
|
||||
[HarmonyPatch(typeof(FreezeButton), nameof(FreezeButton.MaxUses), MethodType.Getter)]
|
||||
public static class FreezeButtonPatch
|
||||
{
|
||||
public static void Postfix(ref int __result)
|
||||
{
|
||||
__result = (int)OptionGroupSingleton<ExtendedFreezerOptions>.Instance.FreezeUses;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class FreezeButton : CustomActionButton<PlayerControl>
|
||||
|
||||
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
|
||||
|
||||
public override int MaxUses => 1;
|
||||
public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
|
||||
|
||||
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
|
||||
|
||||
|
||||
+3
-6
@@ -1,20 +1,17 @@
|
||||
using MiraAPI.Example.Extension.GameOver;
|
||||
using MiraAPI.Example.Extension.Roles;
|
||||
using MiraAPI.Example.GameOver;
|
||||
using MiraAPI.Example.Roles;
|
||||
using MiraAPI.GameEnd;
|
||||
using MiraAPI.Hud;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Buttons;
|
||||
namespace MiraAPI.Example.Buttons;
|
||||
|
||||
public class NeutralKillerButton : CustomActionButton
|
||||
{
|
||||
public override string Name => "Win Game";
|
||||
|
||||
public override float Cooldown => 0f;
|
||||
|
||||
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
|
||||
|
||||
protected override void OnClick()
|
||||
{
|
||||
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
|
||||
@@ -6,7 +6,10 @@ namespace MiraAPI.Example;
|
||||
[RegisterCustomColors]
|
||||
public static class ExampleColors
|
||||
{
|
||||
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f));
|
||||
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f))
|
||||
{
|
||||
ColorBrightness = CustomColorBrightness.Lighter,
|
||||
};
|
||||
|
||||
public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f));
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public static class ExampleEventHandlers
|
||||
[RegisterEvent(15)]
|
||||
public static void HandleVoteEvent(HandleVoteEvent @event)
|
||||
{
|
||||
if (@event.VoteData.Owner.Data.Role is not FreezerRole) return;
|
||||
if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return;
|
||||
|
||||
@event.VoteData.SetRemainingVotes(0);
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
using MiraAPI.Example.Extension.Roles;
|
||||
using MiraAPI.Example.Roles;
|
||||
using MiraAPI.GameEnd;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example.Extension.GameOver;
|
||||
namespace MiraAPI.Example.GameOver;
|
||||
|
||||
public class NeutralKillerGameOver : CustomGameOver
|
||||
{
|
||||
@@ -10,6 +10,8 @@ public class ExampleOptions2 : AbstractOptionGroup
|
||||
|
||||
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
|
||||
|
||||
public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false);
|
||||
public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever");
|
||||
public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false);
|
||||
|
||||
public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false)
|
||||
|
||||
@@ -11,4 +11,7 @@ public class FreezerRoleSettings : AbstractOptionGroup<FreezerRole>
|
||||
|
||||
[ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)]
|
||||
public float FreezeDuration { get; set; } = 5;
|
||||
|
||||
[ModdedNumberOption("Freeze Uses", 1, 5)]
|
||||
public float FreezeUses { get; set; } = 1;
|
||||
}
|
||||
+3
-7
@@ -1,20 +1,16 @@
|
||||
using MiraAPI.Example.Extension.GameOver;
|
||||
using MiraAPI.Example.GameOver;
|
||||
using MiraAPI.GameEnd;
|
||||
using MiraAPI.Roles;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example.Extension.Roles;
|
||||
namespace MiraAPI.Example.Roles;
|
||||
|
||||
public class NeutralKillerRole : ImpostorRole, ICustomRole
|
||||
{
|
||||
public string RoleName => "Outcast Killer";
|
||||
|
||||
public string RoleDescription => "Outcast who can kill.";
|
||||
|
||||
public string RoleLongDescription => this.RoleDescription;
|
||||
|
||||
public string RoleLongDescription => RoleDescription;
|
||||
public Color RoleColor => Color.magenta;
|
||||
|
||||
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
|
||||
|
||||
public CustomRoleConfiguration Configuration => new(this)
|
||||
@@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI.
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example.Extension", "MiraAPI.Example.Extension\MiraAPI.Example.Extension.csproj", "{526FF017-80E1-4094-804D-741AF35AA226}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -23,10 +21,6 @@ Global
|
||||
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -13,19 +13,24 @@ namespace MiraAPI.Colors;
|
||||
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the main color.
|
||||
/// Gets or sets the main color.
|
||||
/// </summary>
|
||||
public Color32 MainColor { get; } = mainColor;
|
||||
public Color32 MainColor { get; set; } = mainColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shadow color.
|
||||
/// Gets or sets the shadow color.
|
||||
/// </summary>
|
||||
public Color32 ShadowColor { get; } = shadowColor;
|
||||
public Color32 ShadowColor { get; set; } = shadowColor;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the color.
|
||||
/// Gets or sets the name of the color.
|
||||
/// </summary>
|
||||
public StringNames Name { get; } = name;
|
||||
public StringNames Name { get; set; } = name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the color is lighter or darker. Can be used to help with colorblind stuff.
|
||||
/// </summary>
|
||||
public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
|
||||
@@ -42,3 +47,19 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to help with colorblind stuff.
|
||||
/// </summary>
|
||||
public enum CustomColorBrightness
|
||||
{
|
||||
/// <summary>
|
||||
/// A lighter color.
|
||||
/// </summary>
|
||||
Lighter,
|
||||
|
||||
/// <summary>
|
||||
/// A darker color.
|
||||
/// </summary>
|
||||
Darker,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace MiraAPI.Events.Vanilla.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked before RpcEndGame is called, allowing cancellation of the game end entirely.
|
||||
/// </summary>
|
||||
public class BeforeGameEndEvent : MiraCancelableEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the reason for the game end.
|
||||
/// </summary>
|
||||
public GameOverReason Reason { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BeforeGameEndEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for the game end.</param>
|
||||
public BeforeGameEndEvent(GameOverReason reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MiraAPI.Game;
|
||||
|
||||
public enum PlayerContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[*.cs]
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
@@ -46,6 +46,11 @@ public interface IModdedOption
|
||||
/// </summary>
|
||||
Func<bool> Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the option should be included with presets.
|
||||
/// </summary>
|
||||
bool IncludeInPreset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ConfigDefinition for the option, used for BepInEx configuration.
|
||||
/// </summary>
|
||||
@@ -57,9 +62,10 @@ public interface IModdedOption
|
||||
/// <param name="toggleOpt">The ToggleOption template.</param>
|
||||
/// <param name="numberOpt">The NumberOption template.</param>
|
||||
/// <param name="stringOpt">The StringOption template.</param>
|
||||
/// <param name="playerOpt">The PlayerOption template.</param>
|
||||
/// <param name="container">>The Transform container for the option.</param>
|
||||
/// <returns>>The created OptionBehaviour object.</returns>
|
||||
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
|
||||
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value as a float.
|
||||
@@ -78,4 +84,23 @@ public interface IModdedOption
|
||||
/// </summary>
|
||||
/// <param name="data">>The byte array representing the network data.</param>
|
||||
void HandleNetData(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the option to a preset configuration file.
|
||||
/// </summary>
|
||||
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
|
||||
/// <param name="saveDefault">Indicates whether to save the default value instead of the current value.</param>
|
||||
void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
|
||||
|
||||
/// <summary>
|
||||
/// Binds the option to a configuration file.
|
||||
/// </summary>
|
||||
/// <param name="config">The ConfigFile to bind the option to.</param>
|
||||
void Bind(ConfigFile config);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the option from a preset configuration file, applying the values to the option's configuration.
|
||||
/// </summary>
|
||||
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
|
||||
void LoadFromPreset(ConfigFile presetConfig);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BepInEx.Configuration;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.GameOptions.Attributes;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Utilities;
|
||||
@@ -21,6 +23,7 @@ public static class ModdedOptionsManager
|
||||
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = [];
|
||||
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
|
||||
|
||||
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
|
||||
internal static readonly Dictionary<uint, IModdedOption> ModdedOptions = [];
|
||||
internal static readonly List<AbstractOptionGroup> Groups = [];
|
||||
|
||||
@@ -42,7 +45,7 @@ public static class ModdedOptionsManager
|
||||
|
||||
Groups.Add(group);
|
||||
TypeToGroup.Add(type, group);
|
||||
pluginInfo.OptionGroups.Add(group);
|
||||
pluginInfo.InternalOptionGroups.Add(group);
|
||||
|
||||
typeof(OptionGroupSingleton<>).MakeGenericType(type)
|
||||
#pragma warning disable S3011
|
||||
@@ -121,7 +124,7 @@ public static class ModdedOptionsManager
|
||||
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
|
||||
|
||||
option.ParentMod = pluginInfo.MiraPlugin;
|
||||
pluginInfo.Options.Add(option);
|
||||
pluginInfo.InternalOptions.Add(option);
|
||||
ModdedOptions.Add(option.Id, option);
|
||||
group.Options.Add(option);
|
||||
}
|
||||
@@ -142,7 +145,7 @@ public static class ModdedOptionsManager
|
||||
// we dont know how other plugins handle their configs
|
||||
// this way, all the options are saved at once, instead of one by one
|
||||
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
|
||||
plugin.PluginConfig.SaveOnConfigSet = false;
|
||||
@@ -158,7 +161,7 @@ public static class ModdedOptionsManager
|
||||
option.HandleNetData(netData.Data);
|
||||
}
|
||||
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
plugin.PluginConfig.Save();
|
||||
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
|
||||
@@ -175,8 +178,10 @@ public static class ModdedOptionsManager
|
||||
/// </summary>
|
||||
/// <param name="__originalMethod">The original setter method.</param>
|
||||
/// <param name="value">The new object value.</param>
|
||||
// ReSharper disable InconsistentNaming
|
||||
#pragma warning disable CA1707
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
|
||||
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
|
||||
#pragma warning restore CA1707
|
||||
{
|
||||
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
|
||||
attribute.SetValue(value);
|
||||
@@ -188,8 +193,10 @@ public static class ModdedOptionsManager
|
||||
/// <param name="__originalMethod">The original getter method.</param>
|
||||
/// <param name="__result">The result of the property getter.</param>
|
||||
/// <returns>False so the original getter gets skipped.</returns>
|
||||
#pragma warning disable CA1707
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
|
||||
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
|
||||
// ReSharper restore InconsistentNaming
|
||||
#pragma warning restore CA1707
|
||||
{
|
||||
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
|
||||
__result = attribute.GetValue();
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
/// <summary>
|
||||
/// Gets the string values of the enum.
|
||||
/// </summary>
|
||||
public string[]? Values { get; }
|
||||
public string[] Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedEnumOption"/> class.
|
||||
@@ -27,7 +27,8 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
/// <param name="defaultValue">The default value as an int.</param>
|
||||
/// <param name="enumType">The Enum type.</param>
|
||||
/// <param name="values">An option list of string values to use in place of the enum name.</param>
|
||||
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue)
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Values = values ?? Enum.GetNames(enumType);
|
||||
Data = ScriptableObject.CreateInstance<StringGameSetting>();
|
||||
@@ -37,13 +38,13 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
data.Type = global::OptionTypes.String;
|
||||
data.Values = values is null ?
|
||||
Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray()
|
||||
: values.Select(CustomStringName.CreateAndRegister).ToArray();
|
||||
: [.. values.Select(CustomStringName.CreateAndRegister)];
|
||||
|
||||
data.Index = Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
|
||||
{
|
||||
var stringOption = Object.Instantiate(stringOpt, container);
|
||||
|
||||
@@ -87,7 +88,7 @@ public class ModdedEnumOption : ModdedOption<int>
|
||||
/// <inheritdoc />
|
||||
protected override void OnValueChanged(int newValue)
|
||||
{
|
||||
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false);
|
||||
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
|
||||
if (!OptionBehaviour)
|
||||
{
|
||||
return;
|
||||
@@ -109,7 +110,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
|
||||
/// <summary>
|
||||
/// Gets the string values of the enum.
|
||||
/// </summary>
|
||||
public string[]? Values { get; }
|
||||
public string[] Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class.
|
||||
@@ -117,7 +118,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
|
||||
/// <param name="title">The title of the option.</param>
|
||||
/// <param name="defaultValue">The default value as an int.</param>
|
||||
/// <param name="values">An option list of string values to use in place of the enum name.</param>
|
||||
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue)
|
||||
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Values = values ?? Enum.GetNames(typeof(T));
|
||||
Data = ScriptableObject.CreateInstance<StringGameSetting>();
|
||||
@@ -127,13 +129,13 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
|
||||
data.Type = global::OptionTypes.String;
|
||||
data.Values = values is null ?
|
||||
Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray()
|
||||
: values.Select(CustomStringName.CreateAndRegister).ToArray();
|
||||
: [.. values.Select(CustomStringName.CreateAndRegister)];
|
||||
|
||||
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
|
||||
{
|
||||
var stringOption = Object.Instantiate(stringOpt, container);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
/// <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="includeInPreset">Whether to include this option in the preset or not.</param>
|
||||
public ModdedNumberOption(
|
||||
string title,
|
||||
float defaultValue,
|
||||
@@ -56,7 +57,8 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
float increment,
|
||||
MiraNumberSuffixes suffixType,
|
||||
string? formatString = null,
|
||||
bool zeroInfinity = false) : base(title, defaultValue)
|
||||
bool zeroInfinity = false,
|
||||
bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
@@ -91,6 +93,7 @@ public class ModdedNumberOption : ModdedOption<float>
|
||||
ToggleOption toggleOpt,
|
||||
NumberOption numberOpt,
|
||||
StringOption stringOpt,
|
||||
PlayerOption playerOpt,
|
||||
Transform container)
|
||||
{
|
||||
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
|
||||
|
||||
@@ -4,6 +4,7 @@ using MiraAPI.Networking;
|
||||
using MiraAPI.PluginLoading;
|
||||
using Reactor.Localization.Utilities;
|
||||
using Reactor.Networking.Rpc;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.GameOptions.OptionTypes;
|
||||
@@ -16,29 +17,19 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
{
|
||||
private IMiraPlugin? _parentMod;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique identifier of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public uint Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the StringName object of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public StringNames StringName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BaseGameSetting data of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public BaseGameSetting Data { get; protected set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent mod of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public IMiraPlugin? ParentMod
|
||||
{
|
||||
get => _parentMod;
|
||||
@@ -46,6 +37,7 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
{
|
||||
if (_parentMod != null || value == null) return;
|
||||
_parentMod = value;
|
||||
|
||||
var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
|
||||
Value = entry.Value;
|
||||
}
|
||||
@@ -66,19 +58,16 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// </summary>
|
||||
public Action<T>? ChangedEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the visibility of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public Func<bool> Visible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the option behaviour of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool IncludeInPreset { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public OptionBehaviour? OptionBehaviour { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the config definition of the option.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public ConfigDefinition? ConfigDefinition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -86,7 +75,8 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
protected ModdedOption(string title, T defaultValue)
|
||||
/// <param name="includeInPreset">Whether to include the option in the preset.</param>
|
||||
protected ModdedOption(string title, T defaultValue, bool includeInPreset = true)
|
||||
{
|
||||
Id = ModdedOptionsManager.NextId;
|
||||
Title = title;
|
||||
@@ -94,6 +84,7 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
Value = defaultValue;
|
||||
StringName = CustomStringName.CreateAndRegister(Title);
|
||||
Visible = () => true;
|
||||
IncludeInPreset = includeInPreset;
|
||||
}
|
||||
|
||||
internal void ValueChanged(OptionBehaviour optionBehaviour)
|
||||
@@ -105,7 +96,8 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// Sets the value of the option.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new value.</param>
|
||||
public void SetValue(T newValue)
|
||||
/// <param name="sendRpc">Whether to send the value to other players.</param>
|
||||
public void SetValue(T newValue, bool sendRpc = true)
|
||||
{
|
||||
var oldVal = Value;
|
||||
Value = newValue;
|
||||
@@ -115,7 +107,7 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
ChangedEvent?.Invoke(Value);
|
||||
}
|
||||
|
||||
if (AmongUsClient.Instance.AmHost)
|
||||
if (sendRpc && AmongUsClient.Instance.AmHost)
|
||||
{
|
||||
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
|
||||
{
|
||||
@@ -128,22 +120,44 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
OnValueChanged(newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the float data of the option.
|
||||
/// </summary>
|
||||
/// <returns>A float object representing the option's value.</returns>
|
||||
/// <inheritdoc />
|
||||
public void SaveToPreset(ConfigFile presetConfig, bool saveDefault=false)
|
||||
{
|
||||
if (ConfigDefinition is null)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
|
||||
return;
|
||||
}
|
||||
Bind(presetConfig);
|
||||
presetConfig[ConfigDefinition].BoxedValue = saveDefault ? DefaultValue : Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind(ConfigFile config)
|
||||
{
|
||||
config.Bind(ConfigDefinition, DefaultValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadFromPreset(ConfigFile presetConfig)
|
||||
{
|
||||
if (presetConfig.TryGetEntry(ConfigDefinition, out ConfigEntry<T> entry))
|
||||
{
|
||||
SetValue(entry.Value, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract float GetFloatData();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the net data of the option.
|
||||
/// </summary>
|
||||
/// <returns>A NetData object representing this option's data.</returns>
|
||||
/// <inheritdoc />
|
||||
public abstract NetData GetNetData();
|
||||
|
||||
/// <summary>
|
||||
/// Handles incoming net data.
|
||||
/// </summary>
|
||||
/// <param name="data">The NetData's byte array.</param>
|
||||
/// <inheritdoc />
|
||||
public abstract void HandleNetData(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
@@ -159,18 +173,12 @@ public abstract class ModdedOption<T> : IModdedOption
|
||||
/// <returns>The value.</returns>
|
||||
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the option behaviour.
|
||||
/// </summary>
|
||||
/// <param name="toggleOpt">The ToggleOption prefab.</param>
|
||||
/// <param name="numberOpt">The NumberOption prefab.</param>
|
||||
/// <param name="stringOpt">The StringOption prefab.</param>
|
||||
/// <param name="container">The options container.</param>
|
||||
/// <returns>A new OptionBehaviour for this modded option.</returns>
|
||||
/// <inheritdoc />
|
||||
public abstract OptionBehaviour CreateOption(
|
||||
ToggleOption toggleOpt,
|
||||
NumberOption numberOpt,
|
||||
StringOption stringOpt,
|
||||
PlayerOption playerOpt,
|
||||
Transform container);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Il2CppSystem.Collections.Generic;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.GameOptions.OptionTypes;
|
||||
|
||||
/// <summary>
|
||||
/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the Values list.
|
||||
/// </summary>
|
||||
public class ModdedPlayerOption : ModdedOption<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the included players.
|
||||
/// </summary>
|
||||
public List<NetworkedPlayerInfo> Values { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a filter for the players included in the option.
|
||||
/// </summary>
|
||||
public Func<NetworkedPlayerInfo, bool>? PlayerFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the option includes a "None" option. If None is selected, value will return -1.
|
||||
/// </summary>
|
||||
public bool AllowNone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
|
||||
/// </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>
|
||||
public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false)
|
||||
{
|
||||
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
|
||||
var data = (PlayerSelectionGameSetting)Data;
|
||||
|
||||
data.Title = StringName;
|
||||
data.Type = global::OptionTypes.Player;
|
||||
|
||||
Values = new List<NetworkedPlayerInfo>();
|
||||
AllowNone = allowNone;
|
||||
Value = DefaultValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OptionBehaviour CreateOption(
|
||||
ToggleOption toggleOpt,
|
||||
NumberOption numberOpt,
|
||||
StringOption stringOpt,
|
||||
PlayerOption playerOpt,
|
||||
Transform container)
|
||||
{
|
||||
var playerOption = Object.Instantiate(playerOpt, container);
|
||||
|
||||
var filteredList = GetFilteredPlayers();
|
||||
Values = filteredList.ToIl2CppList();
|
||||
|
||||
playerOption.SetUpFromData(Data, 20);
|
||||
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
|
||||
|
||||
playerOption.Title = StringName;
|
||||
playerOption.TitleText.text = TranslationController.Instance.GetString(StringName);
|
||||
playerOption.Values = Values;
|
||||
playerOption.Value = Value;
|
||||
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
|
||||
|
||||
OptionBehaviour = playerOption;
|
||||
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
|
||||
|
||||
playerOption.SetValueText();
|
||||
playerOption.AdjustButtonsActiveState();
|
||||
|
||||
return playerOption;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a player ran through the filter.
|
||||
/// </summary>
|
||||
/// <returns>A list of filtered players.</returns>
|
||||
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
|
||||
{
|
||||
return GameData.Instance.AllPlayers.ToArray()
|
||||
.Where(x => PlayerFilter?.Invoke(x) ?? true)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a player from the value. If AllowNone is enabled and none is enabled this will return null.
|
||||
/// </summary>
|
||||
/// <returns>A player or null if chosen none.</returns>
|
||||
public NetworkedPlayerInfo? GetPlayerValue()
|
||||
{
|
||||
return Value < 0 ? null : Values[Value];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override float GetFloatData()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override NetData GetNetData()
|
||||
{
|
||||
return new NetData(Id, BitConverter.GetBytes(Value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void HandleNetData(byte[] data)
|
||||
{
|
||||
SetValue(BitConverter.ToInt32(data));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
|
||||
{
|
||||
return optionBehaviour.GetInt();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnValueChanged(int newValue)
|
||||
{
|
||||
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
|
||||
if (!OptionBehaviour)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (OptionBehaviour is PlayerOption opt)
|
||||
{
|
||||
opt.Value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@ public class ModdedToggleOption : ModdedOption<bool>
|
||||
/// </summary>
|
||||
/// <param name="title">The option title.</param>
|
||||
/// <param name="defaultValue">The default value.</param>
|
||||
public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue)
|
||||
/// <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)
|
||||
{
|
||||
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
|
||||
|
||||
@@ -25,7 +26,7 @@ public class ModdedToggleOption : ModdedOption<bool>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
|
||||
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
|
||||
{
|
||||
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Globalization;
|
||||
using MiraAPI.Events;
|
||||
using MiraAPI.Events.Mira;
|
||||
using MiraAPI.Patches;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@@ -36,6 +37,11 @@ public abstract class CustomActionButton
|
||||
/// </summary>
|
||||
public abstract LoadableAsset<Sprite> Sprite { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the format string for the cooldown timer.
|
||||
/// </summary>
|
||||
public virtual string CooldownTimerFormatString => "0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
|
||||
/// </summary>
|
||||
@@ -178,7 +184,10 @@ public abstract class CustomActionButton
|
||||
|
||||
this.Location = location;
|
||||
|
||||
if (!moveButton) return;
|
||||
if (!moveButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
|
||||
{
|
||||
@@ -446,12 +455,12 @@ public abstract class CustomActionButton
|
||||
{
|
||||
Button.SetFillUp(Timer, EffectDuration);
|
||||
|
||||
Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
|
||||
Button.cooldownTimerText.text = Timer.ToString(CooldownTimerFormatString, NumberFormatInfo.InvariantInfo);
|
||||
Button.cooldownTimerText.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Button.SetCoolDown(Timer, Cooldown);
|
||||
Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public static class CustomButtonManager
|
||||
}
|
||||
|
||||
CustomButtons.Add(button);
|
||||
pluginInfo.Buttons.Add(button);
|
||||
pluginInfo.InternalButtons.Add(button);
|
||||
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
|
||||
#pragma warning disable S3011
|
||||
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
|
||||
@@ -40,8 +40,8 @@ public abstract class BaseModifier : IOptionable
|
||||
/// Gets the parent mod of the modifier.
|
||||
/// </summary>
|
||||
public MiraPluginInfo ParentMod => Array.Find(
|
||||
MiraPluginManager.Instance.RegisteredPlugins(),
|
||||
x => x.Modifiers.Exists(y => y.TypeId == TypeId)
|
||||
MiraPluginManager.Instance.RegisteredPlugins,
|
||||
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)
|
||||
) ?? throw new InvalidOperationException("Modifier is not registered.");
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +64,11 @@ public abstract class BaseModifier : IOptionable
|
||||
/// </summary>
|
||||
public virtual bool ShowInFreeplay => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the color that should be used for the modifier within freeplay.
|
||||
/// </summary>
|
||||
public virtual Color FreeplayFileColor => Color.gray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the modifier is unique. If true, the player can only have one instance of this modifier.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MiraAPI.Modifiers;
|
||||
|
||||
public enum ModifierContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public static class ModifierExtensions
|
||||
_ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException(
|
||||
$"Modifier with ID {typeId} is not registered.");
|
||||
|
||||
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args));
|
||||
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,7 +63,7 @@ public static class ModifierExtensions
|
||||
/// </summary>
|
||||
/// <param name="target">The player to remove the modifier from.</param>
|
||||
/// <param name="uniqueId">The unique ID of the modifier.</param>
|
||||
[MethodRpc((uint)MiraRpc.RemoveModifier)]
|
||||
[MethodRpc((uint)MiraRpc.RemoveModifier, SendImmediately = true)]
|
||||
public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId)
|
||||
{
|
||||
target.RemoveModifier(uniqueId);
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Runtime.Serialization;
|
||||
using MiraAPI.Modifiers.Types;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using Random = System.Random;
|
||||
@@ -16,6 +17,18 @@ namespace MiraAPI.Modifiers;
|
||||
/// </summary>
|
||||
public static class ModifierManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether modifiers should be assigned to players.
|
||||
/// </summary>
|
||||
public static bool MiraAssignsModifiers { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of all registered modifiers.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<BaseModifier> Modifiers { get; internal set; } = [];
|
||||
|
||||
internal static List<BaseModifier> InternalModifiers { get; } = [];
|
||||
|
||||
private static readonly Dictionary<uint, Type> IdToTypeModifierMap = [];
|
||||
private static readonly Dictionary<Type, uint> TypeToIdModifierMap = [];
|
||||
private static readonly Dictionary<int, List<uint>> PrioritiesToIdsMap = [];
|
||||
@@ -74,7 +87,8 @@ public static class ModifierManager
|
||||
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
|
||||
}
|
||||
|
||||
info.Modifiers.Add(modifier);
|
||||
info.InternalModifiers.Add(modifier);
|
||||
InternalModifiers.Add(modifier);
|
||||
|
||||
if (modifier is not GameModifier gameModifier)
|
||||
{
|
||||
@@ -98,7 +112,11 @@ public static class ModifierManager
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void AssignModifiers(List<PlayerControl> plrs)
|
||||
/// <summary>
|
||||
/// Assigns modifiers to players. ONLY CALL THIS METHOD IF YOU KNOW WHAT YOU ARE DOING.
|
||||
/// </summary>
|
||||
/// <param name="plrs">The players to assign modifiers to.</param>
|
||||
public static void AssignModifiers(List<PlayerControl> plrs)
|
||||
{
|
||||
var rand = new Random();
|
||||
|
||||
@@ -108,6 +126,7 @@ public static class ModifierManager
|
||||
.Select(x => Activator.CreateInstance(x.Value) as GameModifier)
|
||||
.OfType<GameModifier>()
|
||||
.Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0)
|
||||
.Shuffle()
|
||||
.OrderByDescending(x => x.Priority())
|
||||
.ToArray();
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Patches;
|
||||
|
||||
#pragma warning disable SA1629
|
||||
/// <summary>
|
||||
/// Allows players to paste text into chat.
|
||||
/// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs
|
||||
/// </summary>
|
||||
#pragma warning restore SA1629
|
||||
[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))]
|
||||
public static class ChatControllerPatch
|
||||
{
|
||||
public static void Prefix(ChatController __instance)
|
||||
{
|
||||
if (!HudManager.Instance.Chat.IsOpenOrOpening) return;
|
||||
|
||||
var ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
|
||||
|
||||
if (!ctrlPressed || !Input.GetKeyDown(KeyCode.V)) return;
|
||||
|
||||
var clipboard = GUIUtility.systemCopyBuffer;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(clipboard)) return;
|
||||
clipboard = clipboard.Replace("<", string.Empty)
|
||||
.Replace(">", string.Empty)
|
||||
.Replace("\r", string.Empty);
|
||||
|
||||
if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift))
|
||||
clipboard = clipboard.Replace("\n", string.Empty);
|
||||
|
||||
__instance.freeChatField.textArea.SetText(__instance.freeChatField.textArea.text + clipboard);
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,16 @@ public static class CanUsePatches
|
||||
public static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
yield return AccessTools.Method(typeof(Console), nameof(Console.CanUse));
|
||||
yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.CanUse));
|
||||
yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(DeconControl), nameof(DeconControl.CanUse));
|
||||
yield return AccessTools.Method(typeof(DoorConsole), nameof(DoorConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(Ladder), nameof(Ladder.CanUse));
|
||||
yield return AccessTools.Method(typeof(MapConsole), nameof(MapConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(OpenDoorConsole), nameof(OpenDoorConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(OptionsConsole), nameof(OptionsConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(PlatformConsole), nameof(PlatformConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(SystemConsole), nameof(SystemConsole.CanUse));
|
||||
yield return AccessTools.Method(typeof(ZiplineConsole), nameof(ZiplineConsole.CanUse));
|
||||
// vent is not necessary as it has its own patch
|
||||
}
|
||||
|
||||
[HarmonyPriority(Priority.Last)]
|
||||
|
||||
@@ -5,29 +5,44 @@ using MiraAPI.GameEnd;
|
||||
|
||||
namespace MiraAPI.Patches.Events;
|
||||
|
||||
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
|
||||
internal static class EndGamePatches
|
||||
{
|
||||
public static bool Prefix(EndGameManager __instance)
|
||||
[HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))]
|
||||
public static class GameManagerPatches
|
||||
{
|
||||
if (CustomGameOver.Instance is not { } gameOver)
|
||||
public static bool Prefix(GameOverReason endReason)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
var @event = new BeforeGameEndEvent(endReason);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
|
||||
var result = gameOver.BeforeEndGameSetup(__instance);
|
||||
return result;
|
||||
return !@event.IsCancelled;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Postfix(EndGameManager __instance)
|
||||
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
|
||||
public static class EndGameManagerPatches
|
||||
{
|
||||
if (CustomGameOver.Instance is { } gameOver)
|
||||
public static bool Prefix(EndGameManager __instance)
|
||||
{
|
||||
gameOver.AfterEndGameSetup(__instance);
|
||||
CustomGameOver.Instance = null;
|
||||
if (CustomGameOver.Instance is not { } gameOver)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var result = gameOver.BeforeEndGameSetup(__instance);
|
||||
return result;
|
||||
}
|
||||
|
||||
var @event = new GameEndEvent(__instance);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
public static void Postfix(EndGameManager __instance)
|
||||
{
|
||||
if (CustomGameOver.Instance is { } gameOver)
|
||||
{
|
||||
gameOver.AfterEndGameSetup(__instance);
|
||||
CustomGameOver.Instance = null;
|
||||
}
|
||||
|
||||
var @event = new GameEndEvent(__instance);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,17 @@ public static class KillAnimationPatches
|
||||
/// <summary>
|
||||
/// Used to trigger the <see cref="AfterMurderEvent"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">The killer.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))]
|
||||
public static void KillAnimPostfix(PlayerControl source, PlayerControl target)
|
||||
[HarmonyPatch(typeof(KillAnimation._CoPerformKill_d__2), nameof(KillAnimation._CoPerformKill_d__2.MoveNext))]
|
||||
public static void KillAnimPostfix(KillAnimation._CoPerformKill_d__2 __instance)
|
||||
{
|
||||
var afterMurderEvent = new AfterMurderEvent(source, target, Helpers.GetBodyById(target.PlayerId));
|
||||
// Checking if state is -1 to wait for the end of coroutine.
|
||||
if (__instance.__1__state != -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var afterMurderEvent = new AfterMurderEvent(__instance.source, __instance.target, Helpers.GetBodyById(__instance.target.PlayerId));
|
||||
MiraEventManager.InvokeEvent(afterMurderEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,52 @@
|
||||
using HarmonyLib;
|
||||
using AmongUs.InnerNet.GameDataMessages;
|
||||
using HarmonyLib;
|
||||
using InnerNet;
|
||||
using MiraAPI.Events;
|
||||
using MiraAPI.Events.Vanilla.Meeting;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities;
|
||||
|
||||
namespace MiraAPI.Patches.Events;
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class ReportBodyEventPatch
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
|
||||
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
|
||||
private static bool ReportBodyEventHook(PlayerControl source, NetworkedPlayerInfo? target)
|
||||
{
|
||||
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
|
||||
|
||||
var @event = new ReportBodyEvent(__instance, target, body);
|
||||
var @event = new ReportBodyEvent(source, target, body);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
return !@event.IsCancelled;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.ReportDeadBody))]
|
||||
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
|
||||
{
|
||||
return ReportBodyEventHook(__instance, target);
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.LateBroadcastReliableMessage))]
|
||||
public static bool LateBroadcastReliableMessagePrefix(IGameDataMessage rpcMessage)
|
||||
{
|
||||
var reportBodyMessage = rpcMessage.TryCast<RpcReportDeadBodyMessage>();
|
||||
|
||||
if (reportBodyMessage == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var source = AmongUsClient.Instance.FindObjectByNetId<PlayerControl>(reportBodyMessage.rpcObjectNetId);
|
||||
if (source == null)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
var target = GameData.Instance.GetPlayerById(reportBodyMessage.targetPlayerId);
|
||||
|
||||
return ReportBodyEventHook(source, target);
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,10 @@ public static class VentEventPatches
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcEnterVent))]
|
||||
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int id)
|
||||
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int ventId)
|
||||
{
|
||||
var pc = __instance.myPlayer;
|
||||
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == id);
|
||||
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == ventId);
|
||||
|
||||
var @event = new EnterVentEvent(pc, vent);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
@@ -40,10 +40,10 @@ public static class VentEventPatches
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcExitVent))]
|
||||
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int id)
|
||||
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int ventId)
|
||||
{
|
||||
var pc = __instance.myPlayer;
|
||||
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == id);
|
||||
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == ventId);
|
||||
|
||||
var @event = new ExitVentEvent(pc, vent);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
|
||||
@@ -22,15 +22,4 @@ internal static class TaskAddButtonPatches
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(TaskAddButton.Role), MethodType.Setter)]
|
||||
public static void RoleGetterPatch(TaskAddButton __instance)
|
||||
{
|
||||
if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole)
|
||||
{
|
||||
__instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray;
|
||||
}
|
||||
__instance.RolloverHandler.OutColor = __instance.FileImage.color;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
@@ -19,7 +20,6 @@ namespace MiraAPI.Patches.Freeplay;
|
||||
internal static class TaskAdderPatches
|
||||
{
|
||||
private static Scroller _scroller = null!;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
|
||||
public static void AddRolesFolder(TaskAdderGame __instance)
|
||||
@@ -32,6 +32,11 @@ internal static class TaskAdderPatches
|
||||
_scroller.allowY = true;
|
||||
_scroller.Inner = inner.transform;
|
||||
|
||||
var scrollerHelper = __instance.TaskParent.gameObject.AddComponent<ManualScrollHelper>();
|
||||
scrollerHelper.scroller = _scroller;
|
||||
scrollerHelper.verticalAxis = RewiredConstsEnum.Action.TaskRVertical;
|
||||
scrollerHelper.scrollSpeed = 10f;
|
||||
|
||||
GameObject hitbox = new("Hitbox")
|
||||
{
|
||||
layer = 5,
|
||||
@@ -67,9 +72,9 @@ internal static class TaskAdderPatches
|
||||
modifiersFolder.FolderName = "Modifiers";
|
||||
modifiersFolder.name = "ModifiersFolder";
|
||||
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
|
||||
if (plugin.InternalRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
|
||||
{
|
||||
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
|
||||
newFolder.name = plugin.PluginId+":Roles";
|
||||
@@ -78,7 +83,7 @@ internal static class TaskAdderPatches
|
||||
rolesFolder.SubFolders.Add(newFolder);
|
||||
}
|
||||
|
||||
if (plugin.Modifiers.Exists(x => x.ShowInFreeplay))
|
||||
if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay))
|
||||
{
|
||||
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
|
||||
newFolder.name = plugin.PluginId+":Modifiers";
|
||||
@@ -295,7 +300,7 @@ internal static class TaskAdderPatches
|
||||
|
||||
if (_scroller)
|
||||
{
|
||||
_scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 5, 1.4f);
|
||||
_scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 4.5f, 1.65f);
|
||||
_scroller.SetYBoundsMin(0.0f);
|
||||
_scroller.ScrollToTop();
|
||||
}
|
||||
@@ -317,9 +322,9 @@ internal static class TaskAdderPatches
|
||||
ref float num2,
|
||||
ref float num3)
|
||||
{
|
||||
for (var m = 0; m < plugin.Modifiers.Count; m++)
|
||||
for (var m = 0; m < plugin.InternalModifiers.Count; m++)
|
||||
{
|
||||
var modifier = plugin.Modifiers[m];
|
||||
var modifier = plugin.InternalModifiers[m];
|
||||
if (!modifier.ShowInFreeplay)
|
||||
{
|
||||
continue;
|
||||
@@ -332,7 +337,8 @@ internal static class TaskAdderPatches
|
||||
taskAddButton.SafePositionWorld = instance.SafePositionWorld;
|
||||
taskAddButton.Text.text = modifier.ModifierName;
|
||||
taskAddButton.Text.EnableMasking();
|
||||
|
||||
taskAddButton.FileImage.color = modifier.FreeplayFileColor;
|
||||
taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor;
|
||||
taskAddButton.Button.OnClick = new Button.ButtonClickedEvent();
|
||||
taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() =>
|
||||
{
|
||||
@@ -368,9 +374,9 @@ internal static class TaskAdderPatches
|
||||
|
||||
private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3)
|
||||
{
|
||||
for (var m = 0; m < plugin.CustomRoles.Count; m++)
|
||||
for (var m = 0; m < plugin.InternalRoles.Count; m++)
|
||||
{
|
||||
var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value;
|
||||
var roleBehaviour = plugin.InternalRoles.ElementAt(m).Value;
|
||||
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
|
||||
roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false })
|
||||
{
|
||||
@@ -384,6 +390,20 @@ internal static class TaskAdderPatches
|
||||
|
||||
instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
|
||||
taskAddButton2.Role = roleBehaviour;
|
||||
if (roleBehaviour is ICustomRole custom)
|
||||
{
|
||||
var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray;
|
||||
if (custom.Team is ModdedRoleTeams.Crewmate)
|
||||
{
|
||||
customColor = Palette.CrewmateBlue;
|
||||
}
|
||||
else if (custom.Team is ModdedRoleTeams.Impostor)
|
||||
{
|
||||
customColor = Palette.ImpostorRed;
|
||||
}
|
||||
taskAddButton2.FileImage.color = customColor;
|
||||
taskAddButton2.RolloverHandler.OutColor = customColor;
|
||||
}
|
||||
if (taskAddButton2.Button == null)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[*.cs]
|
||||
dotnet_diagnostic.S125.severity = none
|
||||
@@ -1,37 +1,25 @@
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Hud;
|
||||
using Reactor.Utilities;
|
||||
|
||||
namespace MiraAPI.Patches.Hud;
|
||||
|
||||
/// <summary>
|
||||
/// Reset button patches.
|
||||
/// </summary>
|
||||
[HarmonyPatch]
|
||||
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
|
||||
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
|
||||
public static class ButtonResetPatches
|
||||
{
|
||||
/// <summary>
|
||||
/// Resets the cooldown and effect of all custom buttons when the meeting starts.
|
||||
/// </summary>
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
|
||||
public static void MeetingHudStartPostfix()
|
||||
public static void Postfix()
|
||||
{
|
||||
foreach (var customActionButton in CustomButtonManager.CustomButtons)
|
||||
{
|
||||
customActionButton.ResetCooldownAndOrEffect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the cooldown and effect of all custom buttons after the exile screen is closed.
|
||||
/// </summary>
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
|
||||
public static void ExileControllerReEnableGameplayPostfix()
|
||||
{
|
||||
foreach (var customActionButton in CustomButtonManager.CustomButtons)
|
||||
{
|
||||
customActionButton.ResetCooldownAndOrEffect();
|
||||
try
|
||||
{
|
||||
customActionButton.ResetCooldownAndOrEffect();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Hud;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
@@ -77,7 +78,14 @@ public static class HudManagerPatches
|
||||
continue;
|
||||
}
|
||||
|
||||
button.CreateButton(location);
|
||||
try
|
||||
{
|
||||
button.CreateButton(location);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to create custom button {button.GetType().Name}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
gridArrange.Start();
|
||||
@@ -105,7 +113,14 @@ public static class HudManagerPatches
|
||||
|
||||
foreach (var button in CustomButtonManager.CustomButtons)
|
||||
{
|
||||
button.SetActive(isActive, role);
|
||||
try
|
||||
{
|
||||
button.SetActive(isActive, role);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to set custom button {button.GetType().Name} active: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public static class HatsTabPatches
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
|
||||
text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})\nPress Ctrl or Tab to cycle pages";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
@@ -114,7 +114,7 @@ public static class HatsTabPatches
|
||||
yield return null;
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.SetScrollerBounds();
|
||||
yield return new WaitForSeconds(0.01f);
|
||||
}
|
||||
__instance.currentHatIsEquipped = true;
|
||||
|
||||
@@ -73,7 +73,7 @@ public static class NameplatesTabPatches
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
|
||||
text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})\nPress Ctrl or Tab to cycle pages";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
@@ -91,7 +91,7 @@ public static class NameplatesTabPatches
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentNameplateIsEquipped = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public static class SkinsTabPatches
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
|
||||
text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})\nPress Ctrl or Tab to cycle pages";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
@@ -94,7 +94,7 @@ public static class SkinsTabPatches
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentSkinIsEquipped = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public static class VisorsTabPatches
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
|
||||
text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})\nPress Ctrl or Tab to cycle pages";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
@@ -90,7 +90,7 @@ public static class VisorsTabPatches
|
||||
hatIndex += 1;
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.SetScrollerBounds();
|
||||
__instance.currentVisorIsEquipped = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,12 @@ using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Patches the GameOptionsMenu to add custom options.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(GameOptionsMenu))]
|
||||
public static class GameOptionsMenuPatch
|
||||
internal static class GameOptionsMenuPatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Update patch for the GameOptionsMenu.
|
||||
/// </summary>
|
||||
/// <param name="__instance">The GameOptionsMenu instance.</param>
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(GameOptionsMenu.Update))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void UpdatePatch(GameOptionsMenu __instance)
|
||||
{
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0)
|
||||
@@ -41,7 +35,7 @@ public static class GameOptionsMenuPatch
|
||||
else
|
||||
{
|
||||
var filteredGroups =
|
||||
GameSettingMenuPatches.SelectedMod?.OptionGroups
|
||||
GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
|
||||
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
|
||||
|
||||
foreach (var group in filteredGroups)
|
||||
@@ -107,7 +101,7 @@ public static class GameOptionsMenuPatch
|
||||
|
||||
private static void ModifiersUpdate(ref float num)
|
||||
{
|
||||
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
|
||||
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
|
||||
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
|
||||
|
||||
foreach (var modGroup in groups)
|
||||
@@ -118,7 +112,7 @@ public static class GameOptionsMenuPatch
|
||||
|
||||
private static void ModifiersCreate(GameOptionsMenu menu)
|
||||
{
|
||||
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
|
||||
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
|
||||
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
|
||||
foreach (var group in groups)
|
||||
{
|
||||
@@ -128,6 +122,7 @@ public static class GameOptionsMenuPatch
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool SettingsPatch(GameOptionsMenu __instance)
|
||||
{
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0)
|
||||
@@ -143,7 +138,7 @@ public static class GameOptionsMenuPatch
|
||||
return false;
|
||||
}
|
||||
|
||||
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups
|
||||
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
|
||||
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
|
||||
|
||||
foreach (var group in filteredGroups)
|
||||
@@ -156,6 +151,7 @@ public static class GameOptionsMenuPatch
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static bool InitPatch(GameOptionsMenu __instance)
|
||||
{
|
||||
if (__instance.Children != null && __instance.Children.Count != 0)
|
||||
@@ -224,6 +220,7 @@ public static class GameOptionsMenuPatch
|
||||
menu.checkboxOrigin,
|
||||
menu.numberOptionOrigin,
|
||||
menu.stringOptionOrigin,
|
||||
menu.playerOptionOrigin,
|
||||
menu.settingsContainer));
|
||||
|
||||
foreach (var newOpt in options)
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BepInEx.Configuration;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Presets;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
internal static class GamePresetsTabPatches
|
||||
{
|
||||
private static GameObject _saveButton = null!;
|
||||
private static GameObject _refreshButton = null!;
|
||||
private static GameObject _folderButton = null!;
|
||||
private static GameObject _newDivider = null!;
|
||||
private static GameObject _presetHolder = null!;
|
||||
private static GridArrange _arrange = null!;
|
||||
|
||||
private static readonly object Lock = new();
|
||||
|
||||
public static void DivideSize(GameObject obj, float amount)
|
||||
{
|
||||
foreach (var collider in obj.GetComponentsInChildren<Collider2D>(true))
|
||||
{
|
||||
if (collider.TryCast<BoxCollider2D>() is { } col)
|
||||
{
|
||||
col.size = new Vector2(col.size.x / amount, col.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rend in obj.GetComponentsInChildren<SpriteRenderer>(true))
|
||||
{
|
||||
rend.size = new Vector2(rend.size.x / amount, rend.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))]
|
||||
public static class GamePresetsOnEnablePatch
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void Postfix(GamePresetsTab __instance)
|
||||
{
|
||||
if (!GameSettingMenu.Instance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CreatePresetMenu(__instance);
|
||||
|
||||
__instance.StandardPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
|
||||
__instance.SecondPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
|
||||
|
||||
if (GameSettingMenuPatches.SelectedModIdx != 0)
|
||||
{
|
||||
__instance.PresetDescriptionText.text = "Select or create an options preset for this mod";
|
||||
}
|
||||
else
|
||||
{
|
||||
__instance.SetSelectedText();
|
||||
}
|
||||
|
||||
// Show the save and refresh buttons
|
||||
if (_saveButton)
|
||||
{
|
||||
_saveButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
|
||||
}
|
||||
if (_refreshButton)
|
||||
{
|
||||
_refreshButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
|
||||
}
|
||||
if (_folderButton)
|
||||
{
|
||||
_folderButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
|
||||
}
|
||||
if (_newDivider)
|
||||
{
|
||||
_newDivider.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
|
||||
}
|
||||
|
||||
var prefab = GameSettingMenu.Instance.GameSettingsButton;
|
||||
|
||||
foreach (var preset in MiraPluginManager.Instance.RegisteredPlugins.SelectMany(x => x.InternalPresets))
|
||||
{
|
||||
var button = Object.Instantiate(prefab, _presetHolder.transform);
|
||||
button.buttonText.text = preset.Name;
|
||||
if (button.buttonText.TryGetComponent<TextTranslatorTMP>(out var tmp))
|
||||
{
|
||||
tmp.DestroyImmediate();
|
||||
}
|
||||
|
||||
button.buttonText.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
button.OnClick = new Button.ButtonClickedEvent();
|
||||
button.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
preset.LoadPreset();
|
||||
}));
|
||||
button.gameObject.SetActive(false);
|
||||
preset.PresetButton = button.gameObject;
|
||||
}
|
||||
|
||||
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
foreach (var button in mod.InternalPresets.Select(x => x.PresetButton))
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.SetActive(mod == GameSettingMenuPatches.SelectedMod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_arrange.Start();
|
||||
_arrange.ArrangeChilds();
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnDisable))]
|
||||
public static class GamePresetsOnDisablePatch
|
||||
{
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static void Postfix()
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("OnDisable called");
|
||||
|
||||
if (_saveButton)
|
||||
{
|
||||
_saveButton.SetActive(false);
|
||||
}
|
||||
if (_refreshButton)
|
||||
{
|
||||
_refreshButton.SetActive(false);
|
||||
}
|
||||
if (_folderButton)
|
||||
{
|
||||
_folderButton.SetActive(false);
|
||||
}
|
||||
|
||||
if (_newDivider)
|
||||
{
|
||||
_newDivider.SetActive(false);
|
||||
}
|
||||
|
||||
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
foreach (var button in mod.InternalPresets.Select(x => x.PresetButton))
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreatePresetMenu(GamePresetsTab presetTab)
|
||||
{
|
||||
var prefab = GameSettingMenu.Instance.GameSettingsButton;
|
||||
if (prefab == null)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("GameSettingsButton prefab is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var tmpTranslator = presetTab.PresetDescriptionText.gameObject.GetComponent<TextTranslatorTMP>();
|
||||
if (tmpTranslator)
|
||||
{
|
||||
tmpTranslator.Destroy();
|
||||
}
|
||||
|
||||
if (!_newDivider)
|
||||
{
|
||||
var oldDiv = presetTab.transform.FindChild("DividerImage");
|
||||
_newDivider = Object.Instantiate(oldDiv.gameObject, oldDiv.transform.parent);
|
||||
_newDivider.transform.localPosition = new Vector3(1.85f, 0.13f, 0f);
|
||||
_newDivider.transform.localScale = new Vector3(1.13f, 1, 1);
|
||||
_newDivider.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, 90));
|
||||
}
|
||||
|
||||
PassiveButton saveButton;
|
||||
if (!_saveButton)
|
||||
{
|
||||
saveButton = Object.Instantiate(prefab, presetTab.transform);
|
||||
_saveButton = saveButton.gameObject;
|
||||
_saveButton.gameObject.name = "SaveButton";
|
||||
_saveButton.transform.localPosition = new Vector3(3.2f, 1.7f, -2);
|
||||
|
||||
// set the button text and alignment
|
||||
var saveText = saveButton.buttonText;
|
||||
saveText.text = "Save";
|
||||
saveText.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
saveText.alignment = TextAlignmentOptions.Center;
|
||||
saveText.transform.parent.localPosition = new Vector3(
|
||||
-.525f,
|
||||
saveText.transform.parent.localPosition.y,
|
||||
saveText.transform.parent.localPosition.z);
|
||||
|
||||
DivideSize(saveButton.gameObject, 2f);
|
||||
|
||||
saveButton.OnClick = new Button.ButtonClickedEvent();
|
||||
saveButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SavePresetPopup.CreatePopup(
|
||||
name =>
|
||||
{
|
||||
if (GameSettingMenuPatches.SelectedMod == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var presetFile = new ConfigFile(
|
||||
Path.Combine(
|
||||
PresetManager.PresetDirectory,
|
||||
GameSettingMenuPatches.SelectedMod.PluginId,
|
||||
$"{name}.cfg"),
|
||||
false)
|
||||
{
|
||||
SaveOnConfigSet = false,
|
||||
};
|
||||
|
||||
foreach (var option in GameSettingMenuPatches.SelectedMod.InternalOptions.Where(x => x.IncludeInPreset))
|
||||
{
|
||||
option.SaveToPreset(presetFile);
|
||||
}
|
||||
|
||||
foreach (var role in GameSettingMenuPatches.SelectedMod.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
|
||||
{
|
||||
role.SaveToPreset(presetFile);
|
||||
}
|
||||
|
||||
presetFile.Save();
|
||||
Refresh();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
saveButton = _saveButton.GetComponent<PassiveButton>();
|
||||
|
||||
PassiveButton refreshButton;
|
||||
if (!_refreshButton)
|
||||
{
|
||||
refreshButton = Object.Instantiate(saveButton, presetTab.transform);
|
||||
_refreshButton = refreshButton.gameObject;
|
||||
_refreshButton.name = "RefreshButton";
|
||||
_refreshButton.transform.localPosition = new Vector3(2.85f, 1.1f, -2);
|
||||
|
||||
var icon = new GameObject("Sprite");
|
||||
icon.transform.SetParent(refreshButton.transform);
|
||||
icon.transform.localPosition = new Vector3(-0.47f, -0.08f, -5f);
|
||||
icon.transform.localScale = new Vector3(0.5f, 0.5f, 1);
|
||||
icon.layer = refreshButton.gameObject.layer;
|
||||
|
||||
var iconSpriteRend = icon.AddComponent<SpriteRenderer>();
|
||||
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
|
||||
|
||||
DivideSize(refreshButton.gameObject, 2f);
|
||||
refreshButton.buttonText.gameObject.Destroy();
|
||||
|
||||
refreshButton.OnClick = new Button.ButtonClickedEvent();
|
||||
refreshButton.OnClick.AddListener((UnityAction)Refresh);
|
||||
}
|
||||
refreshButton = _refreshButton.GetComponent<PassiveButton>();
|
||||
|
||||
if (!_folderButton)
|
||||
{
|
||||
var openFolderButton = Object.Instantiate(refreshButton, presetTab.transform);
|
||||
_folderButton = openFolderButton.gameObject;
|
||||
_folderButton.name = "OpenFolderButton";
|
||||
_folderButton.transform.localPosition = new Vector3(3.55f, 1.1f, -2);
|
||||
|
||||
var folderRend = openFolderButton.transform.FindChild("Sprite").gameObject.GetComponent<SpriteRenderer>();
|
||||
folderRend.sprite = MiraAssets.FolderIcon.LoadAsset();
|
||||
folderRend.transform.localScale = new Vector3(0.4f, 0.4f, 1);
|
||||
|
||||
openFolderButton.OnClick = new Button.ButtonClickedEvent();
|
||||
openFolderButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
var directory = Path.Combine(
|
||||
PresetManager.PresetDirectory,
|
||||
GameSettingMenuPatches.SelectedMod?.PluginId ?? string.Empty);
|
||||
Directory.CreateDirectory(directory);
|
||||
Process.Start(
|
||||
new ProcessStartInfo
|
||||
{
|
||||
FileName = directory,
|
||||
UseShellExecute = true,
|
||||
Verb = "open",
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
_saveButton.SetActive(false);
|
||||
_refreshButton.SetActive(false);
|
||||
_folderButton.SetActive(false);
|
||||
_newDivider.SetActive(false);
|
||||
|
||||
if (!_presetHolder)
|
||||
{
|
||||
_presetHolder = new GameObject("PresetHolder");
|
||||
_presetHolder.transform.SetParent(presetTab.transform, false);
|
||||
_presetHolder.transform.localScale = new Vector3(0.9f, 0.9f, 1);
|
||||
_presetHolder.transform.localPosition = new Vector3(-1.9f, 1.7f, 0);
|
||||
_arrange = _presetHolder.AddComponent<GridArrange>();
|
||||
_arrange.Alignment = GridArrange.StartAlign.Right;
|
||||
_arrange.CellSize = new Vector2(2.15f, -.55f);
|
||||
_arrange.MaxColumns = 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Refresh()
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("Refreshing presets");
|
||||
if (GameSettingMenuPatches.SelectedMod == null)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("Selected mod is null, cannot refresh presets");
|
||||
return;
|
||||
}
|
||||
lock (Lock)
|
||||
{
|
||||
PresetManager.LoadPresets(GameSettingMenuPatches.SelectedMod);
|
||||
}
|
||||
GameSettingMenu.Instance.ChangeTab(0, false); // refresh the tab
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Presets;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
@@ -32,6 +33,26 @@ internal static class GameSettingMenuPatches
|
||||
private static PassiveButton? _smallRolesButton;
|
||||
|
||||
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
|
||||
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
|
||||
|
||||
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
|
||||
{
|
||||
if (_modifiersTab)
|
||||
{
|
||||
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
|
||||
}
|
||||
|
||||
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
|
||||
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
|
||||
|
||||
public static void ChangeTabPrefix(GameSettingMenu __instance)
|
||||
{
|
||||
SaveScrollPositions(__instance);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
|
||||
@@ -39,7 +60,11 @@ internal static class GameSettingMenuPatches
|
||||
{
|
||||
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
|
||||
{
|
||||
if (_modifiersTab) _modifiersTab!.gameObject.SetActive(tabNum == 3);
|
||||
if (_modifiersTab)
|
||||
{
|
||||
_modifiersTab!.gameObject.SetActive(tabNum == 3);
|
||||
}
|
||||
|
||||
_modifiersButton?.SelectButton(tabNum == 3);
|
||||
_smallRolesButton?.SelectButton(tabNum == 2);
|
||||
if (tabNum == 3)
|
||||
@@ -63,7 +88,10 @@ internal static class GameSettingMenuPatches
|
||||
_modifiersButton?.SelectButton(tabNum == 3);
|
||||
_smallRolesButton?.SelectButton(tabNum == 2);
|
||||
|
||||
if (tabNum != 3) return;
|
||||
if (tabNum != 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (__instance.RoleSettingsButton.gameObject.active)
|
||||
{
|
||||
@@ -113,13 +141,9 @@ internal static class GameSettingMenuPatches
|
||||
passiveButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
if (!__instance.RoleSettingsTab.AdvancedRolesSettings.active)
|
||||
{
|
||||
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = __instance.RoleSettingsTab.scrollBar.Inner.localPosition;
|
||||
}
|
||||
OptionsPositions[SelectedModIdx] = __instance.GameSettingsTab.scrollBar.Inner.localPosition;
|
||||
SaveScrollPositions(__instance);
|
||||
SelectedModIdx += 1;
|
||||
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
|
||||
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
|
||||
{
|
||||
SelectedModIdx = 0;
|
||||
}
|
||||
@@ -136,15 +160,11 @@ internal static class GameSettingMenuPatches
|
||||
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
if (!__instance.RoleSettingsTab.AdvancedRolesSettings.active)
|
||||
{
|
||||
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = __instance.RoleSettingsTab.scrollBar.Inner.localPosition;
|
||||
}
|
||||
OptionsPositions[SelectedModIdx] = __instance.GameSettingsTab.scrollBar.Inner.localPosition;
|
||||
SaveScrollPositions(__instance);
|
||||
SelectedModIdx -= 1;
|
||||
if (SelectedModIdx < 0)
|
||||
{
|
||||
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
|
||||
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
|
||||
}
|
||||
|
||||
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
|
||||
@@ -221,57 +241,64 @@ internal static class GameSettingMenuPatches
|
||||
|
||||
_modifierBtnOgPos = _modifiersButton.transform.localPosition;
|
||||
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
PresetManager.LoadPresets(plugin);
|
||||
}
|
||||
|
||||
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
|
||||
}
|
||||
|
||||
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu __instance)
|
||||
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu menu)
|
||||
{
|
||||
__instance.RoleSettingsButton.OnClick = new ButtonClickedEvent();
|
||||
__instance.RoleSettingsButton.OnMouseOver = new UnityEvent();
|
||||
menu.RoleSettingsButton.OnClick = new ButtonClickedEvent();
|
||||
menu.RoleSettingsButton.OnMouseOver = new UnityEvent();
|
||||
|
||||
if (replace)
|
||||
{
|
||||
__instance.RoleSettingsButton.buttonText.text = "Modifiers Settings";
|
||||
__instance.RoleSettingsButton.OnClick.AddListener(
|
||||
menu.RoleSettingsButton.buttonText.text = "Modifiers Settings";
|
||||
menu.RoleSettingsButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
__instance.ChangeTab(3, false);
|
||||
menu.ChangeTab(3, false);
|
||||
}));
|
||||
__instance.RoleSettingsButton.OnMouseOver.AddListener(
|
||||
menu.RoleSettingsButton.OnMouseOver.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
__instance.ChangeTab(3, true);
|
||||
menu.ChangeTab(3, true);
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
__instance.RoleSettingsButton.buttonText.text = "Roles Settings";
|
||||
__instance.RoleSettingsButton.OnClick.AddListener(
|
||||
menu.RoleSettingsButton.buttonText.text = "Roles Settings";
|
||||
menu.RoleSettingsButton.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
__instance.ChangeTab(2, false);
|
||||
menu.ChangeTab(2, false);
|
||||
}));
|
||||
__instance.RoleSettingsButton.OnMouseOver.AddListener(
|
||||
menu.RoleSettingsButton.OnMouseOver.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
__instance.ChangeTab(2, true);
|
||||
menu.ChangeTab(2, true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
|
||||
{
|
||||
if (_text is not null && SelectedModIdx == 0)
|
||||
{
|
||||
_text.text = "Default";
|
||||
_text.text = $"<size=40%>(Page 0/{MiraPluginManager.Instance.RegisteredPlugins.Length})</size>\nDefault";
|
||||
_text.fontSizeMax = 3.2f;
|
||||
SelectedMod = null;
|
||||
}
|
||||
else if (_text is not null)
|
||||
{
|
||||
_text.fontSizeMax = 2.3f;
|
||||
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
|
||||
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
|
||||
|
||||
var name = SelectedMod.MiraPlugin.OptionsTitleText;
|
||||
_text.text = name[..Math.Min(name.Length, 25)];
|
||||
_text.text = $"<size=50%>(Page {SelectedModIdx}/{MiraPluginManager.Instance.RegisteredPlugins.Length})</size>\n" + name[..Math.Min(name.Length, 25)];
|
||||
}
|
||||
|
||||
bool replaceWithModifiers = true;
|
||||
@@ -281,9 +308,11 @@ internal static class GameSettingMenuPatches
|
||||
|
||||
if (SelectedModIdx != 0)
|
||||
{
|
||||
var modHasRoles = SelectedMod!.CustomRoles.Count != 0;
|
||||
var modHasModifiers = SelectedMod.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
|
||||
var modHasOptions = SelectedMod.OptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
|
||||
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
|
||||
var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
|
||||
x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
|
||||
var modHasOptions =
|
||||
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
|
||||
|
||||
_modifiersButton.gameObject.SetActive(true);
|
||||
_smallRolesButton.gameObject.SetActive(true);
|
||||
@@ -323,6 +352,7 @@ internal static class GameSettingMenuPatches
|
||||
{
|
||||
menu.ChangeTab(0, false);
|
||||
}
|
||||
|
||||
// If the mod has roles, we can enable the bigger role button and disable the small one.
|
||||
if (modHasRoles)
|
||||
{
|
||||
@@ -379,6 +409,11 @@ internal static class GameSettingMenuPatches
|
||||
ChangeRoleSettingButton(replaceWithModifiers, menu);
|
||||
}
|
||||
|
||||
if (menu.PresetsTab.gameObject.active)
|
||||
{
|
||||
menu.ChangeTab(0, false);
|
||||
}
|
||||
|
||||
CleanupTab(settings, roles);
|
||||
}
|
||||
|
||||
@@ -390,13 +425,30 @@ internal static class GameSettingMenuPatches
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
child.gameObject.DestroyImmediate();
|
||||
}
|
||||
|
||||
options.Clear();
|
||||
}
|
||||
|
||||
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
|
||||
{
|
||||
if (roles.roleChances != null)
|
||||
{
|
||||
CleanupRoleSettings(roles);
|
||||
}
|
||||
|
||||
if (settings.Children != null)
|
||||
{
|
||||
CleanupSettings(settings);
|
||||
}
|
||||
|
||||
if (_modifiersTab?.Children?.Count > 0)
|
||||
{
|
||||
CleanupSettings(_modifiersTab, true);
|
||||
}
|
||||
|
||||
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
|
||||
{
|
||||
if (rolesMenu.advancedSettingChildren != null)
|
||||
@@ -415,6 +467,7 @@ internal static class GameSettingMenuPatches
|
||||
{
|
||||
role.gameObject.DestroyImmediate();
|
||||
}
|
||||
|
||||
rolesMenu.roleChances?.Clear();
|
||||
|
||||
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
|
||||
@@ -431,7 +484,7 @@ internal static class GameSettingMenuPatches
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupSettings(GameOptionsMenu gameOptMenu)
|
||||
void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
|
||||
{
|
||||
ClearOptions(gameOptMenu.Children);
|
||||
gameOptMenu.Children = null;
|
||||
@@ -442,7 +495,8 @@ internal static class GameSettingMenuPatches
|
||||
.ForEach(header => header.gameObject.DestroyImmediate());
|
||||
|
||||
gameOptMenu.Initialize();
|
||||
if (OptionsPositions.TryGetValue(SelectedModIdx, out var pos))
|
||||
var positions = modifiers ? ModifiersPositions : OptionsPositions;
|
||||
if (positions.TryGetValue(SelectedModIdx, out var pos))
|
||||
{
|
||||
gameOptMenu.scrollBar.Inner.localPosition = pos;
|
||||
gameOptMenu.scrollBar.UpdateScrollBars();
|
||||
@@ -452,20 +506,5 @@ internal static class GameSettingMenuPatches
|
||||
gameOptMenu.scrollBar.ScrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.roleChances != null && SelectedMod?.CustomRoles.Count > 0)
|
||||
{
|
||||
CleanupRoleSettings(roles);
|
||||
}
|
||||
|
||||
if (settings.Children != null && SelectedMod?.OptionGroups.Count > 0)
|
||||
{
|
||||
CleanupSettings(settings);
|
||||
}
|
||||
|
||||
if (_modifiersTab?.Children?.Count > 0)
|
||||
{
|
||||
CleanupSettings(_modifiersTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public static class LobbyViewPanePatches
|
||||
|
||||
private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0
|
||||
? null
|
||||
: MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
|
||||
: MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
|
||||
|
||||
private static PassiveButton? ModifiersTabButton { get; set; }
|
||||
|
||||
@@ -74,7 +74,7 @@ public static class LobbyViewPanePatches
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
SelectedModIdx += 1;
|
||||
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
|
||||
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
|
||||
{
|
||||
SelectedModIdx = 0;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public static class LobbyViewPanePatches
|
||||
SelectedModIdx -= 1;
|
||||
if (SelectedModIdx < 0)
|
||||
{
|
||||
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
|
||||
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
|
||||
}
|
||||
|
||||
Refresh(__instance);
|
||||
@@ -113,24 +113,27 @@ public static class LobbyViewPanePatches
|
||||
menu.scrollBar.ScrollToTop();
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))]
|
||||
public static bool SetTabPatch(LobbyViewSettingsPane __instance)
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(LobbyViewSettingsPane.ChangeTab))]
|
||||
[HarmonyPatch(nameof(LobbyViewSettingsPane.RefreshTab))]
|
||||
// CHANGED BECAUSE OF INLINING
|
||||
public static void SetTabPatch(LobbyViewSettingsPane __instance)
|
||||
{
|
||||
if (__instance.currentTab != ModifiersTabName || SelectedMod == null)
|
||||
{
|
||||
ModifiersTabButton?.SelectButton(false);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
__instance.taskTabButton.SelectButton(false);
|
||||
__instance.rolesTabButton.SelectButton(false);
|
||||
|
||||
ModifiersTabButton?.SelectButton(true);
|
||||
var filteredGroups = SelectedMod.OptionGroups
|
||||
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
|
||||
|
||||
var filteredGroups = SelectedMod.InternalOptionGroups
|
||||
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || (x.OptionableType != null && x.OptionableType.IsAssignableTo(typeof(BaseModifier)))));
|
||||
|
||||
DrawOptions(__instance, filteredGroups);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
@@ -142,8 +145,14 @@ public static class LobbyViewPanePatches
|
||||
return true;
|
||||
}
|
||||
|
||||
var filteredGroups = SelectedMod.OptionGroups
|
||||
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
|
||||
if (__instance.currentTab == ModifiersTabName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filteredGroups = SelectedMod.InternalOptionGroups
|
||||
.Where(x => x is { ShowInModifiersMenu: false, OptionableType: null } && x.GroupVisible());
|
||||
|
||||
DrawOptions(__instance, filteredGroups);
|
||||
return false;
|
||||
}
|
||||
@@ -157,6 +166,11 @@ public static class LobbyViewPanePatches
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.currentTab == ModifiersTabName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DrawRolesTab(__instance);
|
||||
return false;
|
||||
}
|
||||
@@ -165,12 +179,15 @@ public static class LobbyViewPanePatches
|
||||
{
|
||||
var num = 1.44f;
|
||||
|
||||
foreach (var group in groups)
|
||||
var groupArray = groups.Where(x => x.GroupVisible() && x.Options.Any(y => y.Visible())).ToArray();
|
||||
|
||||
foreach (var group in groupArray)
|
||||
{
|
||||
var categoryHeaderMasked = Object.Instantiate(
|
||||
menu.categoryHeaderOrigin,
|
||||
menu.settingsContainer,
|
||||
true);
|
||||
|
||||
categoryHeaderMasked.SetHeader(StringNames.Name, 61);
|
||||
categoryHeaderMasked.Title.text = group.GroupName;
|
||||
categoryHeaderMasked.transform.localScale = Vector3.one;
|
||||
@@ -182,7 +199,7 @@ public static class LobbyViewPanePatches
|
||||
|
||||
foreach (var option in group.Options)
|
||||
{
|
||||
if (!option.Visible.Invoke())
|
||||
if (!option.Visible())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -191,6 +208,7 @@ public static class LobbyViewPanePatches
|
||||
menu.infoPanelOrigin,
|
||||
menu.settingsContainer,
|
||||
true);
|
||||
|
||||
viewSettingsInfoPanel.transform.localScale = Vector3.one;
|
||||
float num2;
|
||||
if (i % 2 == 0)
|
||||
@@ -229,7 +247,7 @@ public static class LobbyViewPanePatches
|
||||
num -= 0.85f;
|
||||
}
|
||||
|
||||
menu.scrollBar.CalculateAndSetYBounds(menu.settingsInfo.Count + 10, 2f, 6f, 0.85f);
|
||||
menu.scrollBar.SetYBoundsMax(-num - 2);
|
||||
}
|
||||
|
||||
private static void DrawRolesTab(LobbyViewSettingsPane instance)
|
||||
@@ -250,7 +268,7 @@ public static class LobbyViewPanePatches
|
||||
|
||||
var list = new List<Type>();
|
||||
|
||||
var roleGroups = SelectedMod.CustomRoles.Values.OfType<ICustomRole>()
|
||||
var roleGroups = SelectedMod.InternalRoles.Values.OfType<ICustomRole>()
|
||||
.ToLookup(x => x.RoleOptionsGroup);
|
||||
|
||||
// sort the groups by priority
|
||||
@@ -315,7 +333,7 @@ public static class LobbyViewPanePatches
|
||||
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
|
||||
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
|
||||
|
||||
var advancedRoleOptions = SelectedMod.OptionGroups
|
||||
var advancedRoleOptions = SelectedMod.InternalOptionGroups
|
||||
.Where(x => x.OptionableType == customRole.GetType())
|
||||
.SelectMany(x => x.Options)
|
||||
.ToList();
|
||||
@@ -402,7 +420,7 @@ public static class LobbyViewPanePatches
|
||||
return 0;
|
||||
}
|
||||
|
||||
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
|
||||
var role = SelectedMod.InternalRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
|
||||
|
||||
if (role == null)
|
||||
{
|
||||
@@ -425,7 +443,7 @@ public static class LobbyViewPanePatches
|
||||
var num = viewPanel.yPosStart;
|
||||
var num2 = 1.08f;
|
||||
|
||||
var filteredOptions = SelectedMod.OptionGroups
|
||||
var filteredOptions = SelectedMod.InternalOptionGroups
|
||||
.Where(x => x.OptionableType == roleType)
|
||||
.SelectMany(x=>x.Options)
|
||||
.ToList();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.Roles;
|
||||
using UnityEngine;
|
||||
using Object = Il2CppSystem.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
@@ -1,12 +1,161 @@
|
||||
using HarmonyLib;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities;
|
||||
using UnityEngine;
|
||||
using Object = Il2CppSystem.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
[HarmonyPatch]
|
||||
public static class OptionsPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))]
|
||||
public static bool RoleIncreaseChancePrefix(RoleOptionSetting __instance)
|
||||
{
|
||||
if (!__instance.Role.IsCustomRole())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.roleChance == 0)
|
||||
{
|
||||
__instance.roleMaxCount = 1;
|
||||
}
|
||||
|
||||
var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
|
||||
__instance.roleChance += increment;
|
||||
|
||||
if (__instance.roleChance > 100)
|
||||
{
|
||||
__instance.roleChance = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseChance))]
|
||||
public static bool RoleDecreaseChancePrefix(RoleOptionSetting __instance)
|
||||
{
|
||||
if (!__instance.Role.IsCustomRole())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.roleChance == 0)
|
||||
{
|
||||
__instance.roleMaxCount = 1;
|
||||
}
|
||||
|
||||
var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
|
||||
__instance.roleChance -= increment;
|
||||
|
||||
if (__instance.roleChance < 0)
|
||||
{
|
||||
__instance.roleChance = 100;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseCount))]
|
||||
public static bool RoleIncreaseCountPrefix(RoleOptionSetting __instance)
|
||||
{
|
||||
if (!__instance.Role.IsCustomRole())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.roleMaxCount == 0)
|
||||
{
|
||||
__instance.roleChance = 50;
|
||||
}
|
||||
|
||||
__instance.roleMaxCount += 1;
|
||||
if (__instance.roleMaxCount > __instance.role.MaxCount)
|
||||
{
|
||||
__instance.roleMaxCount = 0;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseCount))]
|
||||
public static bool RoleDecreaseCountPrefix(RoleOptionSetting __instance)
|
||||
{
|
||||
if (!__instance.Role.IsCustomRole())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.roleMaxCount == 0)
|
||||
{
|
||||
__instance.roleChance = 50;
|
||||
}
|
||||
|
||||
__instance.roleMaxCount -= 1;
|
||||
if (__instance.roleMaxCount < 0)
|
||||
{
|
||||
__instance.roleMaxCount = __instance.role.MaxCount;
|
||||
}
|
||||
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))]
|
||||
[HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))]
|
||||
[HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.AdjustButtonsActiveState))]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustChanceButtonsActiveState))]
|
||||
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustCountButtonsActiveState))]
|
||||
public static bool AdjustButtonsPrefix(OptionBehaviour __instance)
|
||||
{
|
||||
if (__instance.IsCustom())
|
||||
{
|
||||
if (__instance.TryCast<NumberOption>() is { } numberOption)
|
||||
{
|
||||
numberOption.MinusBtn.SetInteractable(true);
|
||||
numberOption.PlusBtn.SetInteractable(true);
|
||||
}
|
||||
if (__instance.TryCast<StringOption>() is { } stringOption)
|
||||
{
|
||||
stringOption.MinusBtn.SetInteractable(true);
|
||||
stringOption.PlusBtn.SetInteractable(true);
|
||||
}
|
||||
if (__instance.TryCast<PlayerOption>() is { } playerOption)
|
||||
{
|
||||
playerOption.MinusBtn.SetInteractable(true);
|
||||
playerOption.PlusBtn.SetInteractable(true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (__instance.TryCast<RoleOptionSetting>() is { } roleOptionSetting && roleOptionSetting.Role.IsCustomRole())
|
||||
{
|
||||
roleOptionSetting.CountMinusBtn.SetInteractable(true);
|
||||
roleOptionSetting.CountPlusBtn.SetInteractable(true);
|
||||
roleOptionSetting.ChanceMinusBtn.SetInteractable(true);
|
||||
roleOptionSetting.ChancePlusBtn.SetInteractable(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Initialize))]
|
||||
public static bool ToggleInit(ToggleOption __instance)
|
||||
@@ -16,15 +165,31 @@ public static class OptionsPatches
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.TitleText.text =
|
||||
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.UpdateValue))]
|
||||
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Toggle))]
|
||||
// UpdateValue was inlined.
|
||||
public static bool ToggleUpdate(ToggleOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.CheckMark.enabled = !__instance.CheckMark.enabled;
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
|
||||
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
|
||||
[HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.UpdateValue))]
|
||||
public static bool UpdateValuePrefix(OptionBehaviour __instance)
|
||||
{
|
||||
return !__instance.IsCustom();
|
||||
}
|
||||
@@ -38,17 +203,84 @@ public static class OptionsPatches
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.TitleText.text =
|
||||
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
|
||||
public static bool NumberUpdate(NumberOption __instance)
|
||||
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))]
|
||||
public static bool NumberIncrease(NumberOption __instance)
|
||||
{
|
||||
return !__instance.IsCustom();
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.Value += __instance.Increment;
|
||||
if (__instance.Value > __instance.ValidRange.max)
|
||||
{
|
||||
__instance.Value = __instance.ValidRange.min;
|
||||
}
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
__instance.AdjustButtonsActiveState();
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))]
|
||||
public static bool NumberDecrease(NumberOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.Value -= __instance.Increment;
|
||||
if (__instance.Value < __instance.ValidRange.min)
|
||||
{
|
||||
__instance.Value = __instance.ValidRange.max;
|
||||
}
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
__instance.AdjustButtonsActiveState();
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))]
|
||||
public static bool StringOptionIncrease(StringOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.Value += 1;
|
||||
if (__instance.Value >= __instance.Values.Length)
|
||||
{
|
||||
__instance.Value = 0;
|
||||
}
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
__instance.AdjustButtonsActiveState();
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))]
|
||||
public static bool StringOptionDecrease(StringOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.Value -= 1;
|
||||
if (__instance.Value < 0)
|
||||
{
|
||||
__instance.Value = __instance.Values.Length - 1;
|
||||
}
|
||||
__instance.OnValueChanged.Invoke(__instance);
|
||||
__instance.AdjustButtonsActiveState();
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
@@ -60,19 +292,9 @@ public static class OptionsPatches
|
||||
return true;
|
||||
}
|
||||
|
||||
__instance.TitleText.text =
|
||||
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
__instance.ValueText.text = TranslationController.Instance.GetString(
|
||||
__instance.Values[__instance.Value],
|
||||
Array.Empty<Object>());
|
||||
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
|
||||
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
|
||||
public static bool StringUpdate(StringOption __instance)
|
||||
{
|
||||
return !__instance.IsCustom();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.OptionTypes;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine;
|
||||
using Object = Il2CppSystem.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Options;
|
||||
|
||||
[HarmonyPatch(typeof(PlayerOption))]
|
||||
public static class PlayerOptionPatches
|
||||
{
|
||||
// Updates the list of players according to the filter.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerOption.FixedUpdate))]
|
||||
public static bool PlayerOptionUpdatePatch(PlayerOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ModdedOptionsManager.CreatedPlayerOptions.TryGetValue(__instance, out var moddedPlrOpt)) return false;
|
||||
|
||||
var filteredPlayers = moddedPlrOpt.GetFilteredPlayers();
|
||||
if (filteredPlayers.Count != __instance.Values.Count)
|
||||
{
|
||||
filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
|
||||
|
||||
__instance.Values = filteredPlayers.ToIl2CppList();
|
||||
moddedPlrOpt.Values = __instance.Values;
|
||||
__instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
|
||||
__instance.SetValueText();
|
||||
}
|
||||
|
||||
__instance.CheckValueChanged();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable going under 0 if you aren't allowed to choose None.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerOption.UpdatePlayerIndex))]
|
||||
public static bool UpdatePlayerIndexPatch(PlayerOption __instance, int index)
|
||||
{
|
||||
if (!__instance.IsCustom() || ModdedOptionsManager.CreatedPlayerOptions[__instance].AllowNone)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var newIndex = Mathf.Clamp(index, 0, __instance.Values.Count - 1);
|
||||
__instance.playerIndex = newIndex;
|
||||
__instance.Value = __instance.Values[newIndex].PlayerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Replace "Round Robin" with "None" in the Host Options Menu.
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(PlayerOption.SetValueText))]
|
||||
public static void SetValueTextPatch(PlayerOption __instance)
|
||||
{
|
||||
if (__instance.playerIndex < 0)
|
||||
{
|
||||
__instance.ValueText.text = TranslationController.Instance.GetString(StringNames.FilterNone);
|
||||
}
|
||||
}
|
||||
|
||||
// Innersloth hardcode literally replaces the value with the HNS impostor option OnEnable, so I have to patch that out. Smh.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerOption.OnEnable))]
|
||||
public static bool OnEnablePatch(PlayerOption __instance)
|
||||
{
|
||||
return !__instance.IsCustom();
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerOption.GetInt))]
|
||||
public static bool GetIntPatch(PlayerOption __instance, ref int __result)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
__result = __instance.Value;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Used to replace "Round Robin" with "None" for option notification & view pane.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PlayerSelectionGameSetting), nameof(PlayerSelectionGameSetting.GetValueString))]
|
||||
public static bool GetStringPatch(PlayerSelectionGameSetting __instance, float value, ref string __result)
|
||||
{
|
||||
var option = ModdedOptionsManager.ModdedOptions.FirstOrDefault(x => x.Value.Data == __instance).Value;
|
||||
if (option == null) return true;
|
||||
|
||||
var networkedPlayerInfo = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(p => p.PlayerId == (int)value);
|
||||
if (networkedPlayerInfo != null)
|
||||
{
|
||||
__result = networkedPlayerInfo.PlayerName;
|
||||
return false;
|
||||
}
|
||||
|
||||
__result = TranslationController.Instance.GetString(StringNames.FilterNone);
|
||||
return false;
|
||||
}
|
||||
|
||||
// "UpdateValue" was inlined and I needed to replace it, so I just rewrote the method where it's called from.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerOption.CheckValueChanged))]
|
||||
public static bool UpdateValuePatch(PlayerOption __instance)
|
||||
{
|
||||
if (!__instance.IsCustom())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.oldValue == __instance.Value) return false;
|
||||
|
||||
__instance.oldValue = __instance.Value;
|
||||
__instance.SetValueText();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Networking;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities;
|
||||
@@ -30,14 +31,26 @@ public static class RoleSettingMenuPatches
|
||||
|
||||
private static float ScrollerNum { get; set; } = 0.522f;
|
||||
|
||||
private static RoleBehaviour? CurrentRole { get; set; }
|
||||
private static List<IModdedOption>? CurrentRoleOptions { get; set; }
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
|
||||
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
|
||||
{
|
||||
__instance.scrollBar.SetScrollBounds(__instance);
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
|
||||
public static bool PatchStart(RolesSettingsMenu __instance)
|
||||
public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
|
||||
{
|
||||
Headers.ForEach(Object.Destroy);
|
||||
RoleOptionSettings.ForEach(Object.Destroy);
|
||||
Headers.Clear();
|
||||
RoleOptionSettings.Clear();
|
||||
CurrentRole = null;
|
||||
CurrentRoleOptions = null;
|
||||
|
||||
__instance.roleChances = new Il2CppSystem.Collections.Generic.List<RoleOptionSetting>();
|
||||
__instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List<OptionBehaviour>();
|
||||
@@ -69,7 +82,7 @@ public static class RoleSettingMenuPatches
|
||||
|
||||
var num3 = 0;
|
||||
|
||||
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
|
||||
var roleGroups = GameSettingMenuPatches.SelectedMod?.InternalRoles.Values.OfType<ICustomRole>()
|
||||
.ToLookup(x => x.RoleOptionsGroup);
|
||||
|
||||
if (roleGroups is null)
|
||||
@@ -204,9 +217,10 @@ public static class RoleSettingMenuPatches
|
||||
headerBtn.OnClick.AddListener(
|
||||
(UnityAction)(() =>
|
||||
{
|
||||
if (RoleGroupHidden.TryGetValue(group, out var value))
|
||||
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
|
||||
if (RoleGroupHidden.TryGetValue(group, out var groupHidden))
|
||||
{
|
||||
RoleGroupHidden[group] = !value;
|
||||
RoleGroupHidden[group] = !groupHidden;
|
||||
}
|
||||
foreach (var header in Headers)
|
||||
{
|
||||
@@ -227,13 +241,20 @@ public static class RoleSettingMenuPatches
|
||||
ScrollerNum -= 0.4f;
|
||||
}
|
||||
}
|
||||
__instance.scrollBar.SetScrollBounds();
|
||||
|
||||
__instance.scrollBar.SetScrollBounds(__instance);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void SetScrollBounds(this Scroller scroller)
|
||||
private static void SetScrollBounds(this Scroller scroller, RolesSettingsMenu roleMenu)
|
||||
{
|
||||
scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0)
|
||||
{
|
||||
scroller.CalculateAndSetYBounds(roleMenu.roleChances.Count + 3, 1f, 6f, 0.43f);
|
||||
return;
|
||||
}
|
||||
|
||||
scroller.SetYBoundsMax(-ScrollerNum - 2);
|
||||
if (RolePositions.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
|
||||
{
|
||||
scroller.Inner.localPosition = scroll;
|
||||
@@ -247,21 +268,45 @@ public static class RoleSettingMenuPatches
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(RolesSettingsMenu.Update))]
|
||||
public static bool UpdatePatch()
|
||||
public static bool UpdatePatch(RolesSettingsMenu __instance)
|
||||
{
|
||||
return GameSettingMenuPatches.SelectedModIdx == 0;
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0) return true;
|
||||
if (CurrentRole == null || CurrentRoleOptions == null) return false;
|
||||
|
||||
var hasImage = CurrentRole.RoleScreenshot != null;
|
||||
var num = hasImage ? -0.872f : -1;
|
||||
|
||||
foreach (var opt in CurrentRoleOptions)
|
||||
{
|
||||
if (opt.OptionBehaviour == null) continue;
|
||||
|
||||
if (!opt.Visible.Invoke())
|
||||
{
|
||||
opt.OptionBehaviour.gameObject.SetActive(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
opt.OptionBehaviour.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
|
||||
opt.OptionBehaviour.gameObject.SetActive(true);
|
||||
num += -0.45f;
|
||||
}
|
||||
|
||||
__instance.scrollBar.SetYBoundsMax(-num - 3);
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
|
||||
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
|
||||
{
|
||||
if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar)
|
||||
if (!__instance.scrollBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
__instance.scrollBar.SetScrollBounds();
|
||||
CurrentRole = null;
|
||||
CurrentRoleOptions = null;
|
||||
__instance.scrollBar.SetScrollBounds(__instance);
|
||||
}
|
||||
|
||||
private static void ValueChanged(OptionBehaviour obj)
|
||||
@@ -313,26 +358,26 @@ public static class RoleSettingMenuPatches
|
||||
optBehaviour.gameObject.DestroyImmediate();
|
||||
}
|
||||
|
||||
CurrentRole = role;
|
||||
__instance.advancedSettingChildren.Clear();
|
||||
|
||||
var hasImage = role.RoleScreenshot != null;
|
||||
var num = hasImage ? -0.872f : -1;
|
||||
|
||||
// TODO: create sub groups under the role settings.
|
||||
var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups
|
||||
.Where(x=> x.OptionableType == role.GetType())
|
||||
.SelectMany(x=>x.Options)
|
||||
var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
|
||||
.Where(x => x.GroupVisible() && x.OptionableType == role.GetType())
|
||||
.SelectMany(x => x.Options)
|
||||
.ToList() ?? [];
|
||||
|
||||
CurrentRoleOptions = filteredOptions;
|
||||
|
||||
foreach (var option in filteredOptions)
|
||||
{
|
||||
var newOpt = option.CreateOption(
|
||||
__instance.checkboxOrigin,
|
||||
__instance.numberOptionOrigin,
|
||||
__instance.stringOptionOrigin,
|
||||
GameSettingMenu.Instance.GameSettingsTab.playerOptionOrigin,
|
||||
__instance.AdvancedRolesSettings.transform);
|
||||
|
||||
newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
|
||||
newOpt.SetClickMask(__instance.ButtonClickMask);
|
||||
|
||||
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren<SpriteRenderer>(true);
|
||||
@@ -350,11 +395,10 @@ public static class RoleSettingMenuPatches
|
||||
newOpt.LabelBackground.enabled = false;
|
||||
__instance.advancedSettingChildren.Add(newOpt);
|
||||
|
||||
num += -0.45f;
|
||||
newOpt.gameObject.SetActive(false);
|
||||
newOpt.Initialize();
|
||||
}
|
||||
|
||||
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
|
||||
__instance.scrollBar.ScrollToTop();
|
||||
}
|
||||
|
||||
@@ -478,6 +522,10 @@ public static class RoleSettingMenuPatches
|
||||
roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor;
|
||||
roleOptionSetting.OnValueChanged = new Action<OptionBehaviour>(ValueChanged);
|
||||
roleOptionSetting.SetClickMask(__instance.ButtonClickMask);
|
||||
roleOptionSetting.ChanceMinusBtn.SetInteractable(true);
|
||||
roleOptionSetting.ChancePlusBtn.SetInteractable(true);
|
||||
roleOptionSetting.CountMinusBtn.SetInteractable(true);
|
||||
roleOptionSetting.CountPlusBtn.SetInteractable(true);
|
||||
__instance.roleChances.Add(roleOptionSetting);
|
||||
|
||||
roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f);
|
||||
@@ -485,8 +533,8 @@ public static class RoleSettingMenuPatches
|
||||
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
|
||||
|
||||
if (GameSettingMenuPatches.SelectedMod is null ||
|
||||
GameSettingMenuPatches.SelectedMod.OptionGroups
|
||||
.Exists(x => x.OptionableType == role.GetType()))
|
||||
GameSettingMenuPatches.SelectedMod.InternalOptionGroups
|
||||
.Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible())))
|
||||
{
|
||||
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
|
||||
newButton.name = "ConfigButton";
|
||||
@@ -517,7 +565,7 @@ public static class RoleSettingMenuPatches
|
||||
roleOptionSetting.ChancePlusBtn.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1)
|
||||
if (index < GameSettingMenuPatches.SelectedMod?.InternalRoles.Count - 1)
|
||||
{
|
||||
ScrollerNum -= 0.43f;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using MiraAPI.Hud;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Voting;
|
||||
using Reactor.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
|
||||
namespace MiraAPI.Patches;
|
||||
@@ -68,6 +69,7 @@ internal static class PlayerControlPatches
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
|
||||
// ReSharper disable once InconsistentNaming
|
||||
// Note: This method is partially inlined in 2025.5.20 but we already use custom murder RPCs.
|
||||
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
|
||||
{
|
||||
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
|
||||
@@ -93,12 +95,19 @@ internal static class PlayerControlPatches
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!button.Enabled(__instance.Data?.Role))
|
||||
try
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!button.Enabled(__instance.Data?.Role))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
button.FixedUpdateHandler(__instance);
|
||||
button.FixedUpdateHandler(__instance);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to update custom button {button.GetType().Name}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,11 @@ public static class GameStartupPatch
|
||||
|
||||
_runOnce = true;
|
||||
|
||||
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return;
|
||||
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations)
|
||||
{
|
||||
CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key);
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace MiraAPI.Patches.Roles;
|
||||
/// <summary>
|
||||
/// Patches the haunt menu to show the actual role name rather then team.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetFilterText))]
|
||||
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))]
|
||||
// SetFilterText was INLINED
|
||||
public static class HauntMenuMinigamePatch
|
||||
{
|
||||
public static void Postfix(HauntMenuMinigame __instance)
|
||||
|
||||
@@ -14,6 +14,8 @@ public static class RoleManagerPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(RoleManager.SetRole))]
|
||||
// NOTE: As of 2025.5.20 update, this is INLINED in ONE place: GameManager::ReviveEveryoneFreeplay(void).
|
||||
// Should not affect normal gameplay, but may cause issues in Freeplay.
|
||||
public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
|
||||
{
|
||||
if (!targetPlayer)
|
||||
@@ -63,7 +65,7 @@ public static class RoleManagerPatches
|
||||
[HarmonyPatch(nameof(RoleManager.SelectRoles))]
|
||||
public static void ModifierSelectionPatches(RoleManager __instance)
|
||||
{
|
||||
if (!AmongUsClient.Instance.AmHost)
|
||||
if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AmongUs.GameOptions;
|
||||
using HarmonyLib;
|
||||
using Il2CppSystem.Collections.Generic;
|
||||
using MiraAPI.Roles;
|
||||
using Reactor.Utilities;
|
||||
|
||||
@@ -11,6 +12,23 @@ namespace MiraAPI.Patches.Roles;
|
||||
[HarmonyPatch(typeof(RoleOptionsCollectionV09))]
|
||||
public static class RoleOptionsCollectionPatch
|
||||
{
|
||||
/// <summary>
|
||||
/// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code.
|
||||
/// </summary>
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(RoleOptionsCollectionV09.AnyRolesEnabled))]
|
||||
public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV09 __instance, ref bool __result)
|
||||
{
|
||||
foreach (KeyValuePair<RoleTypes, RoleDataV09> keyValuePair in __instance.roles)
|
||||
{
|
||||
if (__instance.GetNumPerGame(keyValuePair.Key) > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the role chance for custom Launchpad roles based on config.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace MiraAPI.Patches;
|
||||
|
||||
[HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))]
|
||||
public static class ServerDropdownPatch
|
||||
{
|
||||
public static bool Prefix(ServerDropdown __instance)
|
||||
{
|
||||
var num = 0;
|
||||
__instance.background.size = new Vector2(8.4f, 4.8f);
|
||||
|
||||
foreach (var regionInfo in DestroyableSingleton<ServerManager>.Instance.AvailableRegions)
|
||||
{
|
||||
var findingGame = SceneManager.GetActiveScene().name is "FindAGame";
|
||||
|
||||
if (DestroyableSingleton<ServerManager>.Instance.CurrentRegion.Equals(regionInfo))
|
||||
{
|
||||
__instance.defaultButtonSelected = __instance.firstOption;
|
||||
__instance.firstOption.ChangeButtonText(
|
||||
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
|
||||
regionInfo.TranslateName,
|
||||
regionInfo.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
var region = regionInfo;
|
||||
var serverListButton = __instance.ButtonPool.Get<ServerListButton>();
|
||||
var x = num % 2 == 0 ? -2 : 2;
|
||||
if (findingGame)
|
||||
{
|
||||
x += 2;
|
||||
}
|
||||
var y = -0.55f * (num / 2);
|
||||
serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f);
|
||||
serverListButton.transform.localScale = Vector3.one;
|
||||
serverListButton.Text.text =
|
||||
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
|
||||
regionInfo.TranslateName,
|
||||
regionInfo.Name);
|
||||
serverListButton.Text.ForceMeshUpdate();
|
||||
serverListButton.Button.OnClick.RemoveAllListeners();
|
||||
serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); }));
|
||||
__instance.controllerSelectable.Add(serverListButton.Button);
|
||||
__instance.background.transform.localPosition = new Vector3(
|
||||
findingGame ? 2f : 0f,
|
||||
__instance.initialYPos + (-0.3f * (num / 2)),
|
||||
0f);
|
||||
__instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2)));
|
||||
num++;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,7 @@ internal static class MeetingHudPatches
|
||||
// TODO: figure out a way to do host-authorization since right now any player can send RpcCastVote
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(nameof(MeetingHud.CmdCastVote))]
|
||||
// Although this method is inlined in MeetingHud.Confirm, the next patch fixes that.
|
||||
public static bool CmdCastVoteOverridePatch(MeetingHud __instance, byte playerId, byte suspectIdx)
|
||||
{
|
||||
VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, playerId, suspectIdx);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace MiraAPI.PluginLoading;
|
||||
|
||||
public interface IMiraExtension
|
||||
{
|
||||
public Type GetBasePluginType();
|
||||
}
|
||||
@@ -2,8 +2,19 @@
|
||||
|
||||
namespace MiraAPI.PluginLoading;
|
||||
|
||||
/// <summary>
|
||||
/// The interface that all Mira plugins must implement.
|
||||
/// </summary>
|
||||
public interface IMiraPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name to display on the options menu.
|
||||
/// </summary>
|
||||
string OptionsTitleText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the BepInEx configuration file for the plugin.
|
||||
/// </summary>
|
||||
/// <returns>The BepInEx configuration file for the plugin.</returns>
|
||||
public ConfigFile GetConfigFile();
|
||||
}
|
||||
@@ -6,7 +6,9 @@ using MiraAPI.GameModes;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.Hud;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.Presets;
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
namespace MiraAPI.PluginLoading;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,61 +25,58 @@ public class MiraPluginInfo
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized.
|
||||
/// Gets a read only collection of this plugin's modifiers. This is not safe for modifiers with constructors.
|
||||
/// </summary>
|
||||
/// <returns>Readonly collection of modifiers.</returns>
|
||||
public IReadOnlyCollection<BaseModifier> GetModifiers()
|
||||
{
|
||||
return Modifiers.AsReadOnly();
|
||||
}
|
||||
public IReadOnlyCollection<BaseModifier> Modifiers { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Get a read only collection of this plugin's Option Groups.
|
||||
/// Gets a read only collection of this plugin's Option Groups.
|
||||
/// </summary>
|
||||
/// <returns>Readonly collection of option groups.</returns>
|
||||
public IReadOnlyCollection<AbstractOptionGroup> GetOptionGroups()
|
||||
{
|
||||
return OptionGroups.AsReadOnly();
|
||||
}
|
||||
public IReadOnlyCollection<AbstractOptionGroup> OptionGroups { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read only collection of this plugin's options.
|
||||
/// </summary>
|
||||
/// <returns>Read only collection of options.</returns>
|
||||
public IReadOnlyCollection<IModdedOption> GetOptions()
|
||||
{
|
||||
return Options.AsReadOnly();
|
||||
}
|
||||
public IReadOnlyCollection<IModdedOption> Options { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with.
|
||||
/// </summary>
|
||||
/// <returns>Read only dictionary of IDs and Roles.</returns>
|
||||
public ReadOnlyDictionary<ushort, RoleBehaviour> GetRoles()
|
||||
{
|
||||
return new ReadOnlyDictionary<ushort, RoleBehaviour>(CustomRoles);
|
||||
}
|
||||
public ReadOnlyDictionary<ushort, RoleBehaviour> Roles { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read only collection of this plugin's custom buttons.
|
||||
/// </summary>
|
||||
/// <returns>Read only collection of buttons.</returns>
|
||||
public IReadOnlyCollection<CustomActionButton> GetButtons()
|
||||
public IReadOnlyCollection<CustomActionButton> Buttons { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read only dictionary of this plugin's custom game modes.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<OptionPreset> Presets { get; internal set; } = null!;
|
||||
|
||||
internal void SavePublicCollections()
|
||||
{
|
||||
return Buttons.AsReadOnly();
|
||||
Presets = [..InternalPresets];
|
||||
Modifiers = [..InternalModifiers];
|
||||
OptionGroups = [..InternalOptionGroups];
|
||||
Options = [..InternalOptions];
|
||||
Roles = new ReadOnlyDictionary<ushort, RoleBehaviour>(InternalRoles);
|
||||
Buttons = [..InternalButtons];
|
||||
}
|
||||
|
||||
internal List<AbstractOptionGroup> OptionGroups { get; } = [];
|
||||
internal List<OptionPreset> InternalPresets { get; } = [];
|
||||
|
||||
internal List<IModdedOption> Options { get; } = [];
|
||||
internal List<AbstractOptionGroup> InternalOptionGroups { get; } = [];
|
||||
|
||||
internal List<BaseModifier> Modifiers { get; } = [];
|
||||
internal List<IModdedOption> InternalOptions { get; } = [];
|
||||
|
||||
internal Dictionary<ushort, RoleBehaviour> CustomRoles { get; } = [];
|
||||
internal List<BaseModifier> InternalModifiers { get; } = [];
|
||||
|
||||
internal Dictionary<int, CustomGameMode> GameModes { get; } = [];
|
||||
internal Dictionary<ushort, RoleBehaviour> InternalRoles { get; } = [];
|
||||
|
||||
internal List<CustomActionButton> Buttons { get; } = [];
|
||||
internal Dictionary<int, CustomGameMode> InternalGameModes { get; } = [];
|
||||
|
||||
internal List<CustomActionButton> InternalButtons { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using BepInEx;
|
||||
using BepInEx.Unity.IL2CPP;
|
||||
using MiraAPI.Colors;
|
||||
using MiraAPI.Events;
|
||||
@@ -13,6 +12,7 @@ using MiraAPI.GameOptions;
|
||||
using MiraAPI.GameOptions.Attributes;
|
||||
using MiraAPI.Hud;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.Presets;
|
||||
using MiraAPI.Roles;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Networking;
|
||||
@@ -27,8 +27,9 @@ public sealed class MiraPluginManager
|
||||
{
|
||||
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
|
||||
|
||||
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values];
|
||||
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = new();
|
||||
internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!;
|
||||
|
||||
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = [];
|
||||
internal static MiraPluginManager Instance { get; private set; } = new();
|
||||
|
||||
internal void Initialize()
|
||||
@@ -36,29 +37,101 @@ public sealed class MiraPluginManager
|
||||
Instance = this;
|
||||
IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) =>
|
||||
{
|
||||
if (plugin is IMiraPlugin miraPlugin)
|
||||
if (plugin is not IMiraPlugin miraPlugin)
|
||||
{
|
||||
var miraInfo = new MiraPluginInfo(miraPlugin, pluginInfo);
|
||||
RegisterPlugin(miraInfo, assembly);
|
||||
_registeredPlugins.Add(assembly, miraInfo);
|
||||
return;
|
||||
}
|
||||
if (plugin is IMiraExtension miraExtension)
|
||||
|
||||
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
|
||||
var roles = new List<Type>();
|
||||
|
||||
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
|
||||
info.PluginConfig.SaveOnConfigSet = false;
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
var basePlugin = miraExtension.GetBasePluginType();
|
||||
var basePluginInfo = _registeredPlugins.Values.FirstOrDefault(x => x.MiraPlugin.GetType() == basePlugin);
|
||||
if (basePluginInfo == null)
|
||||
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Base plugin {basePlugin.Name} not found for extension {pluginInfo.Metadata.GUID}.");
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisterPlugin(basePluginInfo, assembly);
|
||||
foreach (var method in type.GetMethods())
|
||||
{
|
||||
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
|
||||
if (eventAttribute == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var paramType = parameters[0].ParameterType;
|
||||
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
|
||||
}
|
||||
|
||||
if (RegisterModifier(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterOptions(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterRole(type, info, out var role))
|
||||
{
|
||||
roles.Add(role);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterButton(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterGameOver(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisterColorClasses(type);
|
||||
}
|
||||
|
||||
info.PluginConfig.Save();
|
||||
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
|
||||
|
||||
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
|
||||
QueuedRoleRegistrations.Add(info, roles);
|
||||
|
||||
_registeredPlugins.Add(assembly, info);
|
||||
|
||||
info.SavePublicCollections();
|
||||
PresetManager.CreateDefaultPreset(info);
|
||||
PresetManager.LoadPresets(info);
|
||||
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
|
||||
};
|
||||
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
|
||||
IL2CPPChainloader.Instance.Finished += () =>
|
||||
{
|
||||
// Save all buttons into a read-only collection for easy access
|
||||
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
|
||||
|
||||
// Cache all the registered plugins into an array for easy access
|
||||
RegisteredPlugins = [.._registeredPlugins.Values];
|
||||
|
||||
ModifierManager.Modifiers = new ReadOnlyCollection<BaseModifier>(ModifierManager.InternalModifiers);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,84 +145,6 @@ public sealed class MiraPluginManager
|
||||
return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId);
|
||||
}
|
||||
|
||||
private void RegisterPlugin(MiraPluginInfo info, Assembly assembly)
|
||||
{
|
||||
var roles = new List<Type>();
|
||||
|
||||
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
|
||||
info.PluginConfig.SaveOnConfigSet = false;
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var method in type.GetMethods())
|
||||
{
|
||||
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
|
||||
if (eventAttribute == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var paramType = parameters[0].ParameterType;
|
||||
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
|
||||
}
|
||||
|
||||
if (RegisterModifier(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterOptions(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterRole(type, info, out var role))
|
||||
{
|
||||
roles.Add(role);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterButton(type, info))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RegisterGameOver(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisterColorClasses(type);
|
||||
}
|
||||
|
||||
info.PluginConfig.Save();
|
||||
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
|
||||
|
||||
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
|
||||
QueuedRoleRegistrations.TryAdd(info, []);
|
||||
QueuedRoleRegistrations[info].AddRange(roles);
|
||||
|
||||
Logger<MiraApiPlugin>.Info($"Registering mod {info.PluginId} with Mira API.");
|
||||
}
|
||||
|
||||
private static bool RegisterGameOver(Type type)
|
||||
{
|
||||
try
|
||||
@@ -200,7 +195,7 @@ public sealed class MiraPluginManager
|
||||
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
|
||||
}
|
||||
|
||||
foreach (var field in type.GetFields().Where(f=>f.FieldType.IsAssignableTo(typeof(IModdedOption))))
|
||||
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for options.");
|
||||
}
|
||||
@@ -271,7 +266,7 @@ public sealed class MiraPluginManager
|
||||
PaletteManager.CustomColors.Add(color);
|
||||
}
|
||||
|
||||
foreach (var field in type.GetFields().Where(f=>f.FieldType.IsAssignableTo(typeof(CustomColor))))
|
||||
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for colors.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Linq;
|
||||
using BepInEx.Configuration;
|
||||
using MiraAPI.GameOptions;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Roles;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Presets;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a preset of game options that can be applied to the game.
|
||||
/// </summary>
|
||||
public class OptionPreset
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the preset.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin associated with the preset.
|
||||
/// </summary>
|
||||
public MiraPluginInfo Plugin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration file of the plugin associated with the preset.
|
||||
/// </summary>
|
||||
public ConfigFile PluginConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration file for the preset.
|
||||
/// </summary>
|
||||
public ConfigFile PresetConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the button associated with the preset in the UI.
|
||||
/// </summary>
|
||||
public GameObject? PresetButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetConfig)
|
||||
{
|
||||
Name = name;
|
||||
Plugin = plugin;
|
||||
PluginConfig = plugin.PluginConfig;
|
||||
PresetConfig = presetConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the preset by applying the values from the preset configuration to the plugin configuration.
|
||||
/// </summary>
|
||||
public void LoadPreset()
|
||||
{
|
||||
PresetConfig.Reload();
|
||||
foreach (var option in Plugin.InternalOptions)
|
||||
{
|
||||
option.LoadFromPreset(PresetConfig);
|
||||
}
|
||||
|
||||
ModdedOptionsManager.SyncAllOptions();
|
||||
|
||||
foreach (var role in Plugin.InternalRoles.Values.OfType<ICustomRole>())
|
||||
{
|
||||
role.LoadFromPreset(PresetConfig);
|
||||
}
|
||||
|
||||
CustomRoleManager.SyncAllRoleSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BepInEx.Configuration;
|
||||
using MiraAPI.PluginLoading;
|
||||
using MiraAPI.Roles;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Presets;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to manage and load game option presets for plugins.
|
||||
/// </summary>
|
||||
public static class PresetManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the directory where the presets are stored.
|
||||
/// </summary>
|
||||
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
|
||||
|
||||
internal static void CreateDefaultPreset(MiraPluginInfo plugin)
|
||||
{
|
||||
var presetPath = Path.Combine(PresetDirectory, plugin.PluginId);
|
||||
if (!Directory.Exists(presetPath))
|
||||
{
|
||||
Directory.CreateDirectory(presetPath);
|
||||
}
|
||||
|
||||
var presetConfig = new ConfigFile(Path.Combine(presetPath, "Default.cfg"), false);
|
||||
|
||||
foreach (var option in plugin.InternalOptions.Where(x => x.IncludeInPreset))
|
||||
{
|
||||
option.SaveToPreset(presetConfig, true);
|
||||
}
|
||||
|
||||
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>())
|
||||
{
|
||||
role.SaveToPreset(presetConfig);
|
||||
}
|
||||
|
||||
presetConfig.Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the presets for the specified plugin by reading the configuration files from the preset directory.
|
||||
/// </summary>
|
||||
/// <param name="plugin">>The plugin for which the presets should be loaded.</param>
|
||||
public static void LoadPresets(MiraPluginInfo plugin)
|
||||
{
|
||||
foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton))
|
||||
{
|
||||
if (btn != null)
|
||||
{
|
||||
Object.DestroyImmediate(btn);
|
||||
}
|
||||
}
|
||||
|
||||
plugin.InternalPresets.Clear();
|
||||
if (!Directory.Exists(PresetDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(PresetDirectory);
|
||||
}
|
||||
|
||||
var pluginPresetPath = Path.Combine(PresetDirectory, plugin.PluginId);
|
||||
if (!Directory.Exists(pluginPresetPath))
|
||||
{
|
||||
Directory.CreateDirectory(pluginPresetPath);
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
Logger<MiraApiPlugin>.Info($"Loading preset file {fileName}");
|
||||
var presetName = Path.GetFileNameWithoutExtension(file);
|
||||
var presetConfig = new ConfigFile(file, false)
|
||||
{
|
||||
SaveOnConfigSet = false,
|
||||
};
|
||||
|
||||
foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset))
|
||||
{
|
||||
option.Bind(presetConfig);
|
||||
}
|
||||
|
||||
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
|
||||
{
|
||||
role.BindConfig(presetConfig);
|
||||
}
|
||||
|
||||
presetConfig.Save();
|
||||
|
||||
plugin.InternalPresets.Add(new OptionPreset(presetName, plugin, presetConfig));
|
||||
}
|
||||
plugin.Presets = [..plugin.InternalPresets];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using MiraAPI.Utilities;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities.Attributes;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
[RegisterInIl2Cpp]
|
||||
#pragma warning disable S3903
|
||||
#pragma warning disable CA1050
|
||||
public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
|
||||
#pragma warning restore CA1050
|
||||
#pragma warning restore S3903
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private TextMeshPro textBoxText;
|
||||
private TextBoxTMP textBox;
|
||||
|
||||
private Action<string> onSave;
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
|
||||
// Cleanup holder object
|
||||
public void OnDestroy()
|
||||
{
|
||||
transform.parent.gameObject.Destroy();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var textboxHolder = transform.GetChild(1).GetChild(1);
|
||||
var saveButton = transform.FindChild("SaveButton").GetComponent<PassiveButton>();
|
||||
var closeButton = transform.FindChild("CloseButton").GetComponent<PassiveButton>();
|
||||
textBox = textboxHolder.GetChild(0).GetComponent<TextBoxTMP>();
|
||||
textBoxText = textBox.transform.GetChild(0).GetComponent<TextMeshPro>();
|
||||
|
||||
saveButton.OnClick = new Button.ButtonClickedEvent();
|
||||
saveButton.OnClick.AddListener((UnityAction)(() =>
|
||||
{
|
||||
onSave.Invoke(textBoxText.text);
|
||||
this.BaseClose();
|
||||
}));
|
||||
|
||||
closeButton.OnClick = new Button.ButtonClickedEvent();
|
||||
closeButton.OnClick.AddListener((UnityAction)(() =>
|
||||
{
|
||||
this.BaseClose();
|
||||
}));
|
||||
|
||||
textBox.OnChange = new Button.ButtonClickedEvent();
|
||||
textBox.OnChange.AddListener((UnityAction)(() =>
|
||||
{
|
||||
textBoxText.text = textBoxText.text.Replace(" ", string.Empty);
|
||||
}));
|
||||
|
||||
textBox.GetComponent<PassiveButton>().OnClick.AddListener((UnityAction)(() =>
|
||||
{
|
||||
textBox.GiveFocus();
|
||||
}));
|
||||
|
||||
foreach (var tmp in gameObject.GetComponentsInChildren<TextMeshPro>())
|
||||
{
|
||||
if (tmp.gameObject.transform.parent.name == "Textbox")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = tmp.text;
|
||||
tmp.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF RadialMenu Material\">{text}</font>";
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreatePopup(Action<string> saveAction)
|
||||
{
|
||||
// Because innerscuff doesnt account for Z value in Close animation
|
||||
var holder = new GameObject("PopupHolder")
|
||||
{
|
||||
transform =
|
||||
{
|
||||
parent = Camera.main!.transform,
|
||||
localPosition = new Vector3(0, -1f, -500),
|
||||
},
|
||||
};
|
||||
|
||||
var gameObject = Instantiate(MiraAssets.PresetSavePopup.LoadAsset(), holder.transform);
|
||||
var popup = gameObject.GetComponent<SavePresetPopup>();
|
||||
popup.onSave = saveAction;
|
||||
popup.Begin(null);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
Binary file not shown.
@@ -35,6 +35,16 @@ public static class CustomRoleManager
|
||||
public static readonly LoadableAsset<AudioClip> ImpostorIntroSound =
|
||||
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of custom roles as RoleBehaviour objects.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<RoleBehaviour> CustomRoleBehaviours { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of custom roles as ICustomRole objects.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ICustomRole> CustomMiraRoles { get; private set; } = [];
|
||||
|
||||
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
|
||||
internal static readonly Dictionary<Type, ushort> RoleIds = [];
|
||||
|
||||
@@ -53,6 +63,9 @@ public static class CustomRoleManager
|
||||
{
|
||||
RoleManager.GhostRoles.Add(role.Role);
|
||||
}
|
||||
|
||||
CustomRoleBehaviours = CustomRoles.Values.ToList();
|
||||
CustomMiraRoles = CustomRoles.Values.OfType<ICustomRole>().ToList();
|
||||
}
|
||||
|
||||
internal static void RegisterRoleTypes(List<Type> roles, MiraPluginInfo pluginInfo)
|
||||
@@ -72,7 +85,7 @@ public static class CustomRoleManager
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginInfo.CustomRoles.Add((ushort)role.Role, role);
|
||||
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
|
||||
}
|
||||
|
||||
pluginInfo.PluginConfig.Save();
|
||||
@@ -161,7 +174,7 @@ public static class CustomRoleManager
|
||||
/// <returns>A MiraPluginInfo object representing the parent mod of the role.</returns>
|
||||
public static MiraPluginInfo FindParentMod(ICustomRole role)
|
||||
{
|
||||
return MiraPluginManager.Instance.RegisteredPlugins().First(plugin => plugin.CustomRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
|
||||
return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,7 +249,7 @@ public static class CustomRoleManager
|
||||
// we dont know how other plugins handle their configs
|
||||
// this way, all the options are saved at once, instead of one by one
|
||||
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
|
||||
plugin.PluginConfig.SaveOnConfigSet = false;
|
||||
@@ -271,7 +284,7 @@ public static class CustomRoleManager
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
|
||||
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
|
||||
{
|
||||
plugin.PluginConfig.Save();
|
||||
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using AmongUs.GameOptions;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Roles;
|
||||
@@ -42,7 +43,7 @@ public static class CustomRoleUtils
|
||||
public static StringBuilder CreateForRole(ICustomRole role)
|
||||
{
|
||||
var taskStringBuilder = new StringBuilder();
|
||||
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
|
||||
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}Your role is <b>{role.RoleName}.</b></color>");
|
||||
taskStringBuilder.Append("<size=70%>");
|
||||
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
|
||||
return taskStringBuilder;
|
||||
@@ -56,7 +57,21 @@ public static class CustomRoleUtils
|
||||
public static LoadableAsset<AudioClip>? GetIntroSound(RoleTypes roleType)
|
||||
{
|
||||
var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType);
|
||||
if (role is ICustomRole customRole) return customRole.Configuration.IntroSound;
|
||||
if (role is ICustomRole customRole)
|
||||
{
|
||||
return customRole.Configuration.IntroSound;
|
||||
}
|
||||
|
||||
return new PreloadedAsset<AudioClip>(role!.IntroSound);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a role is a custom role or not.
|
||||
/// </summary>
|
||||
/// <param name="role">The RoleBehaviour to check.</param>
|
||||
/// <returns>True if the role is a custom role, false otherwise.</returns>
|
||||
public static bool IsCustomRole(this RoleBehaviour role)
|
||||
{
|
||||
return CustomRoleManager.CustomRoles.ContainsKey((ushort)role.Role);
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,45 @@ public interface ICustomRole : IOptionable
|
||||
internal ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}");
|
||||
internal ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}");
|
||||
|
||||
/// <summary>
|
||||
/// Binds the configuration options for this role to the provided ConfigFile.
|
||||
/// </summary>
|
||||
/// <param name="config">The ConfigFile to bind the options to.</param>
|
||||
public virtual void BindConfig(ConfigFile config)
|
||||
{
|
||||
config.Bind(NumConfigDefinition, Configuration.DefaultRoleCount);
|
||||
config.Bind(ChanceConfigDefinition, Configuration.DefaultChance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the role's configuration to a preset ConfigFile.
|
||||
/// </summary>
|
||||
/// <param name="presetConfig">The ConfigFile to save the preset configuration to.</param>
|
||||
/// <param name="useDefault">Whether to use the default values for the configuration.</param>
|
||||
public virtual void SaveToPreset(ConfigFile presetConfig, bool useDefault=false)
|
||||
{
|
||||
BindConfig(presetConfig);
|
||||
presetConfig[NumConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultRoleCount : GetCount();
|
||||
presetConfig[ChanceConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultChance : GetChance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the role's configuration from a preset ConfigFile.
|
||||
/// </summary>
|
||||
/// <param name="presetConfig">The ConfigFile containing the preset configuration.</param>
|
||||
public virtual void LoadFromPreset(ConfigFile presetConfig)
|
||||
{
|
||||
if (presetConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry<int> numEntry))
|
||||
{
|
||||
SetCount(numEntry.Value);
|
||||
}
|
||||
|
||||
if (presetConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry<int> chanceEntry))
|
||||
{
|
||||
SetChance(chanceEntry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the role chance option.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MiraAPI.Roles;
|
||||
|
||||
public enum RoleContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Reactor.Utilities.Extensions;
|
||||
|
||||
namespace MiraAPI.Utilities.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper class to turn any UnityEngine.Object into a <see cref="LoadableAsset{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the asset to be loaded. Must be a subclass of <see cref="UnityEngine.Object"/>.</typeparam>
|
||||
public class LoadableAssetWrapper<T> : LoadableAsset<T> where T : UnityEngine.Object
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoadableAssetWrapper{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to wrap. This should be a reference to a UnityEngine.Object.</param>
|
||||
public LoadableAssetWrapper(T asset)
|
||||
{
|
||||
LoadedAsset = asset.DontDestroy();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T LoadAsset()
|
||||
{
|
||||
return LoadedAsset ?? throw new System.InvalidOperationException("The asset could not be loaded!");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,23 @@ namespace MiraAPI.Utilities.Assets;
|
||||
/// </summary>
|
||||
public static class MiraAssets
|
||||
{
|
||||
static MiraAssets()
|
||||
{
|
||||
var boxTex = SpriteTools.LoadTextureFromResourcePath(
|
||||
"MiraAPI.Resources.RoundedBox.png",
|
||||
System.Reflection.Assembly.GetCallingAssembly());
|
||||
var boxSprite = Sprite.Create(
|
||||
boxTex,
|
||||
new Rect(0, 0, boxTex.width, boxTex.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
100f,
|
||||
0U,
|
||||
SpriteMeshType.Tight,
|
||||
new Vector4(20, 20, 20, 20));
|
||||
|
||||
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color used for teal highlighting in UI.
|
||||
/// </summary>
|
||||
@@ -23,11 +40,31 @@ public static class MiraAssets
|
||||
/// </summary>
|
||||
public static LoadableAsset<GameObject> ModifierDisplay { get; } = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Popup prefab for saving presets.
|
||||
/// </summary>
|
||||
public static LoadableAsset<GameObject> PresetSavePopup { get; } = new LoadableBundleAsset<GameObject>("PresetSavePopup", MiraAssetBundle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Refresh Icon sprite.
|
||||
/// </summary>
|
||||
public static LoadableAsset<Sprite> RefreshIcon { get; } = new LoadableBundleAsset<Sprite>("refresh", MiraAssetBundle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Folder Icon sprite.
|
||||
/// </summary>
|
||||
public static LoadableAsset<Sprite> FolderIcon { get; } = new LoadableBundleAsset<Sprite>("freePlay_folderTaskRoom", MiraAssetBundle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the empty sprite asset.
|
||||
/// </summary>
|
||||
public static LoadableResourceAsset Empty { get; } = new("MiraAPI.Resources.Empty.png");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RoundedBox sprite, which is a rounded rectangle used for UI elements.
|
||||
/// </summary>
|
||||
public static LoadableAsset<Sprite> RoundedBox { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Next Button sprite.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,14 +11,13 @@ namespace MiraAPI.Utilities.Assets;
|
||||
public static class SpriteTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Load a sprite from a resource path.
|
||||
/// Loads and returns a texture from a resource path using the specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">The path to the resource.</param>
|
||||
/// <param name="assembly">The assembly containing the resource.</param>
|
||||
/// <param name="pixelsPerUnit">The pixels per unit for the loaded sprite.</param>
|
||||
/// <returns>A sprite made from the resource.</returns>
|
||||
/// <exception cref="Exception">The resource cannot be found.</exception>
|
||||
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
|
||||
/// <param name="resourcePath">The path to the resource within the assembly.</param>
|
||||
/// <param name="assembly">The assembly from which to load the resource.</param>
|
||||
/// <returns>A <see cref="Texture2D"/> object loaded from the specified resource path.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the resource cannot be found in the specified assembly.</exception>
|
||||
public static Texture2D LoadTextureFromResourcePath(string resourcePath, Assembly assembly)
|
||||
{
|
||||
var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false);
|
||||
var myStream = assembly.GetManifestResourceStream(resourcePath);
|
||||
@@ -33,6 +32,19 @@ public static class SpriteTools
|
||||
}
|
||||
|
||||
tex.name = resourcePath;
|
||||
return tex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and returns a <see cref="Sprite"/> from a resource path using the specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">The path to the resource within the assembly.</param>
|
||||
/// <param name="assembly">The assembly from which to load the resource.</param>
|
||||
/// <param name="pixelsPerUnit">The number of pixels per unit for the sprite.</param>
|
||||
/// <returns>A <see cref="Sprite"/> object created from the texture loaded from the specified resource path.</returns>
|
||||
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
|
||||
{
|
||||
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
|
||||
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
|
||||
sprite.name = resourcePath;
|
||||
return sprite;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MiraAPI.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for IEnumerable.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Shuffle the elements of the source enumerable using a new instance of Random.
|
||||
/// </summary>
|
||||
/// <param name="source">The source enumerable.</param>
|
||||
/// <typeparam name="T">The type of the elements.</typeparam>
|
||||
/// <returns>>The shuffled enumerable.</returns>
|
||||
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
|
||||
{
|
||||
return source.Shuffle(new Random());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuffle the elements of the source enumerable using the provided Random instance.
|
||||
/// </summary>
|
||||
/// <param name="source">The source enumerable.</param>
|
||||
/// <param name="rng">The Random instance to use for shuffling.</param>
|
||||
/// <typeparam name="T">The type of the elements.</typeparam>
|
||||
/// <returns>>The shuffled enumerable.</returns>
|
||||
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(rng);
|
||||
|
||||
return source.ShuffleIterator(rng);
|
||||
}
|
||||
|
||||
private static IEnumerable<T> ShuffleIterator<T>(this IEnumerable<T> source, Random rng)
|
||||
{
|
||||
var buffer = source.ToList();
|
||||
for (var i = 0; i < buffer.Count; i++)
|
||||
{
|
||||
var j = rng.Next(i, buffer.Count);
|
||||
yield return buffer[j];
|
||||
|
||||
buffer[j] = buffer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
+119
-22
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -41,6 +42,94 @@ public static class Extensions
|
||||
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used if you override Minigame.Close.
|
||||
/// </summary>
|
||||
/// <param name="self">The minigame.</param>
|
||||
public static void BaseClose(this Minigame self)
|
||||
{
|
||||
bool isComplete;
|
||||
if (self.amClosing == Minigame.CloseState.Closing)
|
||||
{
|
||||
UnityEngine.Object.Destroy(self.gameObject);
|
||||
return;
|
||||
}
|
||||
if (self.CloseSound && Constants.ShouldPlaySfx())
|
||||
{
|
||||
SoundManager.Instance.PlaySound(self.CloseSound, false, 1f, null);
|
||||
}
|
||||
if (PlayerControl.LocalPlayer.Data.Role.TeamType == RoleTeamTypes.Crewmate)
|
||||
{
|
||||
GameManager.Instance.LogicMinigame.OnMinigameClose();
|
||||
}
|
||||
if (PlayerControl.LocalPlayer)
|
||||
{
|
||||
PlayerControl.HideCursorTemporarily();
|
||||
}
|
||||
self.amClosing = Minigame.CloseState.Closing;
|
||||
self.logger.Info(string.Concat("Closing minigame ", self.GetType().Name));
|
||||
IAnalyticsReporter analytics = DestroyableSingleton<DebugAnalytics>.Instance.Analytics;
|
||||
NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data;
|
||||
TaskTypes taskType = self.TaskType;
|
||||
float realtimeSinceStartup = Time.realtimeSinceStartup - self.timeOpened;
|
||||
PlayerTask myTask = self.MyTask;
|
||||
if (myTask != null)
|
||||
{
|
||||
isComplete = myTask.IsComplete;
|
||||
}
|
||||
else
|
||||
{
|
||||
isComplete = false;
|
||||
}
|
||||
analytics.MinigameClosed(data, taskType, realtimeSinceStartup, isComplete);
|
||||
self.StartCoroutine(self.CoDestroySelf());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cooldown of a button with a formatted string.
|
||||
/// </summary>
|
||||
/// <param name="button">The ActionButton to set the cooldown for.</param>
|
||||
/// <param name="timer">The current timer value.</param>
|
||||
/// <param name="maxTimer">The maximum timer value.</param>
|
||||
/// <param name="format">The format string to use for the timer text.</param>
|
||||
public static void SetCooldownFormat(this ActionButton button, float timer, float maxTimer, string format="0")
|
||||
{
|
||||
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
|
||||
button.isCoolingDown = num > 0f;
|
||||
button.SetCooldownFill(num);
|
||||
if (button.isCoolingDown)
|
||||
{
|
||||
button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
button.cooldownTimerText.gameObject.SetActive(true);
|
||||
return;
|
||||
}
|
||||
button.cooldownTimerText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the fill-up variant of a cooldown button with a formatted string.
|
||||
/// </summary>
|
||||
/// <param name="button">The ActionButton to set the cooldown for.</param>
|
||||
/// <param name="timer">The current timer value.</param>
|
||||
/// <param name="maxTimer">The maximum timer value.</param>
|
||||
/// <param name="format">The format string to use for the timer text.</param>
|
||||
public static void SetFillUpFormat(this ActionButton button, float timer, float maxTimer, string format="0")
|
||||
{
|
||||
var num = Mathf.Clamp(timer / maxTimer, 0f, 1f);
|
||||
button.isCoolingDown = num > 0f;
|
||||
if (button.isCoolingDown && timer < 3f)
|
||||
{
|
||||
button.graphic.transform.localPosition = button.position + (Vector3)UnityEngine.Random.insideUnitCircle * 0.05f;
|
||||
button.cooldownTimerText.text = timer.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
button.cooldownTimerText.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
button.graphic.transform.localPosition = button.position;
|
||||
}
|
||||
button.SetCooldownFill(num);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
|
||||
/// </summary>
|
||||
@@ -65,6 +154,24 @@ public static class Extensions
|
||||
return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to convert a System.Collections.Generic.List to Il2cppSystem.
|
||||
/// </summary>
|
||||
/// <param name="systemList">The list.</param>
|
||||
/// <typeparam name="T">The type in the list.</typeparam>
|
||||
/// <returns>The converted list.</returns>
|
||||
public static Il2CppSystem.Collections.Generic.List<T> ToIl2CppList<T>(this List<T> systemList)
|
||||
{
|
||||
var il2cppList = new Il2CppSystem.Collections.Generic.List<T>();
|
||||
|
||||
foreach (var item in systemList)
|
||||
{
|
||||
il2cppList.Add(item);
|
||||
}
|
||||
|
||||
return il2cppList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a float is an integer.
|
||||
/// </summary>
|
||||
@@ -102,6 +209,12 @@ public static class Extensions
|
||||
return component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum value from a dictionary of integers, returning the key and value.
|
||||
/// </summary>
|
||||
/// <param name="self">The dictionary to search.</param>
|
||||
/// <param name="tie">Whether there is a tie for the maximum value.</param>
|
||||
/// <returns>The key-value pair with the maximum value.</returns>
|
||||
public static KeyValuePair<byte, int> MaxPair(this Dictionary<byte, int> self, out bool tie)
|
||||
{
|
||||
tie = true;
|
||||
@@ -121,6 +234,12 @@ public static class Extensions
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum value from a dictionary of floats, returning the key and value.
|
||||
/// </summary>
|
||||
/// <param name="self">The dictionary to search.</param>
|
||||
/// <param name="tie">Whether there is a tie for the maximum value.</param>
|
||||
/// <returns>The key-value pair with the maximum value.</returns>
|
||||
public static KeyValuePair<byte, float> MaxPair(this Dictionary<byte, float> self, out bool tie)
|
||||
{
|
||||
tie = true;
|
||||
@@ -342,18 +461,6 @@ public static class Extensions
|
||||
return new Color(color.r - amount, color.g - amount, color.b - amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an alternate color based on the original color.
|
||||
/// </summary>
|
||||
/// <param name="color">The original color.</param>
|
||||
/// <param name="amount">The amount to darken or lighten the original color by between 0.0 and 1.0.</param>
|
||||
/// <returns>An alternate color that has been darkened or lightened.</returns>
|
||||
[Obsolete("Use FindAlternateColor for WACG compliance.")]
|
||||
public static Color GetAlternateColor(this Color color, float amount = 0.45f)
|
||||
{
|
||||
return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightens a color by a specified amount.
|
||||
/// </summary>
|
||||
@@ -365,16 +472,6 @@ public static class Extensions
|
||||
return new Color(color.r + amount, color.g + amount, color.b + amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a color is dark.
|
||||
/// </summary>
|
||||
/// <param name="color">The color to check.</param>
|
||||
/// <returns>True if the color is dark, false otherwise.</returns>
|
||||
public static bool IsColorDark(this Color color)
|
||||
{
|
||||
return color.r < 0.5f && color is { g: < 0.5f, b: < 0.5f };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the nearest dead body to a player.
|
||||
/// </summary>
|
||||
|
||||
@@ -71,7 +71,7 @@ public static class VotingUtils
|
||||
/// <param name="source">The player who is sending the RPC. Should be the host.</param>
|
||||
/// <param name="voterId">The player who voted.</param>
|
||||
/// <param name="votedFor">The player who the voter voted for.</param>
|
||||
[MethodRpc((uint)MiraRpc.RemoveVote)]
|
||||
[MethodRpc((uint)MiraRpc.RemoveVote, SendImmediately = true)]
|
||||
public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor)
|
||||
{
|
||||
if (!source.IsHost()) return;
|
||||
@@ -107,7 +107,7 @@ public static class VotingUtils
|
||||
/// <param name="source">The player who sent this RPC.</param>
|
||||
/// <param name="srcPlayerId">The id of the player who casted the vote.</param>
|
||||
/// <param name="suspectPlayerId">The voted player's id.</param>
|
||||
[MethodRpc((uint)MiraRpc.CastVote)]
|
||||
[MethodRpc((uint)MiraRpc.CastVote, SendImmediately = true)]
|
||||
public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId)
|
||||
{
|
||||
CustomCastVote(srcPlayerId, suspectPlayerId);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
[](https://discord.gg/all-of-us-launchpad-794950428756410429)
|
||||
[](https://discord.gg/AEfHJGwggC)
|
||||
|
||||
> This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC.
|
||||
|
||||
# Mira API
|
||||
|
||||
@@ -8,27 +10,32 @@ A thorough, but simple, Among Us modding API and utility library that covers:
|
||||
- Modifiers
|
||||
- Buttons
|
||||
- Custom Colors
|
||||
- Events
|
||||
- Voting
|
||||
- Assets
|
||||
- Compatibility
|
||||
- ~~Game Modes~~ (coming soon)
|
||||
|
||||
Mira API strives to be simple and easy to use, while also using as many base game elements as possible. The result is a less intrusive, better modding API that covers general use cases.
|
||||
Mira API strives to be comprehensive, yet straightforward, while also using as many base game elements as possible.
|
||||
The result is a less intrusive, better modding API that covers general use cases.
|
||||
|
||||
**Join the [Discord](https://discord.gg/all-of-us-launchpad-794950428756410429) for support and to stay updated on the latest releases**
|
||||
**Join the [Discord](https://discord.gg/FYYqJU2bvp) for support and to stay updated on the latest releases**
|
||||
|
||||
# Usage
|
||||
|
||||
To start using Mira API, you need to:
|
||||
1. Add a reference to Mira API either through a [DLL](https://github.com/All-Of-Us-Mods/MiraAPI/releases), project reference, or [NuGet package](https://www.nuget.org/packages/AllOfUs.MiraAPI).
|
||||
2. Add a BepInDependency on your plugin class like this: `[BepInDependency(MiraApiPlugin.Id)]`
|
||||
3. Implement the IMiraPlugin interface on your plugin class.
|
||||
3. Implement the IMiraPlugin interface in your plugin class.
|
||||
|
||||
Mira API also depends on [Reactor](https://github.com/NuclearPowered/Reactor) in order to function properly! Do not forget to include it as a reference and `BepInDependency`!
|
||||
Mira API also depends on [Reactor](https://github.com/NuclearPowered/Reactor) to function properly!
|
||||
Remember to include it as a reference and `BepInDependency`!
|
||||
|
||||
For a full example, see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExamplePlugin.cs).
|
||||
|
||||
## Recommended Project Structure
|
||||
It is highly recommended to follow this project structure when using Mira API in order to keep your code clean and organized. You can also view the Example Mod in this repository for some guidance.
|
||||
It is highly recommended to follow this project structure when using Mira API to keep your code clean and organized.
|
||||
You can also view the Example Mod in this repository for some guidance.
|
||||
```
|
||||
MyMiraMod/
|
||||
├── Buttons/
|
||||
@@ -54,28 +61,36 @@ MyMiraMod/
|
||||
```
|
||||
|
||||
## Roles
|
||||
Roles are very simple in Mira API. There are 3 things you need to do to create a custom role:
|
||||
1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc)
|
||||
Roles are straightforward in Mira API. There are two things you need to do to create a custom role:
|
||||
1. Create a class that inherits from a base game role (like `CrewmateRole`, `ImpostorRole`, etc.)
|
||||
2. Implement the `ICustomRole` interface from Mira API.
|
||||
3. Add the `[RegisterCustomRole]` attribute to the class.
|
||||
|
||||
**Disclaimer: Make sure your plugin class has the following attribute `[ReactorModFlags(ModFlags.RequireOnAllClients)]` or else your roles will not register correctly.**
|
||||
|
||||
Note: For step 1, if you are making neutral roles, choose either `CrewmateRole` or `ImpostorRole` as the base depending on if it can kill or not!
|
||||
Note: For step 1, if you are making neutral roles,
|
||||
you can choose either `CrewmateRole` or `ImpostorRole`,
|
||||
or even `RoleBehaviour` if you are okay with starting from scratch.
|
||||
|
||||
Mira API handles everything else, from adding the proper options to the settings menu, to managing the role assignment at the start of the game. There are no extra steps on the developer's part.
|
||||
Mira API handles everything else, from adding the proper options to the settings menu,
|
||||
to managing the role assignment at the start of the game.
|
||||
There are no extra steps on the developer's part.
|
||||
|
||||
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Roles/CustomRole.cs) for a code example.
|
||||
|
||||
## Modifiers
|
||||
Mira API uses a different definition of 'modifiers' than other Among Us mods. For example, in Town Of Us, a modifier is an extra "ability" that is applied on top of the base role. However, in Mira API, modifiers are very flexible. A modifier is anything that "modifiers" a player's abilities or interactions.
|
||||
Mira API uses a different definition of 'modifiers' than other Among Us mods.
|
||||
For example, in Town Of Us, a modifier is an extra "ability" that is applied on top of the base role.
|
||||
However, in Mira API, modifiers are very flexible.
|
||||
A modifier is anything that "modifiers" a player's abilities or interactions.
|
||||
|
||||
Mira provides 3 classes for working with modifiers:
|
||||
Mira provides three classes for working with modifiers:
|
||||
- `BaseModifier`: The base for every modifier. You MUST add and remove this modifier from a player manually!
|
||||
- `TimedModifier`: A modifier that has a time limit. This modifier has to be added manually, but will automatically remove itself after the time limit
|
||||
- `TimedModifier`: A modifier that has a time limit. This modifier has to be added manually, but will automatically remove itself after the time limit.
|
||||
- `GameModifier`: This works like the typical TOU modifier, where it is automatically applied at the beginning of the game, then removed at the end.
|
||||
|
||||
Modifiers provide various overridable functions and properties for custom behaviour. They can also be used for "tagging" a player. You can check if a player has a modifier through the extension method `HasModifier` on a `PlayerControl` object.
|
||||
Modifiers provide various overridable functions and properties for custom behavior.
|
||||
They can also be used for "tagging" a player.
|
||||
You can check if a player has a modifier through the extension method `HasModifier` on a `PlayerControl` object.
|
||||
|
||||
To start using a modifier, pick one of the base classes above and create a class that inherits from it. Implement the properties and methods you would like, then add the `[RegisterModifier]` attribute to the class.
|
||||
|
||||
@@ -83,9 +98,17 @@ An example Game modifier can be found [here](https://github.com/All-Of-Us-Mods/M
|
||||
An example Timer modifier can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Modifiers/ModifierTimerExample.cs).
|
||||
|
||||
## Options
|
||||
Options are also very simple in Mira API. Mira API handles all the hard work behind the scenes, so developers only have to follow a few steps to create their custom options. The Options API is split up into Groups and Options. Every Option needs to be in a Group.
|
||||
Options are also simple in Mira API.
|
||||
Mira API handles all the hard work behind the scenes,
|
||||
so developers only have to follow a few steps to create their custom options.
|
||||
The Options API is split up into Groups and Options.
|
||||
Every Option needs to be in a Group.
|
||||
|
||||
To create a group, you need to create a class that inherits from the `AbstractOptionGroup` abstract class. Groups contain 4 properties, `GroupName`, `GroupColor`, `GroupVisible`, and `AdvancedRole`. Only the `GroupName` is required.
|
||||
To create a group, you need to create a class that inherits from the `AbstractOptionGroup` abstract class.
|
||||
Groups contain some properties for developers to control,
|
||||
including `GroupName`, `GroupColor`, `GroupVisible`, and `OptionableType`.
|
||||
The full list of properties can be found in the `AbstractOptionGroup` class.
|
||||
Only the `GroupName` is required.
|
||||
|
||||
Here is an example of a group class:
|
||||
```csharp
|
||||
@@ -141,46 +164,39 @@ And this is an example of a ModdedOption property:
|
||||
public ModdedToggleOption YeezusAbility { get; } = new ModdedToggleOption("Yeezus Ability", false);
|
||||
```
|
||||
|
||||
Here is a full list of ModdedOption classes you can use:
|
||||
There are currently three types of ModdedOptions you can create:
|
||||
- `ModdedEnumOption`
|
||||
- `ModdedNumberOption`
|
||||
- `ModdedToggleOption`
|
||||
|
||||
To see a full example of an options class, see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs).
|
||||
To see a full example of an option class,
|
||||
see [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/ExampleOptions.cs).
|
||||
|
||||
### Role Options
|
||||
|
||||
You can also specify a role type for an option or option group.
|
||||
|
||||
To set the role type for an entire group, set the `AdvancedRole` property on that group like this:
|
||||
To set the role type for an entire group, set the `OptionableType` property on that group like this:
|
||||
```csharp
|
||||
public class MyOptionsGroup : AbstractOptionGroup
|
||||
{
|
||||
public override string GroupName => "My Options";
|
||||
public override Type AdvancedRole => typeof(MyRole); // this is the role that will have these options
|
||||
public override Type? OptionableType => typeof(MyRole); // this is the role that will have these options
|
||||
|
||||
[ModdedNumberOption("Ability Uses", min: 0, max: 10)]
|
||||
public float AbilityUses { get; set; } = 5f;
|
||||
}
|
||||
```
|
||||
|
||||
To set the role type for individual options, specify the `roleType` parameter in the option like this:
|
||||
```csharp
|
||||
// this group doesnt specify a role, so it will show up in the global settings
|
||||
public class MyOptionsGroup : AbstractOptionGroup
|
||||
{
|
||||
public override string GroupName => "My Options";
|
||||
|
||||
// this option will only show up in the settings for MyRole
|
||||
[ModdedNumberOption("Ability Uses", min: 0, max: 10, roleType: typeof(MyRole))]
|
||||
public float AbilityUses { get; set; } = 5f;
|
||||
}
|
||||
```
|
||||
You can also use a modifier type for the `OptionableType` property.
|
||||
This will associate that option group with the specified modifier type
|
||||
and display it in the modifier options menu.
|
||||
|
||||
An example can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Options/Roles/CustomRoleSettings.cs).
|
||||
|
||||
## Custom Murders
|
||||
Mira API provides it's own implementation for murders. Our implementation allows for more customization on kills, and helps bypass server checks.
|
||||
Mira API provides its own implementation for murders.
|
||||
Our implementation allows for more customization on kills, and helps bypass server checks.
|
||||
You can use `PlayerControl.RpcCustomMurder` to perform a networked custom murder, or `PlayerControl.CustomMurder` to normally perform a custom murder.
|
||||
For example:
|
||||
```cs
|
||||
@@ -190,18 +206,23 @@ This will kill a player without creating a dead body and without teleporting the
|
||||
|
||||
## Buttons
|
||||
|
||||
Mira API provides a simple interface for adding ability buttons to the game. There is only 2 steps:
|
||||
1. Create a class that inherits from the `CustomActionButton` class and implement the properties and methods.
|
||||
2. Add the `[RegisterCustomButton]` attribute to the class.
|
||||
Mira API provides a simple interface for adding ability buttons to the game.
|
||||
|
||||
All other tasks and logic required to add the button to the game are handled by Mira API.
|
||||
The only thing you need to do is create a class
|
||||
that inherits from the `CustomActionButton` class and implement the properties and methods.
|
||||
|
||||
In case you need to access your `CustomActionButton` instance from another class, you can use the `CustomButtonSingleton` class like this:
|
||||
Mira API handles all other tasks and logic required to add the button to the game.
|
||||
|
||||
In case you need to access your `CustomActionButton` instance from another class,
|
||||
you can use the `CustomButtonSingleton` class like this:
|
||||
```csharp
|
||||
var myButton = CustomButtonSingleton<MyCoolButton>.Instance;
|
||||
```
|
||||
|
||||
The button API is simple, but provides a lot of flexibility. There are various methods you can override to customize the behaviour of your button. See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs) for a full list of methods you can override.
|
||||
The button API is straightforward, but provides a lot of flexibility.
|
||||
There are various methods you can override to customize the behavior of your button.
|
||||
See [this file](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI/Hud/CustomActionButton.cs)
|
||||
for a full list of methods you can override.
|
||||
|
||||
An example button can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/Buttons/ExampleButton.cs).
|
||||
|
||||
@@ -209,7 +230,7 @@ An example button can be found [here](https://github.com/All-Of-Us-Mods/MiraAPI/
|
||||
|
||||
Mira provides a simple Custom Color API that allows you to add custom player colors to the game.
|
||||
|
||||
Creating custom colors isn't difficult, but there are some requirements for your colors to be registered by Mira.
|
||||
Creating custom colors isn't difficult, but there are some requirements for Mira to register your colors.
|
||||
|
||||
1. Create a ***STATIC*** class to house all your `CustomColor` objects.
|
||||
2. Inside this class, create a `CustomColor` property for each color you intend to add.
|
||||
@@ -230,13 +251,17 @@ public static class MyCustomColors
|
||||
|
||||
## Assets
|
||||
|
||||
Mira API provides a simple, but expandable asset system. The core of the system is the `LoadableAsset<T>` class. This is a generic abstract class that provides a pattern for loading assets.
|
||||
Mira API provides a simple, but expandable asset system.
|
||||
The core of the system is the `LoadableAsset<T>` class.
|
||||
This is a generic abstract class that provides a pattern for loading assets.
|
||||
|
||||
Mira API comes with two asset loaders:
|
||||
Mira API comes with a few asset loaders:
|
||||
1. `LoadableBundleAsset<T>`: This is used for loading assets from AssetBundles.
|
||||
2. `LoadableResourceAsset`: This is used for loading **only sprites** from the Embedded Resources within a mod.
|
||||
2. `LoadableAddressableAsset<T>`: This is used for loading assets from Addressables.
|
||||
3. `LoadableResourceAsset`: This is used for loading **only sprites** from the Embedded Resources within a mod.
|
||||
4. `LoadableAudioResourceAsset`: This is used for loading **only audio clips** from the Embedded Resources within a mod.
|
||||
|
||||
The code below shows how to use these asset loaders:
|
||||
The code below shows how to use an asset loaders:
|
||||
```csharp
|
||||
// Load a sprite from an AssetBundle
|
||||
AssetBundle bundle = AssetBundleManager.Load("MyBundle"); // AssetBundleManager is a utility provided by Reactor
|
||||
@@ -252,7 +277,3 @@ Sprite button = buttonSpriteAsset.LoadAsset();
|
||||
You can view an example file [here](https://github.com/All-Of-Us-Mods/MiraAPI/blob/master/MiraAPI.Example/ExampleAssets.cs).
|
||||
|
||||
You can create your own asset loaders by inheriting from `LoadableAsset<T>` and implementing the `LoadAsset` method.
|
||||
|
||||
# Disclaimer
|
||||
|
||||
> This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC.
|
||||
Reference in new issue
Block a user