Compare commits

...
Author SHA1 Message Date
XtraCube fb99f02549 i forgor 2025-02-17 11:10:26 -05:00
XtraCube 8382b9adf1 clearer naming 2025-02-17 11:08:12 -05:00
XtraCube cb1a818377 role configuration improvements 2025-02-17 11:03:39 -05:00
XtraCube e950278bf9 seperate some role configs 2025-02-17 10:58:25 -05:00
XtraCube 3de9482233 add missing minigame stubs 2025-02-17 10:32:28 -05:00
XtraCube 4508aaeb55 add role stubs 2025-02-17 10:32:18 -05:00
XtraCube ee15a1acbe fix neutral default role group title 2025-02-17 09:46:09 -05:00
XtraCube 113c716fd3 hide role groups in settings menu 2025-02-17 00:00:37 -05:00
ang-xd f7c34fb5cd Add ShowInFreeplay configuration 2025-02-16 23:13:04 -05:00
XtraCube 37446964c5 change neutral team to Custom and add more customization 2025-02-16 22:33:16 -05:00
ang-xd 0af392d60e Make buttons act more like vanilla 2025-02-16 01:04:15 -05:00
ang-xd e910805e50 Delete log 2025-02-15 23:54:47 -05:00
ang-xd 2b1db0ee24 Whoops 2025-02-15 17:59:40 -05:00
ang-xd 2bb1d41e65 Fix issue #24 2025-02-15 13:49:38 -05:00
Pietrodjaowjao 7bf35fc8ae Fix player skin resetting on no-teleport murder 2025-02-15 15:49:09 -03:00
16 changed files with 610 additions and 171 deletions

No files matched your search

