Compare commits

..
Author SHA1 Message Date
Atony 245f2e6919 Merge pull request #83 from All-Of-Us-Mods/dev
## What's Changed
- Guardian Angel protection is checked properly.
- The Options Menu no longer throws errors on Android
- Framerate slider applies properly on PC again
- StateMachineWrapper now allows overriding existing states and returns for Coroutines.
2026-04-18 08:41:18 -05:00
Atony fed3abafb2 Bump version 2026-04-18 08:31:59 -05:00
Atony 925d47a275 Fix framerate slider 2026-04-18 08:29:11 -05:00
Atony 45a3597ab2 Update dependencies 2026-04-04 20:50:27 -05:00
Chipseq faeb39d3de use AccessTools to allow any method to be an event handler 2026-04-04 22:05:41 +02:00
Atony 6b46cffe5e Add isKilling check for basic kills 2026-04-03 20:31:27 -05:00
Atony 00a60bb7ad Add methods for StateMachineWrapper 2026-04-03 09:46:44 -05:00
Atony d89bce6956 Merge branch 'master' into dev 2026-04-01 21:33:21 -05:00
Atony 7f92e94293 Fix options menu error on mobile 2026-04-01 21:30:28 -05:00
Atony 6199e55110 Add Guardian Angel protection checks 2026-04-01 21:30:19 -05:00
Pietro420 eac442443b Random Snapshot for kill anim 2026-03-29 22:34:43 -03:00
Pietro420 6321753ec0 Don't use nullable for kill anim 2026-03-29 22:26:07 -03:00
XtraCube e66d45bd21 revert ID change for now 2026-03-23 19:33:16 -04:00
XtraCube 1e00181220 Change plugin identifier for Mira API 2026-03-23 12:07:08 -04:00
Atony fcd7856571 Merge pull request #82 from All-Of-Us-Mods/dev
MiraAPI 0.3.9
2026-03-21 16:25:04 -05:00
Atony 5680d27db1 Merge branch 'master' into dev 2026-03-21 16:22:27 -05:00
Atony 49d77fcf40 Fix issues with PlayerControl.Die race condition 2026-03-11 15:40:50 -05:00
Atony dcb4a3876f Fix neutral color in view pane 2026-03-11 00:23:32 -05:00
Atony 9270de408d Verify that target isn't already dead or dc'd 2026-03-10 18:04:22 -05:00
Atony 8940de76bb Add one last murder check 2026-03-10 00:18:55 -05:00
Atony 1813fe5a84 Adjust functions 2026-03-09 09:43:00 -05:00
Atony 272b14f0e0 Change murders to check with the host beforehand 2026-03-09 09:35:36 -05:00
XtraCube 6cc1d4b762 updated readme 2026-02-27 12:11:41 -05:00
Atony 9dfad78df6 Merge pull request #81 from All-Of-Us-Mods/dev
MiraAPI 0.3.8
2026-02-11 11:50:15 -06:00
Atony 7f506000a4 Check for vanilla kills 2026-02-11 11:40:55 -06:00
Atony 795b6a81d7 Merge branch 'dev' of https://github.com/All-Of-Us-Mods/MiraAPI into dev 2026-02-10 14:00:51 -06:00
Atony bd8d7a3208 Add lobby checks for kills 2026-02-10 14:00:47 -06:00
Atony e8536e5d4b Merge pull request #80 from All-Of-Us-Mods/dev
## What's New
- Individual game options can be reset if they are a part of a preset.
- A new meeting-specific murder rpc is now available for use, checking for kills happening within and outside of a meeting.
- ## What's Changed
- Framerate Slider has been renamed to be more accurate
- Memory usage per frame has been improved
2026-02-08 16:30:05 -06:00
Atony 090db766b2 Bump version from 0.3.6 to 0.3.7 2026-02-08 16:25:37 -06:00
Atony 1f0027108e Automatically change reactor dev build indicator 2026-02-01 13:22:57 -06:00
Atony 665724010d Fix how to play errors without custom roles 2026-01-26 13:02:39 -06:00
Atony 2d58c91289 Replace alt murder rpc 2026-01-21 04:47:27 -06:00
XtraCube b3ed6d67f9 Reduce memory allocations in HudManagerPatches.cs 2026-01-20 16:35:44 -05:00
Atony 64ef78e3a2 Fix wordwrap on local setting 2026-01-20 10:21:37 -06:00
Atony 5e9e4012ef Update CustomMurderRpc.cs 2026-01-19 21:38:45 -06:00
Atony fe04a7033a Change kill button to use alt murder rpc 2026-01-19 21:15:52 -06:00
Atony ba0ee49255 Add reset button for game options 2026-01-19 20:10:21 -06:00
Atony 97528b4380 Add alternative Murder RPC for meeting checks 2026-01-19 19:31:31 -06:00
Atony 50030c113b Merge pull request #78 from All-Of-Us-Mods/dev
## What's New
- Screen shake no longer interferes with scrolling in the host options menu
## What's Changed
- Sabotage flashes no longer break on Starlight
- Using the button scale setting no longer breaks the UI.
- If a role fails to register to a marshalling error (nullables) the game will no longer crash.
2026-01-04 12:41:46 -06:00
Atony 20e51cc993 Bump version 2026-01-04 12:38:43 -06:00
Atony 64b5e01ba1 Improve scrolling in roles menu itself 2026-01-03 19:06:51 -06:00
Atony d4469153ab Improve scrolling in roles menu 2026-01-03 19:00:46 -06:00
Atony ab67c233d8 Add FPS slider (default to 120 fps) 2026-01-03 18:55:12 -06:00
Atony c72eacc7a2 Fix options scroll with screen shake 2026-01-03 18:54:57 -06:00
Atony 13072d6834 Fix dualshock light errors on Starlight 2026-01-02 22:57:58 -06:00
Atony c6342750ef Add checks to prevent errors on Starlight 2026-01-02 15:17:31 -06:00
XtraCube 5c2e95376d add exception handling around role registration 2025-12-31 22:38:01 -05:00
Atony 067859b9fd Fix scale factor not setting properly when changed 2025-12-31 18:36:19 -06:00
Atony fe3bfcdbc4 Merge pull request #77 from All-Of-Us-Mods/dev
- Added Vanilla Button Click Event (and respective Cancel Event)
- Added accessibility settings for sabotage flashes and blares
- Added an in-game slider for resizing ability buttons
2025-12-28 18:00:57 -06:00
Atony 5d01462a32 Adjust local setting groups 2025-12-28 17:55:33 -06:00
Atony a9b6f40ed0 Add Button Scale Slider 2025-12-28 17:51:39 -06:00
Atony eae0024ec3 Add Accessibility settings for sabotages 2025-12-22 21:41:36 -06:00
Atony 2907564d6a Add Vanilla Button Click Event (& Cancel Event) 2025-12-16 22:14:50 -06:00
Atony 7ad003ed1e Merge pull request #75 from All-Of-Us-Mods/dev
- Adds custom game tabs, with their own names and description blurbs.
- All keybinds have been moved to the right for consistency and to prevent hide and seek vent uses being obscured.
- Holding shift will change most options to be split in half, like 2.5s -> 1.25s
- Vanilla roles can be blacklisted from spawning or appearing in menus via other mira mods.
- Roles can check if they can spawn on the current gamemode or under specific conditions, such as the current map.
- Adds options that can have customizable text for -1 and 0 values respectively.
- Adds support for modded roles in Hide and Seek
- Fixes vanilla roles bypassing murder attempts
- Fixes keybinds not being hidden on buttons
- Fixes buttons used via keybinds bypassing ButtonClickEvents
- Fixes crashing for Epic Games users when opening the keybinds menu.
2025-12-14 07:58:01 -06:00
Atony 93eea6ea2f Bump version 2025-12-14 07:50:55 -06:00
Atony 8e09b7fee0 Merge pull request #74 from All-Of-Us-Mods/new-features
- Adds custom game tabs, with their own names and description blurbs.
- All keybinds have been moved to the right for consistency and to prevent hide and seek vent uses being obscured.
- Holding shift will split options in half
- Vanilla roles can be blacklisted from spawning or appearing in menus via other mira mods.
- Roles can check if they can spawn on the current gamemode or under specific conditions, such as the current map.
- Add options that can have customizable text for -1 and 0 values respectively.
- Fix buttons used via keybinds bypassing ButtonClickEvents
2025-12-14 07:47:27 -06:00
Atony 1e4894538c Merge branch 'dev' into new-features 2025-12-14 07:40:15 -06:00
Atony 303aedafc7 Fix keybinds bypassing button click events 2025-12-13 15:29:12 -06:00
Atony 8ed4355504 Clear up I2lCpp warning 2025-12-08 12:28:02 -06:00
Atony 76b02835a7 Update HudManagerPatches.cs 2025-11-30 15:30:17 -06:00
Atony 8d5bc710e7 Move all binds to the top right of buttons 2025-11-30 15:22:37 -06:00
Atony 798ce451d5 Add text for setting descriptions 2025-11-29 14:21:25 -06:00
Atony 136bbde516 Fix MaxUses check 2025-11-29 13:40:21 -06:00
Atony 5252620108 Update CustomActionButton.cs 2025-11-27 23:00:24 -06:00
Atony c9b1c57774 Add toggle for buttons to change uses check 2025-11-27 22:54:03 -06:00
Atony d97df1a891 Add options for negatives as infinity 2025-11-27 22:30:04 -06:00
Atony e93ef6fd1d Add special modded number type 2025-11-27 21:34:20 -06:00
Atony 3afe437101 Fix Hns Task Event not registering properly 2025-11-23 12:50:52 -06:00
Atony 62c01b2f72 Add Hns Manager into the task event 2025-11-23 12:05:31 -06:00
Atony 089d2c1ea5 Add Hide and seek task event 2025-11-23 12:01:41 -06:00
Atony c65511d03e Add checks to hide roles from menus 2025-11-22 15:26:07 -06:00
Atony aee304fb96 Fix nullable roletype errors 2025-11-22 08:19:58 -06:00
Atony 11ba564eed Fix seeker role assignment 2025-11-21 23:18:41 -06:00
Atony 259b9b7097 Add logic for modifiers & roles in hide and seek 2025-11-21 18:23:08 -06:00
Atony 7e3a279b56 Fix imps and crew being shown twice in view pane 2025-11-20 21:51:39 -06:00
Atony 1fd2dd3539 Allow mods to blacklist vanilla roles 2025-11-20 21:39:45 -06:00
XtraCube 3f60bcae2c default constructor not supported for custom role config 2025-11-20 10:18:39 -05:00
Atony fe1580bb7e Fix double round events on Airship & Submerged 2025-11-17 18:45:32 -06:00
Atony 8b23021b2a Fix null error for player without assigned role 2025-11-16 08:41:29 -06:00
Atony fdc715dac2 Expose data for option names 2025-11-07 17:26:06 -06:00
Atony e1ca50be3e Vanilla roles no longer bypass murder cancels 2025-11-01 10:00:42 -05:00
Atony bf9b34e728 Fix modded keybinds not being hidden on buttons 2025-10-26 13:36:35 -05:00
Atony baf80618bb Rename keybinds option 2025-10-26 13:19:22 -05:00
Atony 7d1976f772 Relocale keybinds toggle to local options for epic 2025-10-26 13:15:20 -05:00
Atony 4ce24c2a00 Fix warning on epic games 2025-10-26 12:20:02 -05:00
Atony 7fa75946fd Make ResetCooldowns callable 2025-10-25 18:51:14 -05:00
Atony 43f92d4dc2 Standadize button position 2025-10-25 12:17:44 -05:00
Atony ee22b776eb Fix Vanilla view pane 2025-10-20 18:23:13 -05:00
Atony f31349f6e8 Add shift-intervals for options 2025-10-20 11:17:51 -05:00
Atony 448e2f9152 Adjust size and scale 2025-10-18 13:27:33 -05:00
Atony dde7e70406 Fix category names 2025-10-18 12:32:34 -05:00
Atony 713a7a2bcc Work on adding custom game tabs 2025-10-18 12:19:15 -05:00
Atony 056a9b6102 Merge pull request #72 from All-Of-Us-Mods/dev
Fix Logger error for v0.3.3
2025-10-14 13:53:21 -05:00
Atony 4b144a782d Fix Logger error 2025-10-14 13:53:02 -05:00
Atony ae917a9c77 Merge pull request #71 from All-Of-Us-Mods/dev
MiraAPI v0.3.3
2025-10-14 13:46:58 -05:00
Atony 7a8d608b17 Bump Version (again) 2025-10-14 13:46:08 -05:00
XtraCube 6503bbecb7 use reactor static logging pattern 2025-10-14 14:37:20 -04:00
XtraCube cb794c3fc2 make kill anim patch update resilient 2025-10-14 14:35:20 -04:00
XtraCube 88329bef79 update freeplay patch to be version resistant 2025-10-14 14:14:25 -04:00
XtraCube eb7abecd75 update to latest among us 2025-10-14 14:06:43 -04:00
Atony fc9e9740be Merge pull request #70 from All-Of-Us-Mods/dev
MiraAPI v0.3.2
2025-10-14 09:08:53 -05:00
Atony 3c51c88963 Bump version 2025-10-14 09:05:25 -05:00
Atony e42f5e6029 Log exception 2025-10-12 19:44:30 -05:00
Atony 61be7dda82 Maybe fix the issues with kill animations? 2025-10-12 18:33:45 -05:00
xChipseq 3220a4a820 change texture wrap mode to clamp 2025-10-11 00:46:35 +02:00
XtraCube 8fd142a92e fix custom murder dead body 2025-10-04 14:07:00 -04:00
XtraCube fcec6ad6e5 fix new role fields 2025-10-01 22:17:40 -04:00
XtraCube 10a7ffe886 add new role information to try and fix broken gamemanager 2025-10-01 20:59:44 -04:00
Atony 7e541733b7 More fixes for role names 2025-09-30 18:15:29 -05:00
Atony 99a275a97c Adjust edit pane for self-adjusting role names 2025-09-30 18:08:56 -05:00
XtraCube 67a5f0dd67 fixing inner scuff problems 2025-09-30 11:22:43 -04:00
XtraCube f13d46ec18 android support 2025-09-30 11:13:23 -04:00
Atony f9096b6160 Merge pull request #69 from All-Of-Us-Mods/dev
MiraAPI v0.3.1
2025-09-28 13:17:06 -05:00
Atony 8bf101534f Bump version 2025-09-28 13:13:21 -05:00
Atony 479323acdd Merge pull request #68 from yanpla/dev
Fix DetectiveNotesMinigame
2025-09-28 13:06:21 -05:00
Atony 50c5899f4d Merge branch 'dev' into dev 2025-09-28 13:06:12 -05:00
XtraCube fd719ae42f Merge remote-tracking branch 'origin/dev' into dev 2025-09-26 12:18:18 -04:00
XtraCube e050b043cb use reflection for intro cutscene patches 2025-09-26 12:18:13 -04:00
XtraCube e78c456636 match android 2025-09-25 13:47:54 -04:00
XtraCube 7e8d07672c fix mira asset 2025-09-25 12:27:26 -04:00
XtraCube 77ae01c66a improve player customization tabs 2025-09-25 12:25:12 -04:00
XtraCube ce824f05fa fix selection problem on android 2025-09-25 11:36:34 -04:00
yanpla b5fe3c7afd Remove custom roles from DetectiveNotesMinigame 2025-09-24 19:53:24 +02:00
XtraCube 1750cd3db9 Merge pull request #67 from All-Of-Us-Mods/dev
0.3.0
2025-09-24 08:54:52 -04:00
XtraCube 4dd2554b38 bump version 2025-09-24 08:53:09 -04:00
XtraCube e5cfd8a2fc fix game never ending 2025-09-24 08:24:14 -04:00
XtraCube 6d387cae96 fix round start events for airship 2025-09-24 07:35:50 -04:00
XtraCube 59f3058685 update comment 2025-09-24 07:21:31 -04:00
XtraCube 9dfb40d7bf add new role stubs 2025-09-24 07:12:34 -04:00
XtraCube ffca13e6e6 Merge remote-tracking branch 'origin/dev' into dev 2025-09-21 16:57:06 -04:00
XtraCube c1d2868e77 fix server dropdown 2025-09-21 16:57:00 -04:00
Atony 18c52e0138 Expose certain local option overrides and tabs 2025-09-20 22:17:51 -05:00
XtraCube b64bf7c9de bump version 2025-09-20 22:14:07 -04:00
XtraCube e98c16b9f5 fix role settings tab 2025-09-20 22:12:51 -04:00
xChipseq e166efa666 ignore custom roles in vanilla role options 2025-09-20 23:47:36 +02:00
xChipseq 8da80be027 add HideIconOnHover for local settings 2025-09-20 21:58:30 +02:00
XtraCube af75cc3606 revert un-inline 2025-09-20 11:16:56 -04:00
XtraCube 0f1962394a un-inline rpcendgame 2025-09-19 21:07:22 -04:00
XtraCube 36991f34ee fix index of task adder patch 2025-09-19 20:52:01 -04:00
Atony ae5f3783b4 Add locale to task adder 2025-09-18 05:07:05 -05:00
Atony 12a1908c3e Merge branch 'dev' of https://github.com/All-Of-Us-Mods/MiraAPI into dev 2025-09-18 04:47:11 -05:00
xChipseq 8246e05da6 Merge remote-tracking branch 'origin/dev' into dev 2025-09-17 21:04:24 +02:00
xChipseq c75e039141 fixes 2025-09-17 21:04:05 +02:00
Atony ac66e0d62b Clear up warnings 2025-09-17 12:23:40 -05:00
xChipseq c4cbf0b18d use the old system instead of role children 2025-09-17 19:19:47 +02:00
Pietro420 f3175990d3 ACTUALLY fix Viper 2025-09-14 20:07:17 -03:00
XtraCube 790d868759 fix viper 2025-09-14 15:26:58 -04:00
XtraCube 9f11874189 fix role settings 2025-09-14 15:15:06 -04:00
xChipseq 1b6a2c4eb3 part 1 of trying to fix the task adder 2025-09-12 23:49:24 +02:00
xChipseq 81887c34d9 fix misaligned settings (again) 2025-09-12 23:23:10 +02:00
xChipseq 810fd54e10 i love useless using thingies 2025-09-12 22:33:05 +02:00
xChipseq 5492c2b3cc fix how to play role tutorial 2025-09-12 22:02:21 +02:00
XtraCube f7a515882a Merge pull request #65 from All-Of-Us-Mods/2025.9.9
2025.9.9 Support
2025-09-11 21:00:00 -04:00
Atony 89eb774501 Merge pull request #66 from WanderingPix/fix-roleicons
Fix role icons now appearing if there are no settings for the role.
2025-09-11 05:26:17 -05:00
WanderingPixel 5fd57633ff Update RoleSettingMenuPatches.cs 2025-09-11 11:01:35 +01:00
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
120 changed files with 3473 additions and 900 deletions

No files matched your search

