Compare commits

..
Author SHA1 Message Date
Az ffecd22176 Merge branch 'dev' into id-builder 2026-10-22 20:59:38 -04:00
Az 26541dc07d clear warnings 2026-10-22 20:54:33 -04:00
Atony 2a7570f022 Adjust code that gets roles and modifiers list 2026-10-22 02:01:05 -05:00
Atony 71138c7bbe Display current role and modifiers at top 2026-10-22 01:17:21 -05:00
Atony 2993ee2bbc Resync options upon clicking start
This is necessary to prevent some pretty common option desync that tends to happen. If anyone else can optimize it further or move it somewhere else, go for it.
2026-10-21 23:37:46 -05:00
Atony 79bbebd12a Allow modifiers and roles to override chance text 2026-10-21 17:48:33 -05:00
Az 5021afeee7 improve reflection search 2026-10-18 03:04:04 -04:00
Az 6bfc49af00 rename file 2026-10-18 02:25:04 -04:00
Az 9999c6d871 wip translation id builder 2026-09-25 00:08:43 -04:00
32 changed files with 615 additions and 230 deletions

No files matched your search

@@ -14,8 +14,13 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type
{
internal Delegate? GetChanged(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
@@ -28,8 +33,13 @@ public sealed class ModdedOptionChangedAttribute(string? methodName = null, Type
internal Func<int, Delegate?>? GetListChanged(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, typeof(void), out var hasIndex, valueType))
return null;
@@ -14,8 +14,13 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type?
{
internal Delegate? GetGetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
@@ -28,8 +33,13 @@ public sealed class ModdedOptionGetterAttribute(string? methodName = null, Type?
internal Func<int, Delegate?>? GetListGetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType))
return null;
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using MiraAPI.Utilities;
namespace MiraAPI.GameOptions.Attributes;
@@ -127,7 +128,7 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
}
/// <summary>
/// Resolves the target <see cref="MemberInfo"/> (property or method) and its associated singleton instance getter, if applicable.
/// Resolves the target <see cref="MemberInfo"/> (property, field or method) and its associated singleton instance getter, if applicable.
/// </summary>
/// <param name="group">The option group instance the property belongs to.</param>
/// <param name="property">The option property decorated with this attribute.</param>
@@ -136,16 +137,19 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved member is an instance member on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
/// <returns>The resolved <see cref="MemberInfo"/> if found; otherwise, <see langword="null"/>.</returns>
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "It is safe.")]
protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter)
protected MemberInfo? ResolveMember(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
{
var groupType = group.GetType();
var flags = BindingFlags.Static | BindingFlags.Public;
type = holderType ?? groupType;
MemberName ??= property.Name + defaultSuffix;
type = holderType ?? groupType;
instanceGetter = null;
if (type.IsAssignableTo(typeof(AbstractOptionGroup)))
{
flags |= BindingFlags.Instance;
@@ -158,12 +162,69 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
.GetProperty(nameof(OptionGroupSingleton<>.Instance))!
.GetGetMethod();
}
else
else if (!type.IsStatic())
{
instanceGetter = null;
flags |= BindingFlags.Instance;
var canSearchPrivates = type.GetCustomAttribute<SearchPrivateAttribute>() != null;
if (canSearchPrivates)
flags |= BindingFlags.NonPublic;
var getterFlags = BindingFlags.Static | BindingFlags.Public | (canSearchPrivates ? BindingFlags.NonPublic : BindingFlags.Default);
foreach (var method in type.GetMethods(getterFlags))
{
if (method.GetParameters().Length > 0 || (method.GetCustomAttribute<InstanceGetterAttribute>() == null && method.Name != "GetInstance"))
continue;
instanceGetter = method;
break;
}
if (instanceGetter == null)
{
foreach (var prop in type.GetProperties(getterFlags))
{
if (prop.GetCustomAttribute<InstanceGetterAttribute>() == null && prop.Name != "Instance")
continue;
var getMethod = prop.GetGetMethod(canSearchPrivates);
if (getMethod == null)
continue;
instanceGetter = getMethod;
break;
}
}
if (instanceGetter == null)
{
Error($"No valid static instance getter (method GetInstance or property Instance) found in instanced type {type.Name}. Try renaming the method/property, or decorate it with InstanceGetterAttribute.");
notFound = false;
return null;
}
if (!instanceGetter.ReturnType.IsAssignableTo(type))
{
Error($"Found instance getter {instanceGetter.Name}, but the return type did not match what was expected ({type.Name}).");
notFound = false;
return null;
}
}
else if (type.GetCustomAttribute<SearchPrivateAttribute>() != null)
{
flags |= BindingFlags.NonPublic;
}
var member = type.GetMember(MemberName, flags).FirstOrDefault();
notFound = true;
if (member == null)
return null;
notFound = false;
if (instanceGetter == null)
return member;
@@ -196,10 +257,11 @@ public abstract class ModdedOptionMemberAttribute(string? memberName, Type? hold
/// When this method returns, contains the <see cref="OptionGroupSingleton{T}.Instance"/> getter <see cref="MethodInfo"/>
/// if the resolved method is an instance method on an <see cref="AbstractOptionGroup"/>; otherwise, <see langword="null"/>.
/// </param>
/// <param name="notFound">When this method returns, contains a flag that indicates if the member was not found, as null can be returned from other circumstances too.</param>
/// <returns>The resolved <see cref="MethodInfo"/> if found; otherwise, <see langword="null"/>.</returns>
protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter)
protected MethodInfo? ResolveMethod(AbstractOptionGroup group, PropertyInfo property, out Type type, out MethodInfo? instanceGetter, out bool notFound)
{
return ResolveMember(group, property, out type, out instanceGetter) as MethodInfo;
return ResolveMember(group, property, out type, out instanceGetter, out notFound) as MethodInfo;
}
/// <summary>
@@ -14,8 +14,13 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type?
{
internal Delegate? GetSetter(AbstractOptionGroup group, PropertyInfo property, IModdedOption option)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
var valueType = option.GetValueType();
@@ -28,8 +33,13 @@ public sealed class ModdedOptionSetterAttribute(string? methodName = null, Type?
internal Func<int, Delegate?>? GetListSetter(AbstractOptionGroup group, PropertyInfo property, Type valueType)
{
if (ResolveMethod(group, property, out var type, out var instanceGetter) is not { } method)
if (ResolveMethod(group, property, out var type, out var instanceGetter, out var notFound) is not { } method)
{
if (notFound)
Error($"Method {MemberName} does not exist in {type.FullName}");
return null;
}
if (!ValidateListSignature(method, valueType, out var hasIndex, valueType, valueType))
return null;
@@ -20,9 +20,11 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
internal Func<bool>? GetVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member)
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
{
Error($"Member {MemberName} does not exist in {type.FullName}");
if (notFound)
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
@@ -107,9 +109,11 @@ public sealed class ModdedOptionVisibilityAttribute(string? memberName = null, T
internal Func<int, Delegate?>? GetListVisibility(AbstractOptionGroup group, PropertyInfo property)
{
if (ResolveMember(group, property, out Type type, out var instanceGetter) is not { } member)
if (ResolveMember(group, property, out Type type, out var instanceGetter, out var notFound) is not { } member)
{
Error($"Member {MemberName} does not exist in {type.FullName}");
if (notFound)
Error($"Member {MemberName} does not exist in {type.FullName}");
return null;
}
@@ -0,0 +1,15 @@
using System;
namespace MiraAPI.GameOptions.Attributes;
/// <summary>
/// An attribute used to mark a method or property as one that returns an instance of the class it's declared in.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public sealed class InstanceGetterAttribute : Attribute;
/// <summary>
/// An attribute used to mark a class as one whose reflection search should include private members.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class SearchPrivateAttribute : Attribute;
+5
View File
@@ -77,6 +77,11 @@ public interface IModdedOption
/// </summary>
bool IncludeInPreset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the option should show a notification, toggled on by an overall option synchronization.
/// </summary>
public bool ShowNotification { get; set; }
/// <summary>
/// Gets or sets the <see cref="BepInEx.Configuration.ConfigDefinition"/> for the option, used for BepInEx configuration.
/// </summary>
File renamed without changes.
+4 -3
View File
@@ -365,11 +365,11 @@ public static class ModdedOptionsManager
while (chunks.Count > 0)
{
Rpc<SyncOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, chunks.Dequeue());
Rpc<SyncOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, new MiraOptionNetData(false, chunks.Dequeue()));
}
}
internal static void HandleSyncOptions(NetData[] data)
internal static void HandleSyncOptions(bool showNotifs, NetData[] data)
{
// necessary to disable then re-enable this setting
// we don't know how other plugins handle their configs
@@ -388,6 +388,7 @@ public static class ModdedOptionsManager
continue;
}
option.ShowNotification = showNotifs;
option.HandleNetData(netData.Data);
}
@@ -397,7 +398,7 @@ public static class ModdedOptionsManager
plugin.PluginConfig.SaveOnConfigSet = oldConfigSetting[plugin];
}
if (LobbyInfoPane.Instance)
if (LobbyInfoPane.InstanceExists)
{
LobbyInfoPane.Instance.RefreshPane();
}
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -36,7 +37,7 @@ public class ModdedEnumOption : ModdedOption<int>
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray();
data.Values = Values.Select(x => MiraLocaleManager.GetOrCreateLocaleString(ParentMod!.IdBuilder.CreateEnumOptionId(x, enumType))).ToArray();
data.Index = Value;
Data = data;
@@ -93,13 +94,17 @@ public class ModdedEnumOption : ModdedOption<int>
/// <inheritdoc />
protected override void OnValueChanged(int newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (ShowNotification)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
}
if (!OptionBehaviour)
{
return;
@@ -139,12 +144,18 @@ public class ModdedEnumOption<T> : ModdedOption<T>
var data = ScriptableObject.CreateInstance<StringGameSetting>();
data.Title = StringName;
data.Type = global::OptionTypes.String;
data.Values = Values.Select(MiraLocaleManager.GetOrCreateLocaleString).ToArray();
data.Index = Convert.ToInt32(Value, NumberFormatInfo.InvariantInfo);
Data = data;
}
/// <inheritdoc />
protected override void OnParentModSet(IMiraPlugin plugin)
{
base.OnParentModSet(plugin);
((StringGameSetting)Data).Values = Values.Select(x => MiraLocaleManager.GetOrCreateLocaleString(ParentMod!.IdBuilder.CreateEnumOptionId<T>(x))).ToArray();
}
/// <inheritdoc />
public override OptionBehaviour CreateOption(ToggleOption toggleOpt, NumberOption numberOpt, StringOption stringOpt, PlayerOption playerOpt, Transform container)
{
@@ -196,13 +207,17 @@ public class ModdedEnumOption<T> : ModdedOption<T>
/// <inheritdoc />
protected override void OnValueChanged(T newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (ShowNotification)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(Convert.ToInt32(newValue, NumberFormatInfo.InvariantInfo)),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
}
if (!OptionBehaviour)
{
return;
@@ -199,13 +199,16 @@ public class ModdedNumberOption : ModdedOption<float>
protected override void OnValueChanged(float newValue)
{
Value = Mathf.Clamp(newValue, Min, Max);
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (ShowNotification)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
Data.GetValueString(newValue),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
}
if (OptionBehaviour is NumberOption opt)
{
+26 -27
View File
@@ -4,7 +4,6 @@ using BepInEx.Configuration;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using Reactor.Networking.Rpc;
using UnityEngine;
@@ -14,16 +13,19 @@ namespace MiraAPI.GameOptions.OptionTypes;
/// Represents a modded option.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
public abstract class ModdedOption<T> : IModdedOption
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="includeInPreset">Whether to include the option in the preset.</param>
public abstract class ModdedOption<T>(string title, T defaultValue, bool includeInPreset = true) : IModdedOption
{
/// <inheritdoc />
public uint Id { get; }
public uint Id { get; } = ModdedOptionsManager.NextId;
/// <inheritdoc />
public string Title { get; set; }
public string Title { get; set; } = title;
/// <inheritdoc />
public StringNames StringName { get; }
public StringNames StringName { get; private set; }
/// <inheritdoc />
public BaseGameSetting Data { get; protected set; } = null!;
@@ -36,6 +38,7 @@ public abstract class ModdedOption<T> : IModdedOption
{
if (field != null || value == null) return;
field = value;
OnParentModSet(value);
var entry = field.GetConfigFile().Bind(ConfigDefinition, DefaultValue);
Value = entry.Value;
@@ -57,12 +60,12 @@ public abstract class ModdedOption<T> : IModdedOption
}
}
private T _value;
private T _value = defaultValue;
/// <summary>
/// Gets the default value of the option.
/// </summary>
public T DefaultValue { get; }
public T DefaultValue { get; } = defaultValue;
/// <inheritdoc cref="IModdedOption.Getter"/>
public Func<T, T>? Getter { get; set; }
@@ -74,10 +77,13 @@ public abstract class ModdedOption<T> : IModdedOption
public Action<T>? ChangedEvent { get; set; }
/// <inheritdoc />
public Func<bool> Visible { get; set; }
public Func<bool> Visible { get; set; } = () => true;
/// <inheritdoc />
public bool IncludeInPreset { get; set; }
public bool IncludeInPreset { get; set; } = includeInPreset;
/// <inheritdoc />
public bool ShowNotification { get; set; } = true;
/// <inheritdoc />
public OptionBehaviour? OptionBehaviour { get; protected set; }
@@ -112,23 +118,6 @@ public abstract class ModdedOption<T> : IModdedOption
set => ChangedEvent = value as Action<T>;
}
/// <summary>
/// Initializes a new instance of the <see cref="ModdedOption{T}"/> class.
/// </summary>
/// <param name="title">The option title.</param>
/// <param name="defaultValue">The default value.</param>
/// <param name="includeInPreset">Whether to include the option in the preset.</param>
protected ModdedOption(string title, T defaultValue, bool includeInPreset = true)
{
Id = ModdedOptionsManager.NextId;
Title = title.Translate();
DefaultValue = defaultValue;
Value = defaultValue;
StringName = MiraLocaleManager.GetOrCreateLocaleString(Title);
Visible = DelegateUtilities.AlwaysTrue;
IncludeInPreset = includeInPreset;
}
internal void ValueChanged(OptionBehaviour optionBehaviour)
{
SetValue(GetValueFromOptionBehaviour(optionBehaviour));
@@ -156,10 +145,11 @@ public abstract class ModdedOption<T> : IModdedOption
entry.Value = _value;
}
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [GetNetData()], true);
Rpc<SyncOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, new MiraOptionNetData(true, [GetNetData()]), true);
}
OnValueChanged(_value);
ShowNotification = true;
}
/// <inheritdoc />
@@ -194,6 +184,15 @@ public abstract class ModdedOption<T> : IModdedOption
}
}
/// <summary>
/// An event that is invoked when the option's parent mod as been assigned to the option.
/// </summary>
/// <param name="plugin">The plugin that was assigned.</param>
protected virtual void OnParentModSet(IMiraPlugin plugin)
{
StringName = MiraLocaleManager.GetOrCreateLocaleString(plugin.IdBuilder.CreateOptionTitleId(Title));
}
/// <inheritdoc />
public Type GetValueType()
{
@@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.Networking;
using MiraAPI.PluginLoading;
using MiraAPI.Translation;
using UnityEngine;
using Object = UnityEngine.Object;
@@ -79,7 +80,7 @@ public class ModdedStringOption : ModdedOption<string>
newVals.Insert(index, value);
Values = [.. newVals];
var newStrVals = StringNameValues.ToList();
newStrVals.Insert(index, MiraLocaleManager.GetOrCreateLocaleString(value));
newStrVals.Insert(index, MiraLocaleManager.GetOrCreateLocaleString(ParentMod!.IdBuilder.CreateStringOptionId(value)));
StringNameValues = [.. newStrVals];
}
@@ -101,9 +102,8 @@ public class ModdedStringOption : ModdedOption<string>
return;
}
var strOpt = OptionBehaviour.TryCast<StringOption>();
Warning("OptionBehaviour is valid");
var strOpt = OptionBehaviour.TryCast<StringOption>();
if (strOpt == null)
{
@@ -114,6 +114,16 @@ public class ModdedStringOption : ModdedOption<string>
strOpt.Values = StringNameValues;
strOpt.Value = GetIndex(Value);
Warning("StringOption is valid");
return;
}
/// <inheritdoc />
protected override void OnParentModSet(IMiraPlugin plugin)
{
base.OnParentModSet(plugin);
StringNameValues = [.. Values.Select(x => MiraLocaleManager.GetOrCreateLocaleString(plugin.IdBuilder.CreateStringOptionId(x)))];
((StringGameSetting)Data).Values = StringNameValues;
}
/// <inheritdoc />
@@ -99,13 +99,17 @@ public class ModdedToggleOption : ModdedOption<bool>
/// <inheritdoc />
protected override void OnValueChanged(bool newValue)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
(newValue ? OnValue : OffValue).Translate(),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
if (ShowNotification)
{
ModdedOptionsManager.AddSettingsChangeMessage(
HudManager.Instance.Notifier,
StringName,
(newValue ? OnValue : OffValue).Translate(),
Configuration.PopUpTextColor,
Configuration.PopUpIconTmp,
false);
}
if (!OptionBehaviour)
{
return;
+1 -1
View File
@@ -36,7 +36,7 @@ public partial class MiraApiPlugin : BasePlugin, IMiraPlugin
public static CultureInfo Culture { get; internal set; } = new("en-US");
/// <inheritdoc />
public string OptionsTitleText => "MiraAPI";
public string OptionsTitleText { get; } = "MiraAPI";
/// <inheritdoc />
public bool DisplayOnOptionsMenu => false;
+43
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Il2CppInterop.Runtime.Attributes;
using MiraAPI.GameOptions;
using MiraAPI.PluginLoading;
@@ -96,6 +97,48 @@ public abstract class BaseModifier : IOptionable
/// </summary>
public virtual string ModifierCategoryTitle => MiraLocaleManager.Get("Modifier");
/// <summary>
/// Gets a value indicating whether to exclude the "amount" from the displayed amount text within the role guide.
/// </summary>
public virtual bool ShowModifierAsUnique => false;
/// <summary>
/// Gets the displayed amount and chance of the modifier. Used in the role guide.
/// By default, 0 amount or chance results in a complete 0 amount and chance, so override if necessary.
/// </summary>
/// <param name="amount">The amount of the modifier.</param>
/// <param name="chance">The chance of the modifier.</param>
/// <param name="source">The mod source of the modifier.</param>
/// <returns>The <see cref="string"/> result.</returns>
public virtual string GetAmountText(int amount, int chance, string source)
{
if (amount == 0)
{
chance = 0;
}
if (ShowModifierAsUnique)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0}% ({1})",
chance.ToString(CultureInfo.InvariantCulture),
source);
}
if (chance == 0)
{
amount = 0;
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
chance.ToString(CultureInfo.InvariantCulture),
source);
}
/// <summary>
/// Gets whether the modifier is forcibly shown or disabled in the wiki screen.
/// </summary>
+2 -1
View File
@@ -1,4 +1,5 @@
using MiraAPI.GameModes;
using System.Globalization;
using MiraAPI.GameModes;
using MiraAPI.PluginLoading;
namespace MiraAPI.Modifiers.Types;
+13 -10
View File
@@ -9,29 +9,32 @@ namespace MiraAPI.Networking;
[RegisterCustomRpc((uint)MiraRpc.SyncGameOptions)]
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.)
// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.)
internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, MiraOptionNetData>(plugin, id)
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[]? data)
public override void Write(MessageWriter writer, MiraOptionNetData data)
{
if (data == null)
/*if (data == null)
{
writer.WritePacked(0U);
return;
}
}*/
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
var info = data.Data;
writer.Write(data.ShouldShowNotifications);
writer.WritePacked((uint)info.Length);
foreach (var netData in info)
{
writer.WritePacked(netData.Id);
writer.WriteBytesAndSize(netData.Data);
}
}
public override NetData[] Read(MessageReader reader)
public override MiraOptionNetData Read(MessageReader reader)
{
var shouldShowNotif = reader.ReadBoolean();
var length = reader.ReadPackedUInt32();
var data = new NetData[length];
for (var i = 0; i < length; i++)
@@ -41,16 +44,16 @@ internal sealed class SyncOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCust
data[i] = new NetData(dataId, bytes);
}
return data;
return new MiraOptionNetData(shouldShowNotif, data);
}
public override void Handle(PlayerControl playerControl, NetData[]? data)
public override void Handle(PlayerControl playerControl, MiraOptionNetData data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
ModdedOptionsManager.HandleSyncOptions(data ?? []);
ModdedOptionsManager.HandleSyncOptions(data.ShouldShowNotifications, data.Data ?? []);
}
}
+19 -10
View File
@@ -5,32 +5,41 @@ using Reactor.Networking.Rpc;
namespace MiraAPI.Networking;
internal readonly struct MiraOptionNetData(bool showNotif, NetData[] data)
{
public bool ShouldShowNotifications { get; } = showNotif;
public NetData[] Data { get; } = data;
}
[RegisterCustomRpc((uint)MiraRpc.SyncRoleOptions)]
#pragma warning disable SA1515 // Single-line comment should be preceded by blank line (Justification: ReSharper suppression.)
// ReSharper disable once ClassNeverInstantiated.Global (Justification: Instantiated by Reactor.)
internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, NetData[]>(plugin, id)
internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : PlayerCustomRpc<MiraApiPlugin, MiraOptionNetData>(plugin, id)
#pragma warning restore SA1515 // Single-line comment should be preceded by blank line
{
public override RpcLocalHandling LocalHandling => RpcLocalHandling.None;
public override void Write(MessageWriter writer, NetData[]? data)
public override void Write(MessageWriter writer, MiraOptionNetData data)
{
if (data == null)
/*if (data == null)
{
writer.WritePacked(0U);
return;
}
}*/
writer.WritePacked((uint)data.Length);
foreach (var netData in data)
var info = data.Data;
writer.Write(data.ShouldShowNotifications);
writer.WritePacked((uint)info.Length);
foreach (var netData in info)
{
writer.WritePacked(netData.Id);
writer.WriteBytesAndSize(netData.Data);
}
}
public override NetData[] Read(MessageReader reader)
public override MiraOptionNetData Read(MessageReader reader)
{
var shouldShowNotif = reader.ReadBoolean();
var length = reader.ReadPackedUInt32();
var data = new NetData[length];
for (var i = 0; i < length; i++)
@@ -40,16 +49,16 @@ internal sealed class SyncRoleOptionsRpc(MiraApiPlugin plugin, uint id) : Player
data[i] = new NetData(dataId, bytes);
}
return data;
return new MiraOptionNetData(shouldShowNotif, data);
}
public override void Handle(PlayerControl playerControl, NetData[]? data)
public override void Handle(PlayerControl playerControl, MiraOptionNetData data)
{
if (AmongUsClient.Instance.HostId != playerControl.OwnerId)
{
return;
}
CustomRoleManager.HandleSyncRoleOptions(data ?? []);
CustomRoleManager.HandleSyncRoleOptions(data.ShouldShowNotifications, data.Data ?? []);
}
}
@@ -1,5 +1,7 @@
using HarmonyLib;
using MiraAPI.GameModes;
using MiraAPI.GameOptions;
using MiraAPI.Roles;
namespace MiraAPI.Patches.GameModes;
@@ -55,4 +57,17 @@ internal static class GameStartManagerPatches
__instance.MinPlayers = 1;
}
}
[HarmonyPrefix, HarmonyPatch(nameof(GameStartManager.BeginGame))]
public static void Prefix(GameStartManager __instance)
{
if (!AmongUsClient.Instance.AmHost)
{
return;
}
ModdedOptionsManager.SyncAllOptions();
CustomRoleManager.SyncAllRoleSettings();
GameModeOption.RpcSyncGamemode(PlayerControl.LocalPlayer, GameModeOption.Value);
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ public static class HudManagerPatches
public static IEnumerator CoResizeUI()
{
while (!HudManager.Instance) yield return null;
while (!HudManager.InstanceExists) yield return null;
yield return new WaitForSeconds(0.01f);
ResizeUI(LocalSettingsTabSingleton<MiraApiSettings>.Instance.ButtonUIFactorSlider.Value);
@@ -495,7 +495,7 @@ public static class RoleSettingMenuPatches
if (AmongUsClient.Instance.AmHost)
{
Rpc<SyncRoleOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, [role.GetNetData()], true);
Rpc<SyncRoleOptionsRpc>.Instance.Send(PlayerControl.LocalPlayer, new MiraOptionNetData(true, [role.GetNetData()]), true);
}
GameOptionsManager.Instance.GameHostOptions = GameOptionsManager.Instance.CurrentGameOptions;
+100 -21
View File
@@ -754,7 +754,8 @@ public static class RoleGuidePatches
modifier.ModifierCategoryTitle,
modifier.ParentMod.MiraPlugin.GetAbbreviatedModName(),
null,
modifier));
modifier,
modifier.TypeId));
}
}
@@ -790,7 +791,8 @@ public static class RoleGuidePatches
roleBehaviour.GetCategoryTitle(),
modId,
roleBehaviour,
null));
null,
(uint)roleBehaviour.Role));
}
_roleEntriesMode = activeModeType;
@@ -928,7 +930,7 @@ public static class RoleGuidePatches
}
}
private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier)
private sealed class DetailedPanel(string title, string category, string modId, RoleBehaviour? role, GameModifier? modifier, uint typeId)
{
public MatchInfoRolePanel? Panel { get; set; }
public string Title { get; } = title;
@@ -936,6 +938,7 @@ public static class RoleGuidePatches
public string ModId { get; } = modId;
public RoleBehaviour? Role { get; } = role;
public GameModifier? Modifier { get; } = modifier;
public uint TypeId { get; } = typeId;
public int Amount { get; set; }
public int Chance { get; set; }
public float Likelihood { get; set; }
@@ -1157,7 +1160,9 @@ public static class RoleGuidePatches
entry.Visible = entry.CanShow && MatchesSearch(entry, searchText);
if (entry.Panel)
{
SetPanelCount(entry.Panel!, entry.Amount, entry.Chance, entry.ModId);
dynamic entryType = entry.Modifier != null ? entry.Modifier : entry.Role!;
var text = GetPanelCountText(entryType, entry.Amount, entry.Chance, entry.ModId);
SetPanelCount(entry.Panel!, text);
if (!entry.Visible && entry.Panel!.gameObject.activeSelf)
{
entry.Panel.gameObject.SetActive(false);
@@ -1182,16 +1187,58 @@ public static class RoleGuidePatches
return shown;
}
public static IEnumerable<GameModifier> GetActiveModifiers()
{
return PlayerControl.LocalPlayer.GetModifiers<GameModifier>();
}
public static IEnumerable<RoleBehaviour> GetActiveRoles()
{
IEnumerable<RoleBehaviour> comparisons = [];
var curRole = PlayerControl.LocalPlayer.Data.Role;
comparisons = comparisons.AddItem(curRole);
if (PlayerControl.LocalPlayer && PlayerControl.LocalPlayer.Data && PlayerControl.LocalPlayer.Data.IsDead &&
!comparisons.Any(x => x.Role == PlayerControl.LocalPlayer.Data.RoleWhenAlive.Value))
{
comparisons = comparisons.AddItem(RoleManager.Instance.GetRole(PlayerControl.LocalPlayer.Data.RoleWhenAlive.Value));
}
return comparisons;
}
public static List<uint> GetActiveIds()
{
var activeModifiers = GetActiveModifiers();
List<uint> comparisons = activeModifiers.Select(x => x.TypeId).ToList();
comparisons.AddRange(GetActiveRoles().Select(x => (uint)x.Role));
return comparisons;
}
private static List<DetailedPanel> SortEntries(List<DetailedPanel> entries, string searchText)
{
var activeModifiers = PlayerControl.LocalPlayer.GetModifiers<GameModifier>().Select(x => x.TypeId);
var roles = GetActiveRoles().Select(x => (uint)x.Role).ToList();
List<uint> comparisons = activeModifiers.ToList();
comparisons.AddRange(roles);
var comparer = new BasicGuideComparer(comparisons);
var sorted = entries.Where(p => p.Visible)
.OrderByDescending(p => p.Title.Equals(searchText, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(p =>
p.Title.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase));
return [.. _sortMethod is SortingMethod.Alphabetical
p.Category.Contains(searchText, StringComparison.OrdinalIgnoreCase))
.ThenBy(x => x.Modifier != null ? x.Modifier.TypeId : (uint)x.Role!.Role, comparer);
return
[
.. _sortMethod is SortingMethod.Alphabetical
? sorted.ThenBy(p => p.GetSortKey(_sortGrouping))
: sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))];
: sorted.ThenByDescending(p => p.GetSortKey(_sortGrouping))
];
}
private static bool MatchesSearch(DetailedPanel entry, string searchText)
@@ -1651,10 +1698,15 @@ public static class RoleGuidePatches
public static Sprite GetRandomRoleIcon()
{
var aliveRole = PlayerControl.LocalPlayer.Data?.Role;
if (aliveRole != null && aliveRole.RoleIconSolid != null)
var roles = GetActiveRoles().Shuffle().ToList();
foreach (var role in roles)
{
return aliveRole.RoleIconSolid;
if (role.RoleIconSolid == null)
{
continue;
}
return role.RoleIconSolid;
}
return GetRandomFactionIcon();
@@ -1793,21 +1845,22 @@ public static class RoleGuidePatches
return false;
}
__instance.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}%",
numPerGame.ToString(CultureInfo.InvariantCulture),
chancePerGame);
if (role is ICustomRole customRole)
{
__instance.roleName.text = customRole.RoleName;
__instance.roleDescription.text =
$"<size=60%>{role.GetCategoryTitle()}</size>\n" + customRole.RoleMedDescription;
__instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset();
__instance.roleCount.text += $" ({customRole.ParentMod.MiraPlugin.GetAbbreviatedModName()})";
var text = GetCustomRolePanelCountText(customRole, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(__instance, text);
}
else
{
__instance.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}%",
numPerGame.ToString(CultureInfo.InvariantCulture),
chancePerGame);
__instance.roleName.text = role.NiceName;
__instance.roleDescription.text = $"<size=60%>{role.GetCategoryTitle()}</size>\n" + role.BlurbMed;
__instance.roleIcon.sprite = role.RoleIconColor;
@@ -1823,9 +1876,20 @@ public static class RoleGuidePatches
return false;
}
private static void SetPanelCount(MatchInfoRolePanel panel, int amount, int chance, string modId)
// NOTE: This is specifically meant to be a different method name as to not have the game check this method and the GetPanelCountText method made for roles overall.
private static string GetCustomRolePanelCountText(ICustomRole custom, int amount, int chance, string modId)
{
panel.roleCount.text = string.Format(
return custom.GetAmountText(amount, chance, modId);
}
private static string GetPanelCountText(RoleBehaviour role, int amount, int chance, string modId)
{
if (role is ICustomRole custom)
{
return custom.GetAmountText(amount, chance, modId);
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
@@ -1833,6 +1897,18 @@ public static class RoleGuidePatches
modId);
}
private static string GetPanelCountText(BaseModifier modifier, int amount, int chance, string modId)
{
return modifier.GetAmountText(amount, chance, modId);
}
private static void SetPanelCount(MatchInfoRolePanel panel, string amountChanceText)
{
panel.roleCount.text = string.Format(
CultureInfo.InvariantCulture,
amountChanceText);
}
public static void SetRolePanel(
this MatchInfoRolePanel instance,
RoleBehaviour role,
@@ -1845,14 +1921,16 @@ public static class RoleGuidePatches
instance.roleDescription.text =
$"<size=60%>{role.GetCategoryTitle()}</size>\n" + customRole.RoleMedDescription;
instance.roleIcon.sprite = customRole.Configuration.Icon?.LoadAsset();
SetPanelCount(instance, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
var text = GetCustomRolePanelCountText(customRole, numPerGame, chancePerGame, customRole.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(instance, text);
}
else
{
instance.roleName.text = role.NiceName;
instance.roleDescription.text = $"<size=60%>{role.GetCategoryTitle()}</size>\n" + role.BlurbMed;
instance.roleIcon.sprite = role.RoleIconColor;
SetPanelCount(instance, numPerGame, chancePerGame, "AU");
var text = GetPanelCountText(role, numPerGame, chancePerGame, "AU");
SetPanelCount(instance, text);
}
instance.roleIcon.SetSizeLimit(0.13f);
@@ -1873,7 +1951,8 @@ public static class RoleGuidePatches
instance.roleDescription.text =
$"<size=60%>{modifier.ModifierCategoryTitle}</size>\n" + modifier.ModifierMedDescription;
instance.roleIcon.sprite = modifier.ModifierIcon?.LoadAsset();
SetPanelCount(instance, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName());
var text = GetPanelCountText(modifier, numPerGame, chancePerGame, modifier.ParentMod.MiraPlugin.GetAbbreviatedModName());
SetPanelCount(instance, text);
instance.roleIcon.SetSizeLimit(0.13f);
instance.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
+6
View File
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Linq;
using BepInEx.Configuration;
using MiraAPI.Translation;
namespace MiraAPI.PluginLoading;
@@ -58,6 +59,11 @@ public interface IMiraPlugin
/// </summary>
string CustomOptionMenuTwoDescription => "Apply game settings for this mod!";
/// <summary>
/// Gets the translation id builder for this mod.
/// </summary>
TranslationIdBuilder IdBuilder => TranslationIdBuilder.Default;
/// <summary>
/// Gets the <see cref="ConfigFile"/> for the plugin.
/// </summary>
+1 -40
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="MiraApi.TopRightButtonScaleFactor">Factor de Escala de la IU superior derecha</string>
<string name="MiraApi.WikiOnBottomRow">Botón debajo de la IU principal</string>
<string name="MiraApi.WikiOnBottomRow">Botón de Wiki debajo de la IU principal</string>
<string name="Roles">Roles</string>
<string name="Modifier">Modificador</string>
<string name="Modifiers">Modificadores</string>
@@ -40,43 +40,4 @@ Sobrevive al temporizador o elimina a la Tripulación.</string>
<string name="MiraApi.RoleTeam.Impostor.Group">Roles de Impostor</string>
<string name="MiraApi.RoleTeam.Neutral.Group">Roles Neutrales</string>
<string name="MiraApi.RoleTeam.Other">Otros</string>
<string name="MiraApi.MatchInfoGuide">Guía de Partida</string>
<string name="MiraApi.MatchInfoGuideScaleFactor">Factor de Escala de la Guía de Partida</string>
<string name="MiraApi.WikiTabPosition">Posición de la Pestaña de Botones</string>
<string name="MiraApi.WikiTabPosition.LeftSide">Izquierda</string>
<string name="MiraApi.WikiTabPosition.RightSide">Derecha</string>
<string name="MiraApi.WikiTabPosition.Below">Debajo de la IU</string>
<string name="MiraApi.EnableVariableColors">Activar Colores Variables</string>
<string name="MiraApi.Gamemode">Modo de Juego</string>
<string name="MiraApi.ModifierDisplay.Page">Página</string>
<string name="MiraApi.Page">Página</string>
<string name="MiraApi.Main">Principal</string>
<string name="MiraApi.AbilityType.Basic">Habilidad</string>
<string name="MiraApi.AbilityType.Indirect">Habilidad Indirecta</string>
<string name="MiraApi.Wiki.VanillaSettings">Vanilla</string>
<string name="MiraApi.Wiki.SearchResults">Resultados de búsqueda</string>
<string name="MiraApi.Wiki.PreviousPage">Anterior</string>
<string name="MiraApi.Wiki.NextPage">Siguiente</string>
<string name="MiraApi.Wiki.NoResultsFound">Sin resultados</string>
<string name="On">Sí</string>
<string name="Off">No</string>
<string name="Enabled">Activado</string>
<string name="Disabled">Desactivado</string>
<string name="Active">Activo</string>
<string name="Inactive">Inactivo</string>
<string name="Activated">Activado</string>
<string name="Deactivated">Desactivado</string>
<string name="MiraApi.AbilityType.Interaction">Habilidad de Interacción</string>
<string name="MiraApi.AbilityType.Radius">Habilidad de Radio</string>
<string name="MiraApi.AbilityType.Menu">Habilidad de Menú</string>
<string name="MiraApi.AbilityType.Meeting">Habilidad de Reunión</string>
<string name="MiraApi.AbilityType.SelectiveMeeting">Habilidad Selectiva de Reunión</string>
<string name="MiraApi.AbilityType.MeetingMenu">Habilidad de Menú de Reunión</string>
<string name="Options">Opciones</string>
<string name="MiraApi.Wiki.PlayersTab">Lista de Jugadores</string>
<string name="MiraApi.Wiki.SettingsTab">Opciones de Partida</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Buscar...</string>
<string name="MiraApi.Wiki.FilterLabel">Filtrar</string>
<string name="MiraApi.Wiki.GroupingLabel">Agrupar</string>
<string name="MiraApi.Wiki.OrderLabel">Ordenar</string>
</resources>
+11 -11
View File
@@ -1,14 +1,14 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="MiraApi.TopRightButtonScaleFactor">Échelle de tailles de boutons</string>
<string name="MiraApi.WikiOnBottomRow">Bouton pour le Wiki sous l'interface principale</string>
<string name="Roles">Rôles</string>
<string name="Modifier">Modificateur</string>
<string name="Modifiers">Modificateurs</string>
<string name="MiraApi.RoleTeam.Crewmate">Coéquipier</string>
<string name="MiraApi.RoleTeam.Crewmate.Short">Équipage</string>
<string name="MiraApi.RoleTeam.Impostor">Imposteur</string>
<string name="MiraApi.RoleTeam.Impostor.Short">Impo</string>
<string name="MiraApi.RoleTeam.Neutral">Neutre</string>
<string name="MiraApi.RoleTeam.Neutral.Short">Neutre</string>
<string name="MiraApi.TopRightButtonScaleFactor">Échelle de tailles de boutons</string>
<string name="MiraApi.WikiOnBottomRow">Bouton pour le Wiki sous l'interface principale</string>
<string name="Roles">Rôles</string>
<string name="Modifier">Modificateur</string>
<string name="Modifiers">Modificateurs</string>
<string name="MiraApi.RoleTeam.Crewmate">Coéquipier</string>
<string name="MiraApi.RoleTeam.Crewmate.Short">Équipage</string>
<string name="MiraApi.RoleTeam.Impostor">Imposteur</string>
<string name="MiraApi.RoleTeam.Impostor.Short">Impo</string>
<string name="MiraApi.RoleTeam.Neutral">Neutre</string>
<string name="MiraApi.RoleTeam.Neutral.Short">Neutre</string>
</resources>
+1 -40
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="MiraApi.TopRightButtonScaleFactor">Skala Przycisków w Prawym Górnym</string>
<string name="MiraApi.WikiOnBottomRow">Pokaż Przycisk w Dolnym Rzędzie</string>
<string name="MiraApi.WikiOnBottomRow">Pokaż Wiki w Dolnym Rzędzie</string>
<string name="Roles">Role</string>
<string name="Modifier">Modyfikator</string>
<string name="Modifiers">Modyfikatory</string>
@@ -40,43 +40,4 @@ Przeżyj przez wymagany czas, albo zabij chowających.</string>
<string name="MiraApi.RoleTeam.Impostor.Group">Role Intruzów</string>
<string name="MiraApi.RoleTeam.Neutral.Group">Role Neutralne</string>
<string name="MiraApi.RoleTeam.Other">Inne</string>
<string name="MiraApi.MatchInfoGuide">Informacje O Meczu</string>
<string name="MiraApi.WikiTabPosition.LeftSide">Po Lewo</string>
<string name="MiraApi.WikiTabPosition.RightSide">Po Prawo</string>
<string name="MiraApi.WikiTabPosition.Below">Poniżej UI</string>
<string name="MiraApi.Gamemode">Tryb Gry</string>
<string name="MiraApi.MatchInfoGuideScaleFactor">Skala Menu Z Informacjami O Meczu</string>
<string name="MiraApi.WikiTabPosition">Położenie Przycisków Informacji O Meczu</string>
<string name="MiraApi.ModifierDisplay.Page">Strona</string>
<string name="MiraApi.Page">Strona</string>
<string name="MiraApi.Main">Główne</string>
<string name="MiraApi.AbilityType.Basic">Umiejętność</string>
<string name="MiraApi.AbilityType.Indirect">Umiejętność Pośrednia</string>
<string name="MiraApi.AbilityType.Interaction">Umiejętność Interakcyjna</string>
<string name="MiraApi.AbilityType.Radius">Umiejętność z Zasięgiem</string>
<string name="MiraApi.AbilityType.Menu">Umiejętność z Menu</string>
<string name="MiraApi.AbilityType.Meeting">Umiejętność na Spotkaniu</string>
<string name="MiraApi.AbilityType.SelectiveMeeting">Umiejętność na Spotkaniu z Wyborem</string>
<string name="MiraApi.AbilityType.MeetingMenu">Umiejętność z Menu na Spotkaniu</string>
<string name="Options">Opcję</string>
<string name="MiraApi.Wiki.PlayersTab">Lista Graczy</string>
<string name="MiraApi.Wiki.SettingsTab">Ustawienia Gry</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Wyszukaj...</string>
<string name="MiraApi.Wiki.FilterLabel">Filtruj</string>
<string name="MiraApi.Wiki.OrderLabel">Kolejność</string>
<string name="MiraApi.Wiki.GroupingLabel">Grupuj</string>
<string name="MiraApi.Wiki.VanillaSettings">Vanilla</string>
<string name="MiraApi.Wiki.SearchResults">Wyniki Wyszukiwania</string>
<string name="MiraApi.Wiki.PreviousPage">Poprzednia</string>
<string name="MiraApi.Wiki.NextPage">Następna</string>
<string name="MiraApi.Wiki.NoResultsFound">Brak Wyników</string>
<string name="On">Wł</string>
<string name="Off">Wył</string>
<string name="Enabled">Włączone</string>
<string name="Disabled">Wyłączone</string>
<string name="Active">Aktywne</string>
<string name="Inactive">Nieaktywne</string>
<string name="Activated">Aktywowane</string>
<string name="Deactivated">Dezaktywowane</string>
<string name="MiraApi.EnableVariableColors">Włącz Kolory Zmienne</string>
</resources>
+11 -11
View File
@@ -1,14 +1,14 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="MiraApi.TopRightButtonScaleFactor">Множитель разрешения верх.-правого интерфейса</string>
<string name="MiraApi.WikiOnBottomRow">Кнопка вики под основным интерфейсом</string>
<string name="Roles">Роли</string>
<string name="Modifier">Атрибут</string>
<string name="Modifiers">Атрибуты</string>
<string name="MiraApi.RoleTeam.Crewmate">Член Экипажа</string>
<string name="MiraApi.RoleTeam.Crewmate.Short">Экипаж</string>
<string name="MiraApi.RoleTeam.Impostor">Предатель</string>
<string name="MiraApi.RoleTeam.Impostor.Short">Пред</string>
<string name="MiraApi.RoleTeam.Neutral">Нейтрал</string>
<string name="MiraApi.RoleTeam.Neutral.Short">Нейт</string>
<string name="MiraApi.TopRightButtonScaleFactor">Множитель разрешения верх.-правого интерфейса</string>
<string name="MiraApi.WikiOnBottomRow">Кнопка вики под основным интерфейсом</string>
<string name="Roles">Роли</string>
<string name="Modifier">Атрибут</string>
<string name="Modifiers">Атрибуты</string>
<string name="MiraApi.RoleTeam.Crewmate">Член Экипажа</string>
<string name="MiraApi.RoleTeam.Crewmate.Short">Экипаж</string>
<string name="MiraApi.RoleTeam.Impostor">Предатель</string>
<string name="MiraApi.RoleTeam.Impostor.Short">Пред</string>
<string name="MiraApi.RoleTeam.Neutral">Нейтрал</string>
<string name="MiraApi.RoleTeam.Neutral.Short">Нейт</string>
</resources>
+6 -3
View File
@@ -280,11 +280,11 @@ public static class CustomRoleManager
while (data.Count > 0)
{
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, data.Dequeue());
Rpc<SyncRoleOptionsRpc>.Instance.SendTo(PlayerControl.LocalPlayer, targetId, new MiraOptionNetData(false, data.Dequeue()));
}
}
internal static void HandleSyncRoleOptions(NetData[] data)
internal static void HandleSyncRoleOptions(bool showNotifs, NetData[] data)
{
// necessary to disable then re-enable this setting
// we don't know how other plugins handle their configs
@@ -312,7 +312,10 @@ public static class CustomRoleManager
var num = BitConverter.ToInt32(netData.Data, 0);
var chance = BitConverter.ToInt32(netData.Data, 4);
HudManager.Instance.Notifier.AddRoleSettingsChangeMessage(role.StringName, num, chance, role.TeamType, false);
if (showNotifs)
{
HudManager.Instance.Notifier.AddRoleSettingsChangeMessage(role.StringName, num, chance, role.TeamType, false);
}
try
{
+43
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using BepInEx.Configuration;
using Il2CppInterop.Runtime.Attributes;
@@ -92,6 +93,48 @@ public interface ICustomRole : IOptionable
_ => MiraLocaleManager.Get("MiraApi.RoleTeam.Other"),
};
/// <summary>
/// Gets a value indicating whether to exclude the "amount" from the displayed amount text within the role guide.
/// </summary>
public virtual bool ShowRoleAsUnique => Configuration.MaxRoleCount == 1;
/// <summary>
/// Gets the displayed amount and chance of the role. Used in the role guide.
/// By default, 0 amount or chance results in a complete 0 amount and chance, so override if necessary.
/// </summary>
/// <param name="amount">The amount of the role.</param>
/// <param name="chance">The chance of the role.</param>
/// <param name="source">The mod source of the role.</param>
/// <returns>The <see cref="string"/> result.</returns>
public virtual string GetAmountText(int amount, int chance, string source)
{
if (amount == 0)
{
chance = 0;
}
if (ShowRoleAsUnique)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0}% ({1})",
chance.ToString(CultureInfo.InvariantCulture),
source);
}
if (chance == 0)
{
amount = 0;
}
return string.Format(
CultureInfo.InvariantCulture,
"{0} at {1}% ({2})",
amount.ToString(CultureInfo.InvariantCulture),
chance.ToString(CultureInfo.InvariantCulture),
source);
}
/// <summary>
/// Gets the <see cref="Color"/> of the role.
/// </summary>
@@ -0,0 +1,82 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace MiraAPI.Translation;
/// <summary>
/// A utility class that can build ids based on the provided id part.
/// </summary>
public class TranslationIdBuilder
{
/// <summary>
/// Gets an instance of <see cref="TranslationIdBuilder"/> that has default behaviour.
/// </summary>
public static TranslationIdBuilder Default { get; } = new();
/// <summary>
/// Constructs a translation id for a provided enum option value.
/// </summary>
/// <typeparam name="T">The type of the enum.</typeparam>
/// <param name="value">The enum option value.</param>
/// <returns>The constructed translation id.</returns>
public virtual string CreateEnumOptionId<T>(T value)
where T : Enum
{
return CreateEnumOptionId<T>(value.ToString());
}
/// <summary>
/// Constructs a translation id for a provided enum option value.
/// </summary>
/// <typeparam name="T">The type of the enum.</typeparam>
/// <param name="value">The enum option value.</param>
/// <returns>The constructed translation id.</returns>
public virtual string CreateEnumOptionId<T>(string value)
where T : Enum
{
return CreateEnumOptionId(value, typeof(T));
}
/// <summary>
/// Constructs a translation id for a provided enum option value.
/// </summary>
/// <param name="value">The enum option value.</param>
/// <param name="enumType">The type of the enum.</param>
/// <returns>The constructed translation id.</returns>
public virtual string CreateEnumOptionId(object value, Type enumType)
{
return CreateEnumOptionId(value.ToString()!, enumType);
}
/// <summary>
/// Constructs a translation id for a provided enum option value.
/// </summary>
/// <param name="value">The enum option value.</param>
/// <param name="enumType">The type of the enum.</param>
/// <returns>The constructed translation id.</returns>
public virtual string CreateEnumOptionId(string value, Type enumType)
{
return value;
}
/// <summary>
/// Constructs a translation id for a string option.
/// </summary>
/// <param name="option">The id part.</param>
/// <returns>The constructed translation id.</returns>
[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Just confusing.")]
public virtual string CreateStringOptionId(string option)
{
return option;
}
/// <summary>
/// Constructs a translation id for an option's title.
/// </summary>
/// <param name="title">The id part.</param>
/// <returns>The constructed translation id.</returns>
public virtual string CreateOptionTitleId(string title)
{
return title;
}
}
+31
View File
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MiraAPI.Utilities;
/// <summary>
/// A comparer used to sort role/modifier IDs based on which ones are currently active.
/// </summary>
/// <param name="activeIds">The collection of currently active IDs.</param>
public class BasicGuideComparer(IEnumerable<uint> activeIds) : IComparer<uint>
{
/// <inheritdoc/>
public int Compare(uint x, uint y)
{
var xActive = activeIds.Contains(x);
var yActive = activeIds.Contains(y);
if (xActive && !yActive)
{
return -1;
}
if (!xActive && yActive)
{
return 1;
}
return 0;
}
}