Compare commits

...
18 Commits
Author SHA1 Message Date
XtraCube 86f970c989 copy new meeting event from voting branch 2025-03-08 10:11:19 -05:00
XtraCube 7aafaf478c bump version 2025-03-08 10:10:55 -05:00
XtraCube 451a626370 Merge pull request #30 from All-Of-Us-Mods/optionable
Optionable Types
2025-03-04 18:50:20 -05:00
XtraCube e02c06ca07 final cleanup 2025-03-04 18:29:30 -05:00
XtraCube e43cd0ceff remove debug log 2025-03-04 17:00:16 -05:00
XtraCube e680b5c872 unity lifetime check 2025-03-04 17:00:05 -05:00
XtraCube b6f304fe1d organize example 2025-03-04 16:59:55 -05:00
XtraCube e52f48c20d allow any groups to be shown in modifiers menu 2025-03-03 19:28:20 -05:00
XtraCube d97588b356 add lobby view pane settings 2025-03-03 19:12:09 -05:00
XtraCube 68304a5eb8 simplify + partial unscuff 2025-03-03 17:10:14 -05:00
XtraCube f2add2a65b this is very broken idk why 2025-03-02 19:21:37 -05:00
XtraCube bbff2dd2d9 modifier options (WIP) 2025-03-01 18:34:18 -05:00
ang-xd d9081ed17b Outline text method 2025-03-01 15:30:14 -05:00
XtraCube 6f248221d6 remove extra log 2025-03-01 12:38:04 -05:00
XtraCube b67fdf5ab1 optionable v1 2025-02-28 23:11:31 -05:00
XtraCube 26afd47d2a attribute event registration 2025-02-28 15:41:18 -05:00
XtraCube 2e0ecd95a6 fix things i forgot to commit 2025-02-28 10:17:39 -05:00
XtraCube 669d5765e6 add winner info to verify condition in gameover 2025-02-28 10:14:24 -05:00
42 changed files with 788 additions and 356 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.1.9</VersionPrefix>
<VersionPrefix>0.2.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<Authors>All Of Us, XtraCube, Angxl</Authors>
@@ -19,7 +19,7 @@ public class TeleportButton : CustomActionButton
public override int MaxUses => 2;
public override LoadableAsset<Sprite> Sprite => ExampleAssets.TeleportButton;
public override Color TextOutlineColor => new Color32(221, 176, 152, 255);
public static bool IsZoom { get; private set; }
public override bool Enabled(RoleBehaviour? role)
+5 -5
View File
@@ -12,11 +12,7 @@ public static class ExampleEventHandlers
{
public static void Initialize()
{
// Register event handlers here
MiraEventManager.RegisterEventHandler<MiraButtonClickEvent<FreezeButton>>(FreezeButtonClickHandler, 1);
MiraEventManager.RegisterEventHandler<MiraButtonCancelledEvent<FreezeButton>>(FreezeButtonCancelledHandler);
MiraEventManager.RegisterEventHandler<UpdateSystemEvent>(UpdateSystemEventHandler);
// You can register event handlers with the MiraEventManager class.
MiraEventManager.RegisterEventHandler<BeforeMurderEvent>(@event =>
{
Logger<ExamplePlugin>.Info($"{@event.Source.Data.PlayerName} is about to kill {@event.Target.Data.PlayerName}");
@@ -33,12 +29,15 @@ public static class ExampleEventHandlers
});
}
// Events can be registered using an attribute as well.
[RegisterEvent]
public static void UpdateSystemEventHandler(UpdateSystemEvent @event)
{
Logger<ExamplePlugin>.Error(@event.SystemType.ToString());
}
// Example event handler
[RegisterEvent(1)]
public static void FreezeButtonClickHandler(MiraButtonClickEvent<FreezeButton> @event)
{
Logger<ExamplePlugin>.Warning("Freeze button clicked!");
@@ -51,6 +50,7 @@ public static class ExampleEventHandlers
}
// Example event handler
[RegisterEvent]
public static void FreezeButtonCancelledHandler(MiraButtonCancelledEvent<FreezeButton> @event)
{
Logger<ExamplePlugin>.Warning("Freeze button cancelled!");
@@ -7,9 +7,9 @@ namespace MiraAPI.Example.GameOver;
public class NeutralKillerGameOver : CustomGameOver
{
public override bool VerifyCondition(PlayerControl playerControl)
public override bool VerifyCondition(PlayerControl playerControl, NetworkedPlayerInfo[] winners)
{
return playerControl.Data.Role is NeutralKillerRole;
return winners is [{ Role: NeutralKillerRole }];
}
public override void AfterEndGameSetup(EndGameManager endGameManager)
+9 -7
View File
@@ -1,4 +1,6 @@
using MiraAPI.Modifiers.Types;
using MiraAPI.Example.Options.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers.Types;
namespace MiraAPI.Example.Modifiers;
@@ -6,13 +8,13 @@ public class CaptainModifier : GameModifier
{
public override string ModifierName => "Captain";
public override int GetAmountPerGame()
{
return 1;
}
public override int GetAssignmentChance()
{
return 100;
return (int)OptionGroupSingleton<CaptainModifierSettings>.Instance.Chance;
}
public override int GetAmountPerGame()
{
return (int)OptionGroupSingleton<CaptainModifierSettings>.Instance.Amount;
}
}
@@ -5,6 +5,7 @@ namespace MiraAPI.Example.Modifiers;
public class HighPriorityModifier : GameModifier
{
public override string ModifierName => "High Priority";
public override int GetAssignmentChance()
{
return 100;
@@ -0,0 +1,19 @@
using MiraAPI.Example.Modifiers;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example.Options.Modifiers;
public class CaptainModifierSettings : AbstractOptionGroup<CaptainModifier>
{
public override string GroupName => "Captain Settings";
[ModdedNumberOption("Amount", 0, 5)]
public float Amount { get; set; } = 1;
[ModdedNumberOption("Chance", 0, 100, 10)]
public float Chance { get; set; } = 50;
[ModdedNumberOption("Uses", 1, 5)]
public float NumUses { get; set; } = 3;
}
@@ -0,0 +1,13 @@
using MiraAPI.Example.Modifiers.Freezer;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example.Options.Modifiers;
public class FreezeModifierSettings : AbstractOptionGroup<FreezeModifier>
{
public override string GroupName => "Freeze";
[ModdedToggleOption("Use Color")]
public bool UseColor { get; set; } = true;
}
@@ -0,0 +1,19 @@
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
namespace MiraAPI.Example.Options.Modifiers;
public class GeneralModifierOptions : AbstractOptionGroup
{
public override string GroupName => "General";
public override uint GroupPriority => 0;
public override bool ShowInModifiersMenu => true;
[ModdedToggleOption("Some Boolean Option")]
public bool SomeBooleanOption { get; set; } = true;
[ModdedNumberOption("Number Option", 0, 10, 1)]
public float NumberOption { get; set; } = 5;
}
@@ -1,17 +1,14 @@
using System;
using MiraAPI.Example.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.Utilities;
namespace MiraAPI.Example.Options.Roles;
public class FreezerRoleSettings : AbstractOptionGroup
public class FreezerRoleSettings : AbstractOptionGroup<FreezerRole>
{
public override string GroupName => "Custom Role";
public override Type AdvancedRole => typeof(FreezerRole);
[ModdedNumberOption("Freeze Duration", 1, 15, 1, MiraNumberSuffixes.Seconds)]
public float FreezeDuration { get; set; } = 5;
@@ -1,5 +1,4 @@
using System;
using MiraAPI.Example.Roles;
using MiraAPI.Example.Roles;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.Attributes;
using MiraAPI.GameOptions.OptionTypes;
@@ -7,12 +6,10 @@ using MiraAPI.Utilities;
namespace MiraAPI.Example.Options.Roles;
public class TeleporterOptions : AbstractOptionGroup
public class TeleporterOptions : AbstractOptionGroup<TeleporterRole>
{
public override string GroupName => "Teleporter";
public override Type AdvancedRole => typeof(TeleporterRole);
public ModdedNumberOption TeleportCooldown { get; set; } = new("Teleport Cooldown", 10, 5, 60, 2.5f, MiraNumberSuffixes.Seconds);
[ModdedNumberOption("Teleport Duration", 5, 25, 1, MiraNumberSuffixes.Seconds)]
+26
View File
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Reactor.Utilities;
namespace MiraAPI.Events;
@@ -54,6 +56,30 @@ public static class MiraEventManager
return true;
}
/// <summary>
/// Register an event.
/// </summary>
/// <param name="type">The type of event.</param>
/// <param name="methodInfo">The MethodInfo of the event handler.</param>
/// <param name="priority">The priority of the event handler. Higher values are called first.</param>
public static void RegisterEventHandler(Type type, MethodInfo methodInfo, int priority = 0)
{
if (!type.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Type must be a subclass of MiraEvent: {nameof(type)}");
return;
}
if (!EventWrappers.TryGetValue(type, out var handlers))
{
handlers = [];
EventWrappers.Add(type, handlers);
}
var @delegate = Delegate.CreateDelegate(typeof(Action<>).MakeGenericType(type), methodInfo);
handlers.Add(new MiraEventWrapper(@delegate, priority));
}
/// <summary>
/// Register an event.
/// </summary>
+24
View File
@@ -0,0 +1,24 @@
using System;
namespace MiraAPI.Events;
/// <summary>
/// Register an event.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class RegisterEventAttribute : Attribute
{
/// <summary>
/// Gets the priority of the event.
/// </summary>
public int Priority { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RegisterEventAttribute"/> class.
/// </summary>
/// <param name="priority">The priority of the event.</param>
public RegisterEventAttribute(int priority = 0)
{
Priority = priority;
}
}
@@ -0,0 +1,36 @@
namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary>
/// The event that is invoked when a body is reported.
/// This is invoked in CmdReportBody.
/// </summary>
public class ReportBodyEvent : MiraCancelableEvent
{
/// <summary>
/// Gets the player that is reporting the body.
/// </summary>
public PlayerControl Reporter { get; }
/// <summary>
/// Gets the player that is being reported. Will be null for emergency meeting.
/// </summary>
public NetworkedPlayerInfo? Target { get; }
/// <summary>
/// Gets the body that is being reported. Will be null for emergency meeting.
/// </summary>
public DeadBody? Body { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ReportBodyEvent"/> class.
/// </summary>
/// <param name="reporter">The player who reported the body.</param>
/// <param name="target">The player being reported.</param>
/// <param name="body">The body being reported.</param>
public ReportBodyEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body)
{
Reporter = reporter;
Target = target;
Body = body;
}
}
@@ -1,35 +1,22 @@
namespace MiraAPI.Events.Vanilla.Meeting;
/// <summary>
/// The event that is invoked when a meeting is called.
/// The event that is invoked when a meeting is called. This event is not cancelable.
/// This event is called after Mira resets votes, so if you plan on adding votes to a specific player, do it with this event.
/// </summary>
public class StartMeetingEvent : MiraCancelableEvent
public class StartMeetingEvent : MiraEvent
{
/// <summary>
/// Gets the player that is calling the meeting.
/// Gets the MeetingHud instance.
/// </summary>
public PlayerControl Reporter { get; }
/// <summary>
/// Gets the player that is being reported. Will be null for emergency meeting.
/// </summary>
public NetworkedPlayerInfo? Target { get; }
/// <summary>
/// Gets the body that is being reported. Will be null for emergency meeting.
/// </summary>
public DeadBody? Body { get; }
public MeetingHud MeetingHud { get; }
/// <summary>
/// Initializes a new instance of the <see cref="StartMeetingEvent"/> class.
/// </summary>
/// <param name="reporter">The player who reported the body.</param>
/// <param name="target">The player being reported.</param>
/// <param name="body">The body being reported.</param>
public StartMeetingEvent(PlayerControl reporter, NetworkedPlayerInfo? target, DeadBody? body)
/// <param name="meetingHud">The MeetingHud instance.</param>
public StartMeetingEvent(MeetingHud meetingHud)
{
Reporter = reporter;
Target = target;
Body = body;
MeetingHud = meetingHud;
}
}
+2 -1
View File
@@ -18,8 +18,9 @@ public abstract class CustomGameOver
/// Verifies if the condition for this CustomGameOver is met.
/// </summary>
/// <param name="playerControl">The PlayerControl that requested the Game Over.</param>
/// <param name="winners">The collection of winners.</param>
/// <returns>True if the condition is met, otherwise false.</returns>
public virtual bool VerifyCondition(PlayerControl playerControl)
public virtual bool VerifyCondition(PlayerControl playerControl, NetworkedPlayerInfo[] winners)
{
return true;
}
+21 -5
View File
@@ -16,6 +16,17 @@ public abstract class AbstractOptionGroup
/// </summary>
public abstract string GroupName { get; }
/// <summary>
/// Gets the Optionable type of the group.
/// </summary>
public virtual Type? OptionableType => null;
/// <summary>
/// Gets a value indicating whether the group should be shown in the modifiers menu.
/// </summary>
// TODO: make this not a boolean
public virtual bool ShowInModifiersMenu => false;
/// <summary>
/// Gets the function that determines whether the group should be visible or not.
/// </summary>
@@ -32,12 +43,17 @@ public abstract class AbstractOptionGroup
/// </summary>
public virtual uint GroupPriority => uint.MaxValue;
/// <summary>
/// Gets the role the group is associated with. This is used for the advanced role options menu.
/// </summary>
public virtual Type? AdvancedRole => null;
internal bool AllOptionsHidden { get; set; }
internal CategoryHeaderMasked? Header { get; set; }
}
/// <summary>
/// Base class for option groups. An option group is a collection of options that are displayed together in the options menu.
/// </summary>
/// <typeparam name="T">The type of the optionable that this group contains.</typeparam>
public abstract class AbstractOptionGroup<T> : AbstractOptionGroup where T : IOptionable
{
/// <inheritdoc />
public override Type OptionableType => typeof(T);
}
@@ -8,12 +8,12 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for creating an enum option.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null, Type? roleType = null)
: ModdedOptionAttribute(title, roleType)
public class ModdedEnumOptionAttribute(string title, Type enumType, string[]? values = null)
: ModdedOptionAttribute(title)
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values, RoleType);
var opt = new ModdedEnumOption(Title, (int)(value ?? 0), enumType, values);
return opt;
}
@@ -16,13 +16,12 @@ public class ModdedNumberOptionAttribute(
float increment = 1,
MiraNumberSuffixes suffixType = MiraNumberSuffixes.None,
string? formatString = null,
bool zeroInfinity = false,
Type? roleType = null)
: ModdedOptionAttribute(title, roleType)
bool zeroInfinity = false)
: ModdedOptionAttribute(title)
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity, RoleType);
return new ModdedNumberOption(Title, (float)(value ?? min+increment), min, max, increment, suffixType, formatString, zeroInfinity);
}
/// <inheritdoc />
@@ -8,13 +8,12 @@ namespace MiraAPI.GameOptions.Attributes;
/// Attribute for a toggle option.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="roleType">An optional role type.</param>
[AttributeUsage(AttributeTargets.Property)]
public class ModdedToggleOptionAttribute(string title, Type? roleType = null) : ModdedOptionAttribute(title, roleType)
public class ModdedToggleOptionAttribute(string title) : ModdedOptionAttribute(title)
{
internal override IModdedOption CreateOption(object? value, PropertyInfo property)
{
var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false), RoleType);
var toggleOpt = new ModdedToggleOption(Title, (bool)(value ?? false));
return toggleOpt;
}
-1
View File
@@ -13,7 +13,6 @@ public interface IModdedOption
StringNames StringName { get; }
IMiraPlugin? ParentMod { get; set; }
BaseGameSetting Data { get; }
Type? AdvancedRole { get; set; }
OptionBehaviour? OptionBehaviour { get; }
Func<bool> Visible { get; set; }
ConfigDefinition? ConfigDefinition { get; set; }
+6
View File
@@ -0,0 +1,6 @@
namespace MiraAPI.GameOptions;
/// <summary>
/// Interface for classes that can be used for option groups.
/// </summary>
public interface IOptionable;
+1 -4
View File
@@ -108,7 +108,7 @@ public static class ModdedOptionsManager
RegisterOption(option, group, property.Name, pluginInfo);
}
private static void RegisterOption(
internal static void RegisterOption(
IModdedOption option,
AbstractOptionGroup group,
string propertyName,
@@ -119,10 +119,7 @@ public static class ModdedOptionsManager
option.ConfigDefinition = new ConfigDefinition(groupName, propertyName);
option.ParentMod = pluginInfo.MiraPlugin;
option.AdvancedRole = group.AdvancedRole;
pluginInfo.Options.Add(option);
ModdedOptions.Add(option.Id, option);
group.Options.Add(option);
}
@@ -0,0 +1,15 @@
using System.Linq;
namespace MiraAPI.GameOptions;
internal class ModifierOptionGroup : AbstractOptionGroup
{
public override string GroupName { get; }
public ModifierOptionGroup(string name, IModdedOption[] options, params AbstractOptionGroup[] groups)
{
GroupName = name;
Options.AddRange(options);
Options.AddRange(groups.SelectMany(x=>x.Options));
}
}
@@ -27,8 +27,7 @@ public class ModdedEnumOption : ModdedOption<int>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="enumType">The Enum type.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
/// <param name="roleType">An optional role type to specify for a specific role.</param>
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType)
public ModdedEnumOption(string title, int defaultValue, Type enumType, string[]? values = null) : base(title, defaultValue)
{
Values = values ?? Enum.GetNames(enumType);
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -101,7 +100,6 @@ public class ModdedEnumOption : ModdedOption<int>
}
}
/// <summary>
/// An option for selecting an enum value.
/// </summary>
@@ -118,10 +116,8 @@ public class ModdedEnumOption<T> : ModdedOption<T> where T : Enum
/// </summary>
/// <param name="title">The title of the option.</param>
/// <param name="defaultValue">The default value as an int.</param>
/// <param name="enumType">The Enum type.</param>
/// <param name="values">An option list of string values to use in place of the enum name.</param>
/// <param name="roleType">An optional role type to specify for a specific role.</param>
public ModdedEnumOption(string title, T defaultValue, string[]? values = null, Type? roleType = null) : base(title, defaultValue, roleType)
public ModdedEnumOption(string title, T defaultValue, string[]? values = null) : base(title, defaultValue)
{
Values = values ?? Enum.GetNames(typeof(T));
Data = ScriptableObject.CreateInstance<StringGameSetting>();
@@ -48,7 +48,6 @@ public class ModdedNumberOption : ModdedOption<float>
/// <param name="suffixType">The suffix type.</param>
/// <param name="formatString">Optional format string for the option screen.</param>
/// <param name="zeroInfinity">Whether zero is infinity or not.</param>
/// <param name="roleType">An optional role type.</param>
public ModdedNumberOption(
string title,
float defaultValue,
@@ -57,8 +56,7 @@ public class ModdedNumberOption : ModdedOption<float>
float increment,
MiraNumberSuffixes suffixType,
string? formatString = null,
bool zeroInfinity = false,
Type? roleType = null) : base(title, defaultValue, roleType)
bool zeroInfinity = false) : base(title, defaultValue)
{
Min = min;
Max = max;
@@ -77,11 +75,11 @@ public class ModdedNumberOption : ModdedOption<float>
data.Increment = Increment;
data.ValidRange = new FloatRange(Min, Max);
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");
(defaultValue.IsInteger() &&
Increment.IsInteger() &&
Value.IsInteger() &&
Min.IsInteger() &&
Max.IsInteger() ? "0" : "0.0");
data.ZeroIsInfinity = ZeroInfinity;
data.SuffixType = (NumberSuffixes)SuffixType;
@@ -144,7 +142,7 @@ public class ModdedNumberOption : ModdedOption<float>
Value = Mathf.Clamp(newValue, Min, Max);
HudManager.Instance.Notifier.AddSettingsChangeMessage(
StringName,
Data?.GetValueString(Value),
Data.GetValueString(Value),
false);
if (OptionBehaviour is NumberOption opt)
@@ -2,7 +2,6 @@
using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using Reactor.Localization.Utilities;
using Reactor.Networking.Rpc;
using UnityEngine;
@@ -72,11 +71,6 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary>
public Func<bool> Visible { get; set; }
/// <summary>
/// Gets or sets the advanced role of the option.
/// </summary>
public Type? AdvancedRole { get; set; }
/// <summary>
/// Gets or sets the option behaviour of the option.
/// </summary>
@@ -102,8 +96,7 @@ public abstract class ModdedOption<T> : IModdedOption
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="roleType">The Role Type or null if it doesn't belong to a role.</param>
protected ModdedOption(string title, T defaultValue, Type? roleType)
protected ModdedOption(string title, T defaultValue)
{
Id = ModdedOptionsManager.NextId;
Title = title;
@@ -111,11 +104,6 @@ public abstract class ModdedOption<T> : IModdedOption
Value = defaultValue;
StringName = CustomStringName.CreateAndRegister(Title);
Visible = () => true;
if (roleType is not null && roleType.IsAssignableTo(typeof(ICustomRole)))
{
AdvancedRole = roleType;
}
}
internal void ValueChanged(OptionBehaviour optionBehaviour)
@@ -15,8 +15,7 @@ public class ModdedToggleOption : ModdedOption<bool>
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="roleType">An optional role type.</param>
public ModdedToggleOption(string title, bool defaultValue, Type? roleType = null) : base(title, defaultValue, roleType)
public ModdedToggleOption(string title, bool defaultValue) : base(title, defaultValue)
{
Data = ScriptableObject.CreateInstance<CheckboxGameSetting>();
+20
View File
@@ -46,6 +46,11 @@ public abstract class CustomActionButton
/// </summary>
public virtual int MaxUses => 0;
/// <summary>
/// Gets the button's text outline color.
/// </summary>
public virtual Color TextOutlineColor => Color.clear;
/// <summary>
/// Gets the location of the button on the screen.
/// </summary>
@@ -114,6 +119,11 @@ public abstract class CustomActionButton
Button.SetInfiniteUses();
}
if (TextOutlineColor != Color.clear)
{
SetTextOutline(TextOutlineColor);
}
var pb = Button.GetComponent<PassiveButton>();
pb.OnClick = new Button.ButtonClickedEvent();
pb.OnClick.AddListener((UnityAction)(() =>
@@ -207,6 +217,16 @@ public abstract class CustomActionButton
EffectActive = false;
}
/// <summary>
/// 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);
}
/// <summary>
/// A utility function to override the sprite of the button.
/// </summary>
@@ -0,0 +1,10 @@
namespace MiraAPI.Modifiers;
/// <summary>
/// Represents the assignment configuration for a game modifier.
/// </summary>
/// <param name="DefaultAmount">The default amount of players that can have this modifier in a game.</param>
/// <param name="DefaultChance">The default chance of the modifier being assigned to a player.</param>
/// <param name="CreateAmountOption">Whether Mira should create an amount option for the modifier.</param>
/// <param name="CreateChanceOption">Whether Mira should create a chance option for the modifier.</param>
public record struct AssignmentConfiguration(int DefaultAmount, int DefaultChance, bool CreateAmountOption, bool CreateChanceOption);
+11 -1
View File
@@ -1,11 +1,13 @@
using System;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
namespace MiraAPI.Modifiers;
/// <summary>
/// Base class for all modifiers.
/// </summary>
public abstract class BaseModifier
public abstract class BaseModifier : IOptionable
{
/// <summary>
/// Gets the player that the modifier is attached to.
@@ -32,6 +34,14 @@ public abstract class BaseModifier
/// </summary>
public uint TypeId => ModifierManager.GetModifierTypeId(GetType()) ?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
/// Gets the parent mod of the modifier.
/// </summary>
public MiraPluginInfo ParentMod => Array.Find(
MiraPluginManager.Instance.RegisteredPlugins(),
x => x.Modifiers.Exists(y => y.TypeId == TypeId)
) ?? throw new InvalidOperationException("Modifier is not registered.");
/// <summary>
/// Gets the modifier name.
/// </summary>
+21 -5
View File
@@ -63,16 +63,32 @@ public static class ModifierManager
IdToTypeModifierMap.Add(GetNextTypeId(), modifierType);
TypeToIdModifierMap.Add(modifierType, _nextTypeId);
var bm = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType); // this probably isn't a great idea
info.Modifiers.Add(bm);
if (!typeof(GameModifier).IsAssignableFrom(modifierType))
BaseModifier modifier;
if (modifierType.GetConstructor(Type.EmptyTypes) != null)
{
modifier = (BaseModifier)Activator.CreateInstance(modifierType)!;
}
else
{
// this is probably terrible but its good enough for now.
modifier = (BaseModifier)FormatterServices.GetUninitializedObject(modifierType);
}
info.Modifiers.Add(modifier);
if (modifier is not GameModifier gameModifier)
{
return true;
}
var mod = Activator.CreateInstance(modifierType) as GameModifier;
var priority = mod!.Priority();
if (modifierType.GetConstructor(Type.EmptyTypes) == null)
{
Logger<MiraApiPlugin>.Error($"Game Modifier {modifierType.FullName} does not have a parameterless constructor!");
return false;
}
var priority = gameModifier.Priority();
if (!PrioritiesToIdsMap.TryGetValue(priority, out var list))
{
+1 -1
View File
@@ -48,7 +48,7 @@ public class CustomGameOverRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<
{
if (GameOverManager.TryGetGameOver(data.Reason, out var gameOver))
{
if (!gameOver.VerifyCondition(innerNetObject))
if (!gameOver.VerifyCondition(innerNetObject, [.. data.Winners]))
{
Logger<MiraApiPlugin>.Info($"Game over condition not met for {gameOver.GetType().Name}");
return;
@@ -0,0 +1,19 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static class ReportBodyEventPatch
{
public static bool Prefix(PlayerControl __instance, NetworkedPlayerInfo? target)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
var @event = new ReportBodyEvent(__instance, target, body);
MiraEventManager.InvokeEvent(@event);
return !@event.IsCancelled;
}
}
@@ -1,21 +1,15 @@
using HarmonyLib;
using MiraAPI.Events;
using MiraAPI.Events.Vanilla.Meeting;
using MiraAPI.Utilities;
namespace MiraAPI.Patches.Events;
[HarmonyPatch]
[HarmonyPatch(typeof(MeetingHud), nameof(MeetingHud.Start))]
public static class StartMeetingEventPatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))]
public static bool PlayerControlCmdReportDeadBodyPrefix(PlayerControl __instance, NetworkedPlayerInfo? target)
public static void Postfix(MeetingHud __instance)
{
var body = target != null ? Helpers.GetBodyById(target.PlayerId) : null;
var @event = new StartMeetingEvent(__instance, target, body);
var @event = new StartMeetingEvent(__instance);
MiraEventManager.InvokeEvent(@event);
return !@event.IsCancelled;
}
}
+200 -149
View File
@@ -1,5 +1,8 @@
using System.Linq;
using System;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
@@ -7,6 +10,7 @@ using Reactor.Utilities.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
@@ -30,64 +34,98 @@ public static class GameOptionsMenuPatch
}
var num = 2.1f;
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(
x => x.AdvancedRole is null) ?? [];
foreach (var group in filteredGroups)
if (__instance.name == "MODIFIERS TAB")
{
if (group.Options.Count == 0 || group.Header is null)
ModifiersUpdate(ref num);
}
else
{
var filteredGroups =
GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.OptionableType == null) ?? [];
foreach (var group in filteredGroups)
{
continue;
}
if (!group.GroupVisible.Invoke())
{
group.Header.gameObject.SetActive(false);
foreach (var option in group.Options)
{
option.OptionBehaviour?.gameObject.SetActive(false);
}
continue;
}
group.Header.gameObject.SetActive(true);
group.Header.transform.localScale = Vector3.one * 0.63f;
group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f);
num -= 0.58f;
foreach (var opt in group.Options)
{
var newOpt = opt.OptionBehaviour;
if (newOpt is null)
{
continue;
}
if (!opt.Visible.Invoke())
{
newOpt.gameObject.SetActive(false);
continue;
}
if (!group.AllOptionsHidden)
{
newOpt.gameObject.SetActive(true);
newOpt.transform.localPosition = new Vector3(0.952f, num, -2f);
num -= 0.45f;
}
else
{
newOpt.gameObject.SetActive(false);
}
UpdateGroup(group, ref num);
}
}
__instance.scrollBar.SetYBoundsMax(-num - 1.65f);
}
private static void UpdateGroup(AbstractOptionGroup group, ref float num)
{
if (group.Options.Count == 0 || group.Header is null)
{
return;
}
if (!group.GroupVisible.Invoke())
{
group.Header.gameObject.SetActive(false);
foreach (var option in group.Options)
{
option.OptionBehaviour?.gameObject.SetActive(false);
}
return;
}
group.Header.gameObject.SetActive(true);
group.Header.transform.localScale = Vector3.one * 0.63f;
group.Header.transform.localPosition = new Vector3(-0.903f, num, -2f);
num -= 0.58f;
foreach (var opt in group.Options)
{
var newOpt = opt.OptionBehaviour;
if (newOpt is null)
{
continue;
}
if (!opt.Visible.Invoke())
{
newOpt.gameObject.SetActive(false);
continue;
}
if (!group.AllOptionsHidden)
{
newOpt.gameObject.SetActive(true);
newOpt.transform.localPosition = new Vector3(0.952f, num, -2f);
num -= 0.45f;
}
else
{
newOpt.gameObject.SetActive(false);
}
}
}
private static void ModifiersUpdate(ref float num)
{
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var modGroup in groups)
{
UpdateGroup(modGroup, ref num);
}
}
private static void ModifiersCreate(GameOptionsMenu menu)
{
var groups = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true) ?? [];
foreach (var group in groups)
{
CreateGroup(menu, group);
}
}
[HarmonyPrefix]
[HarmonyPatch(nameof(GameOptionsMenu.CreateSettings))]
public static bool SettingsPatch(GameOptionsMenu __instance)
@@ -99,106 +137,18 @@ public static class GameOptionsMenuPatch
__instance.MapPicker.gameObject.SetActive(false);
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups.Where(x => x.AdvancedRole is null) ?? [];
if (__instance.name == "MODIFIERS TAB")
{
ModifiersCreate(__instance);
return false;
}
var filteredGroups = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x => x.OptionableType == null) ?? [];
foreach (var group in filteredGroups)
{
var categoryHeaderMasked = Object.Instantiate(
__instance.categoryHeaderOrigin,
Vector3.zero,
Quaternion.identity,
__instance.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.size = new Vector2(
categoryHeaderMasked.Background.size.x + 1.5f,
categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.gameObject.SetActive(false);
group.Header = categoryHeaderMasked;
var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform);
newText.text = "<size=70%>(Click to close)</size>";
newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
var options = group.Options.Select(
opt => opt.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
__instance.stringOptionOrigin,
__instance.settingsContainer));
foreach (var newOpt in options)
{
newOpt.SetClickMask(__instance.ButtonClickMask);
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren<SpriteRenderer>(true);
foreach (var renderer in componentsInChildren)
{
if (group.GroupColor != Color.clear)
{
renderer.color = group.GroupColor.GetAlternateColor();
if (renderer.transform.parent.TryGetComponent<GameOptionButton>(out var btn))
{
btn.interactableColor = group.GroupColor.GetAlternateColor();
btn.interactableHoveredColor = Color.white;
}
}
renderer.material.SetInt(PlayerMaterial.MaskLayer, 20);
}
foreach (var textMeshPro in newOpt.GetComponentsInChildren<TextMeshPro>(true))
{
if (group.GroupColor != Color.clear)
{
textMeshPro.color = group.GroupColor;
}
textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f);
textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20);
}
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color =
group.GroupColor != Color.clear ? 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;
}
__instance.Children.Add(newOpt);
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
}
var boxCol = categoryHeaderMasked.gameObject.AddComponent<BoxCollider2D>();
boxCol.size = new Vector2(7, 0.7f);
boxCol.offset = new Vector2(1.5f, -0.3f);
var headerBtn = categoryHeaderMasked.gameObject.AddComponent<PassiveButton>();
headerBtn.ClickSound = __instance.BackButton.GetComponent<PassiveButton>().ClickSound;
headerBtn.OnMouseOver = new UnityEvent();
headerBtn.OnMouseOut = new UnityEvent();
headerBtn.OnClick.AddListener(
(UnityAction)(() =>
{
group.AllOptionsHidden = !group.AllOptionsHidden;
newText.text = group.AllOptionsHidden
? "<size=70%>(Click to open)</size>"
: "<size=70%>(Click to close)</size>";
}));
headerBtn.SetButtonEnableState(true);
CreateGroup(__instance, group);
}
return false;
@@ -233,9 +183,110 @@ public static class GameOptionsMenuPatch
continue;
}
optionBehaviour.OnValueChanged = new System.Action<OptionBehaviour>(__instance.ValueChanged);
optionBehaviour.OnValueChanged = new Action<OptionBehaviour>(__instance.ValueChanged);
}
return false;
}
private static void CreateGroup(GameOptionsMenu menu, AbstractOptionGroup group)
{
var categoryHeaderMasked = Object.Instantiate(
menu.categoryHeaderOrigin,
Vector3.zero,
Quaternion.identity,
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.size = new Vector2(
categoryHeaderMasked.Background.size.x + 1.5f,
categoryHeaderMasked.Background.size.y);
categoryHeaderMasked.gameObject.SetActive(false);
group.Header = categoryHeaderMasked;
var newText = Object.Instantiate(categoryHeaderMasked.Title, categoryHeaderMasked.transform);
newText.text = "<size=70%>(Click to close)</size>";
newText.transform.localPosition = new Vector3(2.6249f, -0.165f, 0f);
newText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
var options = group.Options.Select(
opt => opt.CreateOption(
menu.checkboxOrigin,
menu.numberOptionOrigin,
menu.stringOptionOrigin,
menu.settingsContainer));
foreach (var newOpt in options)
{
newOpt.SetClickMask(menu.ButtonClickMask);
SpriteRenderer[] componentsInChildren = newOpt.GetComponentsInChildren<SpriteRenderer>(true);
foreach (var renderer in componentsInChildren)
{
if (group.GroupColor != Color.clear)
{
renderer.color = group.GroupColor.GetAlternateColor();
if (renderer.transform.parent.TryGetComponent<GameOptionButton>(out var btn))
{
btn.interactableColor = group.GroupColor.GetAlternateColor();
btn.interactableHoveredColor = Color.white;
}
}
renderer.material.SetInt(PlayerMaterial.MaskLayer, 20);
}
foreach (var textMeshPro in newOpt.GetComponentsInChildren<TextMeshPro>(true))
{
if (group.GroupColor != Color.clear)
{
textMeshPro.color = group.GroupColor;
}
textMeshPro.fontMaterial.SetFloat(ShaderID.StencilComp, 3f);
textMeshPro.fontMaterial.SetFloat(ShaderID.Stencil, 20);
}
if (newOpt is ToggleOption toggle)
{
toggle.CheckMark.sprite = MiraAssets.Checkmark.LoadAsset();
toggle.CheckMark.color =
group.GroupColor != Color.clear ? 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;
}
menu.Children.Add(newOpt);
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
}
var boxCol = categoryHeaderMasked.gameObject.AddComponent<BoxCollider2D>();
boxCol.size = new Vector2(7, 0.7f);
boxCol.offset = new Vector2(1.5f, -0.3f);
var headerBtn = categoryHeaderMasked.gameObject.AddComponent<PassiveButton>();
headerBtn.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
headerBtn.OnMouseOver = new UnityEvent();
headerBtn.OnMouseOut = new UnityEvent();
headerBtn.OnClick.AddListener(
(UnityAction)(() =>
{
group.AllOptionsHidden = !group.AllOptionsHidden;
newText.text = group.AllOptionsHidden
? "<size=70%>(Click to open)</size>"
: "<size=70%>(Click to close)</size>";
}));
headerBtn.SetButtonEnableState(true);
}
}
+146 -36
View File
@@ -1,6 +1,6 @@
using System;
using System.Linq;
using HarmonyLib;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities.Assets;
using Reactor.Utilities.Extensions;
@@ -12,26 +12,41 @@ using Object = UnityEngine.Object;
namespace MiraAPI.Patches.Options;
/// <summary>
/// Patches for the <see cref="GameSettingMenu"/> to add support for custom options.
/// </summary>
[HarmonyPatch(typeof(GameSettingMenu))]
public static class GameSettingMenuPatches
internal static class GameSettingMenuPatches
{
/// <summary>
/// Gets the currently selected mod index.
/// </summary>
public static int SelectedModIdx { get; private set; }
/// <summary>
/// Gets the currently selected mod.
/// </summary>
public static MiraPluginInfo? SelectedMod { get; private set; }
private static TextMeshPro? _text;
private static Vector3 _roleBtnOgPos;
private static GameOptionsMenu? _modifiersTab;
private static PassiveButton? _modifiersButton;
private static PassiveButton? _smallRolesButton;
[HarmonyPostfix]
[HarmonyPatch(nameof(GameSettingMenu.ChangeTab))]
public static void ChangeTabPostfix(GameSettingMenu __instance, int tabNum, bool previewOnly)
{
if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly)
{
if (_modifiersTab) _modifiersTab?.gameObject.SetActive(tabNum == 3);
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
}
if (previewOnly)
{
return;
}
_modifiersButton?.SelectButton(tabNum == 3);
_smallRolesButton?.SelectButton(tabNum == 2);
}
/// <summary>
/// Prefix for the <see cref="GameSettingMenu.Start"/> method. Sets up the custom options.
/// </summary>
@@ -54,43 +69,116 @@ public static class GameSettingMenuPatches
_text.fontSizeMax = 3.2f;
_text.overflowMode = TextOverflowModes.Overflow;
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
_text.alignment = TextAlignmentOptions.Center;
var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
nextButton.transform.localPosition = new Vector3(-2.2663f, 1.5272f, -25f);
nextButton.name = "RightArrowButton";
nextButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite = MiraAssets.NextButton.LoadAsset();
nextButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite = MiraAssets.NextButtonActive.LoadAsset();
nextButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButton.LoadAsset();
nextButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().sprite =
MiraAssets.NextButtonActive.LoadAsset();
nextButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().DestroyImmediate();
var passiveButton = nextButton.gameObject.GetComponent<PassiveButton>();
passiveButton.OnClick = new ButtonClickedEvent();
passiveButton.OnClick.AddListener((UnityAction)(() =>
{
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
SelectedModIdx = 0;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
{
SelectedModIdx = 0;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
var backButton = Object.Instantiate(nextButton, __instance.BackButton.transform.parent).gameObject;
backButton.transform.localPosition = new Vector3(-4.4209f, 1.5272f, -25f);
backButton.name = "LeftArrowButton";
backButton.gameObject.GetComponent<CloseButtonConsoleBehaviour>().Destroy();
backButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX = backButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener((UnityAction)(() =>
{
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
backButton.transform.FindChild("Active").gameObject.GetComponent<SpriteRenderer>().flipX =
backButton.transform.FindChild("Inactive").gameObject.GetComponent<SpriteRenderer>().flipX = true;
backButton.gameObject.GetComponent<PassiveButton>().OnClick.AddListener(
(UnityAction)(() =>
{
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
// clone game settings tab for modifiers
_modifiersTab = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent);
_modifiersTab.name = "MODIFIERS TAB";
// create button for modifiers
__instance.RoleSettingsButton.gameObject.SetActive(false);
_smallRolesButton = Object.Instantiate(
__instance.RoleSettingsButton,
__instance.RoleSettingsButton.transform.parent);
var pos = new Vector3(
-3.65f,
_smallRolesButton.transform.localPosition.y,
_smallRolesButton.transform.localPosition.z);
_smallRolesButton.transform.localPosition = pos;
_smallRolesButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, false);
}));
_smallRolesButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(2, true);
}));
var roleText = _smallRolesButton.buttonText;
roleText.text = "Roles";
roleText.GetComponent<TextTranslatorTMP>().Destroy();
roleText.alignment = TextAlignmentOptions.Center;
roleText.transform.parent.localPosition = new Vector3(
-.525f,
roleText.transform.parent.localPosition.y,
roleText.transform.parent.localPosition.z);
foreach (var collider in _smallRolesButton.Colliders)
{
if (collider.TryCast<BoxCollider2D>() is { } col)
{
col.size = new Vector2(col.size.x / 2, col.size.y);
}
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}));
}
foreach (var rend in _smallRolesButton.GetComponentsInChildren<SpriteRenderer>(true))
{
rend.size = new Vector2(rend.size.x / 2, rend.size.y);
}
_modifiersButton = Object.Instantiate(_smallRolesButton, _smallRolesButton.transform.parent);
_modifiersButton.OnClick = new ButtonClickedEvent();
_modifiersButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, false);
}));
_modifiersButton.OnMouseOver = new UnityEvent();
_modifiersButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(3, true);
}));
_modifiersButton.buttonText.text = "Modifiers";
pos.x = -2.27f;
_modifiersButton.transform.localPosition = pos;
_modifiersButton.name = "ModifiersButton";
UpdateText(__instance, __instance.GameSettingsTab, __instance.RoleSettingsTab);
}
private static void UpdateText(GameSettingMenu menu, GameOptionsMenu settings, RolesSettingsMenu roles)
@@ -109,10 +197,30 @@ public static class GameSettingMenuPatches
_text.text = name[..Math.Min(name.Length, 25)];
}
menu.RoleSettingsButton.transform.localPosition = _roleBtnOgPos;
menu.GameSettingsButton.gameObject.SetActive(true);
menu.RoleSettingsButton.gameObject.SetActive(true);
_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)
{
_modifiersButton.gameObject.SetActive(true);
_smallRolesButton.gameObject.SetActive(true);
}
else
{
menu.RoleSettingsButton.gameObject.SetActive(true);
if (SelectedModIdx == 0 && _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 (SelectedMod?.Options.Where(x=>x.AdvancedRole==null).ToList().Count == 0)
@@ -129,7 +237,7 @@ public static class GameSettingMenuPatches
{
menu.RoleSettingsButton.transform.localPosition = menu.GameSettingsButton.transform.localPosition;
}
}
}*/
if (roles.roleChances != null && SelectedMod != null && SelectedMod.CustomRoles.Count != 0)
{
@@ -139,11 +247,13 @@ public static class GameSettingMenuPatches
{
child.gameObject.DestroyImmediate();
}
roles.advancedSettingChildren.Clear();
roles.advancedSettingChildren = null;
}
foreach (var header in roles.RoleChancesSettings.transform.GetComponentsInChildren<CategoryHeaderEditRole>())
foreach (var header in roles.RoleChancesSettings.transform
.GetComponentsInChildren<CategoryHeaderEditRole>())
{
header.gameObject.DestroyImmediate();
}
+83 -43
View File
@@ -2,11 +2,14 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Roles;
using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using Reactor.Localization.Utilities;
using Reactor.Utilities.Extensions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
@@ -17,17 +20,38 @@ namespace MiraAPI.Patches.Options;
[HarmonyPatch(typeof(LobbyViewSettingsPane))]
public static class LobbyViewPanePatches
{
private static int CurrentSelectedMod { get; set; }
private static int SelectedModIdx { get; set; }
private static MiraPluginInfo? SelectedMod => CurrentSelectedMod == 0
private static MiraPluginInfo? SelectedMod => SelectedModIdx == 0
? null
: MiraPluginManager.Instance.RegisteredPlugins()[CurrentSelectedMod - 1];
: MiraPluginManager.Instance.RegisteredPlugins()[SelectedModIdx - 1];
private static PassiveButton? ModifiersTabButton { get; set; }
private static StringNames ModifiersTabName { get; } = CustomStringName.CreateAndRegister("ModifiersTab");
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))]
public static void AwakePatch(LobbyViewSettingsPane __instance)
{
__instance.gameModeText.transform.localPosition = new Vector3(-2.3f, 2.4f, -2);
__instance.gameModeText.GetComponent<TextTranslatorTMP>().Destroy();
// create modifiers button
ModifiersTabButton = Object.Instantiate(__instance.rolesTabButton, __instance.rolesTabButton.transform.parent);
ModifiersTabButton.buttonText.gameObject.GetComponent<TextTranslatorTMP>().Destroy();
ModifiersTabButton.name = "ModifiersTabButton";
var pos = ModifiersTabButton.transform.localPosition;
pos.x = 2.1f;
ModifiersTabButton.transform.localPosition = pos;
ModifiersTabButton.buttonText.text = "Modifiers";
ModifiersTabButton.OnClick = new Button.ButtonClickedEvent();
ModifiersTabButton.OnClick.AddListener(
(UnityAction)(() =>
{
__instance.ChangeTab(ModifiersTabName);
}));
ModifiersTabButton.gameObject.SetActive(SelectedModIdx!=0);
// Create the next button
var nextButton = Object.Instantiate(__instance.BackButton, __instance.BackButton.transform.parent).gameObject;
@@ -49,14 +73,13 @@ public static class LobbyViewPanePatches
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
CurrentSelectedMod += 1;
if (CurrentSelectedMod > MiraPluginManager.Instance.RegisteredPlugins().Length)
SelectedModIdx += 1;
if (SelectedModIdx > MiraPluginManager.Instance.RegisteredPlugins().Length)
{
CurrentSelectedMod = 0;
SelectedModIdx = 0;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
Refresh(__instance);
}));
// Create the back button
@@ -72,34 +95,56 @@ public static class LobbyViewPanePatches
passiveButton2.OnClick.AddListener(
(UnityAction)(() =>
{
CurrentSelectedMod -= 1;
if (CurrentSelectedMod < 0)
SelectedModIdx -= 1;
if (SelectedModIdx < 0)
{
CurrentSelectedMod = MiraPluginManager.Instance.RegisteredPlugins().Length;
SelectedModIdx = MiraPluginManager.Instance.RegisteredPlugins().Length;
}
__instance.RefreshTab();
__instance.scrollBar.ScrollToTop();
Refresh(__instance);
}));
}
[HarmonyPostfix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.Update))]
public static void UpdatePatch(LobbyViewSettingsPane __instance)
private static void Refresh(LobbyViewSettingsPane menu)
{
__instance.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default";
ModifiersTabButton?.gameObject.SetActive(SelectedModIdx != 0);
menu.gameModeText.text = SelectedMod?.PluginInfo.Metadata.Name ?? "Default";
menu.RefreshTab();
menu.scrollBar.ScrollToTop();
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.SetTab))]
public static bool SetTabPatch(LobbyViewSettingsPane __instance)
{
if (__instance.currentTab != ModifiersTabName || SelectedMod == null)
{
ModifiersTabButton?.SelectButton(false);
return true;
}
__instance.taskTabButton.SelectButton(false);
__instance.rolesTabButton.SelectButton(false);
ModifiersTabButton?.SelectButton(true);
var filteredGroups = SelectedMod.OptionGroups
.Where(x => x.GroupVisible() && (x.ShowInModifiersMenu || x.OptionableType?.IsAssignableTo(typeof(BaseModifier))==true));
DrawOptions(__instance, filteredGroups);
return false;
}
[HarmonyPrefix]
[HarmonyPatch(nameof(LobbyViewSettingsPane.DrawNormalTab))]
public static bool DrawNormalTabPatch(LobbyViewSettingsPane __instance)
{
if (CurrentSelectedMod == 0)
if (SelectedModIdx == 0 || SelectedMod == null)
{
return true;
}
DrawOptionsTab(__instance);
var filteredGroups = SelectedMod.OptionGroups
.Where(x => x.OptionableType == null && x.GroupVisible.Invoke());
DrawOptions(__instance, filteredGroups);
return false;
}
@@ -107,7 +152,7 @@ public static class LobbyViewPanePatches
[HarmonyPatch(nameof(LobbyViewSettingsPane.DrawRolesTab))]
public static bool DrawRolesTabPatch(LobbyViewSettingsPane __instance)
{
if (CurrentSelectedMod == 0)
if (SelectedModIdx == 0)
{
return true;
}
@@ -116,29 +161,22 @@ public static class LobbyViewPanePatches
return false;
}
private static void DrawOptionsTab(LobbyViewSettingsPane instance)
private static void DrawOptions(LobbyViewSettingsPane menu, IEnumerable<AbstractOptionGroup> groups)
{
if (SelectedMod == null)
{
return;
}
var num = 1.44f;
var filteredGroups = SelectedMod.OptionGroups.Where(x => x.GroupVisible.Invoke() && x.AdvancedRole is null);
foreach (var group in filteredGroups)
foreach (var group in groups)
{
var categoryHeaderMasked = Object.Instantiate(
instance.categoryHeaderOrigin,
instance.settingsContainer,
menu.categoryHeaderOrigin,
menu.settingsContainer,
true);
categoryHeaderMasked.SetHeader(StringNames.Name, 61);
categoryHeaderMasked.Title.text = group.GroupName;
categoryHeaderMasked.transform.localScale = Vector3.one;
categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f);
instance.settingsInfo.Add(categoryHeaderMasked.gameObject);
num -= 0.85f;
menu.settingsInfo.Add(categoryHeaderMasked.gameObject);
num -= 1.05f;
var i = 0;
@@ -150,8 +188,8 @@ public static class LobbyViewPanePatches
}
var viewSettingsInfoPanel = Object.Instantiate(
instance.infoPanelOrigin,
instance.settingsContainer,
menu.infoPanelOrigin,
menu.settingsContainer,
true);
viewSettingsInfoPanel.transform.localScale = Vector3.one;
float num2;
@@ -160,7 +198,7 @@ public static class LobbyViewPanePatches
num2 = -8.95f;
if (i > 0)
{
num -= 0.59f;
num -= 0.85f;
}
}
else
@@ -184,14 +222,14 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanel.SetInfo(data.Title, data.GetValueString(option.GetFloatData()), 61);
}
instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject);
menu.settingsInfo.Add(viewSettingsInfoPanel.gameObject);
i++;
}
num -= 0.59f;
num -= 0.85f;
}
instance.scrollBar.CalculateAndSetYBounds(instance.settingsInfo.Count + 10, 2f, 6f, 0.59f);
menu.scrollBar.CalculateAndSetYBounds(menu.settingsInfo.Count + 10, 2f, 6f, 0.85f);
}
private static void DrawRolesTab(LobbyViewSettingsPane instance)
@@ -277,8 +315,9 @@ public static class LobbyViewPanePatches
viewSettingsInfoPanelRoleVariant.transform.localScale = Vector3.one;
viewSettingsInfoPanelRoleVariant.transform.localPosition = new Vector3(num2, num, -2f);
var advancedRoleOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == customRole.GetType())
var advancedRoleOptions = SelectedMod.OptionGroups
.Where(x => x.OptionableType == customRole.GetType())
.SelectMany(x => x.Options)
.ToList();
if (numPerGame > 0 && advancedRoleOptions.Count > 0)
@@ -386,8 +425,9 @@ public static class LobbyViewPanePatches
var num = viewPanel.yPosStart;
var num2 = 1.08f;
var filteredOptions = SelectedMod.Options
.Where(x => x.AdvancedRole == roleType)
var filteredOptions = SelectedMod.OptionGroups
.Where(x => x.OptionableType == roleType)
.SelectMany(x=>x.Options)
.ToList();
for (var i = 0; i < filteredOptions.Count; i++)
@@ -304,15 +304,14 @@ public static class RoleSettingMenuPatches
var hasImage = role.RoleScreenshot != null;
var num = hasImage ? -0.872f : -1;
var filteredOptions = GameSettingMenuPatches.SelectedMod?.Options.Where(x => x.AdvancedRole == role.GetType()) ?? [];
// TODO: create sub groups under the role settings.
var filteredOptions = GameSettingMenuPatches.SelectedMod?.OptionGroups
.Where(x=> x.OptionableType == role.GetType())
.SelectMany(x=>x.Options)
.ToList() ?? [];
foreach (var option in filteredOptions)
{
if (option.AdvancedRole is not null && option.AdvancedRole != role.GetType())
{
continue;
}
var newOpt = option.CreateOption(
__instance.checkboxOrigin,
__instance.numberOptionOrigin,
@@ -470,8 +469,8 @@ public static class RoleSettingMenuPatches
roleOptionSetting.titleText.horizontalAlignment = HorizontalAlignmentOptions.Left;
if (GameSettingMenuPatches.SelectedMod is null ||
GameSettingMenuPatches.SelectedMod.Options.Exists(
x => x.AdvancedRole != null && x.AdvancedRole.IsInstanceOfType(role)))
GameSettingMenuPatches.SelectedMod.OptionGroups
.Exists(x => x.OptionableType == role.GetType()))
{
var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform);
newButton.name = "ConfigButton";
@@ -504,7 +503,7 @@ public static class RoleSettingMenuPatches
if (index < GameSettingMenuPatches.SelectedMod?.CustomRoles.Count - 1)
{
ScrollerNum += -0.43f;
ScrollerNum -= 0.43f;
}
return roleOptionSetting;
+20 -2
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
@@ -49,6 +50,23 @@ public sealed class MiraPluginManager
continue;
}
foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public))
{
var eventAttribute = method.GetCustomAttribute<RegisterEventAttribute>();
if (eventAttribute != null)
{
var parameters = method.GetParameters();
if (parameters.Length != 1 || !parameters[0].ParameterType.IsSubclassOf(typeof(MiraEvent)))
{
Logger<MiraApiPlugin>.Error($"Invalid event registration method {method.Name} in {type.Name}");
continue;
}
var paramType = parameters[0].ParameterType;
MiraEventManager.RegisterEventHandler(paramType, method, eventAttribute.Priority);
}
}
if (RegisterModifier(type, info))
{
continue;
@@ -61,7 +79,7 @@ public sealed class MiraPluginManager
if (RegisterRoleAttribute(type, info, out var role))
{
roles.Add(role!);
roles.Add(role);
continue;
}
@@ -155,7 +173,7 @@ public sealed class MiraPluginManager
return false;
}
private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, out Type? role)
private static bool RegisterRoleAttribute(Type type, MiraPluginInfo pluginInfo, [NotNullWhen(true)] out Type? role)
{
role = null;
try
+4 -3
View File
@@ -1,5 +1,6 @@
using System.Text;
using BepInEx.Configuration;
using MiraAPI.GameOptions;
using MiraAPI.Modifiers;
using MiraAPI.PluginLoading;
using MiraAPI.Utilities;
@@ -11,7 +12,7 @@ namespace MiraAPI.Roles;
/// <summary>
/// Interface for custom roles.
/// </summary>
public interface ICustomRole
public interface ICustomRole : IOptionable
{
/// <summary>
/// Gets the name of the role.
@@ -46,7 +47,7 @@ public interface ICustomRole
/// <summary>
/// Gets the role options group.
/// </summary>
public RoleOptionsGroup RoleOptionsGroup => Team switch
RoleOptionsGroup RoleOptionsGroup => Team switch
{
ModdedRoleTeams.Crewmate => RoleOptionsGroup.Crewmate,
ModdedRoleTeams.Impostor => RoleOptionsGroup.Impostor,
@@ -57,7 +58,7 @@ public interface ICustomRole
/// <summary>
/// Gets the role's TeamIntroCutscene configuration.
/// </summary>
public TeamIntroConfiguration? IntroConfiguration => Team switch
TeamIntroConfiguration? IntroConfiguration => Team switch
{
ModdedRoleTeams.Custom => TeamIntroConfiguration.Neutral,
_ => null,
+10
View File
@@ -39,6 +39,16 @@ public static class Extensions
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
/// <summary>
/// Determines if a float is an integer.
/// </summary>
/// <param name="number">The float number.</param>
/// <returns>True if the float is an integer, false otherwise.</returns>
public static bool IsInteger(this float number)
{
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <summary>
/// Gets the best constructor for a type based on the specified arguments.
/// </summary>