mirror of
https://github.com/All-Of-Us-Mods/MiraAPI.git
synced 2026-10-20 11:39:45 +00:00
Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b368c1ef | ||
|
|
103c32f386 | ||
|
|
662b867dc5 | ||
|
|
adae0225e9 | ||
|
|
0d347b8b1b | ||
|
|
4fad586aa7 | ||
|
|
6d51e6806a | ||
|
|
b28a0a1253 | ||
|
|
51b54d4340 | ||
|
|
ea9bf162ea | ||
|
|
57105f487b | ||
|
|
c5e424e8ff | ||
|
|
d3b40aec5a | ||
|
|
521e5a12fd | ||
|
|
e6e4a86546 | ||
|
|
bb0b678747 | ||
|
|
f48143610c | ||
|
|
2cfc5ac039 | ||
|
|
71b537dba9 | ||
|
|
e9beb3b423 | ||
|
|
23c0397afe | ||
|
|
7917d4b5d9 | ||
|
|
bf795a2e36 | ||
|
|
144a95fa3b | ||
|
|
e25b78705b | ||
|
|
19ba9da2c3 | ||
|
|
ce9864ae8b | ||
|
|
8f76f21cb1 | ||
|
|
bc7b835dc8 | ||
|
|
f0b2f9ee2a | ||
|
|
25eafe664b | ||
|
|
db327604c5 | ||
|
|
1462061df9 | ||
|
|
fb40664bd0 | ||
|
|
3cba870980 | ||
|
|
ab2f03ee4d | ||
|
|
645d1dad1c | ||
|
|
a04e0d28db | ||
|
|
b04d9619b8 | ||
|
|
33fd657eb3 | ||
|
|
529fc89fc5 | ||
|
|
6ab283d39c | ||
|
|
08d8a48701 | ||
|
|
708534ed5c | ||
|
|
886ef0e31f | ||
|
|
0b9e54eb50 | ||
|
|
bad6648f44 | ||
|
|
664f8cb2e8 | ||
|
|
989b511276 |
No files matched your search
@@ -1,10 +1,18 @@
|
||||
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 MiraAPI.Utilities.Assets;
|
||||
using Reactor.Utilities;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Example;
|
||||
|
||||
@@ -29,6 +37,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 +82,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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MiraAPI.Roles;
|
||||
using AmongUs.GameOptions;
|
||||
using MiraAPI.Roles;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -12,9 +13,10 @@ 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 void PlayerControlFixedUpdate(PlayerControl playerControl)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -463,7 +462,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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,16 @@ namespace MiraAPI;
|
||||
[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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,13 +67,13 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
foreach (var modifier in _toRemove)
|
||||
foreach (var modifier in _toRemove.ToArray())
|
||||
{
|
||||
modifier.OnDeactivate();
|
||||
Modifiers.Remove(modifier);
|
||||
}
|
||||
|
||||
foreach (var modifier in _toAdd)
|
||||
foreach (var modifier in _toAdd.ToArray())
|
||||
{
|
||||
Modifiers.Add(modifier);
|
||||
modifier.Initialized = true;
|
||||
@@ -79,15 +87,17 @@ public class ModifierComponent(IntPtr cppPtr) : MonoBehaviour(cppPtr)
|
||||
|
||||
if (_toAdd.Count > 0 || _toRemove.Count > 0)
|
||||
{
|
||||
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 +105,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 +152,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 +164,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 +284,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 +306,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 +329,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 +395,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 +467,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 +479,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 +505,6 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,30 @@ public static class ModifierExtensions
|
||||
return player.GetModifierComponent().HasModifier(typeId, predicate);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Checks if the player has a specific modifier by its GUID.
|
||||
/// </summary>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,101 @@
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(HatsTab))]
|
||||
public static class HatsTabPatches
|
||||
{
|
||||
private static SortedList<string, List<HatData>> sortedHats = [];
|
||||
|
||||
[HarmonyPatch(nameof(HatsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(HatsTab __instance)
|
||||
{
|
||||
__instance.currentHat = HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat);
|
||||
var allHats = HatManager.Instance.GetUnlockedHats().ToImmutableList();
|
||||
|
||||
if (sortedHats.Count == 0)
|
||||
{
|
||||
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
|
||||
sortedHats = new SortedList<string, List<HatData>>(comparer);
|
||||
foreach (var hat in allHats)
|
||||
{
|
||||
if (!sortedHats.ContainsKey(hat.StoreName)) sortedHats[hat.StoreName] = [];
|
||||
sortedHats[hat.StoreName].Add(hat);
|
||||
}
|
||||
}
|
||||
|
||||
GenerateHats(__instance);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GenerateHats(HatsTab __instance)
|
||||
{
|
||||
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
int hatIndex = 0;
|
||||
|
||||
foreach ((string groupName, List<HatData> hats) in sortedHats)
|
||||
{
|
||||
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
text.fontSizeMin = 0f;
|
||||
float xLerp = __instance.XRange.Lerp(0.5f);
|
||||
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
foreach (var hat in hats.OrderBy(HatManager.Instance.allHats.IndexOf))
|
||||
{
|
||||
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), hat);
|
||||
hatIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.currentHatIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(HatsTab __instance, Vector2 position, HatData hat)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = hat.ProductId;
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectHat(hat)));
|
||||
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectHat(HatManager.Instance.GetHatById(DataManager.Player.Customization.Hat))));
|
||||
colorChip.Inner.SetHat(hat, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, hat);
|
||||
colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = hat;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities.Assets;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
/// <summary>
|
||||
/// General MainMenuManage patches.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(MainMenuManager))]
|
||||
public static class MainMenuManagerPatches
|
||||
{
|
||||
/// <summary>
|
||||
/// A postifix on Awake to load all the addressables registered.
|
||||
/// </summary>
|
||||
[HarmonyPatch(nameof(MainMenuManager.Awake))]
|
||||
[HarmonyPostfix]
|
||||
public static void AwakePostfix()
|
||||
{
|
||||
AddressablesLoader.LoadAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using Innersloth.Assets;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(NameplatesTab))]
|
||||
public static class NameplatesTabPatches
|
||||
{
|
||||
private static readonly SortedList<string, List<NamePlateData>> SortedNameplates = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
|
||||
|
||||
internal static void AddRange(IEnumerable<(string Key, NamePlateData Visor)> data)
|
||||
{
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (!SortedNameplates.ContainsKey(item.Key)) SortedNameplates.Add(item.Key, []);
|
||||
SortedNameplates[item.Key].Add(item.Visor);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(NameplatesTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(NameplatesTab __instance)
|
||||
{
|
||||
__instance.plateId = HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.namePlate).ProdId;
|
||||
|
||||
if (!SortedNameplates.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedNamePlates().Select(x => ("Vanilla", x)));
|
||||
GenerateHats(__instance);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GenerateHats(NameplatesTab __instance)
|
||||
{
|
||||
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
int hatIndex = 0;
|
||||
|
||||
foreach ((string groupName, List<NamePlateData> visors) in SortedNameplates)
|
||||
{
|
||||
hatIndex = (hatIndex + 1) / 2 * 2; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
text.fontSizeMin = 0f;
|
||||
float xLerp = __instance.XRange.Lerp(0.5f);
|
||||
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 2;
|
||||
foreach (var visor in visors.OrderBy(HatManager.Instance.allNamePlates.IndexOf))
|
||||
{
|
||||
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
|
||||
hatIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.currentNameplateIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(NameplatesTab __instance, Vector2 position, NamePlateData namePlate)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = namePlate.ProductId;
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectNameplate(namePlate)));
|
||||
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))));
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = namePlate.ProdId;
|
||||
|
||||
var x = (NamePlateViewData viewdata) =>
|
||||
{
|
||||
colorChip.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = viewdata?.Image;
|
||||
//(colorChip as NameplateChip).image.sprite = viewdata?.Image;
|
||||
};
|
||||
__instance.StartCoroutine(AddressableAssetExtensions.CoLoadAssetAsync<NamePlateViewData>(__instance, namePlate.GetAssetReference(), x));
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Tag = namePlate;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(SkinsTab))]
|
||||
public static class SkinsTabPatches
|
||||
{
|
||||
private static SortedList<string, List<SkinData>> sortedSkins = [];
|
||||
|
||||
[HarmonyPatch(nameof(SkinsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(SkinsTab __instance)
|
||||
{
|
||||
__instance.skinId = HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin).ProdId;
|
||||
var allSkins = HatManager.Instance.GetUnlockedSkins().ToImmutableList();
|
||||
|
||||
if (sortedSkins.Count == 0)
|
||||
{
|
||||
var comparer = new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture);
|
||||
sortedSkins = new SortedList<string, List<SkinData>>(comparer);
|
||||
foreach (var skin in allSkins)
|
||||
{
|
||||
if (!sortedSkins.ContainsKey(skin.StoreName)) sortedSkins[skin.StoreName] = [];
|
||||
sortedSkins[skin.StoreName].Add(skin);
|
||||
}
|
||||
}
|
||||
|
||||
GenerateHats(__instance);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GenerateHats(SkinsTab __instance)
|
||||
{
|
||||
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
int hatIndex = 0;
|
||||
|
||||
foreach ((string groupName, List<SkinData> skins) in sortedSkins)
|
||||
{
|
||||
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
text.fontSizeMin = 0f;
|
||||
float xLerp = __instance.XRange.Lerp(0.5f);
|
||||
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
foreach (var skin in skins.OrderBy(HatManager.Instance.allSkins.IndexOf))
|
||||
{
|
||||
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), skin);
|
||||
hatIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.currentSkinIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(SkinsTab __instance, Vector2 position, SkinData skin)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = skin.ProductId;
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectSkin(skin)));
|
||||
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectSkin(HatManager.Instance.GetSkinById(DataManager.Player.Customization.Skin))));
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = skin.ProductId;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, skin);
|
||||
skin.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = skin.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = skin;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using AmongUs.Data;
|
||||
using HarmonyLib;
|
||||
using MiraAPI.Utilities;
|
||||
using Reactor.Utilities.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace MiraAPI.Patches.Menu;
|
||||
|
||||
[HarmonyPatch(typeof(VisorsTab))]
|
||||
public static class VisorsTabPatches
|
||||
{
|
||||
private static readonly SortedList<string, List<VisorData>> SortedVisors = new(new ControllableComparer<string>(["vanilla"], [], StringComparer.InvariantCulture));
|
||||
|
||||
internal static void AddRange(IEnumerable<(string Key, VisorData Visor)> data)
|
||||
{
|
||||
foreach (var item in data)
|
||||
{
|
||||
if (!SortedVisors.ContainsKey(item.Key)) SortedVisors.Add(item.Key, []);
|
||||
SortedVisors[item.Key].Add(item.Visor);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(nameof(VisorsTab.OnEnable))]
|
||||
[HarmonyPrefix]
|
||||
public static bool OnEnablePrefix(VisorsTab __instance)
|
||||
{
|
||||
__instance.visorId = HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor).ProdId;
|
||||
|
||||
if (!SortedVisors.ContainsKey("Vanilla")) AddRange(DestroyableSingleton<HatManager>.Instance.GetUnlockedVisors().Select(x => ("Vanilla", x)));
|
||||
GenerateHats(__instance);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void GenerateHats(VisorsTab __instance)
|
||||
{
|
||||
foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.Destroy();
|
||||
__instance.ColorChips.Clear();
|
||||
__instance.scroller.Inner.GetComponentsInChildren<TextMeshPro>().Do(x => x.gameObject.Destroy());
|
||||
|
||||
var groupNameText = __instance.GetComponentInChildren<TextMeshPro>(false);
|
||||
|
||||
int hatIndex = 0;
|
||||
|
||||
foreach ((string groupName, List<VisorData> visors) in SortedVisors)
|
||||
{
|
||||
hatIndex = (hatIndex + 4) / 5 * 5; // yes it looks redundant, but consider hatindex = 0, symbolically it would be 4, the computer calculates 0.
|
||||
var text = Object.Instantiate(groupNameText, __instance.scroller.Inner);
|
||||
text.enabled = true;
|
||||
text.gameObject.transform.localScale = Vector3.one;
|
||||
text.GetComponent<TextTranslatorTMP>().Destroy();
|
||||
text.EnableStencilMasking();
|
||||
text.text = $"{groupName}";
|
||||
text.alignment = TextAlignmentOptions.Center;
|
||||
text.fontSize = 3f;
|
||||
text.fontSizeMax = 3f;
|
||||
text.fontSizeMin = 0f;
|
||||
float xLerp = __instance.XRange.Lerp(0.5f);
|
||||
float yLerp = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
text.transform.localPosition = new Vector3(xLerp, yLerp, -1f);
|
||||
|
||||
hatIndex += 5;
|
||||
foreach (var visor in visors.OrderBy(HatManager.Instance.allVisors.IndexOf))
|
||||
{
|
||||
float hatXposition = __instance.XRange.Lerp(hatIndex % __instance.NumPerRow / (__instance.NumPerRow - 1f));
|
||||
float hatYposition = __instance.YStart - hatIndex / __instance.NumPerRow * __instance.YOffset;
|
||||
GenerateColorChip(__instance, new Vector2(hatXposition, hatYposition), visor);
|
||||
hatIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
__instance.scroller.ContentYBounds.max = -(__instance.YStart - (hatIndex + 1) / __instance.NumPerRow * __instance.YOffset) - 3f;
|
||||
__instance.currentVisorIsEquipped = true;
|
||||
}
|
||||
|
||||
private static void GenerateColorChip(VisorsTab __instance, Vector2 position, VisorData visor)
|
||||
{
|
||||
var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner);
|
||||
colorChip.gameObject.name = visor.ProductId;
|
||||
colorChip.Button.OnClick.AddListener((Action)(() => __instance.ClickEquip()));
|
||||
colorChip.Button.OnMouseOver.AddListener((Action)(() => __instance.SelectVisor(visor)));
|
||||
colorChip.Button.OnMouseOut.AddListener((Action)(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))));
|
||||
colorChip.Button.ClickMask = __instance.scroller.Hitbox;
|
||||
colorChip.ProductId = visor.ProductId;
|
||||
colorChip.SelectionHighlight.gameObject.SetActive(false);
|
||||
__instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor);
|
||||
visor.SetPreview(colorChip.Inner.FrontLayer, __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color);
|
||||
colorChip.transform.localPosition = new Vector3(position.x, position.y, -1f);
|
||||
colorChip.Inner.transform.localPosition = visor.ChipOffset + new Vector2(0f, -0.3f);
|
||||
colorChip.Tag = visor;
|
||||
__instance.ColorChips.Add(colorChip);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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); }));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,4 @@ public static class RoleBehaviourPatch
|
||||
__result = behaviour.RoleColor;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using MiraAPI.Events;
|
||||
using MiraAPI.Events.Vanilla.Gameplay;
|
||||
using MiraAPI.Modifiers;
|
||||
using MiraAPI.Roles;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MiraAPI.Patches.Roles;
|
||||
|
||||
@@ -15,6 +16,43 @@ public static class RoleManagerPatches
|
||||
[HarmonyPatch(nameof(RoleManager.SetRole))]
|
||||
public static void SetRolePatch(RoleManager __instance, PlayerControl targetPlayer, RoleTypes roleType)
|
||||
{
|
||||
if (!targetPlayer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
case false when targetPlayer.Data.IsDead:
|
||||
targetPlayer.Revive();
|
||||
break;
|
||||
}
|
||||
|
||||
var @event = new SetRoleEvent(targetPlayer, roleType);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
}
|
||||
|
||||
@@ -33,4 +33,15 @@ public static class MinigameStubs
|
||||
{
|
||||
// nothing needed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixes the error when creating custom minigames. Credit to Submerged for discovering this fix.
|
||||
/// </summary>
|
||||
/// <param name="__instance">The Minigame instance.</param>
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Minigame), nameof(Minigame.Begin))]
|
||||
public static void MinigameErrorFixPatch(Minigame __instance)
|
||||
{
|
||||
__instance.logger ??= new Logger("Minigame", Logger.Level.Info, Logger.Category.Gameplay);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
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.Utilities;
|
||||
using MiraAPI.Voting;
|
||||
|
||||
namespace MiraAPI.Patches.Voting;
|
||||
|
||||
[HarmonyPatch(typeof(MeetingHud))]
|
||||
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);
|
||||
}
|
||||
}
|
||||
var @event = new StartMeetingEvent(__instance);
|
||||
MiraEventManager.InvokeEvent(@event);
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -97,7 +97,7 @@ public sealed class MiraPluginManager
|
||||
}
|
||||
|
||||
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.");
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 872 B |
@@ -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.
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -93,6 +106,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
using Il2CppInterop.Runtime.Attributes;
|
||||
using MiraAPI.Patches.Menu;
|
||||
using Reactor.Utilities;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
|
||||
namespace MiraAPI.Utilities.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// The component for loading Addressables, and cosmetics.
|
||||
/// </summary>
|
||||
public static class AddressablesLoader
|
||||
{
|
||||
private static bool _isInitialized;
|
||||
|
||||
private static readonly List<(string Location, string ProviderSuffix)> CatalogLocations = [];
|
||||
private static readonly List<string> LoadedLocations = [];
|
||||
|
||||
private static readonly List<string> RegisteredHatKeys = [];
|
||||
private static readonly List<(string, string)> RegisteredVisorKeys = [];
|
||||
private static readonly List<(string, string)> RegisteredNameplateKeys = [];
|
||||
private static readonly List<string> RegisteredSkinKeys = [];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a specific addressables package to load asynchronously at the start of the game, when possible.
|
||||
/// </summary>
|
||||
/// <param name="location">The location, remote or otherwise, of the addressables.</param>
|
||||
/// <param name="providerSuffix">The suffix of the provider for an addressables package.</param>
|
||||
public static void RegisterCatalog(string location, string providerSuffix = "")
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more catalogs.");
|
||||
return;
|
||||
}
|
||||
|
||||
CatalogLocations.Add((location, providerSuffix));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a specific addressables key as only containing <see cref="HatData"/>'s to load.
|
||||
/// </summary>
|
||||
/// <param name="addressablesKey">The key/label/group for a List <see cref="HatData"/>.</param>
|
||||
public static void RegisterHats(string addressablesKey)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for hats.");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredHatKeys.Add(addressablesKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a specific addressables key as only containing <see cref="SkinData"/>'s to load.
|
||||
/// </summary>
|
||||
/// <param name="addressablesKey">The key/label/group for a List <see cref="SkinData"/>.</param>
|
||||
public static void RegisterSkins(string addressablesKey)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for skins.");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredSkinKeys.Add(addressablesKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a specific addressables key as only containing <see cref="NamePlateData"/>'s to load.
|
||||
/// </summary>
|
||||
/// <param name="addressablesKey">The key/label/group for a List <see cref="NamePlateData"/>.</param>
|
||||
/// /// <param name="groupTitle">The title of the group for visors.</param>
|
||||
public static void RegisterNameplates(string addressablesKey, string groupTitle)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for nameplates.");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredNameplateKeys.Add((addressablesKey, groupTitle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a specific addressables key as only containing <see cref="VisorData"/>'s to load.
|
||||
/// </summary>
|
||||
/// <param name="addressablesKey">The key/label/group for a List <see cref="VisorData"/>.</param>
|
||||
/// <param name="groupTitle">The title of the group for visors.</param>
|
||||
public static void RegisterVisors(string addressablesKey, string groupTitle)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized, cannot register more keys for visors.");
|
||||
return;
|
||||
}
|
||||
|
||||
RegisteredVisorKeys.Add((addressablesKey, groupTitle));
|
||||
}
|
||||
|
||||
internal static void LoadAll()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error("AddressablesLoader has already been initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
foreach (var (location, providerSuffix) in CatalogLocations)
|
||||
{
|
||||
Coroutines.Start(CoLoadAddressables(location, providerSuffix));
|
||||
}
|
||||
|
||||
Coroutines.Start(LoadCosmetics());
|
||||
}
|
||||
|
||||
internal static IEnumerator CoLoadAddressables(string location, string suffix = "")
|
||||
{
|
||||
while (!AmongUsClient.Instance) yield return null;
|
||||
// Load the local/remote content catalog
|
||||
var catalogOperation = Addressables.LoadContentCatalog(location, suffix);
|
||||
yield return catalogOperation;
|
||||
|
||||
// Check for errors
|
||||
if (catalogOperation.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to load catalog {location}.");
|
||||
}
|
||||
|
||||
Logger<MiraApiPlugin>.Info($"Loaded addressables {location}.");
|
||||
LoadedLocations.Add(location);
|
||||
}
|
||||
|
||||
[HideFromIl2Cpp]
|
||||
internal static IEnumerator LoadCosmetics()
|
||||
{
|
||||
while (!AmongUsClient.Instance || CatalogLocations.Select(x=>x.Location).Any(x=>!LoadedLocations.Contains(x))) yield return null;
|
||||
|
||||
var hatBehaviours = DiscoverData<HatData>(RegisteredHatKeys);
|
||||
var skinBehaviours = DiscoverData<SkinData>(RegisteredSkinKeys);
|
||||
var namePlateBehaviours = DiscoverAndReportData<NamePlateData>(RegisteredNameplateKeys);
|
||||
var visorBehaviours = DiscoverAndReportData<VisorData>(RegisteredVisorKeys);
|
||||
|
||||
var hatData = new List<HatData>();
|
||||
hatData.AddRange(DestroyableSingleton<HatManager>.Instance.allHats);
|
||||
hatData.ForEach(x => x.StoreName = "Vanilla");
|
||||
DestroyableSingleton<HatManager>.Instance.allHats = PrepareArray(hatData, hatBehaviours);
|
||||
|
||||
var skinData = new List<SkinData>();
|
||||
skinData.AddRange(DestroyableSingleton<HatManager>.Instance.allSkins);
|
||||
skinData.ForEach(x => x.StoreName = "Vanilla");
|
||||
DestroyableSingleton<HatManager>.Instance.allSkins = PrepareArray(skinData, skinBehaviours);
|
||||
|
||||
var visorData = new List<VisorData>();
|
||||
visorData.AddRange(DestroyableSingleton<HatManager>.Instance.allVisors);
|
||||
VisorsTabPatches.AddRange(visorBehaviours);
|
||||
DestroyableSingleton<HatManager>.Instance.allVisors = PrepareArray(visorData, visorBehaviours.Select(x=>x.Data).ToList());
|
||||
|
||||
var namePlateData = new List<NamePlateData>();
|
||||
namePlateData.AddRange(DestroyableSingleton<HatManager>.Instance.allNamePlates);
|
||||
NameplatesTabPatches.AddRange(namePlateBehaviours);
|
||||
DestroyableSingleton<HatManager>.Instance.allNamePlates = PrepareArray(namePlateData, namePlateBehaviours.Select(x => x.Data).ToList());
|
||||
}
|
||||
|
||||
private static T[] PrepareArray<T>(List<T> data, List<T> behaviours) where T : CosmeticData
|
||||
{
|
||||
var count = data.Count;
|
||||
for (int i = 0; i < behaviours.Count; i++)
|
||||
{
|
||||
behaviours[i].displayOrder = count + i;
|
||||
data.Add(behaviours[i]);
|
||||
}
|
||||
return data.ToArray();
|
||||
}
|
||||
|
||||
private static List<T> DiscoverData<T>(List<string> tags)
|
||||
{
|
||||
var behaviours = new List<T>();
|
||||
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allLocations = Addressables.LoadResourceLocationsAsync(tag).WaitForCompletion();
|
||||
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
|
||||
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
|
||||
behaviours.AddRange(array.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
|
||||
}
|
||||
}
|
||||
return behaviours;
|
||||
}
|
||||
|
||||
private static List<(string Category, T Data)> DiscoverAndReportData<T>(List<(string Tag, string Category)> tags)
|
||||
{
|
||||
var behaviours = new List<(string, T)>();
|
||||
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allLocations = Addressables.LoadResourceLocationsAsync(tag.Tag).WaitForCompletion();
|
||||
var assets = Addressables.LoadAssetsAsync<T>(allLocations, null, false).WaitForCompletion();
|
||||
var array = new Il2CppSystem.Collections.Generic.List<T>(assets.Pointer);
|
||||
behaviours.AddRange(array.ToArray().Select(x => (tag.Category, x)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger<MiraApiPlugin>.Error($"Failed to find tag {tag}");
|
||||
}
|
||||
}
|
||||
return behaviours;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace MiraAPI.Utilities.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class for loading an asset from ALL addressable locations.
|
||||
/// </summary>
|
||||
/// <param name="uid">The guid of the asset.</param>
|
||||
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
|
||||
public class LoadableAddressableAsset<T>(string uid) : LoadableAsset<T> where T : UnityEngine.Object
|
||||
{
|
||||
private readonly Action<LoadableAddressableAsset<T>>? _gcAction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoadableAddressableAsset{T}"/> class, with added garbageCollector logic,
|
||||
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
|
||||
/// </summary>
|
||||
/// <param name="uid">The guid of the asset.</param>
|
||||
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
|
||||
public LoadableAddressableAsset(string uid, Action<LoadableAddressableAsset<T>> garbageCollection) : this(uid)
|
||||
{
|
||||
_gcAction = garbageCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the asset from addressables.
|
||||
/// </summary>
|
||||
/// <returns>The asset.</returns>
|
||||
/// <exception cref="Exception">The asset did not load properly.</exception>
|
||||
public override T LoadAsset()
|
||||
{
|
||||
if (LoadedAsset != null)
|
||||
{
|
||||
return LoadedAsset;
|
||||
}
|
||||
|
||||
var remoteReference = new AssetReference(uid);
|
||||
|
||||
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
|
||||
|
||||
LoadedAsset = remoteReference.LoadAssetAsync<T>().WaitForCompletion();
|
||||
if (LoadedAsset == null)
|
||||
{
|
||||
GC.EndNoGCRegion();
|
||||
throw new InvalidOperationException($"INVALID ASSET: {uid}");
|
||||
}
|
||||
|
||||
_gcAction?.Invoke(this);
|
||||
GC.EndNoGCRegion();
|
||||
|
||||
return LoadedAsset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Reactor.Utilities;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace MiraAPI.Utilities.Assets;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class for loading groups of assets from ALL addressable locations.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the asset to be loaded.</typeparam>
|
||||
public class LoadableAddressableAssets<T>(string key) where T : UnityEngine.Object
|
||||
{
|
||||
private readonly Action<LoadableAddressableAssets<T>>? _gcAction;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets reference to the loaded asset. Intended to be used for caching purposes.
|
||||
/// </summary>
|
||||
protected List<T>? LoadedAssets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoadableAddressableAssets{T}"/> class, with added garbageCollector logic,
|
||||
/// to help with https://github.com/BepInEx/Il2CppInterop/issues/40.
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the assets.</param>
|
||||
/// <param name="garbageCollection">A lambda that allows GCHandle.Alloc calls without garbage collection interferences.</param>
|
||||
public LoadableAddressableAssets(string key, Action<LoadableAddressableAssets<T>> garbageCollection) : this(key)
|
||||
{
|
||||
_gcAction = garbageCollection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the asset from addressables.
|
||||
/// </summary>
|
||||
/// <returns>The asset.</returns>
|
||||
/// <exception cref="Exception">The asset did not load properly.</exception>
|
||||
public ReadOnlyCollection<T> LoadAssets()
|
||||
{
|
||||
if (LoadedAssets != null)
|
||||
{
|
||||
return LoadedAssets.AsReadOnly();
|
||||
}
|
||||
|
||||
var locations = Addressables.LoadResourceLocationsAsync(key).WaitForCompletion();
|
||||
|
||||
if (!GC.TryStartNoGCRegion(4096, true)) Logger<MiraApiPlugin>.Error("Could not start NoGCRegion of size 4kb, there is the possibility of injected unmanaged classes being garbage collected as per BepInEx/Il2CppInterop/issues/40");
|
||||
|
||||
var assetsIList = Addressables.LoadAssetsAsync<T>(locations, null, false).WaitForCompletion();
|
||||
if (assetsIList == null)
|
||||
{
|
||||
GC.EndNoGCRegion();
|
||||
throw new InvalidOperationException($"INVALID ASSET/s: {key}");
|
||||
}
|
||||
|
||||
var assetsList = new Il2CppSystem.Collections.Generic.List<T>(assetsIList.Pointer);
|
||||
LoadedAssets = assetsList.ToArray().ToList();
|
||||
|
||||
_gcAction?.Invoke(this);
|
||||
GC.EndNoGCRegion();
|
||||
|
||||
return LoadedAssets.AsReadOnly();
|
||||
}
|
||||
}
|
||||
@@ -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 existing assets.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="LoadableAsset{T}"/>
|
||||
/// <inheritdoc />
|
||||
public class PreloadedAsset<T>(T asset) : LoadableAsset<T> where T : UnityEngine.Object
|
||||
{
|
||||
private readonly T _loadedAsset = asset ?? throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T LoadAsset()
|
||||
{
|
||||
return _loadedAsset;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MiraAPI.Utilities;
|
||||
|
||||
internal sealed class ControllableComparer<T> : IComparer<T> where T : IComparable
|
||||
{
|
||||
private readonly T[] _forcedToBottom;
|
||||
private readonly T[] _forcedToTop;
|
||||
private readonly IComparer<T> _fallbackComparer;
|
||||
public ControllableComparer(T[] forcedToBottom, T[] forcedToTop, IComparer<T> fallbackComparer)
|
||||
{
|
||||
_forcedToBottom = forcedToBottom;
|
||||
_forcedToTop = forcedToTop;
|
||||
_fallbackComparer = fallbackComparer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Compare(T? x, T? y)
|
||||
{
|
||||
if ((_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) || (_forcedToTop.Contains(x) && _forcedToTop.Contains(y)))
|
||||
return _fallbackComparer.Compare(x, y);
|
||||
|
||||
if (_forcedToBottom.Contains(x))
|
||||
return 1;
|
||||
if (_forcedToBottom.Contains(y))
|
||||
return -1;
|
||||
|
||||
if (_forcedToTop.Contains(x))
|
||||
return -1;
|
||||
if (_forcedToTop.Contains(y))
|
||||
return 1;
|
||||
|
||||
return _fallbackComparer.Compare(x, y);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+134
-11
@@ -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;
|
||||
@@ -24,6 +22,123 @@ public static class Helpers
|
||||
return PlayerControl.AllPlayerControls.ToArray().Where(x => !x.Data.IsDead).ToList();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Returns an empty coroutine.
|
||||
/// </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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user