Compare commits

...
70 Commits
Author SHA1 Message Date
XtraCube 0d1d1891a8 Merge branch 'dev' into 2025.3.25 2025-04-05 16:30:17 -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 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
XtraCube 31ffe6274c fix silly mistake with settings 2025-04-05 14:19:52 -04:00
XtraCube aef4d6daf1 actually fix meetinghud vote issue 2025-04-04 13:09:11 -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 3a36acd131 rework modifier assignment 2025-04-03 20:37:09 -04:00
XtraCube 8c763e2a57 fix mira event priorities 2025-04-01 21:22:34 -04:00
XtraCube ce330325e4 fix broken submerged compat patch 2025-04-01 17:03:13 -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 a5ddec48c8 attempt at better config I/O during setup 2025-03-31 22:16:32 -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 a0d3afc7c7 add onmeeting event 2025-03-30 15:38:05 -04:00
XtraCube 3cd7acbc70 fix ejection patch for submerged and airship 2025-03-29 22:07:12 -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 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
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 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 f48143610c fix incorrect chunking algorithm 2025-03-23 15:15:21 -04:00
XtraCube 2cfc5ac039 update example 2025-03-23 14:00:12 -04:00
XtraCube e9beb3b423 fix weird behaviour 2025-03-21 15:24:48 -04:00
ang-xd 7917d4b5d9 Make some changes to role registration for sounds 2025-03-16 19:05:12 -04:00
ang-xd bf795a2e36 Bug fixes with option screen & modifiers options 2025-03-15 23:47:05 -04:00
angxl wtf 144a95fa3b Merge pull request #31 from All-Of-Us-Mods/voting
Voting API
2025-03-15 21:10:19 -04:00
ang-xd e25b78705b Merge branch 'dev' into voting 2025-03-15 17:34:42 -04:00
ang-xd 19ba9da2c3 Remove debug method 2025-03-15 17:22:01 -04:00
ang-xd ce9864ae8b Lots of new Util methods, and fix opt header colors 2025-03-15 17:21:26 -04:00
XtraCube 8f76f21cb1 fix dummy voting in freeplay 2025-03-15 16:12:05 -04:00
ang-xd bc7b835dc8 Admit defeat to custom sprite assets 2025-03-15 15:04:57 -04:00
XtraCube f0b2f9ee2a fix bad naming 2025-03-14 17:39:27 -04:00
XtraCube 25eafe664b improve MeetingSelectEvent 2025-03-13 12:53:00 -04:00
XtraCube db327604c5 Remove PopulateResultsRpc and simplify. 2025-03-13 12:19:08 -04:00
XtraCube 1462061df9 fix REALLY stupid bug 2025-03-13 09:23:26 -04:00
XtraCube fb40664bd0 add CheckForEndVotingEvent.cs 2025-03-12 22:56:41 -04:00
XtraCube 3cba870980 add ProcessVotesEvent.cs and remove "weighting" 2025-03-12 22:30:54 -04:00
XtraCube ab2f03ee4d still broken 2025-03-12 20:13:20 -04:00
ang-xd 645d1dad1c Broken thing 2025-03-12 18:01:58 -04:00
XtraCube a04e0d28db WTF GIT FAIL! 2025-03-12 17:24:40 -04:00
XtraCube b04d9619b8 Merge branch 'dev' into voting
# Conflicts:
#	MiraAPI/Events/Vanilla/Meeting/ReportBodyEvent.cs
#	MiraAPI/Patches/Events/ReportBodyEventPatch.cs
#	MiraAPI/Utilities/Extensions.cs
2025-03-12 17:23:29 -04:00
XtraCube 33fd657eb3 add "OptionsMenuColor" to ICustomRole 2025-03-12 16:48:04 -04:00
XtraCube 529fc89fc5 add WCAG 2.1 compliant alternate color 2025-03-12 16:45:29 -04:00
XtraCube 6ab283d39c increase accessibility of properties in CustomActionButton.cs 2025-03-12 16:04:26 -04:00
XtraCube 08d8a48701 make Player not nullable in basemodifier (because it shouldnt ever be) 2025-03-12 16:02:15 -04:00
XtraCube 708534ed5c make Player not nullable in basemodifier (because it shouldnt ever be) 2025-03-12 16:01:34 -04:00
XtraCube 886ef0e31f fix ToAdd/ToRemove modifier bug 2025-03-12 15:26:24 -04:00
XtraCube 0b9e54eb50 voting API improvements 2025-03-10 22:29:33 -04:00
XtraCube bad6648f44 add weight to CustomVote 2025-03-10 21:52:16 -04:00
ang-xd 664f8cb2e8 Add PopulateResultsEvent.cs 2025-03-01 15:31:48 -05:00
ang-xd 989b511276 Voting "API" 2025-02-28 23:19:34 -05:00
68 changed files with 2210 additions and 440 deletions

No files matched your search

