Compare commits

...
Author SHA1 Message Date
XtraCube 9504862bea add format string option to number options 2025-02-21 12:01:28 -05:00
XtraCube 4b6ffc65df fix after murder event 2025-02-21 09:26:53 -05:00
XtraCube ccde63068b fix lobby view pane issues 2025-02-18 21:49:54 -05:00
XtraCube 5e16429acf fix more scrolling issues 2025-02-18 11:15:13 -05:00
XtraCube 1e47500686 fix scrolling issues 2025-02-18 11:03:50 -05:00
XtraCube 0b1cf998f8 fix coloring issues and hidden roles in rolesettingsmenu 2025-02-17 21:59:58 -05:00
XtraCube 14a3669031 fix rolebehaviour stubs cuz im an idiot 2025-02-17 11:52:57 -05:00
XtraCube ae79cd87d9 :shush: 2025-02-17 11:37:02 -05:00
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
19 changed files with 620 additions and 192 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ public class ExampleOptions : AbstractOptionGroup
[ModdedToggleOption("Toggle Opt 2")]
public bool ToggleOpt2 { get; set; } = true;
[ModdedNumberOption("Number Opt", min: 0, max: 10, suffixType: MiraNumberSuffixes.Percent)]
[ModdedNumberOption("Number Opt", min: 0, max: 10, increment: .25f, formatString: "0.00", suffixType: MiraNumberSuffixes.Percent)]
public float NumberOpt { get; set; } = 4f;
[ModdedEnumOption("Best API", typeof(BestApi), ["Mira API", "Mitochondria", "Reactor"])]
+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.
@@ -15,13 +15,14 @@ public class ModdedNumberOptionAttribute(
float max,
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false,
Type? roleType = null)
: ModdedOptionAttribute(title, roleType)
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, zeroInfinity, RoleType);
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType);
}
/// <inheritdoc />
@@ -46,6 +46,7 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="max">The maximum value.</param>
/// <param name="increment">The increment.</param>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="roleType">An optional role type.</param>
public ModdedNumberOption(
@@ -55,6 +56,7 @@ public class ModdedNumberOption : ModdedOption<float>
float max,
float increment,
MiraNumberSuffixes suffixType,
string? formatString = null,
bool zeroInfinity = false,
Type? roleType = null) : base(title, defaultValue, roleType)
{
@@ -74,7 +76,13 @@ public class ModdedNumberOption : ModdedOption<float>
data.Value = Value;
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
data.FormatString = Increment % 1 == 0 && Value % 1 == 0 && Min % 1 == 0 && Max % 1 == 0 ? "0" : "0.0"; // not sure if the == checks will work since this is floats but we'll find out
data.FormatString = formatString ??
(!Mathf.Approximately(defaultValue, 0) ||
!Mathf.Approximately(Increment % 1, 0) ||
!Mathf.Approximately(Value % 1, 0) ||
!Mathf.Approximately(Min % 1, 0) ||
!Mathf.Approximately(Max % 1, 0) ? "0.0" : "0");
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
data.OptionName = FloatOptionNames.Invalid;
+3 -3
View File
@@ -263,6 +263,9 @@ public static class CustomMurderRpc
deadBody.enabled = true;
}
var afterMurderEvent = new AfterMurderEvent(source, target);
MiraEventManager.InvokeEvent(afterMurderEvent);
if (!isParticipant)
{
yield break;
@@ -275,8 +278,5 @@ public static class CustomMurderRpc
PlayerControl.LocalPlayer.isKilling = false;
source.isKilling = false;
var afterMurderEvent = new AfterMurderEvent(source, target);
MiraEventManager.InvokeEvent(afterMurderEvent);
}
}
@@ -4,17 +4,19 @@ using UnityEngine;
namespace MiraAPI.Patches.Colors;
[HarmonyPatch(typeof(PlayerTab))]
/// <summary>
/// This patch adds scrolling to the colors tab in the player customization menu.
/// </summary>
[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.OnEnable))]
public static class ScrollingColorsPatch
{
// Collider
private static BoxCollider2D? _collider;
/// <summary>
/// Add scrolling to the colors tab
/// Add scrolling to the colors tab.
/// </summary>
[HarmonyPostfix, HarmonyPatch("OnEnable")]
public static void AddScrollingToColorsTabPatch(PlayerTab __instance)
public static void Postfix(PlayerTab __instance)
{
if (!PlayerCustomizationMenu.Instance)
{
+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,
@@ -157,8 +157,6 @@ public static class GameSettingMenuPatches
roles.AdvancedRolesSettings.gameObject.SetActive(false);
roles.RoleChancesSettings.gameObject.SetActive(true);
roles.SetQuotaTab();
roles.scrollBar.CalculateAndSetYBounds(roles.roleChances.Count + 5, 1f, 6f, 0.43f);
roles.scrollBar.ScrollToTop();
}
+36 -25
View File
@@ -6,6 +6,7 @@ using MiraAPI.Utilities.Assets;
using System;
using System.Collections.Generic;
using System.Linq;
using Reactor.Localization.Utilities;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -211,34 +212,43 @@ public static class LobbyViewPanePatches
var list = new List<Type>();
foreach (var team in Enum.GetValues<ModdedRoleTeams>())
{
var filteredRoles = SelectedMod.CustomRoles.Values
.OfType<ICustomRole>()
.Where(x => !x.Configuration.HideSettings && x.Team == team).ToList();
var roleGroups = GameSettingMenuPatches.SelectedMod?.CustomRoles.Values.OfType<ICustomRole>()
.ToLookup(x => x.RoleOptionsGroup);
if (filteredRoles.Count == 0)
if (roleGroups is null)
{
return;
}
// 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;
}
var categoryHeaderRoleVariant =
Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true);
var group = grouping.Key;
switch (team)
var name = group.Name switch
{
case ModdedRoleTeams.Crewmate:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
break;
case ModdedRoleTeams.Impostor:
categoryHeaderRoleVariant.SetHeader(StringNames.ImpostorRolesHeader, 61);
break;
default:
categoryHeaderRoleVariant.SetHeader(StringNames.CrewmateRolesHeader, 61);
categoryHeaderRoleVariant.Title.text = team + " Roles";
categoryHeaderRoleVariant.Background.color = Color.gray;
categoryHeaderRoleVariant.Title.color = Color.white;
break;
"Crewmate" => StringNames.CrewmateRolesHeader,
"Impostor" => StringNames.ImpostorRolesHeader,
_ => CustomStringName.CreateAndRegister(group.Name),
};
var categoryHeaderRoleVariant = Object.Instantiate(instance.categoryHeaderRoleOrigin, instance.settingsContainer, true);
categoryHeaderRoleVariant.SetHeader(name, 61);
if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader))
{
var veryDarkColor = group.Color.DarkenColor(.35f);
categoryHeaderRoleVariant.Title.color = veryDarkColor;
categoryHeaderRoleVariant.Background.color = group.Color;
}
categoryHeaderRoleVariant.transform.localScale = Vector3.one;
@@ -246,7 +256,7 @@ public static class LobbyViewPanePatches
instance.settingsInfo.Add(categoryHeaderRoleVariant.gameObject);
num -= 0.696f;
foreach (var customRole in filteredRoles)
foreach (var customRole in grouping)
{
var roleBehaviour = customRole as RoleBehaviour;
if (roleBehaviour == null)
@@ -259,8 +269,6 @@ public static class LobbyViewPanePatches
var numPerGame =
GameOptionsManager.Instance.CurrentGameOptions.RoleOptions.GetNumPerGame(roleBehaviour.Role);
var flag = numPerGame == 0;
var viewSettingsInfoPanelRoleVariant =
Object.Instantiate(
instance.infoPanelRoleOrigin,
@@ -273,7 +281,7 @@ public static class LobbyViewPanePatches
.Where(x => x.AdvancedRole == customRole.GetType())
.ToList();
if (!flag && advancedRoleOptions.Count > 0)
if (numPerGame > 0 && advancedRoleOptions.Count > 0)
{
list.Add(customRole.GetType());
}
@@ -286,6 +294,8 @@ public static class LobbyViewPanePatches
customRole.RoleColor,
customRole.Configuration.Icon.LoadAsset(),
true);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localScale = new Vector3(0.365f, 0.365f, 1f);
viewSettingsInfoPanelRoleVariant.iconSprite.transform.localPosition = new Vector3(0.7144f, -0.028f, -2);
viewSettingsInfoPanelRoleVariant.titleText.color =
viewSettingsInfoPanelRoleVariant.chanceTitle.color =
@@ -370,6 +380,7 @@ public static class LobbyViewPanePatches
maskLayer,
role.TeamType == RoleTeamTypes.Crewmate,
customRole.Configuration.Icon.LoadAsset());
viewPanel.header.icon.transform.localScale = new Vector3(0.465f, 0.465f, 1f);
viewPanel.divider.material.SetInt(PlayerMaterial.MaskLayer, maskLayer);
var num = viewPanel.yPosStart;
+143 -80
View File
@@ -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,10 +22,20 @@ 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; } = [];
private static float ScrollerNum { get; set; } = 0.522f;
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.SetQuotaTab))]
public static bool PatchStart(RolesSettingsMenu __instance)
{
CategoryHeaderEditRoles.ForEach(Object.Destroy);
CategoryHeaderEditRoles.Clear();
RoleOptionSettings.ForEach(Object.Destroy);
RoleOptionSettings.Clear();
__instance.roleChances = new Il2CppSystem.Collections.Generic.List<RoleOptionSetting>();
__instance.advancedSettingChildren = new Il2CppSystem.Collections.Generic.List<OptionBehaviour>();
@@ -42,7 +54,7 @@ public static class RoleSettingMenuPatches
return true;
}
var num = 0.522f;
ScrollerNum = 0.522f;
__instance.AllButton.transform.parent.gameObject.SetActive(false);
__instance.AllButton.gameObject.SetActive(false);
@@ -54,92 +66,131 @@ 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;
}
var group = grouping.Key;
RoleGroupHidden.TryAdd(group, false);
var name = group.Name switch
{
"Crewmate" => StringNames.CrewmateRolesHeader,
"Impostor" => StringNames.ImpostorRolesHeader,
_ => CustomStringName.CreateAndRegister(group.Name),
};
var categoryHeaderEditRole = Object.Instantiate(
__instance.categoryHeaderEditRoleOrigin,
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
categoryHeaderEditRole.SetHeader(StringNames.CrewmateRolesHeader, 20);
categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, num, -2f);
num -= 0.522f;
foreach (var role in crewRoles)
categoryHeaderEditRole.SetHeader(name, 20);
if (name is not (StringNames.CrewmateRolesHeader or StringNames.ImpostorRolesHeader))
{
if (role is RoleBehaviour roleBehaviour)
var darkColor = group.Color.DarkenColor(.25f);
var veryDarkColor = group.Color.DarkenColor(.35f);
categoryHeaderEditRole.Title.color = veryDarkColor;
categoryHeaderEditRole.blankLabel.color = veryDarkColor;
categoryHeaderEditRole.Background.color = group.Color;
categoryHeaderEditRole.chanceLabel.color = darkColor;
categoryHeaderEditRole.countLabel.color = darkColor;
}
categoryHeaderEditRole.transform.localPosition = new Vector3(4.986f, ScrollerNum, -2f);
categoryHeaderEditRole.transform.Find("LabelSprite").transform.localScale = new Vector3(1.3f, 1, 0.5529f);
categoryHeaderEditRole.transform.Find("QuotaHeader").gameObject.SetActive(!RoleGroupHidden[group]);
CategoryHeaderEditRoles.Add(categoryHeaderEditRole);
ScrollerNum -= 0.522f;
var label = RoleGroupHidden[group]
? "(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[group])
{
foreach (var role in grouping)
{
CreateQuotaOption(__instance, roleBehaviour, ref num, num3);
if (role is not RoleBehaviour roleBehaviour)
{
continue;
}
var option = CreateQuotaOption(__instance, roleBehaviour, 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)(() =>
{
if (RoleGroupHidden.TryGetValue(group, out var value))
{
RoleGroupHidden[group] = !value;
}
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[group])
{
ScrollerNum -= 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.SetScrollBounds();
return false;
}
private static void SetScrollBounds(this Scroller scroller)
{
scroller.CalculateAndSetYBounds(1 + 1.5f * CategoryHeaderEditRoles.Count + RoleOptionSettings.Count, 1f, 6f, 0.43f);
}
[HarmonyPrefix]
[HarmonyPatch(nameof(RolesSettingsMenu.Update))]
public static bool UpdatePatch()
@@ -151,7 +202,12 @@ public static class RoleSettingMenuPatches
[HarmonyPatch(nameof(RolesSettingsMenu.OpenChancesTab))]
public static void OpenChancesTabPostfix(RolesSettingsMenu __instance)
{
__instance.scrollBar.CalculateAndSetYBounds(__instance.roleChances.Count + 5, 1f, 6f, 0.43f);
if (GameSettingMenuPatches.SelectedModIdx == 0)
{
return;
}
__instance.scrollBar.SetScrollBounds();
__instance.scrollBar.ScrollToTop();
}
@@ -295,12 +351,17 @@ 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, int index)
{
if (role is not ICustomRole customRole)
{
Logger<MiraApiPlugin>.Error($"Role {role.NiceName} is not a custom role.");
return;
return null;
}
if (customRole.Configuration.HideSettings)
{
return null;
}
var roleOptionSetting = Object.Instantiate(
@@ -308,7 +369,7 @@ public static class RoleSettingMenuPatches
Vector3.zero,
Quaternion.identity,
__instance.RoleChancesSettings.transform);
roleOptionSetting.transform.localPosition = new Vector3(-0.15f, yPos, -2f);
roleOptionSetting.transform.localPosition = new Vector3(-0.15f, ScrollerNum, -2f);
roleOptionSetting.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, role, 20);
roleOptionSetting.labelSprite.color = customRole.RoleColor;
@@ -355,7 +416,9 @@ public static class RoleSettingMenuPatches
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1)
{
yPos += -0.43f;
ScrollerNum += -0.43f;
}
return roleOptionSetting;
}
}
+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
}
}
+229
View File
@@ -0,0 +1,229 @@
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>
/// <param name="player">The PlayerControl to initialize.</param>
[HarmonyReversePatch]
[HarmonyPatch(typeof(RoleBehaviour), nameof(RoleBehaviour.Initialize))]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Initialize(RoleBehaviour instance, PlayerControl player)
{
// 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), typeof(StringBuilder))]
[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);
+22 -1
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>
+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.");
}