Compare commits

..
100 Commits
Author SHA1 Message Date
xChipseq 834d1d878e fix 2025-09-10 22:45:48 +02:00
xChipseq 790b37e8d3 cool role file icons 2025-09-10 22:40:09 +02:00
xChipseq dfa8b29710 update task adder (and improvements) 2025-09-10 22:07:47 +02:00
xChipseq 2c0042e6a8 bump to 2025.9.9 2025-09-09 22:58:18 +02:00
xChipseq 70f63f8b16 custom role fixes 2025-09-09 22:58:02 +02:00
xChipseq 03a3733966 fix body prefab 2025-09-09 22:57:19 +02:00
xChipseq 4b7fa906b9 fix misaligned options after labels 2025-09-07 22:49:23 +02:00
xChipseq b7173d18ab change mira config 2025-09-07 22:45:06 +02:00
xChipseq ffc7a1a4fd fix keybind icon clipping 2025-09-07 22:43:08 +02:00
XtraCube a1611f5f7a fix merge errors 2025-09-07 16:34:57 -04:00
XtraCube 1664aa1003 Merge branch 'fork/xChipseq/local-settings-rework' into dev
# Conflicts:
#	MiraAPI/MiraApiConfig.cs
#	MiraAPI/PluginLoading/MiraPluginManager.cs
#	MiraAPI/Utilities/Helpers.cs
2025-09-07 16:29:33 -04:00
xChipseq 94197e38dd cleanup 2025-09-07 20:47:59 +02:00
angxl wtf 97a0b2084a Merge pull request #61 from WanderingPix/timed-modifiers-taskadder-icon
Timed modifier QOL changes in Task Adder
2025-09-07 13:50:48 -04:00
Atony 1d87790212 Merge pull request #62 from All-Of-Us-Mods/vote-event-tweaks
Add Before and After Vote Events
2025-09-06 22:54:04 -05:00
Atony d8afc6887d Merge branch 'dev' into vote-event-tweaks 2025-09-06 14:36:22 -05:00
Atony c0a406610d Merge pull request #60 from WanderingPix/Roleicons-in-options-menu
Role icons in options menu
2025-09-06 14:32:14 -05:00
Atony d73c21936c Add Before and After vote events
Useful for local checks before a vote is placed (like with TOU Mira Prosecutor)
2025-09-06 14:19:17 -05:00
xChipseq 449192f0ef remove this annoying log 2025-09-06 19:02:40 +02:00
xChipseq f3614b9a5c fix null errors 2025-09-05 23:33:01 +02:00
angxl wtf a62c6d496c Merge pull request #57 from xChipseq/button-improvements
Custom Button Improvements + Major Keybind API rewrite
2025-09-04 19:02:07 -04:00
xChipseq bebd648691 remove null check, it's always none when null 2025-09-04 21:51:45 +02:00
xChipseq ecadfa86dd remove invoke log 2025-09-04 21:51:26 +02:00
WanderingPixel 5ad53f695f Update TaskAdderPatches.cs 2025-09-04 14:29:39 +01:00
WanderingPixel 31a9c0c98d Update TaskAdderPatches.cs 2025-09-04 13:23:31 +01:00
WanderingPixel ea389b986f Update TaskAdderPatches.cs 2025-09-04 12:58:09 +01:00
WanderingPixel ef85fab6a6 Update TaskAdderPatches.cs 2025-09-04 12:35:40 +01:00
WanderingPixel b2fdaaf89f Timed Modifier Enhancements in TaskAdderGame 2025-09-04 12:29:19 +01:00
WanderingPixel e6043b6f5f Fix Masking issue 2025-09-03 21:27:38 +01:00
WanderingPixel ffe0db0e9e Add role icons in role settings menu 2025-09-03 21:14:07 +01:00
xChipseq 705ef64b4d fix helper method returning null if no keybind 2025-09-02 22:27:04 +02:00
xChipseq ebdb4a09ca make number options accept floats 2025-09-02 19:24:34 +02:00
xChipseq ac113b6a7f add tab open method and fixes 2025-09-02 18:55:27 +02:00
xChipseq 1f6962efe9 add local setting buttons 2025-09-01 23:34:17 +02:00
xChipseq 6b37b9c09c add an error 2025-09-01 22:28:30 +02:00
xChipseq f515516937 remove the two million test tabs 2025-09-01 22:24:58 +02:00
xChipseq 8b2d24ccc1 more stuff 2025-09-01 22:22:32 +02:00
xChipseq 57bab258e5 enum and int options and lots of other stuff i forgot 2025-09-01 21:54:13 +02:00
ang-xd 04d5e72caa Add slider float value 2025-08-31 18:45:22 -04:00
ang-xd b4e6e448f2 Major work/rewrite on local settings 2025-08-31 18:04:52 -04:00
xChipseq b19a0178ad add a limit to multitarget buttons 2025-08-31 23:41:35 +02:00
xChipseq 8a71f13874 fix for keybind loading 2025-08-31 20:26:22 +02:00
xChipseq 809e7a07ab literally one dot 2025-08-31 20:21:50 +02:00
xChipseq 47394c6afe fix example mod 2025-08-31 18:06:53 +02:00
xChipseq a2a1e91439 replace EffectCancelable with a method 2025-08-31 18:03:17 +02:00
xChipseq 2f5ca32475 multitarget buttons 2025-08-31 17:59:38 +02:00
xChipseq 5fb59408fa wip 2025-08-31 17:51:02 +02:00
xChipseq 57c842ece6 Merge remote-tracking branch 'origin/local-settings-rework' into local-settings-rework 2025-08-30 23:08:55 +02:00
Chipseq 63e552245d Sync local settings branch with dev 2025-08-30 23:05:03 +02:00
xChipseq 43d7f80660 add show keybinds setting 2025-08-30 21:41:04 +02:00
xChipseq 45291b0e98 change default keybinds to make sense 2025-08-30 21:31:34 +02:00
xChipseq 45972f71a9 major rework of the keybinds api 2025-08-30 21:13:42 +02:00
xChipseq a6f7171b71 add PauseTimerInVent 2025-08-29 14:32:13 +02:00
xChipseq e6d3819517 add uses mode for buttons 2025-08-29 14:20:19 +02:00
xChipseq b97ada22bc allow the duration to be 0 2025-08-29 13:58:06 +02:00
xChipseq 5f67960b28 cancelable abilities 2025-08-29 13:38:35 +02:00
xChipseq b4e30b2b68 wip 2025-08-29 13:04:18 +02:00
ang-xd 2237d3a2df Additional changes to keybind icons 2025-08-28 12:33:19 -04:00
ang-xd e708c23f81 Fix checks and positions for keybinds 2025-08-28 11:26:45 -04:00
ang-xd 3ca33b1d86 Merge branch 'master' into dev 2025-08-28 11:08:57 -04:00
angxl wtf 8967348cd3 Merge pull request #55 from CallOfCreator/keybinds
Keybind Icons
2025-08-28 11:08:26 -04:00
angxl wtf 305d2c3714 Merge pull request #54 from AtonyGit/dev
Adjust Keybinds for Buttons + Add IntroRoleRevealEvent
2025-08-28 11:08:14 -04:00
Atony 85a0275eea Add IntroRoleRevealEvent
Also needs to be changed for the android branch
2025-08-24 19:48:43 -05:00
Atony f006deab3f Fix Send Immediately warning from Reactor 2025-08-24 18:33:52 -05:00
CallOfCreator a274f23e65 Merge pull request #1 from WanderingPix/keybinds
Add Keybind Icons
2025-08-21 11:36:08 -07:00
Atony f1a5aef581 Remove requirement but allow blank keybind 2025-08-17 20:52:46 -05:00
Atony ce15e93e59 Fix DefaultKeybind name + remove requirement
By default, buttons will use no keybind, and will not add a new keybind option in doing so.
2025-08-17 20:40:03 -05:00
XtraCube 08ca995eb1 Merge pull request #52 from All-Of-Us-Mods/dev
mira api 0.2.1
2025-08-17 16:39:35 -04:00
XtraCube 453259df98 Merge branch 'master' into dev 2025-08-17 16:36:16 -04:00
XtraCube 2ec3eec370 bump version 2025-08-17 16:34:56 -04:00
ang-xd 2ee8730ba4 WIP Left side Modifiers HUD option 2025-08-14 00:56:29 -04:00
XtraCube c89e5938cc Merge pull request #43 from CallOfCreator/keybinds
Implement Keybind API
2025-08-13 09:32:42 -04:00
WanderingPixel 513be01c02 Add Keybind icons for custom buttons 2025-08-09 22:12:15 +01:00
CallOfCreator 01fa1f0915 Update conflict system 2025-08-09 11:41:43 +00:00
CallOfCreator 355eef509b Add support for modifier keys 2025-08-06 18:03:48 +00:00
XtraCube 763a493283 Merge pull request #51 from xChipseq/dummy-improvements
Dummies Fix
2025-08-05 16:50:09 -04:00
xChipseq 68e421bf91 literally just a dot 2025-08-05 22:35:39 +02:00
xChipseq c343ab5e9f dummy fix 2025-08-05 22:34:00 +02:00
XtraCube 9c1ef70ea7 Merge pull request #50 from xChipseq/dummy-improvements
Dummy Voting Improvements
2025-08-04 21:19:18 -04:00
xChipseq c0c8fa08ff changed DidVote to check VotesRemaining instead of Votes 2025-08-04 20:58:24 +02:00
xChipseq e7fba386aa dummy changes 2025-08-04 20:53:50 +02:00
XtraCube 72e5e0c137 Merge pull request #49 from xChipseq/round-start-event
Invoke RoundStartEvent when starting freeplay
2025-08-04 12:30:09 -04:00
XtraCube 4422072060 Merge pull request #48 from AtonyGit/dev
Add BeforeRoundEvent for edge case scenarios
2025-08-04 12:29:51 -04:00
xChipseq 8145c8e599 patch 2025-08-04 15:33:04 +02:00
Atony afd6fbc3b6 Add BeforeRoundStartEvent 2025-08-02 14:24:53 -05:00
XtraCube 74741b41a7 Merge pull request #47 from WanderingPix/Ghosts-included-in-GetClosestPlayer
Add argument for PlayerControl.GetClosestPlayer() to include ghosts
2025-07-31 09:13:04 -04:00
WanderingPixel f98429b5f9 Update GetClosestPlayer method 2025-07-31 09:09:37 -04:00
XtraCube bf7edb30ab fix warnings 2025-07-20 07:30:38 -04:00
XtraCube b4527014b7 Merge pull request #44 from AtonyGit/dev
Allow modifiers to have freeplay colors
2025-07-16 14:41:01 -04:00
Atony 277391ef1c Add freeplay modifier colors 2025-07-11 22:30:09 -05:00
CallOfCreator dfe124d67a Add missing credit from previous commit 2025-07-11 18:06:06 +00:00
CallOfCreator 2b06633488 Cleanup 2025-07-11 18:00:58 +00:00
CallOfCreator 4784b1f0ad Rename CurrentKeybind to DefaultKeybind 2025-07-09 13:48:38 +00:00
CallOfCreator d9d0d6bc62 Implement Keybind API 2025-07-08 20:41:20 +00:00
XtraCube 6b3038f397 shuffle modifiers before assigning 2025-07-07 18:47:12 -04:00
XtraCube 4e1db039a0 Merge pull request #42 from AtonyGit/dev
Fix Regions Dropdown in "Find Game" & Add Tab Count for Cosmetics Menu
2025-07-05 15:36:08 -04:00
Atony 62bee24789 Fix Server Dropdown for Game Finder 2025-07-04 22:43:13 -05:00
Atony 2156c80467 Cosmetic Tab Count Improvements 2025-07-04 22:42:47 -05:00
XtraCube 0ff60071c2 fix canuse event 2025-06-30 20:47:17 -04:00
XtraCube de2f3f0d95 update assetbundle 2025-06-29 22:31:21 -04:00
XtraCube cd818a18cf Merge pull request #40 from All-Of-Us-Mods/dev
0.2.0 Update
2025-06-27 15:52:24 -04:00
107 changed files with 3477 additions and 589 deletions

No files matched your search

