Compare commits

...
159 Commits
Author SHA1 Message Date
XtraCube d7d9028433 Merge branch 'dev' into commission
# Conflicts:
#	MiraAPI/PluginLoading/MiraPluginManager.cs
2025-06-29 20:15:18 -04:00
XtraCube 05f132c506 expose modifiers 2025-06-29 20:03:20 -04:00
XtraCube ab54c44434 expose custom roles in CustomRoleManager.cs 2025-06-29 19:57:58 -04:00
XtraCube 24b52ededc Merge branch 'dev' into commission 2025-06-29 11:23:32 -04:00
XtraCube bf652b68c3 fix warnings 2025-06-28 23:04:30 -04:00
XtraCube 00377c7926 Merge remote-tracking branch 'origin/dev' into dev 2025-06-28 22:57:09 -04:00
XtraCube 55555604f3 error proof buttons 2025-06-28 22:57:04 -04:00
ang-xd de77a9fa92 Add page counter in options screen 2025-06-28 20:55:22 -04:00
ang-xd eba2a037d3 Bug fixes and chat pasting 2025-06-28 19:21:47 -04:00
ang-xd bc7f772de9 Add ModdedPlayerOption and fix role options scroll 2025-06-28 18:44:31 -04:00
ang-xd f43191c4f2 Fix role option visibility & shift 5 increments on chance 2025-06-27 19:16:56 -04:00
XtraCube 56d4e399d9 Merge branch 'master' into dev 2025-06-27 15:50:58 -04:00
XtraCube 0d41ebbc13 adjust server dropdown to fit 2 columns 2025-06-27 15:47:38 -04:00
XtraCube 9388ff487a fix scrolling bug on role menu 2025-06-27 15:21:58 -04:00
XtraCube 720491e5cf make role options wrap around 2025-06-27 15:16:14 -04:00
XtraCube 9884df356e hide hidden role settings in advanced menu 2025-06-27 14:28:09 -04:00
XtraCube 5dbcc1435d Merge branch 'dev' into commission 2025-06-24 23:14:09 -04:00
XtraCube 7ec4addacc cleanup 2025-06-24 23:13:00 -04:00
XtraCube bc9bc0ce24 brace style 2025-06-24 21:38:36 -04:00
XtraCube aff1c311fa update readme 2025-06-24 21:24:40 -04:00
XtraCube 0d2a9d9700 fix warnings 2025-06-24 15:06:45 -04:00
XtraCube 79359ec67d Merge branch 'dev' into commission 2025-06-24 14:57:57 -04:00
XtraCube 669c541279 fix advanced role settings scroll 2025-06-24 14:57:48 -04:00
XtraCube 0479ff96b1 Merge branch 'dev' into commission 2025-06-24 12:32:43 -04:00
XtraCube 4e314a0a7e Revert "fix role settings menu"
This reverts commit d9cc57d355.
2025-06-24 12:32:38 -04:00
XtraCube 2e5f9639b3 fix role settings menu 2025-06-24 12:32:13 -04:00
XtraCube e6d1f440c8 adjust it 2025-06-24 12:27:02 -04:00
XtraCube 2ac8bdc240 fix lobby view settings scroll issues 2025-06-24 12:26:22 -04:00
XtraCube d9cc57d355 fix role settings menu 2025-06-24 11:31:54 -04:00
XtraCube 8fc5137e87 Merge branch 'dev' into commission
# Conflicts:
#	MiraAPI/Patches/Options/GameSettingMenuPatches.cs
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
#	MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
#	MiraAPI/Utilities/Assets/MiraAssets.cs
#	MiraAPI/Utilities/Extensions.cs
2025-06-23 19:18:01 -04:00
XtraCube d5d99001ea privacy change 2025-06-23 19:14:22 -04:00
XtraCube 5628b79814 Merge remote-tracking branch 'origin/presets' into dev
# Conflicts:
#	MiraAPI/GameOptions/IModdedOption.cs
#	MiraAPI/GameOptions/ModdedOptionsManager.cs
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
#	MiraAPI/Patches/Options/GameSettingMenuPatches.cs
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
#	MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
#	MiraAPI/Utilities/Assets/MiraAssets.cs
#	MiraAPI/Utilities/Assets/SpriteTools.cs
#	MiraAPI/Utilities/Extensions.cs
2025-06-23 11:44:30 -04:00
XtraCube 72a44f65f1 fix role menu scrolling bug 2025-06-23 11:31:49 -04:00
XtraCube db6a336a63 Add modifier tab scroll positions and fix scroll positions not saving in some cases. 2025-06-23 11:07:28 -04:00
XtraCube 90b23ec2ca fix role scroll position issue 2025-06-23 10:31:47 -04:00
XtraCube 16ff2f8492 Merge branch 'dev' into commission
# Conflicts:
#	MiraAPI/Utilities/Extensions.cs
2025-06-22 09:01:00 -04:00
XtraCube 20e927208c make options wrap around in values 2025-06-21 20:17:57 -04:00
XtraCube 13ef3bdd49 add format string for cooldown timers 2025-06-21 19:32:28 -04:00
XtraCube b3ff6755bb suppress naming warnings 2025-06-21 18:52:18 -04:00
XtraCube 67604600cd remove obsolete code 2025-06-21 18:52:09 -04:00
XtraCube 6e35de6185 add BeforeGameEndEvent.cs 2025-06-21 18:43:56 -04:00
XtraCube a26ec51c7d send murder RPC immediately 2025-06-21 18:24:32 -04:00
XtraCube 3f6ae4cc41 send gameover immediately 2025-06-21 18:21:37 -04:00
XtraCube 15a2cdcd5b Merge pull request #36 from All-Of-Us-Mods/2025.5.20
Update to 2025.5.20
2025-06-21 17:27:23 -04:00
XtraCube f943277e38 Merge branch 'presets' into commission 2025-06-20 08:43:21 -04:00
XtraCube cf0bd6ec38 fixed presets 2025-06-20 08:43:11 -04:00
XtraCube dbb17a4cd7 Merge branch 'presets' into commission 2025-06-20 08:38:38 -04:00
XtraCube 130236e899 oops fix bad code 2025-06-20 08:38:31 -04:00
XtraCube e83c5e504d Merge branch 'presets' into commission 2025-06-20 08:32:35 -04:00
XtraCube 3b7b247ac0 refresh presets when opening preset menu 2025-06-20 08:32:24 -04:00
XtraCube ed0b4ebbc4 Merge branch 'presets' into commission 2025-06-19 23:24:12 -04:00
XtraCube 52de9a22de fix preset loading networking 2025-06-19 23:24:02 -04:00
XtraCube 9077a13b5d fix RPC warnings 2025-06-19 23:21:14 -04:00
XtraCube 372a9e3aa9 suppress RPC warnings 2025-06-16 21:31:58 -04:00
XtraCube fb3eabfb29 Merge branch 'dev' into commission 2025-06-11 21:26:15 -04:00
XtraCube 10a9dcfa34 suppress RPC warnings 2025-06-11 21:26:08 -04:00
XtraCube df87039884 Merge branch '2025.5.20' into commission
# Conflicts:
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
2025-05-30 17:18:23 -04:00
XtraCube d46fdb0185 comments about rpcmurder inlining 2025-05-30 17:15:52 -04:00
XtraCube a361afbcbf add comment about meetinghud inline issue 2025-05-30 17:12:03 -04:00
XtraCube 24cdac0243 Fix role options collection inlining 2025-05-30 17:07:11 -04:00
XtraCube 5fe75843ad fix haunt menu inlining 2025-05-30 17:01:43 -04:00
XtraCube a0436dcbba Fix ToggleOption inlining 2025-05-30 16:58:32 -04:00
XtraCube 35be381a77 Fix LobbyViewPane inlining 2025-05-30 16:53:15 -04:00
XtraCube 1298beb517 fix kill anim inlining 2025-05-30 16:06:21 -04:00
XtraCube d41c19a176 Adjust kill anim patch for inlining 2025-05-30 15:45:59 -04:00
XtraCube 476f4400ad Fix ReportBodyEventPatch.cs 2025-05-29 22:04:34 -04:00
XtraCube a51369a4a6 Fix ReportBodyEventPatch.cs 2025-05-29 22:04:05 -04:00
XtraCube 23fe7e6feb Merge branch '2025.5.20' into commission 2025-05-27 16:09:52 -04:00
XtraCube 2b46d90570 Merge branch 'dev' into 2025.5.20 2025-05-27 16:09:29 -04:00
XtraCube ae4aaf7af9 adjust mirabundle name to work on epic 2025-05-27 16:09:14 -04:00
XtraCube 757b9436ca adjust role message 2025-05-27 16:09:05 -04:00
XtraCube e2f387a3f6 Merge branch '2025.5.20' into commission 2025-05-23 10:32:45 -04:00
XtraCube 7ea10afffb dont need reactor dev nuget anymore 2025-05-21 18:18:10 -04:00
XtraCube 96f84b741c adjust modifier assignment for commission 2025-05-21 17:16:17 -04:00
XtraCube 0ab80c42f9 Update discord badge 2025-05-20 22:25:21 -04:00
XtraCube bc144568f4 fix harmony error due to renamed parameter 2025-05-20 18:51:55 -04:00
XtraCube 07f402e659 update reactor 2025-05-20 18:42:59 -04:00
XtraCube b8d24c28e1 Merge branch 'dev' into commission
# Conflicts:
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
2025-05-18 15:56:09 -04:00
XtraCube 1a6aed539a fix lobbyview modifier options showing in game options 2025-05-18 14:49:40 -04:00
XtraCube 023cdaa470 fix role setting menu bug 2025-05-18 14:46:24 -04:00
XtraCube 84e3f4085e fix role settings bug 2025-05-18 14:44:13 -04:00
XtraCube efb19cf46f fix broken settings menu 2025-05-18 14:35:53 -04:00
XtraCube 98dd5ca056 fix more warnings 2025-05-18 12:40:40 -04:00
XtraCube 57a31b7761 fix changes in branch 2025-05-18 12:32:10 -04:00
XtraCube 0f5add5251 fix botched merge 2025-05-18 12:31:31 -04:00
XtraCube 7450c73243 Merge branch 'extensions' into commission
# Conflicts:
#	MiraAPI/GameOptions/IModdedOption.cs
#	MiraAPI/GameOptions/ModdedOptionsManager.cs
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
#	MiraAPI/Patches/Options/GameSettingMenuPatches.cs
#	MiraAPI/Patches/Options/RoleSettingMenuPatches.cs
#	MiraAPI/PluginLoading/MiraPluginManager.cs
#	MiraAPI/Utilities/Assets/MiraAssets.cs
#	MiraAPI/Utilities/Assets/SpriteTools.cs
2025-05-18 12:27:49 -04:00
XtraCube bc188191de Initial commit 2025-05-18 11:34:41 -04:00
XtraCube 1f3e21ce19 cleanup more warnings 2025-05-18 10:01:48 -04:00
XtraCube 15e441dd71 add documentation in mira assets 2025-05-18 09:37:27 -04:00
XtraCube 8c67bbbd44 cleanup warnings 2025-05-18 09:32:33 -04:00
XtraCube aa584a8f44 update role notifications to match custom role colors 2025-05-18 09:25:26 -04:00
XtraCube a8a584b61b Fix bug with CustomRoleSingleton.cs 2025-05-18 09:09:14 -04:00
XtraCube acc4b1c8dd Save scroll positions on options and roles pages 2025-05-17 23:27:25 -04:00
XtraCube f9600bc906 fix chameleon role again 2025-05-17 23:27:14 -04:00
XtraCube f3da06870f fix chameleon role errors 2025-05-17 23:20:26 -04:00
XtraCube 9180dec9e1 update example mod to fit new role update 2025-05-17 23:16:01 -04:00
XtraCube ef6dedbd55 add errors for incorrect usage of registrations 2025-05-17 22:40:02 -04:00
XtraCube 5d3c3ed319 Update example events and catch exceptions in events 2025-05-17 22:32:29 -04:00
XtraCube 5b22d667fd remove redundant ICustomRole methods 2025-05-17 22:31:34 -04:00
XtraCube fb894c9f2c add wrappers around modifiers to catch exceptions 2025-05-17 17:42:19 -04:00
XtraCube d0449f57b5 adjust naming in plugin manager 2025-05-17 17:40:05 -04:00
XtraCube 5ffd591258 add dead body to death and murder events 2025-05-17 17:34:04 -04:00
XtraCube cc0ac1b988 Merge remote-tracking branch 'origin/dev' into dev 2025-05-17 17:28:08 -04:00
XtraCube 00b96f6e61 Merge pull request #34 from All-Of-Us-Mods/2025.3.25
2025.3.25 (16.0.0) support
2025-05-17 17:28:06 -04:00
XtraCube 6cb7f9f145 add unregistration of events 2025-05-17 17:26:29 -04:00
XtraCube b94472ee07 save options scroll states 2025-05-16 19:42:59 -04:00
XtraCube f8056c29da Merge branch 'dev' into presets 2025-05-16 19:27:26 -04:00
XtraCube e7f9a3bb60 hide roles in lobby view pane 2025-05-16 19:27:12 -04:00
XtraCube 8d4f2baf1e Merge remote-tracking branch 'origin/presets' into presets 2025-05-16 19:24:52 -04:00
XtraCube 11ba3dae64 save scroll position in roles menu 2025-05-16 19:24:47 -04:00
AlchlcDvl 86145f94f4 Merge branch 'dev' into presets 2025-05-04 22:29:23 -04:00
AlchlcDvl a9ded0b313 Modifier display improvement 2025-05-04 22:15:08 -04:00
XtraCube e0da386567 fix lag and broken roles saving/loading 2025-05-04 18:10:34 -04:00
XtraCube 6f1da2de9f add roles to presets 2025-05-02 19:25:03 -04:00
XtraCube b85358e304 Merge branch '2025.3.25' into presets 2025-05-02 19:19:33 -04:00
XtraCube 2c901bb9b9 remove unused thingy 2025-05-02 14:56:28 -04:00
XtraCube 68f40a96b7 fix preset system 2025-05-02 14:54:42 -04:00
XtraCube a96ffc42f2 fix saving default preset only 2025-04-29 17:23:05 -04:00
XtraCube 0907b14893 Merge branch 'feat-addressables-2' into presets 2025-04-29 17:22:57 -04:00
ang-xd 7a7204b0e0 Fix minigame crashes 2025-04-22 18:14:45 -04:00
ang-xd e57e88c179 Update AbstractOptionGroup.cs 2025-04-19 13:35:03 -04:00
ang-xd 878eab25b8 Merge branch 'dev' into 2025.3.25 2025-04-16 20:11:51 -04:00
angxl wtf e4f15f2ed9 Merge pull request #33 from All-Of-Us-Mods/feat-addressables-2
Addressables Loading
2025-04-16 19:51:27 -04:00
ang-xd bdc9fa28ac oops 2025-04-15 18:20:24 -04:00
ang-xd 64604aff98 Fix bug if you change hideonui 2025-04-15 18:05:20 -04:00
ang-xd 32a44d646d Futher progress on presets UI 2025-04-12 23:29:09 -04:00
ang-xd 8f590b94a8 Merge branch 'feat-addressables-2' into presets 2025-04-12 20:54:35 -04:00
ang-xd 3ba71d8f19 Add pagination 2025-04-12 16:22:55 -04:00
ang-xd e5b6400eb8 Fix CastVote event issue 2025-04-06 14:50:29 -04:00
XtraCube 0d1d1891a8 Merge branch 'dev' into 2025.3.25 2025-04-05 16:30:17 -04:00
XtraCube a3fc847d07 Merge branch 'dev' into 2025.3.25 2025-04-05 14:32:26 -04:00
XtraCube 31ffe6274c fix silly mistake with settings 2025-04-05 14:19:52 -04:00
XtraCube f0e37daf0e Merge branch 'dev' into 2025.3.25 2025-04-01 12:25:59 -04:00
XtraCube 2e769d79f2 try fix 2025-03-30 14:56:18 -04:00
XtraCube 41305ad0e2 Merge branch 'dev' into presets 2025-03-30 12:09:32 -04:00
XtraCube 2418e17af6 this doesnt work 2025-03-29 21:29:46 -04:00
XtraCube 6fb0afe14e add folder button 2025-03-29 21:29:40 -04:00
XtraCube ab0063dd99 update preset system 2025-03-29 14:56:07 -04:00
XtraCube a35bf60870 add rounded box sprite 2025-03-29 14:26:15 -04:00
XtraCube 692b9d7180 improve spritetools 2025-03-29 14:26:06 -04:00
XtraCube b99efaa420 create loadable asset wrapper 2025-03-29 14:25:57 -04:00
XtraCube 2ecfa267d6 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/Patches/Options/LobbyViewPanePatches.cs
2025-03-29 13:55:18 -04:00
XtraCube 9afe821b0f fix stupid innerscuff code 2025-03-27 18:09:27 -04:00
XtraCube a8d076656d more WIP presets 2025-03-26 18:45:48 -04:00
XtraCube be46ee7329 fix custom murder stats 2025-03-25 17:11:51 -04:00
XtraCube 75a15dc090 fix minigame patch 2025-03-25 17:09:16 -04:00
XtraCube cffb104392 update libs 2025-03-25 17:06:02 -04:00
XtraCube 429b64fc03 preset v2 2025-03-23 17:28:22 -04:00
XtraCube bf5dadc6a0 that def doesnt work 2025-03-23 15:34:20 -04:00
XtraCube b2137230da Merge branch 'dev' into presets 2025-03-23 15:19:51 -04:00
XtraCube 9e8c05f21d text input WIP 2025-03-23 15:01:31 -04:00
XtraCube 2f56c95b39 Merge branch 'dev' into presets
# Conflicts:
#	MiraAPI/GameOptions/OptionTypes/ModdedOption.cs
2025-03-23 13:45:49 -04:00
XtraCube 38f72c42a3 presets v1 2025-03-23 13:44:40 -04:00
XtraCube 5b608f5b3a preset WIP 2025-03-21 15:21:18 -04:00
XtraCube 2248726450 IncludeInPreset property 2025-03-17 09:12:26 -04:00
XtraCube c60537cbd1 add comments to imoddedoption 2025-03-17 09:02:24 -04:00
XtraCube f5b4bdd1d4 Update discord links 2025-01-26 15:25:27 -05:00
XtraCube cd6c4f5390 Update README.md 2025-01-26 14:01:44 -05:00
XtraCube 5cc29d903a Update README.md 2025-01-26 14:01:30 -05:00
112 changed files with 3093 additions and 723 deletions

No files matched your search