+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>
+42 -6
View File
@@ -1,9 +1,14 @@
using MiraAPI.Events;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Mira;
using MiraAPI.Events.Vanilla.Gameplay;
using MiraAPI.Events.Vanilla.Map;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Example.Buttons.Freezer;
using MiraAPI.Example.Roles;
using MiraAPI.Utilities;
using Reactor.Utilities;
namespace MiraAPI.Example;
@@ -29,6 +34,38 @@ public static class ExampleEventHandlers
});
}
// If you want to add extra votes to a player, do something like this.
[RegisterEvent]
public static void StartMeetingEvent(StartMeetingEvent _)
{
foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player.Data.Role is MayorRole))
{
plr.GetVoteData().IncreaseRemainingVotes(1);
}
}
// Example of using the Voting API to make a role similar to the Prosecutor from Town of Us.
[RegisterEvent(15)]
public static void HandleVoteEvent(HandleVoteEvent @event)
{
if (@event.VoteData.Owner.Data.Role is not NeutralKillerRole) return;
@event.VoteData.SetRemainingVotes(0);
for (var i = 0; i < 5; i++)
{
@event.VoteData.VoteForPlayer(@event.TargetId);
}
foreach (var plr in PlayerControl.AllPlayerControls.ToArray().Where(player => player != @event.VoteData.Owner))
{
plr.GetVoteData().Votes.Clear();
plr.GetVoteData().VotesRemaining = 0;
}
@event.Cancel();
}
// Events can be registered using an attribute as well.
[RegisterEvent]
public static void UpdateSystemEventHandler(UpdateSystemEvent @event)
@@ -42,11 +79,10 @@ public static class ExampleEventHandlers
{
Logger<ExamplePlugin>.Warning("Freeze button clicked!");
if (PlayerControl.LocalPlayer.Data.PlayerName == "stupid")
{
@event.Cancel();
@event.Button.SetTimer(15f);
}
if (PlayerControl.LocalPlayer.Data.PlayerName != "stupid") return;
@event.Cancel();
@event.Button.SetTimer(15f);
}
// Example event handler
@@ -15,7 +15,7 @@ public class FreezeModifier : TimedModifier
public override void OnActivate()
{
if (Player?.AmOwner == true)
if (Player.AmOwner)
{
Player.moveable = false;
}
@@ -25,15 +25,15 @@ public class FreezeModifier : TimedModifier
{
base.FixedUpdate();
if (Player?.AmOwner == true || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
if (Player.AmOwner || PlayerControl.LocalPlayer.Data.Role is FreezerRole)
{
Player?.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
Player.cosmetics.SetOutline(true, new Nullable<Color>(Palette.LightBlue));
}
}
public override void OnTimerComplete()
{
if (Player?.AmOwner == true)
if (Player.AmOwner)
{
Player.moveable = true;
}
+11 -2
View File
@@ -1,4 +1,6 @@
using MiraAPI.Roles;
using AmongUs.GameOptions;
using MiraAPI.Roles;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -12,11 +14,18 @@ public class ChameloenRole : CrewmateRole, ICustomRole
public Color RoleColor => Palette.AcceptedGreen;
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this)
public CustomRoleConfiguration Configuration => new(this)
{
OptionsScreenshot = ExampleAssets.Banner,
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)
+14
View File
@@ -0,0 +1,14 @@
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Example.Roles;
public class MayorRole : CrewmateRole, ICustomRole
{
public string RoleName => "Mayor";
public string RoleLongDescription => "You get extra votes!";
public string RoleDescription => RoleLongDescription;
public Color RoleColor => new Color32(221, 176, 152, 255);
public ModdedRoleTeams Team => ModdedRoleTeams.Crewmate;
public CustomRoleConfiguration Configuration => new(this);
}
+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;
}
@@ -0,0 +1,26 @@
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Checks if voting is complete. If canceled, the default end voting logic will be skipped.
/// </summary>
public class CheckForEndVotingEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a value indicating whether default voting logic determines that voting is complete.
/// </summary>
public bool IsVotingComplete { get; }
/// <summary>
/// Gets or sets a value indicating whether the voting should be forced to end, regardless of the default logic.
/// </summary>
public bool ForceEndVoting { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="CheckForEndVotingEvent"/> class.
/// </summary>
/// <param name="isVotingComplete">>True if voting is complete, false otherwise.</param>
public CheckForEndVotingEvent(bool isVotingComplete)
{
IsVotingComplete = isVotingComplete;
}
}
@@ -0,0 +1,44 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// The event that is invoked when Mira handles a vote. This is invoked before Mira's behaviour, so be cautious.
/// If you intend on adding custom vote behaviour, cancel the event and do so.
/// If you are NOT canceling the event, keep in mind that Mira automatically removes a vote and adds the suspect to the VotedPlayers list after this event is invoked.
/// </summary>
public class HandleVoteEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets the player who voted.
/// </summary>
public PlayerControl Player { get; }
/// <summary>
/// Gets the player id of the target.
/// </summary>
public byte TargetId { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
public NetworkedPlayerInfo TargetPlayerInfo { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HandleVoteEvent"/> class.
/// </summary>
/// <param name="playerVoteData">The voter's data.</param>
/// <param name="targetId">The target's playerId.</param>
public HandleVoteEvent(PlayerVoteData playerVoteData, byte targetId)
{
VoteData = playerVoteData;
Player = playerVoteData.Owner;
TargetId = targetId;
TargetPlayerInfo = GameData.Instance.GetPlayerById(targetId);
}
}
@@ -0,0 +1,43 @@
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// The event that is invoked when the player tries to select another player to vote.
/// </summary>
public class MeetingSelectEvent : MiraEvent
{
/// <summary>
/// Gets the instance of the voter's vote data.
/// </summary>
public PlayerVoteData VoteData { get; }
/// <summary>
/// Gets the player id of the target.
/// </summary>
public int TargetId { get; }
/// <summary>
/// Gets the player info of the target.
/// </summary>
public NetworkedPlayerInfo TargetPlayerInfo { get; }
/// <summary>
/// Gets or sets a value indicating whether the player is allowed to select the target.
/// </summary>
public bool AllowSelect { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MeetingSelectEvent"/> class.
/// </summary>
/// <param name="playerVoteData">The voter's data.</param>
/// <param name="targetId">The target's playerId.</param>
/// <param name="allowSelect">>Whether the player is allowed to select the target.</param>
public MeetingSelectEvent(PlayerVoteData playerVoteData, int targetId, bool allowSelect)
{
VoteData = playerVoteData;
TargetId = targetId;
TargetPlayerInfo = GameData.Instance.GetPlayerById((byte)targetId);
AllowSelect = allowSelect;
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Called in VotingUtils.HandlePopulateResults.
/// Cancelling is NOT advised but if you do plan on cancelling, please ensure you account for displaying ALL player's votes.
/// </summary>
public class PopulateResultsEvent : MiraCancelableEvent
{
/// <summary>
/// Gets a list of networked votes.
/// </summary>
public List<CustomVote> Votes { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PopulateResultsEvent"/> class.
/// </summary>
/// <param name="votes">The list of votes.</param>
public PopulateResultsEvent(List<CustomVote> votes)
{
Votes = votes;
}
}
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Voting;
namespace MiraAPI.Events.Vanilla.Meeting.Voting;
/// <summary>
/// Ran after calculating votes and before displaying the results. Only ran on the host.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="ProcessVotesEvent"/> class.
/// </remarks>
/// <param name="votes">>The list of votes that are being processed.</param>
/// <param name="exiledPlayer">The player to be exiled. Will be null if no player is to be exiled.</param>
public class ProcessVotesEvent(List<CustomVote> votes, NetworkedPlayerInfo? exiledPlayer = null) : MiraEvent
{
private readonly List<CustomVote> _originalVotes = [.. votes];
private NetworkedPlayerInfo? _exiledPlayer = exiledPlayer;
/// <summary>
/// Gets a value indicating whether the exiled player has been modified by the event.
/// </summary>
public bool ExiledPlayerModified { get; private set; }
/// <summary>
/// Gets a value indicating whether the votes have been modified by the event.
/// </summary>
public bool VotesModified => !Votes.SequenceEqual(_originalVotes);
/// <summary>
/// Gets the list of votes that are being processed.
/// </summary>
public List<CustomVote> Votes { get; } = votes;
/// <summary>
/// Gets or sets the player to be exiled. Will be null if no player is to be exiled.
/// </summary>
public NetworkedPlayerInfo? ExiledPlayer
{
get => _exiledPlayer;
set
{
_exiledPlayer = value;
ExiledPlayerModified = true;
}
}
}
@@ -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 -1
View File
@@ -35,7 +35,7 @@ public abstract class AbstractOptionGroup
/// <summary>
/// Gets the group color. This is used to color the group in the options menu.
/// </summary>
public virtual Color GroupColor => Color.clear;
public virtual Color GroupColor => MiraApiPlugin.DefaultHeaderColor;
/// <summary>
/// Gets the group priority. This is used to determine the order in which groups are displayed in the options menu.
@@ -79,17 +79,7 @@ public abstract class ModdedOption<T> : IModdedOption
/// <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;
public ConfigDefinition? ConfigDefinition { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
+35 -30
View File
@@ -52,9 +52,9 @@ public abstract class CustomActionButton
public virtual Color TextOutlineColor => Color.clear;
/// <summary>
/// Gets the location of the button on the screen.
/// Gets or sets the location of the button on the screen.
/// </summary>
public virtual ButtonLocation Location { get; private set; } = ButtonLocation.BottomLeft;
public virtual ButtonLocation Location { get; set; } = ButtonLocation.BottomLeft;
/// <summary>
/// Gets a value indicating whether the button has an effect ability.
@@ -69,27 +69,27 @@ public abstract class CustomActionButton
/// <summary>
/// Gets or sets a value indicating whether the effect is currently active, if there is one.
/// </summary>
public bool EffectActive { get; protected set; }
public bool EffectActive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the timer is currently active.
/// </summary>
public bool TimerPaused { get; protected set; }
public bool TimerPaused { get; set; }
/// <summary>
/// Gets or sets the amount of uses left.
/// </summary>
public int UsesLeft { get; protected set; }
public int UsesLeft { get; set; }
/// <summary>
/// Gets or sets the timer variable to measure cooldowns and effects.
/// </summary>
public float Timer { get; protected set; }
public float Timer { get; set; }
/// <summary>
/// Gets the button object in game. This is created by Mira API automatically.
/// Gets or sets the button object in game. This is created by Mira API automatically.
/// </summary>
protected ActionButton? Button { get; private set; }
public ActionButton? Button { get; set; }
/// <summary>
/// The method used to create the button.
@@ -168,7 +168,7 @@ public abstract class CustomActionButton
/// Allows you to change the button's location.
/// </summary>
/// <param name="location">The new location.</param>
/// <param name="moveButton">Whether the button's position should change ingame.</param>
/// <param name="moveButton">Whether the button's position should change in-game.</param>
public virtual void SetButtonLocation(ButtonLocation location, bool moveButton = true)
{
if (!HudManager.InstanceExists || Button == null)
@@ -221,7 +221,6 @@ public abstract class CustomActionButton
/// A utility function to change the outline color of the button's text.
/// </summary>
/// <param name="color">The new color.</param>
/// <param name="thickness">The thickness of the outline. Set to 0 if you want to remove it.</param>
public virtual void SetTextOutline(Color color)
{
Button?.buttonLabelText.SetOutlineColor(color);
@@ -364,7 +363,7 @@ public abstract class CustomActionButton
/// <returns>A value that represents whether the button should light up or not.</returns>
public virtual bool CanUse()
{
return !LimitedUses || UsesLeft > 0;
return PlayerControl.LocalPlayer.moveable && (!LimitedUses || UsesLeft > 0);
}
/// <summary>
@@ -418,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)
{
@@ -429,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);
@@ -463,7 +468,7 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
/// <summary>
/// Gets or sets the target object of the button.
/// </summary>
public T? Target { get; protected set; }
public T? Target { get; set; }
/// <summary>
/// Gets the distance the player must be from the target object to use the 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);
}
}
+10
View File
@@ -16,10 +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);
/// <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;
}
}
+8 -1
View File
@@ -12,7 +12,7 @@ public abstract class BaseModifier : IOptionable
/// <summary>
/// Gets the player that the modifier is attached to.
/// </summary>
public PlayerControl? Player { get; internal set; }
public PlayerControl Player { get; internal set; } = null!;
/// <summary>
/// Gets the modifier component that the modifier is attached to.
@@ -104,6 +104,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>
+115 -35
View File
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.Attributes;
@@ -25,14 +24,14 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
/// Gets the active modifiers on the player.
/// </summary>
[HideFromIl2Cpp]
public ImmutableList<BaseModifier> ActiveModifiers => Modifiers.ToImmutableList();
public ImmutableList<BaseModifier> ActiveModifiers { get; private set; } = ImmutableList<BaseModifier>.Empty;
[HideFromIl2Cpp]
private List<BaseModifier> Modifiers { get; set; } = [];
private PlayerControl? _player;
private PlayerControl _player = null!;
private TextMeshPro? _modifierText;
private TextMeshPro _modifierText = null!;
private readonly List<BaseModifier> _toRemove = [];
@@ -43,9 +42,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)
@@ -59,14 +67,18 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
private void FixedUpdate()
{
foreach (var modifier in _toRemove)
var removed = _toRemove.Count > 0;
foreach (var modifier in _toRemove.ToArray())
{
_toRemove.Remove(modifier);
modifier.OnDeactivate();
Modifiers.Remove(modifier);
}
foreach (var modifier in _toAdd)
var added = _toAdd.Count > 0;
foreach (var modifier in _toAdd.ToArray())
{
_toAdd.Remove(modifier);
Modifiers.Add(modifier);
modifier.Initialized = true;
modifier.OnActivate();
@@ -77,17 +89,17 @@ 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);
HudManager.Instance.SetHudActive(_player, _player.Data.Role, HudManager.Instance.TaskPanel.isActiveAndEnabled);
}
_toAdd.Clear();
_toRemove.Clear();
ActiveModifiers = Modifiers.ToImmutableList();
}
foreach (var modifier in Modifiers)
foreach (var modifier in ActiveModifiers)
{
modifier.FixedUpdate();
}
@@ -95,31 +107,42 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
private void Update()
{
foreach (var modifier in Modifiers)
foreach (var modifier in ActiveModifiers)
{
modifier.Update();
}
if (!_modifierText || _player?.AmOwner == false)
if (!_modifierText || !_player.AmOwner)
{
return;
}
var filteredModifiers = Modifiers.Where(mod => !mod.HideOnUi).ToArray();
if (MeetingHud.Instance)
{
return;
}
if (filteredModifiers.Length != 0 && !MeetingHud.Instance)
var showText = false;
var builder = new StringBuilder("<b><size=130%>Modifiers:</b></size>\n");
foreach (var modifier in ActiveModifiers)
{
var stringBuild = new StringBuilder();
foreach (var mod in filteredModifiers)
if (modifier.HideOnUi)
{
stringBuild.Append(CultureInfo.InvariantCulture, $"\n{mod.GetHudString()}");
continue;
}
_modifierText!.text = $"<b><size=130%>Modifiers:</b></size>{stringBuild}";
}
else if (_modifierText!.text != string.Empty)
{
_modifierText.text = string.Empty;
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>
@@ -131,7 +154,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public IEnumerable<T> GetModifiers<T>(Func<T, bool>? predicate=null) where T : BaseModifier
{
return Modifiers.OfType<T>().Where(x => predicate == null || predicate(x));
return ActiveModifiers.OfType<T>().Where(x => predicate == null || predicate(x));
}
/// <summary>
@@ -143,7 +166,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public IEnumerable<BaseModifier> GetModifiers(Type type, Func<BaseModifier, bool>? predicate=null)
{
return Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x)));
return ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x)));
}
/// <summary>
@@ -263,7 +286,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public BaseModifier? GetModifier(Guid modifierGuid)
{
return Modifiers.Find(x => x.UniqueId == modifierGuid);
return ActiveModifiers.Find(x => x.UniqueId == modifierGuid);
}
/// <summary>
@@ -285,7 +308,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public void RemoveModifier(Type type, Func<BaseModifier, bool>? predicate = null)
{
var modifiers = Modifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))).ToList();
var modifiers = ActiveModifiers.Where(x => x.GetType() == type && (predicate == null || predicate(x))).ToList();
if (modifiers.Count > 1)
{
throw new InvalidOperationException($"Cannot remove modifier {type.Name} because there are multiple instances of that modifier.");
@@ -308,7 +331,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public void RemoveModifier(BaseModifier modifier)
{
if (!Modifiers.Contains(modifier))
if (!ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Cannot remove modifier {modifier.ModifierName} because it is not active on this player.");
return;
@@ -374,13 +397,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
}
var id = modifier.TypeId;
if (modifier.Unique && Modifiers.Find(x => x.TypeId == id) != null)
if (modifier.Unique && ActiveModifiers.Find(x => x.TypeId == id) != null)
{
Logger<MiraApiPlugin>.Error($"Player already has modifier with id {id}!");
return null;
}
if (Modifiers.Contains(modifier))
if (ActiveModifiers.Contains(modifier))
{
Logger<MiraApiPlugin>.Error($"Player already has this modifier!");
return null;
@@ -446,7 +469,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public bool HasModifier<T>(Func<T, bool>? predicate=null) where T : BaseModifier
{
return Modifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier)));
return ActiveModifiers.Exists(x => x is T modifier && (predicate == null || predicate(modifier)));
}
/// <summary>
@@ -458,7 +481,7 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public bool HasModifier(Type type, Func<BaseModifier, bool>? predicate=null)
{
return Modifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x)));
return ActiveModifiers.Exists(x => x.GetType() == type && (predicate == null || predicate(x)));
}
/// <summary>
@@ -484,6 +507,63 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
[HideFromIl2Cpp]
public bool HasModifier(Guid id)
{
return Modifiers.Exists(x => x.UniqueId == id);
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;
}
}
+24
View File
@@ -191,6 +191,30 @@ public static class ModifierExtensions
return player.GetModifierComponent().HasModifier(uniqueId);
}
/// <summary>
/// Clears all modifiers from a player.
/// </summary>
/// <param name="plr">The player you want to clear modifiers for.</param>
public static void ClearModifiers(this PlayerControl plr)
{
foreach (var mod in plr.GetModifierComponent().ActiveModifiers)
{
plr.RemoveModifier(mod);
}
}
/// <summary>
/// Clears all modifiers from a player.
/// </summary>
/// <param name="plr">The player you want to clear modifiers for.</param>
public static void RpcClearModifiers(this PlayerControl plr)
{
foreach (var mod in plr.GetModifierComponent().ActiveModifiers)
{
plr.RpcRemoveModifier(mod.UniqueId);
}
}
/// <summary>
/// Tries to get a modifier by its type.
/// </summary>
+39 -50
View File
@@ -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);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Modifiers;
/// <summary>
/// Utilities to make handling modifiers in-game easier.
/// </summary>
public static class ModifierUtils
{
/// <summary>
/// Gets a list of all active in-game modifiers.
/// </summary>
/// <param name="predicate">Select if modifier is valid to be added to list.</param>
/// <typeparam name="T">The modifier type.</typeparam>
/// <returns>A list of modifiers.</returns>
public static IEnumerable<T> GetActiveModifiers<T>(Func<T, bool>? predicate = null) where T : BaseModifier
{
return PlayerControl.AllPlayerControls.ToArray().SelectMany(x => x.GetModifiers<T>(predicate)).OfType<T>();
}
/// <summary>
/// Gets all players with a certain modifier.
/// </summary>
/// <param name="predicate">Select if modifier is valid.</param>
/// <typeparam name="T">The modifier type.</typeparam>
/// <returns>A list of players with that modifier.</returns>
public static IEnumerable<PlayerControl> GetPlayersWithModifier<T>(Func<T, bool>? predicate = null) where T : BaseModifier
{
return PlayerControl.AllPlayerControls.ToArray().Where(x => x.HasModifier<T>(predicate));
}
}
+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
@@ -39,4 +39,9 @@ public enum MiraRpc : uint
/// Custom Game Over RPC.
/// </summary>
CustomGameOver,
/// <summary>
/// Custom RPC to undo a player's vote.
/// </summary>
RemoveVote,
}
@@ -5,10 +5,12 @@ using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
[HarmonyPatch]
public static class ReportBodyEventPatch
{
public static bool Prefix(PlayerControl __instance, NetworkedPlayerInfo? target)
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
@@ -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);
}
}
@@ -1,15 +0,0 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
public static class StartMeetingEventPatch
{
public static void Postfix(MeetingHud __instance)
{
var @event = new StartMeetingEvent(__instance);
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);
}
}
+3 -3
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))]
@@ -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();
}
+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));
}
}
@@ -0,0 +1,12 @@
using HarmonyLib;
namespace MiraAPI.Patches;
[HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))]
internal static class MinigameBeginErrorPatch
{
public static void Prefix(Minigame __instance)
{
__instance.logger ??= new Logger(Logger.Category.Gameplay, "Minigame");
}
}
@@ -2,13 +2,15 @@
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.Modifiers.Types;
using MiraAPI.Utilities;
using Reactor.Utilities;
namespace MiraAPI.Patches.Modifiers;
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.CoEndGame))]
[HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))]
public static class EndGameDidWinPatch
{
public static void Prefix()
public static void Postfix()
{
var gameOverReason = EndGameResult.CachedGameOverReason;
EndGameResult.CachedWinners.Clear();
@@ -17,18 +19,26 @@ public static class EndGameDidWinPatch
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)
bool? modifierDidWin = null;
foreach (var modifier in networkedPlayerInfo.Object.GetModifiers<GameModifier>().OrderByDescending(x => x.Priority()))
{
didWin = modifierWin.Value;
var result = modifier.DidWin(gameOverReason);
if (!result.HasValue) continue;
modifierDidWin = result.Value;
break;
}
if (modifierDidWin.HasValue)
{
didWin = modifierDidWin.Value;
}
if (didWin)
+17 -15
View File
@@ -42,7 +42,7 @@ public static class GameOptionsMenuPatch
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.OptionableType == null) ?? [];
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
{
@@ -144,7 +144,7 @@ public static class GameOptionsMenuPatch
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.OptionableType == null) ?? [];
.Where(x => x.OptionableType == null && !x.ShowInModifiersMenu) ?? [];
foreach (var group in filteredGroups)
{
@@ -198,16 +198,19 @@ public static class GameOptionsMenuPatch
menu.settingsContainer);
categoryHeaderMasked.SetHeader(CustomStringName.CreateAndRegister(group.GroupName), 20);
if (group.GroupColor != Color.clear)
{
categoryHeaderMasked.Background.color = group.GroupColor;
categoryHeaderMasked.Divider.color = group.GroupColor;
categoryHeaderMasked.Title.color = group.GroupColor.GetAlternateColor();
}
categoryHeaderMasked.Background.color = group.GroupColor;
categoryHeaderMasked.Divider.color = group.GroupColor;
categoryHeaderMasked.Title.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.GroupColor.FindAlternateColor();
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp;
categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0);
categoryHeaderMasked.Background.size = new Vector2(
categoryHeaderMasked.Background.size.x + 1.5f,
categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.gameObject.SetActive(false);
group.Header = categoryHeaderMasked;
@@ -230,12 +233,12 @@ public static class GameOptionsMenuPatch
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren<SpriteRenderer>(true);
foreach (var renderer in componentsInChildren)
{
if (group.GroupColor != Color.clear)
if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor)
{
renderer.color = group.GroupColor.GetAlternateColor();
renderer.color = group.GroupColor.FindAlternateColor();
if (renderer.transform.parent.TryGetComponent<GameOptionButton>(out var btn))
{
btn.interactableColor = group.GroupColor.GetAlternateColor();
btn.interactableColor = group.GroupColor.FindAlternateColor();
btn.interactableHoveredColor = Color.white;
}
}
@@ -245,7 +248,7 @@ public static class GameOptionsMenuPatch
foreach (var textMeshPro in newOpt.GetComponentsInChildren<TextMeshPro>(true))
{
if (group.GroupColor != Color.clear)
if (group.GroupColor != MiraApiPlugin.DefaultHeaderColor)
{
textMeshPro.color = group.GroupColor;
}
@@ -257,12 +260,11 @@ public static class GameOptionsMenuPatch
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color =
group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
toggle.CheckMark.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
var rend = toggle.CheckMark.transform.parent.FindChild("ActiveSprite")
.GetComponent<SpriteRenderer>();
rend.sprite = MiraAssets.CheckmarkBox.LoadAsset();
rend.color = group.GroupColor != Color.clear ? group.GroupColor : MiraAssets.AcceptedTeal;
rend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor ? group.GroupColor : MiraAssets.AcceptedTeal;
}
menu.Children.Add(newOpt);
+217 -77
View File
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
@@ -22,6 +23,8 @@ internal static class GameSettingMenuPatches
private static TextMeshPro? _text;
private static Vector3 _roleBtnOgPos;
private static Vector3 _smallRoleBtnOgPos;
private static Vector3 _modifierBtnOgPos;
private static GameOptionsMenu? _modifiersTab;
private static PassiveButton? _modifiersButton;
@@ -33,9 +36,20 @@ internal static class GameSettingMenuPatches
{
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
{
if (_modifiersTab) _modifiersTab?.gameObject.SetActive(tabNum == 3);
if (_modifiersTab) _modifiersTab!.gameObject.SetActive(tabNum == 3);
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum == 3)
{
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
_modifiersButton?.SelectButton(true);
}
}
}
if (previewOnly)
@@ -45,6 +59,17 @@ internal static class GameSettingMenuPatches
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
if (tabNum != 3) return;
if (__instance.RoleSettingsButton.gameObject.active)
{
__instance.RoleSettingsButton.SelectButton(true);
}
else
{
_modifiersButton?.SelectButton(true);
}
}
/// <summary>
@@ -117,6 +142,7 @@ internal static class GameSettingMenuPatches
_modifiersTab.name = "MODIFIERS TAB";
// create button for modifiers
__instance.RoleSettingsButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
__instance.RoleSettingsButton.gameObject.SetActive(false);
_smallRolesButton = Object.Instantiate(
__instance.RoleSettingsButton,
@@ -137,6 +163,8 @@ internal static class GameSettingMenuPatches
__instance.ChangeTab(2, true);
}));
_smallRoleBtnOgPos = _smallRolesButton.transform.localPosition;
var roleText = _smallRolesButton.buttonText;
roleText.text = "Roles";
roleText.GetComponent<TextTranslatorTMP>().Destroy();
@@ -178,9 +206,45 @@ internal static class GameSettingMenuPatches
_modifiersButton.transform.localPosition = pos;
_modifiersButton.name = "ModifiersButton";
_modifierBtnOgPos = _modifiersButton.transform.localPosition;
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}
private static void ChangeRoleSettingButton(bool replace, GameSettingMenu __instance)
{
__instance.RoleSettingsButton.OnClick = new ButtonClickedEvent();
__instance.RoleSettingsButton.OnMouseOver = new UnityEvent();
if (replace)
{
__instance.RoleSettingsButton.buttonText.text = "Modifiers Settings";
__instance.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, false);
}));
__instance.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, true);
}));
}
else
{
__instance.RoleSettingsButton.buttonText.text = "Roles Settings";
__instance.RoleSettingsButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, false);
}));
__instance.RoleSettingsButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, true);
}));
}
}
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
{
if (_text is not null && SelectedModIdx == 0)
@@ -197,106 +261,182 @@ internal static class GameSettingMenuPatches
_text.text = name[..Math.Min(name.Length, 25)];
}
_modifiersButton.gameObject.SetActive(false);
_smallRolesButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(false);
if (SelectedModIdx != 0 &&
SelectedMod?.OptionGroups.Exists(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier)) == true) == true)
bool replaceWithModifiers = true;
_smallRolesButton!.transform.localPosition = _smallRoleBtnOgPos;
_modifiersButton!.transform.localPosition = _modifierBtnOgPos;
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
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);
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(false);
// If there are no registered custom roles in the selected mod, hide the button.
if (!modHasRoles)
{
_smallRolesButton.gameObject.SetActive(false);
if (roles.gameObject.active)
{
menu.ChangeTab(0, false);
}
// If the mod has modifiers, we can disable the smaller modifier button and enable the bigger one.
if (modHasModifiers)
{
_modifiersButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
if (_modifiersTab!.gameObject.active)
{
menu.RoleSettingsButton.SelectButton(true);
}
}
}
// If there are no modifiers in the selected mod, hide the modifiers button.
if (!modHasModifiers)
{
replaceWithModifiers = false;
_modifiersButton.gameObject.SetActive(false);
if (_modifiersTab!.gameObject.active)
{
menu.ChangeTab(0, false);
}
// If the mod has roles, we can enable the bigger role button and disable the small one.
if (modHasRoles)
{
menu.RoleSettingsButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(false);
}
}
// If there are no custom game options registered (that aren't modifier options), hide game settings button.
if (!modHasOptions)
{
menu.GameSettingsButton.gameObject.SetActive(false);
if (settings.gameObject.active)
{
menu.ChangeTab(0, false);
}
// If the mod has roles and modifiers, we can move their buttons to the game settings button position, since nothing is there.
if (menu.RoleSettingsButton.gameObject.active)
{
menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition;
}
else if (_modifiersButton.gameObject.active && _smallRolesButton.gameObject.active)
{
_modifiersButton.transform.localPosition = new Vector3(
_modifiersButton.transform.localPosition.x,
menu.GameSettingsButton.transform.localPosition.y,
_modifiersButton.transform.localPosition.z);
_smallRolesButton.transform.localPosition = new Vector3(
_smallRolesButton.transform.localPosition.x,
menu.GameSettingsButton.transform.localPosition.y,
_smallRolesButton.transform.localPosition.z);
}
}
}
else
{
_modifiersButton.gameObject.SetActive(false);
_smallRolesButton.gameObject.SetActive(false);
menu.RoleSettingsButton.gameObject.SetActive(true);
if (SelectedModIdx == 0 && _modifiersTab.gameObject.active)
menu.GameSettingsButton.gameObject.SetActive(true);
replaceWithModifiers = false;
if (_modifiersTab!.gameObject.active)
{
menu.ChangeTab(0, false);
}
}
//menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
//menu.GameSettingsButton.gameObject.SetActive(true);
//menu.RoleSettingsButton.gameObject.SetActive(true);
// TODO: reimplement this
/*
if (SelectedModIdx != 0)
if (menu.RoleSettingsButton.gameObject.active)
{
if (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0)
{
menu.GameSettingsButton.gameObject.SetActive(false);
}
if (SelectedMod?.CustomRoles.Count == 0)
{
menu.RoleSettingsButton.gameObject.SetActive(false);
}
if (!menu.GameSettingsButton.gameObject.active && menu.RoleSettingsButton.gameObject.active)
{
menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition;
}
}*/
if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0)
{
if (roles.advancedSettingChildren is not null)
{
foreach (var child in roles.advancedSettingChildren)
{
child.gameObject.DestroyImmediate();
}
roles.advancedSettingChildren.Clear();
roles.advancedSettingChildren = null;
}
foreach (var header in roles.RoleChancesSettings.transform
.GetComponentsInChildren<CategoryHeaderEditRole>())
{
header.gameObject.DestroyImmediate();
}
foreach (var roleChance in roles.roleChances)
{
roleChance.gameObject.DestroyImmediate();
}
roles.roleChances.Clear();
roles.AdvancedRolesSettings.gameObject.SetActive(false);
roles.RoleChancesSettings.gameObject.SetActive(true);
roles.SetQuotaTab();
roles.scrollBar.ScrollToTop();
ChangeRoleSettingButton(replaceWithModifiers, menu);
}
if (settings.Children != null && SelectedMod?.OptionGroups.Count != 0)
CleanupTab(settings, roles);
}
private static void ClearOptions(Il2CppSystem.Collections.Generic.List<OptionBehaviour> options)
{
foreach (var child in options)
{
foreach (var child in settings.Children)
if (child.TryCast<GameOptionsMapPicker>() || !child.gameObject)
{
if (child.TryCast<GameOptionsMapPicker>())
{
continue;
}
continue;
}
child.gameObject.DestroyImmediate();
}
options.Clear();
}
if (!child.gameObject)
{
continue;
}
child.gameObject.DestroyImmediate();
private static void CleanupTab(GameOptionsMenu settings, RolesSettingsMenu roles)
{
void CleanupRoleSettings(RolesSettingsMenu rolesMenu)
{
if (rolesMenu.advancedSettingChildren != null)
{
ClearOptions(rolesMenu.advancedSettingChildren);
rolesMenu.advancedSettingChildren = null;
}
foreach (var header in settings.settingsContainer.GetComponentsInChildren<CategoryHeaderMasked>())
rolesMenu.RoleChancesSettings
.transform
.GetComponentsInChildren<CategoryHeaderEditRole>()
.ToList()
.ForEach(header => header.gameObject.DestroyImmediate());
foreach (var role in rolesMenu.roleChances)
{
header.gameObject.DestroyImmediate();
role.gameObject.DestroyImmediate();
}
rolesMenu.roleChances?.Clear();
settings.Children.Clear();
settings.Children = null;
rolesMenu.AdvancedRolesSettings.gameObject.SetActive(false);
rolesMenu.RoleChancesSettings.gameObject.SetActive(true);
rolesMenu.SetQuotaTab();
rolesMenu.scrollBar.ScrollToTop();
}
settings.Initialize();
settings.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);
}
}
}
@@ -250,14 +250,9 @@ public static class LobbyViewPanePatches
var list = new List<Type>();
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
var roleGroups = SelectedMod.CustomRoles.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;
}
@@ -331,7 +326,7 @@ public static class LobbyViewPanePatches
chancePerGame,
61,
customRole.RoleColor,
customRole.Configuration.Icon.LoadAsset(),
customRole.Configuration.Icon?.LoadAsset() ?? MiraAssets.Empty.LoadAsset(),
true);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2);
@@ -340,7 +335,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;
}
@@ -418,7 +413,7 @@ public static class LobbyViewPanePatches
role.StringName,
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Configuration.Icon.LoadAsset());
customRole.Configuration.Icon != null ? customRole.Configuration.Icon.LoadAsset() : MiraAssets.Empty.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
@@ -83,10 +83,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;
}
@@ -127,32 +126,37 @@ public static class RoleSettingMenuPatches
chanceLabel.Destroy();
countLabel.Destroy();
if (name is StringNames.CrewmateRolesHeader)
{
categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue;
categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue;
}
else if (name is StringNames.ImpostorRolesHeader)
{
categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed;
categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed;
}
else
{
categoryHeaderMasked.Title.color = group.Color;
categoryHeaderMasked.Divider.color = group.Color;
categoryHeaderMasked.Background.color = group.Color;
}
categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset();
categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear;
categoryHeaderMasked.Background.sprite.texture.wrapMode = TextureWrapMode.Clamp;
categoryHeaderMasked.Title.color = categoryHeaderMasked.Title.color.IsColorDark() ?
categoryHeaderMasked.Title.color.LightenColor(0.3f) : categoryHeaderMasked.Title.color.LightenColor(0.1f);
categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.5f, -0.1833f, 0);
switch (name)
{
case StringNames.CrewmateRolesHeader:
categoryHeaderMasked.Title.color = Palette.CrewmateRoleHeaderTextBlue;
categoryHeaderMasked.Background.color = Palette.CrewmateRoleHeaderBlue;
break;
case StringNames.ImpostorRolesHeader:
categoryHeaderMasked.Title.color = Palette.ImpostorRoleHeaderTextRed;
categoryHeaderMasked.Background.color = Palette.ImpostorRoleHeaderRed;
break;
default:
categoryHeaderMasked.Title.color = group.Color.Equals(MiraApiPlugin.DefaultHeaderColor) ? Color.white : group.Color.FindAlternateColor();
categoryHeaderMasked.Divider.color = group.Color;
categoryHeaderMasked.Background.color = group.Color;
break;
}
categoryHeaderMasked.Title.fontStyle = __instance.categoryHeaderEditRoleOrigin.Title.fontStyle;
categoryHeaderMasked.Title.font = __instance.categoryHeaderEditRoleOrigin.Title.font;
categoryHeaderMasked.Title.fontMaterial = __instance.categoryHeaderEditRoleOrigin.Title.fontMaterial;
categoryHeaderMasked.Divider.color = categoryHeaderMasked.Background.color;
categoryHeaderMasked.Background.transform.localPosition = new Vector3(0.55f, -0.1833f, 0);
categoryHeaderMasked.Background.size = new Vector2(categoryHeaderMasked.Background.size.x + 1.5f, categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f;
categoryHeaderMasked.transform.localPosition = new Vector3(-0.44f, ScrollerNum, -2f);
categoryHeaderMasked.gameObject.SetActive(true);
@@ -241,7 +245,7 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{
if (GameSettingMenuPatches.SelectedModIdx == 0)
if (GameSettingMenuPatches.SelectedModIdx == 0 || !__instance.scrollBar)
{
return;
}
@@ -383,8 +387,8 @@ public static class RoleSettingMenuPatches
__instance.roleScreenshot.drawMode = SpriteDrawMode.Sliced;
}
__instance.roleHeaderSprite.color = customRole.RoleColor;
__instance.roleHeaderText.color = customRole.RoleColor.GetAlternateColor();
__instance.roleHeaderSprite.color = customRole.OptionsMenuColor;
__instance.roleHeaderText.color = customRole.OptionsMenuColor.FindAlternateColor();
var categoryHeaderMasked = __instance.AdvancedRolesSettings.transform.Find("CategoryHeaderMasked").GetComponent<CategoryHeaderMasked>();
@@ -459,13 +463,13 @@ public static class RoleSettingMenuPatches
roleOptionSetting.transform.localPosition = new Vector3(-0.1f, ScrollerNum, -2f);
roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20);
roleOptionSetting.labelSprite.color = customRole.RoleColor;
roleOptionSetting.labelSprite.color = customRole.OptionsMenuColor;
roleOptionSetting.OnValueChanged = new Action<OptionBehaviour>(ValueChanged);
roleOptionSetting.SetClickMask(__instance.ButtonClickMask);
__instance.roleChances.Add(roleOptionSetting);
roleOptionSetting.titleText.transform.localPosition = new Vector3(-0.5376f, -0.2923f, 0f);
roleOptionSetting.titleText.color = customRole.RoleColor.GetAlternateColor();
roleOptionSetting.titleText.color = customRole.OptionsMenuColor.FindAlternateColor();
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null ||
@@ -483,7 +487,7 @@ public static class RoleSettingMenuPatches
var passiveButton = newButton.GetComponent<GameOptionButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.interactableColor = btnRend.color = customRole.RoleColor.GetAlternateColor();
passiveButton.interactableColor = btnRend.color = customRole.OptionsMenuColor.FindAlternateColor();
passiveButton.interactableHoveredColor = Color.white;
passiveButton.OnClick.AddListener((UnityAction)(() => { ChangeTab(role, __instance); }));
+15 -37
View File
@@ -6,6 +6,7 @@ using MiraAPI.Events.Vanilla.Player;
using MiraAPI.Hud;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities.Extensions;
namespace MiraAPI.Patches;
@@ -14,30 +15,30 @@ 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 comp))
if (__instance.gameObject.TryGetComponent<ModifierComponent>(out var modifierComp))
{
comp.DestroyImmediate();
modifierComp.DestroyImmediate();
}
if (__instance.gameObject.TryGetComponent<PlayerVoteData>(out var voteComp))
{
voteComp.DestroyImmediate();
}
__instance.gameObject.AddComponent<ModifierComponent>();
__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);
@@ -51,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);
@@ -68,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);
@@ -84,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)
@@ -117,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);
}
}
+15 -2
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))]
@@ -31,4 +34,14 @@ public static class EjectionPatches
__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);
}
}
}
@@ -1,4 +1,5 @@
using HarmonyLib;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
namespace MiraAPI.Patches.Roles;
@@ -21,6 +22,14 @@ public static class GameStartupPatch
}
_runOnce = true;
if (MiraPluginManager.Instance.QueuedRoleRegistrations.Count <= 0) return;
foreach (var queue in MiraPluginManager.Instance.QueuedRoleRegistrations)
{
CustomRoleManager.RegisterRoleTypes(queue.Value, queue.Key);
}
MiraPluginManager.Instance.QueuedRoleRegistrations.Clear();
CustomRoleManager.RegisterInRoleManager();
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ public static class RoleBehaviourPatch
__result = behaviour.RoleColor;
return false;
}
}
}
+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)
+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);
@@ -0,0 +1,16 @@
using HarmonyLib;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(DummyBehaviour), nameof(DummyBehaviour.Update))]
internal static class DummyBehaviourPatch
{
public static void Postfix(DummyBehaviour __instance)
{
if (MeetingHud.Instance && __instance.myPlayer.GetVoteData())
{
__instance.voted = __instance.myPlayer.GetVoteData().VotesRemaining == 0;
}
}
}
+291
View File
@@ -0,0 +1,291 @@
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;
namespace MiraAPI.Patches.Voting;
[HarmonyPatch(typeof(MeetingHud))]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Harmony Convention")]
internal static class MeetingHudPatches
{
[HarmonyPostfix]
[HarmonyPatch(nameof(MeetingHud.Start))]
public static void MeetingHudStartPatch(MeetingHud __instance)
{
foreach (var plr in PlayerControl.AllPlayerControls)
{
var voteData = plr.GetVoteData();
voteData.Votes.Clear();
voteData.SetRemainingVotes(1);
if (plr.Data.IsDead || plr.Data.Disconnected)
{
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)
{
var voteData = PlayerControl.LocalPlayer.GetVoteData();
var hasVotes = voteData.VotesRemaining > 0;
var hasVotedFor = voteData.VotedFor((byte)suspectStateIdx);
var @event = new MeetingSelectEvent(voteData, suspectStateIdx, hasVotes && !hasVotedFor);
MiraEventManager.InvokeEvent(@event);
return @event.AllowSelect;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.HandleDisconnect), typeof(PlayerControl), typeof(DisconnectReasons))]
public static bool HandleDisconnect(MeetingHud __instance, PlayerControl pc)
{
if (!AmongUsClient.Instance.AmHost || __instance.playerStates is null || !pc || !GameData.Instance)
{
return false;
}
var playerVoteArea = __instance.playerStates.First(pv => pv.TargetPlayerId == pc.PlayerId);
playerVoteArea.AmDead = true;
playerVoteArea.Overlay.gameObject.SetActive(true);
foreach (var player in Helpers.GetAlivePlayers())
{
var pva = __instance.playerStates.First(pv => pv.TargetPlayerId == player.PlayerId);
var voteData = player.GetVoteData();
if (pva.AmDead || !voteData.VotedFor(pc.PlayerId))
{
continue;
}
voteData.Votes.RemoveAll(x=>x.Suspect==pc.PlayerId);
voteData.VotesRemaining += 1;
VotingUtils.RpcRemoveVote(PlayerControl.LocalPlayer, player.PlayerId, pc.PlayerId);
}
__instance.SetDirtyBit(1U);
__instance.CheckForEndVoting();
if (__instance.state == MeetingHud.VoteStates.Results)
{
__instance.SetupProceedButton();
}
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)
{
var shouldEnd = !Helpers.GetAlivePlayers().Exists(plr => plr.GetVoteData().VotesRemaining > 0);
var checkEndEvent = new CheckForEndVotingEvent(shouldEnd);
MiraEventManager.InvokeEvent(checkEndEvent);
shouldEnd = checkEndEvent.ForceEndVoting || checkEndEvent is { IsCancelled: false, IsVotingComplete: true };
if (!shouldEnd)
{
return false;
}
var votes = VotingUtils.CalculateVotes();
var exiled = VotingUtils.GetExiled(votes, out var isTie);
var @event = new ProcessVotesEvent(votes, exiled);
MiraEventManager.InvokeEvent(@event);
if (@event.VotesModified)
{
votes = @event.Votes;
exiled = VotingUtils.GetExiled(votes, out isTie);
}
if (@event.ExiledPlayerModified)
{
exiled = @event.ExiledPlayer;
}
var voterStates = new Il2CppStructArray<MeetingHud.VoterState>([
.. votes.Select(
v=> new MeetingHud.VoterState
{
VoterId = v.Voter,
VotedForId = v.Suspect,
})
]);
__instance.RpcVotingComplete(voterStates, exiled, isTie);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.PopulateResults))]
public static bool PopulateResultsPatch(MeetingHud __instance, ref Il2CppStructArray<MeetingHud.VoterState> states)
{
var votes = states.Select(x=> new CustomVote(x.VoterId, x.VotedForId)).ToList();
var @event = new PopulateResultsEvent(votes);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled)
{
return false;
}
VotingUtils.HandlePopulateResults(votes);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(MeetingHud.Confirm))]
public static bool ConfirmPatch(MeetingHud __instance, [HarmonyArgument(0)] byte suspect)
{
__instance.CmdCastVote(PlayerControl.LocalPlayer.PlayerId, suspect);
return false;
}
}
+8 -3
View File
@@ -27,7 +27,7 @@ public sealed class MiraPluginManager
private readonly Dictionary<Assembly, MiraPluginInfo> _registeredPlugins = [];
internal MiraPluginInfo[] RegisteredPlugins() => [.. _registeredPlugins.Values];
internal Dictionary<MiraPluginInfo, List<Type>> QueuedRoleRegistrations { get; } = new();
internal static MiraPluginManager Instance { get; private set; } = new();
internal void Initialize()
@@ -43,6 +43,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,14 +99,16 @@ public sealed class MiraPluginManager
RegisterColorClasses(type);
}
info.PluginConfig.Save();
info.PluginConfig.SaveOnConfigSet = oldConfigSetting;
info.OptionGroups.Sort((x, y) => x.GroupPriority.CompareTo(y.GroupPriority));
CustomRoleManager.RegisterRoleTypes(roles, info);
QueuedRoleRegistrations.Add(info, roles);
_registeredPlugins.Add(assembly, info);
Logger<MiraApiPlugin>.Info($"Registering mod {pluginInfo.Metadata.GUID} with Mira API.");
};
IL2CPPChainloader.Instance.Finished += PaletteManager.RegisterAllColors;
IL2CPPChainloader.Instance.Finished += MiraEventManager.SortAllHandlers;
IL2CPPChainloader.Instance.Finished += () =>
{
CustomButtonManager.Buttons = new ReadOnlyCollection<CustomActionButton>(CustomButtonManager.CustomButtons);
Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

+10 -3
View File
@@ -32,7 +32,9 @@ public record struct CustomRoleConfiguration
TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate;
HideSettings = roleBehaviour?.IsDead == true;
ShowInFreeplay = roleBehaviour?.IsDead == false;
OptionsScreenshot = null;
IntroSound = role.Team is ModdedRoleTeams.Crewmate
? CustomRoleManager.CrewmateIntroSound
: CustomRoleManager.ImpostorIntroSound;
}
/// <summary>
@@ -58,12 +60,17 @@ public record struct CustomRoleConfiguration
/// <summary>
/// Gets or sets the Sprite used for the Role Options menu screenshot.
/// </summary>
public LoadableAsset<Sprite>? OptionsScreenshot { get; set; }
public LoadableAsset<Sprite>? OptionsScreenshot { get; set; } = null;
/// <summary>
/// Gets or sets the Sprite used for the Role Icon.
/// </summary>
public LoadableAsset<Sprite> Icon { get; set; } = MiraAssets.Empty;
public LoadableAsset<Sprite>? Icon { get; set; } = null;
/// <summary>
/// Gets or sets the Intro sound for the Role.
/// </summary>
public LoadableAsset<AudioClip>? IntroSound { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating whether the role is affected by light affectors on Airship.
+34
View File
@@ -7,6 +7,7 @@ using Il2CppInterop.Runtime.Injection;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
@@ -22,6 +23,18 @@ namespace MiraAPI.Roles;
/// </summary>
public static class CustomRoleManager
{
/// <summary>
/// The default Among Us Crewmate Intro Sound.
/// </summary>
public static readonly LoadableAsset<AudioClip> CrewmateIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Crewmate)!;
/// <summary>
/// The default Among Us Impostor Intro Sound.
/// </summary>
public static readonly LoadableAsset<AudioClip> ImpostorIntroSound =
CustomRoleUtils.GetIntroSound(RoleTypes.Impostor)!;
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
@@ -46,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);
@@ -58,6 +74,9 @@ public static class CustomRoleManager
pluginInfo.CustomRoles.Add((ushort)role.Role, role);
}
pluginInfo.PluginConfig.Save();
pluginInfo.PluginConfig.SaveOnConfigSet = oldConfigSetting;
}
private static RoleBehaviour? RegisterRole(Type roleType, MiraPluginInfo parentMod)
@@ -93,6 +112,21 @@ public static class CustomRoleManager
roleBehaviour.MaxCount = customRole.Configuration.MaxRoleCount;
roleBehaviour.RoleScreenshot = customRole.Configuration.OptionsScreenshot?.LoadAsset();
if (customRole.Configuration.Icon != null)
{
var asset = customRole.Configuration.Icon.LoadAsset();
if (asset != null)
{
roleBehaviour.RoleIconSolid = asset;
roleBehaviour.RoleIconWhite = asset;
}
}
if (customRole.Configuration.IntroSound != null)
{
roleBehaviour.IntroSound = customRole.Configuration.IntroSound.LoadAsset();
}
if (roleBehaviour.IsDead)
{
RoleManager.GhostRoles.Add(roleBehaviour.Role);
+62
View File
@@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using AmongUs.GameOptions;
using MiraAPI.Utilities.Assets;
using UnityEngine;
namespace MiraAPI.Roles;
/// <summary>
/// Utilities to make handling roles in-game easier.
/// </summary>
public static class CustomRoleUtils
{
/// <summary>
/// Gets all active in-game roles.
/// </summary>
/// <returns>A list of roles.</returns>
// ReSharper disable once MemberCanBePrivate.Global
public static IEnumerable<RoleBehaviour> GetActiveRoles() => PlayerControl.AllPlayerControls.ToArray().Select(x => x.Data.Role);
/// <summary>
/// Gets all active in-game roles in a certain team.
/// </summary>
/// <param name="team">The team you would like to check for.</param>
/// <returns>A list of roles with the team.</returns>
public static IEnumerable<RoleBehaviour> GetActiveRolesOfTeam(ModdedRoleTeams team) => GetActiveRoles().Where(x => x is ICustomRole customRole && customRole.Team == team);
/// <summary>
/// Gets all active in-game roles of a certain type.
/// </summary>
/// <typeparam name="T">The role Type you would like to check for. Must be a RoleBehaviour.</typeparam>
/// <returns>A list of roles with that specific type.</returns>
public static IEnumerable<T> GetActiveRolesOfType<T>() where T : RoleBehaviour => GetActiveRoles().OfType<T>();
/// <summary>
/// Creates a string builder for the Role Tab.
/// </summary>
/// <param name="role">The ICustomRole object.</param>
/// <returns>A StringBuilder.</returns>
public static StringBuilder CreateForRole(ICustomRole role)
{
var taskStringBuilder = new StringBuilder();
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
taskStringBuilder.Append("<size=70%>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
return taskStringBuilder;
}
/// <summary>
/// Returns an intro sound from a role.
/// </summary>
/// <param name="roleType">The role type.</param>
/// <returns>The intro sound.</returns>
public static LoadableAsset<AudioClip>? GetIntroSound(RoleTypes roleType)
{
var role = RoleManager.Instance.AllRoles.FirstOrDefault(role => role.Role == roleType);
if (role is ICustomRole customRole) return customRole.Configuration.IntroSound;
return new PreloadedAsset<AudioClip>(role!.IntroSound);
}
}
+6 -2
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;
@@ -34,6 +33,11 @@ public interface ICustomRole : IOptionable
/// </summary>
Color RoleColor { get; }
/// <summary>
/// Gets the color that should be used in the options menu.
/// </summary>
Color OptionsMenuColor => RoleColor;
/// <summary>
/// Gets the team of the role.
/// </summary>
@@ -201,7 +205,7 @@ public interface ICustomRole : IOptionable
/// Get the custom Role Tab text for this role.
/// </summary>
/// <returns>A StringBuilder with the role tab text.</returns>
StringBuilder SetTabText() => Helpers.CreateForRole(this);
StringBuilder SetTabText() => CustomRoleUtils.CreateForRole(this);
/// <summary>
/// Determine whether a given modifier can be applied to this role.
+1
View File
@@ -10,6 +10,7 @@ public static class MiraAssets
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);
}
@@ -0,0 +1,19 @@
using System;
namespace MiraAPI.Utilities.Assets;
/// <summary>
/// An implementation for creating loadable assets from preloaded 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));
/// <inheritdoc />
public override T LoadAsset()
{
return loadedAsset;
}
}
+4 -1
View File
@@ -30,6 +30,9 @@ public static class SpriteTools
throw new ArgumentException($"Resource not found: {resourcePath}");
}
return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
tex.name = resourcePath;
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;
}
}
+89
View File
@@ -0,0 +1,89 @@
using UnityEngine;
namespace MiraAPI.Utilities;
/// <summary>
/// Extension methods for the Color struct.
/// </summary>
public static class ColorExtensions
{
/// <summary>
/// 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>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;
}
/// <summary>
/// Calculates the contrast ratio between two colors (WCAG 2.1 specification).
/// </summary>
/// <param name="colorA">First color.</param>
/// <param name="colorB">Second color.</param>
/// <returns>Contrast ratio between the two colors.</returns>
public static float GetContrastRatio(this Color colorA, Color colorB)
{
var luminanceA = colorA.GetRelativeLuminance();
var luminanceB = colorB.GetRelativeLuminance();
return (Mathf.Max(luminanceA, luminanceB) + 0.05f) / (Mathf.Min(luminanceA, luminanceB) + 0.05f);
}
/// <summary>
/// Generates an accessible alternate color with sufficient contrast.
/// </summary>
/// <param name="color">Base color.</param>
/// <param name="desiredRatio">Target contrast ratio (default 4.5f for AA).</param>
/// <returns>Alternate color with sufficient contrast.</returns>
public static Color FindAlternateColor(this Color color, float desiredRatio = 4.5f)
{
var lightColor = FindMinimumContrastColor(color, Color.white, desiredRatio);
var darkColor = FindMinimumContrastColor(color, Color.black, desiredRatio);
return lightColor.GetContrastRatio(color) >= darkColor.GetContrastRatio(color) ? lightColor : darkColor;
}
/// <summary>
/// Finds the color with the minimum contrast ratio to the target color.
/// </summary>
/// <param name="baseColor">The base color to start from.</param>
/// <param name="targetColor">The target color to compare against.</param>
/// <param name="desiredRatio">The desired contrast ratio.</param>
/// <returns>The color with the minimum contrast ratio to the target color.</returns>
public static Color FindMinimumContrastColor(Color baseColor, Color targetColor, float desiredRatio)
{
var baseLuminance = baseColor.GetRelativeLuminance();
var isLightening = targetColor == Color.white;
var targetLuminance = isLightening
? desiredRatio * (baseLuminance + 0.05f) - 0.05f
: (baseLuminance + 0.05f) / desiredRatio - 0.05f;
if (isLightening && targetLuminance > 1f) return Color.white;
if (!isLightening && targetLuminance < 0f) return Color.black;
float low = 0f, high = 1f;
var bestColor = targetColor;
for (var i = 0; i < 10; i++)
{
var t = (low + high) / 2f;
var testColor = Color.Lerp(baseColor, targetColor, t);
var testLuminance = testColor.GetRelativeLuminance();
if ((isLightening && testLuminance >= targetLuminance) || (!isLightening && testLuminance <= targetLuminance))
{
high = t;
bestColor = testColor;
}
else
{
low = t;
}
}
return bestColor;
}
}
+105 -1
View File
@@ -2,10 +2,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Voting;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
@@ -39,6 +41,30 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
/// <summary>
/// Gets a PlayerControl from their PlayerVoteArea in a meeting.
/// </summary>
/// <param name="state">The vote area.</param>
/// <returns>The player's PlayerControl.</returns>
public static PlayerControl? GetPlayer(this PlayerVoteArea state) => GameData.Instance.GetPlayerById(state.TargetPlayerId)?.Object;
/// <summary>
/// Gets an int representing the amount of tasks a player has left.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>A count of how many tasks the player has left.</returns>
public static int GetTasksLeft(this PlayerControl player) => player.Data.Tasks.ToArray().Count(x => !x.Complete);
/// <summary>
/// Checks if a PlayerControl is the game's host.
/// </summary>
/// <param name="playerControl">The player you're checking for.</param>
/// <returns>If the player is the host, true, else false.</returns>
public static bool IsHost(this PlayerControl playerControl)
{
return TutorialManager.InstanceExists || AmongUsClient.Instance.HostId == playerControl.OwnerId;
}
/// <summary>
/// Determines if a float is an integer.
/// </summary>
@@ -49,6 +75,71 @@ public static class Extensions
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <summary>
/// Gets a cache of player's vote data components to improve performance.
/// </summary>
public static Dictionary<PlayerControl, PlayerVoteData> VoteDataComponents { get; } = [];
/// <summary>
/// Gets the PlayerVoteData of a player.
/// </summary>
/// <param name="player">The PlayerControl object.</param>
/// <returns>A PlayerVoteData if there is one, null otherwise.</returns>
public static PlayerVoteData GetVoteData(this PlayerControl player)
{
if (VoteDataComponents.TryGetValue(player, out var component))
{
return component;
}
component = player.GetComponent<PlayerVoteData>();
if (!component)
{
throw new InvalidOperationException("PlayerVoteData is not attached to the player.");
}
VoteDataComponents[player] = component;
return component;
}
public static KeyValuePair<byte, int> MaxPair(this Dictionary<byte, int> self, out bool tie)
{
tie = true;
var result = new KeyValuePair<byte, int>(byte.MaxValue, int.MinValue);
foreach (var keyValuePair in self)
{
if (keyValuePair.Value > result.Value)
{
result = keyValuePair;
tie = false;
}
else if (keyValuePair.Value == result.Value)
{
tie = true;
}
}
return result;
}
public static KeyValuePair<byte, float> MaxPair(this Dictionary<byte, float> self, out bool tie)
{
tie = true;
var result = new KeyValuePair<byte, float>(byte.MaxValue, int.MinValue);
foreach (var keyValuePair in self)
{
if (keyValuePair.Value > result.Value)
{
result = keyValuePair;
tie = false;
}
else if (Math.Abs(keyValuePair.Value - result.Value) < .05)
{
tie = true;
}
}
return result;
}
/// <summary>
/// Gets the best constructor for a type based on the specified arguments.
/// </summary>
@@ -65,6 +156,17 @@ public static class Extensions
.FirstOrDefault();
}
/// <summary>
/// Gets a proper string for an enum. (with spaces).
/// </summary>
/// <param name="enum">The enum you would like to change.</param>
/// <returns>A proper string for the enum.</returns>
public static string ToDisplayString(this Enum @enum)
{
var regex = new Regex(@"([^\^])([A-Z][a-z$])");
return regex.Replace(@enum.ToString(), m => $"{m.Groups[1].Value} {m.Groups[2].Value}");
}
/// <summary>
/// Gets the constructors of a type that match the specified arguments.
/// </summary>
@@ -181,12 +283,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)
@@ -245,6 +348,7 @@ public static class Extensions
/// <param name="color">The original color.</param>
/// <param name="amount">The amount to darken or lighten the original color by between 0.0 and 1.0.</param>
/// <returns>An alternate color that has been darkened or lightened.</returns>
[Obsolete("Use FindAlternateColor for WACG compliance.")]
public static Color GetAlternateColor(this Color color, float amount = 0.45f)
{
return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount);
+135 -12
View File
@@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using MiraAPI.Roles;
using TMPro;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -21,7 +19,124 @@ 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>
/// Determines whether a given probability check succeeds.
/// </summary>
/// <param name="probability">An integer value representing the success probability (0-100).</param>
/// <returns>True if the number falls in the range, false if not.</returns>
public static bool CheckChance(int probability)
{
switch (probability)
{
case 0:
return false;
case 100:
return true;
default:
{
var num = Random.RandomRangeInt(1, 101);
return num <= probability;
}
}
}
/// <summary>
/// Retrieves a vent from the ID.
/// </summary>
/// <param name="id">The vent ID.</param>
/// <returns>The vent.</returns>
public static Vent? GetVentById(int id)
{
return ShipStatus.Instance.AllVents.FirstOrDefault(vent => vent.Id == id);
}
/// <summary>
/// Creates an arrow.
/// </summary>
/// <param name="parent">The arrow gameObject's parent.</param>
/// <param name="color">The color of the arrow.</param>
/// <returns>The created ArrowBehaviour.</returns>
public static ArrowBehaviour CreateArrow(Transform parent, Color color)
{
var prefab = Object.FindObjectOfType<ArrowBehaviour>(true);
var arrow = Object.Instantiate(prefab, parent);
arrow.image = arrow.gameObject.GetComponent<SpriteRenderer>();
arrow.image.color = color;
arrow.gameObject.layer = 5;
arrow.gameObject.SetActive(true);
return arrow;
}
/// <summary>
/// Get the closest object of a specific type.
/// </summary>
/// <param name="objectList">A list of all the objects you'd like to check the distance for.</param>
/// <param name="position">The position of where you want to check from. For example: PlayerControl.LocalPlayer.transform.position.</param>
/// <typeparam name="T">The object type.</typeparam>
/// <returns>The closest object.</returns>
public static T? FindClosestObjectOfType<T>(List<T> objectList, Vector3 position) where T : MonoBehaviour
{
T? closest = null;
var closestDistanceSqr = Mathf.Infinity;
foreach (var obj in objectList)
{
if (obj == null)
{
continue;
}
var sqrDistance = (obj.transform.position - position).sqrMagnitude;
if (sqrDistance < closestDistanceSqr)
{
closestDistanceSqr = sqrDistance;
closest = obj;
}
}
return closest;
}
/// <summary>
/// Creates and shows a notification.
/// </summary>
/// <param name="text">The text you want to display.</param>
/// <param name="color">The color of the text and image.</param>
/// <param name="clip">The sound you want to play with the notification.</param>
/// <param name="spr">The sprite beside the notification.</param>
/// <returns>The created notification.</returns>
public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, AudioClip? clip = null, Sprite? spr = null)
{
return CreateAndShowNotification(text, color, new Vector3(0f, 0f, -2f), clip, spr);
}
/// <summary>
/// Creates and shows a notification.
/// </summary>
/// <param name="text">The text you want to display.</param>
/// <param name="color">The color of the text and image.</param>
/// <param name="localPos">The position of the notification.</param>
/// <param name="clip">The sound you want to play with the notification.</param>
/// <param name="spr">The sprite beside the notification.</param>
/// <returns>The created notification.</returns>
public static LobbyNotificationMessage CreateAndShowNotification(string text, Color color, Vector3 localPos, AudioClip? clip = null, Sprite? spr = null)
{
var popper = HudManager.Instance.Notifier;
var newMessage = Object.Instantiate(popper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, popper.transform);
newMessage.transform.localPosition = localPos;
newMessage.SetUp(text, spr ?? null, color, new System.Action(() => popper.OnMessageDestroy(newMessage)));
popper.lastMessageKey = -1;
popper.ShiftMessages();
popper.AddMessageToQueue(newMessage);
if (clip == null) return newMessage;
SoundManager.Instance.StopSound(clip);
SoundManager.Instance.PlaySound(clip, false, 2f);
return newMessage;
}
/// <summary>
@@ -265,16 +380,24 @@ public static class Helpers
}
/// <summary>
/// Creates a string builder for the Role Tab.
/// Converts the first letter of a string to uppercase.
/// </summary>
/// <param name="role">The ICustomRole object.</param>
/// <returns>A StringBuilder.</returns>
public static StringBuilder CreateForRole(ICustomRole role)
/// <param name="str">The string.</param>
/// <returns>The fixed string.</returns>
public static string FirstLetterToUpper(string str)
{
var taskStringBuilder = new StringBuilder();
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleColor.ToTextColor()}You are a <b>{role.RoleName}.</b></color>");
taskStringBuilder.Append("<size=70%>");
taskStringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{role.RoleLongDescription}");
return taskStringBuilder;
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower(CultureInfo.InvariantCulture));
}
/// <summary>
/// Gets a random string based on characters.
/// </summary>
/// <param name="length">The length of the string.</param>
/// <param name="chars">The characters in the random string.</param>
/// <returns>The random string.</returns>
public static string RandomString(int length, string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
{
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[Random.RandomRangeInt(0, s.Length)]).ToArray());
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace MiraAPI.Voting;
/// <summary>
/// Represents a custom vote.
/// </summary>
/// <param name="Voter">The player that voted.</param>
/// <param name="Suspect">The suspect being voted for.</param>
public record struct CustomVote(byte Voter, byte Suspect);
+110
View File
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using Il2CppInterop.Runtime.Attributes;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Voting;
/// <summary>
/// Handles player votes, and removing/adding additional votes.
/// </summary>
[RegisterInIl2Cpp]
public class PlayerVoteData(nint cppPtr) : MonoBehaviour(cppPtr)
{
/// <summary>
/// Gets the owner of this component.
/// </summary>
public PlayerControl Owner { get; private set; } = null!;
/// <summary>
/// Gets the list of votes the owner has.
/// </summary>
[HideFromIl2Cpp]
public List<CustomVote> Votes { get; private set; } = [];
/// <summary>
/// Gets or sets the amount of votes the owner has left.
/// </summary>
public int VotesRemaining
{
get => _votesRemaining;
set => _votesRemaining = Math.Max(0, value);
}
private int _votesRemaining = 1;
private void Awake()
{
Owner = GetComponent<PlayerControl>();
if (Owner == null)
{
Destroy(this);
}
}
/// <summary>
/// Returns whether the owner has voted for the specified player.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
/// <returns>True if the owner voted for the specified player, false otherwise.</returns>
public bool VotedFor(byte playerId)
{
return Votes.Exists(x => x.Suspect == playerId);
}
/// <summary>
/// Adds the voted player to the list.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void VoteForPlayer(byte playerId)
{
Votes.Add(new CustomVote(Owner.PlayerId, playerId));
}
/// <summary>
/// Removes the specified vote.
/// </summary>
/// <param name="vote">The vote you would like to remove.</param>
public void RemovePlayerVote(CustomVote vote)
{
Votes.Remove(vote);
}
/// <summary>
/// Removes a single vote from the owner.
/// </summary>
/// <param name="playerId">The target's playerId.</param>
public void RemovePlayerVote(byte playerId)
{
Votes.Remove(Votes.Find(x=>x.Suspect==playerId));
}
/// <summary>
/// Sets the player's votes remaining.
/// </summary>
/// <param name="votesRemaining">The amount of votes you would like to set it to.</param>
public void SetRemainingVotes(int votesRemaining)
{
VotesRemaining = votesRemaining;
}
/// <summary>
/// Adds votes to the owner.
/// </summary>
/// <param name="amount">The amount of votes you would like to add.</param>
public void IncreaseRemainingVotes(int amount)
{
SetRemainingVotes(VotesRemaining + amount);
}
/// <summary>
/// Removes votes from the owner.
/// </summary>
/// <param name="amount">The amount of votes you would like to remove.</param>
public void DecreaseRemainingVotes(int amount)
{
SetRemainingVotes(VotesRemaining - amount);
}
}
+180
View File
@@ -0,0 +1,180 @@
using System.Collections.Generic;
using System.Linq;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting.Voting;
using MiraAPI.Networking;
using MiraAPI.Utilities;
using Reactor.Networking.Attributes;
using Reactor.Utilities;
namespace MiraAPI.Voting;
/// <summary>
/// Utilities used for the Mira voting system.
/// </summary>
public static class VotingUtils
{
private const byte SkipVoteId = 253;
/// <summary>
/// Gets the exiled player from the list of votes. Returns null if no player is to be exiled.
/// </summary>
/// <param name="votes">>The list of votes to check.</param>
/// <param name="isTie">Whether the vote is a tie.</param>
/// <returns>The player to be exiled. Will be null if no player is to be exiled.</returns>
public static NetworkedPlayerInfo? GetExiled(List<CustomVote> votes, out bool isTie)
{
var max = CalculateNumVotes(votes).MaxPair(out var tie);
isTie = tie;
var exiled = GameData.Instance.AllPlayers.ToArray().FirstOrDefault(v => !tie && v.PlayerId == max.Key);
if (exiled is null || exiled.IsDead || exiled.Disconnected)
{
exiled = null;
}
return exiled;
}
/// <summary>
/// Handles when a vote is added and allows for other mods to override/modify.
/// </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)
{
var @event = new HandleVoteEvent(voteData, suspectIdx);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled)
{
return;
}
voteData.DecreaseRemainingVotes(1);
voteData.VoteForPlayer(suspectIdx);
}
/// <summary>
/// Networks the removal of votes. Used to remove votes when a player disconnects.
/// </summary>
/// <param name="source">The player who is sending the RPC. Should be the host.</param>
/// <param name="voterId">The player who voted.</param>
/// <param name="votedFor">The player who the voter voted for.</param>
[MethodRpc((uint)MiraRpc.RemoveVote)]
public static void RpcRemoveVote(PlayerControl source, byte voterId, byte votedFor)
{
if (source.OwnerId != AmongUsClient.Instance.HostId)
{
return;
}
MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == voterId).UnsetVote();
if (PlayerControl.LocalPlayer.PlayerId != voterId)
{
return;
}
MeetingHud.Instance.playerStates.First(state => state.TargetPlayerId == votedFor).ThumbsDown.enabled = false;
if (!AmongUsClient.Instance.AmHost)
{
var voteData = PlayerControl.LocalPlayer.GetVoteData();
voteData.DecreaseRemainingVotes(1);
voteData.RemovePlayerVote(votedFor);
}
foreach (var t in MeetingHud.Instance.playerStates)
{
t.voteComplete = false;
}
MeetingHud.Instance.SkipVoteButton.voteComplete = false;
MeetingHud.Instance.SkipVoteButton.gameObject.SetActive(true);
}
/// <summary>
/// Calculates the total number of votes.
/// </summary>
/// <param name="votes">A list of calculated votes.</param>
/// <returns>The total votes.</returns>
public static Dictionary<byte, float> CalculateNumVotes(IEnumerable<CustomVote> votes)
{
var dictionary = new Dictionary<byte, float>();
foreach (var vote in votes.Select(v=>v.Suspect))
{
if (!dictionary.TryAdd(vote, 1))
{
dictionary[vote] += 1;
}
}
return dictionary;
}
/// <summary>
/// Calculates votes to check if all players have voted.
/// </summary>
/// <returns>The list of votes.</returns>
public static List<CustomVote> CalculateVotes()
{
return
[
.. Helpers.GetAlivePlayers()
.SelectMany(player => player.GetVoteData().Votes)
];
}
/// <summary>
/// Handles the populating of results locally. Called by the PopulateResultsRpc.
/// </summary>
/// <param name="votes">The list of networked votes.</param>
public static void HandlePopulateResults(List<CustomVote> votes)
{
PopulateResultsEvent @event = new PopulateResultsEvent(votes);
MiraEventManager.InvokeEvent(@event);
if (@event.IsCancelled)
{
return;
}
MeetingHud.Instance.TitleText.text =
DestroyableSingleton<TranslationController>.Instance.GetString(
StringNames.MeetingVotingResults,
Il2CppSystem.Array.Empty<Il2CppSystem.Object>());
var delays = new Dictionary<byte, int>();
var num = 0;
for (var i = 0; i < MeetingHud.Instance.playerStates.Length; i++)
{
var playerVoteArea = MeetingHud.Instance.playerStates[i];
playerVoteArea.ClearForResults();
foreach (var vote in votes)
{
var playerById = GameData.Instance.GetPlayerById(vote.Voter);
if (playerById == null)
{
Logger<MiraApiPlugin>.Error($"Couldn't find player info for voter: {vote.Voter}");
}
else if (i == 0 && vote.Suspect == SkipVoteId)
{
MeetingHud.Instance.BloopAVoteIcon(playerById, num, MeetingHud.Instance.SkippedVoting.transform);
num++;
}
else if (vote.Suspect == playerVoteArea.TargetPlayerId)
{
if (!delays.TryAdd(vote.Suspect, 0))
{
delays[vote.Suspect]++;
}
MeetingHud.Instance.BloopAVoteIcon(playerById, delays[vote.Suspect], playerVoteArea.transform);
}
}
}
}
}