+11 -4
View File
@@ -5,19 +5,26 @@ namespace MiraAPI.Example.Roles;
public class NeutralKillerRole : ImpostorRole, ICustomRole
{
public string RoleName => "Neutral Killer";
public string RoleDescription => "Neutral who can kill.";
public string RoleName => "Outcast Killer";
public string RoleDescription => "Outcast who can kill.";
public string RoleLongDescription => RoleDescription;
public Color RoleColor => Color.magenta;
public ModdedRoleTeams Team => ModdedRoleTeams.Neutral;
public ModdedRoleTeams Team => ModdedRoleTeams.Custom;
public CustomRoleConfiguration Configuration => new CustomRoleConfiguration(this)
public CustomRoleConfiguration Configuration => new(this)
{
UseVanillaKillButton = true,
CanGetKilled = true,
CanUseVent = true,
};
public RoleOptionsGroup RoleOptionsGroup { get; } = new("Outcast", Color.gray);
public TeamIntroConfiguration? IntroConfiguration { get; } = new(
Color.gray,
"NEUTRAL",
"You are an Outcast. You do not have a team.");
public override void SpawnTaskHeader(PlayerControl playerControl)
{
// remove existing task header.
+53 -12
View File
@@ -65,6 +65,11 @@ public abstract class CustomActionButton
/// </summary>
public bool EffectActive { get; protected set; }
/// <summary>
/// Gets or sets a value indicating whether the timer is currently active.
/// </summary>
public bool TimerPaused { get; protected set; }
/// <summary>
/// Gets or sets the amount of uses left.
/// </summary>
@@ -94,6 +99,7 @@ public abstract class CustomActionButton
UsesLeft = MaxUses;
Timer = AmongUsClient.Instance?.NetworkMode == NetworkModes.FreePlay ? 0 : InitialCooldown;
EffectActive = false;
TimerPaused = false;
Button = Object.Instantiate(HudManager.Instance.AbilityButton, parent);
Button.name = Name + "Button";
@@ -248,6 +254,15 @@ public abstract class CustomActionButton
SetTimer(Timer - amount);
}
/// <summary>
/// Sets whether the timer is paused or not.
/// </summary>
/// <param name="val">Whether you want to pause/resume the timer.</param>
public virtual void SetTimerPaused(bool val)
{
TimerPaused = val;
}
/// <summary>
/// Set the amount of uses this button has left.
/// </summary>
@@ -256,6 +271,11 @@ public abstract class CustomActionButton
{
UsesLeft = Mathf.Clamp(amount, 0, int.MaxValue);
Button?.SetUsesRemaining(UsesLeft);
if (Button != null)
{
Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
}
}
/// <summary>
@@ -306,14 +326,24 @@ public abstract class CustomActionButton
}
/// <summary>
/// When the button is enabled, this method is called to determine if the button can be used.
/// When the button is usable, this method is called to determine if the button can be clicked.
/// By default, it takes into account the timer, effect, and uses.
/// You can override it to change the behavior.
/// </summary>
/// <returns>A value that represents whether the button can be used or not.</returns>
/// <returns>A value that represents whether the button can be clicked or not.</returns>
public virtual bool CanClick()
{
return Timer <= 0 && !EffectActive && CanUse();
}
/// <summary>
/// Whether the button should light up or not. This is also the base for CanClick.
/// You can override it to change the behaviour. Do not include timer in here, that is for CanClick.
/// </summary>
/// <returns>A value that represents whether the button should light up or not.</returns>
public virtual bool CanUse()
{
return Timer <= 0 && !EffectActive && (!LimitedUses || UsesLeft > 0);
return !LimitedUses || UsesLeft > 0;
}
/// <summary>
@@ -335,7 +365,7 @@ public abstract class CustomActionButton
/// </summary>
public virtual void ClickHandler()
{
if (!CanUse())
if (!CanClick())
{
return;
}
@@ -344,15 +374,10 @@ public abstract class CustomActionButton
{
UsesLeft--;
Button?.SetUsesRemaining(UsesLeft);
if (Button != null)
{
Button.usesRemainingSprite.color = UsesLeft == 0 ? Color.red : Color.white;
}
}
OnClick();
Button?.SetDisabled();
if (HasEffect)
{
EffectActive = true;
@@ -372,7 +397,7 @@ public abstract class CustomActionButton
/// <param name="playerControl">The local PlayerControl.</param>
public virtual void FixedUpdateHandler(PlayerControl playerControl)
{
if (Timer >= 0)
if (Timer >= 0 && !TimerPaused)
{
Timer -= Time.deltaTime;
}
@@ -392,7 +417,17 @@ public abstract class CustomActionButton
Button?.SetDisabled();
}
Button?.SetCoolDown(Timer, EffectActive ? EffectDuration : Cooldown);
if (EffectActive)
{
Button?.SetFillUp(Timer, EffectDuration);
Button!.cooldownTimerText.text = Mathf.CeilToInt(Timer).ToString();
Button!.cooldownTimerText.gameObject.SetActive(true);
}
else
{
Button?.SetCoolDown(Timer, Cooldown);
}
FixedUpdate(playerControl);
}
@@ -451,6 +486,12 @@ public abstract class CustomActionButton<T> : CustomActionButton where T : MonoB
return base.CanUse() && Target != null;
}
/// <inheritdoc />
public override bool CanClick()
{
return base.CanClick() && Target != null;
}
/// <summary>
/// Use this to reset the button's target after used.
/// </summary>
+11 -5
View File
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AmongUs.GameOptions;
using AmongUs.GameOptions;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.Patches.Stubs;
using Reactor.Utilities.Attributes;
using Reactor.Utilities.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
@@ -86,10 +86,16 @@ public class CustomPlayerMenu(IntPtr il2CppPtr) : Minigame(il2CppPtr)
/// <param name="playerMatch">Function to determine if player should show in the custom menu.</param>
/// <param name="onClick">Onclick action for player.</param>
[HideFromIl2Cpp]
public void Begin(Func<PlayerControl, bool> playerMatch, Action<PlayerControl> onClick)
public void Begin(Func<PlayerControl, bool> playerMatch, Action<PlayerControl?> onClick)
{
MinigameStubs.Begin(this, null);
var back = BackButton.GetComponent<PassiveButton>();
back.OnClick.AddListener((UnityAction)(() =>
{
onClick(null);
}));
DestroyableSingleton<DebugAnalytics>.Instance.Analytics.MinigameOpened(PlayerControl.LocalPlayer.Data, TaskType);
var list = PlayerControl.AllPlayerControls.ToArray().Where(playerMatch).ToList();
potentialVictims = [];
+2 -5
View File
@@ -5,7 +5,6 @@ using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Gameplay;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System.Collections;
using System.Linq;
@@ -116,7 +115,6 @@ public static class CustomMurderRpc
target.RemoveProtection();
}
Logger<MiraApiPlugin>.Error($"{source.PlayerId} failed to murder {target.PlayerId} due to guardian angel protection");
return;
}
@@ -184,7 +182,6 @@ public static class CustomMurderRpc
source.shapeshiftTargetPlayerId,
target.PlayerId);
source.MyPhysics.StartCoroutine(source.KillAnimations.Random()?.CoPerformCustomKill(source, target, createDeadBody, teleportMurderer));
Logger<MiraApiPlugin>.Error($"{source.PlayerId} succeeded in murdering {target.PlayerId}");
}
/// <summary>
@@ -250,11 +247,11 @@ public static class CustomMurderRpc
}
target.Die(DeathReason.Kill, true);
yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim);
sourcePhys.Animations.PlayIdleAnimation();
if (teleportMurderer)
{
yield return source.MyPhysics.Animations.CoPlayCustomAnimation(anim.BlurAnim);
sourcePhys.Animations.PlayIdleAnimation();
source.NetTransform.SnapTo(target.transform.position);
KillAnimation.SetMovement(source, true);
}
+27 -5
View File
@@ -30,6 +30,24 @@ public static class IntroCutscenePatches
MiraEventManager.InvokeEvent(@event);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(IntroCutscene.BeginImpostor))]
public static void BeginImpostorPatch(IntroCutscene __instance)
{
if (PlayerControl.LocalPlayer.Data.Role is not ICustomRole customRole)
{
return;
}
if (customRole.IntroConfiguration is { } introConfig)
{
__instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor);
__instance.TeamTitle.color = introConfig.IntroTeamColor;
__instance.TeamTitle.text = introConfig.IntroTeamTitle;
__instance.ImpostorText.text = introConfig.IntroTeamDescription;
}
}
[HarmonyPrefix]
[HarmonyPatch(nameof(IntroCutscene.BeginCrewmate))]
public static bool BeginCrewmatePatch(IntroCutscene __instance)
@@ -39,7 +57,15 @@ public static class IntroCutscenePatches
return true;
}
if (customRole.Team is not ModdedRoleTeams.Neutral)
if (customRole.IntroConfiguration is { } introConfig)
{
__instance.BackgroundBar.material.SetColor(ShaderID.Color, introConfig.IntroTeamColor);
__instance.TeamTitle.color = introConfig.IntroTeamColor;
__instance.TeamTitle.text = introConfig.IntroTeamTitle;
__instance.ImpostorText.text = introConfig.IntroTeamDescription;
}
if (customRole.Team is not ModdedRoleTeams.Custom)
{
return true;
}
@@ -49,11 +75,7 @@ public static class IntroCutscenePatches
position.y -= 0.25f;
barTransform.position = position;
__instance.BackgroundBar.material.SetColor(ShaderID.Color, Color.gray);
__instance.TeamTitle.text = "NEUTRAL";
__instance.impostorScale = 1f;
__instance.ImpostorText.text = "You are Neutral. You do not have a team.";
__instance.TeamTitle.color = Color.gray;
__instance.ourCrewmate = __instance.CreatePlayer(
0,
@@ -1,14 +1,16 @@
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Networking;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using Reactor.Utilities;
using Reactor.Utilities.Extensions;
using System;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
@@ -20,6 +22,10 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(RolesSettingsMenu))]
public static class RoleSettingMenuPatches
{
private static Dictionary<RoleOptionsGroup, bool> RoleGroupHidden { get; } = [];
private static List<CategoryHeaderEditRole> CategoryHeaderEditRoles { get; } = [];
private static List<RoleOptionSetting> RoleOptionSettings { get; } = [];
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool PatchStart(RolesSettingsMenu __instance)
@@ -54,89 +60,107 @@ public static class RoleSettingMenuPatches
var num3 = 0;
var crewRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role is { Team: ModdedRoleTeams.Crewmate, Configuration.HideSettings: false })
.ToList() ?? [];
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (crewRoles is { Count: > 0 })
if (roleGroups is null)
{
return true;
}
// sort the groups by priority
var sortedRoleGroups = roleGroups
.OrderBy(x => x.Key.Priority)
.ThenBy(x => x.Key.Name);
foreach (var grouping in sortedRoleGroups)
{
if (!grouping.Any())
{
continue;
}
RoleGroupHidden.TryAdd(grouping.Key, false);
var name = grouping.Key.Name switch
{
"Crewmate" => StringNames.CrewmateRolesHeader,
"Impostor" => StringNames.ImpostorRolesHeader,
_ => CustomStringName.CreateAndRegister(grouping.Key.Name),
};
var categoryHeaderEditRole = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20);
categoryHeaderEditRole.SetHeader(name, 20);
categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f);
categoryHeaderEditRole.transform.Find("LabelSprite").transform.localScale = new Vector3(1.3f, 1, 0.5529f);
categoryHeaderEditRole.transform.Find("QuotaHeader").gameObject.SetActive(!RoleGroupHidden[grouping.Key]);
CategoryHeaderEditRoles.Add(categoryHeaderEditRole);
num -= 0.522f;
foreach (var role in crewRoles)
var label = RoleGroupHidden[grouping.Key]
? "(Click to open)"
: "(Click to close)";
var newText = Object.Instantiate(categoryHeaderEditRole.Title, categoryHeaderEditRole.transform);
newText.text = $"<size=50%>{label}</size>";
newText.transform.localPosition = new Vector3(-3.3425f, -0.1706f, -1);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
if (!RoleGroupHidden[grouping.Key])
{
if (role is RoleBehaviour roleBehaviour)
foreach (var role in grouping)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
if (role is not RoleBehaviour roleBehaviour)
{
continue;
}
var option = CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
if (option is not null)
{
RoleOptionSettings.Add(option);
num3++;
}
}
num3++;
}
var boxCol = categoryHeaderEditRole.gameObject.AddComponent<BoxCollider2D>();
boxCol.size = new Vector2(3, 0.25f);
boxCol.offset = new Vector2(-2.05f, -.175f);
var headerBtn = categoryHeaderEditRole.gameObject.AddComponent<PassiveButton>();
headerBtn.ClickSound = __instance.BackButton.GetComponent<PassiveButton>().ClickSound;
headerBtn.OnMouseOver = new UnityEvent();
headerBtn.OnMouseOut = new UnityEvent();
headerBtn.OnClick.AddListener(
(UnityAction)(() =>
{
RoleGroupHidden[grouping.Key] = !RoleGroupHidden[grouping.Key];
foreach (var header in CategoryHeaderEditRoles)
{
header.gameObject.Destroy();
}
CategoryHeaderEditRoles.Clear();
foreach (var option in RoleOptionSettings)
{
option.gameObject.Destroy();
}
RoleOptionSettings.Clear();
__instance.SetQuotaTab();
}));
headerBtn.SetButtonEnableState(true);
if (!RoleGroupHidden[grouping.Key])
{
num -= 0.4f;
}
}
var impRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role is { Team: ModdedRoleTeams.Impostor, Configuration.HideSettings: false })
.ToList() ?? [];
if (impRoles is { Count: > 0 })
{
num -= 0.4f;
var categoryHeaderEditRole2 = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole2.SetHeader(StringNames.ImpostorRolesHeader, 20);
categoryHeaderEditRole2.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var role in impRoles)
{
if (role is RoleBehaviour roleBehaviour)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
}
num3++;
}
}
var neutRoles = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values
.OfType<ICustomRole>()
.Where(role => role is { Team: ModdedRoleTeams.Neutral, Configuration.HideSettings: false })
.ToList() ?? [];
if (neutRoles is { Count: > 0 })
{
num -= 0.4f;
var categoryHeaderEditRole3 = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole3.SetHeader(StringNames.None, 20);
categoryHeaderEditRole3.Title.text = "Neutral Roles";
categoryHeaderEditRole3.Background.color = Color.gray;
categoryHeaderEditRole3.Title.color = Color.white;
categoryHeaderEditRole3.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var role in neutRoles)
{
if (role is RoleBehaviour roleBehaviour)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
}
num3++;
}
}
__instance.scrollBar.CalculateAndSetYBounds(__instance.roleChances.Count + 5, 1f, 6f, 0.43f);
return false;
}
@@ -295,12 +319,12 @@ public static class RoleSettingMenuPatches
__instance.RefreshChildren();
}
private static void CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, ref float yPos, int index)
private static RoleOptionSetting? CreateQuotaOption(RolesSettingsMenu __instance, RoleBehaviour role, ref float yPos, int index)
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
return;
return null;
}
var roleOptionSetting = Object.Instantiate(
@@ -357,5 +381,7 @@ public static class RoleSettingMenuPatches
{
yPos += -0.43f;
}
return roleOptionSetting;
}
}
@@ -0,0 +1,25 @@
using HarmonyLib;
using MiraAPI.Roles;
using UnityEngine;
namespace MiraAPI.Patches.Roles;
[HarmonyPatch(typeof(RoleBehaviour))]
public static class RoleBehaviourPatch
{
/// <summary>
/// Update TeamColor text for Mira roles.
/// </summary>
[HarmonyPrefix]
[HarmonyPatch(nameof(RoleBehaviour.TeamColor), MethodType.Getter)]
public static bool PrefixTeamColorGetter(RoleBehaviour __instance, ref Color __result)
{
if (__instance is not ICustomRole behaviour)
{
return true;
}
__result = behaviour.RoleColor;
return false;
}
}
+30 -22
View File
@@ -1,10 +1,10 @@
using AmongUs.GameOptions;
using HarmonyLib;
using System;
using System.Collections.Generic;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities.Assets;
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
@@ -21,6 +21,10 @@ public static class TaskAdderPatch
private static readonly Dictionary<string, string> ModsFolders = [];
private static Scroller? _scroller;
/// <summary>
/// Adds custom roles to the TaskAdder.
/// </summary>
/// <param name="__instance">TaskAdder instance.</param>
[HarmonyPostfix]
[HarmonyPatch(typeof(TaskAdderGame), nameof(TaskAdderGame.Begin))]
public static void AddRolesFolder(TaskAdderGame __instance)
@@ -86,13 +90,17 @@ public static class TaskAdderPatch
__instance.GoToRoot();
}
/// <summary>
/// Sets the color of the role button based on the role.
/// </summary>
/// <param name="__instance">TaskAddButton instance.</param>
[HarmonyPostfix]
[HarmonyPatch(typeof(TaskAddButton), "Role", MethodType.Setter)]
[HarmonyPatch(typeof(TaskAddButton), nameof(TaskAddButton.Role), MethodType.Setter)]
public static void RoleGetterPatch(TaskAddButton __instance)
{
if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Neutral })
if (__instance.role is ICustomRole { Team: ModdedRoleTeams.Custom } customRole)
{
__instance.FileImage.color = Color.gray;
__instance.FileImage.color = customRole.IntroConfiguration?.IntroTeamColor ?? Color.gray;
}
__instance.RolloverHandler.OutColor = __instance.FileImage.color;
@@ -186,23 +194,23 @@ public static class TaskAdderPatch
switch (taskAddButton.MyTask.TaskType)
{
case TaskTypes.DivertPower:
{
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = TranslationController.Instance.GetString(
StringNames.DivertPowerTo,
TranslationController.Instance.GetString(targetSystem));
break;
}
{
var targetSystem = taskAddButton.MyTask.Cast<DivertPowerTask>().TargetSystem;
taskAddButton.Text.text = TranslationController.Instance.GetString(
StringNames.DivertPowerTo,
TranslationController.Instance.GetString(targetSystem));
break;
}
case TaskTypes.FixWeatherNode:
{
var nodeId = taskAddButton.MyTask.Cast<WeatherNodeTask>().NodeId;
taskAddButton.Text.text =
TranslationController.Instance.GetString(
StringNames.FixWeatherNode) + " " +
TranslationController.Instance.GetString(
WeatherSwitchGame.ControlNames[nodeId]);
break;
}
{
var nodeId = taskAddButton.MyTask.Cast<WeatherNodeTask>().NodeId;
taskAddButton.Text.text =
TranslationController.Instance.GetString(
StringNames.FixWeatherNode) + " " +
TranslationController.Instance.GetString(
WeatherSwitchGame.ControlNames[nodeId]);
break;
}
default:
taskAddButton.Text.text =
TranslationController.Instance.GetString(taskAddButton.MyTask.TaskType);
@@ -276,7 +284,7 @@ public static class TaskAdderPatch
{
var roleBehaviour = plugin.CustomRoles.ElementAt(m).Value;
if (roleBehaviour.Role == RoleTypes.ImpostorGhost || roleBehaviour.Role == RoleTypes.CrewmateGhost ||
roleBehaviour.IsDead)
(roleBehaviour is ICustomRole customRole && !customRole.Configuration.ShowInFreeplay))
{
continue;
}
+12
View File
@@ -21,4 +21,16 @@ public static class MinigameStubs
{
// nothing needed
}
/// <summary>
/// Reverse patch for Minigame.Close.
/// </summary>
/// <param name="instance">The Minigame instance.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(Minigame), nameof(Minigame.Close), [])]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Close(Minigame instance)
{
// nothing needed
}
}
+228
View File
@@ -0,0 +1,228 @@
using System.Runtime.CompilerServices;
using HarmonyLib;
using Il2CppSystem.Text;
namespace MiraAPI.Patches.Stubs;
[HarmonyPatch]
public static class RoleBehaviourStubs
{
/// <summary>
/// Stub method for RoleBehaviour.Initialize.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.Initialize))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Initialize(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.AdjustTasks.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="player">The PlayerControl to adjust tasks for.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AdjustTasks))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AdjustTasks(RoleBehaviour instance, PlayerControl player)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.AppendTaskHint.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="taskStringBuilder">The StringBuilder to append the task hint to.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.AppendTaskHint))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void AppendTaskHint(RoleBehaviour instance, StringBuilder taskStringBuilder)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.CanUse.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="console">The IUsable console to check.</param>
/// <returns>Whether the console can be used.</returns>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.CanUse))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool CanUse(RoleBehaviour instance, IUsable console)
{
return default;
}
/// <summary>
/// Stub method for RoleBehaviour.Deinitialize.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="targetPlayer">The PlayerControl to deinitialize for.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.Deinitialize))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Deinitialize(RoleBehaviour instance, PlayerControl targetPlayer)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.DidWin.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="gameOverReason">The reason for game over.</param>
/// <returns>Whether the role won.</returns>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.DidWin))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool DidWin(RoleBehaviour instance, GameOverReason gameOverReason)
{
return default;
}
/// <summary>
/// Stub method for RoleBehaviour.FindClosestTarget.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <returns>The closest target PlayerControl.</returns>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.FindClosestTarget))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static PlayerControl FindClosestTarget(RoleBehaviour instance)
{
return null;
}
/// <summary>
/// Stub method for RoleBehaviour.GetAbilityDistance.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <returns>The ability distance.</returns>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.GetAbilityDistance))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static float GetAbilityDistance(RoleBehaviour instance)
{
return default;
}
/// <summary>
/// Stub method for RoleBehaviour.IsValidTarget.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="target">The NetworkedPlayerInfo to validate.</param>
/// <returns>Whether the target is valid.</returns>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.IsValidTarget))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsValidTarget(RoleBehaviour instance, NetworkedPlayerInfo target)
{
return default;
}
/// <summary>
/// Stub method for RoleBehaviour.OnDeath.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="reason">The reason for death.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnDeath))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void OnDeath(RoleBehaviour instance, DeathReason reason)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.OnMeetingStart.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnMeetingStart))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void OnMeetingStart(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.OnVotingComplete.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.OnVotingComplete))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void OnVotingComplete(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.SetCooldown.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetCooldown))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void SetCooldown(RoleBehaviour instance)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.SetPlayerTarget.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="target">The PlayerControl target to set.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetPlayerTarget))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void SetPlayerTarget(RoleBehaviour instance, PlayerControl target)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.SetUsableTarget.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="target">The IUsable target to set.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SetUsableTarget))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void SetUsableTarget(RoleBehaviour instance, IUsable target)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.SpawnTaskHeader.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
/// <param name="playerControl">The PlayerControl to spawn the task header for.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.SpawnTaskHeader))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void SpawnTaskHeader(RoleBehaviour instance, PlayerControl playerControl)
{
// nothing needed
}
/// <summary>
/// Stub method for RoleBehaviour.UseAbility.
/// </summary>
/// <param name="instance">The RoleBehaviour object.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.UseAbility))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void UseAbility(RoleBehaviour instance)
{
// nothing needed
}
}
+40 -41
View File
@@ -7,7 +7,7 @@ namespace MiraAPI.Roles;
/// <summary>
/// Used to configure the specific settings of a role.
/// </summary>
public struct CustomRoleConfiguration
public record struct CustomRoleConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomRoleConfiguration"/> struct.
@@ -17,11 +17,6 @@ public struct CustomRoleConfiguration
{
var roleBehaviour = role as RoleBehaviour;
MaxRoleCount = 15;
DefaultRoleCount = 0;
DefaultChance = 0;
OptionsScreenshot = Icon = MiraAssets.Empty;
AffectedByLightOnAirship = role.Team == ModdedRoleTeams.Crewmate;
KillButtonOutlineColor = role.Team switch
{
@@ -29,7 +24,6 @@ public struct CustomRoleConfiguration
ModdedRoleTeams.Crewmate => Palette.CrewmateBlue,
_ => role.RoleColor,
};
RoleHintType = RoleHintType.RoleTab;
GhostRole = role.Team is ModdedRoleTeams.Impostor ? RoleTypes.ImpostorGhost : RoleTypes.CrewmateGhost;
CanGetKilled = roleBehaviour?.IsDead == false && role.Team is not ModdedRoleTeams.Impostor;
UseVanillaKillButton = role.Team is ModdedRoleTeams.Impostor;
@@ -37,86 +31,91 @@ public struct CustomRoleConfiguration
CanUseSabotage = role.Team is ModdedRoleTeams.Impostor;
TasksCountForProgress = role.Team is ModdedRoleTeams.Crewmate;
HideSettings = roleBehaviour?.IsDead == true;
CanModifyChance = true;
ShowInFreeplay = roleBehaviour?.IsDead == false;
}
/// <summary>
/// Gets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start.
/// Gets or sets the hard limit of players that can have this role. This property is used to set a limit in the Role Options menu. If set to 0, the role will not be assigned at start.
/// </summary>
public int MaxRoleCount;
public int MaxRoleCount { get; set; } = 15;
/// <summary>
/// Gets the default role count.
/// Gets or sets the default role count.
/// </summary>
public int DefaultRoleCount;
public int DefaultRoleCount { get; set; } = 0;
/// <summary>
/// Gets the default role chance.
/// Gets or sets the default role chance.
/// </summary>
public int DefaultChance;
public int DefaultChance { get; set; } = 0;
/// <summary>
/// Whether the chance option can be changed or not.
/// Gets or sets a value indicating whether the chance option can be changed or not.
/// </summary>
public bool CanModifyChance;
public bool CanModifyChance { get; set; } = true;
/// <summary>
/// Gets the Sprite used for the Role Options menu screenshot.
/// Gets or sets the Sprite used for the Role Options menu screenshot.
/// </summary>
public LoadableAsset<Sprite> OptionsScreenshot;
public LoadableAsset<Sprite> OptionsScreenshot { get; set; } = MiraAssets.Empty;
/// <summary>
/// Gets the Sprite used for the Role Icon.
/// Gets or sets the Sprite used for the Role Icon.
/// </summary>
public LoadableAsset<Sprite> Icon;
public LoadableAsset<Sprite> Icon { get; set; } = MiraAssets.Empty;
/// <summary>
/// Gets a value indicating whether the role is affected by light affectors on Airship.
/// Gets or sets a value indicating whether the role is affected by light affectors on Airship.
/// </summary>
public bool AffectedByLightOnAirship;
public bool AffectedByLightOnAirship { get; set; }
/// <summary>
/// Gets a value indicating whether the role can be killed by vanilla murder system.
/// Gets or sets a value indicating whether the role can be killed by vanilla murder system.
/// </summary>
public bool CanGetKilled;
public bool CanGetKilled { get; set; }
/// <summary>
/// Gets a value indicating whether the role should use the vanilla kill button.
/// Gets or sets a value indicating whether the role should use the vanilla kill button.
/// </summary>
public bool UseVanillaKillButton;
public bool UseVanillaKillButton { get; set; }
/// <summary>
/// Gets a value indicating whether the role can use vents.
/// Gets or sets a value indicating whether the role can use vents.
/// </summary>
public bool CanUseVent;
public bool CanUseVent { get; set; }
/// <summary>
/// Gets a value indicating whether the role can use the sabotage button.
/// Gets or sets a value indicating whether the role can use the sabotage button.
/// </summary>
public bool CanUseSabotage;
public bool CanUseSabotage { get; set; }
/// <summary>
/// Gets a value indicating whether the role's tasks count towards task progress.
/// Gets or sets a value indicating whether the role's tasks count towards task progress.
/// </summary>
public bool TasksCountForProgress;
public bool TasksCountForProgress { get; set; }
/// <summary>
/// Gets a value indicating whether the role should show up in the Role Options menu.
/// Gets or sets a value indicating whether the role should show up in the Role Options menu.
/// </summary>
public bool HideSettings;
public bool HideSettings { get; set; }
/// <summary>
/// Gets the outline color for the KillButton if <see cref="UseVanillaKillButton"/> is true.
/// Gets or sets a value indicating whether the role should show up in the Freeplay Role Selection menu.
/// </summary>
public Color KillButtonOutlineColor;
public bool ShowInFreeplay { get; set; }
/// <summary>
/// Gets the role hint style. See <see cref="RoleHintType"/> enum for all options.
/// Gets or sets the outline color for the KillButton if <see cref="UseVanillaKillButton"/> is true.
/// </summary>
public RoleHintType RoleHintType;
public Color KillButtonOutlineColor { get; set; }
/// <summary>
/// Gets the Ghost role that is applied when the player is killed.
/// Gets or sets the role hint style. See <see cref="RoleHintType"/> enum for all options.
/// </summary>
public RoleTypes GhostRole;
}
public RoleHintType RoleHintType { get; set; } = RoleHintType.RoleTab;
/// <summary>
/// Gets or sets the Ghost role that is applied when the player is killed.
/// </summary>
public RoleTypes GhostRole { get; set; }
}
+1 -1
View File
@@ -79,7 +79,7 @@ public static class CustomRoleManager
var roleId = RoleIds[roleType];
roleBehaviour.Role = (RoleTypes)roleId;
roleBehaviour.TeamType = customRole.Team == ModdedRoleTeams.Neutral ? RoleTeamTypes.Crewmate : (RoleTeamTypes)customRole.Team;
roleBehaviour.TeamType = customRole.Team == ModdedRoleTeams.Custom ? RoleTeamTypes.Crewmate : (RoleTeamTypes)customRole.Team;
roleBehaviour.NameColor = customRole.RoleColor;
roleBehaviour.StringName = CustomStringName.CreateAndRegister(customRole.RoleName);
roleBehaviour.BlurbName = CustomStringName.CreateAndRegister(customRole.RoleDescription);
+23 -2
View File
@@ -1,4 +1,5 @@
using BepInEx.Configuration;
using System;
using BepInEx.Configuration;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
@@ -43,6 +44,26 @@ public interface ICustomRole
/// </summary>
CustomRoleConfiguration Configuration { get; }
/// <summary>
/// Gets the role options group.
/// </summary>
public RoleOptionsGroup RoleOptionsGroup => Team switch
{
ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate,
ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor,
ModdedRoleTeams.Custom => RoleOptionsGroup.Neutral,
_ => new RoleOptionsGroup(RoleName, RoleColor),
};
/// <summary>
/// Gets the role's TeamIntroCutscene configuration.
/// </summary>
public TeamIntroConfiguration? IntroConfiguration => Team switch
{
ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral,
_ => null,
};
/// <summary>
/// Gets the parent mod of this role.
/// </summary>
@@ -135,7 +156,7 @@ public interface ICustomRole
/// <returns>Whether they can see the role (name color) or not.</returns>
public virtual bool CanLocalPlayerSeeRole(PlayerControl player)
{
return (Team == ModdedRoleTeams.Impostor && player.Data.Role.IsImpostor) || PlayerControl.LocalPlayer.Data.IsDead;
return (PlayerControl.LocalPlayer.Data.Role.IsImpostor && player.Data.Role.IsImpostor) || PlayerControl.LocalPlayer.Data.IsDead;
}
/// <summary>
+1 -1
View File
@@ -4,5 +4,5 @@ public enum ModdedRoleTeams
{
Crewmate,
Impostor,
Neutral,
Custom,
}
+27
View File
@@ -0,0 +1,27 @@
using UnityEngine;
namespace MiraAPI.Roles;
/// <summary>
/// Represents a group of roles.
/// </summary>
/// <param name="Name">The group's name.</param>
/// <param name="Color">The group's color.</param>
/// <param name="Priority">The priority to sort by on role setting screen. Negative means first.</param>
public record struct RoleOptionsGroup(string Name, Color Color, int Priority = 0)
{
/// <summary>
/// Gets the default group for crewmates.
/// </summary>
public static RoleOptionsGroup Crewmate { get; } = new("Crewmate", Palette.CrewmateBlue, -2);
/// <summary>
/// Gets the default group for impostors.
/// </summary>
public static RoleOptionsGroup Impostor { get; } = new("Impostor", Palette.ImpostorRed, -1);
/// <summary>
/// Gets the default group for neutral roles.
/// </summary>
public static RoleOptionsGroup Neutral { get; } = new("Neutral Roles", Color.gray);
}
+20
View File
@@ -0,0 +1,20 @@
using UnityEngine;
namespace MiraAPI.Roles;
/// <summary>
/// Used to configure the TEAM part of the intro cutscene.
/// </summary>
/// <param name="IntroTeamColor">The color to show during the "Team" part of the intro cutscene.</param>
/// <param name="IntroTeamTitle">The title text to show during the "Team" part of the intro cutscene.</param>
/// <param name="IntroTeamDescription">The description text to show during the "Team" part of the intro cutscene.</param>
public record struct TeamIntroConfiguration(Color IntroTeamColor, string IntroTeamTitle, string IntroTeamDescription)
{
/// <summary>
/// Gets the default configuration for the "Neutral" team.
/// </summary>
public static TeamIntroConfiguration Neutral { get; } = new(
Color.gray,
"NEUTRAL",
"You are Neutral. You do not have a team.");
}