+1 -2
View File
@@ -1,2 +1 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
[*.{cs,vb}]
+1
View File
@@ -3,6 +3,7 @@
.vs/
obj/
bin/
.qodo/
*.user
*.lock.json
+3 -3
View File
@@ -1,11 +1,11 @@
<Project>
<ItemGroup>
<PackageReference Include="Reactor" Version="2.3.1" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.697" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2024.10.29" PrivateAssets="all" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.4.15" PrivateAssets="all" />
<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.0.1" PrivateAssets="all" ExcludeAssets="runtime" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
<Target Name="Copy" AfterTargets="Build" Condition="'$(AmongUs)' != ''">
+1 -1
View File
@@ -34,7 +34,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\stylecop.json" Link="stylecop.json" Visible="false" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0.97167">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@@ -1,17 +1,20 @@
using MiraAPI.Example.GameOver;
using MiraAPI.Example.Roles;
using MiraAPI.Example.Extension.GameOver;
using MiraAPI.Example.Extension.Roles;
using MiraAPI.GameEnd;
using MiraAPI.Hud;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Buttons;
namespace MiraAPI.Example.Extension.Buttons;
public class NeutralKillerButton : CustomActionButton
{
public override string Name => "Win Game";
public override float Cooldown => 0f;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
protected override void OnClick()
{
CustomGameOver.Trigger<NeutralKillerGameOver>([PlayerControl.LocalPlayer.Data]);
@@ -0,0 +1,31 @@
using System;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using MiraAPI.PluginLoading;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
namespace MiraAPI.Example.Extension;
[BepInAutoPlugin("mira.example.extension", "MiraExampleExtensionMod")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[BepInDependency(MiraApiPlugin.Id)]
[BepInDependency(ExamplePlugin.Id)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExampleExtensionPlugin : BasePlugin, IMiraExtension
{
public Harmony Harmony { get; } = new(Id);
public override void Load()
{
Harmony.PatchAll();
}
public Type GetBasePluginType()
{
return typeof(ExamplePlugin);
}
}
@@ -1,9 +1,9 @@
using MiraAPI.Example.Roles;
using MiraAPI.Example.Extension.Roles;
using MiraAPI.GameEnd;
using MiraAPI.Utilities;
using UnityEngine;
namespace MiraAPI.Example.GameOver;
namespace MiraAPI.Example.Extension.GameOver;
public class NeutralKillerGameOver : CustomGameOver
{
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0</Version>
<Description>Example extension for Mira API</Description>
<Authors>Angxl, XtraCube</Authors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
<ProjectReference Include="..\MiraAPI.Example\MiraAPI.Example.csproj" />
</ItemGroup>
<Import Project="..\AmongUs.props" />
</Project>
@@ -0,0 +1,16 @@
using MiraAPI.Example.Extension.Options;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Example.Extension.Modifiers;
public class ExtensionModifier : GameModifier
{
public override string ModifierName => "Extension Modifier";
public override string GetDescription() => "This is from the extension mod!";
public override int GetAssignmentChance() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierChance;
public override int GetAmountPerGame() => (int)OptionGroupSingleton<ExtensionModifierOptions>.Instance.ModifierLimit;
}
@@ -0,0 +1,13 @@
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example.Extension.Options;
public class ExtendedFreezerOptions : AbstractOptionGroup<FreezerRole>
{
public override string GroupName => "Extended Freezer Role Options";
[ModdedNumberOption("Freeze Uses", 1, 5)]
public float FreezeUses { get; set; } = 1;
}
@@ -0,0 +1,17 @@
using MiraAPI.Example.Extension.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
namespace MiraAPI.Example.Extension.Options;
public class ExtensionModifierOptions : AbstractOptionGroup<ExtensionModifier>
{
public override string GroupName => "Extension Modifier Options";
[ModdedNumberOption("Modifier Chance", 0, 100, 10, MiraNumberSuffixes.Percent)]
public float ModifierChance { get; set; } = 100f;
[ModdedNumberOption("Modifier Limit", 0, 10)]
public float ModifierLimit { get; set; } = 0;
}
@@ -0,0 +1,15 @@
using HarmonyLib;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Extension.Options;
using MiraAPI.GameOptions;
namespace MiraAPI.Example.Extension.Patches;
[HarmonyPatch(typeof(FreezeButton), nameof(FreezeButton.MaxUses), MethodType.Getter)]
public static class FreezeButtonPatch
{
public static void Postfix(ref int __result)
{
__result = (int)OptionGroupSingleton<ExtendedFreezerOptions>.Instance.FreezeUses;
}
}
@@ -1,16 +1,20 @@
using MiraAPI.Example.GameOver;
using MiraAPI.Example.Extension.GameOver;
using MiraAPI.GameEnd;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Example.Roles;
namespace MiraAPI.Example.Extension.Roles;
public class NeutralKillerRole : ImpostorRole, ICustomRole
{
public string RoleName => "Outcast Killer";
public string RoleDescription => "Outcast who can kill.";
public string RoleLongDescription => RoleDescription;
public string RoleLongDescription => this.RoleDescription;
public Color RoleColor => Color.magenta;
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public CustomRoleConfiguration Configuration => new(this)
@@ -16,7 +16,7 @@ public class FreezeButton : CustomActionButton<PlayerControl>
public override float Cooldown => OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeDuration;
public override int MaxUses => (int)OptionGroupSingleton<FreezerRoleSettings>.Instance.FreezeUses;
public override int MaxUses => 1;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.ExampleButton;
+4 -1
View File
@@ -6,7 +6,10 @@ namespace MiraAPI.Example;
[RegisterCustomColors]
public static class ExampleColors
{
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f));
public static CustomColor Cerulean { get; } = new("Cerulean", new Color(0.0f, 0.48f, 0.65f))
{
ColorBrightness = CustomColorBrightness.Lighter,
};
public static CustomColor Rose { get; } = new("Rose", new Color(0.98f, 0.26f, 0.62f));
+5 -5
View File
@@ -9,10 +9,7 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
namespace MiraAPI.Example;
@@ -21,7 +18,7 @@ public static class ExampleEventHandlers
public static void Initialize()
{
// You can register event handlers with the MiraEventManager class.
MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
var handle = MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
});
@@ -35,6 +32,9 @@ public static class ExampleEventHandlers
{
Logger<ExamplePlugin>.Info($"{@event.Player.Data.PlayerName} completed {@event.Task.TaskType.ToString()}");
});
// You can also unregister event handlers using the handle returned from RegisterEventHandler.
MiraEventManager.UnregisterEventHandler(handle);
}
// If you want to add extra votes to a player, do something like this.
@@ -51,7 +51,7 @@ public static class ExampleEventHandlers
[RegisterEvent(15)]
public static void HandleVoteEvent(HandleVoteEvent @event)
{
if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return;
if (@event.VoteData.Owner.Data.Role is not FreezerRole) return;
@event.VoteData.SetRemainingVotes(0);
+1 -4
View File
@@ -1,12 +1,9 @@
using Il2CppSystem.Collections.Generic;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using UnityEngine;
using Random = System.Random;
namespace MiraAPI.Example.Modifiers;
@@ -1,6 +1,4 @@
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Modifiers;
@@ -10,6 +10,8 @@ public class ExampleOptions2 : AbstractOptionGroup
public override uint GroupPriority => 0; // This group will be displayed first. The default value is uint.MaxValue.
public ModdedPlayerOption PlayerOption2 { get; } = new("YOU MUST CHOSE A PLAYER", false);
public ModdedPlayerOption PlayerOption { get; } = new("Eh whatever");
public ModdedToggleOption ToggleOpt1 { get; } = new("Toggle Option 1", false);
public ModdedToggleOption ToggleOpt2 { get; } = new("Toggle Option 2", false)
@@ -11,7 +11,4 @@ public class FreezerRoleSettings : AbstractOptionGroup<FreezerRole>
[ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)]
public float FreezeDuration { get; set; } = 5;
[ModdedNumberOption("Freeze Uses", 1, 5)]
public float FreezeUses { get; set; } = 1;
}
+35 -13
View File
@@ -1,4 +1,5 @@
using AmongUs.GameOptions;
using MiraAPI.Patches.Stubs;
using MiraAPI.Roles;
using Reactor.Utilities;
using TMPro;
@@ -14,6 +15,8 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
private bool _shouldHide;
public CustomRoleConfiguration Configuration => new(this)
{
OptionsScreenshot = ExampleAssets.Banner,
@@ -22,35 +25,54 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public override void Initialize(PlayerControl player)
{
Player = player;
Logger<ExamplePlugin>.Error("Initializing ChamelonRole for player: " + player.PlayerId);
Logger<ExamplePlugin>.Info("Initializing ChamelonRole for player: " + player.PlayerId);
RoleBehaviourStubs.Initialize(this, player);
_shouldHide = true;
}
public void PlayerControlFixedUpdate(PlayerControl playerControl)
public void FixedUpdate()
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
if (!Player || !_shouldHide)
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
return;
}
if (Player.MyPhysics.Velocity.magnitude > 0)
{
var rend = Player.cosmetics.currentBodySprite.BodySprite;
var tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, 1), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, 1), Time.deltaTime * 4f);
}
}
else
{
SpriteRenderer rend = playerControl.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = playerControl.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
SpriteRenderer rend = Player.cosmetics.currentBodySprite.BodySprite;
TextMeshPro tmp = Player.cosmetics.nameText;
tmp.color = Color.Lerp(tmp.color, new Color(tmp.color.r, tmp.color.g, tmp.color.b, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
rend.color = Color.Lerp(rend.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
foreach (var cosmetic in playerControl.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>())
{
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, playerControl.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
cosmetic.color = Color.Lerp(cosmetic.color, new Color(1, 1, 1, Player.AmOwner ? 0.3f : 0), Time.deltaTime * 4f);
}
}
}
public override void Deinitialize(PlayerControl targetPlayer)
{
Logger<ExamplePlugin>.Info("Deinitializing ChamelonRole for player: " + targetPlayer.PlayerId);
RoleBehaviourStubs.Deinitialize(this, targetPlayer);
_shouldHide = false;
foreach (var cosmetic in Player.cosmetics.transform.GetComponentsInChildren<SpriteRenderer>(true))
{
cosmetic.color = Color.white;
}
Player.cosmetics.currentBodySprite.BodySprite.color = Color.white;
Player.cosmetics.nameText.color = Color.white;
}
}
+6
View File
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiraAPI", "MiraAPI\MiraAPI.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example", "MiraAPI.Example\MiraAPI.Example.csproj", "{19B76855-16CC-43CC-A0A6-5A7FFB43F077}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiraAPI.Example.Extension", "MiraAPI.Example.Extension\MiraAPI.Example.Extension.csproj", "{526FF017-80E1-4094-804D-741AF35AA226}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19B76855-16CC-43CC-A0A6-5A7FFB43F077}.Release|Any CPU.Build.0 = Release|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Debug|Any CPU.Build.0 = Debug|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.ActiveCfg = Release|Any CPU
{526FF017-80E1-4094-804D-741AF35AA226}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+27 -6
View File
@@ -13,19 +13,24 @@ namespace MiraAPI.Colors;
public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
/// <summary>
/// Gets the main color.
/// Gets or sets the main color.
/// </summary>
public Color32 MainColor { get; } = mainColor;
public Color32 MainColor { get; set; } = mainColor;
/// <summary>
/// Gets the shadow color.
/// Gets or sets the shadow color.
/// </summary>
public Color32 ShadowColor { get; } = shadowColor;
public Color32 ShadowColor { get; set; } = shadowColor;
/// <summary>
/// Gets the name of the color.
/// Gets or sets the name of the color.
/// </summary>
public StringNames Name { get; } = name;
public StringNames Name { get; set; } = name;
/// <summary>
/// Gets or sets whether the color is lighter or darker. Can be used to help with colorblind stuff.
/// </summary>
public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker;
/// <inheritdoc />
public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60))
@@ -42,3 +47,19 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha
{
}
}
/// <summary>
/// Can be used to help with colorblind stuff.
/// </summary>
public enum CustomColorBrightness
{
/// <summary>
/// A lighter color.
/// </summary>
Lighter,
/// <summary>
/// A darker color.
/// </summary>
Darker,
}
+22
View File
@@ -0,0 +1,22 @@
using System;
namespace MiraAPI.Events;
/// <summary>
/// Event handle to use when unregistering an event.
/// </summary>
public class MiraEventHandle
{
/// <summary>
/// Gets the type of event.
/// </summary>
public Type EventType { get; }
internal MiraEventWrapper EventWrapper { get; }
internal MiraEventHandle(Type type, MiraEventWrapper eventWrapper)
{
EventType = type;
EventWrapper = eventWrapper;
}
}
+47 -6
View File
@@ -28,7 +28,14 @@ public static class MiraEventManager
foreach (var handler in handlers)
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
try
{
((Action<T>)handler.EventHandler).Invoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {typeof(T).Name}: {ex.ToString()}");
}
}
return true;
@@ -50,7 +57,14 @@ public static class MiraEventManager
foreach (var handler in handlers)
{
handler.EventHandler.DynamicInvoke(eventInstance);
try
{
handler.EventHandler.DynamicInvoke(eventInstance);
}
catch (Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error invoking event handler for {type.Name}: {ex.ToString()}");
}
}
return true;
@@ -62,12 +76,12 @@ public static class MiraEventManager
/// <param name="type">The type of event.</param>
/// <param name="methodInfo">The MethodInfo of the event handler.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {type.FullName}");
return;
throw new InvalidOperationException($"Type must be a subclass of MiraEvent: {type.FullName}");
}
EventWrappers.TryAdd(type, []);
@@ -84,6 +98,7 @@ public static class MiraEventManager
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(type, eventWrapper);
}
/// <summary>
@@ -92,7 +107,8 @@ public static class MiraEventManager
/// <param name="handler">The callback method/handler for the event.</param>
/// <param name="priority">The priority of the event handler. Lower values are called first.</param>
/// <typeparam name="T">Type of event.</typeparam>
public static void RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
/// <returns>An event handle to use when unregistering the event.</returns>
public static MiraEventHandle RegisterEventHandler<T>(Action<T> handler, int priority = 0) where T : MiraEvent
{
EventWrappers.TryAdd(typeof(T), []);
@@ -107,5 +123,30 @@ public static class MiraEventManager
}
handlers.Insert(index, eventWrapper);
return new MiraEventHandle(typeof(T), eventWrapper);
}
/// <summary>
/// Unregister an event using the event handle.
/// </summary>
/// <param name="eventHandle">A handle to the event.</param>
/// <returns>True if the event was unregistered successfully, false otherwise.</returns>
public static bool UnregisterEventHandler(MiraEventHandle eventHandle)
{
if (!EventWrappers.TryGetValue(eventHandle.EventType, out var handlers))
{
return false;
}
if (!handlers.Remove(eventHandle.EventWrapper))
{
return false;
}
if (handlers.Count == 0)
{
EventWrappers.Remove(eventHandle.EventType);
}
return true;
}
}
@@ -15,14 +15,21 @@ public class AfterMurderEvent : MiraEvent
/// </summary>
public PlayerControl Target { get; }
/// <summary>
/// Gets the player's body, if it exists.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="AfterMurderEvent"/> class.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The killed player.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target)
/// <param name="deadBody">The player's vody, if it exists.</param>
public AfterMurderEvent(PlayerControl source, PlayerControl target, DeadBody? deadBody)
{
Source = source;
Target = target;
DeadBody = deadBody;
}
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Invoked before RpcEndGame is called, allowing cancellation of the game end entirely.
/// </summary>
public class BeforeGameEndEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the reason for the game end.
/// </summary>
public GameOverReason Reason { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BeforeGameEndEvent"/> class.
/// </summary>
/// <param name="reason">The reason for the game end.</param>
public BeforeGameEndEvent(GameOverReason reason)
{
Reason = reason;
}
}
@@ -24,6 +24,7 @@ public class UpdateSystemEvent : MiraCancelableEvent
/// Initializes a new instance of the <see cref="UpdateSystemEvent"/> class.
/// </summary>
/// <param name="systemType">The SystemType being updated.</param>
/// <param name="player">The player that is updating the system.</param>
/// <param name="amount">Amount to update System to.</param>
public UpdateSystemEvent(SystemTypes systemType, PlayerControl player, byte amount)
{
@@ -24,6 +24,11 @@ public class HandleVoteEvent : MiraCancelableEvent
/// </summary>
public byte TargetId { get; }
/// <summary>
/// Gets a value indicating whether to prevent the vote from commencing.
/// </summary>
public bool PreventVote { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
@@ -39,6 +44,7 @@ public class HandleVoteEvent : MiraCancelableEvent
VoteData = playerVoteData;
Player = playerVoteData.Owner;
TargetId = targetId;
PreventVote = false;
TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
}
}
@@ -15,14 +15,21 @@ public class PlayerDeathEvent : MiraEvent
/// </summary>
public DeathReason DeathReason { get; }
/// <summary>
/// Gets the dead body associated with the player, if any.
/// </summary>
public DeadBody? DeadBody { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PlayerDeathEvent"/> class.
/// </summary>
/// <param name="player">The player who died.</param>
/// <param name="reason">The reason the player died.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason)
/// <param name="deadBody">The player's body, if it exists.</param>
public PlayerDeathEvent(PlayerControl player, DeathReason reason, DeadBody? deadBody)
{
Player = player;
DeathReason = reason;
DeadBody = deadBody;
}
}
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.CS1591.severity = none
+1 -1
View File
@@ -21,7 +21,7 @@ public static class CustomGameModeManager
}
/// <summary>
/// Current gamemode.
/// Gets the current gamemode.
/// </summary>
public static CustomGameMode? ActiveMode { get; internal set; } = new DefaultMode();
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
namespace MiraAPI.GameOptions;
@@ -11,6 +12,11 @@ public abstract class AbstractOptionGroup
{
internal List<IModdedOption> Options { get; } = [];
/// <summary>
/// Gets a list of the options which are a part of this group.
/// </summary>
public ReadOnlyCollection<IModdedOption> Children => new(Options);
/// <summary>
/// Gets the name of the group. Visible in options menu.
/// </summary>
+84 -1
View File
@@ -6,18 +6,101 @@ using UnityEngine;
namespace MiraAPI.GameOptions;
/// <summary>
/// Interface for modded options.
/// </summary>
public interface IModdedOption
{
/// <summary>
/// Gets the unique identifier for the option.
/// </summary>
uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
string Title { get; }
/// <summary>
/// Gets the StringName for the option, used for localization.
/// </summary>
StringNames StringName { get; }
/// <summary>
/// Gets or sets the MiraPlugin that created this option.
/// </summary>
IMiraPlugin? ParentMod { get; set; }
/// <summary>
/// Gets the game setting data for the option.
/// </summary>
BaseGameSetting Data { get; }
/// <summary>
/// Gets the OptionBehaviour object of the option.
/// </summary>
OptionBehaviour? OptionBehaviour { get; }
/// <summary>
/// Gets or sets the visibility function for the option.
/// </summary>
Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should be included with presets.
/// </summary>
bool IncludeInPreset { get; set; }
/// <summary>
/// Gets or sets the ConfigDefinition for the option, used for BepInEx configuration.
/// </summary>
ConfigDefinition? ConfigDefinition { get; set; }
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container);
/// <summary>
/// Creates the option behaviour for the modded option.
/// </summary>
/// <param name="toggleOpt">The ToggleOption template.</param>
/// <param name="numberOpt">The NumberOption template.</param>
/// <param name="stringOpt">The StringOption template.</param>
/// <param name="playerOpt">The PlayerOption template.</param>
/// <param name="container">>The Transform container for the option.</param>
/// <returns>>The created OptionBehaviour object.</returns>
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container);
/// <summary>
/// Gets the value as a float.
/// </summary>
/// <returns>>The value of the option as a float.</returns>
float GetFloatData();
/// <summary>
/// Gets the NetData for the option, used for network synchronization.
/// </summary>
/// <returns>>Returns the NetData object for the option.</returns>
NetData GetNetData();
/// <summary>
/// Handles incoming network data for the option.
/// </summary>
/// <param name="data">>The byte array representing the network data.</param>
void HandleNetData(byte[] data);
/// <summary>
/// Saves the option to a preset configuration file.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
/// <param name="saveDefault">Indicates whether to save the default value instead of the current value.</param>
void SaveToPreset(ConfigFile presetConfig, bool saveDefault = false);
/// <summary>
/// Binds the option to a configuration file.
/// </summary>
/// <param name="config">The ConfigFile to bind the option to.</param>
void Bind(ConfigFile config);
/// <summary>
/// Loads the option from a preset configuration file, applying the values to the option's configuration.
/// </summary>
/// <param name="presetConfig">The ConfigFile representing the preset configuration.</param>
void LoadFromPreset(ConfigFile presetConfig);
}
+15 -4
View File
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
@@ -21,6 +23,7 @@ public static class ModdedOptionsManager
private static readonly Dictionary<PropertyInfo, ModdedOptionAttribute> OptionAttributes = [];
private static readonly Dictionary<Type, AbstractOptionGroup> TypeToGroup = [];
internal static readonly Dictionary<OptionBehaviour, ModdedPlayerOption> CreatedPlayerOptions = [];
internal static readonly Dictionary<uint, IModdedOption> ModdedOptions = [];
internal static readonly List<AbstractOptionGroup> Groups = [];
@@ -42,10 +45,12 @@ public static class ModdedOptionsManager
Groups.Add(group);
TypeToGroup.Add(type, group);
pluginInfo.OptionGroups.Add(group);
pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, group);
return true;
@@ -119,7 +124,7 @@ public static class ModdedOptionsManager
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
option.ParentMod = pluginInfo.MiraPlugin;
pluginInfo.Options.Add(option);
pluginInfo.InternalOptions.Add(option);
ModdedOptions.Add(option.Id, option);
group.Options.Add(option);
}
@@ -140,7 +145,7 @@ public static class ModdedOptionsManager
// we dont know how other plugins handle their configs
// this way, all the options are saved at once, instead of one by one
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false;
@@ -156,7 +161,7 @@ public static class ModdedOptionsManager
option.HandleNetData(netData.Data);
}
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
@@ -173,7 +178,10 @@ public static class ModdedOptionsManager
/// </summary>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
attribute.SetValue(value);
@@ -185,7 +193,10 @@ public static class ModdedOptionsManager
/// <param name="__originalMethod">The original getter method.</param>
/// <param name="__result">The result of the property getter.</param>
/// <returns>False so the original getter gets skipped.</returns>
#pragma warning disable CA1707
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony naming convention")]
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
#pragma warning restore CA1707
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
__result = attribute.GetValue();
@@ -2,7 +2,9 @@
namespace MiraAPI.GameOptions;
#pragma warning disable CA1852
internal class ModifierOptionGroup : AbstractOptionGroup
#pragma warning restore CA1852
{
public override string GroupName { get; }
@@ -18,7 +18,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption"/> class.
@@ -27,7 +27,8 @@ public class ModdedEnumOption : ModdedOption<int>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="enumType">The Enum type.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue)
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -37,13 +38,13 @@ public class ModdedEnumOption : ModdedOption<int>
data.Type = global::OptionTypes.String;
data.Values = values is null ?
Enum.GetNames(enumType).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray();
: [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Value;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -87,7 +88,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data?.GetValueString(newValue), false);
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
@@ -109,7 +110,7 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <summary>
/// Gets the string values of the enum.
/// </summary>
public string[]? Values { get; }
public string[] Values { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedEnumOption{T}"/> class.
@@ -117,7 +118,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue)
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Values = values ?? Enum.GetNames(typeof(T));
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -127,13 +129,13 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
data.Type = global::OptionTypes.String;
data.Values = values is null ?
Enum.GetNames(typeof(T)).Select(CustomStringName.CreateAndRegister).ToArray()
: values.Select(CustomStringName.CreateAndRegister).ToArray();
: [.. values.Select(CustomStringName.CreateAndRegister)];
data.Index = Convert.ToInt32(Value);
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var stringOption = Object.Instantiate(stringOpt, container);
@@ -48,6 +48,7 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedNumberOption(
string title,
float defaultValue,
@@ -56,7 +57,8 @@ public class ModdedNumberOption : ModdedOption<float>
float increment,
MiraNumberSuffixes suffixType,
string? formatString = null,
bool zeroInfinity = false) : base(title, defaultValue)
bool zeroInfinity = false,
bool includeInPreset = true) : base(title, defaultValue, includeInPreset)
{
Min = min;
Max = max;
@@ -91,6 +93,7 @@ public class ModdedNumberOption : ModdedOption<float>
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container)
{
var numberOption = Object.Instantiate(numberOpt, Vector3.zero, Quaternion.identity, container);
+56 -48
View File
@@ -4,6 +4,7 @@ using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.GameOptions.OptionTypes;
@@ -16,29 +17,19 @@ public abstract class ModdedOption<T> : IModdedOption
{
private IMiraPlugin? _parentMod;
/// <summary>
/// Gets the unique identifier of the option.
/// </summary>
/// <inheritdoc />
public uint Id { get; }
/// <summary>
/// Gets the title of the option.
/// </summary>
/// <inheritdoc />
public string Title { get; }
/// <summary>
/// Gets the StringName object of the option.
/// </summary>
/// <inheritdoc />
public StringNames StringName { get; }
/// <summary>
/// Gets the BaseGameSetting data of the option.
/// </summary>
public BaseGameSetting Data { get; protected init; }
/// <inheritdoc />
public BaseGameSetting Data { get; protected set; } = null!;
/// <summary>
/// Gets or sets the parent mod of the option.
/// </summary>
/// <inheritdoc />
public IMiraPlugin? ParentMod
{
get => _parentMod;
@@ -46,6 +37,7 @@ public abstract class ModdedOption<T> : IModdedOption
{
if (_parentMod != null || value == null) return;
_parentMod = value;
var entry = _parentMod.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
}
@@ -66,19 +58,16 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary>
public Action<T>? ChangedEvent { get; set; }
/// <summary>
/// Gets or sets the visibility of the option.
/// </summary>
/// <inheritdoc />
public Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets the option behaviour of the option.
/// </summary>
/// <inheritdoc />
public bool IncludeInPreset { get; set; }
/// <inheritdoc />
public OptionBehaviour? OptionBehaviour { get; protected set; }
/// <summary>
/// Gets or sets the config definition of the option.
/// </summary>
/// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
@@ -86,7 +75,8 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
protected ModdedOption(string title, T defaultValue)
/// <param name="includeInPreset">Whether to include the option in the preset.</param>
protected ModdedOption(string title, T defaultValue, bool includeInPreset = true)
{
Id = ModdedOptionsManager.NextId;
Title = title;
@@ -94,6 +84,7 @@ public abstract class ModdedOption<T> : IModdedOption
Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title);
Visible = () => true;
IncludeInPreset = includeInPreset;
}
internal void ValueChanged(OptionBehaviour optionBehaviour)
@@ -105,7 +96,8 @@ public abstract class ModdedOption<T> : IModdedOption
/// Sets the value of the option.
/// </summary>
/// <param name="newValue">The new value.</param>
public void SetValue(T newValue)
/// <param name="sendRpc">Whether to send the value to other players.</param>
public void SetValue(T newValue, bool sendRpc = true)
{
var oldVal = Value;
Value = newValue;
@@ -115,7 +107,7 @@ public abstract class ModdedOption<T> : IModdedOption
ChangedEvent?.Invoke(Value);
}
if (AmongUsClient.Instance.AmHost)
if (sendRpc && AmongUsClient.Instance.AmHost)
{
if (ParentMod?.GetConfigFile().TryGetEntry<T>(ConfigDefinition, out var entry) == true)
{
@@ -128,22 +120,44 @@ public abstract class ModdedOption<T> : IModdedOption
OnValueChanged(newValue);
}
/// <summary>
/// Gets the float data of the option.
/// </summary>
/// <returns>A float object representing the option's value.</returns>
/// <inheritdoc />
public void SaveToPreset(ConfigFile presetConfig, bool saveDefault=false)
{
if (ConfigDefinition is null)
{
Logger<MiraApiPlugin>.Error($"Attempted to save {Title} to preset, but ConfigDefinition is null.");
return;
}
Bind(presetConfig);
presetConfig[ConfigDefinition].BoxedValue = saveDefault ? DefaultValue : Value;
}
/// <inheritdoc />
public void Bind(ConfigFile config)
{
config.Bind(ConfigDefinition, DefaultValue);
}
/// <inheritdoc />
public void LoadFromPreset(ConfigFile presetConfig)
{
if (presetConfig.TryGetEntry(ConfigDefinition, out ConfigEntry<T> entry))
{
SetValue(entry.Value, false);
}
else
{
Logger<MiraApiPlugin>.Error($"Attempted to load {Title} from preset, but ConfigDefinition is not found in preset.");
}
}
/// <inheritdoc />
public abstract float GetFloatData();
/// <summary>
/// Gets the net data of the option.
/// </summary>
/// <returns>A NetData object representing this option's data.</returns>
/// <inheritdoc />
public abstract NetData GetNetData();
/// <summary>
/// Handles incoming net data.
/// </summary>
/// <param name="data">The NetData's byte array.</param>
/// <inheritdoc />
public abstract void HandleNetData(byte[] data);
/// <summary>
@@ -159,18 +173,12 @@ public abstract class ModdedOption<T> : IModdedOption
/// <returns>The value.</returns>
public abstract T GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour);
/// <summary>
/// Creates the option behaviour.
/// </summary>
/// <param name="toggleOpt">The ToggleOption prefab.</param>
/// <param name="numberOpt">The NumberOption prefab.</param>
/// <param name="stringOpt">The StringOption prefab.</param>
/// <param name="container">The options container.</param>
/// <returns>A new OptionBehaviour for this modded option.</returns>
/// <inheritdoc />
public abstract OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container);
/// <summary>
@@ -0,0 +1,138 @@
using System;
using System.Linq;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.GameOptions.OptionTypes;
/// <summary>
/// An option for selecting an ingame player. It returns an index of the values list, NOT A PLAYER ID. To get a player out of this, index the Values list.
/// </summary>
public class ModdedPlayerOption : ModdedOption<int>
{
/// <summary>
/// Gets or sets the included players.
/// </summary>
public List<NetworkedPlayerInfo> Values { get; set; }
/// <summary>
/// Gets or sets a filter for the players included in the option.
/// </summary>
public Func<NetworkedPlayerInfo, bool>? PlayerFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option includes a "None" option. If None is selected, value will return -1.
/// </summary>
public bool AllowNone { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedPlayerOption"/> class.
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="allowNone">Whether the option includes a none option. If None is selected, value will return -1.</param>
public ModdedPlayerOption(string title, bool allowNone = true) : base(title, allowNone ? -1 : 0, false)
{
Data = ScriptableObject.CreateInstance<PlayerSelectionGameSetting>();
var data = (PlayerSelectionGameSetting)Data;
data.Title = StringName;
data.Type = global::OptionTypes.Player;
Values = new List<NetworkedPlayerInfo>();
AllowNone = allowNone;
Value = DefaultValue;
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(
ToggleOption toggleOpt,
NumberOption numberOpt,
StringOption stringOpt,
PlayerOption playerOpt,
Transform container)
{
var playerOption = Object.Instantiate(playerOpt, container);
var filteredList = GetFilteredPlayers();
Values = filteredList.ToIl2CppList();
playerOption.SetUpFromData(Data, 20);
playerOption.OnValueChanged = (Il2CppSystem.Action<OptionBehaviour>)ValueChanged;
playerOption.Title = StringName;
playerOption.TitleText.text = TranslationController.Instance.GetString(StringName);
playerOption.Values = Values;
playerOption.Value = Value;
playerOption.playerIndex = filteredList.FindIndex(p => p.PlayerId == Value);
OptionBehaviour = playerOption;
ModdedOptionsManager.CreatedPlayerOptions.TryAdd(playerOption, this);
playerOption.SetValueText();
playerOption.AdjustButtonsActiveState();
return playerOption;
}
/// <summary>
/// Returns a player ran through the filter.
/// </summary>
/// <returns>A list of filtered players.</returns>
public System.Collections.Generic.List<NetworkedPlayerInfo> GetFilteredPlayers()
{
return GameData.Instance.AllPlayers.ToArray()
.Where(x => PlayerFilter?.Invoke(x) ?? true)
.ToList();
}
/// <summary>
/// Returns a player from the value. If AllowNone is enabled and none is enabled this will return null.
/// </summary>
/// <returns>A player or null if chosen none.</returns>
public NetworkedPlayerInfo? GetPlayerValue()
{
return Value < 0 ? null : Values[Value];
}
/// <inheritdoc />
public override float GetFloatData()
{
return Value;
}
/// <inheritdoc />
public override NetData GetNetData()
{
return new NetData(Id, BitConverter.GetBytes(Value));
}
/// <inheritdoc />
public override void HandleNetData(byte[] data)
{
SetValue(BitConverter.ToInt32(data));
}
/// <inheritdoc />
public override int GetValueFromOptionBehaviour(OptionBehaviour optionBehaviour)
{
return optionBehaviour.GetInt();
}
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, Data.GetValueString(newValue), false);
if (!OptionBehaviour)
{
return;
}
if (OptionBehaviour is PlayerOption opt)
{
opt.Value = newValue;
}
}
}
@@ -15,7 +15,8 @@ public class ModdedToggleOption : ModdedOption<bool>
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue)
/// <param name="includeInPreset">Whether to include this option in the preset or not.</param>
public ModdedToggleOption(string title, bool defaultValue, bool includeInPreset=true) : base(title, defaultValue, includeInPreset)
{
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
@@ -25,7 +26,7 @@ public class ModdedToggleOption : ModdedOption<bool>
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, Transform container)
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
var toggleOption = Object.Instantiate(toggleOpt, Vector3.zero, Quaternion.identity, container);
+19 -10
View File
@@ -3,6 +3,7 @@ using System.Globalization;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Patches;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using UnityEngine.Events;
@@ -36,6 +37,11 @@ public abstract class CustomActionButton
/// </summary>
public abstract LoadableAsset<Sprite> Sprite { get; }
/// <summary>
/// Gets the format string for the cooldown timer.
/// </summary>
public virtual string CooldownTimerFormatString => "0";
/// <summary>
/// Gets the button's effect duration in seconds. If the button has no effect, set to 0.
/// </summary>
@@ -178,9 +184,12 @@ public abstract class CustomActionButton
this.Location = location;
if (!moveButton) return;
if (!moveButton)
{
return;
}
if (HudManagerPatches._bottomLeft == null || HudManagerPatches._bottomRight == null)
if (HudManagerPatches.BottomLeft == null || HudManagerPatches.BottomRight == null)
{
return;
}
@@ -188,17 +197,17 @@ public abstract class CustomActionButton
switch (location)
{
case ButtonLocation.BottomLeft:
var gridArrange = HudManagerPatches._bottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches._bottomLeft.GetComponent<AspectPosition>();
var gridArrange = HudManagerPatches.BottomLeft.GetComponent<GridArrange>();
var aspectPosition = HudManagerPatches.BottomLeft.GetComponent<AspectPosition>();
Button.transform.SetParent(HudManagerPatches._bottomLeft.transform);
Button.transform.SetParent(HudManagerPatches.BottomLeft.transform);
gridArrange.Start();
gridArrange.ArrangeChilds();
aspectPosition.AdjustPosition();
break;
case ButtonLocation.BottomRight:
Button.transform.SetParent(HudManagerPatches._bottomRight);
Button.transform.SetParent(HudManagerPatches.BottomRight);
break;
}
}
@@ -435,23 +444,23 @@ public abstract class CustomActionButton
{
if (CanUse())
{
Button.SetEnabled();
Button!.SetEnabled();
}
else
{
Button.SetDisabled();
Button!.SetDisabled();
}
if (EffectActive)
{
Button.SetFillUp(Timer, EffectDuration);
Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.text = Timer.ToString(CooldownTimerFormatString, NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button.SetCoolDown(Timer, Cooldown);
Button.SetCooldownFormat(Timer, Cooldown, CooldownTimerFormatString);
}
}
+3 -1
View File
@@ -29,9 +29,11 @@ public static class CustomButtonManager
}
CustomButtons.Add(button);
pluginInfo.Buttons.Add(button);
pluginInfo.InternalButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
#pragma warning disable S3011
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
#pragma warning restore S3011
.SetValue(null, button);
ButtonEventTypes.Add(buttonType, typeof(MiraButtonClickEvent<>).MakeGenericType(buttonType));
+3 -3
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.Hud;
@@ -13,7 +14,6 @@ public static class CustomButtonSingleton<T> where T : CustomActionButton
/// <summary>
/// Gets the instance of the button.
/// </summary>
#pragma warning disable CA1000
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom action button.")]
public static T Instance => _instance ??= CustomButtonManager.CustomButtons.OfType<T>().Single();
#pragma warning restore CA1000
}
+2 -2
View File
@@ -40,8 +40,8 @@ public abstract class BaseModifier : IOptionable
/// Gets the parent mod of the modifier.
/// </summary>
public MiraPluginInfo ParentMod => Array.Find(
MiraPluginManager.Instance.RegisteredPlugins(),
x => x.Modifiers.Exists(y => y.TypeId == TypeId)
MiraPluginManager.Instance.RegisteredPlugins,
x => x.InternalModifiers.Exists(y => y.TypeId == TypeId)
) ?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
+33 -7
View File
@@ -3,14 +3,11 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Modifiers.ModifierDisplay;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -72,7 +69,15 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in _toRemove.ToArray())
{
_toRemove.Remove(modifier);
modifier.OnDeactivate();
try
{
modifier.OnDeactivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while deactivating modifier {modifier.ModifierName}: {e.ToString()}");
}
Modifiers.Remove(modifier);
}
@@ -82,7 +87,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
try
{
modifier.OnActivate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while activating modifier {modifier.ModifierName}: {e.ToString()}");
}
if (modifier is TimedModifier { AutoStart: true } timer)
{
@@ -102,7 +114,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
foreach (var modifier in ActiveModifiers)
{
modifier.FixedUpdate();
try
{
modifier.FixedUpdate();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while (fixed) updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
@@ -115,7 +134,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
foreach (var modifier in ActiveModifiers)
{
modifier.Update();
try
{
modifier.Update();
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Error while updating modifier {modifier.ModifierName}: {e.ToString()}");
}
}
}
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
@@ -16,18 +17,26 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code used to display Mira modifiers.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the instance of the Modifier Display.
/// </summary>
public static ModifierDisplayComponent? Instance { get; private set; }
private GameObject _children = null!;
private RectTransform _children = null!;
private GameObject _modTemplate = null!;
private PassiveButton _toggleButton = null!;
private TextMeshPro _toggleBtnText = null!;
private RectTransform _pagination = null!;
private TextMeshPro _pageText = null!;
private PassiveButton _nextButton = null!;
private PassiveButton _backButton = null!;
private int _currentPage;
private const int ItemsPerPage = 3;
private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = [];
/// <summary>
@@ -45,7 +54,7 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
{
Instance = this;
_children = transform.GetChild(0).gameObject;
_children = transform.GetChild(0).GetComponent<RectTransform>();
_modTemplate = transform.GetChild(2).gameObject;
_toggleButton = transform.GetChild(1).GetComponent<PassiveButton>();
@@ -60,13 +69,59 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
_toggleButton.HoverSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent<PassiveButton>().HoverSound;
_toggleButton.inactiveSprites = _toggleButton.transform.GetChild(1).gameObject;
_toggleBtnText = _toggleButton.transform.GetChild(0).GetComponent<TextMeshPro>();
_toggleBtnText.font = HudManager.Instance.TaskPanel.taskText.font;
_toggleBtnText.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
_pagination = _children.transform.GetChild(0).GetComponent<RectTransform>();
_pageText = _pagination.transform.GetChild(0).GetComponent<TextMeshPro>();
_nextButton = _pagination.transform.GetChild(1).GetComponent<PassiveButton>();
_backButton = _pagination.transform.GetChild(2).GetComponent<PassiveButton>();
_toggleBtnText.font = _pageText.font = _nextButton.buttonText.font = _backButton.buttonText.font = HudManager.Instance.TaskPanel.taskText.font;
_toggleBtnText.fontMaterial = _pageText.fontMaterial = _nextButton.buttonText.fontMaterial = _backButton.buttonText.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
_nextButton.OnClick = new Button.ButtonClickedEvent();
_nextButton.OnClick.AddListener((UnityAction)(() =>
{
_currentPage++;
UpdatePage();
}));
_backButton.OnClick = new Button.ButtonClickedEvent();
_backButton.OnClick.AddListener((UnityAction)(() =>
{
_currentPage--;
UpdatePage();
}));
IsOpen = false;
_children.gameObject.SetActive(false);
}
/// <summary>
/// Use this to refresh the modifiers UI. Useful if you hide modifiers uis based on options.
/// </summary>
public void RefreshModifiers()
{
UpdateModifiersList([.. PlayerControl.LocalPlayer.GetModifierComponent().ActiveModifiers]);
}
/// <summary>
/// Use this to toggle the visibility of the modifiers tab.
/// </summary>
public void ToggleTab()
{
IsOpen = !IsOpen;
_children.gameObject.SetActive(IsOpen);
RefreshModifiers();
}
internal void DestroyComponent(ModifierUiComponent component)
{
_modifiers.Remove(component.Modifier!);
component.gameObject.DestroyImmediate();
RefreshModifiers();
}
[HideFromIl2Cpp]
internal static ModifierDisplayComponent CreateDisplay()
{
@@ -82,20 +137,54 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
newMod.transform.name = modifier.ModifierName;
var comp = newMod.AddComponent<ModifierUiComponent>();
comp.Modifier = modifier;
newMod.gameObject.SetActive(true);
return comp;
}
private void UpdatePage()
{
var totalPages = Mathf.CeilToInt((float)_modifiers.Count / ItemsPerPage);
_currentPage = Mathf.Clamp(_currentPage, 0, Mathf.Max(0, totalPages - 1));
if (_modifiers.Count <= ItemsPerPage)
{
_modifiers.Do(x => x.Value.gameObject.SetActive(true));
_pagination.gameObject.SetActive(false);
return;
}
_pagination.gameObject.SetActive(true);
if (_pageText != null)
{
_pageText.text = $"Page {_currentPage + 1}/{Mathf.Max(totalPages, 1)}";
}
_nextButton.gameObject.SetActive(true);
_backButton.gameObject.SetActive(true);
_modifiers.Do(x => x.Value.gameObject.SetActive(false));
var start = _currentPage * ItemsPerPage;
var end = Mathf.Min(start + ItemsPerPage, _modifiers.Count);
var modifiers = _modifiers.ToArray();
for (var i = start; i < end; i++)
{
modifiers[i].Value.gameObject.SetActive(true);
}
_pagination.SetAsLastSibling();
}
[HideFromIl2Cpp]
internal void UpdateModifiersList(List<BaseModifier> modifiers)
{
var filteredModifiers = modifiers.Where(x => !x.HideOnUi && x.GetDescription() != string.Empty).ToList();
var modifiersToAdd = filteredModifiers.ToList();
foreach (var mod in _modifiers.ToArray())
{
if (filteredModifiers.Contains(mod.Key))
if (modifiersToAdd.Contains(mod.Key))
{
filteredModifiers.Remove(mod.Key);
modifiersToAdd.Remove(mod.Key);
continue;
}
@@ -103,9 +192,9 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
_modifiers.Remove(mod.Key);
}
_toggleBtnText.text = $"Modifiers ({modifiers.Count})";
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
if (modifiers.Count == 0)
if (filteredModifiers.Count == 0)
{
_toggleButton.gameObject.SetActive(false);
return;
@@ -113,9 +202,11 @@ public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
_toggleButton.gameObject.SetActive(true);
foreach (var mod in filteredModifiers)
foreach (var mod in modifiersToAdd)
{
_modifiers.Add(mod, CreateForModifier(mod));
}
UpdatePage();
}
}
@@ -9,7 +9,7 @@ namespace MiraAPI.Modifiers.ModifierDisplay;
/// The code placed on every Modifier HUD object. Used to handle updating.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
public class ModifierUiComponent(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the modifier which this component is for.
@@ -17,12 +17,13 @@ public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
[HideFromIl2Cpp]
public BaseModifier? Modifier { get; internal set; }
// ReSharper disable InconsistentNaming
private SpriteRenderer modBg = null!;
private TextMeshPro desc = null!;
private SpriteRenderer icon = null!;
private TextMeshPro nameText = null!;
private RectTransform descRect = null!;
// ReSharper restore InconsistentNaming
private void Awake()
{
if (Modifier == null) return;
@@ -43,6 +44,12 @@ public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
{
if (Modifier == null || ModifierDisplayComponent.Instance == null || !ModifierDisplayComponent.Instance.IsOpen) return;
if (Modifier.HideOnUi || Modifier.GetDescription() == string.Empty)
{
ModifierDisplayComponent.Instance.DestroyComponent(this);
return;
}
nameText.text = Modifier.ModifierName;
desc.text = Modifier.GetDescription();
modBg.gameObject.SetActive(true);
+2 -2
View File
@@ -30,7 +30,7 @@ public static class ModifierExtensions
_ = ModifierManager.GetModifierType(typeId) ?? throw new InvalidOperationException(
$"Modifier with ID {typeId} is not registered.");
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args));
Rpc<AddModifierRpc>.Instance.Send(target, new ModifierData(typeId, Guid.NewGuid(), args), true);
}
/// <summary>
@@ -63,7 +63,7 @@ public static class ModifierExtensions
/// </summary>
/// <param name="target">The player to remove the modifier from.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
[MethodRpc((uint)MiraRpc.RemoveModifier)]
[MethodRpc((uint)MiraRpc.RemoveModifier, SendImmediately = true)]
public static void RpcRemoveModifier(this PlayerControl target, Guid uniqueId)
{
target.RemoveModifier(uniqueId);
+6 -4
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using MiraAPI.Utilities;
@@ -11,7 +12,7 @@ namespace MiraAPI.Modifiers;
/// </summary>
public static class ModifierFactory
{
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> _constructorCache = [];
private static readonly Dictionary<(Type, Type[]), Func<object[], BaseModifier>> ConstructorCache = [];
private static Func<object[], BaseModifier> CreateConstructor(Type type, params object[] args)
{
@@ -45,13 +46,13 @@ public static class ModifierFactory
/// <returns>An instance of the modifier.</returns>
public static BaseModifier CreateInstance(Type type, params object[] args)
{
var argTypes = args.Select(arg => arg?.GetType() ?? typeof(object)).ToArray();
var argTypes = args.Select(arg => arg.GetType()).ToArray();
var key = (type, argTypes);
if (!_constructorCache.TryGetValue(key, out var constructor))
if (!ConstructorCache.TryGetValue(key, out var constructor))
{
constructor = CreateConstructor(type, args);
_constructorCache[key] = constructor;
ConstructorCache[key] = constructor;
}
return constructor(args);
@@ -95,6 +96,7 @@ public static class ModifierFactory<T> where T : BaseModifier
/// </summary>
/// <param name="args">Parameters for the constructor.</param>
/// <returns>An instance of the modifier.</returns>
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a factory class.")]
public static T CreateInstance(params object[] args)
{
return Constructor(args);
+19 -3
View File
@@ -5,7 +5,6 @@ using System.Runtime.Serialization;
using MiraAPI.Modifiers.Types;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using Random = System.Random;
@@ -17,6 +16,18 @@ namespace MiraAPI.Modifiers;
/// </summary>
public static class ModifierManager
{
/// <summary>
/// Gets or sets a value indicating whether modifiers should be assigned to players.
/// </summary>
public static bool MiraAssignsModifiers { get; set; } = true;
/// <summary>
/// Gets the list of all registered modifiers.
/// </summary>
public static IReadOnlyList<BaseModifier> Modifiers { get; internal set; } = [];
internal static List<BaseModifier> InternalModifiers { get; } = [];
private static readonly Dictionary<uint, Type> IdToTypeModifierMap = [];
private static readonly Dictionary<Type, uint> TypeToIdModifierMap = [];
private static readonly Dictionary<int, List<uint>> PrioritiesToIdsMap = [];
@@ -75,7 +86,8 @@ public static class ModifierManager
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
}
info.Modifiers.Add(modifier);
info.InternalModifiers.Add(modifier);
InternalModifiers.Add(modifier);
if (modifier is not GameModifier gameModifier)
{
@@ -99,7 +111,11 @@ public static class ModifierManager
return true;
}
internal static void AssignModifiers(List<PlayerControl> plrs)
/// <summary>
/// Assigns modifiers to players. ONLY CALL THIS METHOD IF YOU KNOW WHAT YOU ARE DOING.
/// </summary>
/// <param name="plrs">The players to assign modifiers to.</param>
public static void AssignModifiers(List<PlayerControl> plrs)
{
var rand = new Random();
+8 -7
View File
@@ -1,5 +1,6 @@
using System.Collections;
using System.Linq;
using AmongUs.Data;
using AmongUs.GameOptions;
using Assets.CoreScripts;
using BepInEx.Unity.IL2CPP.Utils;
@@ -96,7 +97,7 @@ public static class CustomMurderRpc
var flag = PlayerControl.LocalPlayer.Data.Role.Role == RoleTypes.GuardianAngel;
if (flag && PlayerControl.LocalPlayer.Data.PlayerId == target.protectedByGuardianId)
{
StatsManager.Instance.IncrementStat(StringNames.StatsGuardianAngelCrewmatesProtected);
DataManager.Player.Stats.IncrementStat(StatID.Role_GuardianAngel_CrewmatesProtected);
AchievementManager.Instance.OnProtectACrewmate();
}
@@ -127,14 +128,14 @@ public static class CustomMurderRpc
DebugAnalytics.Instance.Analytics.Kill(target.Data, source.Data);
if (source.AmOwner)
{
StatsManager.Instance.IncrementStat(
DataManager.Player.Stats.IncrementStat(
GameManager.Instance.IsHideAndSeek()
? StringNames.StatsImpostorKills_HideAndSeek
: StringNames.StatsImpostorKills);
? StatID.HideAndSeek_ImpostorKills
: StatID.ImpostorKills);
if (source.CurrentOutfitType == PlayerOutfitType.Shapeshifted)
{
StatsManager.Instance.IncrementStat(StringNames.StatsShapeshifterShiftedKills);
DataManager.Player.Stats.IncrementStat(StatID.Role_Shapeshifter_ShiftedKills);
}
if (Constants.ShouldPlaySfx() && playKillSound)
@@ -152,7 +153,7 @@ public static class CustomMurderRpc
target.gameObject.layer = LayerMask.NameToLayer("Ghost");
if (target.AmOwner)
{
StatsManager.Instance.IncrementStat(StringNames.StatsTimesMurdered);
DataManager.Player.Stats.IncrementStat(StatID.TimesMurdered);
if (Minigame.Instance)
{
try
@@ -263,7 +264,7 @@ public static class CustomMurderRpc
deadBody.enabled = true;
}
var afterMurderEvent = new AfterMurderEvent(source, target);
var afterMurderEvent = new AfterMurderEvent(source, target, deadBody);
MiraEventManager.InvokeEvent(afterMurderEvent);
if (!isParticipant)
+5
View File
@@ -44,4 +44,9 @@ public enum MiraRpc : uint
/// Custom RPC to undo a player's vote.
/// </summary>
RemoveVote,
/// <summary>
/// Custom RPC to replace Among Us CastVote.
/// </summary>
CastVote,
}
+5
View File
@@ -0,0 +1,5 @@
[*.{cs,vb}]
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CS1591.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1615.severity = none
+33
View File
@@ -0,0 +1,33 @@
using HarmonyLib;
using UnityEngine;
namespace MiraAPI.Patches;
/// <summary>
/// Allows players to paste text into chat.
/// Source: https://github.com/CallOfCreator/NewMod/blob/main/NewMod/Patches/ClipboardPatch.cs
/// </summary>
[HarmonyPatch(typeof(ChatController), nameof(ChatController.Update))]
public static class ChatControllerPatch
{
public static void Prefix(ChatController __instance)
{
if (!HudManager.Instance.Chat.IsOpenOrOpening) return;
var ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
if (!ctrlPressed || !Input.GetKeyDown(KeyCode.V)) return;
var clipboard = GUIUtility.systemCopyBuffer;
if (string.IsNullOrWhiteSpace(clipboard)) return;
clipboard = clipboard.Replace("<", string.Empty)
.Replace(">", string.Empty)
.Replace("\r", string.Empty);
if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift))
clipboard = clipboard.Replace("\n", string.Empty);
__instance.freeChatField.textArea.SetText(__instance.freeChatField.textArea.text + clipboard);
}
}
+28 -13
View File
@@ -5,29 +5,44 @@ using MiraAPI.GameEnd;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
internal static class EndGamePatches
{
public static bool Prefix(EndGameManager __instance)
[HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))]
public static class GameManagerPatches
{
if (CustomGameOver.Instance is not { } gameOver)
public static bool Prefix(GameOverReason endReason)
{
return true;
}
var @event = new BeforeGameEndEvent(endReason);
MiraEventManager.InvokeEvent(@event);
var result = gameOver.BeforeEndGameSetup(__instance);
return result;
return !@event.IsCancelled;
}
}
public static void Postfix(EndGameManager __instance)
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.SetEverythingUp))]
public static class EndGameManagerPatches
{
if (CustomGameOver.Instance is { } gameOver)
public static bool Prefix(EndGameManager __instance)
{
gameOver.AfterEndGameSetup(__instance);
CustomGameOver.Instance = null;
if (CustomGameOver.Instance is not { } gameOver)
{
return true;
}
var result = gameOver.BeforeEndGameSetup(__instance);
return result;
}
var @event = new GameEndEvent(__instance);
MiraEventManager.InvokeEvent(@event);
public static void Postfix(EndGameManager __instance)
{
if (CustomGameOver.Instance is { } gameOver)
{
gameOver.AfterEndGameSetup(__instance);
CustomGameOver.Instance = null;
}
var @event = new GameEndEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
}
}
+10 -5
View File
@@ -1,6 +1,7 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
@@ -13,13 +14,17 @@ public static class KillAnimationPatches
/// <summary>
/// Used to trigger the <see cref="AfterMurderEvent"/>.
/// </summary>
/// <param name="source">The killer.</param>
/// <param name="target">The target.</param>
[HarmonyPostfix]
[HarmonyPatch(typeof(KillAnimation), nameof(KillAnimation.CoPerformKill))]
public static void KillAnimPostfix(PlayerControl source, PlayerControl target)
[HarmonyPatch(typeof(KillAnimation._CoPerformKill_d__2), nameof(KillAnimation._CoPerformKill_d__2.MoveNext))]
public static void KillAnimPostfix(KillAnimation._CoPerformKill_d__2 __instance)
{
var afterMurderEvent = new AfterMurderEvent(source, target);
// 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);
}
}
+36 -5
View File
@@ -1,21 +1,52 @@
using HarmonyLib;
using AmongUs.InnerNet.GameDataMessages;
using HarmonyLib;
using InnerNet;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Utilities;
using Reactor.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch]
public static class ReportBodyEventPatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
private static bool ReportBodyEventHook(PlayerControl source, NetworkedPlayerInfo? target)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
var @event = new ReportBodyEvent(__instance, target, body);
var @event = new ReportBodyEvent(source, target, body);
MiraEventManager.InvokeEvent(@event);
return !@event.IsCancelled;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.ReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
{
return ReportBodyEventHook(__instance, target);
}
[HarmonyPrefix]
[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.LateBroadcastReliableMessage))]
public static bool LateBroadcastReliableMessagePrefix(IGameDataMessage rpcMessage)
{
var reportBodyMessage = rpcMessage.TryCast<RpcReportDeadBodyMessage>();
if (reportBodyMessage == null)
{
return true;
}
var source = AmongUsClient.Instance.FindObjectByNetId<PlayerControl>(reportBodyMessage.rpcObjectNetId);
if (source == null)
{
Logger<MiraApiPlugin>.Error("Failed to find source player for RpcReportDeadBodyMessage with NetId: " + reportBodyMessage.rpcObjectNetId);
return true;
}
var target = GameData.Instance.GetPlayerById(reportBodyMessage.targetPlayerId);
return ReportBodyEventHook(source, target);
}
}
+4 -4
View File
@@ -23,10 +23,10 @@ public static class VentEventPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcEnterVent))]
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int id)
public static bool PlayerPhysicsRpcEnterVentPrefix(PlayerPhysics __instance, int ventId)
{
var pc = __instance.myPlayer;
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == id);
var vent = ShipStatus.Instance.AllVents.FirstOrDefault(v => v.Id == ventId);
var @event = new EnterVentEvent(pc, vent);
MiraEventManager.InvokeEvent(@event);
@@ -40,10 +40,10 @@ public static class VentEventPatches
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.RpcExitVent))]
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int id)
public static bool PlayerPhysicsRpcExitVentPrefix(PlayerPhysics __instance, int ventId)
{
var pc = __instance.myPlayer;
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == id);
var vent = ShipStatus.Instance.AllVents.First(v => v.Id == ventId);
var @event = new ExitVentEvent(pc, vent);
MiraEventManager.InvokeEvent(@event);
+7 -7
View File
@@ -67,9 +67,9 @@ internal static class TaskAdderPatches
modifiersFolder.FolderName = "Modifiers";
modifiersFolder.name = "ModifiersFolder";
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
if (plugin.CustomRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
if (plugin.InternalRoles.Any(x => x.Value is ICustomRole { Configuration.ShowInFreeplay: true }))
{
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Roles";
@@ -78,7 +78,7 @@ internal static class TaskAdderPatches
rolesFolder.SubFolders.Add(newFolder);
}
if (plugin.Modifiers.Exists(x => x.ShowInFreeplay))
if (plugin.InternalModifiers.Exists(x => x.ShowInFreeplay))
{
var newFolder = Object.Instantiate(__instance.RootFolderPrefab, _scroller.Inner);
newFolder.name = plugin.PluginId+":Modifiers";
@@ -317,9 +317,9 @@ internal static class TaskAdderPatches
ref float num2,
ref float num3)
{
for (var m = 0; m < plugin.Modifiers.Count; m++)
for (var m = 0; m < plugin.InternalModifiers.Count; m++)
{
var modifier = plugin.Modifiers[m];
var modifier = plugin.InternalModifiers[m];
if (!modifier.ShowInFreeplay)
{
continue;
@@ -368,9 +368,9 @@ internal static class TaskAdderPatches
private static void PluginRoles(TaskAdderGame instance, MiraPluginInfo plugin, ref float num, ref float num2, ref float num3)
{
for (var m = 0; m < plugin.CustomRoles.Count; m++)
for (var m = 0; m < plugin.InternalRoles.Count; m++)
{
var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value;
var roleBehaviour = plugin.InternalRoles.ElementAt(m).Value;
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour is ICustomRole { Configuration.ShowInFreeplay: false })
{
+2
View File
@@ -0,0 +1,2 @@
[*.cs]
dotnet_diagnostic.S125.severity = none
+12 -24
View File
@@ -1,37 +1,25 @@
using HarmonyLib;
using MiraAPI.Hud;
using Reactor.Utilities;
namespace MiraAPI.Patches.Hud;
/// <summary>
/// Reset button patches
/// </summary>
[HarmonyPatch]
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
public static class ButtonResetPatches
{
/// <summary>
/// Resets the cooldown and effect of all custom buttons when the meeting starts.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
public static void MeetingHudStartPostfix()
public static void Postfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
customActionButton.ResetCooldownAndOrEffect();
}
}
/// <summary>
/// Resets the cooldown and effect of all custom buttons after the exile screen is closed.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))]
public static void ExileControllerReEnableGameplayPostfix()
{
foreach (var customActionButton in CustomButtonManager.CustomButtons)
{
customActionButton.ResetCooldownAndOrEffect();
try
{
customActionButton.ResetCooldownAndOrEffect();
}
catch (System.Exception ex)
{
Logger<MiraApiPlugin>.Error($"Error resetting cooldown and effect for button {customActionButton.GetType().Name}: {ex}");
}
}
}
}
+32 -17
View File
@@ -1,5 +1,6 @@
using HarmonyLib;
using MiraAPI.Hud;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -13,9 +14,9 @@ namespace MiraAPI.Patches;
public static class HudManagerPatches
{
// Custom buttons parent.
internal static GameObject? _bottomLeft;
internal static Transform? _bottomRight;
internal static Transform? _buttons;
public static GameObject? BottomLeft { get; private set; }
public static Transform? BottomRight { get; private set; }
public static Transform? Buttons { get; private set; }
/*
/// <summary>
@@ -36,30 +37,30 @@ public static class HudManagerPatches
[HarmonyPatch(nameof(HudManager.Start))]
public static void StartPostfix(HudManager __instance)
{
if (_buttons == null)
if (Buttons == null)
{
_buttons = __instance.transform.Find("Buttons");
Buttons = __instance.transform.Find("Buttons");
}
if (_bottomRight == null)
if (BottomRight == null)
{
_bottomRight = _buttons.Find("BottomRight");
BottomRight = Buttons.Find("BottomRight");
}
if (_bottomLeft == null)
if (BottomLeft == null)
{
_bottomLeft = Object.Instantiate(_bottomRight.gameObject, _buttons);
BottomLeft = Object.Instantiate(BottomRight.gameObject, Buttons);
}
foreach (var t in _bottomLeft.GetComponentsInChildren<ActionButton>(true))
foreach (var t in BottomLeft.GetComponentsInChildren<ActionButton>(true))
{
t.gameObject.Destroy();
}
var gridArrange = _bottomLeft.GetComponent<GridArrange>();
var aspectPosition = _bottomLeft.GetComponent<AspectPosition>();
var gridArrange = BottomLeft.GetComponent<GridArrange>();
var aspectPosition = BottomLeft.GetComponent<AspectPosition>();
_bottomLeft.name = "BottomLeft";
BottomLeft.name = "BottomLeft";
gridArrange.Alignment = GridArrange.StartAlign.Right;
aspectPosition.Alignment = AspectPosition.EdgeAlignments.LeftBottom;
@@ -67,8 +68,8 @@ public static class HudManagerPatches
{
var location = button.Location switch
{
ButtonLocation.BottomLeft => _bottomLeft.transform,
ButtonLocation.BottomRight => _bottomRight,
ButtonLocation.BottomLeft => BottomLeft.transform,
ButtonLocation.BottomRight => BottomRight,
_ => null,
};
@@ -77,7 +78,14 @@ public static class HudManagerPatches
continue;
}
button.CreateButton(location);
try
{
button.CreateButton(location);
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to create custom button {button.GetType().Name}: {e}");
}
}
gridArrange.Start();
@@ -105,7 +113,14 @@ public static class HudManagerPatches
foreach (var button in CustomButtonManager.CustomButtons)
{
button.SetActive(isActive, role);
try
{
button.SetActive(isActive, role);
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to set custom button {button.GetType().Name} active: {e}");
}
}
}
}
+1 -1
View File
@@ -114,7 +114,7 @@ public static class HatsTabPatches
yield return null;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.SetScrollerBounds();
yield return new WaitForSeconds(0.01f);
}
__instance.currentHatIsEquipped = true;
+2 -2
View File
@@ -91,7 +91,7 @@ public static class NameplatesTabPatches
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.SetScrollerBounds();
__instance.currentNameplateIsEquipped = true;
}
@@ -108,7 +108,7 @@ public static class NameplatesTabPatches
var x = (NamePlateViewData viewdata) =>
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image;
//(colorChip as NameplateChip).image.sprite = viewdata?.Image;
// (colorChip as NameplateChip).image.sprite = viewdata?.Image;
};
__instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x));
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
+1 -1
View File
@@ -94,7 +94,7 @@ public static class SkinsTabPatches
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.SetScrollerBounds();
__instance.currentSkinIsEquipped = true;
}
+1 -1
View File
@@ -90,7 +90,7 @@ public static class VisorsTabPatches
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.SetScrollerBounds();
__instance.currentVisorIsEquipped = true;
}
@@ -0,0 +1,19 @@
using HarmonyLib;
namespace MiraAPI.Patches;
/// <summary>
/// Used to fix the Minigame crash errors (credits Submerged/PH Gaming for the fix).
/// </summary>
[HarmonyPatch]
public static class MinigameBeginErrorPatch
{
private static readonly Logger _logger = new(Logger.Category.Gameplay, "Minigame");
[HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))]
[HarmonyPrefix]
public static void Prefix(Minigame __instance)
{
__instance.logger ??= _logger;
}
}
@@ -14,18 +14,12 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches the GameOptionsMenu to add custom options.
/// </summary>
[HarmonyPatch(typeof(GameOptionsMenu))]
public static class GameOptionsMenuPatch
internal static class GameOptionsMenuPatch
{
/// <summary>
/// Update patch for the GameOptionsMenu.
/// </summary>
/// <param name="__instance">The GameOptionsMenu instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(GameOptionsMenu.Update))]
// ReSharper disable once InconsistentNaming
public static void UpdatePatch(GameOptionsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
@@ -41,7 +35,7 @@ public static class GameOptionsMenuPatch
else
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups
GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
@@ -107,7 +101,7 @@ public static class GameOptionsMenuPatch
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var modGroup in groups)
@@ -118,7 +112,7 @@ public static class GameOptionsMenuPatch
private static void ModifiersCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
var groups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var group in groups)
{
@@ -128,6 +122,7 @@ public static class GameOptionsMenuPatch
[HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))]
// ReSharper disable once InconsistentNaming
public static bool SettingsPatch(GameOptionsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
@@ -143,7 +138,7 @@ public static class GameOptionsMenuPatch
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups
var filteredGroups = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
@@ -156,6 +151,7 @@ public static class GameOptionsMenuPatch
[HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.Initialize))]
// ReSharper disable once InconsistentNaming
public static bool InitPatch(GameOptionsMenu __instance)
{
if (__instance.Children != null && __instance.Children.Count != 0)
@@ -224,6 +220,7 @@ public static class GameOptionsMenuPatch
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.playerOptionOrigin,
menu.settingsContainer));
foreach (var newOpt in options)
@@ -0,0 +1,348 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using BepInEx.Configuration;
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
internal static class GamePresetsTabPatches
{
private static GameObject _saveButton = null!;
private static GameObject _refreshButton = null!;
private static GameObject _folderButton = null!;
private static GameObject _newDivider = null!;
private static GameObject _presetHolder = null!;
private static GridArrange _arrange = null!;
private static readonly object Lock = new();
public static void DivideSize(GameObject obj, float amount)
{
foreach (var collider in obj.GetComponentsInChildren<Collider2D>(true))
{
if (collider.TryCast<BoxCollider2D>() is { } col)
{
col.size = new Vector2(col.size.x / amount, col.size.y);
}
}
foreach (var rend in obj.GetComponentsInChildren<SpriteRenderer>(true))
{
rend.size = new Vector2(rend.size.x / amount, rend.size.y);
}
}
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnEnable))]
public static class GamePresetsOnEnablePatch
{
// ReSharper disable once InconsistentNaming
public static void Postfix(GamePresetsTab __instance)
{
if (!GameSettingMenu.Instance)
{
return;
}
CreatePresetMenu(__instance);
__instance.StandardPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
__instance.SecondPresetButton.gameObject.SetActive(GameSettingMenuPatches.SelectedModIdx == 0);
if (GameSettingMenuPatches.SelectedModIdx != 0)
{
__instance.PresetDescriptionText.text = "Select or create an options preset for this mod";
}
else
{
__instance.SetSelectedText();
}
// Show the save and refresh buttons
if (_saveButton)
{
_saveButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_refreshButton)
{
_refreshButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_folderButton)
{
_folderButton.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
if (_newDivider)
{
_newDivider.SetActive(GameSettingMenuPatches.SelectedModIdx != 0);
}
var prefab = GameSettingMenu.Instance.GameSettingsButton;
foreach (var preset in MiraPluginManager.Instance.RegisteredPlugins.SelectMany(x => x.InternalPresets))
{
var button = Object.Instantiate(prefab, _presetHolder.transform);
button.buttonText.text = preset.Name;
if (button.buttonText.TryGetComponent<TextTranslatorTMP>(out var tmp))
{
tmp.DestroyImmediate();
}
button.buttonText.alignment = TextAlignmentOptions.Center;
button.OnClick = new Button.ButtonClickedEvent();
button.OnClick.AddListener(
(UnityAction)(() =>
{
preset.LoadPreset();
}));
button.gameObject.SetActive(false);
preset.PresetButton = button.gameObject;
}
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
{
foreach (var button in mod.InternalPresets.Select(x => x.PresetButton))
{
if (button != null)
{
button.SetActive(mod == GameSettingMenuPatches.SelectedMod);
}
}
}
_arrange.Start();
_arrange.ArrangeChilds();
}
}
[HarmonyPatch(typeof(GamePresetsTab), nameof(GamePresetsTab.OnDisable))]
public static class GamePresetsOnDisablePatch
{
// ReSharper disable once InconsistentNaming
public static void Postfix()
{
Logger<MiraApiPlugin>.Error("OnDisable called");
if (_saveButton)
{
_saveButton.SetActive(false);
}
if (_refreshButton)
{
_refreshButton.SetActive(false);
}
if (_folderButton)
{
_folderButton.SetActive(false);
}
if (_newDivider)
{
_newDivider.SetActive(false);
}
foreach (var mod in MiraPluginManager.Instance.RegisteredPlugins)
{
foreach (var button in mod.InternalPresets.Select(x => x.PresetButton))
{
if (button != null)
{
button.SetActive(false);
}
}
}
}
}
public static void CreatePresetMenu(GamePresetsTab presetTab)
{
var prefab = GameSettingMenu.Instance.GameSettingsButton;
if (prefab == null)
{
Logger<MiraApiPlugin>.Error("GameSettingsButton prefab is null");
return;
}
var tmpTranslator = presetTab.PresetDescriptionText.gameObject.GetComponent<TextTranslatorTMP>();
if (tmpTranslator)
{
tmpTranslator.Destroy();
}
if (!_newDivider)
{
var oldDiv = presetTab.transform.FindChild("DividerImage");
_newDivider = Object.Instantiate(oldDiv.gameObject, oldDiv.transform.parent);
_newDivider.transform.localPosition = new Vector3(1.85f, 0.13f, 0f);
_newDivider.transform.localScale = new Vector3(1.13f, 1, 1);
_newDivider.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, 90));
}
PassiveButton saveButton;
if (!_saveButton)
{
saveButton = Object.Instantiate(prefab, presetTab.transform);
_saveButton = saveButton.gameObject;
_saveButton.gameObject.name = "SaveButton";
_saveButton.transform.localPosition = new Vector3(3.2f, 1.7f, -2);
// set the button text and alignment
var saveText = saveButton.buttonText;
saveText.text = "Save";
saveText.GetComponent<TextTranslatorTMP>().Destroy();
saveText.alignment = TextAlignmentOptions.Center;
saveText.transform.parent.localPosition = new Vector3(
-.525f,
saveText.transform.parent.localPosition.y,
saveText.transform.parent.localPosition.z);
DivideSize(saveButton.gameObject, 2f);
saveButton.OnClick = new Button.ButtonClickedEvent();
saveButton.OnClick.AddListener(
(UnityAction)(() =>
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return;
}
SavePresetPopup.CreatePopup(
name =>
{
if (GameSettingMenuPatches.SelectedMod == null)
{
return;
}
if (name == string.Empty)
{
return;
}
var presetFile = new ConfigFile(
Path.Combine(
PresetManager.PresetDirectory,
GameSettingMenuPatches.SelectedMod.PluginId,
$"{name}.cfg"),
false)
{
SaveOnConfigSet = false,
};
foreach (var option in GameSettingMenuPatches.SelectedMod.InternalOptions.Where(x => x.IncludeInPreset))
{
option.SaveToPreset(presetFile);
}
foreach (var role in GameSettingMenuPatches.SelectedMod.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
{
role.SaveToPreset(presetFile);
}
presetFile.Save();
Refresh();
});
}));
}
saveButton = _saveButton.GetComponent<PassiveButton>();
PassiveButton refreshButton;
if (!_refreshButton)
{
refreshButton = Object.Instantiate(saveButton, presetTab.transform);
_refreshButton = refreshButton.gameObject;
_refreshButton.name = "RefreshButton";
_refreshButton.transform.localPosition = new Vector3(2.85f, 1.1f, -2);
var icon = new GameObject("Sprite");
icon.transform.SetParent(refreshButton.transform);
icon.transform.localPosition = new Vector3(-0.47f, -0.08f, -5f);
icon.transform.localScale = new Vector3(0.5f, 0.5f, 1);
icon.layer = refreshButton.gameObject.layer;
var iconSpriteRend = icon.AddComponent<SpriteRenderer>();
iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset();
DivideSize(refreshButton.gameObject, 2f);
refreshButton.buttonText.gameObject.Destroy();
refreshButton.OnClick = new Button.ButtonClickedEvent();
refreshButton.OnClick.AddListener((UnityAction)Refresh);
}
refreshButton = _refreshButton.GetComponent<PassiveButton>();
if (!_folderButton)
{
var openFolderButton = Object.Instantiate(refreshButton, presetTab.transform);
_folderButton = openFolderButton.gameObject;
_folderButton.name = "OpenFolderButton";
_folderButton.transform.localPosition = new Vector3(3.55f, 1.1f, -2);
var folderRend = openFolderButton.transform.FindChild("Sprite").gameObject.GetComponent<SpriteRenderer>();
folderRend.sprite = MiraAssets.FolderIcon.LoadAsset();
folderRend.transform.localScale = new Vector3(0.4f, 0.4f, 1);
openFolderButton.OnClick = new Button.ButtonClickedEvent();
openFolderButton.OnClick.AddListener(
(UnityAction)(() =>
{
var directory = Path.Combine(
PresetManager.PresetDirectory,
GameSettingMenuPatches.SelectedMod?.PluginId ?? string.Empty);
Directory.CreateDirectory(directory);
Process.Start(
new ProcessStartInfo
{
FileName = directory,
UseShellExecute = true,
Verb = "open",
});
}));
}
_saveButton.SetActive(false);
_refreshButton.SetActive(false);
_folderButton.SetActive(false);
_newDivider.SetActive(false);
if (!_presetHolder)
{
_presetHolder = new GameObject("PresetHolder");
_presetHolder.transform.SetParent(presetTab.transform, false);
_presetHolder.transform.localScale = new Vector3(0.9f, 0.9f, 1);
_presetHolder.transform.localPosition = new Vector3(-1.9f, 1.7f, 0);
_arrange = _presetHolder.AddComponent<GridArrange>();
_arrange.Alignment = GridArrange.StartAlign.Right;
_arrange.CellSize = new Vector2(2.15f, -.55f);
_arrange.MaxColumns = 2;
}
}
private static void Refresh()
{
Logger<MiraApiPlugin>.Error("Refreshing presets");
if (GameSettingMenuPatches.SelectedMod == null)
{
Logger<MiraApiPlugin>.Error("Selected mod is null, cannot refresh presets");
return;
}
lock (Lock)
{
PresetManager.LoadPresets(GameSettingMenuPatches.SelectedMod);
}
GameSettingMenu.Instance.ChangeTab(0, false); // refresh the tab
}
}
+109 -41
View File
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Presets;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
using TMPro;
@@ -30,13 +32,39 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
private static Dictionary<int, Vector3> OptionsPositions { get; } = [];
private static Dictionary<int, Vector3> ModifiersPositions { get; } = [];
private static void SaveScrollPositions(GameSettingMenu gameSettingMenu)
{
if (_modifiersTab)
{
ModifiersPositions[SelectedModIdx] = _modifiersTab!.scrollBar.Inner.localPosition;
}
RoleSettingMenuPatches.RolePositions[SelectedModIdx] = gameSettingMenu.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionsPositions[SelectedModIdx] = gameSettingMenu.GameSettingsTab.scrollBar.Inner.localPosition;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
public static void ChangeTabPrefix(GameSettingMenu __instance)
{
SaveScrollPositions(__instance);
}
[HarmonyPostfix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly)
{
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
{
if (_modifiersTab) _modifiersTab!.gameObject.SetActive(tabNum == 3);
if (_modifiersTab)
{
_modifiersTab!.gameObject.SetActive(tabNum == 3);
}
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum == 3)
@@ -60,7 +88,10 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum != 3) return;
if (tabNum != 3)
{
return;
}
if (__instance.RoleSettingsButton.gameObject.active)
{
@@ -110,8 +141,9 @@ internal static class GameSettingMenuPatches
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
SaveScrollPositions(__instance);
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{
SelectedModIdx = 0;
}
@@ -128,10 +160,11 @@ internal static class GameSettingMenuPatches
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
SaveScrollPositions(__instance);
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
@@ -208,57 +241,64 @@ internal static class GameSettingMenuPatches
_modifierBtnOgPos = _modifiersButton.transform.localPosition;
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
PresetManager.LoadPresets(plugin);
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu __instance)
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu menu)
{
__instance.RoleSettingsButton.OnClick = new ButtonClickedEvent();
__instance.RoleSettingsButton.OnMouseOver = new UnityEvent();
menu.RoleSettingsButton.OnClick = new ButtonClickedEvent();
menu.RoleSettingsButton.OnMouseOver = new UnityEvent();
if (replace)
{
__instance.RoleSettingsButton.buttonText.text = "Modifiers Settings";
__instance.RoleSettingsButton.OnClick.AddListener(
menu.RoleSettingsButton.buttonText.text = "Modifiers Settings";
menu.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, false);
menu.ChangeTab(3, false);
}));
__instance.RoleSettingsButton.OnMouseOver.AddListener(
menu.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, true);
menu.ChangeTab(3, true);
}));
}
else
{
__instance.RoleSettingsButton.buttonText.text = "Roles Settings";
__instance.RoleSettingsButton.OnClick.AddListener(
menu.RoleSettingsButton.buttonText.text = "Roles Settings";
menu.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, false);
menu.ChangeTab(2, false);
}));
__instance.RoleSettingsButton.OnMouseOver.AddListener(
menu.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, true);
menu.ChangeTab(2, true);
}));
}
}
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (_text is not null && SelectedModIdx == 0)
{
_text.text = "Default";
_text.text = $"<size=40%>(Page 0/{MiraPluginManager.Instance.RegisteredPlugins.Length})</size>\nDefault";
_text.fontSizeMax = 3.2f;
SelectedMod = null;
}
else if (_text is not null)
{
_text.fontSizeMax = 2.3f;
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
SelectedMod = MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
var name = SelectedMod.MiraPlugin.OptionsTitleText;
_text.text = name[..Math.Min(name.Length, 25)];
_text.text = $"<size=50%>(Page {SelectedModIdx}/{MiraPluginManager.Instance.RegisteredPlugins.Length})</size>\n" + name[..Math.Min(name.Length, 25)];
}
bool replaceWithModifiers = true;
@@ -268,9 +308,11 @@ internal static class GameSettingMenuPatches
if (SelectedModIdx != 0)
{
var modHasRoles = SelectedMod!.CustomRoles.Count != 0;
var modHasModifiers = SelectedMod.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions = SelectedMod.OptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
var modHasRoles = SelectedMod!.InternalRoles.Count != 0;
var modHasModifiers = SelectedMod.InternalOptionGroups.Exists(
x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true);
var modHasOptions =
SelectedMod.InternalOptionGroups.Exists(x => x.OptionableType == null && !x.ShowInModifiersMenu);
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
@@ -310,6 +352,7 @@ internal static class GameSettingMenuPatches
{
menu.ChangeTab(0, false);
}
// If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles)
{
@@ -366,6 +409,11 @@ internal static class GameSettingMenuPatches
ChangeRoleSettingButton(replaceWithModifiers, menu);
}
if (menu.PresetsTab.gameObject.active)
{
menu.ChangeTab(0, false);
}
CleanupTab(settings, roles);
}
@@ -377,13 +425,30 @@ internal static class GameSettingMenuPatches
{
continue;
}
child.gameObject.DestroyImmediate();
}
options.Clear();
}
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (roles.roleChances != null)
{
CleanupRoleSettings(roles);
}
if (settings.Children != null)
{
CleanupSettings(settings);
}
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab, true);
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{
if (rolesMenu.advancedSettingChildren != null)
@@ -402,15 +467,24 @@ internal static class GameSettingMenuPatches
{
role.gameObject.DestroyImmediate();
}
rolesMenu.roleChances?.Clear();
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
rolesMenu.SetQuotaTab();
rolesMenu.scrollBar.ScrollToTop();
if (RoleSettingMenuPatches.RolePositions.TryGetValue(SelectedModIdx, out var pos))
{
rolesMenu.scrollBar.Inner.localPosition = pos;
rolesMenu.scrollBar.UpdateScrollBars();
}
else
{
rolesMenu.scrollBar.ScrollToTop();
}
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
void CleanupSettings(GameOptionsMenu gameOptMenu, bool modifiers = false)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
@@ -421,22 +495,16 @@ internal static class GameSettingMenuPatches
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
gameOptMenu.scrollBar.ScrollToTop();
}
if (roles.roleChances != null && SelectedMod?.CustomRoles.Count > 0)
{
CleanupRoleSettings(roles);
}
if (settings.Children != null && SelectedMod?.OptionGroups.Count > 0)
{
CleanupSettings(settings);
}
if (_modifiersTab?.Children?.Count > 0)
{
CleanupSettings(_modifiersTab);
var positions = modifiers ? ModifiersPositions : OptionsPositions;
if (positions.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
}
}
}
+42 -19
View File
@@ -24,7 +24,7 @@ public static class LobbyViewPanePatches
private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0
? null
: MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
: MiraPluginManager.Instance.RegisteredPlugins[SelectedModIdx - 1];
private static PassiveButton? ModifiersTabButton { get; set; }
@@ -74,7 +74,7 @@ public static class LobbyViewPanePatches
(UnityAction)(() =>
{
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{
SelectedModIdx = 0;
}
@@ -98,7 +98,7 @@ public static class LobbyViewPanePatches
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
}
Refresh(__instance);
@@ -113,24 +113,27 @@ public static class LobbyViewPanePatches
menu.scrollBar.ScrollToTop();
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))]
public static bool SetTabPatch(LobbyViewSettingsPane __instance)
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.ChangeTab))]
[HarmonyPatch(nameof(LobbyViewSettingsPane.RefreshTab))]
// CHANGED BECAUSE OF INLINING
public static void SetTabPatch(LobbyViewSettingsPane __instance)
{
if (__instance.currentTab != ModifiersTabName || SelectedMod == null)
{
ModifiersTabButton?.SelectButton(false);
return true;
return;
}
__instance.taskTabButton.SelectButton(false);
__instance.rolesTabButton.SelectButton(false);
ModifiersTabButton?.SelectButton(true);
var filteredGroups = SelectedMod.OptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || (x.OptionableType != null && x.OptionableType.IsAssignableTo(typeof(BaseModifier)))));
DrawOptions(__instance, filteredGroups);
return false;
}
[HarmonyPrefix]
@@ -142,8 +145,14 @@ public static class LobbyViewPanePatches
return true;
}
var filteredGroups = SelectedMod.OptionGroups
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
if (__instance.currentTab == ModifiersTabName)
{
return false;
}
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x is { ShowInModifiersMenu: false, OptionableType: null } && x.GroupVisible());
DrawOptions(__instance, filteredGroups);
return false;
}
@@ -157,6 +166,11 @@ public static class LobbyViewPanePatches
return true;
}
if (__instance.currentTab == ModifiersTabName)
{
return false;
}
DrawRolesTab(__instance);
return false;
}
@@ -165,12 +179,15 @@ public static class LobbyViewPanePatches
{
var num = 1.44f;
foreach (var group in groups)
var groupArray = groups.Where(x => x.GroupVisible() && x.Options.Any(y => y.Visible())).ToArray();
foreach (var group in groupArray)
{
var categoryHeaderMasked = Object.Instantiate(
menu.categoryHeaderOrigin,
menu.settingsContainer,
true);
categoryHeaderMasked.SetHeader(StringNames.Name, 61);
categoryHeaderMasked.Title.text = group.GroupName;
categoryHeaderMasked.transform.localScale = Vector3.one;
@@ -182,7 +199,7 @@ public static class LobbyViewPanePatches
foreach (var option in group.Options)
{
if (!option.Visible.Invoke())
if (!option.Visible())
{
continue;
}
@@ -191,6 +208,7 @@ public static class LobbyViewPanePatches
menu.infoPanelOrigin,
menu.settingsContainer,
true);
viewSettingsInfoPanel.transform.localScale = Vector3.one;
float num2;
if (i % 2 == 0)
@@ -229,7 +247,7 @@ public static class LobbyViewPanePatches
num -= 0.85f;
}
menu.scrollBar.CalculateAndSetYBounds(menu.settingsInfo.Count + 10, 2f, 6f, 0.85f);
menu.scrollBar.SetYBoundsMax(-num - 2);
}
private static void DrawRolesTab(LobbyViewSettingsPane instance)
@@ -250,7 +268,7 @@ public static class LobbyViewPanePatches
var list = new List<Type>();
var roleGroups = SelectedMod.CustomRoles.Values.OfType<ICustomRole>()
var roleGroups = SelectedMod.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
// sort the groups by priority
@@ -291,6 +309,11 @@ public static class LobbyViewPanePatches
foreach (var customRole in grouping)
{
if (customRole.Configuration.HideSettings)
{
continue;
}
var roleBehaviour = customRole as RoleBehaviour;
if (roleBehaviour == null)
{
@@ -310,7 +333,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
var advancedRoleOptions = SelectedMod.OptionGroups
var advancedRoleOptions = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == customRole.GetType())
.SelectMany(x => x.Options)
.ToList();
@@ -397,7 +420,7 @@ public static class LobbyViewPanePatches
return 0;
}
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
var role = SelectedMod.InternalRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
if (role == null)
{
@@ -420,7 +443,7 @@ public static class LobbyViewPanePatches
var num = viewPanel.yPosStart;
var num2 = 1.08f;
var filteredOptions = SelectedMod.OptionGroups
var filteredOptions = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == roleType)
.SelectMany(x=>x.Options)
.ToList();
@@ -2,6 +2,7 @@
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.Roles;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
@@ -23,15 +24,13 @@ public static class NotificationPopperPatch
return true;
}
var text = teamType == RoleTeamTypes.Crewmate
? Palette.CrewmateSettingChangeText.ToTextColor()
: Palette.ImpostorRed.ToTextColor();
var textColor = customRole.OptionsMenuColor.ToTextColor();
var item = TranslationController.Instance.GetString(
StringNames.LobbyChangeSettingNotificationRole,
string.Concat(
"<font=\"Barlow-Black SDF\" material=\"Barlow-Black Outline\">",
text,
textColor,
TranslationController.Instance.GetString(key, Array.Empty<Object>()),
"</color></font>"
),
+244 -22
View File
@@ -1,12 +1,161 @@
using HarmonyLib;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Il2CppSystem;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch]
public static class OptionsPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))]
public static bool RoleIncreaseChancePrefix(RoleOptionSetting __instance)
{
if (!__instance.Role.IsCustomRole())
{
return true;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 1;
}
var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
__instance.roleChance += increment;
if (__instance.roleChance > 100)
{
__instance.roleChance = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseChance))]
public static bool RoleDecreaseChancePrefix(RoleOptionSetting __instance)
{
if (!__instance.Role.IsCustomRole())
{
return true;
}
if (__instance.roleChance == 0)
{
__instance.roleMaxCount = 1;
}
var increment = Input.GetKey(KeyCode.LeftShift) ? 5 : 10;
__instance.roleChance -= increment;
if (__instance.roleChance < 0)
{
__instance.roleChance = 100;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseCount))]
public static bool RoleIncreaseCountPrefix(RoleOptionSetting __instance)
{
if (!__instance.Role.IsCustomRole())
{
return true;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 50;
}
__instance.roleMaxCount += 1;
if (__instance.roleMaxCount > __instance.role.MaxCount)
{
__instance.roleMaxCount = 0;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseCount))]
public static bool RoleDecreaseCountPrefix(RoleOptionSetting __instance)
{
if (!__instance.Role.IsCustomRole())
{
return true;
}
if (__instance.roleMaxCount == 0)
{
__instance.roleChance = 50;
}
__instance.roleMaxCount -= 1;
if (__instance.roleMaxCount < 0)
{
__instance.roleMaxCount = __instance.role.MaxCount;
}
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.AdjustButtonsActiveState))]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustChanceButtonsActiveState))]
[HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.AdjustCountButtonsActiveState))]
public static bool AdjustButtonsPrefix(OptionBehaviour __instance)
{
if (__instance.IsCustom())
{
if (__instance.TryCast<NumberOption>() is { } numberOption)
{
numberOption.MinusBtn.SetInteractable(true);
numberOption.PlusBtn.SetInteractable(true);
}
if (__instance.TryCast<StringOption>() is { } stringOption)
{
stringOption.MinusBtn.SetInteractable(true);
stringOption.PlusBtn.SetInteractable(true);
}
if (__instance.TryCast<PlayerOption>() is { } playerOption)
{
playerOption.MinusBtn.SetInteractable(true);
playerOption.PlusBtn.SetInteractable(true);
}
return false;
}
if (__instance.TryCast<RoleOptionSetting>() is { } roleOptionSetting && roleOptionSetting.Role.IsCustomRole())
{
roleOptionSetting.CountMinusBtn.SetInteractable(true);
roleOptionSetting.CountPlusBtn.SetInteractable(true);
roleOptionSetting.ChanceMinusBtn.SetInteractable(true);
roleOptionSetting.ChancePlusBtn.SetInteractable(true);
return false;
}
return true;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Initialize))]
public static bool ToggleInit(ToggleOption __instance)
@@ -16,15 +165,31 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text =
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.UpdateValue))]
[HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Toggle))]
// UpdateValue was inlined.
public static bool ToggleUpdate(ToggleOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.CheckMark.enabled = !__instance.CheckMark.enabled;
__instance.OnValueChanged.Invoke(__instance);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
[HarmonyPatch(typeof(PlayerOption), nameof(PlayerOption.UpdateValue))]
public static bool UpdateValuePrefix(OptionBehaviour __instance)
{
return !__instance.IsCustom();
}
@@ -38,17 +203,84 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text =
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.UpdateValue))]
public static bool NumberUpdate(NumberOption __instance)
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))]
public static bool NumberIncrease(NumberOption __instance)
{
return !__instance.IsCustom();
if (!__instance.IsCustom())
{
return true;
}
__instance.Value += __instance.Increment;
if (__instance.Value > __instance.ValidRange.max)
{
__instance.Value = __instance.ValidRange.min;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))]
public static bool NumberDecrease(NumberOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value -= __instance.Increment;
if (__instance.Value < __instance.ValidRange.min)
{
__instance.Value = __instance.ValidRange.max;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))]
public static bool StringOptionIncrease(StringOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value += 1;
if (__instance.Value >= __instance.Values.Length)
{
__instance.Value = 0;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))]
public static bool StringOptionDecrease(StringOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
__instance.Value -= 1;
if (__instance.Value < 0)
{
__instance.Value = __instance.Values.Length - 1;
}
__instance.OnValueChanged.Invoke(__instance);
__instance.AdjustButtonsActiveState();
return false;
}
[HarmonyPrefix]
@@ -60,19 +292,9 @@ public static class OptionsPatches
return true;
}
__instance.TitleText.text =
TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = TranslationController.Instance.GetString(
__instance.Values[__instance.Value],
Array.Empty<Object>());
__instance.TitleText.text = TranslationController.Instance.GetString(__instance.Title, Array.Empty<Object>());
__instance.ValueText.text = TranslationController.Instance.GetString(__instance.Values[__instance.Value], Array.Empty<Object>());
return false;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(StringOption), nameof(StringOption.UpdateValue))]
public static bool StringUpdate(StringOption __instance)
{
return !__instance.IsCustom();
}
}
@@ -0,0 +1,128 @@
using System;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Utilities;
using Reactor.Utilities;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(PlayerOption))]
public static class PlayerOptionPatches
{
// Updates the list of players according to the filter.
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerOption.FixedUpdate))]
public static bool PlayerOptionUpdatePatch(PlayerOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
if (!ModdedOptionsManager.CreatedPlayerOptions.TryGetValue(__instance, out var moddedPlrOpt)) return false;
var filteredPlayers = moddedPlrOpt.GetFilteredPlayers();
if (filteredPlayers.Count != __instance.Values.Count)
{
filteredPlayers.Sort((a, b) => a.PlayerId.CompareTo(b.PlayerId));
__instance.Values = filteredPlayers.ToIl2CppList();
moddedPlrOpt.Values = __instance.Values;
__instance.playerIndex = filteredPlayers.FindIndex(p => p.PlayerId == __instance.Value);
__instance.SetValueText();
}
__instance.CheckValueChanged();
return false;
}
// Disable going under 0 if you aren't allowed to choose None.
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerOption.UpdatePlayerIndex))]
public static bool UpdatePlayerIndexPatch(PlayerOption __instance, int index)
{
if (!__instance.IsCustom() || ModdedOptionsManager.CreatedPlayerOptions[__instance].AllowNone)
{
return true;
}
var newIndex = Mathf.Clamp(index, 0, __instance.Values.Count - 1);
__instance.playerIndex = newIndex;
__instance.Value = __instance.Values[newIndex].PlayerId;
return false;
}
// Replace "Round Robin" with "None" in the Host Options Menu.
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerOption.SetValueText))]
public static void SetValueTextPatch(PlayerOption __instance)
{
if (__instance.playerIndex < 0)
{
__instance.ValueText.text = TranslationController.Instance.GetString(StringNames.FilterNone);
}
}
// Innersloth hardcode literally replaces the value with the HNS impostor option OnEnable, so I have to patch that out. Smh.
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerOption.OnEnable))]
public static bool OnEnablePatch(PlayerOption __instance)
{
return !__instance.IsCustom();
}
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerOption.GetInt))]
public static bool GetIntPatch(PlayerOption __instance, ref int __result)
{
if (!__instance.IsCustom())
{
return true;
}
__result = __instance.Value;
return false;
}
// Used to replace "Round Robin" with "None" for option notification & view pane.
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerSelectionGameSetting), nameof(PlayerSelectionGameSetting.GetValueString))]
public static bool GetStringPatch(PlayerSelectionGameSetting __instance, float value, ref string __result)
{
var option = ModdedOptionsManager.ModdedOptions.FirstOrDefault(x => x.Value.Data == __instance).Value;
if (option == null) return true;
var networkedPlayerInfo = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(p => p.PlayerId == (int)value);
if (networkedPlayerInfo != null)
{
__result = networkedPlayerInfo.PlayerName;
return false;
}
__result = TranslationController.Instance.GetString(StringNames.FilterNone);
return false;
}
// "UpdateValue" was inlined and I needed to replace it, so I just rewrote the method where it's called from.
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerOption.CheckValueChanged))]
public static bool UpdateValuePatch(PlayerOption __instance)
{
if (!__instance.IsCustom())
{
return true;
}
if (__instance.oldValue == __instance.Value) return false;
__instance.oldValue = __instance.Value;
__instance.SetValueText();
return false;
}
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Utilities;
@@ -22,20 +23,34 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(RolesSettingsMenu))]
public static class RoleSettingMenuPatches
{
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; set; } = new();
private static List<GameObject> Headers { get; set; } = new();
private static List<RoleOptionSetting> RoleOptionSettings { get; set; } = new();
public static Dictionary<int, Vector3> RolePositions { get; set; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; set; } = [];
private static List<GameObject> Headers { get; set; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; set; } = [];
private static float ScrollerNum { get; set; } = 0.522f;
private static RoleBehaviour? CurrentRole { get; set; }
private static List<IModdedOption>? CurrentRoleOptions { get; set; }
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static void SetQuotaTabPostfix(RolesSettingsMenu __instance)
{
__instance.scrollBar.SetScrollBounds(__instance);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool PatchStart(RolesSettingsMenu __instance)
public static bool SetQuotaTabPatch(RolesSettingsMenu __instance)
{
Headers.ForEach(Object.Destroy);
RoleOptionSettings.ForEach(Object.Destroy);
Headers.Clear();
RoleOptionSettings.Clear();
CurrentRole = null;
CurrentRoleOptions = null;
__instance.roleChances = new Il2CppSystem.Collections.Generic.List<RoleOptionSetting>();
__instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List<OptionBehaviour>();
@@ -67,7 +82,7 @@ public static class RoleSettingMenuPatches
var num3 = 0;
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
var roleGroups = GameSettingMenuPatches.SelectedMod?.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (roleGroups is null)
@@ -202,9 +217,10 @@ public static class RoleSettingMenuPatches
headerBtn.OnClick.AddListener(
(UnityAction)(() =>
{
if (RoleGroupHidden.TryGetValue(group, out var value))
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
if (RoleGroupHidden.TryGetValue(group, out var groupHidden))
{
RoleGroupHidden[group] = !value;
RoleGroupHidden[group] = !groupHidden;
}
foreach (var header in Headers)
{
@@ -225,33 +241,72 @@ public static class RoleSettingMenuPatches
ScrollerNum -= 0.4f;
}
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.SetScrollBounds(__instance);
return false;
}
private static void SetScrollBounds(this Scroller scroller)
private static void SetScrollBounds(this Scroller scroller, RolesSettingsMenu roleMenu)
{
scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
scroller.CalculateAndSetYBounds(roleMenu.roleChances.Count + 3, 1f, 6f, 0.43f);
return;
}
scroller.SetYBoundsMax(-ScrollerNum - 2);
if (RolePositions.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
{
scroller.Inner.localPosition = scroll;
scroller.UpdateScrollBars();
}
else
{
scroller.ScrollToTop();
}
}
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.Update))]
public static bool UpdatePatch()
public static bool UpdatePatch(RolesSettingsMenu __instance)
{
return GameSettingMenuPatches.SelectedModIdx == 0;
if (GameSettingMenuPatches.SelectedModIdx == 0) return true;
if (CurrentRole == null || CurrentRoleOptions == null) return false;
var hasImage = CurrentRole.RoleScreenshot != null;
var num = hasImage ? -0.872f : -1;
foreach (var opt in CurrentRoleOptions)
{
if (opt.OptionBehaviour == null) continue;
if (!opt.Visible.Invoke())
{
opt.OptionBehaviour.gameObject.SetActive(false);
continue;
}
opt.OptionBehaviour.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
opt.OptionBehaviour.gameObject.SetActive(true);
num += -0.45f;
}
__instance.scrollBar.SetYBoundsMax(-num - 3);
return false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar)
if (!__instance.scrollBar)
{
return;
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.ScrollToTop();
CurrentRole = null;
CurrentRoleOptions = null;
__instance.scrollBar.SetScrollBounds(__instance);
}
private static void ValueChanged(OptionBehaviour obj)
@@ -303,26 +358,26 @@ public static class RoleSettingMenuPatches
optBehaviour.gameObject.DestroyImmediate();
}
CurrentRole = role;
__instance.advancedSettingChildren.Clear();
var hasImage = role.RoleScreenshot != null;
var num = hasImage ? -0.872f : -1;
// TODO: create sub groups under the role settings.
var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x=> x.OptionableType == role.GetType())
.SelectMany(x=>x.Options)
var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x => x.GroupVisible() && x.OptionableType == role.GetType())
.SelectMany(x => x.Options)
.ToList() ?? [];
CurrentRoleOptions = filteredOptions;
foreach (var option in filteredOptions)
{
var newOpt = option.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
__instance.stringOptionOrigin,
GameSettingMenu.Instance.GameSettingsTab.playerOptionOrigin,
__instance.AdvancedRolesSettings.transform);
newOpt.transform.localPosition = new Vector3(hasImage ? 2.17f : 1.1f, num, -2f);
newOpt.SetClickMask(__instance.ButtonClickMask);
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren<SpriteRenderer>(true);
@@ -340,11 +395,10 @@ public static class RoleSettingMenuPatches
newOpt.LabelBackground.enabled = false;
__instance.advancedSettingChildren.Add(newOpt);
num += -0.45f;
newOpt.gameObject.SetActive(false);
newOpt.Initialize();
}
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
__instance.scrollBar.ScrollToTop();
}
@@ -356,6 +410,8 @@ public static class RoleSettingMenuPatches
return;
}
RolePositions[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = TranslationController.Instance.GetString(
role.StringName,
@@ -466,6 +522,10 @@ public static class RoleSettingMenuPatches
roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor;
roleOptionSetting.OnValueChanged = new Action<OptionBehaviour>(ValueChanged);
roleOptionSetting.SetClickMask(__instance.ButtonClickMask);
roleOptionSetting.ChanceMinusBtn.SetInteractable(true);
roleOptionSetting.ChancePlusBtn.SetInteractable(true);
roleOptionSetting.CountMinusBtn.SetInteractable(true);
roleOptionSetting.CountPlusBtn.SetInteractable(true);
__instance.roleChances.Add(roleOptionSetting);
roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f);
@@ -473,8 +533,8 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.OptionGroups
.Exists(x => x.OptionableType == role.GetType()))
GameSettingMenuPatches.SelectedMod.InternalOptionGroups
.Exists(x => x.GroupVisible() && x.OptionableType == role.GetType() && x.Options.Any(y => y.Visible())))
{
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
@@ -505,7 +565,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.ChancePlusBtn.gameObject.SetActive(false);
}
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1)
if (index < GameSettingMenuPatches.SelectedMod?.InternalRoles.Count - 1)
{
ScrollerNum -= 0.43f;
}
+15 -11
View File
@@ -5,8 +5,9 @@ using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches;
@@ -41,7 +42,7 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var deathEvent = new PlayerDeathEvent(__instance, reason);
var deathEvent = new PlayerDeathEvent(__instance, reason, Helpers.GetBodyById(__instance.PlayerId));
MiraEventManager.InvokeEvent(deathEvent);
var modifiersComponent = __instance.GetComponent<ModifierComponent>();
@@ -68,6 +69,7 @@ internal static class PlayerControlPatches
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
// ReSharper disable once InconsistentNaming
// Note: This method is partially inlined in 2025.5.20 but we already use custom murder RPCs.
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
{
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
@@ -81,11 +83,6 @@ internal static class PlayerControlPatches
// ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{
if (__instance.Data?.Role is ICustomRole customRole)
{
customRole.PlayerControlFixedUpdate(__instance);
}
if (!__instance.AmOwner)
{
return;
@@ -98,12 +95,19 @@ internal static class PlayerControlPatches
continue;
}
if (!button.Enabled(__instance.Data?.Role))
try
{
continue;
}
if (!button.Enabled(__instance.Data?.Role))
{
continue;
}
button.FixedUpdateHandler(__instance);
button.FixedUpdateHandler(__instance);
}
catch (System.Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to update custom button {button.GetType().Name}: {e}");
}
}
}
}
+1 -3
View File
@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Meeting;
+6 -2
View File
@@ -10,7 +10,7 @@ public static class GameStartupPatch
private static bool _runOnce;
/// <summary>
/// This is used for registering roles when the game opens, might be a janky solution, but it works
/// This is used for registering roles when the game opens, might be a janky solution, but it works.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(MainMenuManager.Start))]
@@ -23,7 +23,11 @@ public static class GameStartupPatch
_runOnce = true;
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return;
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0)
{
return;
}
foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations)
{
CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key);
+3 -2
View File
@@ -7,7 +7,8 @@ namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches the haunt menu to show the actual role name rather then team.
/// </summary>
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetFilterText))]
[HarmonyPatch(typeof(HauntMenuMinigame), nameof(HauntMenuMinigame.SetHauntTarget))]
// SetFilterText was INLINED
public static class HauntMenuMinigamePatch
{
public static void Postfix(HauntMenuMinigame __instance)
@@ -24,4 +25,4 @@ public static class HauntMenuMinigamePatch
__instance.FilterText.text = role.NiceName;
__instance.FilterText.color = color;
}
}
}
@@ -62,8 +62,6 @@ public static class HudManagerPatches
if (role is ICustomRole { Configuration.RoleHintType: RoleHintType.RoleTab } customRole)
{
customRole.HudUpdate(__instance);
if (_roleTab == null)
{
_roleTab = CustomRoleManager.CreateRoleTab(customRole);
+3 -1
View File
@@ -14,6 +14,8 @@ public static class RoleManagerPatches
{
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleManager.SetRole))]
// NOTE: As of 2025.5.20 update, this is INLINED in ONE place: GameManager::ReviveEveryoneFreeplay(void).
// Should not affect normal gameplay, but may cause issues in Freeplay.
public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
{
if (!targetPlayer)
@@ -63,7 +65,7 @@ public static class RoleManagerPatches
[HarmonyPatch(nameof(RoleManager.SelectRoles))]
public static void ModifierSelectionPatches(RoleManager __instance)
{
if (!AmongUsClient.Instance.AmHost)
if (!AmongUsClient.Instance.AmHost || !ModifierManager.MiraAssignsModifiers)
{
return;
}
@@ -1,5 +1,6 @@
using AmongUs.GameOptions;
using HarmonyLib;
using Il2CppSystem.Collections.Generic;
using MiraAPI.Roles;
using Reactor.Utilities;
@@ -8,15 +9,32 @@ namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches to return the correct role counts.
/// </summary>
[HarmonyPatch(typeof(RoleOptionsCollectionV08))]
[HarmonyPatch(typeof(RoleOptionsCollectionV09))]
public static class RoleOptionsCollectionPatch
{
/// <summary>
/// Set the role chance for custom Launchpad roles based on config
/// This patch fixes GetNumPerGame being inlined (2025.5.20) in the original code.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.AnyRolesEnabled))]
public static bool AnyRolesEnabledPrefix(RoleOptionsCollectionV09 __instance, ref bool __result)
{
foreach (KeyValuePair<RoleTypes, RoleDataV09> keyValuePair in __instance.roles)
{
if (__instance.GetNumPerGame(keyValuePair.Key) > 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Set the role chance for custom Launchpad roles based on config.
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV08.GetChancePerGame))]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.GetChancePerGame))]
public static bool GetChancePrefix(RoleTypes role, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null)
@@ -42,11 +60,11 @@ public static class RoleOptionsCollectionPatch
}
/// <summary>
/// Set the amount for custom Launchpad roles based on config
/// Set the amount for custom Launchpad roles based on config.
/// </summary>
/// <returns>Return false to skip original method, true to not.</returns>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleOptionsCollectionV08.GetNumPerGame))]
[HarmonyPatch(nameof(RoleOptionsCollectionV09.GetNumPerGame))]
public static bool GetNumPrefix(RoleTypes role, ref int __result)
{
if (!CustomRoleManager.GetCustomRoleBehaviour(role, out var customRole) || customRole == null)
+1 -1
View File
@@ -4,7 +4,7 @@ using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(SabotageButton))]
public class SabotageButtonPatch
public static class SabotageButtonPatch
{
/// <summary>
/// Patches the Sabotage button to check if the player's custom role can use sabotage.
+52
View File
@@ -0,0 +1,52 @@
using HarmonyLib;
using UnityEngine;
using UnityEngine.Events;
namespace MiraAPI.Patches;
[HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))]
public static class ServerDropdownPatch
{
public static bool Prefix(ServerDropdown __instance)
{
var num = 0;
__instance.background.size = new Vector2(8.4f, 4.8f);
foreach (var regionInfo in DestroyableSingleton<ServerManager>.Instance.AvailableRegions)
{
if (DestroyableSingleton<ServerManager>.Instance.CurrentRegion.Equals(regionInfo))
{
__instance.defaultButtonSelected = __instance.firstOption;
__instance.firstOption.ChangeButtonText(
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
regionInfo.TranslateName,
regionInfo.Name));
}
else
{
var region = regionInfo;
var serverListButton = __instance.ButtonPool.Get<ServerListButton>();
var x = num % 2 == 0 ? -2 : 2;
var y = -0.55f * (num / 2);
serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f);
serverListButton.transform.localScale = Vector3.one;
serverListButton.Text.text =
DestroyableSingleton<TranslationController>.Instance.GetStringWithDefault(
regionInfo.TranslateName,
regionInfo.Name);
serverListButton.Text.ForceMeshUpdate();
serverListButton.Button.OnClick.RemoveAllListeners();
serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); }));
__instance.controllerSelectable.Add(serverListButton.Button);
__instance.background.transform.localPosition = new Vector3(
0f,
__instance.initialYPos + (-0.3f * (num / 2)),
0f);
__instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2)));
num++;
}
}
return false;
}
}
-11
View File
@@ -33,15 +33,4 @@ public static class MinigameStubs
{
// nothing needed
}
/// <summary>
/// Fixes the error when creating custom minigames. Credit to Submerged for discovering this fix.
/// </summary>
/// <param name="__instance">The Minigame instance.</param>
[HarmonyPrefix]
[HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))]
public static void MinigameErrorFixPatch(Minigame __instance)
{
__instance.logger ??= new Logger("Minigame", Logger.Level.Info, Logger.Category.Gameplay);
}
}
+10 -79
View File
@@ -140,84 +140,6 @@ internal static class MeetingHudPatches
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.CastVote))]
public static bool CastVotePatch(MeetingHud __instance, byte srcPlayerId, byte suspectPlayerId)
{
var plr = GameData.Instance.GetPlayerById(srcPlayerId);
if (plr == null)
{
return false;
}
var voteData = plr.Object.GetVoteData();
if (voteData == null || voteData.VotesRemaining == 0 ||
voteData.VotedFor(suspectPlayerId))
{
return false;
}
VotingUtils.HandleVote(voteData, suspectPlayerId);
__instance.SetDirtyBit(1U);
__instance.CheckForEndVoting();
if (voteData.VotesRemaining != 0)
{
return false;
}
__instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId);
PlayerControl.LocalPlayer.RpcSendChatNote(srcPlayerId, ChatNoteTypes.DidVote);
return false;
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.CmdCastVote))]
public static void CmdCastVotePatch(MeetingHud __instance, byte playerId, byte suspectIdx)
{
var plr = GameData.Instance.GetPlayerById(playerId);
var voteData = plr.Object.GetVoteData();
if (!AmongUsClient.Instance.AmHost)
{
if (voteData == null || voteData.VotesRemaining == 0 ||
voteData.VotedFor(suspectIdx))
{
return;
}
VotingUtils.HandleVote(voteData, suspectIdx);
if (voteData.VotesRemaining == 0)
{
__instance.playerStates.First(x => x.TargetPlayerId == playerId).SetVote(suspectIdx);
}
}
if (PlayerControl.LocalPlayer.PlayerId != playerId)
{
return;
}
SoundManager.Instance.PlaySound(__instance.VoteLockinSound, false);
foreach (var playerVoteArea in __instance.playerStates)
{
playerVoteArea.ClearButtons();
}
__instance.SkipVoteButton.ClearButtons();
var localVoteData = PlayerControl.LocalPlayer.GetVoteData();
if (localVoteData == null || localVoteData.VotesRemaining != 0) return;
__instance.SkipVoteButton.voteComplete = true;
__instance.SkipVoteButton.gameObject.SetActive(false);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.CheckForEndVoting))]
public static bool EndCheck(MeetingHud __instance)
@@ -280,12 +202,21 @@ internal static class MeetingHudPatches
return false;
}
// TODO: figure out a way to do host-authorization since right now any player can send RpcCastVote
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.CmdCastVote))]
// Although this method is inlined in MeetingHud.Confirm, the next patch fixes that.
public static bool CmdCastVoteOverridePatch(MeetingHud __instance, byte playerId, byte suspectIdx)
{
VotingUtils.RpcCastVote(PlayerControl.LocalPlayer, playerId, suspectIdx);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Confirm))]
public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect)
{
__instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, suspect);
return false;
}
}
+8
View File
@@ -0,0 +1,8 @@
using System;
namespace MiraAPI.PluginLoading;
public interface IMiraExtension
{
public Type GetBasePluginType();
}
+11
View File
@@ -2,8 +2,19 @@
namespace MiraAPI.PluginLoading;
/// <summary>
/// The interface that all Mira plugins must implement.
/// </summary>
public interface IMiraPlugin
{
/// <summary>
/// Gets the name to display on the options menu.
/// </summary>
string OptionsTitleText { get; }
/// <summary>
/// Gets the BepInEx configuration file for the plugin.
/// </summary>
/// <returns>The BepInEx configuration file for the plugin.</returns>
public ConfigFile GetConfigFile();
}
+30 -31
View File
@@ -6,7 +6,9 @@ using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace MiraAPI.PluginLoading;
/// <summary>
@@ -23,61 +25,58 @@ public class MiraPluginInfo
}
/// <summary>
/// Get a read only collection of this plugin's modifiers. This is probably not safe because they are uninitialized.
/// Gets a read only collection of this plugin's modifiers. This is not safe for modifiers with constructors.
/// </summary>
/// <returns>Readonly collection of modifiers.</returns>
public IReadOnlyCollection<BaseModifier> GetModifiers()
{
return Modifiers.AsReadOnly();
}
public IReadOnlyCollection<BaseModifier> Modifiers { get; private set; } = null!;
/// <summary>
/// Get a read only collection of this plugin's Option Groups.
/// Gets a read only collection of this plugin's Option Groups.
/// </summary>
/// <returns>Readonly collection of option groups.</returns>
public IReadOnlyCollection<AbstractOptionGroup> GetOptionGroups()
{
return OptionGroups.AsReadOnly();
}
public IReadOnlyCollection<AbstractOptionGroup> OptionGroups { get; private set; } = null!;
/// <summary>
/// Gets a read only collection of this plugin's options.
/// </summary>
/// <returns>Read only collection of options.</returns>
public IReadOnlyCollection<IModdedOption> GetOptions()
{
return Options.AsReadOnly();
}
public IReadOnlyCollection<IModdedOption> Options { get; private set; } = null!;
/// <summary>
/// Gets a read only dictionary of Role IDs and the RoleBehaviour object they are associated with.
/// </summary>
/// <returns>Read only dictionary of IDs and Roles.</returns>
public ReadOnlyDictionary<ushort, RoleBehaviour> GetRoles()
{
return new ReadOnlyDictionary<ushort, RoleBehaviour>(CustomRoles);
}
public ReadOnlyDictionary<ushort, RoleBehaviour> Roles { get; private set; } = null!;
/// <summary>
/// Gets a read only collection of this plugin's custom buttons.
/// </summary>
/// <returns>Read only collection of buttons.</returns>
public IReadOnlyCollection<CustomActionButton> GetButtons()
public IReadOnlyCollection<CustomActionButton> Buttons { get; private set; } = null!;
/// <summary>
/// Gets a read only dictionary of this plugin's custom game modes.
/// </summary>
public IReadOnlyCollection<OptionPreset> Presets { get; internal set; } = null!;
internal void SavePublicCollections()
{
return Buttons.AsReadOnly();
Presets = [..InternalPresets];
Modifiers = [..InternalModifiers];
OptionGroups = [..InternalOptionGroups];
Options = [..InternalOptions];
Roles = new ReadOnlyDictionary<ushort, RoleBehaviour>(InternalRoles);
Buttons = [..InternalButtons];
}
internal List<AbstractOptionGroup> OptionGroups { get; } = [];
internal List<OptionPreset> InternalPresets { get; } = [];
internal List<IModdedOption> Options { get; } = [];
internal List<AbstractOptionGroup> InternalOptionGroups { get; } = [];
internal List<BaseModifier> Modifiers { get; } = [];
internal List<IModdedOption> InternalOptions { get; } = [];
internal Dictionary<ushort, RoleBehaviour> CustomRoles { get; } = [];
internal List<BaseModifier> InternalModifiers { get; } = [];
internal Dictionary<int, CustomGameMode> GameModes { get; } = [];
internal Dictionary<ushort, RoleBehaviour> InternalRoles { get; } = [];
internal List<CustomActionButton> Buttons { get; } = [];
internal Dictionary<int, CustomGameMode> InternalGameModes { get; } = [];
internal List<CustomActionButton> InternalButtons { get; } = [];
/// <summary>
/// Gets the plugin's ID, as defined in the plugin's BepInEx metadata.
+120 -70
View File
@@ -12,6 +12,7 @@ using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Presets;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Networking;
@@ -26,8 +27,9 @@ public sealed class MiraPluginManager
{
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values];
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = new();
internal MiraPluginInfo[] RegisteredPlugins { get; private set; } = null!;
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = [];
internal static MiraPluginManager Instance { get; private set; } = new();
internal void Initialize()
@@ -35,83 +37,31 @@ public sealed class MiraPluginManager
Instance = this;
IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) =>
{
if (plugin is not IMiraPlugin miraPlugin)
if (plugin is IMiraPlugin miraPlugin)
{
return;
var miraInfo = new MiraPluginInfo(miraPlugin, pluginInfo);
RegisterPlugin(miraInfo, assembly);
_registeredPlugins.Add(assembly, miraInfo);
}
var info = new MiraPluginInfo(miraPlugin, pluginInfo);
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
else if (plugin is IMiraExtension miraExtension)
{
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
var basePlugin = miraExtension.GetBasePluginType();
var basePluginInfo = _registeredPlugins.Values.FirstOrDefault(x => x.MiraPlugin.GetType() == basePlugin);
if (basePluginInfo == null)
{
continue;
Logger<MiraApiPlugin>.Error($"Base plugin {basePlugin.Name} not found for extension {pluginInfo.Metadata.GUID}.");
return;
}
foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute != null)
{
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterRoleAttribute(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButtonAttribute(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
RegisterColorClasses(type);
RegisterPlugin(basePluginInfo, assembly);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += () =>
{
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
RegisteredPlugins = [.. _registeredPlugins.Values];
ModifierManager.Modifiers = new ReadOnlyCollection<BaseModifier>(ModifierManager.InternalModifiers);
};
}
@@ -125,6 +75,90 @@ public sealed class MiraPluginManager
return Instance._registeredPlugins.Values.FirstOrDefault(plugin => plugin.PluginId == pluginId);
}
private void RegisterPlugin(MiraPluginInfo info, Assembly assembly)
{
Logger<MiraApiPlugin>.Info($"Registering plugin {info.PluginId} with Mira API.");
var roles = new List<Type>();
var oldConfigSetting = info.PluginConfig.SaveOnConfigSet;
info.PluginConfig.SaveOnConfigSet = false;
foreach (var type in assembly.GetTypes())
{
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
{
continue;
}
foreach (var method in type.GetMethods())
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute == null)
{
continue;
}
if (!method.IsStatic)
{
Logger<MiraApiPlugin>.Error($"Event method {method.Name} in {type.Name} must be static.");
continue;
}
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
if (RegisterModifier(type, info))
{
continue;
}
if (RegisterOptions(type, info))
{
continue;
}
if (RegisterRole(type, info, out var role))
{
roles.Add(role);
continue;
}
if (RegisterButton(type, info))
{
continue;
}
if (RegisterGameOver(type))
{
continue;
}
RegisterColorClasses(type);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.TryAdd(info, []);
QueuedRoleRegistrations[info].AddRange(roles);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"{info.PluginId} finished registering.");
}
private static bool RegisterGameOver(Type type)
{
try
@@ -154,6 +188,12 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties())
{
if (property.GetMethod?.IsStatic == true)
{
Logger<MiraApiPlugin>.Error($"Option property {property.Name} in {type.Name} must not be static.");
continue;
}
if (property.PropertyType.IsAssignableTo(typeof(IModdedOption)))
{
ModdedOptionsManager.RegisterPropertyOption(type, property, pluginInfo);
@@ -169,16 +209,21 @@ public sealed class MiraPluginManager
ModdedOptionsManager.RegisterAttributeOption(type, attribute, property, pluginInfo);
}
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.");
}
return true;
}
catch (Exception e)
{
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e}");
Logger<MiraApiPlugin>.Error($"Failed to register options for {type.Name}: {e.ToString()}");
}
return false;
}
private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
private static bool RegisterRole(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
{
role = null;
try
@@ -234,6 +279,11 @@ public sealed class MiraPluginManager
PaletteManager.CustomColors.Add(color);
}
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.");
}
}
catch (Exception e)
{
@@ -254,7 +304,7 @@ public sealed class MiraPluginManager
}
}
private static bool RegisterButtonAttribute(Type type, MiraPluginInfo pluginInfo)
private static bool RegisterButton(Type type, MiraPluginInfo pluginInfo)
{
try
{
+74
View File
@@ -0,0 +1,74 @@
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Presets;
/// <summary>
/// Represents a preset of game options that can be applied to the game.
/// </summary>
public class OptionPreset
{
/// <summary>
/// Gets the name of the preset.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the plugin associated with the preset.
/// </summary>
public MiraPluginInfo Plugin { get; }
/// <summary>
/// Gets the configuration file of the plugin associated with the preset.
/// </summary>
public ConfigFile PluginConfig { get; }
/// <summary>
/// Gets the configuration file for the preset.
/// </summary>
public ConfigFile PresetConfig { get; }
/// <summary>
/// Gets or sets the button associated with the preset in the UI.
/// </summary>
public GameObject? PresetButton { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="OptionPreset"/> class with the specified name and configuration file.
/// </summary>
/// <param name="name">>The name of the preset.</param>
/// <param name="plugin">The plugin associated with the preset.</param>
/// <param name="presetConfig">>The configuration file for the preset.</param>
public OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetConfig)
{
Name = name;
Plugin = plugin;
PluginConfig = plugin.PluginConfig;
PresetConfig = presetConfig;
}
/// <summary>
/// Loads the preset by applying the values from the preset configuration to the plugin configuration.
/// </summary>
public void LoadPreset()
{
PresetConfig.Reload();
foreach (var option in Plugin.InternalOptions)
{
option.LoadFromPreset(PresetConfig);
}
ModdedOptionsManager.SyncAllOptions();
foreach (var role in Plugin.InternalRoles.Values.OfType<ICustomRole>())
{
role.LoadFromPreset(PresetConfig);
}
CustomRoleManager.SyncAllRoleSettings();
}
}
+96
View File
@@ -0,0 +1,96 @@
using System.IO;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Presets;
/// <summary>
/// Provides functionality to manage and load game option presets for plugins.
/// </summary>
public static class PresetManager
{
/// <summary>
/// Gets the directory where the presets are stored.
/// </summary>
public static string PresetDirectory { get; } = Path.GetFullPath("mira_presets", Application.persistentDataPath);
internal static void CreateDefaultPreset(MiraPluginInfo plugin)
{
var presetPath = Path.Combine(PresetDirectory, plugin.PluginId);
if (!Directory.Exists(presetPath))
{
Directory.CreateDirectory(presetPath);
}
var presetConfig = new ConfigFile(Path.Combine(presetPath, "Default.cfg"), false);
foreach (var option in plugin.InternalOptions.Where(x => x.IncludeInPreset))
{
option.SaveToPreset(presetConfig, true);
}
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>())
{
role.SaveToPreset(presetConfig);
}
presetConfig.Save();
}
/// <summary>
/// Loads the presets for the specified plugin by reading the configuration files from the preset directory.
/// </summary>
/// <param name="plugin">>The plugin for which the presets should be loaded.</param>
public static void LoadPresets(MiraPluginInfo plugin)
{
foreach (var btn in plugin.InternalPresets.Select(x=>x.PresetButton))
{
if (btn != null)
{
Object.DestroyImmediate(btn);
}
}
plugin.InternalPresets.Clear();
if (!Directory.Exists(PresetDirectory))
{
Directory.CreateDirectory(PresetDirectory);
}
var pluginPresetPath = Path.Combine(PresetDirectory, plugin.PluginId);
if (!Directory.Exists(pluginPresetPath))
{
Directory.CreateDirectory(pluginPresetPath);
}
foreach (var file in Directory.GetFiles(pluginPresetPath, "*.cfg"))
{
var fileName = Path.GetFileName(file);
Logger<MiraApiPlugin>.Info($"Loading preset file {fileName}");
var presetName = Path.GetFileNameWithoutExtension(file);
var presetConfig = new ConfigFile(file, false)
{
SaveOnConfigSet = false,
};
foreach (var option in plugin.InternalOptions.Where(x=>x.IncludeInPreset))
{
option.Bind(presetConfig);
}
foreach (var role in plugin.InternalRoles.Values.OfType<ICustomRole>().Where(x=>!x.Configuration.HideSettings))
{
role.BindConfig(presetConfig);
}
presetConfig.Save();
plugin.InternalPresets.Add(new OptionPreset(presetName, plugin, presetConfig));
}
plugin.Presets = [..plugin.InternalPresets];
}
}
+98
View File
@@ -0,0 +1,98 @@
using System;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
[RegisterInIl2Cpp]
#pragma warning disable S3903
#pragma warning disable CA1050
public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr)
#pragma warning restore CA1050
#pragma warning restore S3903
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
private TextMeshPro textBoxText;
private TextBoxTMP textBox;
private Action<string> onSave;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
// Cleanup holder object
public void OnDestroy()
{
transform.parent.gameObject.Destroy();
}
public override void Close()
{
// no-op
}
private void Awake()
{
var textboxHolder = transform.GetChild(1).GetChild(1);
var saveButton = transform.FindChild("SaveButton").GetComponent<PassiveButton>();
var closeButton = transform.FindChild("CloseButton").GetComponent<PassiveButton>();
textBox = textboxHolder.GetChild(0).GetComponent<TextBoxTMP>();
textBoxText = textBox.transform.GetChild(0).GetComponent<TextMeshPro>();
saveButton.OnClick = new Button.ButtonClickedEvent();
saveButton.OnClick.AddListener((UnityAction)(() =>
{
onSave.Invoke(textBoxText.text);
this.BaseClose();
}));
closeButton.OnClick = new Button.ButtonClickedEvent();
closeButton.OnClick.AddListener((UnityAction)(() =>
{
this.BaseClose();
}));
textBox.OnChange = new Button.ButtonClickedEvent();
textBox.OnChange.AddListener((UnityAction)(() =>
{
textBoxText.text = textBoxText.text.Replace(" ", string.Empty);
}));
textBox.GetComponent<PassiveButton>().OnClick.AddListener((UnityAction)(() =>
{
textBox.GiveFocus();
}));
foreach (var tmp in gameObject.GetComponentsInChildren<TextMeshPro>())
{
if (tmp.gameObject.transform.parent.name == "Textbox")
{
continue;
}
var text = tmp.text;
tmp.text = $"<font=\"LiberationSans SDF\" material=\"LiberationSans SDF RadialMenu Material\">{text}</font>";
}
}
public static void CreatePopup(Action<string> saveAction)
{
// Because innerscuff doesnt account for Z value in Close animation
var holder = new GameObject("PopupHolder")
{
transform =
{
parent = Camera.main!.transform,
localPosition = new Vector3(0, -1f, -500),
},
};
var gameObject = Instantiate(MiraAssets.PresetSavePopup.LoadAsset(), holder.transform);
var popup = gameObject.GetComponent<SavePresetPopup>();
popup.onSave = saveAction;
popup.Begin(null);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.
Binary file not shown.
+17 -4
View File
@@ -35,6 +35,16 @@ public static class CustomRoleManager
public static readonly LoadableAsset<AudioClip> ImpostorIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
/// <summary>
/// Gets the list of custom roles as RoleBehaviour objects.
/// </summary>
public static IReadOnlyList<RoleBehaviour> CustomRoleBehaviours { get; private set; } = [];
/// <summary>
/// Gets the list of custom roles as ICustomRole objects.
/// </summary>
public static IReadOnlyList<ICustomRole> CustomMiraRoles { get; private set; } = [];
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
@@ -53,6 +63,9 @@ public static class CustomRoleManager
{
RoleManager.GhostRoles.Add(role.Role);
}
CustomRoleBehaviours = CustomRoles.Values.ToList();
CustomMiraRoles = CustomRoles.Values.OfType<ICustomRole>().ToList();
}
internal static void RegisterRoleTypes(List<Type> roles, MiraPluginInfo pluginInfo)
@@ -72,7 +85,7 @@ public static class CustomRoleManager
continue;
}
pluginInfo.CustomRoles.Add((ushort)role.Role, role);
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
}
pluginInfo.PluginConfig.Save();
@@ -161,7 +174,7 @@ public static class CustomRoleManager
/// <returns>A MiraPluginInfo object representing the parent mod of the role.</returns>
public static MiraPluginInfo FindParentMod(ICustomRole role)
{
return MiraPluginManager.Instance.RegisteredPlugins().First(plugin => plugin.CustomRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
}
/// <summary>
@@ -236,7 +249,7 @@ public static class CustomRoleManager
// we dont know how other plugins handle their configs
// this way, all the options are saved at once, instead of one by one
var oldConfigSetting = new Dictionary<MiraPluginInfo, bool>();
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
oldConfigSetting.Add(plugin, plugin.PluginConfig.SaveOnConfigSet);
plugin.PluginConfig.SaveOnConfigSet = false;
@@ -271,7 +284,7 @@ public static class CustomRoleManager
}
}
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins())
foreach (var plugin in MiraPluginManager.Instance.RegisteredPlugins)
{
plugin.PluginConfig.Save();
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
+5 -5
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace MiraAPI.Roles;
@@ -8,12 +9,11 @@ namespace MiraAPI.Roles;
/// <typeparam name="T">The role you are trying to access.</typeparam>
public static class CustomRoleSingleton<T> where T : ICustomRole
{
private static ICustomRole? _instance;
private static T? _instance;
/// <summary>
/// Gets the instance of the role.
/// </summary>
#pragma warning disable CA1000
public static ICustomRole Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single();
#pragma warning restore CA1000
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "This is a utility class to get the instance of a custom role.")]
public static T Instance => _instance ??= CustomRoleManager.CustomRoles.Values.OfType<T>().Single();
}
+17 -2
View File
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using AmongUs.GameOptions;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Roles;
@@ -42,7 +43,7 @@ public static class CustomRoleUtils
public static StringBuilder CreateForRole(ICustomRole role)
{
var taskStringBuilder = new StringBuilder();
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}Your role is <b>{role.RoleName}.</b></color>");
taskStringBuilder.Append("<size=70%>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
return taskStringBuilder;
@@ -56,7 +57,21 @@ public static class CustomRoleUtils
public static LoadableAsset<AudioClip>? GetIntroSound(RoleTypes roleType)
{
var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType);
if (role is ICustomRole customRole) return customRole.Configuration.IntroSound;
if (role is ICustomRole customRole)
{
return customRole.Configuration.IntroSound;
}
return new PreloadedAsset<AudioClip>(role!.IntroSound);
}
/// <summary>
/// Determines if a role is a custom role or not.
/// </summary>
/// <param name="role">The RoleBehaviour to check.</param>
/// <returns>True if the role is a custom role, false otherwise.</returns>
public static bool IsCustomRole(this RoleBehaviour role)
{
return CustomRoleManager.CustomRoles.ContainsKey((ushort)role.Role);
}
}
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more