Compare commits

...
127 Commits
Author SHA1 Message Date
XtraCube cf0bd6ec38 fixed presets 2025-06-20 08:43:11 -04:00
XtraCube 130236e899 oops fix bad code 2025-06-20 08:38:31 -04:00
XtraCube 3b7b247ac0 refresh presets when opening preset menu 2025-06-20 08:32:24 -04:00
XtraCube 52de9a22de fix preset loading networking 2025-06-19 23:24:02 -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 96a47d480a Add Modifiers HUD 2025-04-12 12:35:09 -04:00
XtraCube bb72d885cf Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-04-09 22:06:37 -04:00
XtraCube e242975d75 fix modifier errors in end game win patch 2025-04-09 22:06:28 -04:00
ang-xd be6bc07a62 Merge branch 'dev' into feat-addressables-2 2025-04-06 19:53:32 -04:00
ang-xd a6d55ec90f Fix confirm ejects 2025-04-06 19:52:47 -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 54d85a6996 Merge branch 'dev' into feat-addressables-2 2025-04-05 16:28:41 -04:00
XtraCube 531a799a24 fix double initialization of roles 2025-04-05 16:28:34 -04:00
XtraCube 3e6eb49d93 fix scroller in freeplay role menu 2025-04-05 15:00:51 -04:00
XtraCube 3fabe85a18 fix scroller in freeplay role menu 2025-04-05 15:00:35 -04:00
XtraCube a3fc847d07 Merge branch 'dev' into 2025.3.25 2025-04-05 14:32:26 -04:00
XtraCube ddfad0752d proper fix for meeting vote issue (I HATE IL2CPP) 2025-04-05 14:31:46 -04:00
ang-xd f1846375ca Fix end game stuff 2025-04-05 14:31:46 -04:00
XtraCube fcb693e899 fix modifier adding/removing AGAIN 2025-04-05 14:31:46 -04:00
XtraCube 21bec074fa improve GetAlivePlayers 2025-04-05 14:31:45 -04:00
ang-xd 30516104aa Merge branch 'feat-addressables-2' of https://github.com/All-Of-Us-Mods/MiraAPI into feat-addressables-2 2025-04-05 14:31:43 -04:00
ang-xd e7508e299b Faster loading of hats 2025-04-05 14:31:37 -04:00
XtraCube b0cd1d30b3 Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-04-05 14:30:57 -04:00
XtraCube 9f3a40b48f proper fix for meeting vote issue (I HATE IL2CPP) 2025-04-05 14:30:52 -04:00
XtraCube 31ffe6274c fix silly mistake with settings 2025-04-05 14:19:52 -04:00
ang-xd 91c9a27df8 Fix end game stuff 2025-04-05 13:06:35 -04:00
MyDragonBreath 7484dd1a9f possible pagination... 2025-04-05 23:17:55 +11:00
XtraCube 9c9964eecf fix modifier adding/removing AGAIN 2025-04-04 18:26:16 -04:00
XtraCube 78fad65b97 improve GetAlivePlayers 2025-04-04 18:25:57 -04:00
XtraCube aef4d6daf1 actually fix meetinghud vote issue 2025-04-04 13:09:11 -04:00
XtraCube 87b94b1b6d actually fix meetinghud vote issue 2025-04-04 13:08:35 -04:00
XtraCube cf92b97e8b slight logic adjustment to fix inactive modifier checks 2025-04-04 12:42:24 -04:00
XtraCube bd0aa68cb8 slight logic adjustment to fix inactive modifier checks 2025-04-04 12:42:12 -04:00
XtraCube f43a542134 fix voting never ends bug 2025-04-04 12:35:26 -04:00
XtraCube efd176f7a4 fix timer bug related to effects 2025-04-04 12:35:26 -04:00
XtraCube b2fabdb139 add check inactive variants of hasmodifier 2025-04-04 12:35:26 -04:00
XtraCube a651099217 fix voting never ends bug 2025-04-04 12:34:41 -04:00
XtraCube e2f39c29a8 fix timer bug related to effects 2025-04-04 12:18:37 -04:00
XtraCube 140b697aaf add check inactive variants of hasmodifier 2025-04-04 09:33:59 -04:00
XtraCube a56b6d52ef rework modifier assignment 2025-04-03 21:05:44 -04:00
XtraCube 3a36acd131 rework modifier assignment 2025-04-03 20:37:09 -04:00
XtraCube 3adb7ef018 Merge branch 'dev' into feat-addressables-2 2025-04-01 21:22:41 -04:00
XtraCube 8c763e2a57 fix mira event priorities 2025-04-01 21:22:34 -04:00
XtraCube 2df50112eb Merge branch 'dev' into feat-addressables-2 2025-04-01 17:03:29 -04:00
XtraCube ce330325e4 fix broken submerged compat patch 2025-04-01 17:03:13 -04:00
XtraCube 516cc83170 Merge branch 'dev' into feat-addressables-2 2025-04-01 13:09:10 -04:00
XtraCube 89cd32d4a4 fix round start event 2025-04-01 13:09:00 -04:00
XtraCube f0e37daf0e Merge branch 'dev' into 2025.3.25 2025-04-01 12:25:59 -04:00
XtraCube 71e2f2e375 Merge branch 'dev' into feat-addressables-2 2025-03-31 22:16:38 -04:00
XtraCube a5ddec48c8 attempt at better config I/O during setup 2025-03-31 22:16:32 -04:00
XtraCube 9e6524817a Merge branch 'dev' into feat-addressables-2 2025-03-30 20:25:39 -04:00
XtraCube 5f4461f7e5 suppress il2cpp warnings 2025-03-30 16:26:22 -04:00
XtraCube 5e2ca191ba proper submerged compat 2025-03-30 16:19:52 -04:00
XtraCube 5af10b52a4 Merge branch 'dev' into feat-addressables-2 2025-03-30 15:44:47 -04:00
XtraCube a0d3afc7c7 add onmeeting event 2025-03-30 15:38:05 -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 a8ac00d270 Merge branch 'dev' into feat-addressables-2 2025-03-29 22:07:24 -04:00
XtraCube 3cd7acbc70 fix ejection patch for submerged and airship 2025-03-29 22:07:12 -04:00
XtraCube ce583d11de Merge branch 'dev' into feat-addressables-2 2025-03-29 21:51:43 -04:00
XtraCube 31132e6e65 add RoundStartEvent.cs 2025-03-29 21:51:31 -04:00
XtraCube 1a53e2b045 add new meeting and intro events 2025-03-29 21:39:44 -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 32b368c1ef update other addressable related things 2025-03-29 17:58:05 -04:00
XtraCube 103c32f386 finish AddressablesLoader.cs 2025-03-29 17:56:50 -04:00
XtraCube 662b867dc5 Merge remote-tracking branch 'origin/feat-addressables-2' into feat-addressables-2 2025-03-29 17:40:17 -04:00
XtraCube adae0225e9 fix merge error 2025-03-29 17:40:00 -04:00
XtraCube 0d347b8b1b Merge branch 'dev' into feat-addressables-2
# Conflicts:
#	MiraAPI.Example/ExampleEventHandlers.cs
2025-03-29 17:39:46 -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 4fad586aa7 use new alternate color in lobby 2025-03-29 13:53:58 -04:00
XtraCube 6d51e6806a fix warning in role stubs 2025-03-29 13:37:06 -04:00
XtraCube b28a0a1253 cleanup CustomPlayerMenu.cs 2025-03-29 13:35:16 -04:00
XtraCube 51b54d4340 reorganize vent patch 2025-03-29 13:12:19 -04:00
XtraCube ea9bf162ea rework modifier component caching 2025-03-29 13:12:12 -04:00
XtraCube 57105f487b cleanup 2025-03-29 13:11:15 -04:00
XtraCube c5e424e8ff hide empty role groups in settings 2025-03-29 12:26:38 -04:00
MyDragonBreath d3b40aec5a remove tou and me specific example 2025-03-29 23:25:55 +11:00
XtraCube 521e5a12fd more accurate doc 2025-03-29 00:27:07 -04:00
XtraCube e6e4a86546 fix stupid innerscuff code 2025-03-28 15:20:31 -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 bb0b678747 fix VERY stupid issue with LobbyViewPanePatches.cs 2025-03-24 18:57:06 -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 f48143610c fix incorrect chunking algorithm 2025-03-23 15:15:21 -04:00
XtraCube 9e8c05f21d text input WIP 2025-03-23 15:01:31 -04:00
XtraCube 2cfc5ac039 update example 2025-03-23 14:00:12 -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
MyDragonBreath 71b537dba9 final addressables 2025-03-23 22:59:11 +11:00
XtraCube e9beb3b423 fix weird behaviour 2025-03-21 15:24:48 -04:00
XtraCube 5b608f5b3a preset WIP 2025-03-21 15:21:18 -04:00
MyDragonBreath 23c0397afe an approach to addressables 2025-03-19 23:07:09 +11: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
88 changed files with 3222 additions and 620 deletions

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
.vs/
obj/
bin/
.qodo/
*.user
*.lock.json
+2 -2
View File
@@ -2,10 +2,10 @@
<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="AmongUs.GameLibs.Steam" Version="2025.3.25" 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
View File
@@ -5,6 +5,7 @@ namespace MiraAPI.Example;
public static class ExampleAssets
{
public static LoadableResourceAsset ExampleButton { get; } = new("MiraAPI.Example.Resources.ExampleButton.png");
public static LoadableResourceAsset CallMeetingButton { get; } = new("MiraAPI.Example.Resources.CallMeeting.png");
// Credit to EpicHorrors for the teleport button asset.
public static LoadableResourceAsset TeleportButton { get; } = new("MiraAPI.Example.Resources.TeleportButton.png");
+5 -5
View File
@@ -9,7 +9,10 @@ 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;
@@ -38,12 +41,9 @@ public static class ExampleEventHandlers
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
foreach (var plr in PlayerControl.AllPlayerControls)
foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole))
{
if (plr.Data.Role is MayorRole)
{
plr.GetVoteData().IncreaseRemainingVotes(1);
}
plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
+3 -5
View File
@@ -9,11 +9,9 @@
<ProjectReference Include="..\MiraAPI\MiraAPI.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\ExampleButton.png" />
<EmbeddedResource Include="Resources\FortniteBanner.jpeg" />
<EmbeddedResource Include="Resources\TeleportButton.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<Import Project="..\AmongUs.props" />
</Project>
+18 -1
View File
@@ -1,12 +1,29 @@
using MiraAPI.Example.Options.Modifiers;
using Il2CppSystem.Collections.Generic;
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;
public class CaptainModifier : GameModifier
{
public override string ModifierName => "Captain";
public override LoadableAsset<Sprite>? ModifierIcon => ExampleAssets.CallMeetingButton;
public override void OnDeath(DeathReason reason)
{
Player.RemoveModifier(this);
}
public override string GetDescription()
{
return $"You can call a meeting from anywhere on the map.";
}
public override int GetAssignmentChance()
{
@@ -1,4 +1,6 @@
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Example.Modifiers;
@@ -6,6 +8,11 @@ public class HighPriorityModifier : GameModifier
{
public override string ModifierName => "High Priority";
public override string GetDescription()
{
return "You are high priority! Idrk what this means either tbh...";
}
public override int GetAssignmentChance()
{
return 100;
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+7 -1
View File
@@ -1,6 +1,6 @@
using AmongUs.GameOptions;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -20,6 +20,12 @@ public class ChameloenRole : CrewmateRole, ICustomRole
IntroSound = CustomRoleUtils.GetIntroSound(RoleTypes.Shapeshifter),
};
public override void Initialize(PlayerControl player)
{
Player = player;
Logger<ExamplePlugin>.Error("Initializing ChamelonRole for player: " + player.PlayerId);
}
public void PlayerControlFixedUpdate(PlayerControl playerControl)
{
if (playerControl.MyPhysics.Velocity.magnitude > 0)
+25 -21
View File
@@ -21,7 +21,7 @@ public static class MiraEventManager
public static bool InvokeEvent<T>(T eventInstance) where T : MiraEvent
{
EventWrappers.TryGetValue(typeof(T), out var handlers);
if (handlers == null)
if (handlers == null || handlers.Count == 0)
{
return false;
}
@@ -43,7 +43,7 @@ public static class MiraEventManager
public static bool InvokeEvent(MiraEvent eventInstance, Type type)
{
EventWrappers.TryGetValue(type, out var handlers);
if (handlers == null)
if (handlers == null || handlers.Count == 0)
{
return false;
}
@@ -61,47 +61,51 @@ public static class MiraEventManager
/// </summary>
/// <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. Higher values are called first.</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)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {nameof(type)}");
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {type.FullName}");
return;
}
if (!EventWrappers.TryGetValue(type, out var handlers))
{
handlers = [];
EventWrappers.Add(type, handlers);
}
EventWrappers.TryAdd(type, []);
var handlers = EventWrappers[type];
var @delegate = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(type), methodInfo);
handlers.Add(new MiraEventWrapper(@delegate, priority));
var eventWrapper = new MiraEventWrapper(@delegate, priority);
var index = handlers.BinarySearch(eventWrapper, Comparer<MiraEventWrapper>.Create((a, b) => a.Priority.CompareTo(b.Priority)));
if (index < 0)
{
index = ~index;
}
handlers.Insert(index, eventWrapper);
}
/// <summary>
/// Register an event.
/// </summary>
/// <param name="handler">The callback method/handler for the event.</param>
/// <param name="priority">The priority of the event handler. Higher values are called first.</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
{
if (!EventWrappers.ContainsKey(typeof(T)))
{
EventWrappers.Add(typeof(T), []);
}
EventWrappers.TryAdd(typeof(T), []);
var handlers = EventWrappers[typeof(T)];
handlers.Add(new MiraEventWrapper(handler, priority));
}
var eventWrapper = new MiraEventWrapper(handler, priority);
internal static void SortAllHandlers()
{
foreach (var handlers in EventWrappers.Values)
var index = handlers.BinarySearch(eventWrapper, Comparer<MiraEventWrapper>.Create((a, b) => a.Priority.CompareTo(b.Priority)));
if (index < 0)
{
handlers.Sort((a, b) => a.Priority.CompareTo(b.Priority));
index = ~index;
}
handlers.Insert(index, eventWrapper);
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ public class MiraEventWrapper
public Delegate EventHandler { get; }
/// <summary>
/// Gets the priority of the handler. Higher values are called first.
/// Gets the priority of the handler. Lower values are called first.
/// </summary>
public int Priority { get; }
+2 -2
View File
@@ -9,14 +9,14 @@ namespace MiraAPI.Events;
public class RegisterEventAttribute : Attribute
{
/// <summary>
/// Gets the priority of the event.
/// Gets the priority of the event. Lower values are called first.
/// </summary>
public int Priority { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RegisterEventAttribute"/> class.
/// </summary>
/// <param name="priority">The priority of the event.</param>
/// <param name="priority">The priority of the event. Lower values are called first.</param>
public RegisterEventAttribute(int priority = 0)
{
Priority = priority;
@@ -0,0 +1,12 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// The event that is invoked when the intro cutscene has finished playing.
/// </summary>
public class IntroEndEvent(IntroCutscene introCutscene) : MiraEvent
{
/// <summary>
/// Gets the instance of the IntroCutscene.
/// </summary>
public IntroCutscene IntroCutscene { get; } = introCutscene;
}
@@ -0,0 +1,21 @@
namespace MiraAPI.Events.Vanilla.Gameplay;
/// <summary>
/// Start Round event, invoked on IntroCutscene.OnDestroy and ExileController.WrapUp.
/// </summary>
public class RoundStartEvent : MiraEvent
{
/// <summary>
/// Gets a value indicating whether the event was triggered by the IntroCutscene or ExileController.
/// </summary>
public bool TriggeredByIntro { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RoundStartEvent"/> class.
/// </summary>
/// <param name="triggeredByIntro">Whether the event was triggered by the intro or not.</param>
public RoundStartEvent(bool triggeredByIntro)
{
TriggeredByIntro = triggeredByIntro;
}
}
@@ -0,0 +1,12 @@
namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary>
/// The event that is invoked when the meeting has ended.
/// </summary>
public class EndMeetingEvent(MeetingHud meetingHud) : MiraEvent
{
/// <summary>
/// Gets the MeetingHud instance.
/// </summary>
public MeetingHud MeetingHud { get; } = meetingHud;
}
@@ -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);
}
}
@@ -0,0 +1,13 @@
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// The event that is invoked when the voting phase of a meeting is complete.
/// </summary>
/// <param name="meetingHud">The MeetingHud instance.</param>
public class VotingCompleteEvent(MeetingHud meetingHud) : MiraEvent
{
/// <summary>
/// Gets the MeetingHud instance.
/// </summary>
public MeetingHud MeetingHud { get; } = meetingHud;
}
@@ -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>
+82
View File
@@ -6,18 +6,100 @@ 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; }
/// <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="container">>The Transform container for the option.</param>
/// <returns>>The created OptionBehaviour object.</returns>
OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, 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);
}
+6 -4
View File
@@ -42,7 +42,7 @@ public static class ModdedOptionsManager
Groups.Add(group);
TypeToGroup.Add(type, group);
pluginInfo.OptionGroups.Add(group);
pluginInfo.InternalOptionGroups.Add(group);
typeof(OptionGroupSingleton<>).MakeGenericType(type)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
@@ -119,7 +119,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 +140,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 +156,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,6 +173,7 @@ public static class ModdedOptionsManager
/// </summary>
/// <param name="__originalMethod">The original setter method.</param>
/// <param name="value">The new object value.</param>
// ReSharper disable once InconsistentNaming
public static void PropertySetterPatch(MethodBase __originalMethod, object value)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetSetMethod() == __originalMethod).Value;
@@ -185,6 +186,7 @@ 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>
// ReSharper disable InconsistentNaming
public static bool PropertyGetterPatch(MethodBase __originalMethod, ref object __result)
{
var attribute = OptionAttributes.First(pair => pair.Key.GetGetMethod() == __originalMethod).Value;
@@ -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,7 +38,7 @@ 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;
}
@@ -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,9 +129,9 @@ 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 />
@@ -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;
+55 -59
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;
@@ -66,37 +57,25 @@ 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>
public ConfigDefinition? ConfigDefinition
{
get => _configDefinition;
set
{
if (_configDefinition is not null) return;
_configDefinition = value;
}
}
private ConfigDefinition? _configDefinition;
/// <inheritdoc />
public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </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;
@@ -104,6 +83,7 @@ public abstract class ModdedOption<T> : IModdedOption
Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title);
Visible = () => true;
IncludeInPreset = includeInPreset;
}
internal void ValueChanged(OptionBehaviour optionBehaviour)
@@ -115,7 +95,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;
@@ -125,7 +106,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)
{
@@ -138,22 +119,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>
@@ -169,14 +172,7 @@ 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,
@@ -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>();
+24 -18
View File
@@ -417,9 +417,12 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Timer >= 0 && !TimerPaused)
if (Timer >= 0)
{
Timer -= Time.deltaTime;
if (!TimerPaused)
{
Timer -= Time.deltaTime;
}
}
else if (HasEffect && EffectActive)
{
@@ -428,25 +431,28 @@ public abstract class CustomActionButton
OnEffectEnd();
}
if (CanUse())
if (Button)
{
Button?.SetEnabled();
}
else
{
Button?.SetDisabled();
}
if (CanUse())
{
Button.SetEnabled();
}
else
{
Button.SetDisabled();
}
if (EffectActive)
{
Button?.SetFillUp(Timer, EffectDuration);
if (EffectActive)
{
Button.SetFillUp(Timer, EffectDuration);
Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
Button!.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button?.SetCoolDown(Timer, Cooldown);
Button.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString(NumberFormatInfo.InvariantInfo);
Button.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button.SetCoolDown(Timer, Cooldown);
}
}
FixedUpdate(playerControl);
+1 -1
View File
@@ -29,7 +29,7 @@ public static class CustomButtonManager
}
CustomButtons.Add(button);
pluginInfo.Buttons.Add(button);
pluginInfo.InternalButtons.Add(button);
typeof(CustomButtonSingleton<>).MakeGenericType(buttonType)
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic)!
.SetValue(null, button);
+27 -27
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
@@ -8,6 +9,8 @@ using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#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.
namespace MiraAPI.Hud;
@@ -16,22 +19,18 @@ namespace MiraAPI.Hud;
/// </summary>
/// <param name="il2CppPtr">Used by Il2Cpp. Do not use constructor, this is a MonoBehaviour.</param>
[RegisterInIl2Cpp]
[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Unity Convention")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Unity Convention")]
public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
public ShapeshifterPanel PanelPrefab;
public float XStart = -0.8f;
public float YStart = 2.15f;
public float XOffset = 1.95f;
public float YOffset = -0.65f;
public UiElement BackButton;
public UiElement DefaultButtonSelected;
public ShapeshifterPanel panelPrefab;
public float xStart = -0.8f;
public float yStart = 2.15f;
public float xOffset = 1.95f;
public float yOffset = -0.65f;
public UiElement backButton;
public UiElement defaultButtonSelected;
public List<ShapeshifterPanel> potentialVictims;
/// <summary>
@@ -42,17 +41,17 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
var shapeShifterRole = RoleManager.Instance.GetRole(RoleTypes.Shapeshifter);
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>().ShapeshifterMenu;
var ogMenu = shapeShifterRole.TryCast<ShapeshifterRole>()!.ShapeshifterMenu;
var newMenu = Instantiate(ogMenu);
var customMenu = newMenu.gameObject.AddComponent<CustomPlayerMenu>();
customMenu.PanelPrefab = newMenu.PanelPrefab;
customMenu.XStart = newMenu.XStart;
customMenu.YStart = newMenu.YStart;
customMenu.XOffset = newMenu.XOffset;
customMenu.YOffset = newMenu.YOffset;
customMenu.BackButton = newMenu.BackButton;
var back = customMenu.BackButton.GetComponent<PassiveButton>();
customMenu.panelPrefab = newMenu.PanelPrefab;
customMenu.xStart = newMenu.XStart;
customMenu.yStart = newMenu.YStart;
customMenu.xOffset = newMenu.XOffset;
customMenu.yOffset = newMenu.YOffset;
customMenu.backButton = newMenu.BackButton;
var back = customMenu.backButton.GetComponent<PassiveButton>();
back.OnClick.RemoveAllListeners();
back.OnClick.AddListener((UnityAction)(() =>
{
@@ -65,7 +64,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
newMenu.DestroyImmediate();
customMenu.transform.SetParent(Camera.main.transform, false);
customMenu.transform.SetParent(Camera.main!.transform, false);
customMenu.transform.localPosition = new Vector3(0f, 0f, -50f);
return customMenu;
}
@@ -75,6 +74,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
ControllerManager.Instance.CloseOverlayMenu(name);
}
/// <inheritdoc />
public override void Begin(PlayerTask task)
{
throw new NotImplementedException("Use the other Begin method.");
@@ -90,7 +90,7 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
{
MinigameStubs.Begin(this, null);
var back = BackButton.GetComponent<PassiveButton>();
var back = backButton.GetComponent<PassiveButton>();
back.OnClick.AddListener((UnityAction)(() =>
{
onClick(null);
@@ -107,13 +107,13 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
var num = i % 3;
var num2 = i / 3;
var flag = PlayerControl.LocalPlayer.Data.Role.NameColor == player.Data.Role.NameColor;
var shapeshifterPanel = Instantiate(PanelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(XStart + num * XOffset, YStart + num2 * YOffset, -1f);
var shapeshifterPanel = Instantiate(panelPrefab, transform);
shapeshifterPanel.transform.localPosition = new Vector3(xStart + num * xOffset, yStart + num2 * yOffset, -1f);
shapeshifterPanel.SetPlayer(i, player.Data, (Il2CppSystem.Action)(() => { onClick(player); }));
shapeshifterPanel.NameText.color = flag ? player.Data.Role.NameColor : Color.white;
potentialVictims.Add(shapeshifterPanel);
list2.Add(shapeshifterPanel.Button);
}
ControllerManager.Instance.OpenOverlayMenu(name, BackButton, DefaultButtonSelected, list2);
ControllerManager.Instance.OpenOverlayMenu(name, backButton, defaultButtonSelected, list2);
}
}
+114
View File
@@ -0,0 +1,114 @@
using System;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Attributes;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Hud;
/// <summary>
/// Simple input box for user input.
/// </summary>
[RegisterInIl2Cpp]
public class InputBox : MonoBehaviour
{
public InputField inputField = null!;
public void CreateDialog(string title, string placeholder, string submitText, Action<string> onSubmit)
{
var canvasObj = new GameObject("DialogCanvas");
canvasObj.transform.SetParent(transform);
canvasObj.layer = LayerMask.NameToLayer("UI");
var canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.overrideSorting = true;
canvas.sortingOrder = 1000; // Ensure the canvas is on top of other UI elements
canvasObj.AddComponent<CanvasScaler>().uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
canvasObj.AddComponent<GraphicRaycaster>();
var panelObj = new GameObject("DialogPanel");
panelObj.transform.SetParent(canvasObj.transform);
var panelRect = panelObj.AddComponent<RectTransform>();
var img = panelObj.AddComponent<Image>();
img.color = new Color(1, 1, 1, 0.9f);
img.raycastTarget = true;
img.sprite = MiraAssets.RoundedBox.LoadAsset();
img.type = Image.Type.Sliced;
panelRect.sizeDelta = new Vector2(300, 200);
panelRect.anchoredPosition = Vector2.zero;
var titleText = CreateText(panelObj, title);
titleText.alignment = TextAnchor.MiddleCenter;
titleText.fontSize = 26;
titleText.fontStyle = FontStyle.Bold;
titleText.color = Color.white;
var titleRect = titleText.GetComponent<RectTransform>();
titleRect.sizeDelta = new Vector2(280, 50);
titleRect.anchoredPosition = new Vector2(0, 70);
var inputObj = new GameObject("InputField");
inputObj.transform.SetParent(panelObj.transform);
var inputRect = inputObj.AddComponent<RectTransform>();
inputRect.sizeDelta = new Vector2(220, 40);
inputRect.anchoredPosition = new Vector2(0, 20);
var inputBg = inputObj.AddComponent<Image>();
inputBg.color = new Color(1, 1, 1, 0.8f);
inputField = inputObj.AddComponent<InputField>();
inputField.textComponent = CreateText(inputObj, string.Empty, 18, Color.black);
inputField.placeholder = CreateText(inputObj, placeholder, 18, new Color(0.5f, 0.5f, 0.5f));
var buttonObj = new GameObject("SubmitButton");
buttonObj.transform.SetParent(panelObj.transform);
var button = buttonObj.AddComponent<Button>();
var buttonRect = buttonObj.AddComponent<RectTransform>();
buttonRect.sizeDelta = new Vector2(150, 50);
buttonRect.anchoredPosition = new Vector2(0, -50);
var buttonImg = buttonObj.AddComponent<Image>();
buttonImg.color = new Color(1f, 1f, 1f, 1f);
buttonImg.sprite = MiraAssets.RoundedBox.LoadAsset();
buttonImg.type = Image.Type.Sliced;
var buttonText = CreateText(buttonObj, submitText, 20, Color.white);
buttonText.alignment = TextAnchor.MiddleCenter;
var colors = button.colors;
colors.highlightedColor = Color.green;
colors.pressedColor = MiraAssets.AcceptedTeal;
button.colors = colors;
button.onClick.AddListener((UnityAction)(() =>
{
onSubmit(inputField.text);
DestroyImmediate(canvasObj);
}));
inputField.ActivateInputField();
}
private static Text CreateText(GameObject parent, string textContent = "", int fontSize = 20, Color? textColor = null, Font? font = null)
{
var textObj = new GameObject("Text");
textObj.transform.SetParent(parent.transform);
var text = textObj.AddComponent<Text>();
text.font = font ?? Resources.GetBuiltinResource<Font>("Arial.ttf");
text.text = textContent;
text.color = textColor ?? Color.white;
text.fontSize = fontSize;
text.alignment = TextAnchor.MiddleCenter;
var rect = textObj.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(200, 40);
rect.anchoredPosition = Vector2.zero;
return text;
}
}
+11 -9
View File
@@ -1,19 +1,12 @@
using System;
using BepInEx;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MonoMod.Utils;
using Reactor;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
using UnityEngine.ProBuilder;
using Delegate = Il2CppSystem.Delegate;
namespace MiraAPI;
@@ -23,11 +16,20 @@ namespace MiraAPI;
[BepInAutoPlugin("mira.api", "MiraAPI")]
[BepInProcess("Among Us.exe")]
[BepInDependency(ReactorPlugin.Id)]
[BepInDependency(ModCompatibility.SubmergedId, BepInDependency.DependencyFlags.SoftDependency)]
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class MiraApiPlugin : BasePlugin
{
/// <summary>
/// Gets the branding Mira API color.
/// </summary>
public static Color MiraColor { get; } = new Color32(238, 154, 112, 255);
public static Color DefaultHeaderColor = new Color32(77, 77, 77, 255);
/// <summary>
/// Gets the default color for headers in the options menu.
/// </summary>
public static Color DefaultHeaderColor { get; } = new Color32(77, 77, 77, 255);
private static MiraPluginManager? PluginManager { get; set; }
internal Harmony Harmony { get; } = new(Id);
+28
View File
@@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
namespace MiraAPI;
/// <summary>
/// Mod compatibility tools.
/// </summary>
public static class ModCompatibility
{
/// <summary>
/// The ID for the Submerged mod.
/// </summary>
public const string SubmergedId = "Submerged";
/// <summary>
/// Checks if the Submerged mod is loaded.
/// </summary>
/// <param name="submergedAssembly">Submerged mod assembly if loaded, null otherwise.</param>
/// <returns>True if the Submerged mod is loaded, false otherwise.</returns>
public static bool SubmergedLoaded([NotNullWhen(true)] out Assembly? submergedAssembly)
{
var result = IL2CPPChainloader.Instance.Plugins.TryGetValue(SubmergedId, out var plugin);
submergedAssembly = result ? plugin?.Instance.GetType().Assembly : null;
return result;
}
}
+21 -7
View File
@@ -1,6 +1,8 @@
using System;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Modifiers;
@@ -38,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>
@@ -48,9 +50,14 @@ public abstract class BaseModifier : IOptionable
public abstract string ModifierName { get; }
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI.
/// Gets the modifier icon. Useless if HideOnUi is true.
/// </summary>
public virtual bool HideOnUi => false;
public virtual LoadableAsset<Sprite>? ModifierIcon => null;
/// <summary>
/// Gets a value indicating whether the modifier is hidden on the UI. Will be hidden either way if no description is provided.
/// </summary>
public virtual bool HideOnUi => GetDescription() == string.Empty;
/// <summary>
/// Gets a value indicating whether the modifier is shown in the freeplay menu.
@@ -63,10 +70,10 @@ public abstract class BaseModifier : IOptionable
public virtual bool Unique => true;
/// <summary>
/// Gets the HUD information for this modifier. Defaults to the modifier name. Does nothing if <see cref="HideOnUi"/> is true.
/// Gets the HUD description for this modifier. Does nothing if <see cref="HideOnUi"/> is true. Required to be visible on UI.
/// </summary>
/// <returns>The information string for the HUD.</returns>
public virtual string GetHudString() => ModifierName;
/// <returns>The description string for the HUD.</returns>
public virtual string GetDescription() => string.Empty;
/// <summary>
/// Called when the modifier is activated.
@@ -104,6 +111,13 @@ public abstract class BaseModifier : IOptionable
{
}
/// <summary>
/// Called when a meeting starts.
/// </summary>
public virtual void OnMeetingStart()
{
}
/// <summary>
/// Determines whether the player can vent.
/// </summary>
+90 -36
View File
@@ -5,6 +5,7 @@ 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;
@@ -26,12 +27,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public ImmutableList<BaseModifier> ActiveModifiers { get; private set; } = ImmutableList<BaseModifier>.Empty;
private ModifierDisplayComponent? ModifierDisplay { get; set; }
[HideFromIl2Cpp]
private List<BaseModifier> Modifiers { get; set; } = [];
private PlayerControl? _player;
private TextMeshPro? _modifierText;
private PlayerControl _player = null!;
private readonly List<BaseModifier> _toRemove = [];
@@ -42,9 +43,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
_toRemove.AddRange(Modifiers);
}
private void Start()
private void Awake()
{
_player = GetComponent<PlayerControl>();
if (_player == null)
{
Logger<MiraApiPlugin>.Error("ModifierComponent could not find PlayerControl on the same GameObject.");
Destroy(this);
return;
}
ModifierExtensions.ModifierComponents.Add(_player, this);
Modifiers = [];
if (!_player.AmOwner)
@@ -52,20 +62,24 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
return;
}
_modifierText = Helpers.CreateTextLabel("ModifierText", HudManager.Instance.transform, AspectPosition.EdgeAlignments.RightTop, new Vector3(10.1f, 3.5f, 0), textAlignment: TextAlignmentOptions.Right);
_modifierText.verticalAlignment = VerticalAlignmentOptions.Top;
ModifierDisplay = ModifierDisplayComponent.CreateDisplay();
ModifierDisplay?.UpdateModifiersList(Modifiers);
}
private void FixedUpdate()
{
var removed = _toRemove.Count > 0;
foreach (var modifier in _toRemove.ToArray())
{
_toRemove.Remove(modifier);
modifier.OnDeactivate();
Modifiers.Remove(modifier);
}
var added = _toAdd.Count > 0;
foreach (var modifier in _toAdd.ToArray())
{
_toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
@@ -76,14 +90,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
}
if (_toAdd.Count > 0 || _toRemove.Count > 0)
if (removed || added)
{
if (_player?.AmOwner == true)
if (_player.AmOwner && HudManager.InstanceExists)
{
HudManager.Instance?.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
ModifierDisplay?.UpdateModifiersList(Modifiers);
}
_toAdd.Clear();
_toRemove.Clear();
ActiveModifiers = Modifiers.ToImmutableList();
}
@@ -92,6 +104,11 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
modifier.FixedUpdate();
}
if (_player.AmOwner && ModifierDisplay && HudManager.InstanceExists)
{
ModifierDisplay!.gameObject.SetActive(MeetingHud.Instance || HudManager.Instance.TaskPanel.isActiveAndEnabled);
}
}
private void Update()
@@ -100,32 +117,12 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
modifier.Update();
}
}
if (!_modifierText || _player?.AmOwner == false)
{
return;
}
if (MeetingHud.Instance)
{
return;
}
var showText = false;
var builder = new StringBuilder("<b><size=130%>Modifiers:</b></size>\n");
foreach (var modifier in ActiveModifiers)
{
if (modifier.HideOnUi)
{
continue;
}
showText = true;
builder.AppendLine(modifier.GetHudString());
}
_modifierText!.text = showText ? builder.ToString() : string.Empty;
private void OnDestroy()
{
// Remove this component from the global list when destroyed
ModifierExtensions.ModifierComponents.Remove(_player);
}
/// <summary>
@@ -492,4 +489,61 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
return ActiveModifiers.Exists(x => x.UniqueId == id);
}
/// <summary>
/// Checks if a player has an active modifier by its type.
/// </summary>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <typeparam name="T">The Type of the Modifier.</typeparam>
/// <returns>True if the Modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier<T>(bool checkInactive, Func<T, bool>? predicate=null) where T : BaseModifier
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm is T modifier && (predicate == null || predicate(modifier));
}
/// <summary>
/// Checks if a player has an active modifier by its type.
/// </summary>
/// <param name="type">The modifier type.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <returns>True if the Modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(Type type, bool checkInactive, Func<BaseModifier, bool>? predicate=null)
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm.GetType() == type && (predicate == null || predicate(bm));
}
/// <summary>
/// Checks if a player has an active modifier by its type ID.
/// </summary>
/// <param name="id">The modifier's type ID.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <param name="predicate">The predicate to check the modifier.</param>
/// <returns>True if the modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(uint id, bool checkInactive, Func<BaseModifier, bool>? predicate=null)
{
var type = ModifierManager.GetModifierType(id) ?? throw new InvalidOperationException(
$"Cannot get modifier with id {id} because it is not registered.");
return HasModifier(type, checkInactive, predicate);
}
/// <summary>
/// Checks if a player has an active modifier by its unique ID.
/// </summary>
/// <param name="id">The modifier's guid.</param>
/// <param name="checkInactive">Whether to check inactive modifiers (those pending to be added).</param>
/// <returns>True if the modifier is present, false otherwise.</returns>
[HideFromIl2Cpp]
public bool HasModifier(Guid id, bool checkInactive)
{
return ActiveModifiers.Exists(MatchExpr) || (checkInactive && _toAdd.Exists(MatchExpr));
bool MatchExpr(BaseModifier bm) => bm.UniqueId == id;
}
}
@@ -0,0 +1,212 @@
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;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace MiraAPI.Modifiers.ModifierDisplay;
/// <summary>
/// The code used to display Mira modifiers.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierDisplayComponent(nint ptr) : MonoBehaviour(ptr)
{
/// <summary>
/// Gets the instance of the Modifier Display.
/// </summary>
public static ModifierDisplayComponent? Instance { get; private set; }
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 = 0;
private const int _itemsPerPage = 3;
private readonly Dictionary<BaseModifier, ModifierUiComponent> _modifiers = [];
/// <summary>
/// Gets a read only dictionary of the created modifier UI components.
/// </summary>
[HideFromIl2Cpp]
public ReadOnlyDictionary<BaseModifier, ModifierUiComponent> Modifiers => new(_modifiers);
/// <summary>
/// Gets a value indicating whether the modifier display hud is currently open.
/// </summary>
public bool IsOpen { get; private set; }
private void Awake()
{
Instance = this;
_children = transform.GetChild(0).GetComponent<RectTransform>();
_modTemplate = transform.GetChild(2).gameObject;
_toggleButton = transform.GetChild(1).GetComponent<PassiveButton>();
_toggleButton.OnClick = new Button.ButtonClickedEvent();
_toggleButton.OnClick.AddListener((UnityAction)(() =>
{
IsOpen = !IsOpen;
_children.gameObject.SetActive(!_children.gameObject.activeSelf);
}));
_toggleButton.ClickSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent<PassiveButton>().ClickSound;
_toggleButton.HoverSound = HudManager.Instance.GameMenu.StreamerModeButton.GetComponent<PassiveButton>().HoverSound;
_toggleButton.inactiveSprites = _toggleButton.transform.GetChild(1).gameObject;
_toggleBtnText = _toggleButton.transform.GetChild(0).GetComponent<TextMeshPro>();
_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()
{
var gameObject = Instantiate(MiraAssets.ModifierDisplay.LoadAsset(), HudManager.Instance.transform);
var minigame = gameObject.AddComponent<ModifierDisplayComponent>();
return minigame;
}
[HideFromIl2Cpp]
private ModifierUiComponent CreateForModifier(BaseModifier modifier)
{
var newMod = Instantiate(_modTemplate, _children.transform);
newMod.transform.name = modifier.ModifierName;
var comp = newMod.AddComponent<ModifierUiComponent>();
comp.Modifier = modifier;
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 (modifiersToAdd.Contains(mod.Key))
{
modifiersToAdd.Remove(mod.Key);
continue;
}
mod.Value.gameObject.Destroy();
_modifiers.Remove(mod.Key);
}
_toggleBtnText.text = $"Modifiers ({filteredModifiers.Count})";
if (filteredModifiers.Count == 0)
{
_toggleButton.gameObject.SetActive(false);
return;
}
_toggleButton.gameObject.SetActive(true);
foreach (var mod in modifiersToAdd)
{
_modifiers.Add(mod, CreateForModifier(mod));
}
UpdatePage();
}
}
@@ -0,0 +1,71 @@
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using TMPro;
using UnityEngine;
namespace MiraAPI.Modifiers.ModifierDisplay;
/// <summary>
/// The code placed on every Modifier HUD object. Used to handle updating.
/// </summary>
[RegisterInIl2Cpp]
public class ModifierUiComponent(nint ptr) : MonoBehaviour(ptr)
{
/// <summary>
/// Gets the modifier which this component is for.
/// </summary>
[HideFromIl2Cpp]
public BaseModifier? Modifier { get; internal set; }
private SpriteRenderer modBg = null!;
private TextMeshPro desc = null!;
private SpriteRenderer icon = null!;
private TextMeshPro nameText = null!;
private RectTransform descRect = null!;
private void Awake()
{
if (Modifier == null) return;
modBg = transform.GetChild(0).GetComponent<SpriteRenderer>();
desc = modBg.transform.GetChild(0).GetComponent<TextMeshPro>();
icon = modBg.transform.GetChild(1).GetComponent<SpriteRenderer>();
var nameBg = transform.GetChild(1);
nameText = nameBg.GetChild(0).GetComponent<TextMeshPro>();
descRect = desc.gameObject.GetComponent<RectTransform>();
nameText.font = desc.font = HudManager.Instance.TaskPanel.taskText.font;
nameText.fontMaterial = desc.fontMaterial = HudManager.Instance.TaskPanel.taskText.fontMaterial;
}
// Might not be the best solution, but used to update name/description/icon if updated in the modifier.
private void FixedUpdate()
{
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);
icon.gameObject.SetActive(Modifier.ModifierIcon != null);
if (icon.gameObject && icon.gameObject.activeSelf && Modifier.ModifierIcon != null)
{
icon.sprite = Modifier.ModifierIcon?.LoadAsset();
icon.size = new Vector2(0.7f, 0.7f);
icon.tileMode = SpriteTileMode.Continuous;
descRect.localPosition = new Vector3(0.38f, -0.1f, -5);
descRect.sizeDelta = new Vector2(2.38f, 0.65f);
}
else
{
descRect.localPosition = new Vector3(0, -0.1f, -5);
descRect.sizeDelta = new Vector2(3.1f, 0.65f);
}
}
}
+11 -11
View File
@@ -180,6 +180,17 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(typeId, predicate);
}
/// <summary>
/// Checks if the player has a specific modifier by its GUID.
/// </summary>
/// <param name="player">The PlayerControl instance.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
/// <returns>True if the player has the modifier, false otherwise.</returns>
public static bool HasModifier(this PlayerControl player, Guid uniqueId)
{
return player.GetModifierComponent().HasModifier(uniqueId);
}
/// <summary>
/// Clears all modifiers from a player.
/// </summary>
@@ -204,17 +215,6 @@ public static class ModifierExtensions
}
}
/// <summary>
/// Checks if the player has a specific modifier by its GUID.
/// </summary>
/// <param name="player">The PlayerControl instance.</param>
/// <param name="uniqueId">The unique ID of the modifier.</param>
/// <returns>True if the player has the modifier, false otherwise.</returns>
public static bool HasModifier(this PlayerControl player, Guid uniqueId)
{
return player.GetModifierComponent().HasModifier(uniqueId);
}
/// <summary>
/// Tries to get a modifier by its type.
/// </summary>
+40 -51
View File
@@ -75,7 +75,7 @@ public static class ModifierManager
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
}
info.Modifiers.Add(modifier);
info.InternalModifiers.Add(modifier);
if (modifier is not GameModifier gameModifier)
{
@@ -103,68 +103,57 @@ public static class ModifierManager
{
var rand = new Random();
foreach (var priority in PrioritiesToIdsMap.Keys.OrderByDescending(x => x))
// Filter and sort modifiers by descending priority.
var modifiers = IdToTypeModifierMap
.Where(x=>x.Value.IsAssignableTo(typeof(GameModifier)))
.Select(x => Activator.CreateInstance(x.Value) as GameModifier)
.OfType<GameModifier>()
.Where(x => x.GetAmountPerGame() > 0 && x.GetAssignmentChance() > 0)
.OrderByDescending(x => x.Priority())
.ToArray();
foreach (var modifier in modifiers)
{
var filteredModifiers = new List<uint>();
var assignments = modifier.GetAmountPerGame();
foreach (var id in PrioritiesToIdsMap[priority])
var validPlayers = plrs.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId)).ToList();
if (validPlayers.Count == 0)
{
var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
var chance = Math.Clamp(mod!.GetAssignmentChance(), 0, 100);
var count = mod.GetAmountPerGame();
if (chance == 0 || count == 0)
{
continue;
}
var maxCount = plrs.Count(x => IsGameModifierValid(x, mod, id));
if (maxCount == 0)
{
Logger<MiraApiPlugin>.Warning($"No players are valid for {mod.ModifierName}");
continue;
}
var num = Math.Clamp(count, 0, maxCount);
for (var i = 0; i < num; i++)
{
var randomNum = rand.Next(100);
if (randomNum < chance)
{
filteredModifiers.Add(id);
}
}
}
if (filteredModifiers.Count == 0)
{
Logger<MiraApiPlugin>.Warning($"No filtered modifiers for priority {priority}");
Logger<MiraApiPlugin>.Warning($"No valid players for modifier {modifier.ModifierName}");
continue;
}
var shuffledList = filteredModifiers.Randomize();
assignments = Math.Min(assignments, validPlayers.Count);
var availablePlayers = new List<PlayerControl>(validPlayers);
if (shuffledList.Count > plrs.Count)
for (var i = 0; i < assignments; i++)
{
shuffledList = shuffledList.GetRange(0, plrs.Count);
}
foreach (var id in shuffledList)
{
var mod = Activator.CreateInstance(IdToTypeModifierMap[id]) as GameModifier;
var plr = plrs.Where(x => IsGameModifierValid(x, mod!, id)).Random();
if (!plr || plr == null)
var chance = Math.Clamp(modifier.GetAssignmentChance(), 0, 100);
if (rand.Next(100) >= chance)
{
Logger<MiraApiPlugin>.Warning($"Somehow valid players for modifier {mod!.ModifierName} disappeared");
continue;
}
else
var candidates = availablePlayers
.Where(x => IsGameModifierValid(x, modifier, modifier.TypeId))
.ToList();
if (candidates.Count == 0)
{
plr.RpcAddModifier(id);
Logger<MiraApiPlugin>.Warning(
$"No available players for modifier {modifier.ModifierName} at assignment {i + 1}");
break;
}
var plr = candidates.Random();
if (plr == null)
{
Logger<MiraApiPlugin>.Warning($"Valid player for modifier {modifier.ModifierName} disappeared");
continue;
}
plr.RpcAddModifier(modifier.TypeId);
availablePlayers.Remove(plr);
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ public abstract class GameModifier : BaseModifier
public virtual bool IsModifierValidOn(RoleBehaviour role) => true;
/// <summary>
/// Determines whether the player won the game with this modifier.
/// Determines whether the player won the game with this modifier. The modifier with the highest priority will override other modifiers.
/// </summary>
/// <param name="reason">The reason why the game ended.</param>
/// <returns>True if the player won, false if they lost. Return null to use the player's default win condition.</returns>
+4 -4
View File
@@ -45,12 +45,12 @@ public abstract class TimedModifier : BaseModifier
public override bool HideOnUi => !TimerActive;
/// <summary>
/// Gets the HUD information for Timed Modifier, including the time remaining.
/// Gets the HUD description for Timed Modifier, including the time remaining.
/// </summary>
/// <returns>A string with the hud information + timer.</returns>
public override string GetHudString()
/// <returns>A string with the time remaining.</returns>
public override string GetDescription()
{
return base.GetHudString() + $" <size=70%>({Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s)</size>";
return $"Remaining Time: {Math.Round(Duration - TimeRemaining, 0)}s/{Duration}s";
}
/// <summary>
+7 -6
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
+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,
}
@@ -0,0 +1,15 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(SpawnInMinigame), nameof(SpawnInMinigame.Close))]
internal static class SpawnInMinigamePatch
{
public static void Postfix()
{
var @event = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
}
}
@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
namespace MiraAPI.Patches.Events;
[HarmonyPatch]
internal static class SubmergedSpawnGamePatch
{
public static bool Prepare()
{
return ModCompatibility.SubmergedLoaded(out _);
}
public static IEnumerable<MethodBase> TargetMethods()
{
if (ModCompatibility.SubmergedLoaded(out var subAss) &&
subAss.GetType("Submerged.SpawnIn.SubmarineSelectSpawn") is { } selectSpawn)
{
yield return AccessTools.Method(selectSpawn, "OnDestroy");
}
}
public static void Postfix()
{
var @event = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
}
}
+10 -10
View File
@@ -18,7 +18,7 @@ namespace MiraAPI.Patches.Freeplay;
[HarmonyPatch(typeof(TaskAdderGame))]
internal static class TaskAdderPatches
{
private static Scroller? _scroller;
private static Scroller _scroller = null!;
[HarmonyPostfix]
[HarmonyPatch(nameof(TaskAdderGame.Begin))]
@@ -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";
@@ -293,9 +293,9 @@ internal static class TaskAdderPatches
chip.GetComponent<SpriteRenderer>().maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
}
if (_scroller && _scroller != null)
if (_scroller)
{
_scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 3, 1f);
_scroller.CalculateAndSetYBounds(__instance.ActiveItems.Count, 6, 5, 1.4f);
_scroller.SetYBoundsMin(0.0f);
_scroller.ScrollToTop();
}
@@ -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 })
{
+4 -4
View File
@@ -56,11 +56,11 @@ public static class IntroCutscenePatches
}
}
/*
[HarmonyPostfix]
[HarmonyPatch(nameof(IntroCutscene.OnDestroy))]
public static void GameBeginPatch()
public static void GameBeginPatch(IntroCutscene __instance)
{
CustomGameModeManager.ActiveMode?.Initialize();
}*/
MiraEventManager.InvokeEvent(new IntroEndEvent(__instance));
MiraEventManager.InvokeEvent(new RoundStartEvent(true));
}
}
+141
View File
@@ -0,0 +1,141 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(HatsTab))]
public static class HatsTabPatches
{
private static SortedList<string, List<HatData>> sortedHats = [];
private static int currentPage;
[HarmonyPatch(nameof(HatsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(HatsTab __instance)
{
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
if (sortedHats.Count == 0)
{
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedHats = new SortedList<string, List<HatData>>(comparer);
foreach (var hat in allHats)
{
if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
sortedHats[hat.StoreName].Add(hat);
}
}
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(HatsTab), nameof(HatsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(HatsTab __instance)
{
if (sortedHats.Count == 0) return;
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? sortedHats.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > sortedHats.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
}
}
private static IEnumerator? loadRoutine;
private static int hatIndex;
private static void GenerateHats(HatsTab __instance, int page)
{
if (loadRoutine != null) Coroutines.Stop(loadRoutine);
hatIndex = 0;
foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
var (groupName, hats) = sortedHats.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
var xLerp = __instance.XRange.Lerp(0.5f);
var yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
loadRoutine = Coroutines.Start(CoGenerateChips(__instance, hats));
}
private static IEnumerator CoGenerateChips(HatsTab __instance, List<HatData> hats)
{
__instance.scroller.ScrollToTop();
var batchSize = 5;
for (var i = 0; i < hats.Count; i += batchSize)
{
var batch = hats.Skip(i).Take(batchSize).ToList();
foreach (var hat in batch.OrderBy(HatManager.Instance.allHats.IndexOf))
{
var hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
var hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), hat);
hatIndex += 1;
yield return null;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
yield return new WaitForSeconds(0.01f);
}
__instance.currentHatIsEquipped = true;
loadRoutine = null;
}
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = hat.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = hat;
__instance.ColorChips.Add(colorChip);
}
}
@@ -0,0 +1,21 @@
using HarmonyLib;
using MiraAPI.Utilities.Assets;
namespace MiraAPI.Patches.Menu;
/// <summary>
/// General MainMenuManage patches.
/// </summary>
[HarmonyPatch(typeof(MainMenuManager))]
public static class MainMenuManagerPatches
{
/// <summary>
/// A postifix on Awake to load all the addressables registered.
/// </summary>
[HarmonyPatch(nameof(MainMenuManager.Awake))]
[HarmonyPostfix]
public static void AwakePostfix()
{
AddressablesLoader.LoadAll();
}
}
@@ -0,0 +1,118 @@
using AmongUs.Data;
using HarmonyLib;
using Innersloth.Assets;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(NameplatesTab))]
public static class NameplatesTabPatches
{
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
{
foreach (var item in data)
{
if (!SortedNameplates.ContainsKey(item.Key)) SortedNameplates.Add(item.Key, []);
SortedNameplates[item.Key].Add(item.Visor);
}
}
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(NameplatesTab __instance)
{
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(NameplatesTab __instance)
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? SortedNameplates.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > SortedNameplates.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
}
}
private static void GenerateHats(NameplatesTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
var (groupName, nameplates) = SortedNameplates.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 2;
foreach (var visor in nameplates.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentNameplateIsEquipped = true;
}
private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = namePlate.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = namePlate.ProdId;
var x = (NamePlateViewData viewdata) =>
{
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().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);
colorChip.Tag = namePlate;
__instance.ColorChips.Add(colorChip);
}
}
+118
View File
@@ -0,0 +1,118 @@
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(SkinsTab))]
public static class SkinsTabPatches
{
private static SortedList<string, List<SkinData>> sortedSkins = [];
private static int currentPage;
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(SkinsTab __instance)
{
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
if (sortedSkins.Count == 0)
{
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
sortedSkins = new SortedList<string, List<SkinData>>(comparer);
foreach (var skin in allSkins)
{
if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
sortedSkins[skin.StoreName].Add(skin);
}
}
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(SkinsTab), nameof(SkinsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(SkinsTab __instance)
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? sortedSkins.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > sortedSkins.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
}
}
private static void GenerateHats(SkinsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
var (groupName, skins) = sortedSkins.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), skin);
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentSkinIsEquipped = true;
}
private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = skin.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = skin.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = skin;
__instance.ColorChips.Add(colorChip);
}
}
+114
View File
@@ -0,0 +1,114 @@
using AmongUs.Data;
using HarmonyLib;
using MiraAPI.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Menu;
[HarmonyPatch(typeof(VisorsTab))]
public static class VisorsTabPatches
{
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
private static int currentPage;
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
{
foreach (var item in data)
{
if (!SortedVisors.ContainsKey(item.Key)) SortedVisors.Add(item.Key, []);
SortedVisors[item.Key].Add(item.Visor);
}
}
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
[HarmonyPrefix]
public static bool OnEnablePrefix(VisorsTab __instance)
{
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
GenerateHats(__instance, currentPage);
return false;
}
[HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.Update))]
[HarmonyPrefix]
public static void UpdatePrefix(VisorsTab __instance)
{
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl) || Input.GetKeyDown(KeyCode.LeftArrow))
{
currentPage--;
currentPage = currentPage < 0 ? SortedVisors.Count - 1 : currentPage;
GenerateHats(__instance, currentPage);
}
else if (Input.GetKeyDown(KeyCode.Tab) || Input.GetKeyDown(KeyCode.RightArrow))
{
currentPage++;
currentPage = currentPage > SortedVisors.Count - 1 ? 0 : currentPage;
GenerateHats(__instance, currentPage);
}
}
private static void GenerateHats(VisorsTab __instance, int page)
{
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
__instance.ColorChips.Clear();
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
int hatIndex = 0;
var (groupName, visors) = SortedVisors.ToArray()[page];
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
text.enabled = true;
text.gameObject.transform.localScale = Vector3.one;
text.GetComponent<TextTranslatorTMP>().Destroy();
text.EnableStencilMasking();
text.text = $"{groupName}\nPress Ctrl or Tab to cycle pages";
text.alignment = TextAlignmentOptions.Center;
text.fontSize = 3f;
text.fontSizeMax = 3f;
text.fontSizeMin = 0f;
float xLerp = __instance.XRange.Lerp(0.5f);
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
hatIndex += 5;
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
{
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
hatIndex += 1;
}
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
__instance.currentVisorIsEquipped = true;
}
private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
{
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
colorChip.gameObject.name = visor.ProductId;
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
colorChip.ProductId = visor.ProductId;
colorChip.SelectionHighlight.gameObject.SetActive(false);
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
colorChip.Tag = visor;
__instance.ColorChips.Add(colorChip);
}
}
@@ -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;
}
}
+35 -12
View File
@@ -1,40 +1,63 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Patches.Modifiers;
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CoEndGame))]
public static class EndGameDidWinPatch
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))]
internal static class EndGameDidWinPatch
{
public static void Prefix()
private static List<CachedPlayerData> CachedWinners { get; set; } = [];
public static void Prefix(EndGameResult endGameResult)
{
var gameOverReason = EndGameResult.CachedGameOverReason;
EndGameResult.CachedWinners.Clear();
CachedWinners.Clear();
var gameOverReason = endGameResult.GameOverReason;
var players = GameData.Instance.AllPlayers.ToArray();
for (var i = 0; i < GameData.Instance.PlayerCount; i++)
{
var networkedPlayerInfo = players[i];
if (networkedPlayerInfo == null)
if (!networkedPlayerInfo)
{
continue;
}
var didWin = networkedPlayerInfo.Role.DidWin(gameOverReason);
var modifierWin = networkedPlayerInfo.Object?.GetModifierComponent().GetModifiers<GameModifier>()
.FirstOrDefault(x => x.DidWin(gameOverReason) != null)?.DidWin(gameOverReason);
if (modifierWin != null)
if (!networkedPlayerInfo.Object)
{
didWin = modifierWin.Value;
if (didWin)
{
CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
}
continue;
}
foreach (var modifier in networkedPlayerInfo.Object.GetModifiers<GameModifier>().OrderByDescending(x => x.Priority()))
{
var result = modifier.DidWin(gameOverReason);
if (!result.HasValue) continue;
didWin = result.Value;
break;
}
if (didWin)
{
EndGameResult.CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
CachedWinners.Add(new CachedPlayerData(networkedPlayerInfo));
}
}
}
public static void Postfix(EndGameResult endGameResult)
{
EndGameResult.CachedWinners.Clear();
foreach (var winner in CachedWinners)
{
EndGameResult.CachedWinners.Add(winner);
}
}
}
@@ -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)
@@ -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.Presets))
{
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.Presets.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.Presets.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
}
}
+111 -52
View File
@@ -1,10 +1,11 @@
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;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
@@ -31,13 +32,20 @@ internal static class GameSettingMenuPatches
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
private static Dictionary<int, Vector3> OptionScrolls = [];
[HarmonyPostfix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
// ReSharper disable once InconsistentNaming
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)
@@ -61,7 +69,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)
{
@@ -79,6 +90,7 @@ internal static class GameSettingMenuPatches
/// <param name="__instance">The GameSettingMenu instance.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(GameSettingMenu.Start))]
// ReSharper disable once InconsistentNaming
public static void StartPrefix(GameSettingMenu __instance)
{
_roleBtnOgPos = __instance.RoleSettingsButton.transform.localPosition;
@@ -111,8 +123,13 @@ internal static class GameSettingMenuPatches
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
__instance.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionScrolls[SelectedModIdx] =
__instance.GameSettingsTab.scrollBar.Inner.localPosition;
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins.Length)
{
SelectedModIdx = 0;
}
@@ -129,10 +146,15 @@ internal static class GameSettingMenuPatches
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
RoleSettingMenuPatches.Scrolls[SelectedModIdx] =
__instance.RoleSettingsTab.scrollBar.Inner.localPosition;
OptionScrolls[SelectedModIdx] =
__instance.GameSettingsTab.scrollBar.Inner.localPosition;
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins.Length;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
@@ -209,54 +231,61 @@ 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.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)];
@@ -269,9 +298,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);
@@ -311,6 +342,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)
{
@@ -367,6 +399,11 @@ internal static class GameSettingMenuPatches
ChangeRoleSettingButton(replaceWithModifiers, menu);
}
if (menu.PresetsTab.gameObject.active)
{
menu.ChangeTab(0, false);
}
CleanupTab(settings, roles);
}
@@ -378,13 +415,52 @@ 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);
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
gameOptMenu.Initialize();
if (OptionScrolls.TryGetValue(SelectedModIdx, out var pos))
{
gameOptMenu.scrollBar.Inner.localPosition = pos;
gameOptMenu.scrollBar.UpdateScrollBars();
}
else
{
gameOptMenu.scrollBar.ScrollToTop();
}
}
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{
if (rolesMenu.advancedSettingChildren != null)
@@ -403,41 +479,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();
}
void CleanupSettings(GameOptionsMenu gameOptMenu)
{
ClearOptions(gameOptMenu.Children);
gameOptMenu.Children = null;
gameOptMenu.settingsContainer
.GetComponentsInChildren<CategoryHeaderMasked>()
.ToList()
.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);
if (SelectedModIdx == 0)
{
if (RoleSettingMenuPatches.Scrolls.TryGetValue(0, out var pos))
{
rolesMenu.scrollBar.Inner.localPosition = pos;
rolesMenu.scrollBar.UpdateScrollBars();
}
else
{
rolesMenu.scrollBar.ScrollToTop();
}
}
}
}
}
+16 -16
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);
@@ -127,7 +127,7 @@ public static class LobbyViewPanePatches
__instance.rolesTabButton.SelectButton(false);
ModifiersTabButton?.SelectButton(true);
var filteredGroups = SelectedMod.OptionGroups
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
DrawOptions(__instance, filteredGroups);
return false;
@@ -142,7 +142,7 @@ public static class LobbyViewPanePatches
return true;
}
var filteredGroups = SelectedMod.OptionGroups
var filteredGroups = SelectedMod.InternalOptionGroups
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
DrawOptions(__instance, filteredGroups);
return false;
@@ -250,14 +250,9 @@ public static class LobbyViewPanePatches
var list = new List<Type>();
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
var roleGroups = SelectedMod.InternalRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (roleGroups is null)
{
return;
}
// sort the groups by priority
var sortedRoleGroups = roleGroups
.OrderBy(x => x.Key.Priority)
@@ -265,7 +260,7 @@ public static class LobbyViewPanePatches
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any())
if (!grouping.Any() || grouping.All(x => x.Configuration.HideSettings))
{
continue;
}
@@ -296,6 +291,11 @@ public static class LobbyViewPanePatches
foreach (var customRole in grouping)
{
if (customRole.Configuration.HideSettings)
{
continue;
}
var roleBehaviour = customRole as RoleBehaviour;
if (roleBehaviour == null)
{
@@ -315,7 +315,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();
@@ -340,7 +340,7 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
viewSettingsInfoPanelRoleVariant.chanceBackground.color =
viewSettingsInfoPanelRoleVariant.background.color =
customRole.RoleColor.GetAlternateColor();
customRole.RoleColor.FindAlternateColor();
instance.settingsInfo.Add(viewSettingsInfoPanelRoleVariant.gameObject);
num -= 0.664f;
}
@@ -402,7 +402,7 @@ public static class LobbyViewPanePatches
return 0;
}
var role = SelectedMod.CustomRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
var role = SelectedMod.InternalRoles.Values.FirstOrDefault(x => x.GetType() == roleType);
if (role == null)
{
@@ -425,7 +425,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();
@@ -22,15 +22,16 @@ 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> Scrolls { get; } = [];
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; } = [];
private static List<GameObject> Headers { get; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; } = [];
private static float ScrollerNum { get; set; } = 0.522f;
[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);
@@ -67,7 +68,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)
@@ -83,10 +84,9 @@ public static class RoleSettingMenuPatches
var quotaThing = __instance.categoryHeaderEditRoleOrigin.transform.FindChild("QuotaHeader");
var template = __instance.transform.parent.parent.GetComponent<GameSettingMenu>().GameSettingsTab.categoryHeaderOrigin;
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any())
if (!grouping.Any() || grouping.All(x=>x.Configuration.HideSettings))
{
continue;
}
@@ -233,6 +233,15 @@ public static class RoleSettingMenuPatches
private static void SetScrollBounds(this Scroller scroller)
{
scroller.CalculateAndSetYBounds(1 + 1.5f * Headers.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
if (Scrolls.TryGetValue(GameSettingMenuPatches.SelectedModIdx, out var scroll))
{
scroller.Inner.localPosition = scroll;
scroller.UpdateScrollBars();
}
else
{
scroller.ScrollToTop();
}
}
[HarmonyPrefix]
@@ -246,13 +255,12 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar)
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return;
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.ScrollToTop();
}
private static void ValueChanged(OptionBehaviour obj)
@@ -310,7 +318,7 @@ public static class RoleSettingMenuPatches
var num = hasImage ? -0.872f : -1;
// TODO: create sub groups under the role settings.
var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups
var filteredOptions = GameSettingMenuPatches.SelectedMod?.InternalOptionGroups
.Where(x=> x.OptionableType == role.GetType())
.SelectMany(x=>x.Options)
.ToList() ?? [];
@@ -345,6 +353,7 @@ public static class RoleSettingMenuPatches
newOpt.Initialize();
}
Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.scrollBar.CalculateAndSetYBounds(__instance.advancedSettingChildren.Count + 3, 1f, 6f, 0.45f);
__instance.scrollBar.ScrollToTop();
}
@@ -357,6 +366,8 @@ public static class RoleSettingMenuPatches
return;
}
Scrolls[GameSettingMenuPatches.SelectedModIdx] = __instance.scrollBar.Inner.localPosition;
__instance.roleDescriptionText.text = customRole.RoleLongDescription;
__instance.roleTitleText.text = TranslationController.Instance.GetString(
role.StringName,
@@ -474,7 +485,7 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.OptionGroups
GameSettingMenuPatches.SelectedMod.InternalOptionGroups
.Exists(x => x.OptionableType == role.GetType()))
{
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
@@ -506,7 +517,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;
}
+6 -35
View File
@@ -15,14 +15,11 @@ namespace MiraAPI.Patches;
/// General patches for the PlayerControl class.
/// </summary>
[HarmonyPatch(typeof(PlayerControl))]
public static class PlayerControlPatches
internal static class PlayerControlPatches
{
/// <summary>
/// Adds the modifier component to the player on start.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Start))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlStartPostfix(PlayerControl __instance)
{
if (__instance.gameObject.TryGetComponent<ModifierComponent>(out var modifierComp))
@@ -39,12 +36,9 @@ public static class PlayerControlPatches
__instance.gameObject.AddComponent<PlayerVoteData>();
}
/// <summary>
/// Calls the OnDeath method for all active modifiers.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.Die))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlDiePostfix(PlayerControl __instance, DeathReason reason)
{
var deathEvent = new PlayerDeathEvent(__instance, reason);
@@ -58,13 +52,9 @@ public static class PlayerControlPatches
}
}
/// <summary>
/// Used to trigger the <see cref="CompleteTaskEvent"/>.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
/// <param name="idx">The task id.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.CompleteTask))]
// ReSharper disable once InconsistentNaming
public static void PlayerCompleteTaskPostfix(PlayerControl __instance, uint idx)
{
var playerTask = __instance.myTasks.ToArray().First(playerTask => playerTask.Id == idx);
@@ -75,14 +65,9 @@ public static class PlayerControlPatches
}
}
/// <summary>
/// Used to trigger the <see cref="BeforeMurderEvent"/>.
/// </summary>
/// <param name="__instance">The source player.</param>
/// <param name="target">The target.</param>
/// <param name="didSucceed">Whether the kill succeeded.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.RpcMurderPlayer))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlMurderPrefix(PlayerControl __instance, PlayerControl target, ref bool didSucceed)
{
var beforeMurderEvent = new BeforeMurderEvent(__instance, target);
@@ -91,12 +76,9 @@ public static class PlayerControlPatches
didSucceed = beforeMurderEvent.IsCancelled;
}
/// <summary>
/// FixedUpdate handler for custom roles and custom buttons.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPostfix]
[HarmonyPatch(nameof(PlayerControl.FixedUpdate))]
// ReSharper disable once InconsistentNaming
public static void PlayerControlFixedUpdatePostfix(PlayerControl __instance)
{
if (__instance.Data?.Role is ICustomRole customRole)
@@ -124,15 +106,4 @@ public static class PlayerControlPatches
button.FixedUpdateHandler(__instance);
}
}
/// <summary>
/// Clear from modifier component cache.
/// </summary>
/// <param name="__instance">PlayerControl instance.</param>
[HarmonyPrefix]
[HarmonyPatch(nameof(PlayerControl.OnDestroy))]
public static void PlayerControlOnDestroyPrefix(PlayerControl __instance)
{
ModifierExtensions.ModifierComponents.Remove(__instance);
}
}
+16 -3
View File
@@ -1,5 +1,8 @@
using HarmonyLib;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Roles;
@@ -9,7 +12,7 @@ namespace MiraAPI.Patches.Roles;
/// Patches for custom ejection messages.
/// </summary>
[HarmonyPatch(typeof(ExileController))]
public static class EjectionPatches
internal static class EjectionPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(ExileController.Begin))]
@@ -24,11 +27,21 @@ public static class EjectionPatches
return;
}
if (role.GetCustomEjectionMessage(__instance.initData.networkedPlayer) == null)
if (!GameManager.Instance.LogicOptions.GetConfirmImpostor() || role.GetCustomEjectionMessage(__instance.initData.networkedPlayer) == null)
{
return;
}
__instance.completeString = role.GetCustomEjectionMessage(__instance.initData.networkedPlayer);
}
[HarmonyPatch(typeof(ExileController), nameof(ExileController.WrapUp))]
public static class WrapUpPatch
{
public static void Postfix()
{
var @event = new RoundStartEvent(false);
MiraEventManager.InvokeEvent(@event);
}
}
}
+42 -2
View File
@@ -5,18 +5,58 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleManager))]
public static class RoleManagerPatches
{
[HarmonyPostfix]
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleManager.SetRole))]
public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
public static bool SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
{
if (!targetPlayer)
{
return false;
}
var data = targetPlayer.Data;
if (data == null)
{
Debug.LogError("It shouldn't be possible, but " + targetPlayer.name + " still doesn't have PlayerData during role selection.");
return false;
}
if (data.Role)
{
data.Role.Deinitialize(targetPlayer);
Object.Destroy(data.Role.gameObject);
}
var roleBehaviour = Object.Instantiate<RoleBehaviour>(__instance.AllRoles.First(r => r.Role == roleType), data.gameObject.transform);
roleBehaviour.Initialize(targetPlayer);
targetPlayer.Data.Role = roleBehaviour;
targetPlayer.Data.RoleType = roleType;
// basically everything is the same except this one if statement
// innersloth decided to check the roleType manually instead of the role behaviour.
if (!roleBehaviour.IsDead)
{
targetPlayer.Data.RoleWhenAlive = new Il2CppSystem.Nullable<RoleTypes>(roleType);
}
roleBehaviour.AdjustTasks(targetPlayer);
switch (roleBehaviour.IsDead)
{
case true when !targetPlayer.Data.IsDead:
targetPlayer.Die(DeathReason.Kill, false);
return false;
case false when targetPlayer.Data.IsDead:
targetPlayer.Revive();
break;
}
var @event = new SetRoleEvent(targetPlayer, roleType);
MiraEventManager.InvokeEvent(@event);
return false;
}
[HarmonyPostfix]
@@ -8,7 +8,7 @@ namespace MiraAPI.Patches.Roles;
/// <summary>
/// Patches to return the correct role counts.
/// </summary>
[HarmonyPatch(typeof(RoleOptionsCollectionV08))]
[HarmonyPatch(typeof(RoleOptionsCollectionV09))]
public static class RoleOptionsCollectionPatch
{
/// <summary>
@@ -16,7 +16,7 @@ public static class RoleOptionsCollectionPatch
/// </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)
@@ -46,7 +46,7 @@ public static class RoleOptionsCollectionPatch
/// </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)
-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);
}
}
+8 -5
View File
@@ -4,6 +4,9 @@ using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
/// <summary>
/// Stub methods for the RoleBehaviour class. Needed because of Il2Cpp limitations on injected classes.
/// </summary>
[HarmonyPatch]
public static class RoleBehaviourStubs
{
@@ -57,7 +60,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CanUse(RoleBehaviour instance, IUsable console)
{
return default;
return false;
}
/// <summary>
@@ -84,7 +87,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason)
{
return default;
return false;
}
/// <summary>
@@ -97,7 +100,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static PlayerControl FindClosestTarget(RoleBehaviour instance)
{
return null;
return null!;
}
/// <summary>
@@ -110,7 +113,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static float GetAbilityDistance(RoleBehaviour instance)
{
return default;
return 0;
}
/// <summary>
@@ -124,7 +127,7 @@ public static class RoleBehaviourStubs
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target)
{
return default;
return false;
}
/// <summary>
@@ -8,17 +8,13 @@ using UnityEngine;
namespace MiraAPI.Patches;
/// <summary>
/// Vent patches to make sure the player is able to use the vent.
/// Used to change vent behaviour for the event system, custom roles, and modifiers.
/// </summary>
[HarmonyPatch(typeof(Vent))]
public static class VentPatches
[HarmonyPatch(typeof(Vent), nameof(Vent.CanUse))]
internal static class VentCanUsePatch
{
/// <summary>
/// CanUse patch.
/// </summary>
[HarmonyPostfix]
[HarmonyPatch(nameof(Vent.CanUse))]
public static void VentCanUsePostfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
// ReSharper disable InconsistentNaming
public static void Postfix(Vent __instance, ref float __result, [HarmonyArgument(0)] NetworkedPlayerInfo pc, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse)
{
var @event = new PlayerCanUseEvent(__instance.Cast<IUsable>());
MiraEventManager.InvokeEvent(@event);
+65 -80
View File
@@ -1,15 +1,19 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Voting;
using Reactor.Utilities;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(MeetingHud))]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")]
internal static class MeetingHudPatches
{
[HarmonyPostfix]
@@ -26,11 +30,62 @@ internal static class MeetingHudPatches
{
voteData.SetRemainingVotes(0);
}
foreach (var modifier in plr.GetModifierComponent().ActiveModifiers)
{
modifier.OnMeetingStart();
}
}
var @event = new StartMeetingEvent(__instance);
MiraEventManager.InvokeEvent(@event);
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.OnDestroy))]
public static void OnDestroyPatch(MeetingHud __instance)
{
MiraEventManager.InvokeEvent(new EndMeetingEvent(__instance));
}
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.VotingComplete))]
public static void VotingCompletePatch(MeetingHud __instance)
{
MiraEventManager.InvokeEvent(new VotingCompleteEvent(__instance));
}
// this is necessary because the actual ForceSkipAll method is inlined.
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Update))]
public static void ForceSkipPatch(MeetingHud __instance)
{
if (__instance.state is not (MeetingHud.VoteStates.NotVoted or MeetingHud.VoteStates.Voted))
{
return;
}
var logicOptionsNormal = GameManager.Instance.LogicOptions.Cast<LogicOptionsNormal>();
var votingTime = logicOptionsNormal.GetVotingTime();
if (votingTime <= 0)
{
return;
}
var num2 = __instance.discussionTimer - logicOptionsNormal.GetDiscussionTime();
if (!AmongUsClient.Instance.AmHost || num2 < votingTime)
{
return;
}
foreach (var plr in PlayerControl.AllPlayerControls)
{
var voteData = plr.GetVoteData();
voteData.SetRemainingVotes(0);
}
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Select))]
public static bool SelectPatch(MeetingHud __instance, int suspectStateIdx)
@@ -86,84 +141,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)
@@ -226,12 +203,20 @@ 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))]
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;
}
}
+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.
+19 -4
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()
@@ -43,6 +45,9 @@ public sealed class MiraPluginManager
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())
{
if (type.GetCustomAttribute<MiraIgnoreAttribute>() != null)
@@ -96,17 +101,27 @@ public sealed class MiraPluginManager
RegisterColorClasses(type);
}
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.InternalOptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
info.SavePublicCollections();
PresetManager.CreateDefaultPreset(info);
PresetManager.LoadPresets(info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers;
IL2CPPChainloader.Instance.Finished += () =>
{
// Save all buttons into a read-only collection for easy access
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
// Cache all the registered plugins into an array for easy access
RegisteredPlugins = [.._registeredPlugins.Values];
};
}
+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();
}
}
+95
View File
@@ -0,0 +1,95 @@
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"))
{
Logger<MiraApiPlugin>.Info($"Loading preset file {file}");
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];
}
}
+91
View File
@@ -0,0 +1,91 @@
using System;
using Il2CppInterop.Runtime.InteropTypes.Fields;
using MiraAPI.Modifiers.ModifierDisplay;
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;
[RegisterInIl2Cpp]
public class SavePresetPopup(nint ptr) : Minigame(ptr)
{
private PassiveButton saveButton;
private PassiveButton closeButton;
private TextMeshPro textBoxText;
private TextBoxTMP textBox;
private Action<string> onSave;
// Cleanup holder object
public void OnDestroy()
{
transform.parent.gameObject.Destroy();
}
public override void Close()
{
}
private void Awake()
{
var textboxHolder = transform.GetChild(1).GetChild(1);
saveButton = transform.FindChild("SaveButton").GetComponent<PassiveButton>();
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,
},
};
holder.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.
-2
View File
@@ -1,8 +1,6 @@
using AmongUs.GameOptions;
using Il2CppSystem.Web.Util;
using MiraAPI.Utilities.Assets;
using UnityEngine;
using Helpers = MiraAPI.Utilities.Helpers;
namespace MiraAPI.Roles;
+10 -5
View File
@@ -14,7 +14,6 @@ using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
@@ -60,6 +59,9 @@ public static class CustomRoleManager
{
roles.ForEach(x => RoleIds.Add(x, GetNextRoleId()));
var oldConfigSetting = pluginInfo.PluginConfig.SaveOnConfigSet;
pluginInfo.PluginConfig.SaveOnConfigSet = false;
foreach (var roleType in roles)
{
ClassInjector.RegisterTypeInIl2Cpp(roleType);
@@ -70,8 +72,11 @@ public static class CustomRoleManager
continue;
}
pluginInfo.CustomRoles.Add((ushort)role.Role, role);
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
}
pluginInfo.PluginConfig.Save();
pluginInfo.PluginConfig.SaveOnConfigSet = oldConfigSetting;
}
private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod)
@@ -156,7 +161,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>
@@ -231,7 +236,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;
@@ -266,7 +271,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];
+39 -1
View File
@@ -3,7 +3,6 @@ using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using Reactor.Utilities;
using UnityEngine;
@@ -85,6 +84,45 @@ public interface ICustomRole : IOptionable
internal ConfigDefinition NumConfigDefinition => new("Roles", $"Num {GetType().FullName}");
internal ConfigDefinition ChanceConfigDefinition => new("Roles", $"Chance {GetType().FullName}");
/// <summary>
/// Binds the configuration options for this role to the provided ConfigFile.
/// </summary>
/// <param name="config">The ConfigFile to bind the options to.</param>
public virtual void BindConfig(ConfigFile config)
{
config.Bind(NumConfigDefinition, Configuration.DefaultRoleCount);
config.Bind(ChanceConfigDefinition, Configuration.DefaultChance);
}
/// <summary>
/// Saves the role's configuration to a preset ConfigFile.
/// </summary>
/// <param name="presetConfig">The ConfigFile to save the preset configuration to.</param>
/// <param name="useDefault">Whether to use the default values for the configuration.</param>
public virtual void SaveToPreset(ConfigFile presetConfig, bool useDefault=false)
{
BindConfig(presetConfig);
presetConfig[NumConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultRoleCount : GetCount();
presetConfig[ChanceConfigDefinition].BoxedValue = useDefault ? Configuration.DefaultChance : GetChance();
}
/// <summary>
/// Loads the role's configuration from a preset ConfigFile.
/// </summary>
/// <param name="presetConfig">The ConfigFile containing the preset configuration.</param>
public virtual void LoadFromPreset(ConfigFile presetConfig)
{
if (presetConfig.TryGetEntry(NumConfigDefinition, out ConfigEntry<int> numEntry))
{
SetCount(numEntry.Value);
}
if (presetConfig.TryGetEntry(ChanceConfigDefinition, out ConfigEntry<int> chanceEntry))
{
SetChance(chanceEntry.Value);
}
}
/// <summary>
/// Gets the role chance option.
/// </summary>
@@ -0,0 +1,222 @@
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Menu;
using Reactor.Utilities;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// The component for loading Addressables, and cosmetics.
/// </summary>
public static class AddressablesLoader
{
private static bool _isInitialized;
private static readonly List<(string Location, string ProviderSuffix)> CatalogLocations = [];
private static readonly List<string> LoadedLocations = [];
private static readonly List<string> RegisteredHatKeys = [];
private static readonly List<(string, string)> RegisteredVisorKeys = [];
private static readonly List<(string, string)> RegisteredNameplateKeys = [];
private static readonly List<string> RegisteredSkinKeys = [];
/// <summary>
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
/// </summary>
/// <param name="location">The location, remote or otherwise, of the addressables.</param>
/// <param name="providerSuffix">The suffix of the provider for an addressables package.</param>
public static void RegisterCatalog(string location, string providerSuffix = "")
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
return;
}
CatalogLocations.Add((location, providerSuffix));
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="HatData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="HatData"/>.</param>
public static void RegisterHats(string addressablesKey)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
return;
}
RegisteredHatKeys.Add(addressablesKey);
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="SkinData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="SkinData"/>.</param>
public static void RegisterSkins(string addressablesKey)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
return;
}
RegisteredSkinKeys.Add(addressablesKey);
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="NamePlateData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="NamePlateData"/>.</param>
/// /// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterNameplates(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
return;
}
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
}
/// <summary>
/// Registers a specific addressables key as only containing <see cref="VisorData"/>'s to load.
/// </summary>
/// <param name="addressablesKey">The key/label/group for a List <see cref="VisorData"/>.</param>
/// <param name="groupTitle">The title of the group for visors.</param>
public static void RegisterVisors(string addressablesKey, string groupTitle)
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
return;
}
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
}
internal static void LoadAll()
{
if (_isInitialized)
{
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized.");
return;
}
_isInitialized = true;
foreach (var (location, providerSuffix) in CatalogLocations)
{
Coroutines.Start(CoLoadAddressables(location, providerSuffix));
}
Coroutines.Start(LoadCosmetics());
}
internal static IEnumerator CoLoadAddressables(string location, string suffix = "")
{
while (!AmongUsClient.Instance) yield return null;
// Load the local/remote content catalog
var catalogOperation = Addressables.LoadContentCatalog(location, suffix);
yield return catalogOperation;
// Check for errors
if (catalogOperation.Status != AsyncOperationStatus.Succeeded)
{
Logger<MiraApiPlugin>.Error($"Failed to load catalog {location}.");
}
Logger<MiraApiPlugin>.Info($"Loaded addressables {location}.");
LoadedLocations.Add(location);
}
[HideFromIl2Cpp]
internal static IEnumerator LoadCosmetics()
{
while (!AmongUsClient.Instance || CatalogLocations.Select(x=>x.Location).Any(x=>!LoadedLocations.Contains(x))) yield return null;
var hatBehaviours = DiscoverData<HatData>(RegisteredHatKeys);
var skinBehaviours = DiscoverData<SkinData>(RegisteredSkinKeys);
var namePlateBehaviours = DiscoverAndReportData<NamePlateData>(RegisteredNameplateKeys);
var visorBehaviours = DiscoverAndReportData<VisorData>(RegisteredVisorKeys);
var hatData = new List<HatData>();
hatData.AddRange(DestroyableSingleton<HatManager>.Instance.allHats);
hatData.ForEach(x => x.StoreName = "Vanilla");
DestroyableSingleton<HatManager>.Instance.allHats = PrepareArray(hatData, hatBehaviours);
var skinData = new List<SkinData>();
skinData.AddRange(DestroyableSingleton<HatManager>.Instance.allSkins);
skinData.ForEach(x => x.StoreName = "Vanilla");
DestroyableSingleton<HatManager>.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
var visorData = new List<VisorData>();
visorData.AddRange(DestroyableSingleton<HatManager>.Instance.allVisors);
VisorsTabPatches.AddRange(visorBehaviours);
DestroyableSingleton<HatManager>.Instance.allVisors = PrepareArray(visorData, visorBehaviours.Select(x=>x.Data).ToList());
var namePlateData = new List<NamePlateData>();
namePlateData.AddRange(DestroyableSingleton<HatManager>.Instance.allNamePlates);
NameplatesTabPatches.AddRange(namePlateBehaviours);
DestroyableSingleton<HatManager>.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours.Select(x => x.Data).ToList());
}
private static T[] PrepareArray<T>(List<T> data, List<T> behaviours) where T : CosmeticData
{
var count = data.Count;
for (int i = 0; i < behaviours.Count; i++)
{
behaviours[i].displayOrder = count + i;
data.Add(behaviours[i]);
}
return data.ToArray();
}
private static List<T> DiscoverData<T>(List<string> tags)
{
var behaviours = new List<T>();
foreach (var tag in tags)
{
try
{
var allLocations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion();
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray());
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
}
}
return behaviours;
}
private static List<(string Category, T Data)> DiscoverAndReportData<T>(List<(string Tag, string Category)> tags)
{
var behaviours = new List<(string, T)>();
foreach (var tag in tags)
{
try
{
var allLocations = Addressables.LoadResourceLocationsAsync(tag.Tag).WaitForCompletion();
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x)));
}
catch
{
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
}
}
return behaviours;
}
}
@@ -0,0 +1,55 @@
using System;
using Reactor.Utilities;
using UnityEngine.AddressableAssets;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading an asset from ALL addressable locations.
/// </summary>
/// <param name="uid">The guid of the asset.</param>
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
public class LoadableAddressableAsset<T>(string uid) : LoadableAsset<T> where T : UnityEngine.Object
{
private readonly Action<LoadableAddressableAsset<T>>? _gcAction;
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAddressableAsset{T}"/> class, with added garbageCollector logic,
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
/// </summary>
/// <param name="uid">The guid of the asset.</param>
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
public LoadableAddressableAsset(string uid, Action<LoadableAddressableAsset<T>> garbageCollection) : this(uid)
{
_gcAction = garbageCollection;
}
/// <summary>
/// Loads the asset from addressables.
/// </summary>
/// <returns>The asset.</returns>
/// <exception cref="Exception">The asset did not load properly.</exception>
public override T LoadAsset()
{
if (LoadedAsset != null)
{
return LoadedAsset;
}
var remoteReference = new AssetReference(uid);
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
LoadedAsset = remoteReference.LoadAssetAsync<T>().WaitForCompletion();
if (LoadedAsset == null)
{
GC.EndNoGCRegion();
throw new InvalidOperationException($"INVALID ASSET: {uid}");
}
_gcAction?.Invoke(this);
GC.EndNoGCRegion();
return LoadedAsset;
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Reactor.Utilities;
using UnityEngine.AddressableAssets;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// A utility class for loading groups of assets from ALL addressable locations.
/// </summary>
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
public class LoadableAddressableAssets<T>(string key) where T : UnityEngine.Object
{
private readonly Action<LoadableAddressableAssets<T>>? _gcAction;
/// <summary>
/// Gets or sets reference to the loaded asset. Intended to be used for caching purposes.
/// </summary>
protected List<T>? LoadedAssets { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAddressableAssets{T}"/> class, with added garbageCollector logic,
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
/// </summary>
/// <param name="key">The key of the assets.</param>
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
public LoadableAddressableAssets(string key, Action<LoadableAddressableAssets<T>> garbageCollection) : this(key)
{
_gcAction = garbageCollection;
}
/// <summary>
/// Loads the asset from addressables.
/// </summary>
/// <returns>The asset.</returns>
/// <exception cref="Exception">The asset did not load properly.</exception>
public ReadOnlyCollection<T> LoadAssets()
{
if (LoadedAssets != null)
{
return LoadedAssets.AsReadOnly();
}
var locations = Addressables.LoadResourceLocationsAsync(key).WaitForCompletion();
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
var assetsIList = Addressables.LoadAssetsAsync<T>(locations, null, false).WaitForCompletion();
if (assetsIList == null)
{
GC.EndNoGCRegion();
throw new InvalidOperationException($"INVALID ASSET/s: {key}");
}
var assetsList = new Il2CppSystem.Collections.Generic.List<T>(assetsIList.Pointer);
LoadedAssets = assetsList.ToArray().ToList();
_gcAction?.Invoke(this);
GC.EndNoGCRegion();
return LoadedAssets.AsReadOnly();
}
}
@@ -0,0 +1,25 @@
using Reactor.Utilities.Extensions;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// Wrapper class to turn any UnityEngine.Object into a <see cref="LoadableAsset{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the asset to be loaded. Must be a subclass of <see cref="UnityEngine.Object"/>.</typeparam>
public class LoadableAssetWrapper<T> : LoadableAsset<T> where T : UnityEngine.Object
{
/// <summary>
/// Initializes a new instance of the <see cref="LoadableAssetWrapper{T}"/> class.
/// </summary>
/// <param name="asset">The asset to wrap. This should be a reference to a UnityEngine.Object.</param>
public LoadableAssetWrapper(T asset)
{
LoadedAsset = asset.DontDestroy();
}
/// <inheritdoc />
public override T LoadAsset()
{
return LoadedAsset ?? throw new System.InvalidOperationException("The asset could not be loaded!");
}
}
+71 -9
View File
@@ -1,16 +1,78 @@
using UnityEngine;
using Reactor.Utilities;
using UnityEngine;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// Various assets used throughout Mira API.
/// </summary>
public static class MiraAssets
{
public static readonly LoadableResourceAsset Empty = new("MiraAPI.Resources.Empty.png");
static MiraAssets()
{
var boxTex = SpriteTools.LoadTextureFromResourcePath(
"MiraAPI.Resources.RoundedBox.png",
System.Reflection.Assembly.GetCallingAssembly());
var boxSprite = Sprite.Create(
boxTex,
new Rect(0, 0, boxTex.width, boxTex.height),
new Vector2(0.5f, 0.5f),
100f,
0U,
SpriteMeshType.Tight,
new Vector4(20, 20, 20, 20));
public static readonly LoadableResourceAsset NextButton = new("MiraAPI.Resources.NextButton.png");
public static readonly LoadableResourceAsset NextButtonActive = new("MiraAPI.Resources.NextButtonActive.png");
public static readonly LoadableResourceAsset Cog = new("MiraAPI.Resources.Cog.png");
public static readonly LoadableResourceAsset Checkmark = new("MiraAPI.Resources.Checkmark.png");
public static readonly LoadableResourceAsset CategoryHeader = new("MiraAPI.Resources.CategoryHeader.png");
public static readonly LoadableResourceAsset CheckmarkBox = new("MiraAPI.Resources.CheckMarkBox.png");
public static readonly Color32 AcceptedTeal = new(43, 233, 198, 255);
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
}
internal static readonly AssetBundle MiraAssetBundle = AssetBundleManager.Load("mirabundle");
internal static readonly LoadableAsset<GameObject> ModifierDisplay = new LoadableBundleAsset<GameObject>("Modifiers", MiraAssetBundle);
internal static readonly LoadableAsset<GameObject> PresetSavePopup = new LoadableBundleAsset<GameObject>("PresetSavePopup", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> RefreshIcon = new LoadableBundleAsset<Sprite>("refresh", MiraAssetBundle);
internal static readonly LoadableAsset<Sprite> FolderIcon = new LoadableBundleAsset<Sprite>("freePlay_folderTaskRoom", MiraAssetBundle);
/// <summary>
/// Gets the accepted teal color used for various selections in the UI.
/// </summary>
public static Color32 AcceptedTeal { get; } = new(43, 233, 198, 255);
/// <summary>
/// Gets the empty sprite.
/// </summary>
public static LoadableAsset<Sprite> Empty { get; } = new LoadableResourceAsset("MiraAPI.Resources.Empty.png");
/// <summary>
/// Gets the RoundedBox sprite, which is a rounded rectangle used for UI elements.
/// </summary>
public static LoadableAsset<Sprite> RoundedBox { get; }
/// <summary>
/// Gets the NextButton sprite.
/// </summary>
public static LoadableAsset<Sprite> NextButton { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButton.png");
/// <summary>
/// Gets the active variant of the NextButton sprite.
/// </summary>
public static LoadableAsset<Sprite> NextButtonActive { get; } = new LoadableResourceAsset("MiraAPI.Resources.NextButtonActive.png");
/// <summary>
/// Gets the Cog icon sprite.
/// </summary>
public static LoadableAsset<Sprite> Cog { get; } = new LoadableResourceAsset("MiraAPI.Resources.Cog.png");
/// <summary>
/// Gets the Checkmark icon sprite.
/// </summary>
public static LoadableAsset<Sprite> Checkmark { get; } = new LoadableResourceAsset("MiraAPI.Resources.Checkmark.png");
/// <summary>
/// Gets the Checkmark Box sprite.
/// </summary>
public static LoadableAsset<Sprite> CheckmarkBox { get; } = new LoadableResourceAsset("MiraAPI.Resources.CheckMarkBox.png");
/// <summary>
/// Gets the custom category header sprite used in the options menu.
/// </summary>
public static LoadableAsset<Sprite> CategoryHeader { get; } = new LoadableResourceAsset("MiraAPI.Resources.CategoryHeader.png");
}
+3 -3
View File
@@ -3,17 +3,17 @@
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// An implementation for creating loadable assets from preloaded assets.
/// An implementation for creating loadable assets from existing assets.
/// </summary>
/// <inheritdoc cref="LoadableAsset{T}"/>
/// <inheritdoc />
public class PreloadedAsset<T>(T asset) : LoadableAsset<T> where T : UnityEngine.Object
{
private readonly T loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
private readonly T _loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
/// <inheritdoc />
public override T LoadAsset()
{
return loadedAsset;
return _loadedAsset;
}
}
+19 -6
View File
@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Reflection;
using Reactor.Utilities.Extensions;
using UnityEngine;
@@ -12,12 +11,13 @@ namespace MiraAPI.Utilities.Assets;
public static class SpriteTools
{
/// <summary>
/// Load a sprite from a resource path.
/// Loads and returns a texture from a resource path using the specified assembly.
/// </summary>
/// <param name="resourcePath">The path to the resource.</param>
/// <returns>A sprite made from the resource.</returns>
/// <exception cref="Exception">The resource cannot be found.</exception>
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
/// <param name="resourcePath">The path to the resource within the assembly.</param>
/// <param name="assembly">The assembly from which to load the resource.</param>
/// <returns>A <see cref="Texture2D"/> object loaded from the specified resource path.</returns>
/// <exception cref="ArgumentException">Thrown when the resource cannot be found in the specified assembly.</exception>
public static Texture2D LoadTextureFromResourcePath(string resourcePath, Assembly assembly)
{
var tex = new Texture2D(1, 1, TextureFormat.ARGB32, false);
var myStream = assembly.GetManifestResourceStream(resourcePath);
@@ -32,6 +32,19 @@ public static class SpriteTools
}
tex.name = resourcePath;
return tex;
}
/// <summary>
/// Loads and returns a <see cref="Sprite"/> from a resource path using the specified assembly.
/// </summary>
/// <param name="resourcePath">The path to the resource within the assembly.</param>
/// <param name="assembly">The assembly from which to load the resource.</param>
/// <param name="pixelsPerUnit">The number of pixels per unit for the sprite.</param>
/// <returns>A <see cref="Sprite"/> object created from the texture loaded from the specified resource path.</returns>
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
sprite.name = resourcePath;
return sprite;
+3 -3
View File
@@ -8,16 +8,16 @@ namespace MiraAPI.Utilities;
public static class ColorExtensions
{
/// <summary>
/// Gets the relative luminance of a color (WCAG 2.1 specification).
/// Gets the relative luminance of a color (WCAG 2.1 specification). 0 is black, 1 is white.
/// </summary>
/// <param name="color">Color to calculate luminance for.</param>
/// <returns>Relative luminance value.</returns>
/// <returns>Luminance value between 0 and 1.</returns>
public static float GetRelativeLuminance(this Color color)
{
var r = color.r <= 0.03928f ? color.r / 12.92f : Mathf.Pow((color.r + 0.055f) / 1.055f, 2.4f);
var g = color.g <= 0.03928f ? color.g / 12.92f : Mathf.Pow((color.g + 0.055f) / 1.055f, 2.4f);
var b = color.b <= 0.03928f ? color.b / 12.92f : Mathf.Pow((color.b + 0.055f) / 1.055f, 2.4f);
return 0.2126f * r + 0.7152f * g + 0.0722f * b;
return (0.2126f * r) + (0.7152f * g) + (0.0722f * b);
}
/// <summary>
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Utilities;
internal sealed class ControllableComparer<T> : IComparer<T> where T : IComparable
{
private readonly T[] _forcedToBottom;
private readonly T[] _forcedToTop;
private readonly IComparer<T> _fallbackComparer;
public ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer<T> fallbackComparer)
{
_forcedToBottom = forcedToBottom;
_forcedToTop = forcedToTop;
_fallbackComparer = fallbackComparer;
}
/// <inheritdoc/>
public int Compare(T? x, T? y)
{
if ((_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) || (_forcedToTop.Contains(x) && _forcedToTop.Contains(y)))
return _fallbackComparer.Compare(x, y);
if (_forcedToBottom.Contains(x))
return 1;
if (_forcedToBottom.Contains(y))
return -1;
if (_forcedToTop.Contains(x))
return -1;
if (_forcedToTop.Contains(y))
return 1;
return _fallbackComparer.Compare(x, y);
}
}
+45 -1
View File
@@ -41,6 +41,49 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
/// <summary>
/// Used if you override Minigame.Close.
/// </summary>
/// <param name="self">The minigame.</param>
public static void BaseClose(this Minigame self)
{
bool isComplete;
if (self.amClosing == Minigame.CloseState.Closing)
{
UnityEngine.Object.Destroy(self.gameObject);
return;
}
if (self.CloseSound && Constants.ShouldPlaySfx())
{
SoundManager.Instance.PlaySound(self.CloseSound, false, 1f, null);
}
if (PlayerControl.LocalPlayer.Data.Role.TeamType == RoleTeamTypes.Crewmate)
{
GameManager.Instance.LogicMinigame.OnMinigameClose();
}
if (PlayerControl.LocalPlayer)
{
PlayerControl.HideCursorTemporarily();
}
self.amClosing = Minigame.CloseState.Closing;
self.logger.Info(string.Concat("Closing minigame ", self.GetType().Name));
IAnalyticsReporter analytics = DestroyableSingleton<DebugAnalytics>.Instance.Analytics;
NetworkedPlayerInfo data = PlayerControl.LocalPlayer.Data;
TaskTypes taskType = self.TaskType;
float realtimeSinceStartup = Time.realtimeSinceStartup - self.timeOpened;
PlayerTask myTask = self.MyTask;
if (myTask != null)
{
isComplete = myTask.IsComplete;
}
else
{
isComplete = false;
}
analytics.MinigameClosed(data, taskType, realtimeSinceStartup, isComplete);
self.StartCoroutine(self.CoDestroySelf());
}
/// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary>
@@ -283,12 +326,13 @@ public static class Extensions
if (count + length > chunkSize)
{
chunks.Enqueue(current.ToArray());
chunks.Enqueue([.. current]);
current.Clear();
count = 0;
}
current.Add(netData);
count += length;
}
if (current.Count > 0)
+1 -1
View File
@@ -19,7 +19,7 @@ public static class Helpers
/// <returns>A list of alive players.</returns>
public static List<PlayerControl> GetAlivePlayers()
{
return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList();
return [.. GameData.Instance.AllPlayers.ToArray().Where(x => !x.IsDead && !x.Disconnected && x.Object).Select(x=>x.Object)];
}
/// <summary>
+2
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using UnityEngine;
@@ -19,6 +20,7 @@ public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr)
/// <summary>
/// Gets the list of votes the owner has.
/// </summary>
[HideFromIl2Cpp]
public List<CustomVote> Votes { get; private set; } = [];
/// <summary>
+71 -5
View File
@@ -41,16 +41,26 @@ public static class VotingUtils
/// </summary>
/// <param name="voteData">The player's vote data.</param>
/// <param name="suspectIdx">Who the player voted for.</param>
public static void HandleVote(PlayerVoteData voteData, byte suspectIdx)
/// <param name="cancelVote">Whether you want the vote to commence or not.</param>
private static void HandleVote(PlayerVoteData voteData, byte suspectIdx, out bool cancelVote)
{
var @event = new HandleVoteEvent(voteData, suspectIdx);
MiraEventManager.InvokeEvent(@event);
cancelVote = @event.PreventVote;
if (@event.IsCancelled)
{
return;
}
if (voteData.VotesRemaining == 0 ||
voteData.VotedFor(suspectIdx))
{
cancelVote = true;
return;
}
voteData.DecreaseRemainingVotes(1);
voteData.VoteForPlayer(suspectIdx);
}
@@ -64,10 +74,7 @@ public static class VotingUtils
[MethodRpc((uint)MiraRpc.RemoveVote)]
public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor)
{
if (source.OwnerId != AmongUsClient.Instance.HostId)
{
return;
}
if (!source.IsHost()) return;
MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote();
@@ -94,6 +101,65 @@ public static class VotingUtils
MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true);
}
/// <summary>
/// Networks the casting of a vote. We replace the vanilla solution with a custom version that works for the use case.
/// </summary>
/// <param name="source">The player who sent this RPC.</param>
/// <param name="srcPlayerId">The id of the player who casted the vote.</param>
/// <param name="suspectPlayerId">The voted player's id.</param>
[MethodRpc((uint)MiraRpc.CastVote)]
public static void RpcCastVote(PlayerControl source, byte srcPlayerId, byte suspectPlayerId)
{
CustomCastVote(srcPlayerId, suspectPlayerId);
}
private static void CustomCastVote(byte srcPlayerId, byte suspectPlayerId)
{
var plr = GameData.Instance.GetPlayerById(srcPlayerId);
if (!plr) return;
var voteData = plr.Object.GetVoteData();
if (!voteData) return;
HandleVote(voteData, suspectPlayerId, out var cancelVote);
// Handle local behaviour for the voter (for some reason checking AmOwner does not work for host)
if (PlayerControl.LocalPlayer.PlayerId == srcPlayerId)
{
if (!cancelVote) SoundManager.Instance.PlaySound(MeetingHud.Instance.VoteLockinSound, false);
foreach (var playerVoteArea in MeetingHud.Instance.playerStates)
{
playerVoteArea.ClearButtons();
}
MeetingHud.Instance.SkipVoteButton.ClearButtons();
var localVoteData = PlayerControl.LocalPlayer.GetVoteData();
if (!localVoteData || localVoteData.VotesRemaining != 0) return;
MeetingHud.Instance.SkipVoteButton.voteComplete = true;
MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(false);
}
if (cancelVote) return;
// If player has no more votes, then make it show that the player has used all their votes.
if (voteData.VotesRemaining == 0)
{
MeetingHud.Instance.playerStates.First(x => x.TargetPlayerId == srcPlayerId).SetVote(suspectPlayerId);
}
// If host, then check end voting, and set votes/send chat if applicable
if (!PlayerControl.LocalPlayer.IsHost()) return;
MeetingHud.Instance.SetDirtyBit(1U);
MeetingHud.Instance.CheckForEndVoting();
if (voteData.VotesRemaining != 0) return;
PlayerControl.LocalPlayer.RpcSendChatNote(srcPlayerId, ChatNoteTypes.DidVote);
}
/// <summary>
/// Calculates the total number of votes.
/// </summary>