+3 -3
View File
@@ -1,13 +1,13 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="Reactor" Version="2.5.0-ci.371" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.4.15" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.3.31" 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>
+2 -2
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.2.1</VersionPrefix>
<VersionPrefix>0.4.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -42,4 +42,4 @@
<PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
</Project>
+18 -2
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Globalization;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Events.Vanilla.Gameplay;
@@ -17,7 +18,7 @@ public static class ExampleEventHandlers
{
public static void Initialize()
{
// You can register event handlers with the MiraEventManager class.
// You can register event handlers with the MiraEventManager class
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
@@ -37,6 +38,21 @@ public static class ExampleEventHandlers
MiraEventManager.UnregisterEventHandler(handle);
}
// If you want to check vanilla buttons, use something like this!
[RegisterEvent]
public static void VanillaButtonClickHandler(VanillaButtonClickEvent @event)
{
Logger<ExamplePlugin>.Info($"{@event.Button.buttonLabelText.text} is being used!");
if (@event.PlayerTarget != null)
{
Logger<ExamplePlugin>.Info($"{@event.PlayerTarget.Data.PlayerName} is being targeted!");
}
else if (@event.VentTarget != null)
{
Logger<ExamplePlugin>.Info($"Vent with id {@event.VentTarget.Id.ToString(CultureInfo.InvariantCulture)} is being targeted!");
}
}
// If you want to add extra votes to a player, do something like this.
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
+3 -1
View File
@@ -1,4 +1,5 @@
using MiraAPI.Example.GameOver;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Example.GameOver;
using MiraAPI.GameEnd;
using MiraAPI.Roles;
using UnityEngine;
@@ -22,6 +23,7 @@ public class NeutralKillerRole : ImpostorRole, ICustomRole
public RoleOptionsGroup RoleOptionsGroup { get; } = new("Outcast", Color.gray);
[HideFromIl2Cpp]
public TeamIntroConfiguration? IntroConfiguration { get; } = new(
Color.gray,
"OUTCAST",
+2 -2
View File
@@ -34,7 +34,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
}
@@ -63,7 +63,7 @@ public static class MiraEventManager
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
}
@@ -1,4 +1,6 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Networking;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Event that is invoked before a player is murdered. This event is cancelable.
@@ -15,6 +17,24 @@ public sealed class BeforeMurderEvent : MiraCancelableEvent
/// </summary>
public PlayerControl Target { get; }
/// <summary>
/// Gets whether the murder was meant to be done in a meeting via an enum.
/// </summary>
public MeetingCheck InMeeting { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeMurderEvent"/> class.
/// </summary>
/// <param name="source">The player that is killing the target.</param>
/// <param name="target">The player that is being killed.</param>
/// <param name="inMeeting">Whether the murder is intended to be triggered in a meeting.</param>
public BeforeMurderEvent(PlayerControl source, PlayerControl target, MeetingCheck inMeeting)
{
Source = source;
Target = target;
InMeeting = inMeeting;
}
/// <summary>
/// Initializes a new instance of the <see cref="BeforeMurderEvent"/> class.
/// </summary>
@@ -24,5 +44,6 @@ public sealed class BeforeMurderEvent : MiraCancelableEvent
{
Source = source;
Target = target;
InMeeting = MeetingCheck.Ignore;
}
}
@@ -0,0 +1,44 @@
using MiraAPI.Hud;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Invoked when a Vanilla Button click is cancelled. Do not use for custom buttons.
/// </summary>
/// <typeparam name="T">The Vanilla Button type.</typeparam>
public sealed class VanillaButtonCancelledEvent<T> : MiraEvent where T : AbilityButton
{
/// <summary>
/// Gets Vanilla Button whose click was cancelled.
/// </summary>
public T Button { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonCancelledEvent{T}"/> class.
/// </summary>
/// <param name="button">The Mira Button whose click was cancelled.</param>
public VanillaButtonCancelledEvent(T button)
{
Button = button;
}
}
/// <summary>
/// Invoked when a Vanilla Button click is cancelled. Do not use for custom buttons.
/// </summary>
public sealed class VanillaButtonCancelledEvent : MiraEvent
{
/// <summary>
/// Gets Vanilla Button whose click was cancelled.
/// </summary>
public AbilityButton Button { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonCancelledEvent"/> class.
/// </summary>
/// <param name="button">The Vanilla Button whose click was cancelled.</param>
public VanillaButtonCancelledEvent(AbilityButton button)
{
Button = button;
}
}
@@ -0,0 +1,65 @@
using MiraAPI.Hud;
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Button click event for Vanilla Buttons only. Do not use for custom buttons.
/// </summary>
/// <typeparam name="T">The Vanilla Button type.</typeparam>
public sealed class VanillaButtonClickEvent<T> : MiraCancelableEvent where T : AbilityButton
{
/// <summary>
/// Gets Vanilla Button that was clicked.
/// </summary>
public T Button { get; }
/// <summary>
/// Gets the generic click event that is invoked before this button-specific event.
/// </summary>
public VanillaButtonClickEvent GenericClickEvent { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonClickEvent{T}"/> class.
/// </summary>
/// <param name="button">The Mira Button that was clicked.</param>
/// <param name="genericClickEvent">The generic click event invoked before button-specific events.</param>
public VanillaButtonClickEvent(T button, VanillaButtonClickEvent genericClickEvent)
{
GenericClickEvent = genericClickEvent;
Button = button;
}
}
/// <summary>
/// Button click event for Vanilla Buttons only. Do not use for custom buttons.
/// </summary>
public sealed class VanillaButtonClickEvent : MiraCancelableEvent
{
/// <summary>
/// Gets Vanilla Button that was clicked.
/// </summary>
public AbilityButton Button { get; }
/// <summary>
/// Gets the target player, if any, of the Vanilla Button that was clicked.
/// </summary>
public PlayerControl? PlayerTarget { get; }
/// <summary>
/// Gets the target vent, if any, of the Vanilla Button that was clicked.
/// </summary>
public Vent? VentTarget { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VanillaButtonClickEvent"/> class.
/// </summary>
/// <param name="button">The Vanilla Button that was clicked.</param>
/// <param name="playerTarget">The Player target, if any.</param>
/// <param name="ventTarget">The Vent target, if any.</param>
public VanillaButtonClickEvent(AbilityButton button, PlayerControl? playerTarget = null, Vent? ventTarget = null)
{
Button = button;
PlayerTarget = playerTarget;
VentTarget = ventTarget;
}
}
@@ -0,0 +1,35 @@
namespace MiraAPI.Events.Vanilla.Player;
/// <summary>
/// The event that is invoked when a player completes a task within Hide and Seek. Non-cancelable.
/// </summary>
public class CompleteHnsTaskEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the <see cref="PlayerControl"/>.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Gets the instance of the <see cref="PlayerTask"/> that the player completed.
/// </summary>
public PlayerTask Task { get; }
/// <summary>
/// Gets the instance of the <see cref="HnsManager"/> attached to the game.
/// </summary>
public HideAndSeekManager HnsManager { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CompleteHnsTaskEvent"/> class.
/// </summary>
/// <param name="hnsManager">The Hide and Seek Manager currently active.</param>
/// <param name="player">The player who completed the task.</param>
/// <param name="task">The task that the player completed.</param>
public CompleteHnsTaskEvent(HideAndSeekManager hnsManager, PlayerControl player, PlayerTask task)
{
HnsManager = hnsManager;
Player = player;
Task = task;
}
}
+2 -2
View File
@@ -30,13 +30,13 @@ public static class GameOverManager
if (gameOverType.IsAbstract)
{
Logger<MiraApiPlugin>.Error("The type must not be abstract.");
Error("The type must not be abstract.");
return false;
}
if (gameOverType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error("The type must have a parameterless constructor.");
Error("The type must have a parameterless constructor.");
return false;
}
+4 -4
View File
@@ -37,7 +37,7 @@ public static class CustomGameModeManager
return;
}
Logger<MiraApiPlugin>.Error($"No gamemode with id {id} found!");
Error($"No gamemode with id {id} found!");
}
/// <summary>
@@ -48,7 +48,7 @@ public static class CustomGameModeManager
{
if (!typeof(CustomGameMode).IsAssignableFrom(gameModeType))
{
Logger<MiraApiPlugin>.Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
Warning($"{gameModeType.Name} does not inherit CustomGameMode!");
return;
}
@@ -56,13 +56,13 @@ public static class CustomGameModeManager
if (modeObj is not CustomGameMode gameMode)
{
Logger<MiraApiPlugin>.Error($"Failed to create instance of {gameModeType.Name}");
Error($"Failed to create instance of {gameModeType.Name}");
return;
}
if (GameModes.Any(x => x.Key == gameMode.Id))
{
Logger<MiraApiPlugin>.Error($"ID for gamemode {gameMode.Name} already exists!");
Error($"ID for gamemode {gameMode.Name} already exists!");
return;
}
+36 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MiraAPI.Modifiers;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -28,11 +29,16 @@ public abstract class AbstractOptionGroup
public virtual Type? OptionableType => null;
/// <summary>
/// Gets a value indicating whether the group should be shown in the modifiers menu.
/// Gets a value indicating whether the group should be shown in the modifiers menu. This is deprecated, please use ParentMenu!
/// </summary>
// TODO: make this not a boolean
public virtual bool ShowInModifiersMenu => false;
/// <summary>
/// Gets a value indicating which menu the group is in.
/// </summary>
public virtual MenuCategory ParentMenu => MenuCategory.Roles;
/// <summary>
/// Gets the function that determines whether the group should be visible or not.
/// </summary>
@@ -62,4 +68,33 @@ public abstract class AbstractOptionGroup<T> : AbstractOptionGroup where T : IOp
{
/// <inheritdoc />
public override Type OptionableType => typeof(T);
/// <summary>
/// Gets a value indicating which menu the group is in.
/// </summary>
public override MenuCategory ParentMenu
{
get
{
var type = typeof(T);
if (type == typeof(BaseModifier))
{
return MenuCategory.Modifiers;
}
else if (type == typeof(RoleBehaviour))
{
return MenuCategory.Roles;
}
return MenuCategory.Game;
}
}
}
public enum MenuCategory
{
Game,
Roles,
Modifiers,
CustomOne,
CustomTwo
}
@@ -21,7 +21,15 @@ public class ModdedNumberOptionAttribute(
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity);
return new ModdedNumberOption(
Title,
(float)(value ?? min + increment),
min,
max,
increment,
suffixType,
formatString,
zeroInfinity);
}
/// <inheritdoc />
+2 -2
View File
@@ -17,9 +17,9 @@ public interface IModdedOption
uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// Gets or sets the title of the option.
/// </summary>
string Title { get; }
string Title { get; set; }
/// <summary>
/// Gets the StringName for the option, used for localization.
@@ -0,0 +1,40 @@
using System;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Component added onto options generated via MiraAPI. This allows developers to hook patches onto options in general, without using a list.
/// </summary>
[RegisterInIl2Cpp]
public class MiraNumberOptionComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets or sets the modded option associated with the object.
/// </summary>
[HideFromIl2Cpp]
public ModdedNumberOption NumberOption { get; set; }
/// <summary>
/// Gets or sets the default increment.
/// </summary>
public float DefaultIncrement { get; set; }
/// <summary>
/// Gets or sets a value indicating whether holding shift will split the increment in half.
/// </summary>
public bool ShiftIncrementToggle { get; set; }
/// <summary>
/// Gets or sets a value indicating what Zero displays.
/// </summary>
public string ZeroValue { get; set; } = "#";
/// <summary>
/// Gets or sets a value indicating what Negative One displays.
/// </summary>
public string NegativeValue { get; set; } = "#";
}
+6 -6
View File
@@ -39,7 +39,7 @@ public static class ModdedOptionsManager
if (TypeToGroup.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Group {type.Name} already exists.");
Error($"Group {type.Name} already exists.");
return false;
}
@@ -60,13 +60,13 @@ public static class ModdedOptionsManager
{
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
if (property.GetValue(group) is not IModdedOption option)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -81,13 +81,13 @@ public static class ModdedOptionsManager
{
if (OptionAttributes.ContainsKey(property))
{
Logger<MiraApiPlugin>.Error($"Property {property.Name} already has an attribute registered.");
Error($"Property {property.Name} already has an attribute registered.");
return;
}
if (!TypeToGroup.TryGetValue(type, out var group))
{
Logger<MiraApiPlugin>.Error($"Failed to get group for {type.Name}");
Error($"Failed to get group for {type.Name}");
return;
}
@@ -95,7 +95,7 @@ public static class ModdedOptionsManager
if (option == null)
{
Logger<MiraApiPlugin>.Error($"Failed to get option for {property.Name}");
Error($"Failed to get option for {property.Name}");
return;
}
@@ -47,6 +47,8 @@ public class ModdedEnumOption : ModdedOption<int>
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.name =
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
@@ -138,6 +140,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
stringOption.name =
$"{ParentMod!.OptionsTitleText}.EnumOption.{TranslationController.Instance.GetString(StringName)}";
stringOption.SetUpFromData(Data, 20);
stringOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
@@ -32,10 +32,82 @@ public class ModdedNumberOption : ModdedOption<float>
/// </summary>
public MiraNumberSuffixes SuffixType { get; }
/// <summary>
/// Gets a value indicating what zero appears as.
/// </summary>
public string ZeroWordValue { get; }
/// <summary>
/// Gets a value indicating what a negative one appears as.
/// </summary>
public string NegativeWordValue { get; }
/// <summary>
/// Gets a value indicating whether zero is infinity.
/// </summary>
public bool ZeroInfinity { get; }
public bool ZeroInfinity => ZeroWordValue == "∞";
/// <summary>
/// Gets a value indicating whether holding shift will divide the increment.
/// </summary>
public bool ShiftIncrement { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as a float.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroBehavior">Determines what is shown for zero. Options: ∞, #, or a word value (such as Off).</param>
/// <param name="negativeBehavior">Determines what is shown for negative 1. Options: ∞, #, or a word value (such as Off).</param>
/// <param name="halfIncrements">Whether increments can be split in half.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption(
string title,
float defaultValue,
float min,
float max,
float increment,
string zeroBehavior,
string negativeBehavior,
MiraNumberSuffixes suffixType,
string? formatString = null,
bool halfIncrements = false,
bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
{
Min = min;
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroWordValue = zeroBehavior;
NegativeWordValue = negativeBehavior;
ShiftIncrement = halfIncrements;
Value = Mathf.Clamp(defaultValue, min, max);
Data = ScriptableObject.CreateInstance<FloatGameSetting>();
var data = (FloatGameSetting)Data;
data.Type = global::OptionTypes.Float;
data.Title = StringName;
data.Value = Value;
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
data.FormatString = formatString ??
(defaultValue.IsInteger() &&
Increment.IsInteger() &&
Value.IsInteger() &&
Min.IsInteger() &&
Max.IsInteger() ? "0" : "0.0");
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedNumberOption"/> class.
@@ -64,7 +136,9 @@ public class ModdedNumberOption : ModdedOption<float>
Max = max;
Increment = increment;
SuffixType = suffixType;
ZeroInfinity = zeroInfinity;
ZeroWordValue = zeroInfinity ? "∞" : "#";
NegativeWordValue = "#";
ShiftIncrement = increment != 1f;
Value = Mathf.Clamp(defaultValue, min, max);
@@ -97,6 +171,14 @@ public class ModdedNumberOption : ModdedOption<float>
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
numberOption.name =
$"{ParentMod!.OptionsTitleText}.NumberOption.{TranslationController.Instance.GetString(StringName)}";
var optionComponent = numberOption.gameObject.AddComponent<MiraNumberOptionComponent>();
optionComponent.NumberOption = this;
optionComponent.DefaultIncrement = Increment;
optionComponent.ShiftIncrementToggle = ShiftIncrement;
optionComponent.NegativeValue = NegativeWordValue;
optionComponent.ZeroValue = ZeroWordValue;
numberOption.SetUpFromData(Data, 20);
numberOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
@@ -21,7 +21,7 @@ public abstract class ModdedOption<T> : IModdedOption
public uint Id { get; }
/// <inheritdoc />
public string Title { get; }
public string Title { get; set; }
/// <inheritdoc />
public StringNames StringName { get; }
@@ -125,7 +125,7 @@ public abstract class ModdedOption<T> : IModdedOption
{
if (ConfigDefinition is null)
{
Logger<MiraApiPlugin>.Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
return;
}
Bind(presetConfig);
@@ -147,7 +147,7 @@ public abstract class ModdedOption<T> : IModdedOption
}
else
{
Logger<MiraApiPlugin>.Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
}
}
@@ -55,6 +55,8 @@ public class ModdedPlayerOption : ModdedOption<int>
Transform container)
{
var playerOption = Object.Instantiate(playerOpt, container);
playerOption.name =
$"{ParentMod!.OptionsTitleText}.PlayerOption.{TranslationController.Instance.GetString(StringName)}";
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
@@ -29,6 +29,8 @@ public class ModdedToggleOption : ModdedOption<bool>
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
toggleOption.name =
$"{ParentMod!.OptionsTitleText}.ToggleOption.{TranslationController.Instance.GetString(StringName)}";
toggleOption.SetUpFromData(Data, 20);
+46 -8
View File
@@ -64,9 +64,14 @@ public abstract class CustomActionButton
public virtual float EffectDuration => 0;
/// <summary>
/// Gets the maximum amount of uses the button has. If the button has infinite uses, set to 0.
/// Gets a value indicating whether the button has limited uses.
/// </summary>
public virtual int MaxUses => 0;
public bool LimitedUses => ZeroIsInfinite ? MaxUses > 0 : MaxUses >= 0;
/// <summary>
/// Gets the maximum number of uses the button has. If the button has infinite uses, set to 0 or -1 based on what value that ZeroIsInfinite is set to.
/// </summary>
public virtual int MaxUses => ZeroIsInfinite ? 0 : -1;
/// <summary>
/// Gets the value indicating uses mode.
@@ -89,9 +94,9 @@ public abstract class CustomActionButton
public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft;
/// <summary>
/// Gets a value indicating whether the button has limited uses.
/// Gets or sets a value indicating whether limited uses are determined via zero or a negative number of uses.
/// </summary>
public bool LimitedUses => MaxUses > 0;
public virtual bool ZeroIsInfinite { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the effect is currently active, if there is one.
@@ -206,7 +211,39 @@ public abstract class CustomActionButton
{
if (Enabled(PlayerControl.LocalPlayer.Data.Role))
{
ClickHandler();
// Invoke the generic button click event.
var genericEvent = new MiraButtonClickEvent(this);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new MiraButtonCancelledEvent(this));
}
// Invoke the button click event for specific button.
var eventType = CustomButtonManager.ButtonEventTypes[GetType()];
var @event = (MiraCancelableEvent)Activator.CreateInstance(eventType, this, genericEvent)!;
var specificInvoked = MiraEventManager.InvokeEvent(@event, eventType);
if (@event.IsCancelled)
{
var cancelEventType = CustomButtonManager.ButtonCancelledEventTypes[GetType()];
var cancelEvent = (MiraEvent)Activator.CreateInstance(cancelEventType, this)!;
MiraEventManager.InvokeEvent(cancelEvent, cancelEventType);
}
if (specificInvoked)
{
if (!@event.IsCancelled)
{
ClickHandler();
}
}
else
{
if (!genericEvent.IsCancelled)
{
ClickHandler();
}
}
}
});
@@ -214,9 +251,10 @@ public abstract class CustomActionButton
Helpers.CreateKeybindIcon(
Button.gameObject,
Keybind.CurrentKey,
new Vector3(MaxUses <= 0 ? -0.4f : 0.4f, 0.45f, -9f)
new Vector3(0.4f, 0.45f, -9f)
);
KeybindText = KeybindIcon.transform.GetChild(0).GetComponent<TextMeshPro>();
HudManagerPatches.ModdedKeybindIcons.Add(KeybindText);
Button.usesRemainingSprite.transform.localPosition = new(-0.341f, 0.45f, -0.1f);
}
}
@@ -233,7 +271,7 @@ public abstract class CustomActionButton
return;
}
this.Location = location;
Location = location;
if (!moveButton)
{
@@ -494,7 +532,7 @@ public abstract class CustomActionButton
if (Keybind != null && KeybindText != null)
{
KeybindText.text = Keybind.CurrentKey.ToString();
KeybindIcon?.SetActive(ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
KeybindIcon?.SetActive(ActiveInputManager.currentControlType is ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value &&
Keybind.CurrentKey != KeyboardKeyCode.None);
}
+1 -1
View File
@@ -69,4 +69,4 @@ public class BaseKeybind
{
Handler -= action;
}
}
}
+3 -3
View File
@@ -26,11 +26,11 @@ public static class KeybindManager
try
{
var instance = KeybindUtils.RewiredInputManager!;
foreach (var keybind in KeybindManager.Keybinds)
foreach (var keybind in Keybinds)
{
if (instance.userData.actions.ToArray().Any(x => x.name == keybind.Id))
{
Logger<MiraApiPlugin>.Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
Warning($"Keybind of id {keybind.Id} already exists. Skipping it");
continue;
}
@@ -44,7 +44,7 @@ public static class KeybindManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while registering keybinds in Rewired: {e}");
Error($"Error while registering keybinds in Rewired: {e}");
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace MiraAPI.Keybinds;
public class MiraKeybind : BaseKeybind
{
/// <summary>
/// Gets name of the keybind
/// Gets name of the keybind.
/// </summary>
public string Name { get; }
+1 -1
View File
@@ -19,6 +19,6 @@ public class VanillaKeybind : BaseKeybind
public VanillaKeybind(int id)
{
RewiredInputAction = KeybindUtils.GetInputActionById(id);
Id = RewiredInputAction?.name;
Id = RewiredInputAction != null ? RewiredInputAction.name : id.ToString();
}
}
@@ -20,7 +20,7 @@ public class LocalEnumSettingAttribute(
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
public override LocalEnumSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalEnumSetting(tab, configEntryBase, configEntryBase.SettingType, _name, _description, names);
}
@@ -29,7 +29,7 @@ public class LocalNumberSettingAttribute(
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
public override LocalNumberSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalNumberSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), increment, suffixType, formatString);
}
@@ -20,5 +20,5 @@ public abstract class LocalSettingAttribute(
/// <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);
public abstract ILocalSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase);
}
@@ -29,7 +29,7 @@ public class LocalSliderSettingAttribute(
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
public override LocalSliderSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalSliderSetting(tab, configEntryBase, _name, _description, new FloatRange(min, max), displayValue, suffixType, formatString, roundValue);
}
@@ -18,7 +18,7 @@ public class LocalToggleSettingAttribute(
private readonly string? _description = description;
/// <inheritdoc/>
internal override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
public override LocalToggleSetting CreateSetting(Type tab, ConfigEntryBase configEntryBase)
{
return new LocalToggleSetting(tab, configEntryBase, _name, _description);
}
@@ -24,7 +24,7 @@ public static class LocalSettingsManager
if (TypeToTab.ContainsKey(type))
{
Logger<MiraApiPlugin>.Error($"Local settings tab {type.Name} already exists.");
Error($"Local settings tab {type.Name} already exists.");
return false;
}
@@ -128,6 +128,7 @@ public abstract class LocalSettingsTab(ConfigFile config)
{
CreateLabel(generalLabel, Scroller.Inner, pair.Key, ref contentOffset);
contentOrder = 1;
contentIndex = 1;
}
foreach (var setting in pair.Value)
@@ -245,6 +246,7 @@ public abstract class LocalSettingsTab(ConfigFile config)
tabButtonText.maxVisibleCharacters = int.MaxValue;
tabButtonText.text = TabName;
tabButtonText.alignment = TextAlignmentOptions.Left;
tabButtonRend?.gameObject.SetActive(!TabAppearance.HideIconOnHover);
}));
tabButton.OnMouseOut.AddListener((UnityAction)(() =>
{
@@ -4,7 +4,7 @@ using UnityEngine;
namespace MiraAPI.LocalSettings;
/// <summary>
/// Struct for modyfing tab apparance.
/// Struct for modifying tab appearance.
/// Defines how the settings look.
/// </summary>
public record struct LocalSettingTabAppearance()
@@ -24,6 +24,11 @@ public record struct LocalSettingTabAppearance()
/// </summary>
public Color TabButtonColor { get; set; } = Color.white;
/// <summary>
/// Gets or sets a value indicating whether the icon should be hidden when tab button is hovered.
/// </summary>
public bool HideIconOnHover { get; set; } = true;
/// <summary>
/// Gets or sets hover color of the tab button.
/// </summary>
@@ -78,7 +83,7 @@ public record struct LocalSettingTabAppearance()
/// Gets or sets hover color of enum buttons.
/// </summary>
public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen;
/// <summary>
/// Gets or sets color of buttons.
/// </summary>
@@ -20,7 +20,10 @@ public abstract class LocalSettingBase<T> : ILocalSetting
/// <inheritdoc />
public ConfigEntryBase ConfigEntry { get; }
internal LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this));
/// <summary>
/// Gets the tab of the local setting.
/// </summary>
public LocalSettingsTab? Tab => LocalSettingsManager.Tabs.FirstOrDefault(x => x.Settings.Contains(this));
/// <summary>
/// Initializes a new instance of the <see cref="LocalSettingBase{T}"/> class.
@@ -53,7 +56,7 @@ public abstract class LocalSettingBase<T> : ILocalSetting
}
/// <summary>
/// Gets the value of the config entry, casted to the setting type.
/// Gets the value of the config entry, cast to the setting type.
/// </summary>
/// <returns>The value.</returns>
public virtual T GetValue()
+14 -2
View File
@@ -1,4 +1,6 @@
using BepInEx;
global using static Reactor.Utilities.Logger<MiraAPI.MiraApiPlugin>;
using System;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
@@ -20,6 +22,11 @@ namespace MiraAPI;
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
/// <summary>
/// Gets a value indicating whether the current device is running Starlight (on mobile).
/// </summary>
public static bool IsMobile => Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone;
/// <summary>
/// Gets the branding Mira API color.
/// </summary>
@@ -30,6 +37,11 @@ public partial class MiraApiPlugin : BasePlugin
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
/// <summary>
/// Gets a value indicating whether the current build is a development build or not. This mostly avoids confusion for users asking why the mod appears red.
/// </summary>
public static bool IsDevBuild => Version.Contains("ci", StringComparison.OrdinalIgnoreCase) || Version.Contains("dev", StringComparison.OrdinalIgnoreCase);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
@@ -38,7 +50,7 @@ public partial class MiraApiPlugin : BasePlugin
{
Harmony.PatchAll();
ReactorCredits.Register("Mira API", Version, true, ReactorCredits.AlwaysShow);
ReactorCredits.Register("Mira API", Version, IsDevBuild, ReactorCredits.AlwaysShow);
PluginManager = new MiraPluginManager();
PluginManager.Initialize();
+60 -11
View File
@@ -1,13 +1,15 @@
using BepInEx.Configuration;
using MiraAPI.LocalSettings;
using MiraAPI.LocalSettings.Attributes;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI;
/// <summary>
/// Mira API Config File Handler
/// Mira API Config File Handler.
/// </summary>
public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
{
@@ -19,34 +21,81 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
{
TabButtonHoverColor = MiraApiPlugin.MiraColor,
TabIcon = MiraAssets.SettingsIcon,
HideIconOnHover = false,
};
/// <summary>
/// Gets or sets the value stored for scaling buttons properly.
/// </summary>
public static float OldButtonScaleFactor { get; set; }
/// <inheritdoc />
public override void Open()
{
base.Open();
OldButtonScaleFactor = ButtonUIFactorSlider.Value;
}
/// <inheritdoc />
public override void OnOptionChanged(ConfigEntryBase configEntry)
{
base.OnOptionChanged(configEntry);
if (configEntry == ButtonUIFactorSlider)
{
if (HudManager.InstanceExists)
{
HudManagerPatches.ResizeUI(1f / OldButtonScaleFactor);
HudManagerPatches.ResizeUI(ButtonUIFactorSlider.Value);
}
OldButtonScaleFactor = ButtonUIFactorSlider.Value;
}
else if (configEntry == SetFpsSlider)
{
Application.targetFrameRate = (int)SetFpsSlider.Value;
}
}
/// <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);
public ConfigEntry<bool> ModifiersHudLeftSide { get; private set; } = config.Bind("Visuals/UI", "Show Modifiers HUD on Left Side", false);
/// <summary>
/// Gets whether vents and their connections appear on the map.
/// Gets whether to show keybinds on buttons.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> ShowVentsOnMap { get; private set; } = config.Bind("Displays", "Show Vent Layout on Map", true);
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Visuals/UI", "Show Keybinds on Buttons", true);
/// <summary>
/// Gets whether cooldowns for buttons below 10 seconds appear with decimals.
/// Gets the scale of the buttons.
/// </summary>
[LocalSliderSetting(min: 0.5f, max: 1.5f, suffixType: MiraNumberSuffixes.Multiplier, formatString: "0.00", displayValue: true)]
public ConfigEntry<float> ButtonUIFactorSlider { get; private set; } =
config.Bind("Visuals/UI", "Button Scale Factor", 0.75f);
/// <summary>
/// Gets the fps specified by the player.
/// </summary>
[LocalSliderSetting(min: 60f, max: 240f, suffixType: MiraNumberSuffixes.None, formatString: "0", displayValue: true, roundValue: true)]
public ConfigEntry<float> SetFpsSlider { get; private set; } =
config.Bind("Visuals/UI", "Max FPS", 120f);
/// <summary>
/// Gets whether to apply cosmetic changes to the TaskAdder.
/// </summary>
[LocalToggleSetting]
public ConfigEntry<bool> PreciseCooldowns { get; private set; } = config.Bind("Buttons", "Precise Button Cooldowns Under 10s", false);
public ConfigEntry<bool> PrettyTaskAdder { get; private set; } = config.Bind("Freeplay", "Pretty Task Laptop", true);
/// <summary>
/// Gets the size of ability buttons in-game.
/// Gets whether to show the red flash from sabotages.
/// </summary>
[LocalSliderSetting(min: 0.5f, max: 1.5f, displayValue: true, formatString: "0.00", suffixType: MiraNumberSuffixes.Multiplier)]
public ConfigEntry<float> ButtonUiScale { get; private set; } = config.Bind("Buttons", "Button Scale Factor", 0.8f);
[LocalToggleSetting]
public ConfigEntry<bool> EnableSabotageFlashes { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Flashes", true);
/// <summary>
/// Gets whether to show keybinds in the control mapper.
/// Gets whether to enable the sabotage sound effects or not.
/// </summary>
public ConfigEntry<bool> ShowKeybinds { get; private set; } = config.Bind("Keybinds", "Show Keybinds in Control Mapper", true);
[LocalToggleSetting]
public ConfigEntry<bool> EnableSabotageBlares { get; private set; } = config.Bind("Accessibility", "Enable Sabotage Blare", true);
}
+11 -11
View File
@@ -47,7 +47,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_player = GetComponent<PlayerControl>();
if (_player == null)
{
Logger<MiraApiPlugin>.Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Destroy(this);
return;
}
@@ -77,7 +77,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier);
@@ -95,7 +95,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
@@ -122,7 +122,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
@@ -153,7 +153,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
}
@@ -336,7 +336,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = modifiers.FirstOrDefault();
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {type.Name} because it is not active.");
Error($"Cannot remove modifier {type.Name} because it is not active.");
return;
}
@@ -352,7 +352,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
if (!ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
}
@@ -382,7 +382,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
var modifier = GetModifier(uniqueId);
if (modifier == null)
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
Error($"Cannot remove modifier with unique id {uniqueId} because it is not active.");
return;
}
@@ -412,19 +412,19 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
// TODO: Make a proper synchronization system.
if (LobbyBehaviour.Instance)
{
Logger<MiraApiPlugin>.Warning($"Modifiers added in the lobby won't sync to new players!");
Warning($"Modifiers added in the lobby won't sync to new players!");
}
var id = modifier.TypeId;
if (modifier.Unique && ActiveModifiers.Find(x => x.TypeId == id) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {id}!");
Error($"Player already has modifier with id {id}!");
return null;
}
if (ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has this modifier!");
Error($"Player already has this modifier!");
return null;
}
+1 -1
View File
@@ -83,7 +83,7 @@ public static class ModifierExtensions
var modifier = target.GetModifier(typeId, predicate);
if (modifier is null)
{
Logger<MiraApiPlugin>.Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
Error($"Player {target.PlayerId} does not have modifier with type ID {typeId}.");
return;
}
+6 -5
View File
@@ -97,7 +97,7 @@ public static class ModifierManager
if (modifierType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
return false;
}
@@ -137,7 +137,7 @@ public static class ModifierManager
var validPlayers = plrs.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId)).ToList();
if (validPlayers.Count == 0)
{
Logger<MiraApiPlugin>.Warning($"No valid players for modifier {modifier.ModifierName}");
Warning($"No valid players for modifier {modifier.ModifierName}");
continue;
}
@@ -158,7 +158,7 @@ public static class ModifierManager
if (candidates.Count == 0)
{
Logger<MiraApiPlugin>.Warning(
Warning(
$"No available players for modifier {modifier.ModifierName} at assignment {i + 1}");
break;
}
@@ -166,7 +166,7 @@ public static class ModifierManager
var plr = candidates.Random();
if (plr == null)
{
Logger<MiraApiPlugin>.Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
continue;
}
@@ -179,6 +179,7 @@ public static class ModifierManager
private static bool IsGameModifierValid(PlayerControl player, GameModifier modifier, uint modifierId)
{
return (player.Data.Role is not ICustomRole role || role.IsModifierApplicable(modifier)) &&
modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId);
modifier.IsModifierValidOn(player.Data.Role) && !player.HasModifier(modifierId) &&
modifier.CanSpawnOnCurrentMode();
}
}
+6
View File
@@ -39,4 +39,10 @@ public abstract class GameModifier : BaseModifier
/// <param name="reason">The reason why the game ended.</param>
/// <returns>True if the player won, false if they lost. Return null to use the player's default win condition.</returns>
public virtual bool? DidWin(GameOverReason reason) => null;
/// <summary>
/// Determines whether the modifier can spawn in general, accounting for gamemodes and everything else.
/// </summary>
/// <returns>True if the modifier is able to spawn, otherwise false.</returns>
public virtual bool CanSpawnOnCurrentMode() => !GameManager.Instance.IsHideAndSeek();
}
+1 -1
View File
@@ -80,7 +80,7 @@ public abstract class TimedModifier : BaseModifier
{
if (TimerActive)
{
Logger<MiraApiPlugin>.Error("Can't start a timer that has already been started.");
Error("Can't start a timer that has already been started.");
return;
}
+2 -2
View File
@@ -50,7 +50,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
{
if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners]))
{
Logger<MiraApiPlugin>.Info($"Game over condition not met for {gameOver.GetType().Name}");
Info($"Game over condition not met for {gameOver.GetType().Name}");
return;
}
@@ -64,7 +64,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
else
{
CustomGameOver.Instance = null;
Logger<MiraApiPlugin>.Error($"Unknown game over reason: {data.Reason}");
Error($"Unknown game over reason: {data.Reason}");
}
}
}
+154 -11
View File
@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Linq;
using AmongUs.Data;
using AmongUs.GameOptions;
@@ -6,10 +7,14 @@ using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Rewired;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Networking;
@@ -19,7 +24,7 @@ namespace MiraAPI.Networking;
public static class CustomMurderRpc
{
/// <summary>
/// Networked Custom Murder method.
/// Networked Custom Murder method, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
@@ -29,7 +34,6 @@ 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)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
@@ -40,16 +44,117 @@ public static class CustomMurderRpc
bool showKillAnim = true,
bool playKillSound = true)
{
source.RpcCustomMurder(
target,
MeetingCheck.Ignore,
didSucceed,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="inMeeting">Whether the murder is intended to be triggered in a meeting.</param>
/// <param name="didSucceed">Whether the murder was successful or not.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <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)]
public static void RpcCustomMurder(
this PlayerControl source,
PlayerControl target,
MeetingCheck inMeeting,
bool didSucceed = true,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance)
{
return;
}
var murderResultFlags = didSucceed ? MurderResultFlags.Succeeded : MurderResultFlags.FailedError;
var beforeMurderEvent = new BeforeMurderEvent(source, target);
var beforeMurderEvent = new BeforeMurderEvent(source, target, inMeeting);
MiraEventManager.InvokeEvent(beforeMurderEvent);
var isMeetingActive = MeetingHud.Instance != null || ExileController.Instance != null;
if ((inMeeting is MeetingCheck.ForMeeting && !isMeetingActive) || (inMeeting is MeetingCheck.OutsideMeeting && isMeetingActive))
{
beforeMurderEvent.Cancel();
}
if (beforeMurderEvent.IsCancelled)
if (target.ProtectedByGa())
{
beforeMurderEvent.Cancel();
murderResultFlags = MurderResultFlags.FailedProtected;
}
else if (beforeMurderEvent.IsCancelled)
{
murderResultFlags = MurderResultFlags.FailedError;
}
if (beforeMurderEvent.IsCancelled && source.AmOwner)
{
source.isKilling = true;
}
if (!PlayerControl.LocalPlayer.IsHost())
{
return;
}
RpcConfirmCustomMurder(
PlayerControl.LocalPlayer,
source,
target,
murderResultFlags,
resetKillTimer,
createDeadBody,
teleportMurderer,
showKillAnim,
playKillSound);
}
/// <summary>
/// Networked Custom Murder method, which checks for meetings as well, ran by clients for the host to confirm kills.
/// </summary>
/// <param name="host">The game host.</param>
/// <param name="source">The killer.</param>
/// <param name="target">The player to murder.</param>
/// <param name="murderResultFlags">End result for the murder.</param>
/// <param name="resetKillTimer">Should the kill timer be reset.</param>
/// <param name="createDeadBody">Should a dead body be created.</param>
/// <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.ConfirmCustomMurder, LocalHandling = RpcLocalHandling.After)]
public static void RpcConfirmCustomMurder(
this PlayerControl host,
PlayerControl source,
PlayerControl target,
MurderResultFlags murderResultFlags,
bool resetKillTimer = true,
bool createDeadBody = true,
bool teleportMurderer = true,
bool showKillAnim = true,
bool playKillSound = true)
{
if (LobbyBehaviour.Instance || !host.IsHost() || target.Data.IsDead || target.Data.Disconnected)
{
source.isKilling = false;
return;
}
var murderResultFlags2 = MurderResultFlags.DecisionByHost | murderResultFlags;
source.CustomMurder(
@@ -84,8 +189,7 @@ public static class CustomMurderRpc
bool playKillSound = true)
{
source.isKilling = false;
var data = target.Data;
if (resultFlags.HasFlag(MurderResultFlags.FailedError))
if (resultFlags.HasFlag(MurderResultFlags.FailedError) || LobbyBehaviour.Instance)
{
return;
}
@@ -169,7 +273,14 @@ public static class CustomMurderRpc
if (showKillAnim)
{
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, data);
try
{
HudManager.Instance.KillOverlay.ShowKillAnimation(source.Data, target.Data);
}
catch (Exception e)
{
Error($"Error with kill animation: {e.ToString()}");
}
}
target.cosmetics.SetNameMask(false);
@@ -182,7 +293,7 @@ public static class CustomMurderRpc
source.CurrentOutfitType == PlayerOutfitType.Shapeshifted,
source.shapeshiftTargetPlayerId,
target.PlayerId);
source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
source.MyPhysics.StartCoroutine(source.KillAnimations.RandomSnapshot().CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
}
/// <summary>
@@ -201,8 +312,12 @@ public static class CustomMurderRpc
bool createDeadBody = true,
bool teleportMurderer = true)
{
if (LobbyBehaviour.Instance)
{
yield break;
}
var cam = Camera.main?.GetComponent<FollowerCamera>();
var isParticipant = PlayerControl.LocalPlayer == source || PlayerControl.LocalPlayer == target;
var isParticipant = source.AmOwner || target.AmOwner;
var sourcePhys = source.MyPhysics;
if (teleportMurderer)
@@ -222,7 +337,7 @@ public static class CustomMurderRpc
if (createDeadBody)
{
deadBody = Object.Instantiate(GameManager.Instance.DeadBodyPrefab);
deadBody = Object.Instantiate(GameManager.Instance.GetDeadBody(source.Data.Role));
deadBody.enabled = false;
deadBody.ParentId = target.PlayerId;
deadBody.bodyRenderers.ToList().ForEach(target.SetPlayerMaterialColors);
@@ -233,6 +348,13 @@ public static class CustomMurderRpc
deadBody.transform.position = vector;
}
source.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
target.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
// no idea if this causes bugs, but innersloth is brain-dead
// I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF I HATE INNERSCUFF
PlayerControl.LocalPlayer.Data.Role.KillAnimSpecialSetup(deadBody, source, target);
if (isParticipant)
{
if (cam != null)
@@ -281,3 +403,24 @@ public static class CustomMurderRpc
source.isKilling = false;
}
}
/// <summary>
/// Checks for custom murders.
/// </summary>
public enum MeetingCheck
{
/// <summary>
/// Checks for meetings.
/// </summary>
ForMeeting,
/// <summary>
/// Checks for a meeting to not be active.
/// </summary>
OutsideMeeting,
/// <summary>
/// Doesn't check for a meeting at all.
/// </summary>
Ignore,
}
+5
View File
@@ -35,6 +35,11 @@ public enum MiraRpc : uint
/// </summary>
CustomMurder,
/// <summary>
/// Custom Murder RPC Confirmation.
/// </summary>
ConfirmCustomMurder,
/// <summary>
/// Custom Game Over RPC.
/// </summary>
@@ -0,0 +1,67 @@
using System.Collections;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using UnityEngine;
namespace MiraAPI.Patches.Accessibility;
[HarmonyPatch]
public static class HudManagerFlashPatches
{
[HarmonyPatch(typeof(HudManager), nameof(HudManager.StartReactorFlash))]
[HarmonyPrefix]
public static bool ReactorFlashPrefix(HudManager __instance)
{
if (__instance.ReactorFlash == null)
{
__instance.ReactorFlash = __instance.StartCoroutine(CoReactorFlash().WrapToIl2Cpp());
}
return false;
}
[HarmonyPatch(typeof(HudManager), nameof(HudManager.StartOxyFlash))]
[HarmonyPrefix]
public static bool OxygenFlashPrefix(HudManager __instance)
{
if (__instance.OxyFlash == null)
{
__instance.OxyFlash = __instance.StartCoroutine(CoReactorFlash().WrapToIl2Cpp());
}
return false;
}
public static IEnumerator CoReactorFlash()
{
if (!HudManager.InstanceExists)
{
yield break;
}
var hudManager = HudManager.Instance;
WaitForSeconds wait = new WaitForSeconds(1f);
bool light = false;
hudManager.FullScreen.color = new Color(1f, 0f, 0f, 0.37254903f);
while (true)
{
var settins = LocalSettingsTabSingleton<MiraApiSettings>.Instance;
hudManager.FullScreen.gameObject.SetActive(settins.EnableSabotageFlashes.Value && !hudManager.FullScreen.gameObject.activeSelf);
if (settins.EnableSabotageBlares.Value)
{
SoundManager.Instance.PlaySound(ShipStatus.Instance.SabotageSound, false, 1f, null);
}
light = !light;
/*if (!MiraApiPlugin.IsMobile)
{
if (hudManager.lightFlashHandle == null)
{
hudManager.lightFlashHandle = DualshockLightManager.Instance.AllocateLight();
hudManager.lightFlashHandle.color = new Color(1f, 0f, 0f, 1f);
hudManager.lightFlashHandle.intensity = 1f;
}
hudManager.lightFlashHandle.color.SetAlpha(light ? 1f : 0f);
}*/
yield return wait;
}
}
}
@@ -0,0 +1,42 @@
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches;
[HarmonyPatch(typeof(DetectiveNotesMinigame), nameof(DetectiveNotesMinigame.SetImpostorPopup))]
public static class DetectiveNotesMinigamePatch
{
public static void Prefix(DetectiveNotesMinigame __instance, bool active)
{
__instance.impostorTypePopup.gameObject.SetActive(active);
if (!active)
{
ControllerManager.Instance.CloseOverlayMenu(__instance.ImpostorOverlay);
ControllerManager.Instance.SetUpSelectables(ControllerManager.Instance.CurrentUiState, __instance.ControllerSelectable[0], __instance.ControllerSelectable);
ControllerManager.Instance.SetCurrentSelected(__instance.ControllerSelectable[2]);
return;
}
if (__instance.impostorButton != null)
{
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, __instance.impostorTypePopup, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
return;
}
__instance.impostorButton = new List<GameObject>();
var allRoles = DestroyableSingleton<RoleManager>.Instance.AllRoles;
var customRoles = CustomRoleManager.CustomRoleBehaviours;
foreach (var role in allRoles)
{
if (role.TeamType != RoleTeamTypes.Impostor || role.Role == RoleTypes.ImpostorGhost || customRoles.Contains(role) )
continue;
var gameObject = Object.Instantiate(__instance.impostorTypePrefab, __instance.impostorTypeParent);
__instance.impostorButton.Add(gameObject);
gameObject.GetComponent<DetectiveImpostorType>().Initialize(__instance, role);
__instance.ImpostorIconControllerSelectables.Add(gameObject.GetComponent<PassiveButton>());
}
ControllerManager.Instance.OpenOverlayMenu(__instance.ImpostorOverlay, __instance.impostorTypePopup, __instance.ImpostorIconControllerSelectables[0], __instance.ImpostorIconControllerSelectables, false);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace MiraAPI.Patches.Events;
internal static class EndGamePatches
{
[HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))]
public static class GameManagerPatches
public static class GameManagerRpcEndGamePatch
{
public static bool Prefix(GameOverReason endReason)
{
@@ -1,13 +1,20 @@
using HarmonyLib;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(TutorialManager._RunTutorial_d__3), nameof(TutorialManager._RunTutorial_d__3.MoveNext))]
[HarmonyPatch]
public static class FreeplayRoundStartPatch
{
public static void Postfix(TutorialManager._RunTutorial_d__3 __instance, ref bool __result)
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<TutorialManager>(nameof(TutorialManager.RunTutorial))!;
}
public static void Postfix(ref bool __result)
{
if (!__result)
{
@@ -0,0 +1,17 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Player;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(HideAndSeekManager))]
public static class HnsFinishTaskPatch
{
[HarmonyPostfix]
[HarmonyPatch(nameof(HideAndSeekManager.FinishTask))]
public static void FinishTaskPostfix(HideAndSeekManager __instance, PlayerTask task)
{
var completeTaskEvent = new CompleteHnsTaskEvent(__instance, task.Owner, task);
MiraEventManager.InvokeEvent(completeTaskEvent);
}
}
@@ -0,0 +1,35 @@
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
/// <summary>
/// Patch to invoke AfterMurderEvent after a kill animation is performed.
/// </summary>
[HarmonyPatch]
public static class KillAnimationMurderEventPatch
{
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<KillAnimation>(nameof(KillAnimation.CoPerformKill))!;
}
public static void Postfix(Il2CppObjectBase __instance)
{
var wrapper = new StateMachineWrapper<KillAnimation>(__instance);
if (wrapper.GetState() != 1)
{
return;
}
var source = wrapper.GetParameter<PlayerControl>("source");
var target = wrapper.GetParameter<PlayerControl>("target");
var afterMurderEvent = new AfterMurderEvent(source, target, Helpers.GetBodyById(target.PlayerId));
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
@@ -1,30 +0,0 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
/// <summary>
/// General patches for the KillAnimation class.
/// </summary>
[HarmonyPatch]
public static class KillAnimationPatches
{
/// <summary>
/// Used to trigger the <see cref="AfterMurderEvent"/>.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(KillAnimation._CoPerformKill_d__2), nameof(KillAnimation._CoPerformKill_d__2.MoveNext))]
public static void KillAnimPostfix(KillAnimation._CoPerformKill_d__2 __instance)
{
// Checking if state is -1 to wait for the end of coroutine.
if (__instance.__1__state != -1)
{
return;
}
var afterMurderEvent = new AfterMurderEvent(__instance.source, __instance.target, Helpers.GetBodyById(__instance.target.PlayerId));
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
@@ -41,7 +41,7 @@ public static class ReportBodyEventPatch
var source = AmongUsClient.Instance.FindObjectByNetId<PlayerControl>(reportBodyMessage.rpcObjectNetId);
if (source == null)
{
Logger<MiraApiPlugin>.Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
return true;
}
@@ -1,4 +1,5 @@
using HarmonyLib;
/*
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
@@ -18,3 +19,4 @@ internal static class SpawnInMinigamePatch
MiraEventManager.InvokeEvent(@event2);
}
}
*/
@@ -1,4 +1,5 @@
using System.Collections.Generic;
/*
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
@@ -34,3 +35,4 @@ internal static class SubmergedSpawnGamePatch
MiraEventManager.InvokeEvent(@event2);
}
}
*/
@@ -0,0 +1,75 @@
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Usables;
namespace MiraAPI.Patches.Events;
/// <summary>
/// Patches to invoke vent related MiraEvents.
/// </summary>
[HarmonyPatch]
public static class VanillaButtonPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(AbilityButton), nameof(AbilityButton.DoClick))]
public static bool DoClickPrefix(AbilityButton __instance)
{
// Invoke the generic button click event.
PlayerControl? playerTarget = null;
Vent? ventTarget = null;
var role = PlayerControl.LocalPlayer.Data.Role;
if (role.Role is RoleTypes.Tracker)
{
playerTarget = role.Cast<TrackerRole>().currentTarget;
}
else if (role.Role is RoleTypes.GuardianAngel)
{
playerTarget = role.Cast<GuardianAngelRole>().currentTarget;
}
else if (role.Role is RoleTypes.Engineer)
{
ventTarget = role.Cast<EngineerRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget, ventTarget);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance));
}
if (!genericEvent.IsCancelled)
{
return true;
}
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SecondaryAbilityButton), nameof(SecondaryAbilityButton.DoClick))]
public static bool SecondaryDoClickPrefix(AbilityButton __instance)
{
// Invoke the generic button click event.
PlayerControl? playerTarget = null;
var role = PlayerControl.LocalPlayer.Data.Role;
if (role.Role is RoleTypes.Detective)
{
playerTarget = role.Cast<DetectiveRole>().currentTarget;
}
var genericEvent = new VanillaButtonClickEvent(__instance, playerTarget);
MiraEventManager.InvokeEvent(genericEvent);
if (genericEvent.IsCancelled)
{
MiraEventManager.InvokeEvent(new VanillaButtonCancelledEvent(__instance));
}
if (!genericEvent.IsCancelled)
{
return true;
}
return false;
}
}
@@ -1,7 +1,5 @@
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Freeplay;
+347 -268
View File
@@ -1,15 +1,18 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
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;
@@ -18,9 +21,15 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Freeplay;
[HarmonyPatch(typeof(TaskAdderGame))]
internal static class TaskAdderPatches
public static class TaskAdderPatches
{
private static Scroller _scroller = null!;
private static Dictionary<string, TaskFolder> folders = new();
public static string CrewmateName => TranslationController.Instance.GetString(StringNames.Crewmate);
public static string ImpostorName => TranslationController.Instance.GetString(StringNames.Impostor);
public static string NeutralName => "Neutral";
public static string ModifiersName => "Modifiers";
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
public static void AddRolesFolder(TaskAdderGame __instance)
@@ -60,46 +69,165 @@ 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 crewmateFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == CrewmateName)!;
var impostorFolder = __instance.Root.SubFolders.ToArray().FirstOrDefault(x => x.FolderName == ImpostorName)!;
//var neutralFolder = __instance.CreateFolder("Neutral", __instance.Root, 2, Color.gray);
var modifiersFolder = __instance.CreateFolder(ModifiersName, __instance.Root, 0, Color.blue);
var rolesFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
rolesFolder.gameObject.SetActive(false);
rolesFolder.FolderName = "Roles";
rolesFolder.name = "RolesFolder";
var modifiersFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
modifiersFolder.gameObject.SetActive(false);
modifiersFolder.FolderName = "Modifiers";
modifiersFolder.name = "ModifiersFolder";
folders.Clear();
folders.Add(crewmateFolder.FolderName, crewmateFolder);
folders.Add(impostorFolder.FolderName, impostorFolder);
//folders.Add("Neutrals", neutralFolder);
folders.Add(ModifiersName, modifiersFolder);
int folderIdx = 2;
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);
pluginFolder.name = $"Roles:{folderName}:{plugin.PluginId}";
pluginFolders.Add(folderName, teamFolder);
}
}
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();
}
[HarmonyPrefix]
[HarmonyPatch(nameof(TaskAdderGame.PopulateRoot))]
private static bool PopulateRootPrefix(TaskAdderGame __instance, TaskAdderGame.FolderType folderType, TaskFolder rootFolder, Il2CppSystem.Collections.Generic.Dictionary<string, TaskFolder> folders, Il2CppReferenceArray<NormalPlayerTask> taskList)
{
if (folderType != TaskAdderGame.FolderType.Tasks)
{
if (folderType != TaskAdderGame.FolderType.Roles)
{
return false;
}
TaskFolder crewFolder = folders["Crewmates"] =
Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
crewFolder.gameObject.SetActive(false);
crewFolder.SetFolderColor(TaskFolder.FolderColor.Blue);
TaskFolder impFolder = folders["Impostors"] =
Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
impFolder.gameObject.SetActive(false);
impFolder.SetFolderColor(TaskFolder.FolderColor.Red);
crewFolder.FolderName = CrewmateName;
impFolder.FolderName = ImpostorName;
rootFolder.SubFolders.Insert(0, crewFolder);
rootFolder.SubFolders.Insert(0, impFolder);
Il2CppSystem.Collections.Generic.List<RoleBehaviour> impRoles = new();
Il2CppSystem.Collections.Generic.List<RoleBehaviour> crewRoles = new();
foreach (var role in RoleManager.Instance.AllRoles)
{
if (role.Role != RoleTypes.ImpostorGhost && role.Role != RoleTypes.CrewmateGhost && !role.IsCustomRole())
{
if (role.TeamType == RoleTeamTypes.Crewmate)
{
crewRoles.Add(role);
}
else
{
impRoles.Add(role);
}
}
}
crewFolder.RoleChildren = crewRoles;
impFolder.RoleChildren = impRoles;
return false;
}
if (taskList == null)
{
return false;
}
foreach (NormalPlayerTask normalPlayerTask in taskList)
{
SystemTypes systemTypes = normalPlayerTask.StartAt;
if (normalPlayerTask is DivertPowerTask task)
{
systemTypes = task.TargetSystem;
}
if (systemTypes == SystemTypes.LowerEngine)
{
systemTypes = SystemTypes.UpperEngine;
}
if (!folders.TryGetValue(systemTypes.ToString(), out var taskFolder3))
{
taskFolder3 = folders[systemTypes.ToString()] = Object.Instantiate(__instance.RootFolderPrefab, __instance.transform);
taskFolder3.SetFolderColor(TaskFolder.FolderColor.Tan);
taskFolder3.gameObject.SetActive(false);
if (systemTypes == SystemTypes.UpperEngine)
{
taskFolder3.FolderName = TranslationController.Instance.GetString(StringNames.Engines);
}
else
{
taskFolder3.FolderName = TranslationController.Instance.GetString(systemTypes);
}
rootFolder.SubFolders.Add(taskFolder3);
}
taskFolder3.TaskChildren.Add(normalPlayerTask);
}
return false;
}
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,
@@ -122,40 +250,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)
{
@@ -164,139 +298,202 @@ 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 => x is not 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.GetRoleName() : "Be_" + role.GetRoleName() + ".exe";
roleAddButton.Role = role;
if (prettyEnabled)
{
var pluginId = split[0];
var folderName = split[1];
roleAddButton.Text.fontSizeMin = 1;
roleAddButton.FileImage.sprite = role.IsImpostor
? MiraAssets.ImpostorFile.LoadAsset()
: MiraAssets.CrewmateFile.LoadAsset();
}
if (MiraPluginManager.GetPluginByGuid(pluginId) is { } plugin)
__instance.AddFileAsChildCustom(roleAddButton, ref num, ref num2, ref num3);
if (roleAddButton != null && roleAddButton.Button != null)
{
ControllerManager.Instance.AddSelectableUiElement(roleAddButton.Button);
}
}
string[] split = [];
try
{
split = taskFolder.name.Replace("(Clone)", string.Empty).Split(':');
}
catch
{
// I hate you
}
if (split is ["Roles", _, _] && folders.Any(x => taskFolder.IsChildOf(x.Value)))
{
var plugin = MiraPluginManager.GetPluginByGuid(split[2]);
if (plugin != null)
{
foreach (var role in plugin.InternalRoles)
{
if (folderName == "Roles(Clone)")
var roleBehaviour = role.Value;
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false })
{
PluginRoles(__instance, plugin, ref num, ref num2, ref num3);
continue;
}
else if (folderName == "Modifiers(Clone)")
var crewLocale = TranslationController.Instance.GetString(StringNames.Crewmate);
var impLocale = TranslationController.Instance.GetString(StringNames.Impostor);
var teamFolder = roleBehaviour.IsImpostor ? impLocale : crewLocale;
if (roleBehaviour is ICustomRole cs)
{
PluginModifiers(__instance, plugin, ref num, ref num2, ref num3);
teamFolder = cs.Configuration.FreeplayFolder;
}
if (split[1] != teamFolder)
{
continue;
}
var roleAddButton = Object.Instantiate(__instance.RoleButton);
roleAddButton.MyTask = null;
roleAddButton.SafePositionWorld = __instance.SafePositionWorld;
roleAddButton.Text.text = prettyEnabled ? roleBehaviour.GetRoleName() : "Be_" + roleBehaviour.GetRoleName() + ".exe";
roleAddButton.Text.EnableMasking();
roleAddButton.role = roleBehaviour;
if (prettyEnabled)
{
roleAddButton.Text.fontSizeMin = 1;
roleAddButton.FileImage.sprite = roleBehaviour.IsImpostor
? MiraAssets.ImpostorFile.LoadAsset()
: MiraAssets.CrewmateFile.LoadAsset();
}
if (roleBehaviour 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)
{
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);
ControllerManager.Instance.AddSelectableUiElement(roleAddButton.Button);
}
}
}
foreach (var chip in __instance.ActiveItems)
if (folders.TryGetValue(ModifiersName, out var modifiersFolder) && taskFolder.IsChildOf(modifiersFolder))
{
chip.GetComponentInChildren<TextMeshPro>().EnableStencilMasking();
chip.GetComponent<SpriteRenderer>().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
var plugin = MiraPluginManager.GetPluginByGuid(taskFolder.name.Replace("(Clone)", string.Empty));
if (plugin != null)
{
foreach (var modifier in plugin.InternalModifiers)
{
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.fontSizeMin = 1;
taskAddButton.Text.EnableMasking();
taskAddButton.FileImage.color = modifier.FreeplayFileColor;
taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor;
if (modifier is TimedModifier timed)
{
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);
}
}
}
if (_scroller)
@@ -306,129 +503,11 @@ internal static class TaskAdderPatches
_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.fontSizeMin = 1;
taskAddButton.Text.EnableMasking();
taskAddButton.FileImage.color = modifier.FreeplayFileColor;
taskAddButton.RolloverHandler.OutColor = modifier.FreeplayFileColor;
taskAddButton.Button.OnClick = new Button.ButtonClickedEvent();
taskAddButton.Button.OnClick.AddListener((UnityEngine.Events.UnityAction)(() =>
{
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;
}
}));
if (modifier is TimedModifier timed)
{
taskAddButton.FileImage.sprite = MiraAssets.TimedModifierFile.LoadAsset();
taskAddButton.Text.text += $"({timed.Duration}s)";
}
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 (roleBehaviour is ICustomRole custom)
{
var customColor = custom.IntroConfiguration?.IntroTeamColor ?? Color.gray;
if (custom.Team is ModdedRoleTeams.Crewmate)
{
customColor = Palette.CrewmateBlue;
}
else if (custom.Team is ModdedRoleTeams.Impostor)
{
customColor = Palette.ImpostorRed;
}
taskAddButton2.FileImage.color = customColor;
taskAddButton2.RolloverHandler.OutColor = customColor;
}
if (taskAddButton2.Button == null)
{
continue;
}
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;
}
}
}
}
+6 -1
View File
@@ -9,6 +9,11 @@ namespace MiraAPI.Patches.Hud;
public static class ButtonResetPatches
{
public static void Postfix()
{
ResetCooldowns();
}
public static void ResetCooldowns()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
@@ -18,7 +23,7 @@ public static class ButtonResetPatches
}
catch (System.Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
}
if (customActionButton.UsesMode == ButtonUsesMode.PerRound)
+144 -13
View File
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using HarmonyLib;
using MiraAPI.Hud;
using MiraAPI.Keybinds;
@@ -25,7 +25,87 @@ 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();
private static Dictionary<TextMeshPro, int> vanillaKeybindIcons = [];
internal static List<TextMeshPro> ModdedKeybindIcons = [];
public static IEnumerator CoResizeUI()
{
while (!HudManager.Instance)
{
yield return null;
}
yield return new WaitForSeconds(0.01f);
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
}
public static void ResizeUI(float scaleFactor)
{
var baseButtons = HudManager.Instance.transform.FindChild("Buttons");
if (baseButtons != null)
{
foreach (var aspect in baseButtons.GetComponentsInChildren<AspectPosition>(true))
{
if (aspect.gameObject == null)
{
continue;
}
if (aspect.gameObject.transform.parent.name == "TopRight")
{
continue;
}
if (aspect.gameObject.transform.parent.transform.parent.name == "TopRight")
{
continue;
}
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
aspect.DistanceFromEdge *= new Vector2(scaleFactor, scaleFactor);
aspect.gameObject.SetActive(!aspect.isActiveAndEnabled);
}
}
foreach (var button in HudManager.Instance.GetComponentsInChildren<ActionButton>(true))
{
if (button.gameObject == null)
{
continue;
}
button.gameObject.SetActive(!button.isActiveAndEnabled);
button.gameObject.transform.localScale *= scaleFactor;
button.gameObject.SetActive(!button.isActiveAndEnabled);
}
if (baseButtons != null)
{
foreach (var arrange in baseButtons.GetComponentsInChildren<GridArrange>(true))
{
if (!arrange.gameObject || !arrange.transform)
{
continue;
}
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
arrange.CellSize = new Vector2(scaleFactor, scaleFactor);
arrange.gameObject.SetActive(!arrange.isActiveAndEnabled);
if (arrange.isActiveAndEnabled && arrange.gameObject.transform.childCount != 0)
{
try
{
arrange.ArrangeChilds();
}
catch
{
// Error($"Error arranging child objects in GridArrange: {e}");
}
}
}
}
}
/*
/// <summary>
@@ -73,6 +153,23 @@ public static class HudManagerPatches
gridArrange.Alignment = GridArrange.StartAlign.Right;
aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom;
if (Constants.GetPlatformType() is Platforms.Android or Platforms.IPhone)
{
var fakeButton = Object.Instantiate(__instance.AbilityButton, BottomLeft.transform);
foreach (var renderer in fakeButton.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
}
fakeButton.buttonLabelText.Destroy();
fakeButton.cooldownTimerText.Destroy();
fakeButton.usesRemainingText.Destroy();
fakeButton.GetComponent<PassiveButton>().Destroy();
fakeButton.ToggleVisible(true);
fakeButton.Destroy();
}
ModdedKeybindIcons = [];
foreach (var button in CustomButtonManager.CustomButtons)
{
var location = button.Location switch
@@ -93,16 +190,18 @@ public static class HudManagerPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to create custom button {button.GetType().Name}: {e}");
Error($"Failed to create custom button {button.GetType().Name}: {e}");
}
}
__instance.ImpostorVentButton.transform.SetParent(null);
__instance.ImpostorVentButton.transform.SetParent(BottomRight.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
vanillaKeybindIcons = [];
var keybindIconPos = new Vector3(-0.4f, 0.4f, -9.5f);
var keybindIconPos = new Vector3(0.4f, 0.4f, -9.5f);
var vanillaButtons = new Dictionary<GameObject, int>
{
{ __instance.KillButton.gameObject, 8 },
@@ -125,9 +224,15 @@ public static class HudManagerPatches
}
var icon = Helpers.CreateKeybindIcon(buttonObj, key, keybindIconPos);
vanillaKeybindIcons.Add(icon.transform.GetChild(0).GetComponent<TextMeshPro>(), actionId);
var comp = buttonObj.GetComponent<ActionButton>();
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
if (!MiraApiPlugin.IsMobile)
{
var comp = buttonObj.GetComponent<ActionButton>();
KeybindManager.VanillaKeybinds[comp.GetType()].Button = comp;
}
}
MiraApiSettings.OldButtonScaleFactor =
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value;
Coroutines.Start(CoResizeUI());
}
/// <summary>
@@ -155,7 +260,7 @@ public static class HudManagerPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to set custom button {button.GetType().Name} active: {e}");
Error($"Failed to set custom button {button.GetType().Name} active: {e}");
}
}
}
@@ -164,25 +269,51 @@ public static class HudManagerPatches
[HarmonyPostfix]
public static void UpdatePostfix()
{
var canSeeBinds = ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard &&
LocalSettingsTabSingleton<MiraApiSettings>.Instance.ShowKeybinds.Value;
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);
btnIcon.Key.transform.parent.gameObject.SetActive(canSeeBinds);
}
foreach (var btnIcon in ModdedKeybindIcons)
{
btnIcon.transform.parent.gameObject.SetActive(canSeeBinds);
}
var player = ReInput.players.GetPlayer(0);
var keyboard = player.controllers.Keyboard;
foreach (var entry in KeybindManager.Keybinds)
{
var keyboard = player.controllers.Keyboard;
bool modifierKeysPressed = entry.ModifierKeys.All(k => keyboard.GetModifierKey(k)) ||
entry.ModifierKeys.Length <= 0;
var modKeys = entry.ModifierKeys;
bool modifierKeysPressed;
if (modKeys.Length == 0)
{
modifierKeysPressed = true;
}
else
{
modifierKeysPressed = true;
foreach (var key in modKeys)
{
if (keyboard.GetModifierKey(key)) continue;
modifierKeysPressed = false;
break;
}
}
if (player.GetButtonDown(entry.Id) && modifierKeysPressed)
{
entry.Invoke();
}
}
// suppressed warning because we want minimum allocations here
#pragma warning disable S3267
foreach (var entry in KeybindManager.VanillaKeybinds.Values)
#pragma warning restore S3267
{
if (player.GetButtonDown(entry.Id))
{
+73 -16
View File
@@ -1,8 +1,12 @@
using HarmonyLib;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
namespace MiraAPI.Patches;
@@ -29,14 +33,29 @@ 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)
[HarmonyPatch]
public static class IntroCutsceneShowRolePatch
{
var realInstance = __instance.__4__this;
var @event = new IntroRoleRevealEvent(realInstance);
MiraEventManager.InvokeEvent(@event);
public static MethodBase TargetMethod()
{
return Helpers.GetStateMachineMoveNext<IntroCutscene>(nameof(IntroCutscene.ShowRole))!;
}
public static void Postfix(Il2CppObjectBase __instance)
{
var wrapper = new StateMachineWrapper<IntroCutscene>(__instance);
// run before the first yield
if (wrapper.GetState() != 1)
{
return;
}
var introCutscene = wrapper.Instance;
Info("IntroCutscene ShowRole reached");
var @event = new IntroRoleRevealEvent(introCutscene);
MiraEventManager.InvokeEvent(@event);
}
}
[HarmonyPrefix]
@@ -66,16 +85,54 @@ public static class IntroCutscenePatches
}
}
[HarmonyPostfix]
[HarmonyPatch(nameof(IntroCutscene.OnDestroy))]
public static void GameBeginPatch(IntroCutscene __instance)
[HarmonyPatch]
public static class IntroCutsceneDestroyPatch
{
MiraEventManager.InvokeEvent(new IntroEndEvent(__instance));
private static bool _usedFallback;
var @event = new BeforeRoundStartEvent(true);
MiraEventManager.InvokeEvent(@event);
public static MethodBase TargetMethod()
{
var onDestroy = AccessTools.Method(typeof(IntroCutscene), "OnDestroy");
if (onDestroy != null)
{
_usedFallback = false;
Info("Using OnDestroy for IntroCutsceneDestroyPatch");
return onDestroy;
}
if (@event.IsCancelled) return;
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
_usedFallback = true;
return Helpers.GetStateMachineMoveNext<IntroCutscene>(nameof(IntroCutscene.CoBegin))!;
}
public static void Postfix(Il2CppObjectBase __instance)
{
IntroCutscene introCutscene;
if (_usedFallback)
{
var wrapper = new StateMachineWrapper<IntroCutscene>(__instance);
// run after the final yield
if (wrapper.GetState() != -1)
{
return;
}
introCutscene = wrapper.Instance;
}
else
{
introCutscene = __instance.Cast<IntroCutscene>();
}
Info("IntroCutscene ended");
MiraEventManager.InvokeEvent(new IntroEndEvent(introCutscene));
var @event = new BeforeRoundStartEvent(true);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled) return;
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
}
@@ -15,7 +15,8 @@ namespace MiraAPI.Patches.Keybinds;
[HarmonyPatch(typeof(ControlMapper))]
public static class ControlMapperPatches
{
[HarmonyPrefix]
// Patching this crashes for Epic Games users
/*[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Start))]
private static void StartPrefix(ControlMapper __instance)
{
@@ -37,7 +38,7 @@ public static class ControlMapperPatches
entry.Value = !entry.Value;
text.text = $"Show Keybinds: {(entry.Value ? "On" : "Off")}";
}));
}
}*/
[HarmonyPrefix]
[HarmonyPatch(nameof(ControlMapper.Update))]
@@ -16,8 +16,15 @@ public static class OptionsMenuBehaviourPatch
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;
try
{
_remap_background = _remap_rollover.transform.FindChild("Background").GetComponent<SpriteRenderer>();
_remap_background.color = _conflicts ? Color.red : Color.white;
}
catch
{
// ignored, this normally breaks when on mobile since there's no remap button screen.
}
}
[HarmonyPostfix]
@@ -22,7 +22,7 @@ public static class OptionsMenuPatches
private static Dictionary<int, List<GameObject>> tabButtons = new();
/// <summary>
/// Creates the tabs and their content
/// Creates the tabs and their content.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(OptionsMenuBehaviour.Start))]
@@ -8,7 +8,7 @@ public static class ToggleBehaviourPatch
{
/// <summary>
/// ResetText was inlined ofc.
/// Skill issue
/// Skill issue.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(ToggleButtonBehaviour.UpdateText))]
+35 -13
View File
@@ -6,10 +6,13 @@ using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
@@ -20,6 +23,20 @@ public static class HatsTabPatches
private static SortedList<string, List<HatData>> sortedHats = [];
private static int currentPage;
private static void PreviousPage(HatsTab hatsTab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
GenerateHats(hatsTab, currentPage);
}
private static void NextPage(HatsTab hatsTab)
{
currentPage++;
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
GenerateHats(hatsTab, currentPage);
}
[HarmonyPatch(nameof(HatsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(HatsTab __instance)
@@ -38,6 +55,8 @@ public static class HatsTabPatches
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
@@ -45,22 +64,17 @@ public static class HatsTabPatches
[HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
NextPage(__instance);
}
}
@@ -84,10 +98,10 @@ public static class HatsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})\nPress Ctrl or Tab to cycle pages";
text.text = $"{groupName} ({currentPage + 1}/{sortedHats.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
@@ -125,9 +139,17 @@ public static class HatsTabPatches
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = hat.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(hat)));
}
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.SelectionHighlight.gameObject.SetActive(false);
+54
View File
@@ -0,0 +1,54 @@
using System;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
public static class InventoryUtility
{
public static void CreateNextBackButtons<T>(T tab, Action<T> previousPage, Action<T> nextPage) where T : InventoryTab
{
var title = tab.transform.FindChild("Text");
title.localPosition = new Vector3(.9123f, -.23f, -55);
title.GetComponent<TextMeshPro>().alignment = TextAlignmentOptions.Center;
tab.scroller.Hitbox.transform.localScale -= new Vector3(0, 0.1f, 0);
tab.scroller.Hitbox.transform.localPosition -= new Vector3(0, 0.1f, 0);
var leftButtonElement = Object.Instantiate(PlayerCustomizationMenu.Instance.BackButton.gameObject, tab.transform);
var leftSpriteRenderer = leftButtonElement.GetComponent<SpriteRenderer>();
var leftButton = leftButtonElement.GetComponent<PassiveButton>();
leftButtonElement.transform.localPosition = new Vector3(-1.1f, -0.2f, -55);
leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
leftSpriteRenderer.flipX = true;
leftButton.OnMouseOver = new UnityEvent();
leftButton.OnMouseOver.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
leftButton.OnMouseOut = new UnityEvent();
leftButton.OnMouseOut.AddListener((Action)(() => leftSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
leftButton.OnClick = new Button.ButtonClickedEvent();
leftButton.OnClick.AddListener((Action)(() => previousPage(tab)));
var rightButtonElement = Object.Instantiate(leftButtonElement, tab.transform);
var rightSpriteRenderer = rightButtonElement.GetComponent<SpriteRenderer>();
var rightButton = rightButtonElement.GetComponent<PassiveButton>();
rightButtonElement.transform.localPosition = new Vector3(2.8f, -0.2f, -55);
rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset();
rightSpriteRenderer.flipX = false;
rightButton.OnMouseOver = new UnityEvent();
rightButton.OnMouseOver.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButtonActive.LoadAsset()));
rightButton.OnMouseOut = new UnityEvent();
rightButton.OnMouseOut.AddListener((Action)(() => rightSpriteRenderer.sprite = MiraAssets.NextButton.LoadAsset()));
rightButton.OnClick = new Button.ButtonClickedEvent();
rightButton.OnClick.AddListener((Action)(() => nextPage(tab)));
}
}
+14 -1
View File
@@ -1,5 +1,9 @@
using HarmonyLib;
using System.Collections;
using HarmonyLib;
using MiraAPI.LocalSettings;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Menu;
@@ -17,5 +21,14 @@ public static class MainMenuManagerPatches
public static void AwakePostfix()
{
AddressablesLoader.LoadAll();
Coroutines.Start(SetFps());
}
private static IEnumerator SetFps()
{
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
yield return new WaitForSeconds(1f);
Application.targetFrameRate = (int)LocalSettingsTabSingleton<MiraApiSettings>.Instance.SetFpsSlider.Value;
}
}
+36 -12
View File
@@ -26,6 +26,20 @@ public static class NameplatesTabPatches
}
}
private static void PreviousPage(NameplatesTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(NameplatesTab tab)
{
currentPage++;
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
@@ -33,6 +47,9 @@ public static class NameplatesTabPatches
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
@@ -45,15 +62,11 @@ public static class NameplatesTabPatches
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
NextPage(__instance);
}
}
@@ -73,10 +86,10 @@ public static class NameplatesTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})\nPress Ctrl or Tab to cycle pages";
text.text = $"{groupName} ({currentPage + 1}/{SortedNameplates.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
@@ -99,9 +112,20 @@ public static class NameplatesTabPatches
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = namePlate.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() => __instance.SelectNameplate(
HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = namePlate.ProdId;
+33 -12
View File
@@ -18,6 +18,20 @@ public static class SkinsTabPatches
private static SortedList<string, List<SkinData>> sortedSkins = [];
private static int currentPage;
private static void PreviousPage(SkinsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(SkinsTab tab)
{
currentPage++;
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(SkinsTab __instance)
@@ -36,6 +50,8 @@ public static class SkinsTabPatches
}
}
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
@@ -48,15 +64,11 @@ public static class SkinsTabPatches
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
NextPage(__instance);
}
}
@@ -76,10 +88,10 @@ public static class SkinsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})\nPress Ctrl or Tab to cycle pages";
text.text = $"{groupName} ({currentPage + 1}/{sortedSkins.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
@@ -102,9 +114,18 @@ public static class SkinsTabPatches
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = skin.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() =>
__instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectSkin(skin)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = skin.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
+37 -12
View File
@@ -25,6 +25,21 @@ public static class VisorsTabPatches
}
}
private static void PreviousPage(VisorsTab tab)
{
currentPage--;
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
GenerateHats(tab, currentPage);
}
private static void NextPage(VisorsTab tab)
{
currentPage++;
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
GenerateHats(tab, currentPage);
}
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
@@ -32,6 +47,9 @@ public static class VisorsTabPatches
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
InventoryUtility.CreateNextBackButtons(__instance, PreviousPage, NextPage);
GenerateHats(__instance, currentPage);
return false;
@@ -44,15 +62,11 @@ public static class VisorsTabPatches
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
PreviousPage(__instance);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
NextPage(__instance);
}
}
@@ -72,10 +86,10 @@ public static class VisorsTabPatches
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})\nPress Ctrl or Tab to cycle pages";
text.text = $"{groupName} ({currentPage + 1}/{SortedVisors.Count})";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSize = 5f;
text.fontSizeMax = 5f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
@@ -98,9 +112,20 @@ public static class VisorsTabPatches
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = visor.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard)
{
colorChip.Button.OnClick.AddListener((Action)__instance.ClickEquip);
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
colorChip.Button.OnMouseOut.AddListener(
(Action)(() =>
__instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
}
else
{
colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectVisor(visor)));
}
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = visor.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
+167 -14
View File
@@ -1,8 +1,11 @@
using System;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
@@ -10,6 +13,7 @@ using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
@@ -17,6 +21,20 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(GameOptionsMenu))]
internal static class GameOptionsMenuPatch
{
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.OnEnable))]
public static void OpenPatch(GameOptionsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.OnDisable))]
public static void ClosePatch(GameOptionsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.Update))]
// ReSharper disable once InconsistentNaming
@@ -32,11 +50,20 @@ internal static class GameOptionsMenuPatch
{
ModifiersUpdate(ref num);
}
else if (__instance.name == "CUSTOM TAB 1")
{
CustomMenuOneUpdate(ref num);
}
else if (__instance.name == "CUSTOM TAB 2")
{
CustomMenuTwoUpdate(ref num);
}
else
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
.Where(x => x.OptionableType == null &&
!x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
@@ -99,10 +126,52 @@ internal static class GameOptionsMenuPatch
}
}
private static void CustomMenuOneUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var modGroup in groups)
{
UpdateGroup(modGroup, ref num);
}
}
private static void CustomMenuOneCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomOne) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
}
private static void CustomMenuTwoUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var modGroup in groups)
{
UpdateGroup(modGroup, ref num);
}
}
private static void CustomMenuTwoCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ParentMenu == MenuCategory.CustomTwo) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
}
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var modGroup in groups)
{
@@ -113,7 +182,7 @@ internal static class GameOptionsMenuPatch
private static void ModifiersCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
@@ -137,9 +206,19 @@ internal static class GameOptionsMenuPatch
ModifiersCreate(__instance);
return false;
}
else if (__instance.name == "CUSTOM TAB 1")
{
CustomMenuOneCreate(__instance);
return false;
}
else if (__instance.name == "CUSTOM TAB 2")
{
CustomMenuTwoCreate(__instance);
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu && x.ParentMenu == MenuCategory.Roles) ?? [];
foreach (var group in filteredGroups)
{
@@ -196,7 +275,9 @@ internal static class GameOptionsMenuPatch
categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20);
categoryHeaderMasked.Background.color = group.GroupColor;
categoryHeaderMasked.Divider.color = group.GroupColor;
categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor();
categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
@@ -215,13 +296,20 @@ internal static class GameOptionsMenuPatch
newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
var options = group.Options.Select(
opt => opt.CreateOption(
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.playerOptionOrigin,
menu.settingsContainer));
var options = group.Options.Select(opt => opt.CreateOption(
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.playerOptionOrigin,
menu.settingsContainer));
OptionPreset? defaultPreset = null;
if (GameSettingMenuPatches.SelectedMod != null && PresetManager.DefaultPresets.TryGetValue(
GameSettingMenuPatches.SelectedMod,
out var preset))
{
defaultPreset = preset;
}
foreach (var newOpt in options)
{
@@ -257,14 +345,79 @@ internal static class GameOptionsMenuPatch
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite")
.GetComponent<SpriteRenderer>();
rend.sprite = MiraAssets.CheckmarkBox.LoadAsset();
rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}
menu.Children.Add(newOpt);
var resetBtn = new GameObject("ResetOption");
resetBtn.transform.parent = newOpt.transform;
resetBtn.transform.localScale = new(.5f, .5f, 1);
resetBtn.layer = LayerMask.NameToLayer("UI");
resetBtn.transform.localPosition = new Vector3(-3.1f, 0f, -2f);
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
if (newOpt is ToggleOption toggleOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(toggleOpt);
}));
}
else if (newOpt is NumberOption numOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(numOpt);
}));
}
else if (newOpt is StringOption strOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(strOpt);
}));
}
if (!defaultPreset!.IsOptionInPreset(newOpt))
{
resetBtn.Destroy();
}
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
@@ -117,7 +117,7 @@ internal static class GamePresetsTabPatches
// ReSharper disable once InconsistentNaming
public static void Postfix()
{
Logger<MiraApiPlugin>.Error("OnDisable called");
Error("OnDisable called");
if (_saveButton)
{
@@ -155,7 +155,7 @@ internal static class GamePresetsTabPatches
var prefab = GameSettingMenu.Instance.GameSettingsButton;
if (prefab == null)
{
Logger<MiraApiPlugin>.Error("GameSettingsButton prefab is null");
Error("GameSettingsButton prefab is null");
return;
}
@@ -318,10 +318,10 @@ internal static class GamePresetsTabPatches
private static void Refresh()
{
Logger<MiraApiPlugin>.Error("Refreshing presets");
Error("Refreshing presets");
if (GameSettingMenuPatches.SelectedMod == null)
{
Logger<MiraApiPlugin>.Error("Selected mod is null, cannot refresh presets");
Error("Selected mod is null, cannot refresh presets");
return;
}
lock (Lock)
+202 -33
View File
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
@@ -27,13 +28,23 @@ internal static class GameSettingMenuPatches
private static Vector3 _roleBtnOgPos;
private static Vector3 _smallRoleBtnOgPos;
private static Vector3 _modifierBtnOgPos;
private static Vector3 _customOneBtnOgPos;
private static Vector3 _customTwoBtnOgPos;
private static GameOptionsMenu? _modifiersTab;
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
private static GameOptionsMenu? _customOneTab;
private static PassiveButton? _customOneButton;
private static GameOptionsMenu? _customTwoTab;
private static PassiveButton? _customTwoButton;
private static GameObject? _nextModButton;
private static GameObject? _previousModButton;
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
private static Dictionary<int, Vector3> CustomOnePositions { get; } = [];
private static Dictionary<int, Vector3> CustomTwoPositions { get; } = [];
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
{
@@ -41,6 +52,14 @@ internal static class GameSettingMenuPatches
{
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
}
else if (_customOneTab)
{
CustomOnePositions[SelectedModIdx] = _customOneTab!.scrollBar.Inner.localPosition;
}
else if (_customTwoTab)
{
CustomTwoPositions[SelectedModIdx] = _customTwoTab!.scrollBar.Inner.localPosition;
}
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
@@ -65,9 +84,21 @@ internal static class GameSettingMenuPatches
_modifiersTab!.gameObject.SetActive(tabNum == 3);
}
if (_customOneTab)
{
_customOneTab!.gameObject.SetActive(tabNum == 4);
}
if (_customTwoTab)
{
_customTwoTab!.gameObject.SetActive(tabNum == 5);
}
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum == 3)
_customOneButton?.SelectButton(tabNum == 4);
_customTwoButton?.SelectButton(tabNum == 5);
if (tabNum >= 3 && tabNum <= 5)
{
if (__instance.RoleSettingsButton.gameObject.active)
{
@@ -75,7 +106,21 @@ internal static class GameSettingMenuPatches
}
else
{
_modifiersButton?.SelectButton(true);
switch (tabNum)
{
case 3:
_modifiersButton?.SelectButton(true);
__instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.ModifierMenuDescription : "Configure modifiers and their settings here!";
break;
case 4:
_customOneButton?.SelectButton(true);
__instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuOneDescription : "Apply game settings for this mod!";
break;
case 5:
_customTwoButton?.SelectButton(true);
__instance.MenuDescriptionText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuTwoDescription : "Apply game settings for this mod!";
break;
}
}
}
}
@@ -87,19 +132,30 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
_customOneButton?.SelectButton(tabNum == 4);
_customTwoButton?.SelectButton(tabNum == 5);
if (tabNum != 3)
if (tabNum >= 3 && tabNum <= 5)
{
return;
}
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
_modifiersButton?.SelectButton(true);
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
switch (tabNum)
{
case 3:
_modifiersButton?.SelectButton(true);
break;
case 4:
_customOneButton?.SelectButton(true);
break;
case 5:
_customTwoButton?.SelectButton(true);
break;
}
}
}
}
@@ -127,16 +183,16 @@ internal static class GameSettingMenuPatches
_text.alignment = TextAlignmentOptions.Center;
var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
nextButton.name = "RightArrowButton";
nextButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite =
_nextModButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
_nextModButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
_nextModButton.name = "RightArrowButton";
_nextModButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButton.LoadAsset();
nextButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite =
_nextModButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButtonActive.LoadAsset();
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
_nextModButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
var passiveButton = nextButton.gameObject.GetComponent<PassiveButton>();
var passiveButton = _nextModButton.gameObject.GetComponent<PassiveButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
@@ -151,13 +207,13 @@ internal static class GameSettingMenuPatches
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
backButton.name = "LeftArrowButton";
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
backButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX =
backButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
_previousModButton = Object.Instantiate(_nextModButton, __instance.BackButton.transform.parent).gameObject;
_previousModButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
_previousModButton.name = "LeftArrowButton";
_previousModButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
_previousModButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX =
_previousModButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
_previousModButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
SaveScrollPositions(__instance);
@@ -174,6 +230,61 @@ internal static class GameSettingMenuPatches
_modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_modifiersTab.name = "MODIFIERS TAB";
// clone game settings tab for both custom tabs
_customOneTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_customOneTab.name = "CUSTOM TAB 1";
_customTwoTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_customTwoTab.name = "CUSTOM TAB 2";
// create button for the first custom category
var customPos = new Vector3(
__instance.RoleSettingsButton.transform.localPosition.x,
__instance.RoleSettingsButton.transform.localPosition.y,
__instance.RoleSettingsButton.transform.localPosition.z);
_customOneButton = Object.Instantiate(__instance.RoleSettingsButton, __instance.RoleSettingsButton.transform.parent);
_customOneButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
_customOneButton.OnClick = new ButtonClickedEvent();
_customOneButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(4, false);
}));
_customOneButton.OnMouseOver = new UnityEvent();
_customOneButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(4, true);
}));
_customOneButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameOne : "Custom Category 1";
customPos.y -= 0.637f;
_customOneButton.transform.localPosition = customPos;
_customOneButton.name = "CustomOneButton";
_customOneBtnOgPos = _customOneButton.transform.localPosition;
// create button for the first custom category
_customTwoButton = Object.Instantiate(_customOneButton, _customOneButton.transform.parent);
_customTwoButton.OnClick = new ButtonClickedEvent();
_customTwoButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(5, false);
}));
_customTwoButton.OnMouseOver = new UnityEvent();
_customTwoButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(5, true);
}));
_customTwoButton.buttonText.text = SelectedMod != null ? SelectedMod.MiraPlugin.CustomOptionMenuNameTwo : "Custom Category 2";
customPos.y -= 0.637f;
_customTwoButton.transform.localPosition = customPos;
_customTwoButton.name = "CustomTwoButton";
_customTwoBtnOgPos = _customTwoButton.transform.localPosition;
// create button for modifiers
__instance.RoleSettingsButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
__instance.RoleSettingsButton.gameObject.SetActive(false);
@@ -304,20 +415,32 @@ internal static class GameSettingMenuPatches
bool replaceWithModifiers = true;
_smallRolesButton!.transform.localPosition = _smallRoleBtnOgPos;
_modifiersButton!.transform.localPosition = _modifierBtnOgPos;
_customOneButton!.transform.localPosition = _customOneBtnOgPos;
_customTwoButton!.transform.localPosition = _customTwoBtnOgPos;
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
if (SelectedModIdx != 0)
{
_customOneButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameOne;
_customTwoButton.buttonText.text = SelectedMod!.MiraPlugin.CustomOptionMenuNameTwo;
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
var modHasCustomOne = SelectedMod.InternalOptionGroups.Exists(
x => x.ParentMenu == MenuCategory.CustomOne);
var modHasCustomTwo = SelectedMod.InternalOptionGroups.Exists(
x => x.ParentMenu == MenuCategory.CustomTwo);
var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
x => x.ShowInModifiersMenu || x.ParentMenu == MenuCategory.Modifiers || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions =
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && (!x.ShowInModifiersMenu ||
(x.ParentMenu != MenuCategory.Modifiers && x.ParentMenu != MenuCategory.Roles)));
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
_customOneButton.gameObject.SetActive(true);
_customTwoButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(false);
var defaultButton = menu.RoleSettingsButton;
// If there are no registered custom roles in the selected mod, hide the button.
if (!modHasRoles)
@@ -334,6 +457,7 @@ internal static class GameSettingMenuPatches
{
_modifiersButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
defaultButton = _modifiersButton;
if (_modifiersTab!.gameObject.active)
{
@@ -356,6 +480,7 @@ internal static class GameSettingMenuPatches
// If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles)
{
defaultButton = menu.RoleSettingsButton;
menu.RoleSettingsButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(false);
}
@@ -370,6 +495,7 @@ internal static class GameSettingMenuPatches
{
menu.ChangeTab(0, false);
}
defaultButton = menu.RoleSettingsButton;
// If the mod has roles and modifiers, we can move their buttons to the game settings button position, since nothing is there.
if (menu.RoleSettingsButton.gameObject.active)
@@ -389,16 +515,37 @@ internal static class GameSettingMenuPatches
_smallRolesButton.transform.localPosition.z);
}
}
// If the mod has no primary custom tab, we can go ahead and hide the button.
if (!modHasCustomOne)
{
_customOneButton.gameObject.SetActive(false);
if (_customOneTab!.gameObject.active)
{
defaultButton.SelectButton(true);
}
}
// If the mod has no secondary custom tab, we can go ahead and hide the button.
if (!modHasCustomTwo)
{
_customTwoButton.gameObject.SetActive(false);
if (_customTwoTab!.gameObject.active)
{
defaultButton.SelectButton(true);
}
}
}
else
{
_modifiersButton.gameObject.SetActive(false);
_smallRolesButton.gameObject.SetActive(false);
_customOneButton.gameObject.SetActive(false);
_customTwoButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
replaceWithModifiers = false;
if (_modifiersTab!.gameObject.active)
if (_modifiersTab!.gameObject.active || _customOneTab!.gameObject.active || _customTwoTab!.gameObject.active)
{
menu.ChangeTab(0, false);
}
@@ -446,7 +593,17 @@ internal static class GameSettingMenuPatches
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab, true);
CleanupSettings(_modifiersTab, MenuCategory.Modifiers);
}
if (_customOneTab?.Children?.Count > 0)
{
CleanupSettings(_customOneTab, MenuCategory.CustomOne);
}
if (_customTwoTab?.Children?.Count > 0)
{
CleanupSettings(_customTwoTab, MenuCategory.CustomTwo);
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
@@ -484,7 +641,7 @@ internal static class GameSettingMenuPatches
}
}
void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
void CleanupSettings(GameOptionsMenu gameOptMenu, MenuCategory menuCategory = MenuCategory.Roles)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
@@ -495,7 +652,19 @@ internal static class GameSettingMenuPatches
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
var positions = modifiers ? ModifiersPositions : OptionsPositions;
var positions = OptionsPositions;
switch (menuCategory)
{
case MenuCategory.Modifiers:
positions = ModifiersPositions;
break;
case MenuCategory.CustomOne:
positions = CustomOnePositions;
break;
case MenuCategory.CustomTwo:
positions = CustomTwoPositions;
break;
}
if (positions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
+145 -6
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
@@ -163,7 +164,117 @@ public static class LobbyViewPanePatches
{
if (SelectedModIdx == 0)
{
return true;
float num = 0.95f;
float num2 = -6.53f;
CategoryHeaderMasked categoryHeaderMasked =
Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
categoryHeaderMasked.SetHeader(StringNames.RoleQuotaLabel, 61);
categoryHeaderMasked.transform.localScale = Vector3.one;
categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, 1.26f, -2f);
__instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
var list = new List<RoleBehaviour>();
for (int i = 0; i < 2; i++)
{
var categoryHeaderRoleVariant =
Object.Instantiate(__instance.categoryHeaderRoleOrigin, __instance.settingsContainer);
categoryHeaderRoleVariant.SetHeader(
(i == 0) ? StringNames.CrewmateRolesHeader : StringNames.ImpostorRolesHeader,
61);
categoryHeaderRoleVariant.transform.localScale = Vector3.one;
categoryHeaderRoleVariant.transform.localPosition = new Vector3(0.09f, num, -2f);
__instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
var roles = RoleManager.Instance.AllRoles.ToArray().Where(x =>
x is not ICustomRole && !x.IsRoleBlacklisted() && x.Role != RoleTypes.Crewmate &&
x.Role != RoleTypes.Impostor && ((i == 0 && x.TeamType is RoleTeamTypes.Crewmate) ||
(i == 1 && x.TeamType is RoleTeamTypes.Impostor)) &&
x.Role != RoleTypes.CrewmateGhost && x.Role != RoleTypes.ImpostorGhost).ToList();
for (int j = 0; j < roles.Count; j++)
{
var roleBehaviour = roles[j];
int chancePerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetChancePerGame(
roleBehaviour.Role);
int numPerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions
.GetNumPerGame(roleBehaviour.Role);
bool flag = numPerGame == 0;
var viewSettingsInfoPanelRoleVariant =
Object.Instantiate(
__instance.infoPanelRoleOrigin,
__instance.settingsContainer);
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
if (!flag)
{
list.Add(roleBehaviour);
}
var color = (i == 0) ? Palette.CrewmateRoleBlue : Palette.ImpostorRoleRed;
if (roleBehaviour is ICustomRole custom && (custom.Team is not ModdedRoleTeams.Crewmate &&
custom.Team is not ModdedRoleTeams.Impostor))
{
color = Color.grey;
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
numPerGame,
chancePerGame,
61,
color,
roleBehaviour.RoleIconSolid,
i == 0,
flag);
__instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
}
if (list.Count > 0)
{
var categoryHeaderMasked2 =
Object.Instantiate(__instance.categoryHeaderOrigin, __instance.settingsContainer);
categoryHeaderMasked2.SetHeader(StringNames.RoleSettingsLabel, 61);
categoryHeaderMasked2.transform.localScale = Vector3.one;
categoryHeaderMasked2.transform.localPosition = new Vector3(-9.77f, num, -2f);
__instance.settingsInfo.Add(categoryHeaderMasked2.gameObject);
num -= 2.1f;
float num3 = 0f;
for (int k = 0; k < list.Count; k++)
{
float num4;
if (k % 2 == 0)
{
num4 = -5.8f;
if (k > 0)
{
num -= num3 + 0.85f;
num3 = 0f;
}
}
else
{
num4 = 0.14999962f;
}
var advancedRoleViewPanel =
Object.Instantiate(__instance.advancedRolePanelOrigin, __instance.settingsContainer);
advancedRoleViewPanel.transform.localScale = Vector3.one;
advancedRoleViewPanel.transform.localPosition = new Vector3(num4, num, -2f);
float num5 = advancedRoleViewPanel.SetUp(list[k], 0.85f, 61);
if (num5 > num3)
{
num3 = num5;
}
__instance.settingsInfo.Add(advancedRoleViewPanel.gameObject);
}
}
__instance.scrollBar.SetYBoundsMax(-num);
return false;
}
if (__instance.currentTab == ModifiersTabName)
@@ -278,7 +389,7 @@ public static class LobbyViewPanePatches
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings))
if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings || !x.VisibleInSettings()))
{
continue;
}
@@ -309,7 +420,7 @@ public static class LobbyViewPanePatches
foreach (var customRole in grouping)
{
if (customRole.Configuration.HideSettings)
if (customRole.Configuration.HideSettings || !customRole.VisibleInSettings())
{
continue;
}
@@ -344,7 +455,7 @@ public static class LobbyViewPanePatches
}
viewSettingsInfoPanelRoleVariant.SetInfo(
roleBehaviour.NiceName,
roleBehaviour.GetRoleName(),
numPerGame,
chancePerGame,
61,
@@ -409,6 +520,34 @@ public static class LobbyViewPanePatches
instance.scrollBar.SetYBoundsMax(-num);
}
public static void SetModdedHeader(this CategoryHeaderRoleVariant header, StringNames roleName, int maskLayer, ModdedRoleTeams team, Sprite? roleIcon = null)
{
header.SetHeader(roleName, maskLayer);
if (team is ModdedRoleTeams.Crewmate)
{
header.Background.color = Palette.CrewmateRoleHeaderBlue;
header.Divider.color = Palette.CrewmateRoleHeaderBlue;
header.Title.color = Palette.CrewmateRoleHeaderTextBlue;
}
else if (team is ModdedRoleTeams.Impostor)
{
header.Background.color = Palette.ImpostorRoleHeaderRed;
header.Divider.color = Palette.ImpostorRoleHeaderRed;
header.Title.color = Palette.ImpostorRoleHeaderTextRed;
}
else
{
header.Background.color = Color.grey;
header.Divider.color = Color.grey;
header.Title.color = new Color32(50, 50, 50, 255);
}
if (roleIcon != null && header.icon != null)
{
header.icon.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
header.icon.sprite = roleIcon;
}
}
private static float SetUpAdvancedRoleViewPanel(
AdvancedRoleViewPanel viewPanel,
Type roleType,
@@ -432,10 +571,10 @@ public static class LobbyViewPanePatches
return 0;
}
viewPanel.header.SetHeader(
viewPanel.header.SetModdedHeader(
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Team,
customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
+20 -2
View File
@@ -216,7 +216,16 @@ public static class OptionsPatches
return true;
}
__instance.Value += __instance.Increment;
var increment = __instance.Increment;
if (Input.GetKey(KeyCode.LeftShift) &&
__instance.TryGetComponent<MiraNumberOptionComponent>(out var miraNumber) &&
miraNumber.ShiftIncrementToggle)
{
increment /= 2;
}
__instance.Value += increment;
if (__instance.Value > __instance.ValidRange.max)
{
__instance.Value = __instance.ValidRange.min;
@@ -235,7 +244,16 @@ public static class OptionsPatches
return true;
}
__instance.Value -= __instance.Increment;
var increment = __instance.Increment;
if (Input.GetKey(KeyCode.LeftShift) &&
__instance.TryGetComponent<MiraNumberOptionComponent>(out var miraNumber) &&
miraNumber.ShiftIncrementToggle)
{
increment /= 2;
}
__instance.Value -= increment;
if (__instance.Value < __instance.ValidRange.min)
{
__instance.Value = __instance.ValidRange.max;
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
@@ -34,6 +35,19 @@ public static class RoleSettingMenuPatches
private static RoleBehaviour? CurrentRole { get; set; }
private static List<IModdedOption>? CurrentRoleOptions { get; set; }
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.OnEnable))]
public static void OpenPatch(RolesSettingsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.OnDisable))]
public static void ClosePatch(RolesSettingsMenu __instance)
{
HudManager.Instance.PlayerCam.OverrideScreenShakeEnabled = true;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
@@ -57,6 +71,7 @@ public static class RoleSettingMenuPatches
var maskBg = __instance.scrollBar.transform.FindChild("MaskBg");
var hitbox = __instance.scrollBar.transform.FindChild("Hitbox");
var dividerImage = __instance.transform.FindChild("HeaderButtons/DividerImage");
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
@@ -67,7 +82,64 @@ public static class RoleSettingMenuPatches
maskBg.localScale = new Vector3(6.6811f, 3.3563f, 0.5598f);
hitbox.localPosition = new Vector3(0.3297f, -.2333f, 4f);
hitbox.localScale = new Vector3(1, 1, 1);
return true;
dividerImage.gameObject.SetActive(true);
var num = 0.662f;
var list = CustomRoleManager.AllRoles.Where(
r => !r.IsCustomRole() && !r.IsRoleBlacklisted() && r.TeamType == RoleTeamTypes.Crewmate && r.Role != RoleTypes.Crewmate &&
r.Role != RoleTypes.CrewmateGhost).ToList();
var list2 = CustomRoleManager.AllRoles.Where(
r => !r.IsCustomRole() && !r.IsRoleBlacklisted() && r.TeamType == RoleTeamTypes.Impostor && r.Role != RoleTypes.Impostor &&
r.Role != RoleTypes.ImpostorGhost).ToList();
if (__instance.roleTabs == null || __instance.roleTabs.Count == 0)
{
var num2 = -1.928f;
__instance.roleTabs = new();
__instance.roleTabs.Add(__instance.AllButton);
foreach (var t in list)
{
__instance.AddRoleTab(t, ref num2);
}
foreach (var t in list2)
{
__instance.AddRoleTab(t, ref num2);
}
}
var categoryHeaderEditRole = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20);
categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
var num3 = 0;
foreach (var t in list)
{
__instance.CreateQuotaOption(t, ref num, num3);
num3++;
}
num -= 0.22f;
var categoryHeaderEditRole2 = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20);
categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var t in list2)
{
__instance.CreateQuotaOption(t, ref num, num3);
num3++;
}
return false;
}
ScrollerNum = 0.522f;
@@ -79,15 +151,16 @@ public static class RoleSettingMenuPatches
maskBg.localScale = new Vector3(6.6811f, 4.1563f, 0.5598f);
hitbox.localPosition = new Vector3(0.3297f, -.6333f, 4f);
hitbox.localScale = new Vector3(1, 1.2f, 1);
dividerImage.gameObject.SetActive(false);
var num3 = 0;
var num4 = 0;
var roleGroups = GameSettingMenuPatches.SelectedMod?.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (roleGroups is null)
{
return true;
return false;
}
// sort the groups by priority
@@ -100,7 +173,7 @@ public static class RoleSettingMenuPatches
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings))
if (!grouping.Any() || grouping.All(x=> x.Configuration.HideSettings || !x.VisibleInSettings()))
{
continue;
}
@@ -197,11 +270,11 @@ public static class RoleSettingMenuPatches
continue;
}
var option = CreateQuotaOption(__instance, roleBehaviour, num3);
var option = CreateQuotaOption(__instance, roleBehaviour, num4);
if (option is not null)
{
RoleOptionSettings.Add(option);
num3++;
num4++;
}
}
}
@@ -332,7 +405,7 @@ public static class RoleSettingMenuPatches
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Warning(e);
Warning(e);
}
roleSetting.UpdateValuesAndText(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions);
@@ -406,16 +479,14 @@ public static class RoleSettingMenuPatches
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
Error($"Role {role.NiceName} is not a custom role.");
return;
}
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = TranslationController.Instance.GetString(
role.StringName,
new Il2CppReferenceArray<Il2CppSystem.Object>(0));
__instance.roleTitleText.text = role.GetRoleName();
var imgBg = __instance.AdvancedRolesSettings.transform.FindChild("Imagebackground");
var labelBg = __instance.AdvancedRolesSettings.transform.FindChild("InfoLabelBackground");
@@ -502,7 +573,7 @@ public static class RoleSettingMenuPatches
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
Error($"Role {role.NiceName} is not a custom role.");
return null;
}
@@ -519,6 +590,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.transform.localPosition = new Vector3(-0.1f, ScrollerNum, -2f);
roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20);
roleOptionSetting.titleText.text = role.GetRoleName();
roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor;
roleOptionSetting.OnValueChanged = new Action<OptionBehaviour>(ValueChanged);
roleOptionSetting.SetClickMask(__instance.ButtonClickMask);
@@ -532,6 +604,19 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor();
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
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 (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.InternalOptionGroups
.Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible())))
@@ -551,18 +636,6 @@ 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 })
+30 -1
View File
@@ -1,5 +1,8 @@
using System.Globalization;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
using UnityEngine;
@@ -28,7 +31,33 @@ public static class SettingPatches
string result;
var suffix = (MiraNumberSuffixes)__instance.SuffixType;
if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f)
var custom =
ModdedOptionsManager.ModdedOptions.Values.FirstOrDefault(opt =>
opt.OptionBehaviour != null && opt.OptionBehaviour.Data == __instance);
if (custom is ModdedNumberOption moddedNumberOption)
{
if (moddedNumberOption.NegativeWordValue != "#" && (int)value == -1)
{
result = $"<b>{moddedNumberOption.NegativeWordValue}</b>";
}
else if (moddedNumberOption.ZeroWordValue != "#" && Mathf.Abs(value) < 0.0001f)
{
result = $"<b>{moddedNumberOption.ZeroWordValue}</b>";
}
else
{
result = suffix switch
{
MiraNumberSuffixes.None => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo),
MiraNumberSuffixes.Multiplier => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "x",
MiraNumberSuffixes.Percent => value.ToString(__instance.FormatString, NumberFormatInfo.InvariantInfo) + "%",
_ => TranslationController.Instance.GetString(
StringNames.GameSecondsAbbrev,
(Il2CppSystem.Object[])[value.ToString(__instance.FormatString, CultureInfo.InvariantCulture)]),
};
}
}
else if (__instance.ZeroIsInfinity && Mathf.Abs(value) < 0.0001f)
{
result = "<b>∞</b>";
}
+64 -9
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Globalization;
using System.Linq;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
@@ -67,15 +68,67 @@ internal static class PlayerControlPatches
}
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
// ReSharper disable once InconsistentNaming
// Note: This method is partially inlined in 2025.5.20 but we already use custom murder RPCs.
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
[HarmonyPatch(nameof(PlayerControl.CheckMurder))]
public static void PlayerControlCheckMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool __runOriginal)
{
__runOriginal = false;
__instance.logger.Debug($"Checking if {__instance.PlayerId} murdered {(target == null ? "null player" : target.PlayerId.ToString(NumberFormatInfo.InvariantInfo))}");
__instance.isKilling = false;
if (AmongUsClient.Instance.IsGameOver || !AmongUsClient.Instance.AmHost)
{
return;
}
if (!target || __instance.Data.IsDead || !__instance.Data.Role.IsImpostor || __instance.Data.Disconnected)
{
int num = target ? target!.PlayerId : -1;
__instance.logger.Warning($"Bad kill from {__instance.PlayerId} to {num}");
__instance.RpcMurderPlayer(target, false);
return;
}
NetworkedPlayerInfo data = target!.Data;
if (data == null || data.IsDead || target.inVent || target.MyPhysics.Animations.IsPlayingEnterVentAnimation() ||
target.MyPhysics.Animations.IsPlayingAnyLadderAnimation() || target.inMovingPlat)
{
__instance.logger.Warning("Invalid target data for kill");
__instance.RpcMurderPlayer(target, false);
return;
}
if (MeetingHud.Instance)
{
__instance.logger.Warning("Tried to kill while a meeting was starting");
__instance.RpcMurderPlayer(target, false);
return;
}
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
MiraEventManager.InvokeEvent(beforeMurderEvent);
didSucceed = beforeMurderEvent.IsCancelled;
if (beforeMurderEvent.IsCancelled)
{
return;
}
__instance.isKilling = true;
__instance.RpcMurderPlayer(target, true);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
[HarmonyPatch(nameof(PlayerControl.MurderPlayer))]
// ReSharper disable once InconsistentNaming
public static bool MurderPlayerPrefix(PlayerControl __instance)
{
if (LobbyBehaviour.Instance)
{
return false;
}
return true;
}
[HarmonyPostfix]
@@ -88,17 +141,19 @@ internal static class PlayerControlPatches
return;
}
var role = __instance.Data?.Role;
foreach (var button in CustomButtonManager.CustomButtons)
{
if (__instance.Data?.Role == null)
if (role == null)
{
continue;
}
try
{
if (!button.Enabled(__instance.Data?.Role))
if (!button.Enabled(role))
{
button.SetActive(false, role);
continue;
}
@@ -106,7 +161,7 @@ internal static class PlayerControlPatches
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to update custom button {button.GetType().Name}: {e}");
Error($"Failed to update custom button {button.GetType().Name}: {e}");
}
}
}
+2 -2
View File
@@ -33,8 +33,8 @@ internal static class EjectionPatches
__instance.completeString = role.GetCustomEjectionMessage(__instance.initData.networkedPlayer);
}
[HarmonyPatch(typeof(ExileController), nameof(ExileController.WrapUp))]
public static class WrapUpPatch
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
public static class ReEnableGameplayPatch
{
public static void Postfix()
{
+3 -2
View File
@@ -1,11 +1,12 @@
using HarmonyLib;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches the haunt menu to show the actual role name rather then team.
/// Patches the haunt menu to show the actual role name rather than team.
/// </summary>
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))]
// SetFilterText was INLINED
@@ -22,7 +23,7 @@ public static class HauntMenuMinigamePatch
var role = __instance.HauntTarget.Data.Role;
var color = role is ICustomRole custom ? custom.RoleColor : role.TeamColor;
__instance.FilterText.text = role.NiceName;
__instance.FilterText.text = role.GetRoleName();
__instance.FilterText.color = color;
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(HowToPlayScene), nameof(HowToPlayScene.OpenRolesSelectionMenu))]
internal static class HowToPlayPatch
{
// yes i patched the entire method
private static void Prefix(HowToPlayScene __instance)
{
if (RoleManager.Instance.AllRoles.ToArray().All(x => !x.IsCustomRole()))
{
return;
}
__instance.sceneIndex = 0;
__instance.category = HowToPlayScene.HowToPlayCategory.RolesSelection;
__instance.startPage.SetActive(false);
if (__instance.roleButtonsParent.childCount == 0)
{
using (IEnumerator<RoleBehaviour> enumerator = RoleManager.Instance.AllRoles.ToArray().Where(x => !x.IsCustomRole()).GetEnumerator())
{
while (enumerator.MoveNext())
{
RoleBehaviour role = enumerator.Current;
if (!role.IsSimpleRole && role.Role != RoleTypes.CrewmateGhost && role.Role != RoleTypes.ImpostorGhost)
{
HowToPlayRoleButton component = Object.Instantiate(__instance.roleButtonPrefab, __instance.roleButtonsParent).GetComponent<HowToPlayRoleButton>();
Sprite roleIcon = __instance.rolesScenes.ToArray().First(r => r.role == role.Role).roleIcon;
component.SetRoleInfo(role, roleIcon);
component.SetButtonAction((Il2CppSystem.Action)(() =>
{
__instance.OpenRolePage(role.Role);
}));
__instance.controllerSelectables.Add(component.GetComponent<PassiveButton>());
}
}
}
foreach (UiElement uiElement in __instance.controllerSelectables)
{
uiElement.ReceiveMouseOut();
}
ControllerManager.Instance.NewScene(__instance.name, __instance.closeButton, __instance.defaultButtonSelected, __instance.controllerSelectables, false);
}
__instance.DisableAllScenes();
__instance.roleSelectionScene.SetActive(true);
ControllerManager.Instance.SetDefaultSelection(__instance.defaultButtonSelected, null);
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ public static class KillButtonPatches
}
/// <summary>
/// Use Custom Murder if player is custom role.
/// Use Custom Murder instead of Murder Player from vanilla.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(KillButton.DoClick))]
@@ -58,7 +58,7 @@ public static class KillButtonPatches
return false;
}
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget);
PlayerControl.LocalPlayer.RpcCustomMurder(__instance.currentTarget, MeetingCheck.OutsideMeeting);
__instance.SetTarget(null);
return false;
@@ -0,0 +1,183 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using System.Linq;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(LogicRoleSelectionHnS))]
public static class LogicRoleSelectionHnsPatch
{
[HarmonyPrefix]
[HarmonyPatch(nameof(LogicRoleSelectionHnS.AssignRolesForTeam))]
public static bool AssignRolesForTeam(
LogicRoleSelectionHnS __instance,
List<NetworkedPlayerInfo> players,
IGameOptions opts,
RoleTeamTypes team,
int teamMax,
Il2CppSystem.Nullable<RoleTypes> defaultRole)
{
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Team: {team}, Max: {teamMax}, Players: {players.Count}, DefaultRole: {defaultRole}");
int num = 0;
IRoleOptionsCollection roleOptions = opts.RoleOptions;
var source = RoleManager.Instance.AllRoles.ToArray()
.Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) &&
CustomRoleUtils.CanSpawnOnCurrentMode(role));
var assignmentData = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
role,
roleOptions.GetNumPerGame(role.Role),
roleOptions.GetChancePerGame(role.Role))).ToList();
var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100);
var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100)));
List<RoleTypes> list = new List<RoleTypes>();
if (team == RoleTeamTypes.Crewmate)
{
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
roleAssignmentData2.Count = count - 1;
if (count <= 0)
{
break;
}
list.Add(roleAssignmentData.Role.Role);
}
}
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment");
AssignRolesFromList(players, teamMax, list, ref num);
var list2 = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
role,
roleOptions.GetNumPerGame(role.Role),
roleOptions.GetChancePerGame(role.Role))).ToList();
list.Clear();
foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
{
for (int i = 0; i < roleAssignmentData3.Count; i++)
{
if (HashRandom.Next(101) < roleAssignmentData3.Chance)
{
list.Add(roleAssignmentData3.Role.Role);
}
}
}
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Potential Assignment");
AssignRolesFromList(players, teamMax, list, ref num);
var basicRole = RoleTypes.Engineer;
while (list.Count < players.Count && list.Count + num < teamMax)
{
list.Add(basicRole);
}
AssignRolesFromList(players, teamMax, list, ref num);
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Fallback Assignment");
}
else if (team == RoleTeamTypes.Impostor)
{
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Before Guaranteed Assignment");
var newImpostors = new List<NetworkedPlayerInfo>();
// Specified Seeker
if (__instance.hnsManager.LogicOptionsHnS.HasImpostorPlayerID() &&
__instance.hnsManager.LogicOptionsHnS.ValidateImpostorPlayerID(players) &&
!AmongUsClient.Instance.IsGamePublic)
{
NetworkedPlayerInfo networkedPlayerInfo = players.ToArray()
.First(p => p.PlayerId == __instance.hnsManager.LogicOptionsHnS.ImpostorPlayerID());
players.Remove(networkedPlayerInfo);
newImpostors.Add(networkedPlayerInfo);
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}");
}
// Random Seeker
else
{
int num2 = 0;
while (num2 < teamMax && players.Count > 0)
{
PseudoRandomList<NetworkedPlayerInfo> pseudoRandomList = new PseudoRandomList<NetworkedPlayerInfo>(AmongUsClient.Instance.GameId);
players._items.Do(x => pseudoRandomList.Add(x));
for (int i = 0; i < GameData.RoundsPlayedInSession; i++)
{
pseudoRandomList.PickRandom();
}
NetworkedPlayerInfo networkedPlayerInfo = pseudoRandomList.PickRandom();
players.Remove(networkedPlayerInfo);
newImpostors.Add(networkedPlayerInfo);
num2++;
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: Seeker is {networkedPlayerInfo.PlayerName}, ID: {networkedPlayerInfo.PlayerId}");
}
}
Error($"MiraAPI.Patches.Roles.LogicRoleSelectionHnsPatch - AssignRolesForTeam: After Guaranteed Assignment");
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
roleAssignmentData2.Count = count - 1;
if (count <= 0)
{
break;
}
list.Add(roleAssignmentData.Role.Role);
}
}
AssignRolesFromList(newImpostors, teamMax, list, ref num);
var list2 = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
role,
roleOptions.GetNumPerGame(role.Role),
roleOptions.GetChancePerGame(role.Role))).ToList();
list.Clear();
foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
{
for (int i = 0; i < roleAssignmentData3.Count; i++)
{
if (HashRandom.Next(101) < roleAssignmentData3.Chance)
{
list.Add(roleAssignmentData3.Role.Role);
}
}
}
AssignRolesFromList(newImpostors, teamMax, list, ref num);
var basicRole = RoleTypes.Impostor;
while (list.Count < newImpostors.Count && list.Count + num < teamMax)
{
list.Add(basicRole);
}
AssignRolesFromList(newImpostors, teamMax, list, ref num);
}
return false;
}
public static void AssignRolesFromList(List<NetworkedPlayerInfo> players, int teamMax, List<RoleTypes> roleList, ref int rolesAssigned)
{
while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax)
{
int index = HashRandom.FastNext(roleList.Count);
RoleTypes roleType = roleList[index];
roleList.RemoveAt(index);
int index2 = HashRandom.FastNext(players.Count);
players[index2].Object.RpcSetRole(roleType, false);
players.RemoveAt(index2);
rolesAssigned++;
}
}
}
@@ -0,0 +1,93 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using System.Linq;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(LogicRoleSelectionNormal))]
public static class LogicRoleSelectionNormalPatch
{
[HarmonyPrefix]
[HarmonyPatch(nameof(LogicRoleSelectionNormal.AssignRolesForTeam))]
public static bool AssignRolesForTeam(
LogicRoleSelectionNormal __instance,
List<NetworkedPlayerInfo> players,
IGameOptions opts,
RoleTeamTypes team,
int teamMax,
Il2CppSystem.Nullable<RoleTypes> defaultRole)
{
int num = 0;
var source = RoleManager.Instance.AllRoles.ToArray()
.Where(role => role.TeamType == team && !RoleManager.IsGhostRole(role.Role) &&
CustomRoleUtils.CanSpawnOnCurrentMode(role));
List<RoleTypes> list = new List<RoleTypes>();
IRoleOptionsCollection roleOptions = opts.RoleOptions;
var assignmentData = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
role,
roleOptions.GetNumPerGame(role.Role),
roleOptions.GetChancePerGame(role.Role))).ToList();
var source2 = CustomRoleUtils.GetPossibleRoles(assignmentData, x => x.Chance == 100);
var guaranteedRoles = source.Where(x => source2.Contains(((ushort)x.Role, 100)));
foreach (RoleManager.RoleAssignmentData roleAssignmentData in guaranteedRoles.Select((x) =>
new RoleManager.RoleAssignmentData(x, roleOptions.GetNumPerGame(x.Role), 100)))
{
while (true)
{
RoleManager.RoleAssignmentData roleAssignmentData2 = roleAssignmentData;
int count = roleAssignmentData2.Count;
roleAssignmentData2.Count = count - 1;
if (count <= 0)
{
break;
}
list.Add(roleAssignmentData.Role.Role);
}
}
__instance.AssignRolesFromList(players, teamMax, list, ref num);
var list2 = source.Where(x => !x.IsDead).Select(role =>
new RoleManager.RoleAssignmentData(
role,
roleOptions.GetNumPerGame(role.Role),
roleOptions.GetChancePerGame(role.Role))).ToList();
list.Clear();
foreach (RoleManager.RoleAssignmentData roleAssignmentData3 in list2)
{
for (int i = 0; i < roleAssignmentData3.Count; i++)
{
if (HashRandom.Next(101) < roleAssignmentData3.Chance)
{
list.Add(roleAssignmentData3.Role.Role);
}
}
}
__instance.AssignRolesFromList(players, teamMax, list, ref num);
var defaultRole2 = team is RoleTeamTypes.Crewmate ? RoleTypes.Crewmate : RoleTypes.Impostor;
try
{
defaultRole2 = defaultRole.Value;
}
catch
{
// Ignored
}
while (list.Count < players.Count && list.Count + num < teamMax)
{
list.Add(defaultRole2);
}
__instance.AssignRolesFromList(players, teamMax, list, ref num);
return false;
}
}
+2 -2
View File
@@ -20,7 +20,7 @@ public static class NameTagPatch
return false;
}
if (PlayerControl.LocalPlayer.Data.Role.IsImpostor && otherPlayerRole.IsImpostor)
if (PlayerControl.LocalPlayer.Data?.Role?.IsImpostor == true && otherPlayerRole.IsImpostor)
{
return true;
}
@@ -30,7 +30,7 @@ public static class NameTagPatch
return true;
}
if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data.Role || !otherPlayerRole)
if (!PlayerControl.LocalPlayer || !PlayerControl.LocalPlayer.Data || !PlayerControl.LocalPlayer.Data?.Role || !otherPlayerRole)
{
__result = Color.white;
}
+3 -2
View File
@@ -1,6 +1,7 @@
using System.Linq;
using AmongUs.GameOptions;
using HarmonyLib;
using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
@@ -25,7 +26,7 @@ public static class RoleManagerPatches
var data = targetPlayer.Data;
if (data == null)
{
Debug.LogError("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
Error("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
return false;
}
if (data.Role)
@@ -33,7 +34,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;
@@ -9,17 +9,17 @@ namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches to return the correct role counts.
/// </summary>
[HarmonyPatch(typeof(RoleOptionsCollectionV09))]
[HarmonyPatch(typeof(RoleOptionsCollectionV10))]
public static class RoleOptionsCollectionPatch
{
/// <summary>
/// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code.
/// This patch fixes GetNumPerGame being inlined (2025.9.9) in the original code.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.AnyRolesEnabled))]
public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV09 __instance, ref bool __result)
[HarmonyPatch(nameof(RoleOptionsCollectionV10.AnyRolesEnabled))]
public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV10 __instance, ref bool __result)
{
foreach (KeyValuePair<RoleTypes, RoleDataV09> keyValuePair in __instance.roles)
foreach (KeyValuePair<RoleTypes, RoleDataV10> keyValuePair in __instance.roles)
{
if (__instance.GetNumPerGame(keyValuePair.Key) > 0)
{
@@ -34,7 +34,7 @@ public static class RoleOptionsCollectionPatch
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.GetChancePerGame))]
[HarmonyPatch(nameof(RoleOptionsCollectionV10.GetChancePerGame))]
public static bool GetChancePrefix(RoleTypes role, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null)
@@ -51,7 +51,7 @@ public static class RoleOptionsCollectionPatch
var chance = customRole.GetChance();
if (chance == null)
{
Logger<MiraApiPlugin>.Error($"Chance is null, defaulting to zero.");
Error($"Chance is null, defaulting to zero.");
chance = 0;
}
@@ -64,7 +64,7 @@ public static class RoleOptionsCollectionPatch
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.GetNumPerGame))]
[HarmonyPatch(nameof(RoleOptionsCollectionV10.GetNumPerGame))]
public static bool GetNumPrefix(RoleTypes role, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null)
@@ -81,7 +81,7 @@ public static class RoleOptionsCollectionPatch
var count = customRole.GetCount();
if (count == null)
{
Logger<MiraApiPlugin>.Error($"Count is null, defaulting to zero.");
Error($"Count is null, defaulting to zero.");
count = 0;
}
+19 -8
View File
@@ -1,4 +1,6 @@
using HarmonyLib;
using System.Linq;
using HarmonyLib;
using Reactor.Utilities;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
@@ -8,20 +10,29 @@ namespace MiraAPI.Patches;
[HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))]
public static class ServerDropdownPatch
{
public static bool RegionEquals(this IRegionInfo region, IRegionInfo other)
{
return region.Name == other.Name &&
region.TranslateName == other.TranslateName &&
region.PingServer == other.PingServer &&
region.TargetServer == other.TargetServer &&
region.Servers.All(s=>other.Servers.Any(x=>x.Equals(s)));
}
public static bool Prefix(ServerDropdown __instance)
{
var num = 0;
__instance.background.size = new Vector2(8.4f, 4.8f);
foreach (var regionInfo in DestroyableSingleton<ServerManager>.Instance.AvailableRegions)
foreach (var regionInfo in ServerManager.Instance.AvailableRegions)
{
var findingGame = SceneManager.GetActiveScene().name is "FindAGame";
if (DestroyableSingleton<ServerManager>.Instance.CurrentRegion.Equals(regionInfo))
if (ServerManager.Instance.CurrentRegion.RegionEquals(regionInfo))
{
__instance.defaultButtonSelected = __instance.firstOption;
__instance.firstOption.ChangeButtonText(
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
TranslationController.Instance.GetStringWithDefault(
regionInfo.TranslateName,
regionInfo.Name));
}
@@ -34,11 +45,11 @@ public static class ServerDropdownPatch
{
x += 2;
}
var y = -0.55f * (num / 2);
var y = -0.55f * (num / 2f);
serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f);
serverListButton.transform.localScale = Vector3.one;
serverListButton.Text.text =
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
TranslationController.Instance.GetStringWithDefault(
regionInfo.TranslateName,
regionInfo.Name);
serverListButton.Text.ForceMeshUpdate();
@@ -47,9 +58,9 @@ public static class ServerDropdownPatch
__instance.controllerSelectable.Add(serverListButton.Button);
__instance.background.transform.localPosition = new Vector3(
findingGame ? 2f : 0f,
__instance.initialYPos + (-0.3f * (num / 2)),
__instance.initialYPos + -0.3f * (num / 2f),
0f);
__instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2)));
__instance.background.size = new Vector2(__instance.background.size.x, 1.2f + 0.6f * (num / 2f));
num++;
}
}
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using HarmonyLib;
using Hazel;
using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
@@ -229,4 +230,76 @@ public static class RoleBehaviourStubs
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.UseSecondaryAbility.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.UseSecondaryAbility))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void UseSecondaryAbility(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.OnRoleSet.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnRoleSet))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void OnRoleSet(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.InitializeMeetingAbilityButton.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.InitializeMeetingAbilityButton))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void InitializeMeetingAbilityButton(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.FindClosestBody.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.FindClosestBody))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static DeadBody FindClosestBody(RoleBehaviour instance)
{
return null!;
}
/// <summary>
/// Stub method for RoleBehaviour.HandleRoleRpc.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.HandleRoleRpc))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void HandleRoleRpc(RoleBehaviour instance, byte callId, MessageReader reader)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.KillAnimSpecialSetup.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.KillAnimSpecialSetup))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void KillAnimSpecialSetup(RoleBehaviour instance, DeadBody deadBody, PlayerControl killer, PlayerControl victim)
{
// nothing needed
}
}
+25
View File
@@ -12,6 +12,31 @@ public interface IMiraPlugin
/// </summary>
string OptionsTitleText { get; }
/// <summary>
/// Gets the name for the first custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuNameOne => "Custom Category 1";
/// <summary>
/// Gets the name for the second custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuNameTwo => "Custom Category 2";
/// <summary>
/// Gets the description for the second custom category in the game options menu, if any.
/// </summary>
public virtual string ModifierMenuDescription => "Configure modifiers and their settings here!";
/// <summary>
/// Gets the description for the first custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuOneDescription => "Apply game settings for this mod!";
/// <summary>
/// Gets the description for the second custom category in the game options menu, if any.
/// </summary>
public virtual string CustomOptionMenuTwoDescription => "Apply game settings for this mod!";
/// <summary>
/// Gets the BepInEx configuration file for the plugin.
/// </summary>
+1 -1
View File
@@ -51,7 +51,7 @@ public class MiraPluginInfo
public IReadOnlyCollection<CustomActionButton> Buttons { get; private set; } = null!;
/// <summary>
/// Gets a read only dictionary of this plugin's custom game modes.
/// Gets a read only dictionary of this plugin's custom presets.
/// </summary>
public IReadOnlyCollection<OptionPreset> Presets { get; internal set; } = null!;
+25 -25
View File
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.Colors;
using MiraAPI.Events;
using MiraAPI.GameEnd;
@@ -21,7 +22,6 @@ using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
using Reactor.Utilities;
using Rewired;
namespace MiraAPI.PluginLoading;
@@ -60,7 +60,7 @@ public sealed class MiraPluginManager
continue;
}
foreach (var method in type.GetMethods())
foreach (var method in AccessTools.GetDeclaredMethods(type))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
@@ -70,14 +70,14 @@ public sealed class MiraPluginManager
if (!method.IsStatic)
{
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
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}");
Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
@@ -131,7 +131,7 @@ public sealed class MiraPluginManager
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () =>
@@ -167,7 +167,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register game over {type.Name}: {e}");
Error($"Failed to register game over {type.Name}: {e}");
return false;
}
}
@@ -190,7 +190,7 @@ public sealed class MiraPluginManager
{
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
@@ -211,14 +211,14 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(IModdedOption))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for options.");
Error($"{field.Name} is a field, not a property. Use properties for options.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
}
@@ -235,7 +235,7 @@ public sealed class MiraPluginManager
if (!ModList.GetById(pluginInfo.PluginId).IsRequiredOnAllClients)
{
Logger<MiraApiPlugin>.Error("Custom roles are only supported on all clients.");
Error("Custom roles are only supported on all clients.");
return false;
}
@@ -244,7 +244,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register role for {type.Name}: {e}");
Error($"Failed to register role for {type.Name}: {e}");
}
return false;
}
@@ -260,7 +260,7 @@ public sealed class MiraPluginManager
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.Error($"Color class {type.Name} must be static.");
Error($"Color class {type.Name} must be static.");
return;
}
@@ -273,7 +273,7 @@ public sealed class MiraPluginManager
if (property.GetValue(null) is not CustomColor color)
{
Logger<MiraApiPlugin>.Error($"Color property {property.Name} in {type.Name} is not a CustomColor.");
Error($"Color property {property.Name} in {type.Name} is not a CustomColor.");
continue;
}
@@ -282,12 +282,12 @@ public sealed class MiraPluginManager
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
{
Logger<MiraApiPlugin>.Error($"{field.Name} is a field, not a property. Use properties for colors.");
Error($"{field.Name} is a field, not a property. Use properties for colors.");
}
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register color class {type.Name}: {e}");
Error($"Failed to register color class {type.Name}: {e}");
}
}
@@ -299,7 +299,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register modifier {type.Name}: {e}");
Error($"Failed to register modifier {type.Name}: {e}");
return false;
}
}
@@ -312,7 +312,7 @@ public sealed class MiraPluginManager
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register button {type.Name}: {e}");
Error($"Failed to register button {type.Name}: {e}");
}
return false;
@@ -355,13 +355,13 @@ public sealed class MiraPluginManager
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
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.");
Error($"Option property {property.Name} in {type.Name} has to be a config entry.");
continue;
}
@@ -376,14 +376,14 @@ public sealed class MiraPluginManager
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.");
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()}");
Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
@@ -400,7 +400,7 @@ public sealed class MiraPluginManager
if (!type.IsStatic())
{
Logger<MiraApiPlugin>.Error($"Keybinds class {type.Name} must be static.");
Error($"Keybinds class {type.Name} must be static.");
return;
}
@@ -413,7 +413,7 @@ public sealed class MiraPluginManager
if (property.GetValue(null) is not MiraKeybind keybind)
{
Logger<MiraApiPlugin>.Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind.");
Error($"Keybind property {property.Name} in {type.Name} is not a MiraKeybind.");
continue;
}
@@ -430,12 +430,12 @@ public sealed class MiraPluginManager
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.");
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}");
Error($"Failed to register keybind class {type.Name}: {e}");
}
}
}
+22
View File
@@ -71,4 +71,26 @@ public class OptionPreset
CustomRoleManager.SyncAllRoleSettings();
}
/// <summary>
/// Resets the specified option with the preset provided.
/// </summary>
public void ResetOption(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
selectedOpt.LoadFromPreset(PresetConfig);
/*ModdedOptionsManager.SyncAllOptions();
CustomRoleManager.SyncAllRoleSettings();*/
}
/// <summary>
/// Gets whether the specified option is included in the preset.
/// </summary>
/// <returns>>The value of whether the option is in the preset.</returns>
public bool IsOptionInPreset(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
}
}
+66 -2
View File
@@ -1,4 +1,5 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
@@ -18,6 +19,14 @@ public static class PresetManager
/// </summary>
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
internal static Dictionary<MiraPluginInfo, OptionPreset> DefaultPresets { get; } = [];
internal static List<OptionPreset> InternalMasterPresets { get; } = [];
/// <summary>
/// Gets a read only dictionary of this plugin's custom presets.
/// </summary>
public static IReadOnlyCollection<OptionPreset> MasterPresets { get; internal set; } = null!;
internal static void CreateDefaultPreset(MiraPluginInfo plugin)
{
var presetPath = Path.Combine(PresetDirectory, plugin.PluginId);
@@ -39,6 +48,7 @@ public static class PresetManager
}
presetConfig.Save();
DefaultPresets.Add(plugin, new OptionPreset("Default", plugin, presetConfig));
}
/// <summary>
@@ -70,7 +80,7 @@ public static class PresetManager
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Logger<MiraApiPlugin>.Info($"Loading preset file {fileName}");
Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
@@ -93,4 +103,58 @@ public static class PresetManager
}
plugin.Presets = [..plugin.InternalPresets];
}
/// <summary>
/// Loads the master presets by reading the configuration files from the preset directory.
/// </summary>
public static void LoadMasterPreset()
{
/*foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton))
{
if (btn != null)
{
Object.DestroyImmediate(btn);
}
}
plugin.InternalPresets.Clear();*/
if (!Directory.Exists(PresetDirectory))
{
Directory.CreateDirectory(PresetDirectory);
}
var pluginPresetPath = Path.Combine(PresetDirectory, "MasterPresets");
if (!Directory.Exists(pluginPresetPath))
{
Directory.CreateDirectory(pluginPresetPath);
}
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
SaveOnConfigSet = false,
};
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset))
{
option.Bind(presetConfig);
}
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
{
role.BindConfig(presetConfig);
}
}
presetConfig.Save();
// InternalMasterPresets.Add(new OptionPreset(presetName, plugin, presetConfig));
}
MasterPresets = [..InternalMasterPresets];
}
}
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: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more