+2 -2
View File
@@ -2,12 +2,12 @@
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<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="AmongUs.GameLibs.Steam" Version="2025.9.9" 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" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(AmongUs)/BepInEx/plugins/" UseSymboliclinksIfPossible="true" />
</Target>
+1 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.2.0</VersionPrefix>
<VersionPrefix>0.2.1</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -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;
}
}
@@ -3,6 +3,7 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
@@ -15,10 +16,10 @@ public class FreezeButton : CustomActionButton<PlayerControl>
public override string Name => "Freeze";
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override int MaxUses => 1;
public override bool PauseTimerInVent => true;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility;
protected override void OnClick()
{
+2
View File
@@ -1,5 +1,6 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Modifiers;
using MiraAPI.Utilities.Assets;
using UnityEngine;
@@ -15,6 +16,7 @@ public class MeetingButton : CustomActionButton
public override int MaxUses => 3;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
public override MiraKeybind? Keybind => MiraGlobalKeybinds.ModifierPrimaryAbility;
public override bool Enabled(RoleBehaviour? role)
{
@@ -1,19 +1,19 @@
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.Keybinds;
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;
public override MiraKeybind Keybind => ExampleKeybinds.NeutralWinKeybind;
protected override void OnClick()
{
@@ -3,6 +3,7 @@ using MiraAPI.Example.Options.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
@@ -21,6 +22,7 @@ public class TeleportButton : CustomActionButton
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public override Color TextOutlineColor => new Color32(221, 176, 152, 255);
public static bool IsZoom { get; private set; }
public override MiraKeybind? Keybind => MiraGlobalKeybinds.PrimaryAbility;
public override bool Enabled(RoleBehaviour? role)
{
+35
View File
@@ -0,0 +1,35 @@
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
public class ThinkButton : CustomActionButton
{
public override string Name => "Think";
public override float Cooldown => 15f;
public override float EffectDuration => 10f;
public override int MaxUses => 1;
public override ButtonUsesMode UsesMode => ButtonUsesMode.PerRound;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
// This button does absolutely nothing besides changing its text
Button!.OverrideText("Thinking...");
}
public override void OnEffectEnd()
{
Button!.OverrideText("Think");
}
public override bool IsEffectCancellable()
{
return true;
}
public override bool Enabled(RoleBehaviour? role)
{
return role != null && role.IsImpostor;
}
}
+1 -1
View File
@@ -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);
+10
View File
@@ -0,0 +1,10 @@
using MiraAPI.Keybinds;
using Rewired;
namespace MiraAPI.Example;
[RegisterCustomKeybinds]
public static class ExampleKeybinds
{
public static MiraKeybind NeutralWinKeybind { get; } = new("Neutral Win", KeyboardKeyCode.C, [ModifierKey.Shift]);
}
+53
View File
@@ -0,0 +1,53 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Example;
public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config)
{
public override string TabName => "Mira Example";
protected override bool ShouldCreateLabels => false;
public override LocalSettingTabAppearance TabAppearance => new()
{
ToggleActiveColor = Color.blue,
ToggleInactiveColor = Color.red,
};
[LocalSettingsButton]
public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick);
[LocalToggleSetting]
public ConfigEntry<bool> ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true);
[LocalToggleSetting]
public ConfigEntry<bool> ExampleToggle2 { get; private set; } = config.Bind("General", "Example Bool 2", false);
[LocalSliderSetting(suffixType: MiraNumberSuffixes.Percent, formatString: "0", roundValue: true)]
public ConfigEntry<float> ExampleSlider { get; private set; } = config.Bind("General", "Slider", 50f);
[LocalNumberSetting]
public ConfigEntry<float> ExampleNumber { get; private set; } = config.Bind("General", "Example Number", 4f);
[LocalNumberSetting(min: 0.1f, max: 2.5f, increment: 0.1f, formatString: "0.0", suffixType: MiraNumberSuffixes.Multiplier)]
public ConfigEntry<float> ExampleFloatNumber { get; private set; } = config.Bind("General", "Example Multplier", 1f);
[LocalEnumSetting]
public ConfigEntry<ExampleEnumSetting> ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries);
private static void OnExampleButtonClick()
{
LocalSettingsTabSingleton<MiraApiSettings>.Instance.Open();
}
}
public enum ExampleEnumSetting
{
Cheeseburger,
Sandwich,
Fries,
ChickenNuggets,
}
+2 -1
View File
@@ -1,4 +1,5 @@
using BepInEx;
using System.Linq;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
@@ -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
{
@@ -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;
}
@@ -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)
-6
View File
@@ -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
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Event invoked on IntroCutscene.OnDestroy and ExileController.WrapUp to determine if it should run the round event or not.
/// </summary>
public class BeforeRoundStartEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController.
/// </summary>
public bool TriggeredByIntro { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeRoundStartEvent"/> class.
/// </summary>
/// <param name="triggeredByIntro">Whether the event was triggered by the intro or not.</param>
public BeforeRoundStartEvent(bool triggeredByIntro)
{
TriggeredByIntro = triggeredByIntro;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// The event that is invoked when the player's role is shown on the intro cutscene. Non cancelable.
/// </summary>
public class IntroRoleRevealEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the IntroCutscene.
/// </summary>
public IntroCutscene IntroCutscene { get; }
/// <summary>
/// Initializes a new instance of the <see cref="IntroRoleRevealEvent"/> class.
/// </summary>
/// <param name="introCutscene">The intro cutscene.</param>
public IntroRoleRevealEvent(IntroCutscene introCutscene)
{
IntroCutscene = introCutscene;
}
}
@@ -1,7 +1,7 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp.
/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp if the event isn't cancelled.
/// </summary>
public class RoundStartEvent : MiraEvent
{
@@ -0,0 +1,30 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Event that is invoked after the local player successfully votes a player or skips. This event is not cancelable.
/// </summary>
public class AfterVoteEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteArea VoteArea { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AfterVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteArea">The player vote area that was voted on.</param>
/// <param name="voter">The player who voted.</param>
public AfterVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter)
{
VoteArea = playerVoteArea;
Player = voter;
}
}
@@ -0,0 +1,30 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Event that is invoked before the local player confirms a vote on a player or when skipping. This event is cancelable.
/// </summary>
public class BeforeVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteArea VoteArea { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteArea">The player vote area that was voted on.</param>
/// <param name="voter">The player who voted.</param>
public BeforeVoteEvent(PlayerVoteArea playerVoteArea, PlayerControl voter)
{
VoteArea = playerVoteArea;
Player = voter;
}
}
@@ -0,0 +1,44 @@
using System;
using MiraAPI.Utilities;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Invoked before the dummy selects their vote. If canceled, the dummy won't vote.
/// </summary>
public class DummyVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the dummy that needs to vote.
/// </summary>
public DummyBehaviour Dummy { get; }
/// <summary>
/// Gets the instance of the dummy's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets or sets a value indicating whether the dummy can vote to skip the meeting.
/// </summary>
public bool CanSkip { get; set; }
/// <summary>
/// Gets or sets a predicate for validating whether the dummy can vote on a player.
/// By default, excludes dead and disconnected players.
/// </summary>
public Predicate<PlayerControl> PlayerIsValid { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="DummyVoteEvent"/> class.
/// </summary>
/// <param name="dummy">The voting dummy.</param>
public DummyVoteEvent(DummyBehaviour dummy)
{
Dummy = dummy;
VoteData = dummy.myPlayer.GetVoteData();
CanSkip = true;
PlayerIsValid = x => !x.Data.IsDead && !x.Data.Disconnected;
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace MiraAPI.Hud;
/// <summary>
/// The uses mode for custom buttons.
/// </summary>
public enum ButtonUsesMode
{
/// <summary>
/// Uses per game.
/// </summary>
PerGame,
/// <summary>
/// Uses per round.
/// </summary>
PerRound,
}
+82 -11
View File
@@ -2,9 +2,13 @@
using System.Globalization;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -32,6 +36,12 @@ public abstract class CustomActionButton
/// </summary>
public abstract float Cooldown { get; }
/// <summary>
/// Gets a value indicating whether the timer should stop decreasing when the player is venting.
/// Note that both cooldown decrease and effect timer decrease is disabled.
/// </summary>
public virtual bool PauseTimerInVent => false;
/// <summary>
/// Gets the sprite of the button. Use <see cref="LoadableResourceAsset"/> to load a sprite from a resource path. Use <see cref="LoadableBundleAsset{T}"/> to load a sprite from an asset bundle.
/// </summary>
@@ -43,7 +53,12 @@ public abstract class CustomActionButton
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
/// Gets a value indicating whether the button has an effect ability. By default true if <see cref="EffectDuration"/> is greater than 0.
/// </summary>
public virtual bool HasEffect => EffectDuration > 0;
/// <summary>
/// Gets the button's effect duration in seconds. If 0, the effect needs to be ended manually.
/// </summary>
public virtual float EffectDuration => 0;
@@ -52,6 +67,16 @@ public abstract class CustomActionButton
/// </summary>
public virtual int MaxUses => 0;
/// <summary>
/// Gets the value indicating uses mode.
/// </summary>
public virtual ButtonUsesMode UsesMode => ButtonUsesMode.PerGame;
/// <summary>
/// Gets the keybind for this button. If null, no keybind will be added.
/// </summary>
public virtual MiraKeybind? Keybind => null;
/// <summary>
/// Gets the button's text outline color.
/// </summary>
@@ -62,11 +87,6 @@ public abstract class CustomActionButton
/// </summary>
public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft;
/// <summary>
/// Gets a value indicating whether the button has an effect ability.
/// </summary>
public bool HasEffect => EffectDuration > 0;
/// <summary>
/// Gets a value indicating whether the button has limited uses.
/// </summary>
@@ -97,6 +117,16 @@ public abstract class CustomActionButton
/// </summary>
public ActionButton? Button { get; set; }
/// <summary>
/// Gets the gameObject used for the keybind icon.
/// </summary>
public GameObject? KeybindIcon { get; private set; }
/// <summary>
/// Gets or sets the keybind icon text.
/// </summary>
private TextMeshPro? KeybindText { get; set; }
/// <summary>
/// The method used to create the button.
/// </summary>
@@ -168,6 +198,26 @@ public abstract class CustomActionButton
}
}
}));
if (Keybind != null)
{
Keybind.OnActivate(() =>
{
if (Enabled(PlayerControl.LocalPlayer.Data.Role))
{
ClickHandler();
}
});
KeybindIcon =
Helpers.CreateKeybindIcon(
Button.gameObject,
Keybind.CurrentKey,
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
/// <summary>
@@ -354,6 +404,13 @@ public abstract class CustomActionButton
{
}
/// <summary>
/// Returns a value indicating whether the effect can be ended early by the player.
/// Always false by default.
/// </summary>
/// <returns>Can the effect be canceled.</returns>
public virtual bool IsEffectCancellable() => false;
/// <summary>
/// When the button is usable, this method is called to determine if the button can be clicked.
/// By default, it takes into account the timer, effect, and uses.
@@ -362,7 +419,7 @@ public abstract class CustomActionButton
/// <returns>A value that represents whether the button can be clicked or not.</returns>
public virtual bool CanClick()
{
return Timer <= 0 && !EffectActive && CanUse();
return (EffectActive ? IsEffectCancellable() : Timer <= 0) && CanUse();
}
/// <summary>
@@ -372,7 +429,8 @@ public abstract class CustomActionButton
/// <returns>A value that represents whether the button should light up or not.</returns>
public virtual bool CanUse()
{
return PlayerControl.LocalPlayer.moveable && (!LimitedUses || UsesLeft > 0);
return PlayerControl.LocalPlayer.moveable &&
((EffectActive && IsEffectCancellable()) || (!EffectActive && (!LimitedUses || UsesLeft > 0)));
}
/// <summary>
@@ -394,6 +452,12 @@ public abstract class CustomActionButton
/// </summary>
public virtual void ClickHandler()
{
if (EffectActive && IsEffectCancellable())
{
ResetCooldownAndOrEffect();
return;
}
if (!CanClick())
{
return;
@@ -426,14 +490,21 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Keybind != null && KeybindText != null)
{
KeybindText.text = Keybind.CurrentKey.ToString();
KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value);
}
if (Timer >= 0)
{
if (!TimerPaused)
if (!TimerPaused && (!PauseTimerInVent || !playerControl.inVent))
{
Timer -= Time.deltaTime;
}
}
else if (HasEffect && EffectActive)
else if (HasEffect && EffectActive && EffectDuration > 0)
{
EffectActive = false;
Timer = Cooldown;
@@ -451,7 +522,7 @@ public abstract class CustomActionButton
Button!.SetDisabled();
}
if (EffectActive)
if (EffectActive && EffectDuration > 0)
{
Button.SetFillUp(Timer, EffectDuration);
+1
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Reflection;
using MiraAPI.Events.Mira;
using MiraAPI.PluginLoading;
using UnityEngine;
namespace MiraAPI.Hud;
+78
View File
@@ -0,0 +1,78 @@
using System.Linq;
using HarmonyLib;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Hud;
/// <summary>
/// Custom action button that has multiple target objects.
/// </summary>
/// <typeparam name="T">The type of the target objects.</typeparam>
public abstract class MultiTargetButton<T> : CustomActionButton where T : MonoBehaviour
{
/// <summary>
/// Gets or sets the list of targets of the button.
/// </summary>
public T[] Targets { get; protected set; } = [];
/// <inheritdoc cref="CustomActionButton{T}.Distance"/>
public virtual float Distance => PlayerControl.LocalPlayer.Data.Role.GetAbilityDistance();
/// <summary>
/// Gets the maximum amount of targets the button can have.
/// </summary>
public virtual int MaxTargets { get; } = int.MaxValue;
/// <inheritdoc cref="CustomActionButton{T}.IsTargetValid"/>
public virtual bool IsTargetValid(T target)
{
return target != null;
}
/// <summary>
/// The method used to get the target objects.
/// </summary>
/// <returns>The list of target objects.</returns>
public abstract T[] GetTargets();
/// <inheritdoc cref="CustomActionButton{T}.SetOutline"/>
/// <param name="target">The target object to set the oultine.</param>
public abstract void SetOutline(T target, bool active);
/// <inheritdoc cref="CustomActionButton{T}.CanUse"/>
public override bool CanUse()
{
var newTargets = GetTargets();
foreach (var target in Targets)
{
if (!newTargets.Contains(target))
{
SetOutline(target, false);
}
}
Targets = newTargets
.Where(IsTargetValid)
.Take(MaxTargets)
.ToArray();
Targets.Do(t => SetOutline(t, true));
return base.CanUse() && Targets.Length > 0;
}
/// <inheritdoc cref="CustomActionButton{T}.CanClick"/>
public override bool CanClick()
{
return base.CanClick() && Targets.Length > 0;
}
/// <summary>
/// Use this to reset the button's targets after used.
/// </summary>
public virtual void ResetTarget()
{
Targets.Do(t => SetOutline(t, false));
Targets = [];
}
}
+94
View File
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Lets you register and manage mod keybinds with conflict checks and rebinding support.
/// </summary>
public static class KeybindManager
{
/// <summary>
/// Gets a list of all registered keybinds.
/// </summary>
public static List<MiraKeybind> Keybinds { get; } = new();
internal static void RewiredInit()
{
try
{
var instance = KeybindUtils.RewiredInputManager!;
foreach (var keybind in KeybindManager.Keybinds)
{
if (instance.userData.actions.ToArray().Any(x => x.name == keybind.Id))
{
Logger<MiraApiPlugin>.Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
continue;
}
keybind.RewiredInputAction = instance.userData.RegisterModBind(
keybind.Id,
keybind.Name,
keybind.SourcePluginName,
keybind.DefaultKey,
modifiers: keybind.ModifierKeys);
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while registering keybinds in Rewired: {e}");
}
}
/// <summary>
/// Returns all conflicts where keybinds use the same key.
/// </summary>
/// <returns>A list of pairs of keybinds that conflict.</returns>
public static Dictionary<KeyboardKeyCode, List<MiraKeybind>> GetConflicts()
{
var conflicts = new Dictionary<KeyboardKeyCode, List<MiraKeybind>>();
foreach (var keybind in Keybinds)
{
var all = GetKeybindsForKey(keybind.CurrentKey, true);
if (all.Length > 1)
{
if (!conflicts.TryGetValue(keybind.CurrentKey, out var group))
{
conflicts.Add(keybind.CurrentKey, new List<MiraKeybind>());
group = conflicts[keybind.CurrentKey];
}
if (!group.Contains(keybind))
{
group.Add(keybind);
}
}
}
return conflicts;
}
/// <summary>
/// Returns all keybinds for a specified keycode.
/// </summary>
/// <param name="keyCode">The key to look for.</param>
/// /// <param name="exclusiveCheck">Should only exlusive keybinds be present.</param>
/// <returns>The list of keybinds.</returns>
public static MiraKeybind[] GetKeybindsForKey(KeyboardKeyCode keyCode, bool exclusiveCheck = false)
{
var all = new List<MiraKeybind>();
foreach (var keybind in Keybinds)
{
if (keybind.CurrentKey == keyCode && (keybind.Exclusive || !exclusiveCheck))
{
all.Add(keybind);
}
}
return all.ToArray();
}
}
+61
View File
@@ -0,0 +1,61 @@
using System;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Collection of keybind-related utility classes.
/// </summary>
public static class KeybindUtils
{
/// <summary>
/// Gets the Rewired <see cref="InputManager_Base"/> instance.
/// </summary>
public static InputManager_Base? RewiredInputManager { get; internal set; }
/// <summary>
/// Returns the currently assigned keycode of a keybind.
/// </summary>
/// <param name="keybind">The keybind to get the keycode from.</param>
/// <returns>The currently assigned keycode.</returns>
public static KeyboardKeyCode? GetKeycodeByKeybind(MiraKeybind keybind)
{
if (keybind.RewiredInputAction == null)
{
return null;
}
return GetKeycodeByActionId(keybind.RewiredInputAction.id);
}
/// <summary>
/// Gets the keycode for an action with ReInput.
/// </summary>
/// <param name="actionId">The action ID.</param>
/// <returns>The keyboard keycode.</returns>
public static KeyboardKeyCode GetKeycodeByActionId(int actionId)
{
var player = ReInput.players.GetPlayer(0);
return player.controllers.maps.GetFirstElementMapWithAction(ControllerType.Keyboard, actionId, false)?
.keyboardKeyCode ?? KeyboardKeyCode.None;
}
/// <summary>
/// Finds and returns an unused KeyCode that is not equal to the excluded key.
/// </summary>
/// <param name="exclude">The KeyCode to skip during the search.</param>
/// <returns>
/// The first available KeyCode not currently used by any registered keybind,
/// or KeyCode.None if none are available.
/// </returns>
public static KeyboardKeyCode FindAvailableKey(KeyboardKeyCode exclude)
{
foreach (KeyboardKeyCode key in Enum.GetValues<KeyboardKeyCode>())
{
if (key == exclude) continue;
bool used = KeybindManager.Keybinds.Exists(e => e.DefaultKey == key);
if (!used) return key;
}
return KeyboardKeyCode.None;
}
}
+41
View File
@@ -0,0 +1,41 @@
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Class containing all default keybinds.
/// Recommended for cross-mod compatibility.
/// </summary>
[RegisterCustomKeybinds]
public static class MiraGlobalKeybinds
{
/// <summary>
/// Gets the keybind for primary abilities.
/// </summary>
public static MiraKeybind PrimaryAbility { get; } = new("Primary Ability", KeyboardKeyCode.T);
/// <summary>
/// Gets the keybind for secondary abilities.
/// </summary>
public static MiraKeybind SecondaryAbility { get; } = new("Secondary Ability", KeyboardKeyCode.Y);
/// <summary>
/// Gets the keybind for Tetiary abilities.
/// </summary>
public static MiraKeybind TertiaryAbility { get; } = new("Tertiary Ability", KeyboardKeyCode.U);
/// <summary>
/// Gets the keybind for primary modifier abilities.
/// </summary>
public static MiraKeybind ModifierPrimaryAbility { get; } = new("Modifier Primary Ability", KeyboardKeyCode.I);
/// <summary>
/// Gets the keybind for secondary modifier abilities.
/// </summary>
public static MiraKeybind ModifierSecondaryAbility { get; } = new("Modifier Secondary Ability", KeyboardKeyCode.O);
/// <summary>
/// Gets the keybind for Tetiary modifier abilities.
/// </summary>
public static MiraKeybind ModifierTertiaryAbility { get; } = new("Modifier Tertiary Ability", KeyboardKeyCode.P);
}
+109
View File
@@ -0,0 +1,109 @@
using System;
using System.Globalization;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.Keybinds;
/// <summary>
/// Represents a registered keybind.
/// </summary>
public class MiraKeybind
{
/// <summary>
/// Gets name of the keybind
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the unique identifier for this keybind. Used in Rewired.
/// </summary>
public string Id { get; }
/// <summary>
/// Gets the default keycode of this keybind.
/// </summary>
public KeyboardKeyCode DefaultKey { get; }
/// <summary>
/// Gets a value indicating whether this keybind should be checked for conflicts with other exclusive keybinds using the same key.
/// </summary>
public bool Exclusive { get; }
/// <summary>
/// Gets the modifier keys assinged to this keybind.
/// Due to Rewired limitations, there can only be 3 modifier keys.
/// </summary>
public ModifierKey[] ModifierKeys { get; }
/// <summary>
/// Gets the Rewired <see cref="InputAction"/> assinged for this keybind.
/// </summary>
public InputAction? RewiredInputAction { get; internal set; }
/// <summary>
/// Gets the currently assigned keycode.
/// </summary>
public KeyboardKeyCode CurrentKey => KeybindUtils.GetKeycodeByKeybind(this) ?? KeyboardKeyCode.None;
/// <summary>
/// Gets or sets the handler of the keybind. Invoked when the keybind is activated.
/// </summary>
private Action Handler { get; set; }
internal string? SourcePluginName { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MiraKeybind"/> class.
/// </summary>
/// <param name="name">The name of the keybind.</param>
/// <param name="defaultKeycode">The default keycode.</param>
/// <param name="modifierKeys">Up to 3 optional modifier keys.</param>
/// <param name="exclusive">Is exclusive.</param>
public MiraKeybind(
string name,
KeyboardKeyCode? defaultKeycode,
ModifierKey[]? modifierKeys = null,
bool exclusive = true)
{
Name = name;
Id = name.ToLower(CultureInfo.InvariantCulture).Replace(' ', '_');
DefaultKey = defaultKeycode ?? KeyboardKeyCode.None;
ModifierKeys = modifierKeys ?? Array.Empty<ModifierKey>();
Exclusive = exclusive;
Handler = () => { };
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Name} ({Id})";
}
/// <summary>
/// Triggers the keybind, invoking its handler.
/// </summary>
public void Invoke()
{
Handler.Invoke();
}
/// <summary>
/// Adds an <see cref="Action"/> that will be invoked when the keybind is used.
/// </summary>
/// <param name="action">The <see cref="Action"/> to add.</param>
public void OnActivate(Action action)
{
Handler += action;
}
/// <summary>
/// Removes an <see cref="Action"/> that would be invoked when the keybind is used.
/// Not recommended to use - add checks in the method added in <see cref="OnActivate"/> itself rather than removing it from the keybind.
/// </summary>
/// <param name="action">The <see cref="Action"/> to remove.</param>
public void RemoveOnActivate(Action action)
{
Handler -= action;
}
}
@@ -0,0 +1,9 @@
using System;
namespace MiraAPI.Keybinds;
/// <summary>
/// Used to mark a class for custom keybinds registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class RegisterCustomKeybindsAttribute : Attribute;
@@ -0,0 +1,27 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates an enum setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <param name="names">Optional custom enum names.</param>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalEnumSettingAttribute(
string? name = null,
string? description = null,
string[]? names = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, _name, _description, names);
}
}
@@ -0,0 +1,36 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
using MiraAPI.Utilities;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a number setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <param name="min">Minimum range.</param>
/// <param name="max">Maximum range.</param>
/// <param name="increment">Increment per use.</param>
/// <param name="suffixType">Suffix for the value.</param>
/// <param name="formatString">Format string used when formating.</param>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalNumberSettingAttribute(
string? name = null,
string? description = null,
float min = 1,
float max = 5,
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalNumberSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), increment, suffixType, formatString);
}
}
@@ -0,0 +1,24 @@
using System;
using BepInEx.Configuration;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Base class for all local settings attributes.
/// </summary>
/// <param name="name">The name of the setting. Defaults to entry key.</param>
/// <param name="description">The description of the setting. Defalts to entry description.</param>
[AttributeUsage(AttributeTargets.Property)]
public abstract class LocalSettingAttribute(
string? name = null,
string? description = null
) : Attribute
{
/// <summary>
/// Returns the created setting object.
/// </summary>
/// <param name="tab">Gets the tab where the setting is located.</param>
/// <param name="configEntryBase">Gets the config entry the setting is attached to.</param>
/// <returns>The created class.</returns>
internal abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase);
}
@@ -0,0 +1,12 @@
using System;
using BepInEx.Configuration;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Attribute for local setting buttons.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class LocalSettingsButtonAttribute : Attribute
{
}
@@ -0,0 +1,36 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
using MiraAPI.Utilities;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a slider setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <inheritdoc/>
/// <param name="min">Minimum range.</param>
/// <param name="max">Maximum range.</param>
/// <param name="roundValue">Should the value be rounded.</param>
/// <param name="suffixType">Suffix for the value.</param>
[AttributeUsage(AttributeTargets.Property)]
public class LocalSliderSettingAttribute(
string? name = null,
float min = 0,
float max = 100,
string? description = null,
bool displayValue = false,
string? formatString = null,
bool roundValue = false,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalSliderSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue);
}
}
@@ -0,0 +1,25 @@
using System;
using BepInEx.Configuration;
using MiraAPI.LocalSettings.SettingTypes;
namespace MiraAPI.LocalSettings.Attributes;
/// <summary>
/// Creates a toggle setting for the <see cref="ConfigEntry{T}"/>.
/// </summary>
/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Property)]
public class LocalToggleSettingAttribute(
string? name = null,
string? description = null
) : LocalSettingAttribute(name, description)
{
private readonly string? _name = name;
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalToggleSetting(tab, configEntryBase, _name, _description);
}
}
+42
View File
@@ -0,0 +1,42 @@
using BepInEx.Configuration;
using UnityEngine;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Interface for all local settings.
/// </summary>
public interface ILocalSetting
{
/// <summary>
/// Gets the preferred name of a setting.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of a setting.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the setting's gameobject.
/// </summary>
GameObject? Setting { get; }
/// <summary>
/// Gets the setting's config entry.
/// </summary>
ConfigEntryBase ConfigEntry { get; }
/// <summary>
/// Used to create the setting.
/// </summary>
/// <param name="toggle">Toggle template.</param>
/// <param name="slider">Slider template.</param>
/// <param name="parent">The parent of the setting.</param>
/// <param name="offset">The offset.</param>
/// <param name="order">The order.</param>
/// <param name="last">Whether it's the last on the row.</param>
/// <returns>The created setting.</returns>
GameObject? CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last);
}
@@ -0,0 +1,78 @@
using System;
using MiraAPI.Utilities;
using Reactor.Localization.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Represents a local settings button.
/// </summary>
public class LocalSettingsButton(string text, Action onClick)
{
/// <summary>
/// Gets or sets the button text.
/// </summary>
public string Text { get; set; } = text;
/// <summary>
/// Gets or sets the button on-click action.
/// </summary>
public Action OnClick { get; set; } = onClick;
/// <summary>
/// Gets the tab instance this button belongs to.
/// </summary>
public LocalSettingsTab? Tab { get; internal set; }
internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
button.transform.FindChild("ButtonHighlight")?.gameObject.DestroyImmediate();
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = Text;
button.name = Text;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.ButtonColor;
rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor;
rollover.Target = background;
background.color = Tab!.TabAppearance.ButtonColor;
button.OnClick.AddListener((UnityAction)OnClick);
background.transform.localScale = new Vector3(1, 1.25f, 1);
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
}
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
using MiraAPI.GameOptions;
using Reactor.Utilities;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Manages local settings.
/// </summary>
public static class LocalSettingsManager
{
internal static readonly Dictionary<Type, LocalSettingsTab> TypeToTab = [];
internal static readonly List<LocalSettingsTab> Tabs = [];
internal static bool RegisterTab(Type type, BasePlugin pluginInfo)
{
if (Activator.CreateInstance(type, pluginInfo.Config) is not LocalSettingsTab tab)
{
return false;
}
if (TypeToTab.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Local settings tab {type.Name} already exists.");
return false;
}
Tabs.Add(tab);
TypeToTab.Add(type, tab);
typeof(LocalSettingsTabSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, tab);
return true;
}
}
+297
View File
@@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.Patches.LocalSettings;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Represents a local settings tab. Gets a button by default.
/// </summary>
public abstract class LocalSettingsTab(ConfigFile config)
{
/// <summary>
/// Gets the name of the tab.
/// </summary>
public abstract string TabName { get; }
/// <summary>
/// Gets the tab appearance.
/// </summary>
public virtual LocalSettingTabAppearance TabAppearance { get; } = new();
/// <summary>
/// Gets a value indicating whether a button should be created for the tab.
/// </summary>
public virtual bool ShouldCreateButton => true;
/// <summary>
/// Gets a value indicating whether the tab should have category labels.
/// </summary>
protected virtual bool ShouldCreateLabels => true;
/// <summary>
/// Gets the mod's config file. Automatically found in the plugin loader.
/// </summary>
public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
/// <summary>
/// Gets the tab button instance.
/// </summary>
public TabGroup? TabButton { get; internal set; }
/// <summary>
/// Gets the list of settings.
/// </summary>
public List<ILocalSetting> Settings { get; } = [];
/// <summary>
/// Gets the list of buttons.
/// </summary>
public List<LocalSettingsButton> Buttons { get; } = [];
/// <summary>
/// Gets the index of this tab.
/// </summary>
protected int TabIndex => LocalSettingsManager.Tabs.IndexOf(this) + 10;
private Scroller? Scroller { get; set; }
/// <summary>
/// Invoked when an option value is changed.
/// </summary>
/// <param name="configEntry">The config entry which was changed.</param>
public virtual void OnOptionChanged(ConfigEntryBase configEntry)
{
}
/// <summary>
/// Attempts to open the tab.
/// Override for custom behavior.
/// </summary>
public virtual void Open()
{
OptionsMenuPatches.Instance?.OpenTabGroup(TabIndex);
}
/// <summary>
/// Creates the tab <see cref="GameObject"/> and it's content.
/// </summary>
/// <param name="instance">The <see cref="OptionsMenuBehaviour"/> instace.</param>
/// <returns>The created tab <see cref="GameObject"/>.</returns>
public virtual GameObject CreateTab(OptionsMenuBehaviour instance)
{
var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform);
tab.name = $"{TabName}Tab";
tab.transform.DestroyChildren();
tab.gameObject.SetActive(false);
if (Scroller == null)
{
Scroller = Helpers.CreateScroller(tab.transform, OptionsMenuPatches.MaskCollider);
}
if (TabButton != null)
{
TabButton.Content = tab.gameObject;
}
var generalLabel = instance.transform.FindChild("GeneralTab").FindChild("ControlGroup")
.FindChild("ControlText_TMP").gameObject;
var toggle = instance.transform.FindChild("GeneralTab").FindChild("ChatGroup").FindChild("CensorChatButton").GetComponent<ToggleButtonBehaviour>();
var slider = instance.transform.FindChild("GeneralTab").FindChild("SoundGroup").FindChild("SFXSlider").GetComponent<SlideBar>();
Dictionary<string, List<ILocalSetting>> entriesByGroup = new();
foreach (var entry in Settings)
{
var group = entry.ConfigEntry.Definition.Section;
if (!entriesByGroup.ContainsKey(group))
entriesByGroup.Add(group, []);
entriesByGroup[group].Add(entry);
}
float contentOffset = 0;
var contentOrder = 1;
var contentIndex = 1;
foreach (var pair in entriesByGroup)
{
if (ShouldCreateLabels)
{
CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset);
contentOrder = 1;
}
foreach (var setting in pair.Value)
{
var obj = setting.CreateOption(
toggle,
slider,
Scroller.Inner,
ref contentOffset,
ref contentOrder,
contentIndex == pair.Value.Count);
obj!.GetComponentsInChildren<SpriteRenderer>(true).Do(x =>
{
x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
x.sortingOrder = 150;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<MeshRenderer>(true).Do(x =>
{
x.sortingOrder = 151;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<PassiveButton>().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; });
contentIndex++;
}
contentOrder = 1;
}
contentIndex = 1;
contentOrder = 1;
foreach (var button in Buttons)
{
var obj = button.CreateButton(
toggle,
Scroller.Inner,
ref contentOffset,
ref contentOrder,
contentIndex == Buttons.Count);
obj!.GetComponentsInChildren<SpriteRenderer>(true).Do(x =>
{
x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
x.sortingOrder = 150;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<MeshRenderer>(true).Do(x =>
{
x.sortingOrder = 151;
x.sortingLayerName = "Default";
});
obj.GetComponentsInChildren<PassiveButton>().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; });
contentIndex++;
}
Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0));
return tab;
}
/// <summary>
/// Creates the tab button.
/// </summary>
/// <param name="instance">The <see cref="OptionsMenuBehaviour"/> instance.</param>
/// <param name="tabIdx">The tab index.</param>
/// <param name="offset">The current button offset.</param>
/// <returns>The created button <see cref="GameObject"/>.</returns>
public virtual GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset)
{
var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform);
tabButtonObject.name = $"{TabName} Button";
tabButtonObject.transform.localPosition = new Vector3(2.4f, 2.1f - offset, 5.5f);
tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1);
tabButtonObject.Button.color = TabAppearance.TabButtonColor;
TabButton = tabButtonObject;
var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
tabButtonText.GetComponent<TextTranslatorTMP>().Destroy(); // i hate text translators
tabButtonText.transform.localPosition = new Vector3(0.078f, 0, 0);
tabButtonText.transform.localScale = new Vector3(0.9f, 0.9f);
tabButtonText.alignment = TextAlignmentOptions.Right;
tabButtonText.text = $"<b>{GetShortName(TabName)}</b>";
var tabButton = tabButtonObject.GetComponent<PassiveButton>();
var rollover = tabButtonObject.Rollover;
rollover.OverColor = TabAppearance.TabButtonHoverColor;
rollover.OutColor = TabAppearance.TabButtonColor;
SpriteRenderer? tabButtonRend = null;
if (TabAppearance.TabIcon != null)
{
tabButtonRend = new GameObject("sprite").AddComponent<SpriteRenderer>();
tabButtonRend.gameObject.layer = 5; // ui layer
tabButtonRend.transform.SetParent(tabButtonObject.transform);
tabButtonRend.transform.localPosition = new Vector3(0.5f, 0, -2);
tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1);
tabButtonRend.sprite = TabAppearance.TabIcon.LoadAsset();
tabButtonText.gameObject.SetActive(false);
}
var tabIndex = tabIdx; // Local copies of the tab variables so we can use them in the lambdas
var tabOffset = offset;
tabButton.OnClick.AddListener((UnityAction)(() => { instance.OpenTabGroup(tabIndex + 10); }));
tabButton.OnMouseOver.AddListener((UnityAction)(() =>
{
tabButton.transform.localPosition = new Vector3(3.55f, 2.1f - tabOffset, 5.5f);
tabButtonText.gameObject.SetActive(true);
tabButtonText.transform.localPosition = new Vector3(-0.024f, 0, 0);
tabButtonText.maxVisibleCharacters = int.MaxValue;
tabButtonText.text = TabName;
tabButtonText.alignment = TextAlignmentOptions.Left;
}));
tabButton.OnMouseOut.AddListener((UnityAction)(() =>
{
if (!tabButtonObject.Content.gameObject.activeSelf)
{
//tabButtonObject.Button.color = TabColor;
//tabButtonObject.Rollover.OutColor = TabColor;
}
tabButton.transform.localPosition = new Vector3(2.4f, 2.1f - tabOffset, 5.5f);
tabButtonText.alignment = TextAlignmentOptions.Right;
if (tabButtonRend != null)
{
tabButtonRend.gameObject.SetActive(true);
tabButtonText.gameObject.SetActive(false);
}
tabButtonText.transform.localPosition = new Vector3(0.1f, 0, 0);
tabButtonText.maxVisibleCharacters = 4;
tabButtonText.text = $"<b>{GetShortName(TabName)}</b>";
}));
return tabButtonObject.gameObject;
}
private static void CreateLabel(GameObject template, Transform parent, string text, ref float offset)
{
var label = Object.Instantiate(template, parent);
label.transform.localPosition = new Vector3(0, 1.85f - offset);
label.GetComponent<TextTranslatorTMP>().Destroy();
label.name = text;
var tmp = label.GetComponent<TextMeshPro>();
tmp.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\"><b>{text}</b></font>";
tmp.alignment = TextAlignmentOptions.Center;
var meshRend = label.GetComponent<MeshRenderer>();
meshRend.sortingLayerName = "Default";
meshRend.sortingOrder = 151;
offset += 0.5f;
}
private static string GetShortName(string name)
{
var shortName = string.Empty;
name.Split(' ').Do(x => shortName += x[0]);
return shortName;
}
}
@@ -0,0 +1,91 @@
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Struct for modyfing tab apparance.
/// Defines how the settings look.
/// </summary>
public record struct LocalSettingTabAppearance()
{
/// <summary>
/// Gets or sets main color of the tab.
/// </summary>
public Color TabColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets tab button icon. No icon by default.
/// </summary>
public LoadableAsset<Sprite>? TabIcon { get; set; }
/// <summary>
/// Gets or sets color of the tab button.
/// </summary>
public Color TabButtonColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of the tab button.
/// </summary>
public Color TabButtonHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets active color of the tab button.
/// </summary>
public Color TabButtonActiveColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets hover color of toggles.
/// </summary>
public Color ToggleHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets active color of toggles.
/// </summary>
public Color ToggleActiveColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets inactive color of toggles.
/// </summary>
public Color ToggleInactiveColor { get; set; } = Color.red;
/// <summary>
/// Gets or sets color of number buttons.
/// </summary>
public Color NumberColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of number buttons.
/// </summary>
public Color NumberHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of sliders.
/// </summary>
public Color SliderColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of sliders.
/// </summary>
public Color SliderHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of enum buttons.
/// </summary>
public Color EnumColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of enum buttons.
/// </summary>
public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of buttons.
/// </summary>
public Color ButtonColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets hover color of buttons.
/// </summary>
public Color ButtonHoverColor { get; set; } = Palette.AcceptedGreen;
}
@@ -0,0 +1,19 @@
using System.Linq;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Singleton for local settings tabs.
/// </summary>
/// <typeparam name="T">The settings tab type.</typeparam>
public static class LocalSettingsTabSingleton<T> where T : LocalSettingsTab
{
private static T? _instance;
/// <summary>
/// Gets the instance of the setting tab.
/// </summary>
#pragma warning disable CA1000
public static T Instance => _instance ??= LocalSettingsManager.Tabs.OfType<T>().Single();
#pragma warning restore CA1000
}
@@ -0,0 +1,132 @@
using System;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for enums.
/// </summary>
public class LocalEnumSetting : LocalSettingBase<int>
{
/// <summary>
/// Gets the enum type of the setting.
/// </summary>
public Type EnumType { get; }
/// <summary>
/// Gets the enum values.
/// </summary>
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalEnumSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="enumType">The enum type.</param>
/// <param name="values">The optional values array to replace the enum names.</param>
public LocalEnumSetting(
Type tab,
ConfigEntryBase configEntry,
Type enumType,
string? name = null,
string? description = null,
string[]? values = null)
: base(tab, configEntry, name, description)
{
EnumType = enumType;
Values = values ?? Enum
.GetValues(configEntry.SettingType)
.Cast<Enum>()
.Select(x => x.ToDisplayString())
.ToArray();
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent<SpriteRenderer>();
if (highlight != null)
{
highlight.color = Tab!.TabAppearance.EnumHoverColor;
highlight.gameObject.SetActive(false);
}
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = GetValueText();
button.name = Name;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.EnumColor;
rollover.OverColor = Tab!.TabAppearance.EnumHoverColor;
background.color = Tab!.TabAppearance.EnumColor;
button.OnClick.AddListener((UnityAction)(() =>
{
int value = GetValue();
value++;
if (value >= Values.Length)
{
value = 0;
}
SetValue(value);
tmp.text = GetValueText();
}));
button.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
highlight?.gameObject.SetActive(true);
}));
button.OnMouseOut.AddListener((UnityAction)(() =>
{
tmp.text = GetValueText();
highlight?.gameObject.SetActive(false);
}));
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{Values[GetValue()]}</font></b>";
}
}
@@ -0,0 +1,145 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for numbers.
/// </summary>
public class LocalNumberSetting : LocalSettingBase<float>
{
/// <summary>
/// Gets the range of the button.
/// </summary>
public FloatRange NumberRange { get; }
/// <summary>
/// Gets the increment of the value when button is pressed.
/// </summary>
public float Increment { get; }
/// <summary>
/// Gets a format for the text to use to format the number.
/// </summary>
public string FormatString { get; }
/// <summary>
/// Gets the suffix for the number value.
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalNumberSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="numberRange">The value range.</param>
/// <param name="increment">The increment per click.</param>
/// <param name="suffixType">The suffix used for formating.</param>
/// <param name="formatString">The format string used for formating.</param>
public LocalNumberSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null,
FloatRange? numberRange = null,
float? increment = null,
MiraNumberSuffixes? suffixType = null,
string? formatString = null)
: base(tab, configEntry, name, description)
{
SuffixType = suffixType ?? MiraNumberSuffixes.None;
NumberRange = numberRange ?? new FloatRange(1, 5);
Increment = increment ?? 1;
FormatString = formatString ?? "0";
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var button = Object.Instantiate(toggle, parent).GetComponent<PassiveButton>();
var tmp = button.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var rollover = button.GetComponent<ButtonRolloverHandler>();
tmp.GetComponent<TextTranslatorTMP>().Destroy();
button.gameObject.SetActive(true);
var toggleComp = button.GetComponent<ToggleButtonBehaviour>();
var background = toggleComp.Background;
var highlight = button.transform.FindChild("ButtonHighlight")?.GetComponent<SpriteRenderer>();
if (highlight != null)
{
highlight.color = Tab!.TabAppearance.NumberHoverColor;
highlight.gameObject.SetActive(false);
}
toggleComp.Destroy();
if (last && order == 1)
{
// Button in the middle
button.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
tmp.text = GetValueText();
button.name = Name;
button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OutColor = Tab!.TabAppearance.NumberColor;
rollover.OverColor = Tab!.TabAppearance.NumberHoverColor;
background.color = Tab!.TabAppearance.NumberColor;
button.OnClick.AddListener((UnityAction)(() =>
{
float value = GetValue();
value += Increment;
if (value > NumberRange.max)
{
value = NumberRange.min;
}
SetValue(value);
tmp.text = GetValueText();
}));
button.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
highlight?.gameObject.SetActive(true);
}));
button.OnMouseOut.AddListener((UnityAction)(() =>
{
tmp.text = GetValueText();
highlight?.gameObject.SetActive(false);
}));
Helpers.DivideSize(button.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return button.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
var value = GetValue();
var formated = Helpers.FormatValue(value, SuffixType, FormatString);
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{formated}</font></b>";
}
}
@@ -0,0 +1,73 @@
using System;
using System.Linq;
using BepInEx.Configuration;
using UnityEngine;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <inheritdoc />
public abstract class LocalSettingBase<T> : ILocalSetting
{
/// <inheritdoc />
public string Name { get; }
/// <inheritdoc />
public string Description { get; }
/// <inheritdoc />
public GameObject? Setting { get; } = null!;
/// <inheritdoc />
public ConfigEntryBase ConfigEntry { get; }
internal LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this));
/// <summary>
/// Initializes a new instance of the <see cref="LocalSettingBase{T}"/> class.
/// </summary>
/// <param name="tab">The tab to create the setting in.</param>
/// <param name="configEntry">The config entry.</param>
/// <param name="name">The name of the setting.</param>
/// <param name="description">The description of the setting.</param>
protected LocalSettingBase(Type tab, ConfigEntryBase configEntry, string? name = null, string? description = null)
{
ConfigEntry = configEntry;
Name = name ?? ConfigEntry.Definition.Key;
Description = description ?? ConfigEntry.Description.Description;
LocalSettingsManager.TypeToTab[tab].Settings.Add(this);
}
/// <summary>
/// Creates an instance of the setting.
/// </summary>
/// <returns>The created setting.</returns>
public abstract GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last);
/// <summary>
/// Returns the formated string to use in the text of the setting.
/// </summary>
/// <returns>The value text.</returns>
protected virtual string GetValueText()
{
return string.Empty;
}
/// <summary>
/// Gets the value of the config entry, casted to the setting type.
/// </summary>
/// <returns>The value.</returns>
public virtual T GetValue()
{
return (T)ConfigEntry.BoxedValue;
}
/// <summary>
/// Sets the value of the config entry.
/// </summary>
/// <param name="value">The value to set to.</param>
public virtual void SetValue(T value)
{
ConfigEntry.BoxedValue = value;
Tab?.OnOptionChanged(ConfigEntry);
}
}
@@ -0,0 +1,120 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for sliders.
/// </summary>
public class LocalSliderSetting : LocalSettingBase<float>
{
/// <summary>
/// Gets the range of the slider.
/// </summary>
public FloatRange SliderRange { get; }
/// <summary>
/// Gets a value indicating whether the value should be displayed next to name.
/// </summary>
public bool DisplayValue { get; }
/// <summary>
/// Gets a format for the text to use to format the number.
/// </summary>
public string FormatString { get; }
/// <summary>
/// Gets a value indicating whether the value should be rounded.
/// </summary>
public bool RoundValue { get; }
/// <summary>
/// Gets the suffix for the number value.
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LocalSliderSetting"/> class.
/// </summary>
/// <inheritdoc/>
/// <param name="sliderRange">The value range.</param>
/// <param name="suffixType">The suffix used for formating.</param>
/// <param name="formatString">The format string used for formating.</param>
/// <param name="roundValue">Should the value be rounded.</param>
public LocalSliderSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null,
FloatRange? sliderRange = null,
bool displayValue = false,
MiraNumberSuffixes? suffixType = null,
string? formatString = null,
bool roundValue = false)
: base(tab, configEntry, name, description)
{
SliderRange = sliderRange ?? new FloatRange(0, 100);
DisplayValue = displayValue;
SuffixType = suffixType ?? MiraNumberSuffixes.None;
FormatString = formatString ?? "0.0";
RoundValue = roundValue;
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var newSlider = Object.Instantiate(slider, parent).GetComponent<SlideBar>();
var rollover = newSlider.GetComponent<ButtonRolloverHandler>();
newSlider.Title = newSlider.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>(); // Why the hell slider has a title property that is not even assigned???
newSlider.Title.GetComponent<TextTranslatorTMP>().Destroy();
newSlider.gameObject.SetActive(true);
newSlider.Bar.color = Tab!.TabAppearance.SliderColor;
if (order == 2)
offset += 0.5f;
newSlider.Bar.transform.localPosition = new Vector3(2.85f, 0, 0);
newSlider.transform.localPosition = new Vector3(-2.12f, 1.85f - offset, -7);
newSlider.name = Name;
newSlider.Range = new FloatRange(-1.5f, 1.5f);
newSlider.SetValue(Mathf.InverseLerp(SliderRange.min, SliderRange.max, GetValue()));
rollover.OutColor = Tab!.TabAppearance.SliderColor;
rollover.OverColor = Tab!.TabAppearance.SliderHoverColor;
newSlider.Title.transform.localPosition = new Vector3(0.5f, 0, -1f);
newSlider.Title.horizontalAlignment = DisplayValue ? HorizontalAlignmentOptions.Left : HorizontalAlignmentOptions.Center;
newSlider.Title.text = GetValueText();
newSlider.OnValueChange.AddListener((UnityAction)(() =>
{
SetValue(RoundValue
? Mathf.Round(Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value))
: Mathf.Lerp(SliderRange.min, SliderRange.max, newSlider.Value));
newSlider.Title.text = GetValueText();
}));
order = 1;
offset += 0.5f;
return newSlider.gameObject;
}
/// <inheritdoc/>
protected override string GetValueText()
{
if (DisplayValue)
{
var value = GetValue();
var formated = Helpers.FormatValue(value, SuffixType, FormatString);
var maxFormated = Helpers.FormatValue(SliderRange.max, SuffixType, FormatString);
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}: <b>{formated} / {maxFormated}</font></b>";
}
return $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">{Name}</font></b>";
}
}
@@ -0,0 +1,88 @@
using System;
using BepInEx.Configuration;
using MiraAPI.Utilities;
using Reactor.Localization.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.LocalSettings.SettingTypes;
/// <summary>
/// Local setting class for toggles.
/// </summary>
public class LocalToggleSetting : LocalSettingBase<bool>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalToggleSetting"/> class.
/// </summary>
/// <inheritdoc/>
public LocalToggleSetting(
Type tab,
ConfigEntryBase configEntry,
string? name = null,
string? description = null
) : base(tab, configEntry, name, description)
{
}
/// <inheritdoc />
public override GameObject CreateOption(ToggleButtonBehaviour toggle, SlideBar slider, Transform parent, ref float offset, ref int order, bool last)
{
var toggleObject = Object.Instantiate(toggle, parent).GetComponent<ToggleButtonBehaviour>();
var tmp = toggleObject.transform.FindChild("Text_TMP").GetComponent<TextMeshPro>();
var passiveButton = toggleObject.GetComponent<PassiveButton>();
var rollover = toggleObject.GetComponent<ButtonRolloverHandler>();
toggleObject.gameObject.SetActive(true);
if (last && order == 1)
{
// Toggle in the middle
toggleObject.transform.localPosition = new Vector3(0, 1.85f - offset, -7);
}
else
{
toggleObject.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7);
}
toggleObject.BaseText = CustomStringName.CreateAndRegister(Name);
toggleObject.UpdateText(GetValue());
toggleObject.name = Name;
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
passiveButton.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent();
rollover.OverColor = Tab!.TabAppearance.ToggleHoverColor;
passiveButton.OnClick.AddListener((UnityAction)(() =>
{
SetValue(!GetValue());
toggleObject.UpdateText(GetValue());
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
}));
passiveButton.OnMouseOver.AddListener((UnityAction)(() =>
{
if (!Description.IsNullOrWhiteSpace())
{
tmp.text = Description;
}
}));
passiveButton.OnMouseOut.AddListener((UnityAction)(() =>
{
toggleObject.UpdateText(GetValue());
toggleObject.Background.color = GetValue() ? Tab!.TabAppearance.ToggleActiveColor : Tab!.TabAppearance.ToggleInactiveColor;
}));
Helpers.DivideSize(toggleObject.gameObject, 1.1f);
order++;
if (order > 2 && !last)
{
offset += 0.5f;
order = 1;
}
if (last)
offset += 0.6f;
return toggleObject.gameObject;
}
}
+39
View File
@@ -0,0 +1,39 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Utilities.Assets;
namespace MiraAPI;
/// <summary>
/// Mira API Config File Handler
/// </summary>
public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
{
/// <inheritdoc />
public override string TabName => "Mira API";
/// <inheritdoc />
public override LocalSettingTabAppearance TabAppearance => new()
{
TabButtonHoverColor = MiraApiPlugin.MiraColor,
TabIcon = MiraAssets.SettingsIcon,
};
/// <summary>
/// Gets whether the modifiers hud should be on the left side of the screen (under roles/task tab). Recommended for streamers.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Displays", "Show Modifiers HUD on Left Side", false);
/// <summary>
/// Gets whether to show keybinds in the control mapper.
/// </summary>
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
/// <summary>
/// Gets whether to apply cosmetic changes to the TaskAdder.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true);
}
+5
View File
@@ -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>
+14 -1
View File
@@ -4,8 +4,10 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types;
using MiraAPI.Patches.Roles;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using UnityEngine;
@@ -126,7 +128,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
{
ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
var taskPanelOpen = HudManager.Instance.TaskPanel.open;
var roleTabOpen = HudManagerPatches.RoleTab != null && HudManagerPatches.RoleTab.open;
var leftSideHud = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ModifiersHudLeftSide.Value;
if (leftSideHud && (taskPanelOpen || roleTabOpen))
{
ModifierDisplay!.gameObject.SetActive(false);
}
else
{
ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
}
}
}
@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
@@ -94,6 +95,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
IsOpen = false;
_children.gameObject.SetActive(false);
if (LocalSettingsTabSingleton<MiraApiSettings>.Instance.ModifiersHudLeftSide.Value)
{
var aspect = GetComponent<AspectPosition>();
_toggleButton.transform.localPosition = new Vector3(-1f, 2.7f, 0f);
_children.transform.localPosition = new Vector3(-0.2f, -0.05f, 0f);
aspect.Alignment = AspectPosition.EdgeAlignments.LeftTop;
aspect.DistanceFromEdge = new Vector3(1.8f, 2.55f, -20f);
aspect.AdjustPosition();
}
}
/// <summary>
@@ -109,7 +120,16 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
/// </summary>
public void ToggleTab()
{
IsOpen = !IsOpen;
SetOpened(!IsOpen);
}
/// <summary>
/// Use this to set the visibility of the modifiers tab.
/// </summary>
/// <param name="val">Whether it should be visible or not.</param>
public void SetOpened(bool val)
{
IsOpen = val;
_children.gameObject.SetActive(IsOpen);
RefreshModifiers();
}
@@ -118,7 +138,6 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
RefreshModifiers();
}
@@ -194,13 +213,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
if (filteredModifiers.Count == 0)
{
_toggleButton.gameObject.SetActive(false);
return;
}
_toggleButton.gameObject.SetActive(true);
_toggleButton.gameObject.SetActive(filteredModifiers.Count != 0);
foreach (var mod in modifiersToAdd)
{
+1 -1
View File
@@ -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, SendImmediately = true)]
[MethodRpc((uint)MiraRpc.RemoveModifier)]
public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId)
{
target.RemoveModifier(uniqueId);
+2
View File
@@ -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;
@@ -125,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();
+2 -2
View File
@@ -29,7 +29,7 @@ public static class CustomMurderRpc
/// <param name="teleportMurderer">Should the killer be snapped to the dead player.</param>
/// <param name="showKillAnim">Should the kill animation be shown.</param>
/// <param name="playKillSound">Should the kill sound be played.</param>
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before, SendImmediately = true)]
[MethodRpc((uint)MiraRpc.CustomMurder, LocalHandling = RpcLocalHandling.Before)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
@@ -222,7 +222,7 @@ public static class CustomMurderRpc
if (createDeadBody)
{
deadBody = Object.Instantiate(GameManager.Instance.DeadBodyPrefab);
deadBody = Object.Instantiate(GameManager.Instance.deadBodyPrefab[0]);
deadBody.enabled = false;
deadBody.ParentId = target.PlayerId;
deadBody.bodyRenderers.ToList().ForEach(target.SetPlayerMaterialColors);
+2
View File
@@ -3,10 +3,12 @@ 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
{
+7 -5
View File
@@ -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)]
@@ -0,0 +1,17 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(TutorialManager._RunTutorial_d__3), nameof(TutorialManager._RunTutorial_d__3.MoveNext))]
public static class FreeplayRoundStartPatch
{
public static void Postfix(TutorialManager._RunTutorial_d__3 __instance, ref bool __result)
{
if (!__result)
{
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
}
@@ -9,7 +9,12 @@ internal static class SpawnInMinigamePatch
{
public static void Postfix()
{
var @event = new RoundStartEvent(false);
var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled) return;
var @event2 = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event2);
}
}
@@ -25,7 +25,12 @@ internal static class SubmergedSpawnGamePatch
public static void Postfix()
{
var @event = new RoundStartEvent(false);
var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled) return;
var @event2 = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event2);
}
}
@@ -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;
}
}
+211 -251
View File
@@ -1,15 +1,17 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AmongUs.GameOptions;
using System.Text;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
@@ -19,6 +21,7 @@ namespace MiraAPI.Patches.Freeplay;
internal static class TaskAdderPatches
{
private static Scroller _scroller = null!;
private static Dictionary<string, TaskFolder> folders = new();
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
@@ -32,6 +35,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,
@@ -54,46 +62,80 @@ internal static class TaskAdderPatches
__instance.RoleButton.GetComponent<PassiveButton>().ClickMask = collider;
__instance.RootFolderPrefab.GetComponent<PassiveButton>().ClickMask = collider;
__instance.RootFolderPrefab.gameObject.SetActive(false);
__instance.TaskParent = inner.transform;
var rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
rolesFolder.gameObject.SetActive(false);
rolesFolder.FolderName = "Roles";
rolesFolder.name = "RolesFolder";
var crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == "Crewmate")!;
var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == "Impostor")!;
var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray);
var modifiersFolder = __instance.CreateFolder("Modifiers", __instance.Root, 0, Color.blue);
var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
modifiersFolder.gameObject.SetActive(false);
modifiersFolder.FolderName = "Modifiers";
modifiersFolder.name = "ModifiersFolder";
folders.Clear();
folders.Add("Crewmate", crewmateFolder);
folders.Add("Impostor", impostorFolder);
folders.Add("Neutral", neutralFolder);
folders.Add("Modifiers", modifiersFolder);
int folderIdx = 3;
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
if (plugin.InternalRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
var pluginFolders = new Dictionary<string, TaskFolder>();
foreach (var role in plugin.InternalRoles
.Where(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true })
.Select(x => x.Value))
{
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Roles";
newFolder.FolderName = plugin.PluginInfo.Metadata.Name;
newFolder.gameObject.SetActive(false);
rolesFolder.SubFolders.Add(newFolder);
var customRole = role as ICustomRole;
var folderName = customRole!.Configuration.FreeplayFolder;
if (!folders.TryGetValue(folderName, out var teamFolder))
{
teamFolder = __instance.CreateFolder(folderName, __instance.Root, folderIdx++, customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray);
folders.Add(folderName, teamFolder);
}
if (!pluginFolders.TryGetValue(folderName, out var pluginFolder))
{
pluginFolder = __instance.CreateFolder(plugin.PluginInfo.Metadata.Name, teamFolder);
pluginFolders.Add(folderName, teamFolder);
}
pluginFolder.RoleChildren.Add(role);
}
if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay))
{
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Modifiers";
newFolder.FolderName = plugin.PluginInfo.Metadata.Name;
newFolder.gameObject.SetActive(false);
modifiersFolder.SubFolders.Add(newFolder);
__instance.CreateFolder(plugin.PluginInfo.Metadata.Name, modifiersFolder)
.gameObject.name = plugin.PluginId;
}
}
__instance.Root.SubFolders.Add(rolesFolder);
__instance.Root.SubFolders.Add(modifiersFolder);
// Modifier folder at the end
__instance.Root.SubFolders.Insert(folderIdx, modifiersFolder);
__instance.GoToRoot();
}
private static TaskFolder CreateFolder(this TaskAdderGame instance, string name, TaskFolder parent, int idx = -1, Color? color = null)
{
var folder = Object.Instantiate(instance.RootFolderPrefab, instance.transform);
folder.gameObject.SetActive(false);
folder.FolderName = name;
folder.SetCustomColor(color ?? new Color(0.937f, 0.811f, 0.592f));
folder.name = name + "Folder";
if (idx == -1)
parent.SubFolders.Add(folder);
else if (idx > 0)
parent.SubFolders.Insert(idx, folder);
return folder;
}
private static void SetCustomColor(this TaskFolder folder, Color color)
{
folder.currentFolderColor = color;
folder.folderSpriteRenderer.color = color;
folder.buttonRolloverHandler.OutColor = color;
folder.buttonRolloverHandler.UnselectedColor = color;
}
private static void AddFileAsChildCustom(
this TaskAdderGame instance,
TaskAddButton item,
@@ -116,40 +158,46 @@ internal static class TaskAdderPatches
instance.ActiveItems.Add(item.transform);
}
private static bool IsChildOf(this TaskFolder child, TaskFolder parent) => parent.SubFolders
.ToArray()
.Any(x => x.FolderName == child.FolderName);
// yes it might be crazy patching the entire method, but i tried so many other methods and only this works :cry:
// true -chip
[HarmonyPrefix]
[HarmonyPatch(nameof(TaskAdderGame.ShowFolder))]
public static bool ShowPatch(TaskAdderGame __instance, TaskFolder taskFolder)
{
var stringBuilder = new Il2CppSystem.Text.StringBuilder(64);
StringBuilder stringBuilder = new StringBuilder(64);
__instance.Hierarchy.Add(taskFolder);
for (var i = 0; i < __instance.Hierarchy.Count; i++)
foreach (var t in __instance.Hierarchy)
{
stringBuilder.Append(__instance.Hierarchy.ToArray()[i].FolderName);
stringBuilder.Append("\\");
stringBuilder.Append(t.FolderName);
stringBuilder.Append('\\');
}
__instance.PathText.text = stringBuilder.ToString();
for (var j = 0; j < __instance.ActiveItems.Count; j++)
var prettyEnabled = LocalSettingsTabSingleton<MiraApiSettings>.Instance.PrettyTaskAdder.Value;
if (prettyEnabled)
{
Object.Destroy(__instance.ActiveItems.ToArray()[j].gameObject);
__instance.PathText.fontSizeMin = 3;
__instance.PathText.fontSizeMax = 3;
__instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate();
}
__instance.ActiveItems.ToArray().Do(x => x.gameObject.Destroy());
__instance.ActiveItems.Clear();
var num = 0f;
var num2 = 0f;
var num3 = 0f;
for (var k = 0; k < taskFolder.SubFolders.Count; k++)
{
var taskFolder2 = Object.Instantiate(taskFolder.SubFolders.ToArray()[k], __instance.TaskParent);
var folderTransform = taskFolder2.transform;
float num = 0f;
float num2 = 0f;
float num3 = 0f;
foreach (var t in taskFolder.SubFolders)
{
TaskFolder taskFolder2 = Object.Instantiate(t, __instance.TaskParent);
taskFolder2.gameObject.SetActive(true);
taskFolder2.Parent = __instance;
folderTransform.localPosition = new Vector3(num, num2, 0f);
folderTransform.localScale = Vector3.one;
num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.3f);
taskFolder2.transform.localPosition = new Vector3(num, num2, 0f);
taskFolder2.transform.localScale = Vector3.one;
num3 = Mathf.Max(num3, taskFolder2.Text.bounds.size.y + 1.1f);
num += __instance.folderWidth;
if (num > __instance.lineWidth)
{
@@ -158,249 +206,161 @@ internal static class TaskAdderPatches
num3 = 0f;
}
__instance.ActiveItems.Add(folderTransform);
if (!taskFolder2 || !taskFolder2.Button)
__instance.ActiveItems.Add(taskFolder2.transform);
if (taskFolder2 != null && taskFolder2.Button != null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button);
if (!string.IsNullOrEmpty(__instance.restorePreviousSelectionByFolderName) &&
taskFolder2.FolderName.Equals(__instance.restorePreviousSelectionByFolderName, StringComparison.Ordinal))
{
__instance.restorePreviousSelectionFound = taskFolder2.Button;
ControllerManager.Instance.AddSelectableUiElement(taskFolder2.Button);
}
}
var flag = false;
var list = taskFolder.Children.ToArray().OrderBy(t => t.TaskType).ToList();
for (var l = 0; l < list.Count; l++)
var list = taskFolder.TaskChildren
.ToArray()
.OrderBy(x => x.TaskType)
.ToArray();
foreach (var task in list)
{
var taskAddButton = Object.Instantiate(__instance.TaskPrefab);
taskAddButton.MyTask = list.ToArray()[l];
switch (taskAddButton.MyTask.TaskType)
TaskAddButton taskAddButton = Object.Instantiate(__instance.TaskPrefab);
taskAddButton.MyTask = task;
switch (task.TaskType)
{
case TaskTypes.DivertPower:
{
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = TranslationController.Instance.GetString(
StringNames.DivertPowerTo,
TranslationController.Instance.GetString(targetSystem));
break;
}
{
var targetSystem = task.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = TranslationController.Instance.GetString(
StringNames.DivertPowerTo,
TranslationController.Instance.GetString(targetSystem));
break;
}
case TaskTypes.FixWeatherNode:
{
var nodeId = taskAddButton.MyTask.Cast<WeatherNodeTask>().NodeId;
taskAddButton.Text.text =
TranslationController.Instance.GetString(
StringNames.FixWeatherNode) + " " +
TranslationController.Instance.GetString(
WeatherSwitchGame.ControlNames[nodeId]);
break;
}
{
var nodeId = task.Cast<WeatherNodeTask>().NodeId;
taskAddButton.Text.text =
TranslationController.Instance.GetString(
StringNames.FixWeatherNode) + " " +
TranslationController.Instance.GetString(
WeatherSwitchGame.ControlNames[nodeId]);
break;
}
default:
taskAddButton.Text.text =
TranslationController.Instance.GetString(taskAddButton.MyTask.TaskType);
TranslationController.Instance.GetString(task.TaskType);
break;
}
__instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3);
if (taskAddButton.Button == null)
if (taskAddButton != null && taskAddButton.Button != null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
if (__instance.Hierarchy.Count == 1 || flag)
{
continue;
}
var component =
ControllerManager.Instance.CurrentUiState.CurrentSelection.GetComponent<TaskFolder>();
if (component != null)
{
__instance.restorePreviousSelectionByFolderName = component.FolderName;
}
ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button);
flag = true;
}
if (taskFolder.FolderName == "Roles")
{
for (var m = 0; m < RoleManager.Instance.AllRoles.Length; m++)
{
var roleBehaviour = RoleManager.Instance.AllRoles[m];
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
CustomRoleManager.CustomRoles.ContainsKey((ushort)roleBehaviour.Role))
{
continue;
}
var taskAddButton2 = Object.Instantiate(__instance.RoleButton);
taskAddButton2.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
__instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2.Button == null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
switch (m)
{
case 0 when __instance.restorePreviousSelectionFound != null:
ControllerManager.Instance.SetDefaultSelection(__instance.restorePreviousSelectionFound);
__instance.restorePreviousSelectionByFolderName = string.Empty;
__instance.restorePreviousSelectionFound = null;
break;
case 0:
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
break;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button, false);
}
}
if (taskFolder)
var roleChildren = taskFolder.RoleChildren
.ToArray()
.Where(x => !(taskFolder.IsChildOf(__instance.Root) && x is ICustomRole))
.ToArray();
foreach (var role in roleChildren)
{
var split = taskFolder.name.Split(':');
if (split.Length == 2)
TaskAddButton roleAddButton = Object.Instantiate(__instance.RoleButton);
roleAddButton.SafePositionWorld = __instance.SafePositionWorld;
roleAddButton.Text.text = prettyEnabled ? role.NiceName : "Be_" + role.NiceName + ".exe";
roleAddButton.Role = role;
if (prettyEnabled)
{
var pluginId = split[0];
var folderName = split[1];
if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin)
roleAddButton.Text.fontSizeMin = 1;
roleAddButton.FileImage.sprite = role.IsImpostor
? MiraAssets.ImpostorFile.LoadAsset()
: MiraAssets.CrewmateFile.LoadAsset();
}
if (role is ICustomRole custom)
{
if (prettyEnabled && custom.Team == ModdedRoleTeams.Custom) roleAddButton.FileImage.sprite = MiraAssets.CustomTeamFile.LoadAsset();
var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray;
if (custom.Team is ModdedRoleTeams.Crewmate)
{
if (folderName == "Roles(Clone)")
customColor = Palette.CrewmateBlue;
}
else if (custom.Team is ModdedRoleTeams.Impostor)
{
customColor = Palette.ImpostorRed;
}
roleAddButton.FileImage.color = customColor;
roleAddButton.RolloverHandler.OutColor = customColor;
}
__instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3);
if (roleAddButton != null && roleAddButton.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(roleAddButton.Button);
}
}
if (folders.TryGetValue("Modifiers", out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder))
{
var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.gameObject.name.Replace("(Clone)", string.Empty));
if (plugin != null)
{
foreach (var modifier in plugin.InternalModifiers)
{
if (!modifier.ShowInFreeplay)
{
PluginRoles(__instance, plugin, ref num, ref num2, ref num3);
continue;
}
else if (folderName == "Modifiers(Clone)")
var taskAddButton = Object.Instantiate(__instance.RoleButton);
taskAddButton.name = modifier.TypeId.ToString(NumberFormatInfo.InvariantInfo);
taskAddButton.role = null;
taskAddButton.MyTask = null;
taskAddButton.SafePositionWorld = __instance.SafePositionWorld;
taskAddButton.Text.text = modifier.ModifierName;
taskAddButton.Text.fontSizeMin = 1;
taskAddButton.Text.EnableMasking();
taskAddButton.FileImage.color = modifier.FreeplayFileColor;
taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor;
if (modifier is TimedModifier timed)
{
PluginModifiers(__instance, plugin, ref num, ref num2, ref num3);
taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset();
taskAddButton.Text.text += $" ({timed.Duration}s)";
}
else
{
taskAddButton.FileImage.sprite = MiraAssets.ModifierFile.LoadAsset();
}
taskAddButton.Button.OnClick = new Button.ButtonClickedEvent();
var m = modifier;
taskAddButton.Button.OnClick.AddListener((UnityAction)(() =>
{
var id = m.TypeId;
if (PlayerControl.LocalPlayer.HasModifier(id))
{
PlayerControl.LocalPlayer.RemoveModifier(id);
taskAddButton.Overlay.enabled = false;
}
else
{
PlayerControl.LocalPlayer.AddModifier(id);
taskAddButton.Overlay.enabled = true;
}
}));
__instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3);
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
}
}
}
foreach (var chip in __instance.ActiveItems)
{
chip.GetComponentInChildren<TextMeshPro>().EnableStencilMasking();
chip.GetComponent<SpriteRenderer>().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
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();
}
if (__instance.Hierarchy.Count == 1)
{
ControllerManager.Instance.SetBackButton(__instance.BackButton);
return false;
}
ControllerManager.Instance.SetBackButton(__instance.Hierarchy.Count == 1
? __instance.BackButton
: __instance.FolderBackButton);
ControllerManager.Instance.SetBackButton(__instance.FolderBackButton);
__instance.SetPreviousControllerSelection(taskFolder.FolderName);
return false;
}
private static void PluginModifiers(
TaskAdderGame instance,
MiraPluginInfo plugin,
ref float num,
ref float num2,
ref float num3)
{
for (var m = 0; m < plugin.InternalModifiers.Count; m++)
{
var modifier = plugin.InternalModifiers[m];
if (!modifier.ShowInFreeplay)
{
continue;
}
var taskAddButton = Object.Instantiate(instance.RoleButton);
taskAddButton.name = modifier.TypeId.ToString(NumberFormatInfo.InvariantInfo);
taskAddButton.role = null;
taskAddButton.MyTask = null;
taskAddButton.SafePositionWorld = instance.SafePositionWorld;
taskAddButton.Text.text = modifier.ModifierName;
taskAddButton.Text.EnableMasking();
taskAddButton.Button.OnClick = new Button.ButtonClickedEvent();
taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() =>
{
var id = modifier.TypeId;
if (PlayerControl.LocalPlayer.HasModifier(id))
{
PlayerControl.LocalPlayer.RemoveModifier(id);
taskAddButton.Overlay.enabled = false;
}
else
{
PlayerControl.LocalPlayer.AddModifier(id);
taskAddButton.Overlay.enabled = true;
}
}));
instance.AddFileAsChildCustom(taskAddButton, ref num, ref num2, ref num3);
ControllerManager.Instance.AddSelectableUiElement(taskAddButton.Button);
switch (m)
{
case 0 when instance.restorePreviousSelectionFound != null:
ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound);
instance.restorePreviousSelectionByFolderName = string.Empty;
instance.restorePreviousSelectionFound = null;
break;
case 0:
ControllerManager.Instance.SetDefaultSelection(taskAddButton.Button);
break;
}
}
}
private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3)
{
for (var m = 0; m < plugin.InternalRoles.Count; m++)
{
var roleBehaviour = plugin.InternalRoles.ElementAt(m).Value;
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false })
{
continue;
}
var taskAddButton2 = Object.Instantiate(instance.RoleButton);
taskAddButton2.SafePositionWorld = instance.SafePositionWorld;
taskAddButton2.Text.text = "Be_" + roleBehaviour.NiceName + ".exe";
taskAddButton2.Text.EnableMasking();
instance.AddFileAsChildCustom(taskAddButton2, ref num, ref num2, ref num3);
taskAddButton2.Role = roleBehaviour;
if (taskAddButton2.Button == null)
{
continue;
}
ControllerManager.Instance.AddSelectableUiElement(taskAddButton2.Button);
switch (m)
{
case 0 when instance.restorePreviousSelectionFound != null:
ControllerManager.Instance.SetDefaultSelection(instance.restorePreviousSelectionFound);
instance.restorePreviousSelectionByFolderName = string.Empty;
instance.restorePreviousSelectionFound = null;
break;
case 0:
ControllerManager.Instance.SetDefaultSelection(taskAddButton2.Button);
break;
}
}
}
}
@@ -20,6 +20,11 @@ public static class ButtonResetPatches
{
Logger<MiraApiPlugin>.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
}
if (customActionButton.UsesMode == ButtonUsesMode.PerRound)
{
customActionButton.SetUses(customActionButton.MaxUses);
}
}
}
}
+60 -1
View File
@@ -1,7 +1,14 @@
using HarmonyLib;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Rewired;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -18,6 +25,8 @@ public static class HudManagerPatches
public static Transform? BottomRight { get; private set; }
public static Transform? Buttons { get; private set; }
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = new();
/*
/// <summary>
/// Trigger hudstart on current custom gamemode
@@ -92,6 +101,31 @@ public static class HudManagerPatches
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
vanillaKeybindIcons = [];
var keybindIconPos = new Vector3(-0.4f, 0.4f, -9.5f);
var vanillaButtons = new Dictionary<GameObject, int>
{
{ __instance.KillButton.gameObject, 8 },
{ __instance.UseButton.gameObject, 6 },
{ __instance.ReportButton.gameObject, 7 },
{ __instance.ImpostorVentButton.gameObject, 50 },
{ __instance.SabotageButton.gameObject, 4 },
};
foreach (var kvp in vanillaButtons)
{
var buttonObj = kvp.Key;
var actionId = kvp.Value;
var key = KeybindUtils.GetKeycodeByActionId(actionId);
if (key == KeyboardKeyCode.None)
{
continue;
}
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
}
}
/// <summary>
@@ -123,4 +157,29 @@ public static class HudManagerPatches
}
}
}
[HarmonyPatch(nameof(HudManager.Update))]
[HarmonyPostfix]
public static void UpdatePostfix()
{
foreach (var btnIcon in vanillaKeybindIcons)
{
btnIcon.Key.text = KeybindUtils.GetKeycodeByActionId(btnIcon.Value).ToString();
btnIcon.Key.transform.parent.gameObject.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value);
}
foreach (var entry in KeybindManager.Keybinds)
{
var player = ReInput.players.GetPlayer(0);
var keyboard = player.controllers.Keyboard;
bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) ||
entry.ModifierKeys.Length <= 0;
if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
{
entry.Invoke();
}
}
}
}
+15
View File
@@ -29,6 +29,16 @@ public static class IntroCutscenePatches
MiraEventManager.InvokeEvent(@event);
}
[HarmonyPostfix]
[HarmonyPatch(typeof(IntroCutscene._ShowRole_d__41), nameof(IntroCutscene._ShowRole_d__41.MoveNext))]
[HarmonyPriority(Priority.Last)]
public static void ShowRolePatch(IntroCutscene._ShowRole_d__41 __instance)
{
var realInstance = __instance.__4__this;
var @event = new IntroRoleRevealEvent(realInstance);
MiraEventManager.InvokeEvent(@event);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(IntroCutscene.BeginImpostor))]
[HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))]
@@ -61,6 +71,11 @@ public static class IntroCutscenePatches
public static void GameBeginPatch(IntroCutscene __instance)
{
MiraEventManager.InvokeEvent(new IntroEndEvent(__instance));
var @event = new BeforeRoundStartEvent(true);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled) return;
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
@@ -0,0 +1,67 @@
using System.Linq;
using HarmonyLib;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Rewired.UI.ControlMapper;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(ControlMapper))]
public static class ControlMapperPatches
{
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Start))]
private static void StartPrefix(ControlMapper __instance)
{
var doneButton = GameObject.Find("DoneButton")?.GetComponent<CustomButton>();
if (doneButton == null)
{
return;
}
var toggle = Object.Instantiate(doneButton, doneButton.transform.parent);
var text = toggle.GetComponentInChildren<TextMeshProUGUI>();
var entry = LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds;
toggle.gameObject.name = "KeybindsVisibleToggle";
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
toggle.onClick.RemoveAllListeners();
toggle.GetComponent<ButtonInfo>().identifier = "KeybindsVisibleToggle";
toggle.onClick.AddListener((UnityAction)(() =>
{
entry.Value = !entry.Value;
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
}));
}
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Update))]
private static void UpdatePrefix(ControlMapper __instance)
{
foreach (var element in __instance.themedElements)
{
var info = element.GetComponent<InputFieldInfo>();
if (info == null)
{
continue;
}
var key = info.glyphOrText?.actionElementMap?.keyboardKeyCode;
if (key == null)
{
continue;
}
var conflicts = KeybindManager.GetConflicts();
if (conflicts.Any(x => x.Key.ToString() == key.ToString()))
{
element.GetComponent<Image>().color = Color.red;
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
using HarmonyLib;
using MiraAPI.Keybinds;
using Rewired;
namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(InputManager_Base), nameof(InputManager_Base.Awake))]
public static class KeybindMenuPatch
{
private static bool _registered;
[HarmonyPrefix]
private static void AwakePrefix(InputManager_Base __instance)
{
if (_registered)
{
return;
}
KeybindUtils.RewiredInputManager = __instance;
KeybindManager.RewiredInit();
_registered = true;
}
}
@@ -0,0 +1,40 @@
using HarmonyLib;
using MiraAPI.Keybinds;
using UnityEngine;
namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(OptionsMenuBehaviour))]
public static class OptionsMenuBehaviourPatch
{
private static ButtonRolloverHandler? _remap_rollover;
private static SpriteRenderer? _remap_background;
private static bool _conflicts => KeybindManager.GetConflicts().Count > 0;
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Open))]
private static void OpenPostfix(OptionsMenuBehaviour __instance)
{
_remap_rollover = GameObject.Find("Remap Controls")?.GetComponent<ButtonRolloverHandler>()!;
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Update))]
private static void UpdatePostfix(OptionsMenuBehaviour __instance)
{
if (_remap_rollover == null)
{
return;
}
if (_remap_background == null)
{
return;
}
_remap_rollover.OutColor = _conflicts ? Color.red : Color.white;
_remap_rollover.UnselectedColor = _conflicts ? Color.red : Color.white;
_remap_rollover.OverColor = _conflicts ? new Color32(255, 55, 55, 255) : Palette.AcceptedGreen;
}
}
@@ -0,0 +1,232 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Patches.LocalSettings;
[HarmonyPatch(typeof(OptionsMenuBehaviour))]
public static class OptionsMenuPatches
{
internal static OptionsMenuBehaviour? Instance { get; private set; }
internal static BoxCollider2D MaskCollider;
private static SpriteRenderer? background;
private static int currentPage = 1;
private static Dictionary<int, List<GameObject>> tabButtons = new();
/// <summary>
/// Creates the tabs and their content
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Start))]
public static void StartPostfix(OptionsMenuBehaviour __instance)
{
Instance = __instance;
// Fix for tabs not being clickable in the main menu
if (!AmongUsClient.Instance.IsInGame)
{
__instance.Background.GetComponent<BoxCollider2D>().enabled = false;
__instance.transform.FindChild("Tint").SetLocalZ(5.6f);
}
background = __instance.Background;
var maskObj = new GameObject
{
layer = 5,
name = "SpriteMask",
};
maskObj.transform.SetParent(__instance.transform);
maskObj.transform.localPosition = new Vector3(0, -0.3f, 0);
maskObj.transform.localScale = new Vector3(500, 120, 1);
var blank = Sprite.Create(
Texture2D.whiteTexture,
new Rect(0, 0, Texture2D.whiteTexture.width, Texture2D.whiteTexture.height),
new Vector2(0.5f, 0.5f));
var mask = maskObj.AddComponent<SpriteMask>();
mask.sprite = blank;
mask.isCustomRangeActive = true;
mask.sortingLayerName = "Default";
mask.backSortingOrder = 100;
mask.frontSortingOrder = 500;
MaskCollider = maskObj.AddComponent<BoxCollider2D>();
MaskCollider.size = new Vector2(0.01f, 0.1f);
MaskCollider.isTrigger = true;
MaskCollider.enabled = true;
currentPage = 1;
tabButtons.Clear();
float yOffset = 0;
int i = 0;
int tabIdx = 0;
int page = 1;
foreach (var settings in LocalSettingsManager.Tabs)
{
var tab = settings.CreateTab(__instance);
if (settings.ShouldCreateButton)
{
var button = settings.CreateTabButton(__instance, ref tabIdx, ref yOffset);
button.GetComponent<TabGroup>().Content = tab;
button.SetActive(page == currentPage);
if (!tabButtons.TryGetValue(page, out var list))
{
tabButtons.Add(page, new());
list = tabButtons[page];
}
list.Add(button);
yOffset += 0.6f;
tabIdx++;
i++;
}
if (i > 7)
{
i = 0;
yOffset = 0;
page++;
}
}
void ChangePage(int increase)
{
currentPage += increase;
if (currentPage > tabButtons.Count)
{
currentPage = 1;
}
if (currentPage < 1)
{
currentPage = tabButtons.Count;
}
}
// Create page buttons
// TODO: In-game menu has no close button. I tried making one from scratch but failed. Add one later plz
if (tabButtons.Count <= 1)
{
return;
}
var nextButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
nextButton.transform.localPosition = new Vector3(3.2f, 2.6f, -10);
nextButton.transform.localScale = new Vector2(0.25f, 0.25f);
var nextRend = nextButton.GetComponent<SpriteRenderer>();
nextRend.sprite = MiraAssets.NextButton.LoadAsset();
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
var nextRollover = nextButton.gameObject.AddComponent<ButtonRolloverHandler>();
nextRollover.Target = nextRend;
nextRollover.OverColor = Palette.AcceptedGreen;
nextButton.OnClick = new Button.ButtonClickedEvent();
nextButton.OnClick.AddListener(
(UnityAction)(() =>
{
ChangePage(1);
UpdatePages();
}));
var backButton = Object.Instantiate(__instance.BackButton, __instance.transform).GetComponent<PassiveButton>();
backButton.transform.localPosition = new Vector3(2.9f, 2.6f, -10);
backButton.transform.localScale = new Vector2(0.25f, 0.25f);
var backRend = backButton.GetComponent<SpriteRenderer>();
backRend.sprite = MiraAssets.NextButton.LoadAsset();
backRend.flipX = true;
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
var backRollover = backButton.gameObject.AddComponent<ButtonRolloverHandler>();
backRollover.Target = backRend;
backRollover.OverColor = Palette.AcceptedGreen;
backButton.OnClick = new Button.ButtonClickedEvent();
backButton.OnClick.AddListener(
(UnityAction)(() =>
{
ChangePage(-1);
UpdatePages();
}));
}
/// <summary>
/// Modifies how opening tabs is handled for the custom ones to work.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.OpenTabGroup))]
public static bool OpenTabGroupPrefix(OptionsMenuBehaviour __instance, ref int index)
{
__instance.Tabs.ToList().ForEach(x => x.Close()); // Close all vanilla tabs
LocalSettingsManager.Tabs.ForEach(CustomClose); // Close all mod tabs
// Tabs with index 10 and above are the mod settings tabs
if (index >= 10)
{
CustomOpen(LocalSettingsManager.Tabs[index - 10]);
return false;
}
return true;
}
/// <summary>
/// Makes the custom tabs close when opening the options menu and resets pages.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Open))]
public static void OpenPostfix(OptionsMenuBehaviour __instance)
{
LocalSettingsManager.Tabs.ForEach(CustomClose);
currentPage = 1;
UpdatePages();
}
private static void UpdatePages()
{
foreach (var pages in tabButtons)
{
pages.Value?
.Where(x => x != null)
.Do(
x => x?.SetActive(
pages.Key == currentPage));
}
}
private static void CustomOpen(LocalSettingsTab tab)
{
if (tab.TabButton?.Button)
tab.TabButton!.Button.color = tab.TabAppearance.TabButtonHoverColor;
if (tab.TabButton?.Rollover)
tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonHoverColor;
if (tab.TabButton?.Content)
tab.TabButton!.Content.SetActive(true);
if (background != null)
background.color = tab.TabAppearance.TabColor;
}
private static void CustomClose(LocalSettingsTab tab)
{
if (tab.TabButton?.Button)
tab.TabButton!.Button.color = tab.TabAppearance.TabButtonColor;
if (tab.TabButton?.Rollover)
tab.TabButton!.Rollover.OutColor = tab.TabAppearance.TabButtonColor;
if (tab.TabButton?.Content)
tab.TabButton!.Content.SetActive(false);
if (background != null)
background.color = Color.white;
}
}
@@ -0,0 +1,37 @@
using HarmonyLib;
using UnityEngine;
namespace MiraAPI.Patches.LocalSettings;
[HarmonyPatch(typeof(ToggleButtonBehaviour))]
public static class ToggleBehaviourPatch
{
/// <summary>
/// ResetText was inlined ofc.
/// Skill issue
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))]
public static bool UpdateTextPrefix(ToggleButtonBehaviour __instance, bool on)
{
var color = on ? new Color(0f, 1f, 0.16470589f, 1f) : Color.white;
__instance.onState = on;
__instance.Background.color = color;
// Used to mask the text
__instance.Text.richText = true;
__instance.Text.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF - Chat Message Masked\">" +
$"{DestroyableSingleton<TranslationController>.Instance.GetString(__instance.BaseText)}: <b>" +
$"{DestroyableSingleton<TranslationController>.Instance.GetString(__instance.onState ? StringNames.SettingsOn : StringNames.SettingsOff)}</b></font>";
__instance.Text.sortingOrder = 151;
__instance.Text.renderer.sortingLayerName = "Default";
__instance.Text.renderer.sortingOrder = 151;
if (__instance.Rollover)
{
__instance.Rollover.ChangeOutColor(color);
}
return false;
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
@@ -6,6 +6,7 @@ using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
@@ -28,22 +29,6 @@ internal static class GamePresetsTabPatches
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
{
@@ -207,7 +192,7 @@ internal static class GamePresetsTabPatches
saveText.transform.parent.localPosition.y,
saveText.transform.parent.localPosition.z);
DivideSize(saveButton.gameObject, 2f);
Helpers.DivideSize(saveButton.gameObject, 2f);
saveButton.OnClick = new Button.ButtonClickedEvent();
saveButton.OnClick.AddListener(
@@ -276,7 +261,7 @@ internal static class GamePresetsTabPatches
var iconSpriteRend = icon.AddComponent<SpriteRenderer>();
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
DivideSize(refreshButton.gameObject, 2f);
Helpers.DivideSize(refreshButton.gameObject, 2f);
refreshButton.buttonText.gameObject.Destroy();
refreshButton.OnClick = new Button.ButtonClickedEvent();
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
@@ -528,7 +528,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.CountPlusBtn.SetInteractable(true);
__instance.roleChances.Add(roleOptionSetting);
roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f);
roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.25f, -0.2923f, 0f);
roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor();
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
@@ -551,6 +551,18 @@ public static class RoleSettingMenuPatches
passiveButton.interactableHoveredColor = Color.white;
passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); }));
if (customRole.Configuration.Icon != null)
{
var roleIcon = new GameObject("RoleIcon");
roleIcon.transform.parent = roleOptionSetting.transform;
roleIcon.transform.localScale = new(.25f, .25f, 1);
roleIcon.layer = LayerMask.NameToLayer("UI");
roleIcon.transform.localPosition = new Vector3(-1.3f, -0.3f, -2f);
var rend = roleIcon.AddComponent<SpriteRenderer>();
rend.sprite = customRole.Configuration.Icon.LoadAsset();
rend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
}
if (customRole.Configuration is { MaxRoleCount: 0 })
+6 -1
View File
@@ -38,8 +38,13 @@ internal static class EjectionPatches
{
public static void Postfix()
{
var @event = new RoundStartEvent(false);
var @event = new BeforeRoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled) return;
var @event2 = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event2);
}
}
}
+8 -8
View File
@@ -12,7 +12,7 @@ namespace MiraAPI.Patches.Roles;
public static class HudManagerPatches
{
// Custom role tab.
private static TaskPanelBehaviour? _roleTab;
public static TaskPanelBehaviour? RoleTab;
/// <summary>
/// Fixes Kill Button not showing for Neutral killing role.
@@ -38,9 +38,9 @@ public static class HudManagerPatches
__instance.SabotageButton.gameObject.SetActive(isActive && customRole.Configuration.CanUseSabotage);
}
if (_roleTab)
if (RoleTab)
{
_roleTab?.gameObject.SetActive(isActive);
RoleTab?.gameObject.SetActive(isActive);
}
}
@@ -62,16 +62,16 @@ public static class HudManagerPatches
if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole)
{
if (_roleTab == null)
if (RoleTab == null)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
RoleTab = CustomRoleManager.CreateRoleTab(customRole);
}
CustomRoleManager.UpdateRoleTab(_roleTab, customRole);
CustomRoleManager.UpdateRoleTab(RoleTab, customRole);
}
else if (_roleTab)
else if (RoleTab)
{
_roleTab?.gameObject.Destroy();
RoleTab?.gameObject.Destroy();
}
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ public static class RoleManagerPatches
data.Role.Deinitialize(targetPlayer);
Object.Destroy(data.Role.gameObject);
}
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform);
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.ToArray().First(r => r.Role == roleType), data.gameObject.transform);
roleBehaviour.Initialize(targetPlayer);
targetPlayer.Data.Role = roleBehaviour;
targetPlayer.Data.RoleType = roleType;
+8 -1
View File
@@ -1,6 +1,7 @@
using HarmonyLib;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
namespace MiraAPI.Patches;
@@ -14,6 +15,8 @@ public static class ServerDropdownPatch
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;
@@ -27,6 +30,10 @@ public static class ServerDropdownPatch
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;
@@ -39,7 +46,7 @@ public static class ServerDropdownPatch
serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); }));
__instance.controllerSelectable.Add(serverListButton.Button);
__instance.background.transform.localPosition = new Vector3(
0f,
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)));
+68 -6
View File
@@ -1,16 +1,78 @@
using HarmonyLib;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))]
internal static class DummyBehaviourPatch
[HarmonyPatch(typeof(DummyBehaviour))]
internal static class DummyBehaviourPatches
{
public static void Postfix(DummyBehaviour __instance)
[HarmonyPrefix]
[HarmonyPatch(nameof(DummyBehaviour.Update))]
public static bool DummyUpdatePatch(DummyBehaviour __instance)
{
if (MeetingHud.Instance && __instance.myPlayer.GetVoteData())
NetworkedPlayerInfo data = __instance.myPlayer.Data;
if (data == null || data.IsDead) return false;
if (MeetingHud.Instance)
{
__instance.voted = __instance.myPlayer.GetVoteData().VotesRemaining == 0;
if (!__instance.voted)
{
__instance.StartCoroutine(CoDoVote(__instance).WrapToIl2Cpp());
}
}
else
{
__instance.voted = false;
}
return false;
}
private static IEnumerator CoDoVote(DummyBehaviour dummy)
{
dummy.voted = true;
yield return new WaitForSeconds(dummy.voteTime.Next());
var dummyVoteEvent = new DummyVoteEvent(dummy);
MiraEventManager.InvokeEvent(dummyVoteEvent);
if (dummyVoteEvent.IsCancelled)
{
dummy.voted = false;
yield break;
}
List<byte> potentialSuspects = new();
potentialSuspects.AddRange(PlayerControl.AllPlayerControls
.ToArray()
.Where(p => p != dummy.myPlayer)
.Where(p => dummyVoteEvent.PlayerIsValid(p))
.Select(p => p.PlayerId));
if (dummyVoteEvent.CanSkip)
{
potentialSuspects.Add(253);
}
if (CanVote(dummy))
{
VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, dummy.myPlayer.PlayerId, potentialSuspects.Random());
}
}
private static bool CanVote(this DummyBehaviour dummy)
{
var dummyVoteData = dummy.myPlayer.GetVoteData();
return dummyVoteData.VotesRemaining > 0;
}
}
@@ -15,6 +15,24 @@ namespace MiraAPI.Patches.Voting;
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")]
internal static class MeetingHudPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.VoteForMe))]
public static bool VotePatch(PlayerVoteArea __instance)
{
var beforeVoteEvent = new BeforeVoteEvent(__instance, PlayerControl.LocalPlayer);
MiraEventManager.InvokeEvent(beforeVoteEvent);
if (beforeVoteEvent.IsCancelled)
{
return false;
}
var afterVoteEvent = new AfterVoteEvent(__instance, PlayerControl.LocalPlayer);
MiraEventManager.InvokeEvent(afterVoteEvent);
return true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.Start))]
public static void MeetingHudStartPatch(MeetingHud __instance)
-8
View File
@@ -1,8 +0,0 @@
using System;
namespace MiraAPI.PluginLoading;
public interface IMiraExtension
{
public Type GetBasePluginType();
}
+1
View File
@@ -5,6 +5,7 @@ using BepInEx.Configuration;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.LocalSettings;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
+216 -95
View File
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using MiraAPI.Colors;
using MiraAPI.Events;
@@ -11,12 +12,16 @@ using MiraAPI.GameEnd;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.PluginLoading;
@@ -37,32 +42,111 @@ 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;
}
else 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 (RegisterLocalTabs(type, plugin))
{
continue;
}
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
RegisterColorClasses(type);
RegisterKeybinds(type, plugin);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
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);
};
RegisterKeybinds(typeof(MiraGlobalKeybinds), PluginSingleton<MiraApiPlugin>.Instance);
RegisterLocalTabs(typeof(MiraApiSettings), PluginSingleton<MiraApiPlugin>.Instance);
}
/// <summary>
@@ -75,90 +159,6 @@ public sealed class MiraPluginManager
return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId);
}
private void RegisterPlugin(MiraPluginInfo info, Assembly assembly)
{
Logger<MiraApiPlugin>.Info($"Registering plugin {info.PluginId} with Mira API.");
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.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.TryAdd(info, []);
QueuedRoleRegistrations[info].AddRange(roles);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"{info.PluginId} finished registering.");
}
private static bool RegisterGameOver(Type type)
{
try
@@ -317,4 +317,125 @@ public sealed class MiraPluginManager
return false;
}
private static bool RegisterLocalTabs(Type type, BasePlugin pluginInfo)
{
try
{
if (!type.IsAssignableTo(typeof(LocalSettingsTab)))
{
return false;
}
if (!LocalSettingsManager.RegisterTab(type, pluginInfo))
{
return false;
}
foreach (var property in type.GetProperties())
{
if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance)
{
continue;
}
if (property.GetCustomAttribute<LocalSettingsButtonAttribute>() != null &&
property.PropertyType.IsAssignableTo(typeof(LocalSettingsButton)))
{
var button = property.GetValue(tabInstance) as LocalSettingsButton;
button!.Tab = tabInstance;
tabInstance.Buttons.Add(button);
continue;
}
if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType))
{
continue;
}
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} has to be a config entry.");
continue;
}
var attribute = property.GetCustomAttribute<LocalSettingAttribute>();
if (attribute == null)
{
continue;
}
attribute.CreateSetting(type, configEntry);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(ConfigEntryBase)) && f.GetCustomAttribute<LocalSettingAttribute>() != null))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for local settings.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
}
private static void RegisterKeybinds(Type type, BasePlugin source)
{
try
{
if (type.GetCustomAttribute<RegisterCustomKeybindsAttribute>() == null)
{
return;
}
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.Error($"Keybinds class {type.Name} must be static.");
return;
}
foreach (var property in type.GetProperties())
{
if (property.PropertyType != typeof(MiraKeybind))
{
continue;
}
if (property.GetValue(null) is not MiraKeybind keybind)
{
Logger<MiraApiPlugin>.Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind.");
continue;
}
KeybindManager.Keybinds.Add(keybind);
if (source is IMiraPlugin miraPlugin)
{
keybind.SourcePluginName = miraPlugin.OptionsTitleText;
}
else if (source is MiraApiPlugin)
{
keybind.SourcePluginName = "MiraAPI";
}
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(MiraKeybind))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for keybinds.");
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register keybind class {type.Name}: {e}");
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.
+11
View File
@@ -32,6 +32,12 @@ public record struct CustomRoleConfiguration
TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate;
HideSettings = roleBehaviour?.IsDead == true;
ShowInFreeplay = roleBehaviour?.IsDead == false;
FreeplayFolder = role.Team switch
{
ModdedRoleTeams.Crewmate => "Crewmate",
ModdedRoleTeams.Impostor => "Impostor",
_ => "Neutral",
};
IntroSound = role.Team is ModdedRoleTeams.Crewmate
? CustomRoleManager.CrewmateIntroSound
: CustomRoleManager.ImpostorIntroSound;
@@ -112,6 +118,11 @@ public record struct CustomRoleConfiguration
/// </summary>
public bool ShowInFreeplay { get; set; }
/// <summary>
/// Gets or sets a freeplay folder this role belongs to. Defaults to team folder.
/// </summary>
public string FreeplayFolder { get; set; }
/// <summary>
/// Gets or sets the outline color for the KillButton if <see cref="UseVanillaKillButton"/> is true.
/// </summary>
+9 -1
View File
@@ -35,6 +35,11 @@ public static class CustomRoleManager
public static readonly LoadableAsset<AudioClip> ImpostorIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
/// <summary>
/// Gets the list of all roles from vanilla RoleManager.
/// </summary>
public static RoleBehaviour[] AllRoles => RoleManager.Instance.AllRoles.ToArray();
/// <summary>
/// Gets the list of custom roles as RoleBehaviour objects.
/// </summary>
@@ -57,7 +62,10 @@ public static class CustomRoleManager
internal static void RegisterInRoleManager()
{
RoleManager.Instance.AllRoles = RoleManager.Instance.AllRoles.Concat(CustomRoles.Values).ToArray();
foreach (var role in CustomRoles.Values)
{
RoleManager.Instance.AllRoles.Add(role);
}
foreach (var role in CustomRoles.Values.Where(x => x.IsDead))
{
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more