From 3bf066f38c0002d3b052b8f23d5e3eecda12629f Mon Sep 17 00:00:00 2001
From: Az <56699741+AlchlcDvl@users.noreply.github.com>
Date: Tue, 29 Sep 2026 09:53:18 -0400
Subject: [PATCH] add variable color system + color spaces
---
.editorconfig | 3 +
MiraAPI/Colors/ColorSpaces/CmykColor.cs | 103 +++++++++++++++++
MiraAPI/Colors/ColorSpaces/HsbColor.cs | 114 +++++++++++++++++++
MiraAPI/Colors/ColorSpaces/HslColor.cs | 117 ++++++++++++++++++++
MiraAPI/Colors/CustomColor.cs | 2 +-
MiraAPI/Colors/PaletteManager.cs | 56 ++++++++++
MiraAPI/Colors/VariableColor.cs | 69 ++++++++++++
MiraAPI/Colors/VariableColorBehaviour.cs | 46 ++++++++
MiraAPI/MiraApiSettings.cs | 6 +
MiraAPI/Patches/Hud/VariableColorPatches.cs | 98 ++++++++++++++++
MiraAPI/PluginLoading/MiraPluginManager.cs | 5 +-
MiraAPI/Resources/Locale/en_US.xml | 1 +
12 files changed, 618 insertions(+), 2 deletions(-)
create mode 100644 MiraAPI/Colors/ColorSpaces/CmykColor.cs
create mode 100644 MiraAPI/Colors/ColorSpaces/HsbColor.cs
create mode 100644 MiraAPI/Colors/ColorSpaces/HslColor.cs
create mode 100644 MiraAPI/Colors/VariableColor.cs
create mode 100644 MiraAPI/Colors/VariableColorBehaviour.cs
create mode 100644 MiraAPI/Patches/Hud/VariableColorPatches.cs
diff --git a/.editorconfig b/.editorconfig
index af9feb4..c0529a8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -86,3 +86,6 @@ dotnet_diagnostic.SA1133.severity = none
# IDE0001: Simplify Names
dotnet_diagnostic.IDE0001.severity = warning
+
+# IDE0079: Remove unnecessary suppression
+dotnet_diagnostic.IDE0079.severity = none
diff --git a/MiraAPI/Colors/ColorSpaces/CmykColor.cs b/MiraAPI/Colors/ColorSpaces/CmykColor.cs
new file mode 100644
index 0000000..569e848
--- /dev/null
+++ b/MiraAPI/Colors/ColorSpaces/CmykColor.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+
+namespace MiraAPI.Colors.ColorSpaces;
+
+///
+/// A struct that denotes the CMYK (Cyan, Magenta, Yellow, Key) color space.
+///
+/// The cyan component.
+/// The magenta component.
+/// The yellow component.
+/// The key component.
+/// The alpha component.
+[Serializable]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
+public record struct CmykColor(float c, float m, float y, float k, float a = 1f)
+{
+ ///
+ /// Implicitly converts a to a Unity .
+ ///
+ /// The color being converted.
+ public static implicit operator Color(CmykColor c)
+ {
+ return ToColor(c);
+ }
+
+ ///
+ /// Implicitly converts a Unity to a .
+ ///
+ /// The color being converted.
+ public static implicit operator CmykColor(Color c)
+ {
+ return FromColor(c);
+ }
+
+ ///
+ /// Converts a to a standard Unity RGB .
+ ///
+ /// The CMYK color to convert.
+ /// A Unity representing the same visual color.
+ public static Color ToColor(CmykColor cmyk)
+ {
+ var r = (1f - cmyk.c) * (1f - cmyk.k);
+ var g = (1f - cmyk.m) * (1f - cmyk.k);
+ var b = (1f - cmyk.y) * (1f - cmyk.k);
+
+ return new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), Mathf.Clamp01(b), cmyk.a);
+ }
+
+ ///
+ /// Converts a standard Unity RGB to a .
+ ///
+ /// The Unity Color to convert.
+ /// A representing the same visual color.
+ public static CmykColor FromColor(Color color)
+ {
+ var r = color.r;
+ var g = color.g;
+ var b = color.b;
+
+ var k = 1f - Mathf.Max(r, g, b);
+
+ if (k >= 1f)
+ return new CmykColor(0f, 0f, 0f, 1f, color.a);
+
+ var c = (1f - r - k) / (1f - k);
+ var m = (1f - g - k) / (1f - k);
+ var y = (1f - b - k) / (1f - k);
+
+ return new CmykColor(Mathf.Clamp01(c), Mathf.Clamp01(m), Mathf.Clamp01(y), Mathf.Clamp01(k), color.a);
+ }
+
+ ///
+ /// Linearly interpolates between two values.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors, clamped to a 0 to 1 range.
+ /// The interpolated .
+ public static CmykColor Lerp(CmykColor a, CmykColor b, float t)
+ {
+ t = Mathf.Clamp01(t);
+ return LerpUnclamped(a, b, t);
+ }
+
+ ///
+ /// Linearly interpolates between two values without clamping the interpolant.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors.
+ /// The interpolated .
+ public static CmykColor LerpUnclamped(CmykColor a, CmykColor b, float t)
+ {
+ return new CmykColor(
+ Mathf.LerpUnclamped(a.c, b.c, t),
+ Mathf.LerpUnclamped(a.m, b.m, t),
+ Mathf.LerpUnclamped(a.y, b.y, t),
+ Mathf.LerpUnclamped(a.k, b.k, t),
+ Mathf.LerpUnclamped(a.a, b.a, t));
+ }
+}
diff --git a/MiraAPI/Colors/ColorSpaces/HsbColor.cs b/MiraAPI/Colors/ColorSpaces/HsbColor.cs
new file mode 100644
index 0000000..4dec19a
--- /dev/null
+++ b/MiraAPI/Colors/ColorSpaces/HsbColor.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+
+namespace MiraAPI.Colors.ColorSpaces;
+
+///
+/// A struct that denotes the HSB (Hue, Saturation, Brightness) color space.
+///
+/// The hue component.
+/// The saturation component.
+/// The brightness component.
+/// The alpha component.
+[Serializable]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
+public record struct HsbColor(float h, float s, float b, float a = 1f)
+{
+ ///
+ /// Implicitly converts an to a Unity .
+ ///
+ /// The color being converted.
+ public static implicit operator Color(HsbColor c)
+ {
+ return ToColor(c);
+ }
+
+ ///
+ /// Implicitly converts a Unity to an .
+ ///
+ /// The color being converted.
+ public static implicit operator HsbColor(Color c)
+ {
+ return FromColor(c);
+ }
+
+ ///
+ /// Converts an to a standard Unity RGB .
+ ///
+ /// The HSB color to convert.
+ /// A Unity representing the same visual color.
+ public static Color ToColor(HsbColor hsb)
+ {
+ var r = hsb.b;
+ var g = hsb.b;
+ var b = hsb.b;
+
+ if (hsb.s <= Mathf.Epsilon)
+ return new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), Mathf.Clamp01(b), hsb.a);
+
+ var max = hsb.b;
+ var dif = hsb.b * hsb.s;
+ var min = hsb.b - dif;
+
+ var h = hsb.h * 360f;
+
+ (r, g, b) = h switch
+ {
+ < 60f => (max, h * dif / 60f + min, min),
+ < 120f => (-(h - 120f) * dif / 60f + min, max, min),
+ < 180f => (min, max, (h - 120f) * dif / 60f + min),
+ < 240f => (min, -(h - 240f) * dif / 60f + min, max),
+ < 300f => ((h - 240f) * dif / 60f + min, min, max),
+ <= 360f => (max, min, -(h - 360f) * dif / 60f + min),
+ _ => (0f, 0f, 0f),
+ };
+
+ return new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), Mathf.Clamp01(b), hsb.a);
+ }
+
+ ///
+ /// Converts a standard Unity RGB to an .
+ ///
+ /// The Unity Color to convert.
+ /// An representing the same visual color.
+ public static HsbColor FromColor(Color color)
+ {
+ Color.RGBToHSV(color, out var h, out var s, out var v);
+ return new HsbColor(h, s, v, color.a);
+ }
+
+ ///
+ /// Linearly interpolates between two values, using the shortest path for the hue.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors, clamped to a 0 to 1 range.
+ /// The interpolated .
+ public static HsbColor Lerp(HsbColor a, HsbColor b, float t)
+ {
+ t = Mathf.Clamp01(t);
+ return LerpUnclamped(a, b, t);
+ }
+
+ ///
+ /// Linearly interpolates between two values without clamping the interpolant.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors.
+ /// The interpolated .
+ public static HsbColor LerpUnclamped(HsbColor a, HsbColor b, float t)
+ {
+ var hueDifference = Mathf.Repeat(b.h - a.h, 1f);
+
+ if (hueDifference > 0.5f)
+ hueDifference -= 1f;
+
+ return new HsbColor(
+ Mathf.Repeat(a.h + (hueDifference * t), 1f),
+ Mathf.LerpUnclamped(a.s, b.s, t),
+ Mathf.LerpUnclamped(a.b, b.b, t),
+ Mathf.LerpUnclamped(a.a, b.a, t));
+ }
+}
diff --git a/MiraAPI/Colors/ColorSpaces/HslColor.cs b/MiraAPI/Colors/ColorSpaces/HslColor.cs
new file mode 100644
index 0000000..824a032
--- /dev/null
+++ b/MiraAPI/Colors/ColorSpaces/HslColor.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+
+namespace MiraAPI.Colors.ColorSpaces;
+
+///
+/// A struct that denotes the HSL (Hue, Saturation, Lightness) color space.
+///
+/// The hue component.
+/// The saturation component.
+/// The lightness component.
+/// The alpha component.
+[Serializable]
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Unity parity.")]
+public record struct HslColor(float h, float s, float l, float a = 1f)
+{
+ ///
+ /// Implicitly converts an to a Unity .
+ ///
+ /// The color being converted.
+ public static implicit operator Color(HslColor c)
+ {
+ return ToColor(c);
+ }
+
+ ///
+ /// Implicitly converts a Unity to an .
+ ///
+ /// The color being converted.
+ public static implicit operator HslColor(Color c)
+ {
+ return FromColor(c);
+ }
+
+ ///
+ /// Converts an to a standard Unity RGB .
+ ///
+ /// The HSL color to convert.
+ /// A Unity representing the same visual color.
+ public static Color ToColor(HslColor hsl)
+ {
+ if (hsl.s <= Mathf.Epsilon)
+ return new Color(Mathf.Clamp01(hsl.l), Mathf.Clamp01(hsl.l), Mathf.Clamp01(hsl.l), hsl.a);
+
+ var c = (1f - Mathf.Abs(2f * hsl.l - 1f)) * hsl.s;
+ var hPrime = hsl.h * 6f;
+ var x = c * (1f - Mathf.Abs((hPrime % 2f) - 1f));
+ var m = hsl.l - c / 2f;
+
+ var (r, g, b) = hPrime switch
+ {
+ < 1f => (c, x, 0f),
+ < 2f => (x, c, 0f),
+ < 3f => (0f, c, x),
+ < 4f => (0f, x, c),
+ < 5f => (x, 0f, c),
+ < 6f => (c, 0f, x),
+ _ => (0f, 0f, 0f),
+ };
+
+ return new Color(Mathf.Clamp01(r + m), Mathf.Clamp01(g + m), Mathf.Clamp01(b + m), hsl.a);
+ }
+
+ ///
+ /// Converts a standard Unity RGB to an .
+ ///
+ /// The Unity Color to convert.
+ /// An representing the same visual color.
+ public static HslColor FromColor(Color color)
+ {
+ var max = Mathf.Max(color.r, Mathf.Max(color.g, color.b));
+ var min = Mathf.Min(color.r, Mathf.Min(color.g, color.b));
+ var l = (max + min) / 2f;
+ var delta = max - min;
+
+ var s = delta == 0f ? 0f : delta / (1f - Mathf.Abs(2f * l - 1f));
+
+ Color.RGBToHSV(color, out var h, out _, out _);
+
+ return new HslColor(h, s, l, color.a);
+ }
+
+ ///
+ /// Linearly interpolates between two values, using the shortest path for the hue.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors, clamped to a 0 to 1 range.
+ /// The interpolated .
+ public static HslColor Lerp(HslColor a, HslColor b, float t)
+ {
+ t = Mathf.Clamp01(t);
+ return LerpUnclamped(a, b, t);
+ }
+
+ ///
+ /// Linearly interpolates between two values without clamping the interpolant.
+ ///
+ /// The starting color.
+ /// The ending color.
+ /// The interpolation value between the two colors.
+ /// The interpolated .
+ public static HslColor LerpUnclamped(HslColor a, HslColor b, float t)
+ {
+ var hueDifference = Mathf.Repeat(b.h - a.h, 1f);
+
+ if (hueDifference > 0.5f)
+ hueDifference -= 1f;
+
+ return new HslColor(
+ Mathf.Repeat(a.h + (hueDifference * t), 1f),
+ Mathf.LerpUnclamped(a.s, b.s, t),
+ Mathf.LerpUnclamped(a.l, b.l, t),
+ Mathf.LerpUnclamped(a.a, b.a, t));
+ }
+}
diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs
index a778e31..2c60dd5 100644
--- a/MiraAPI/Colors/CustomColor.cs
+++ b/MiraAPI/Colors/CustomColor.cs
@@ -10,7 +10,7 @@ namespace MiraAPI.Colors;
/// The name of the option.
/// The main color.
/// The shadow color.
-public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
+public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor)
{
///
/// Gets or sets the main color.
diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs
index fe7e725..50a0ed0 100644
--- a/MiraAPI/Colors/PaletteManager.cs
+++ b/MiraAPI/Colors/PaletteManager.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
+using UnityEngine;
namespace MiraAPI.Colors;
@@ -15,6 +16,8 @@ public static class PaletteManager
public static CustomColor[] RegisteredColors => [.. CustomColors];
internal static readonly List CustomColors = [];
+ internal static readonly HashSet VariableColorNames = [];
+ internal static readonly Dictionary ColorIdToColorMap = [];
internal static void RegisterAllColors()
{
@@ -22,11 +25,64 @@ public static class PaletteManager
var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray();
var stringNames = CustomColors.Select(x => x.Name).ToArray();
+ var originalLength = Palette.PlayerColors.Length;
+
Palette.PlayerColors = Palette.PlayerColors.ToArray().AddRangeToArray(colors);
Palette.ShadowColors = Palette.ShadowColors.ToArray().AddRangeToArray(shadowColors);
Palette.ColorNames = Palette.ColorNames.ToArray().AddRangeToArray(stringNames);
Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors);
Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors);
+
+ for (var i = 0; i < originalLength + CustomColors.Count; i++)
+ ColorIdToColorMap[i + originalLength] = CustomColors[i];
+ }
+
+ ///
+ /// Checks to see if the provided color ID belongs to a variant color.
+ ///
+ /// The id of the color.
+ /// true if the color is a variable color; false otherwise.
+ public static bool IsVariable(int colorId)
+ {
+ return VariableColorNames.Contains(Palette.ColorNames[colorId]);
+ }
+
+ ///
+ /// Checks to see if the provided color ID belongs to a custom color.
+ ///
+ /// The id of the color.
+ /// true if the color is a custom color; false otherwise.
+ public static bool IsCustomColor(int colorId)
+ {
+ return ColorIdToColorMap.ContainsKey(colorId);
+ }
+
+ ///
+ /// Gets the main color of the provided color ID.
+ ///
+ /// The id of the color.
+ /// The main color associated with the provided ID.
+ public static Color32 GetMainColor(int colorId)
+ {
+ return !ColorIdToColorMap.TryGetValue(colorId, out var custom)
+ ? Palette.PlayerColors[colorId]
+ : (custom is VariableColor variable
+ ? variable.EvaluateMainColor()
+ : custom.MainColor);
+ }
+
+ ///
+ /// Gets the shadow color of the provided color ID.
+ ///
+ /// The id of the color.
+ /// The shadow color associated with the provided ID.
+ public static Color32 GetShadowColor(int colorId)
+ {
+ return !ColorIdToColorMap.TryGetValue(colorId, out var custom)
+ ? Palette.ShadowColors[colorId]
+ : (custom is VariableColor variable
+ ? variable.EvaluateShadowColor()
+ : custom.ShadowColor);
}
}
diff --git a/MiraAPI/Colors/VariableColor.cs b/MiraAPI/Colors/VariableColor.cs
new file mode 100644
index 0000000..0206423
--- /dev/null
+++ b/MiraAPI/Colors/VariableColor.cs
@@ -0,0 +1,69 @@
+using System;
+using MiraAPI.LocalSettings;
+using MiraAPI.Translation;
+using MiraAPI.Utilities;
+using UnityEngine;
+
+namespace MiraAPI.Colors;
+
+///
+/// Represents a dynamic custom color that changes over time.
+///
+/// The name of the color.
+/// The function to evaluate the dynamic main color.
+/// The function to evaluate the dynamic shadow color.
+/// The static main color used when dynamic colors are disabled.
+/// The static shadow color used when dynamic colors are disabled.
+public sealed class VariableColor(StringNames name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null)
+ : CustomColor(name, mainColor ?? mainEvaluator(0f), shadowColor ?? shadowEvaluator?.Invoke(0f) ?? (mainColor ?? mainEvaluator(0f)).GetShadowColor(60))
+{
+ ///
+ /// Gets or sets a delegate that takes a normalized time value and returns the dynamic main color.
+ ///
+ public Func MainEvaluator { get; set; } = mainEvaluator;
+
+ ///
+ /// Gets or sets a delegate that takes a normalized time value and returns the dynamic shadow color.
+ ///
+ public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(60));
+
+ ///
+ public VariableColor(StringNames name, Func mainEvaluator, Color32? mainColor = null)
+ : this(name, mainEvaluator, null, mainColor, null)
+ {
+ }
+
+ ///
+ public VariableColor(string name, Func mainEvaluator, Color32? mainColor = null)
+ : this(name, mainEvaluator, null, mainColor, null)
+ {
+ }
+
+ ///
+ public VariableColor(string name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null)
+ : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor)
+ {
+ }
+
+ ///
+ /// Evaluates the dynamic main color based on the current in-game time.
+ ///
+ /// The dynamically evaluated color, or the static if static variant colors are enabled in the user's settings.
+ public Color32 EvaluateMainColor()
+ {
+ return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value
+ ? MainEvaluator(Time.time)
+ : MainColor;
+ }
+
+ ///
+ /// Evaluates the dynamic shadow color based on the current in-game time.
+ ///
+ /// The dynamically evaluated color, or the static if static variant colors are enabled in the user's settings.
+ public Color32 EvaluateShadowColor()
+ {
+ return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value
+ ? ShadowEvaluator(Time.time)
+ : ShadowColor;
+ }
+}
diff --git a/MiraAPI/Colors/VariableColorBehaviour.cs b/MiraAPI/Colors/VariableColorBehaviour.cs
new file mode 100644
index 0000000..1b7efda
--- /dev/null
+++ b/MiraAPI/Colors/VariableColorBehaviour.cs
@@ -0,0 +1,46 @@
+using System.Diagnostics.CodeAnalysis;
+using Il2CppInterop.Runtime.Attributes;
+using MiraAPI.Colors;
+using MiraAPI.Utilities;
+using UnityEngine;
+
+namespace MiraAPI;
+
+///
+/// A mono script that adjusts the material colors of a component that it's attached to based on the provided variable color.
+///
+[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")]
+[SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Unity fields.")]
+[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Read above.")]
+public sealed class VariableColorBehaviour : MonoBehaviour
+{
+ private SpriteRenderer rend;
+ private Material material;
+ private VariableColor? color;
+
+ ///
+ /// Sets the current variant color.
+ ///
+ /// The new variant color.
+ [HideFromIl2Cpp]
+ public void SetColor(VariableColor? col)
+ {
+ color = col;
+ }
+
+ private void Awake()
+ {
+ rend = GetComponent();
+ material = rend.material;
+ }
+
+ private void Update()
+ {
+ if (!rend || color == null)
+ return;
+
+ material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor());
+ material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor());
+ material.SetColor(ShaderID.VisorColor, Palette.VisorColor);
+ }
+}
diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs
index 1560955..ca448b0 100644
--- a/MiraAPI/MiraApiSettings.cs
+++ b/MiraAPI/MiraApiSettings.cs
@@ -233,4 +233,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config)
///
[LocalToggleSetting]
public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true);
+
+ ///
+ /// Gets whether variant colors will remain static or not.
+ ///
+ [LocalToggleSetting]
+ public ConfigEntry EnableVariableColors { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableVariableColors", true);
}
diff --git a/MiraAPI/Patches/Hud/VariableColorPatches.cs b/MiraAPI/Patches/Hud/VariableColorPatches.cs
new file mode 100644
index 0000000..628a1f4
--- /dev/null
+++ b/MiraAPI/Patches/Hud/VariableColorPatches.cs
@@ -0,0 +1,98 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using AmongUs.Data;
+using HarmonyLib;
+using MiraAPI.Colors;
+using UnityEngine;
+
+namespace MiraAPI.Patches.Hud;
+
+[HarmonyPatch(typeof(PlayerMaterial))]
+internal static class SetPlayerMaterialPatch
+{
+ [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))]
+ public static bool Prefix(int colorId, Renderer rend)
+ {
+ var r = rend.gameObject.GetComponent()
+ ?? rend.gameObject.AddComponent();
+ r.SetColor(PaletteManager.IsVariable(colorId) ? (VariableColor)PaletteManager.ColorIdToColorMap[colorId] : null);
+ return !PaletteManager.IsVariable(colorId);
+ }
+
+ [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))]
+ public static void Prefix(Renderer rend)
+ {
+ var r = rend.gameObject.GetComponent()
+ ?? rend.gameObject.AddComponent();
+ r.SetColor(null);
+ }
+}
+
+[HarmonyPatch(typeof(PlayerTab), nameof(PlayerTab.Update))]
+internal static class PlayerTabPatch
+{
+ public static void Postfix(PlayerTab __instance)
+ {
+ for (var i = 0; i < __instance.ColorChips.Count; i++)
+ {
+ __instance.ColorChips[i].Inner.SpriteColor = PaletteManager.GetMainColor(i);
+ }
+ }
+}
+
+[HarmonyPatch(typeof(ChatNotification), nameof(ChatNotification.Update))]
+internal static class ChatNotifRainbowPatch
+{
+ public static void Postfix(ChatNotification __instance)
+ {
+ if (!__instance.gameObject.active || !PaletteManager.IsVariable(__instance.player.cosmetics.ColorId))
+ return;
+
+ var str = ColorUtility.ToHtmlStringRGB(PaletteManager.GetMainColor(__instance.player.cosmetics.ColorId));
+ __instance.playerNameText.text = "" + __instance.playerNameText.text.WithoutRichText();
+ }
+
+ private static string WithoutRichText(this string text)
+ {
+ var richTagRegex = new Regex(@"<[^>]*>", default, Regex.InfiniteMatchTimeout);
+ return richTagRegex.Replace(text, string.Empty);
+ }
+}
+
+[HarmonyPatch(typeof(HostInfoPanel), nameof(HostInfoPanel.Update))]
+internal static class RainbowLobbyInfoPanePatch
+{
+ [SuppressMessage("Style", "IDE0045:Convert to conditional expression", Justification = "Operator becomes too large.")]
+ public static void Postfix(HostInfoPanel __instance)
+ {
+ if (!__instance.gameObject.activeInHierarchy || !PaletteManager.IsVariable(__instance.player.cosmetics.ColorId))
+ {
+ return;
+ }
+
+ var host = GameData.Instance.GetHost();
+ var text = ColorUtility.ToHtmlStringRGB(PaletteManager.GetMainColor(__instance.player.cosmetics.ColorId));
+ __instance.hostLabel.text =
+ TranslationController.Instance.GetString(StringNames.HostNounLabel);
+ if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage))
+ {
+ __instance.hostLabel.text = __instance.hostLabel.text.Insert(0, "");
+ __instance.hostLabel.text = __instance.hostLabel.text.Insert(__instance.hostLabel.text.Length, "");
+ }
+
+ if (AmongUsClient.Instance.AmHost)
+ {
+ __instance.playerName.text = (string.IsNullOrEmpty(host.PlayerName)
+ ? "..."
+ : $"{host.PlayerName}")
+ + " " +
+ TranslationController.Instance.GetString(StringNames.HostYouLabel);
+ }
+ else
+ {
+ __instance.playerName.text =
+ (string.IsNullOrEmpty(host.PlayerName) ? "..." : $"{host.PlayerName}") +
+ " (" + __instance.player.ColorBlindName + ")";
+ }
+ }
+}
diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs
index a3b5158..9058eac 100644
--- a/MiraAPI/PluginLoading/MiraPluginManager.cs
+++ b/MiraAPI/PluginLoading/MiraPluginManager.cs
@@ -404,7 +404,7 @@ public sealed class MiraPluginManager
foreach (var property in type.GetProperties())
{
- if (property.PropertyType != typeof(CustomColor))
+ if (property.PropertyType.IsAssignableTo(typeof(CustomColor)))
{
continue;
}
@@ -416,6 +416,9 @@ public sealed class MiraPluginManager
}
PaletteManager.CustomColors.Add(color);
+
+ if (color is VariableColor)
+ PaletteManager.VariableColorNames.Add(color.Name);
}
foreach (var field in type.GetFields().Where(f => f.FieldType.IsAssignableTo(typeof(CustomColor))))
diff --git a/MiraAPI/Resources/Locale/en_US.xml b/MiraAPI/Resources/Locale/en_US.xml
index 7cd2c1c..be53733 100644
--- a/MiraAPI/Resources/Locale/en_US.xml
+++ b/MiraAPI/Resources/Locale/en_US.xml
@@ -12,6 +12,7 @@
Display Wiki on Bottom Row
Enable Sabotage Flashes
Enable Sabotage Blare
+ Enable Variable Color Changes
RETURN TO ROLE SETTINGS
Custom Category 1
Custom Category 2