Merge pull request #110 from rewalo/wiki-preset-fixes

Wiki preset fixes
This commit is contained in:
Sameer Umar authored and GitHub committed 2026-10-11 00:42:13 -04:00
commit 362cfc02ff
13 files changed
+1298 -1061

No files matched your search

+7 -48
View File
@@ -9,8 +9,6 @@ using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Modifiers;
@@ -113,54 +111,15 @@ public abstract class BaseModifier : IOptionable
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
public virtual GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent)
{
parent.ScrollToTop();
var obj = Helpers.CreateAdvancedWikiTab(
var abilities = WikiAbilities;
return Helpers.CreateAdvancedWikiPage(
guide,
ModifierNameLocale,
ModifierName + $" ({ModifierCategoryTitle})",
ModifierWikiDescription,
titleText,
out var desc);
var num = 0;
if (WikiAbilities.Count != 0)
{
var grid = Object.Instantiate(parent.Inner, obj.transform);
var layoutGroup = grid.GetComponent<GridLayoutGroup>();
layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical;
layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
layoutGroup.spacing = new Vector2(0.75f, 0.4f);
grid.DestroyChildren();
foreach (var ability in WikiAbilities)
{
num++;
var panel = Object.Instantiate(
guide.MatchInfoRolePanelPrefab,
grid);
panel.roleCount.text = ability.AbilityType;
panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleName.text = ability.Name;
panel.roleName.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleDescription.text = ability.Description;
panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f);
panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f);
panel.roleIcon.sprite = ability.Icon.LoadAsset();
panel.roleIcon.SetSizeLimit(0.13f);
panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f);
}
grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f);
grid.localScale = new Vector3(1.3f, 1.3f, 1);
}
obj.transform.SetParent(parent.Inner.transform);
obj.transform.localPosition = new Vector3(0f, 0f, 0f);
parent.SetYBoundsMax(Mathf.Clamp(desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f, 0f, 999f));
return obj;
parent,
ModifierNameLocale,
$"{this.GetTmpIcon()} {ModifierName} ({ModifierCategoryTitle})".TrimStart(),
ModifierWikiDescription,
abilities);
}
/// <summary>
+44 -70
View File
@@ -618,6 +618,8 @@ internal static class GameOptionsMenuPatch
{
menu.MapPicker.gameObject.SetActive(false);
var mod = MenuState.Instance.CurrentMod;
var container = MenuState.Instance.CurrentContainer.transform;
PresetManager.DefaultPresets.TryGetValue(mod, out var defaultPreset);
var filteredGroups = MenuState.Instance.CurrentMenu switch
{
@@ -643,15 +645,13 @@ internal static class GameOptionsMenuPatch
{
foreach (var group in filteredGroups)
{
yield return CoCreateGroup(menu, group);
yield return CoCreateGroup(menu, group, container, defaultPreset);
}
}
}
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group)
private static IEnumerator CoCreateGroup(GameOptionsMenu menu, AbstractOptionGroup group, Transform container, OptionPreset? defaultPreset)
{
var container = MenuState.Instance.CurrentContainer.transform;
group.Ready = false;
if (group.Header == null || !group.Header)
{
@@ -713,12 +713,6 @@ internal static class GameOptionsMenuPatch
menu.playerOptionOrigin,
container));
OptionPreset? defaultPreset = null;
if (PresetManager.DefaultPresets.TryGetValue(MenuState.Instance.CurrentMod, out var preset))
{
defaultPreset = preset;
}
yield return options.CoLoopWithBudget(newOpt =>
{
newOpt.SetClickMask(menu.ButtonClickMask);
@@ -765,73 +759,53 @@ internal static class GameOptionsMenuPatch
}
menu.Children.Add(newOpt);
var resetBtn = new GameObject("ResetOption")
if (defaultPreset != null && defaultPreset.IsOptionInPreset(newOpt) &&
newOpt is ToggleOption or NumberOption or StringOption)
{
transform =
var resetBtn = new GameObject("ResetOption")
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
if (newOpt is ToggleOption toggleOpt)
{
transform =
{
parent = newOpt.transform,
localScale = new(.5f, .5f, 1),
localPosition = new Vector3(-3.1f, 0f, -2f),
},
layer = LayerMask.NameToLayer("UI"),
};
var resetRend = resetBtn.AddComponent<SpriteRenderer>();
resetRend.sprite = MiraAssets.ResetButton.LoadAsset();
resetRend.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
var resetBoxCol = resetBtn.gameObject.AddComponent<BoxCollider2D>();
resetBoxCol.size = new Vector2(1f, 1f);
resetBoxCol.offset = new Vector2(0, 0);
var passiveButton = resetBtn.AddComponent<PassiveButton>();
passiveButton.OnClick = new Button.ButtonClickedEvent();
passiveButton.ClickSound = menu.BackButton.GetComponent<PassiveButton>().ClickSound;
passiveButton.OnMouseOver = new UnityEvent();
passiveButton.OnMouseOver.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor != MiraApiPlugin.DefaultHeaderColor
? group.GroupColor
: MiraAssets.AcceptedTeal;
}));
passiveButton.OnMouseOut = new UnityEvent();
passiveButton.OnMouseOut.AddListener(
(UnityAction)(() =>
{
resetRend.color = group.GroupColor.Equals(MiraApiPlugin.DefaultHeaderColor)
? Color.white
: group.GroupColor.FindAlternateColor();
}));
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(toggleOpt);
defaultPreset.ResetOption(newOpt);
}));
}
else if (newOpt is NumberOption numOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(numOpt);
}));
}
else if (newOpt is StringOption strOpt)
{
passiveButton.OnClick.AddListener(
(UnityAction)(() =>
{
defaultPreset!.ResetOption(strOpt);
}));
}
if (!defaultPreset!.IsOptionInPreset(newOpt))
{
resetBtn.Destroy();
}
newOpt.Initialize();
newOpt.gameObject.SetActive(false);
@@ -4,7 +4,6 @@ using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities;
using UnityEngine;
using Object = Il2CppSystem.Object;
namespace MiraAPI.Patches.Options;
File diff suppressed because it is too large. Load diff
+8 -3
View File
@@ -70,7 +70,12 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <param name="baseOption">The option to reset.</param>
public void ResetOption(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
if (selectedOpt == null)
{
return;
}
selectedOpt.LoadFromPreset(PresetConfig);
/*ModdedOptionsManager.SyncAllOptions();
@@ -84,7 +89,7 @@ public class OptionPreset(string name, MiraPluginInfo plugin, ConfigFile presetC
/// <returns>The value of whether the option is in the preset.</returns>
public bool IsOptionInPreset(OptionBehaviour baseOption)
{
var selectedOpt = Plugin.InternalOptions.First(x => x.OptionBehaviour == baseOption);
return selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
var selectedOpt = Plugin.InternalOptions.FirstOrDefault(x => x.OptionBehaviour == baseOption);
return selectedOpt != null && selectedOpt.ConfigDefinition != null && PresetConfig.ContainsKey(selectedOpt.ConfigDefinition);
}
}
+1
View File
@@ -55,4 +55,5 @@ Outlive the timer or eliminate the crew.</string>
<string name="Options">Options</string>
<string name="MiraApi.Wiki.PlayersTab">Player List</string>
<string name="MiraApi.Wiki.SettingsTab">Game Settings</string>
<string name="MiraApi.Wiki.SearchPlaceholder">Search...</string>
</resources>
+5 -1
View File
@@ -57,6 +57,7 @@ public static class CustomRoleManager
internal static readonly Dictionary<ushort, RoleBehaviour> CustomRoles = [];
internal static readonly Dictionary<Type, ushort> RoleIds = [];
internal static readonly Dictionary<Type, MiraPluginInfo> RoleParentMods = [];
private static CppCollections.List<BaseGameSetting>? _emptySettings;
private static Il2CppReferenceArray<OverlayKillAnimation>? _emptyKillAnimations;
@@ -105,6 +106,7 @@ public static class CustomRoleManager
}
pluginInfo.InternalRoles.Add((ushort)role.Role, role);
RoleParentMods[roleType] = pluginInfo;
}
catch (Exception ex)
{
@@ -211,7 +213,9 @@ public static class CustomRoleManager
/// <returns>A <see cref="MiraPluginInfo"/> object representing the parent mod of the role.</returns>
public static MiraPluginInfo FindParentMod(ICustomRole role)
{
return MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
return RoleParentMods.TryGetValue(role.GetType(), out var mod)
? mod
: MiraPluginManager.Instance.RegisteredPlugins.First(plugin => plugin.InternalRoles.ContainsValue(role as RoleBehaviour ?? throw new InvalidOperationException()));
}
/// <summary>
+7 -48
View File
@@ -12,8 +12,6 @@ using MiraAPI.Utilities;
using MiraAPI.Utilities.Assets;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace MiraAPI.Roles;
@@ -324,54 +322,15 @@ public interface ICustomRole : IOptionable
/// <returns>The <see cref="GameObject"/> of the wiki.</returns>
GameObject GetAdvancedWiki(MatchInfoGuide guide, TextMeshPro titleText, Scroller parent)
{
parent.ScrollToTop();
var obj = Helpers.CreateAdvancedWikiTab(
var abilities = WikiAbilities;
return Helpers.CreateAdvancedWikiPage(
guide,
RoleNameLocale,
RoleName + $" ({RoleFactionTitle})",
RoleWikiDescription,
titleText,
out var desc);
var num = 0;
if (WikiAbilities.Count != 0)
{
var grid = Object.Instantiate(parent.Inner, obj.transform);
var layoutGroup = grid.GetComponent<GridLayoutGroup>();
layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical;
layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
layoutGroup.spacing = new Vector2(0.75f, 0.4f);
grid.DestroyChildren();
foreach (var ability in WikiAbilities)
{
num++;
var panel = Object.Instantiate(
guide.MatchInfoRolePanelPrefab,
grid);
panel.roleCount.text = ability.AbilityType;
panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleName.text = ability.Name;
panel.roleName.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleDescription.text = ability.Description;
panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f);
panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f);
panel.roleIcon.sprite = ability.Icon.LoadAsset();
panel.roleIcon.SetSizeLimit(0.13f);
panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f);
}
grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f);
grid.localScale = new Vector3(1.3f, 1.3f, 1);
}
obj.transform.SetParent(parent.Inner.transform);
obj.transform.localPosition = new Vector3(0f, 0f, 0f);
parent.SetYBoundsMax(Mathf.Clamp((desc.textBounds.size.y - 2) + (Mathf.Ceil(num / 2f) * 1.45f), 0f, 999f));
return obj;
parent,
RoleNameLocale,
$"{this.GetTmpIcon()} {RoleName} ({RoleFactionTitle})",
RoleWikiDescription,
abilities);
}
/// <summary>
+1 -1
View File
@@ -19,7 +19,7 @@ public static class MiraAssets
new Vector2(0.5f, 0.5f),
100f,
0U,
SpriteMeshType.Tight,
SpriteMeshType.FullRect,
new Vector4(20, 20, 20, 20));
RoundedBox = new LoadableAssetWrapper<Sprite>(boxSprite);
+7 -1
View File
@@ -48,7 +48,13 @@ public static class SpriteTools
public static Sprite LoadSpriteFromPath(string resourcePath, Assembly assembly, float pixelsPerUnit)
{
var tex = LoadTextureFromResourcePath(resourcePath, assembly);
var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), pixelsPerUnit);
var sprite = Sprite.Create(
tex,
new Rect(0, 0, tex.width, tex.height),
new Vector2(0.5f, 0.5f),
pixelsPerUnit,
0,
SpriteMeshType.FullRect);
sprite.name = resourcePath;
return sprite;
}
+1 -1
View File
@@ -242,7 +242,7 @@ public static class Extensions
public static IEnumerator CoLoopWithBudget<T>(this IEnumerable<T> collection, Action<T> action)
{
var fps = Application.targetFrameRate > 0 ? Application.targetFrameRate : 60;
long budget = 1 / (fps * 2); // default is half of current frame time.
var budget = 1000L / (fps * 2); // default is half of current frame time.
yield return collection.CoLoopWithBudget(budget, action);
}
+190 -91
View File
@@ -6,16 +6,19 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using HarmonyLib;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using MiraAPI.GameOptions;
using MiraAPI.GameOptions.OptionTypes;
using MiraAPI.Modifiers;
using MiraAPI.Roles;
using MiraAPI.Translation;
using MiraAPI.Utilities.Assets;
using Rewired;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using MethodBase = System.Reflection.MethodBase;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
@@ -27,83 +30,67 @@ namespace MiraAPI.Utilities;
/// </summary>
public static class Helpers
{
private static readonly Regex TrailingZeroRegex = new(@"\.0+(?!\d)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
public static ReadOnlyCollection<IModdedOption>? GetModdedOptionsForType(Type classType)
{
var optionGroups =
AccessTools.Field(typeof(ModdedOptionsManager), "Groups").GetValue(null) as List<AbstractOptionGroup>;
var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType);
return optionGroups?.FirstOrDefault(x => x.OptionableType == classType)?.Children;
return group?.Children;
}
public static string GetOptionsText(Type classType)
{
var options = GetModdedOptionsForType(classType);
var group = ModdedOptionsManager.Groups.FirstOrDefault(x => x.OptionableType == classType);
var options = group?.Children;
var summaryProvider = group as ISummarizedOptions;
var hiddenKeys = summaryProvider?.WikiHiddenOptionKeys;
if (options == null)
{
return string.Empty;
}
ISummarizedOptions? summaryProvider;
IReadOnlySet<StringNames>? hiddenKeys;
try
{
var optionGroups =
AccessTools.Field(typeof(ModdedOptionsManager), "Groups").GetValue(null) as List<AbstractOptionGroup>;
summaryProvider =
optionGroups?.FirstOrDefault(x => x.OptionableType == classType) as ISummarizedOptions;
hiddenKeys = summaryProvider?.WikiHiddenOptionKeys;
}
catch
{
summaryProvider = null;
hiddenKeys = null;
}
var builder = new StringBuilder();
builder.AppendLine(
MiraApiPlugin.Culture,
$"\n<size=50%> \n</size><b><color=#29D7B5>{MiraLocaleManager.Get("Options")}</color></b>");
var lines = new List<string>();
var insertedSummary = false;
foreach (var option in options)
{
if (!insertedSummary && summaryProvider != null && hiddenKeys != null)
StringNames? key = option switch
{
StringNames? key = option switch
{
ModdedToggleOption t => t.StringName,
ModdedEnumOption e => e.StringName,
ModdedNumberOption n => n.StringName,
_ => null,
};
if (key.HasValue && hiddenKeys.Contains(key.Value))
{
foreach (var line in summaryProvider.GetWikiOptionSummaryLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
builder.AppendLine(line);
}
}
ModdedToggleOption t => t.StringName,
ModdedEnumOption e => e.StringName,
ModdedNumberOption n => n.StringName,
_ => null,
};
insertedSummary = true;
if (!insertedSummary && summaryProvider != null && hiddenKeys != null &&
key.HasValue && hiddenKeys.Contains(key.Value))
{
foreach (var line in summaryProvider.GetWikiOptionSummaryLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
lines.Add(line);
}
}
insertedSummary = true;
}
if (!option.Visible())
{
continue;
}
if (key.HasValue && hiddenKeys?.Contains(key.Value) == true)
{
continue;
}
switch (option)
{
case ModdedToggleOption toggleOption:
if (!toggleOption.Visible())
{
continue;
}
if (hiddenKeys != null && hiddenKeys.Contains(toggleOption.StringName))
{
continue;
}
builder.AppendLine(
lines.Add(
TranslationController.Instance.GetString(toggleOption.StringName) + ": " +
toggleOption.Value);
break;
@@ -116,65 +103,49 @@ public static class Helpers
builder.AppendLine(enumOption.Title + ": " + enumOption.Values[enumOption.Value]);
break;*/
case ModdedEnumOption enumOption:
if (!enumOption.Visible())
{
continue;
}
if (hiddenKeys != null && hiddenKeys.Contains(enumOption.StringName))
{
continue;
}
builder.AppendLine(
lines.Add(
TranslationController.Instance.GetString(enumOption.StringName) + ": " +
MiraLocaleManager.Get(
enumOption.Values[enumOption.Value],
enumOption.Values[enumOption.Value]));
break;
case ModdedNumberOption numberOption:
if (!numberOption.Visible())
{
continue;
}
if (hiddenKeys != null && hiddenKeys.Contains(numberOption.StringName))
{
continue;
}
var optionStr = numberOption.Data.GetValueString(numberOption.Value);
if (optionStr.Contains(".000"))
{
optionStr = optionStr.Replace(".000", string.Empty);
}
else if (optionStr.Contains(".00"))
{
optionStr = optionStr.Replace(".00", string.Empty);
}
else if (optionStr.Contains(".0"))
{
optionStr = optionStr.Replace(".0", string.Empty);
}
var optionStr = TrailingZeroRegex.Replace(
numberOption.Data.GetValueString(numberOption.Value),
string.Empty);
var title = TranslationController.Instance.GetString(numberOption.StringName);
if (numberOption is { NegativeWordValue: not "#", Value: -1 })
{
builder.AppendLine(title + $": {numberOption.NegativeWordValue}");
lines.Add(title + $": {numberOption.NegativeWordValue}");
}
else if (numberOption is { ZeroWordValue: not "#", Value: 0 })
{
builder.AppendLine(title + $": {numberOption.ZeroWordValue}");
lines.Add(title + $": {numberOption.ZeroWordValue}");
}
else
{
builder.AppendLine(title + ": " + optionStr);
lines.Add(title + ": " + optionStr);
}
break;
}
}
if (lines.Count == 0)
{
return string.Empty;
}
var builder = new StringBuilder();
builder.AppendLine(
MiraApiPlugin.Culture,
$"\n<size=50%> \n</size><b><color=#29D7B5>{MiraLocaleManager.Get("Options")}</color></b>");
foreach (var line in lines)
{
builder.AppendLine(line);
}
return builder.ToString();
}
@@ -718,6 +689,40 @@ public static class Helpers
return role is ICustomRole custom ? custom.RoleLongDescription : role.BlurbLong;
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a role, matching how role names are shown in-game.
/// Uses the role's <see cref="CustomRoleConfiguration.IconTmp"/> if set, otherwise falls back to the faction icon.
/// </summary>
/// <param name="role">The <see cref="RoleBehaviour"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag.</returns>
public static string GetTmpIcon(this RoleBehaviour role)
{
return role is ICustomRole custom ? custom.GetTmpIcon() : $"<sprite name=\"AmongUs.Role.{role.Role}\">";
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a custom role, matching how role names are shown in-game.
/// Uses the role's <see cref="CustomRoleConfiguration.IconTmp"/> if set, otherwise falls back to the faction icon.
/// </summary>
/// <param name="role">The <see cref="ICustomRole"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag.</returns>
public static string GetTmpIcon(this ICustomRole role)
{
return role.Configuration.IconTmp
? $"<sprite name=\"{role.Configuration.IconTmp.name}\">"
: $"<sprite name=\"AmongUs.Role.{role.Team}\">";
}
/// <summary>
/// Returns the TextMeshPro sprite tag for a modifier, or an empty string if it has no <see cref="BaseModifier.IconTmp"/>.
/// </summary>
/// <param name="modifier">The <see cref="BaseModifier"/>.</param>
/// <returns>A <c>&lt;sprite&gt;</c> rich text tag, or <see cref="string.Empty"/>.</returns>
public static string GetTmpIcon(this BaseModifier modifier)
{
return modifier.IconTmp ? $"<sprite name=\"{modifier.IconTmp.name}\">" : string.Empty;
}
/// <summary>
/// Returns whether the <see cref="RoleBehaviour"/> is blacklisted from appearing and spawning. (Only applicable to vanilla roles).
/// </summary>
@@ -743,6 +748,7 @@ public static class Helpers
public static GameObject CreateAdvancedWikiTab(MatchInfoGuide guide, string objName, string title, string description, TextMeshPro titleTmp, out TextMeshPro descriptionTmp)
{
var obj = new GameObject(objName);
titleTmp.richText = true;
titleTmp.text = title;
descriptionTmp = Object.Instantiate(guide.MatchInfoRolePanelPrefab.roleCount, obj.transform);
descriptionTmp.fontSizeMin = descriptionTmp.fontSizeMax = descriptionTmp.fontSize = 2f;
@@ -757,4 +763,97 @@ public static class Helpers
descriptionTmp.ForceMeshUpdate();
return obj;
}
/// <summary>
/// Creates a full advanced wiki page with a title, a description, and an optional grid of ability panels.
/// </summary>
/// <param name="guide">The guide object.</param>
/// <param name="titleTmp">The <see cref="TextMeshPro"/> instance of the title.</param>
/// <param name="parent">The <see cref="Scroller"/> the page should be parented to.</param>
/// <param name="objName">The name of the page object.</param>
/// <param name="title">The title of the page.</param>
/// <param name="description">The description of the page.</param>
/// <param name="abilities">The abilities to display in a grid below the description.</param>
/// <param name="variableTextSizing">Whether to use variable text sizing for ability descriptions (allows different heights).</param>
/// <returns>The <see cref="GameObject"/> of the wiki page.</returns>
public static GameObject CreateAdvancedWikiPage(
MatchInfoGuide guide,
TextMeshPro titleTmp,
Scroller parent,
string objName,
string title,
string description,
IReadOnlyCollection<AdvancedWikiAbilityDescription> abilities,
bool variableTextSizing = false)
{
parent.ScrollToTop();
var obj = CreateAdvancedWikiTab(
guide,
objName,
title,
description,
titleTmp,
out var desc);
var num = 0;
var maxTextSize = 0f;
if (abilities.Count != 0)
{
var grid = Object.Instantiate(parent.Inner, obj.transform);
var layoutGroup = grid.GetComponent<GridLayoutGroup>();
layoutGroup.startAxis = GridLayoutGroup.Axis.Vertical;
layoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
layoutGroup.spacing = new Vector2(0.75f, 0.4f);
grid.DestroyChildren();
foreach (var ability in abilities)
{
num++;
var panel = Object.Instantiate(guide.MatchInfoRolePanelPrefab, grid);
SetupAbilityPanel(panel, ability, variableTextSizing);
if (variableTextSizing)
{
var descSize = panel.roleDescription.textBounds.size.y;
if (maxTextSize < descSize)
{
maxTextSize = descSize;
}
}
}
grid.localPosition = new Vector3(-3.9f, 1.1f - desc.textBounds.size.y, 0f);
grid.localScale = new Vector3(1.3f, 1.3f, 1);
}
obj.transform.SetParent(parent.Inner.transform);
obj.transform.localPosition = new Vector3(0f, 0f, 0f);
var scrollHeight = variableTextSizing
? (desc.textBounds.size.y - 2) + maxTextSize + 0.475f
: desc.textBounds.size.y - 2 + Mathf.Ceil(num / 2f) * 1.45f;
parent.SetYBoundsMax(Mathf.Clamp(scrollHeight, 0f, 999f));
return obj;
}
private static void SetupAbilityPanel(MatchInfoRolePanel panel, AdvancedWikiAbilityDescription ability, bool variableTextSizing = false)
{
panel.roleCount.text = ability.AbilityType;
panel.roleCount.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleName.text = ability.Name;
panel.roleName.transform.localPosition += new Vector3(-0.04f, 0);
panel.roleDescription.text = ability.Description;
panel.roleDescription.rectTransform.sizeDelta = new Vector2(2.601f, 0.8f);
panel.roleDescription.transform.localPosition += new Vector3(0, -0.1f);
if (variableTextSizing)
{
panel.roleDescription.alignment = TextAlignmentOptions.Top;
panel.roleDescription.fontSizeMin = 1.5f;
panel.roleDescription.ForceMeshUpdate();
}
panel.roleIcon.sprite = ability.Icon.LoadAsset();
panel.roleIcon.SetSizeLimit(0.13f);
panel.roleIcon.material.SetInt(PlayerMaterial.MaskLayer, 50);
panel.roleName.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleDescription.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleCount.fontMaterial.SetFloat(panel.STENCIL_NAME, 50f);
panel.roleIcon.transform.localScale = new Vector3(3.5f, 3.5f, 1f);
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using Reactor.Utilities.Attributes;
using UnityEngine;
namespace MiraAPI.Utilities;
/// <summary>
/// A spinning ring used to indicate that content is loading.
/// </summary>
[RegisterInIl2Cpp]
public class LoadingRing(IntPtr cppPtr) : MonoBehaviour(cppPtr)
{
private static Sprite? _ringSprite;
/// <summary>
/// Gets or sets the rotation speed in degrees per second.
/// </summary>
public float Speed { get; set; } = 270f;
/// <summary>
/// Creates a <see cref="LoadingRing"/> on the UI layer.
/// </summary>
/// <param name="parent">The transform to parent the spinner to.</param>
/// <param name="localPosition">The local position of the spinner.</param>
/// <param name="scale">The uniform scale of the spinner.</param>
/// <returns>The created <see cref="LoadingRing"/>.</returns>
public static LoadingRing Create(Transform parent, Vector3 localPosition, float scale)
{
if (!_ringSprite)
{
_ringSprite = CreateRingSprite();
}
var obj = new GameObject("LoadingRing")
{
transform =
{
parent = parent,
localPosition = localPosition,
localScale = Vector3.one * scale,
},
layer = LayerMask.NameToLayer("UI"),
};
var renderer = obj.AddComponent<SpriteRenderer>();
renderer.sprite = _ringSprite;
return obj.AddComponent<LoadingRing>();
}
/// <summary>
/// Rotates the ring.
/// </summary>
public void Update()
{
transform.Rotate(0f, 0f, -Speed * Time.deltaTime);
}
private static Sprite CreateRingSprite()
{
const int size = 128;
const float innerRadius = 46f;
const float outerRadius = 60f;
var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
{
filterMode = FilterMode.Bilinear,
wrapMode = TextureWrapMode.Clamp,
};
var pixels = new Color[size * size];
for (var y = 0; y < size; y++)
{
for (var x = 0; x < size; x++)
{
var dx = x + 0.5f - (size / 2f);
var dy = y + 0.5f - (size / 2f);
var radius = Mathf.Sqrt((dx * dx) + (dy * dy));
var ring = Mathf.Clamp01(radius - innerRadius) * Mathf.Clamp01(outerRadius - radius);
var angle = (Mathf.Atan2(dy, dx) + Mathf.PI) / (2f * Mathf.PI);
pixels[(y * size) + x] = new Color(1f, 1f, 1f, ring * Mathf.Lerp(1f, 0.1f, angle));
}
}
texture.SetPixels(pixels);
texture.Apply();
texture.hideFlags = HideFlags.HideAndDontSave;
var sprite = Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);
sprite.hideFlags = HideFlags.HideAndDontSave;
return sprite;
}
}