diff --git a/.editorconfig b/.editorconfig index 2f48813..8d85d5f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -98,3 +98,6 @@ dotnet_diagnostic.IDE0045.severity = none # IDE0046: Convert to conditional expression dotnet_diagnostic.IDE0046.severity = none + +# S1244: Floating point numbers should not be tested for equality +dotnet_diagnostic.S1244.severity = none diff --git a/MiraAPI.Example/ExampleColors.cs b/MiraAPI.Example/ExampleColors.cs index cde5aae..297a370 100644 --- a/MiraAPI.Example/ExampleColors.cs +++ b/MiraAPI.Example/ExampleColors.cs @@ -1,4 +1,6 @@ using MiraAPI.Colors; +using MiraAPI.Colors.ColorSpaces; +using MiraAPI.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -14,4 +16,9 @@ public static class ExampleColors public static CustomColor Rose { get; } = new("color.rose", new Color(0.98f, 0.26f, 0.62f)); public static CustomColor Gold { get; } = new("color.gold", new Color(1.0f, 0.84f, 0.0f)); + + public static VariableColor Rainbow { get; } = new("color.rainbow", t => new HsbColor(MathUtilities.PingPong(t, 1f, 0.3f), 1f, 1f)) + { + ColorBrightness = CustomColorBrightness.Lighter, + }; } diff --git a/MiraAPI.Example/Resources/Locale/en_US.xml b/MiraAPI.Example/Resources/Locale/en_US.xml index 56f74c8..8328c69 100644 --- a/MiraAPI.Example/Resources/Locale/en_US.xml +++ b/MiraAPI.Example/Resources/Locale/en_US.xml @@ -80,4 +80,5 @@ Eliminate every other faction. Cerulean Rose Gold + Rainbow diff --git a/MiraAPI/Colors/ColorSpaces/CmykColor.cs b/MiraAPI/Colors/ColorSpaces/CmykColor.cs new file mode 100644 index 0000000..11ec563 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/CmykColor.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +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 key = 1f - cmyk.k; + + var r = (1f - cmyk.c) * key; + var g = (1f - cmyk.m) * key; + var b = (1f - cmyk.y) * key; + + return new Color(MathUtilities.Clamp01(r), MathUtilities.Clamp01(g), MathUtilities.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, MathF.Max(g, b)); + + if (k >= 1f) + return new CmykColor(0f, 0f, 0f, 1f, color.a); + + var invK = 1f / (1f - k); + + var c = (1f - r - k) * invK; + var m = (1f - g - k) * invK; + var y = (1f - b - k) * invK; + + return new CmykColor(MathUtilities.Clamp01(c), MathUtilities.Clamp01(m), MathUtilities.Clamp01(y), MathUtilities.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) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(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( + MathUtilities.LerpUnclamped(a.c, b.c, t), + MathUtilities.LerpUnclamped(a.m, b.m, t), + MathUtilities.LerpUnclamped(a.y, b.y, t), + MathUtilities.LerpUnclamped(a.k, b.k, t), + MathUtilities.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..b566333 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HsbColor.cs @@ -0,0 +1,136 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +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) + { + if (hsb.s <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), MathUtilities.Clamp01(hsb.b), hsb.a); + + var c = hsb.b * hsb.s; + var hPrime = hsb.h * 6f; + var x = c * (1f - MathF.Abs((hPrime % 2f) - 1f)); + var m = hsb.b - c; + + 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( + MathUtilities.Clamp01(r + m), + MathUtilities.Clamp01(g + m), + MathUtilities.Clamp01(b + m), + 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) + { + 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 delta = max - min; + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + var s = max == 0f ? 0f : delta / max; + var b = max; + + return new HsbColor(h, s, b, 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) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(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 = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HsbColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.b, b.b, t), + MathUtilities.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..466f159 --- /dev/null +++ b/MiraAPI/Colors/ColorSpaces/HslColor.cs @@ -0,0 +1,132 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Utilities; +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 <= float.Epsilon) + return new Color(MathUtilities.Clamp01(hsl.l), MathUtilities.Clamp01(hsl.l), MathUtilities.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(MathUtilities.Clamp01(r + m), MathUtilities.Clamp01(g + m), MathUtilities.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)); + + var h = 0f; + + if (delta > 0f) + { + if (max == color.r) + h = (color.g - color.b) / delta % 6f; + else if (max == color.g) + h = (color.b - color.r) / delta + 2f; + else + h = (color.r - color.g) / delta + 4f; + + h /= 6f; + + if (h < 0f) + h += 1f; + } + + 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) + { + return LerpUnclamped(a, b, MathUtilities.Clamp01(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 = MathUtilities.Repeat(b.h - a.h, 1f); + + if (hueDifference > 0.5f) + hueDifference -= 1f; + + return new HslColor( + MathUtilities.Repeat(a.h + (hueDifference * t), 1f), + MathUtilities.LerpUnclamped(a.s, b.s, t), + MathUtilities.LerpUnclamped(a.l, b.l, t), + MathUtilities.LerpUnclamped(a.a, b.a, t)); + } +} diff --git a/MiraAPI/Colors/CustomColor.cs b/MiraAPI/Colors/CustomColor.cs index a778e31..db91356 100644 --- a/MiraAPI/Colors/CustomColor.cs +++ b/MiraAPI/Colors/CustomColor.cs @@ -1,4 +1,5 @@ -using MiraAPI.Translation; +using System.Diagnostics.CodeAnalysis; +using MiraAPI.Translation; using MiraAPI.Utilities; using UnityEngine; @@ -10,17 +11,37 @@ namespace MiraAPI.Colors; /// The name of the option. /// The main color. /// The shadow color. -public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal code.")] +[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1513:Closing brace should be followed by blank line", Justification = "Looks weird.")] +public class CustomColor(StringNames name, Color32 mainColor, Color32 shadowColor) { /// /// Gets or sets the main color. /// - public Color32 MainColor { get; set; } = mainColor; + public Color32 MainColor + { + get; + set + { + field = value; + InternalMainColor = value; + } + } + = mainColor; /// /// Gets or sets the shadow color. /// - public Color32 ShadowColor { get; set; } = shadowColor; + public Color32 ShadowColor + { + get; + set + { + field = value; + InternalShadowColor = value; + } + } + = shadowColor; /// /// Gets or sets the name of the color. @@ -32,6 +53,9 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public CustomColorBrightness ColorBrightness { get; set; } = CustomColorBrightness.Darker; + internal Color InternalMainColor = mainColor; + internal Color InternalShadowColor = shadowColor; + /// public CustomColor(StringNames name, Color32 mainColor) : this(name, mainColor, mainColor.GetShadowColor(60)) @@ -56,6 +80,11 @@ public sealed class CustomColor(StringNames name, Color32 mainColor, Color32 sha /// public enum CustomColorBrightness { + /// + /// An invalid color. + /// + Invalid = -1, + /// /// A lighter color. /// diff --git a/MiraAPI/Colors/PaletteManager.cs b/MiraAPI/Colors/PaletteManager.cs index fe7e725..3b9cd4a 100644 --- a/MiraAPI/Colors/PaletteManager.cs +++ b/MiraAPI/Colors/PaletteManager.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using HarmonyLib; +using UnityEngine; namespace MiraAPI.Colors; @@ -15,9 +17,36 @@ public static class PaletteManager public static CustomColor[] RegisteredColors => [.. CustomColors]; internal static readonly List CustomColors = []; + internal static readonly HashSet VariableColorNames = []; + + private static readonly CustomColorBrightness[] ColorBrightnessMap = + [ + CustomColorBrightness.Darker, // Red + CustomColorBrightness.Darker, // Blue + CustomColorBrightness.Darker, // Green + CustomColorBrightness.Lighter, // Pink + CustomColorBrightness.Lighter, // Orange + CustomColorBrightness.Lighter, // Yellow + CustomColorBrightness.Darker, // Black + CustomColorBrightness.Lighter, // White + CustomColorBrightness.Darker, // Purple + CustomColorBrightness.Darker, // Brown + CustomColorBrightness.Lighter, // Cyan + CustomColorBrightness.Lighter, // Lime + CustomColorBrightness.Darker, // Maroon + CustomColorBrightness.Lighter, // Rose + CustomColorBrightness.Lighter, // Banana + CustomColorBrightness.Darker, // Gray + CustomColorBrightness.Darker, // Tan + CustomColorBrightness.Lighter, // Coral + ]; + + private static int originalLength; internal static void RegisterAllColors() { + originalLength = Palette.PlayerColors.Length; + var colors = CustomColors.Select(x => x.MainColor).ToArray(); var shadowColors = CustomColors.Select(x => x.ShadowColor).ToArray(); var stringNames = CustomColors.Select(x => x.Name).ToArray(); @@ -29,4 +58,90 @@ public static class PaletteManager Palette.TextColors = Palette.TextColors.ToArray().AddRangeToArray(colors); Palette.TextOutlineColors = Palette.TextOutlineColors.ToArray().AddRangeToArray(shadowColors); } + + /// + /// Checks to see if the provided color ID belongs to a variable color. + /// + /// The id of the color. + /// true if the color is a variable color; false otherwise. + public static bool IsVariable(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return false; + + 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 (uint)(colorId - originalLength) < (uint)CustomColors.Count; + } + + /// + /// Gets the main color of the provided color ID. + /// + /// The id of the color. + /// The main color associated with the provided ID. + public static Color GetMainColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.PlayerColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateMainColor(Time.time) + : custom.InternalMainColor); + } + + /// + /// Gets the shadow color of the provided color ID. + /// + /// The ID of the color. + /// The shadow color associated with the provided ID. + public static Color GetShadowColor(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return Color.white; + + return !TryGetCustomColor(colorId, out var custom) + ? Palette.ShadowColors[colorId] + : (custom is VariableColor variable + ? variable.EvaluateShadowColor(Time.time) + : custom.InternalShadowColor); + } + + /// + /// Gets the brightness type of the color associated with the provided ID. + /// + /// The ID of the color. + /// The brightness type of the provided ID. + public static CustomColorBrightness GetBrightness(int colorId) + { + if ((uint)colorId >= (uint)Palette.ColorNames.Length) + return CustomColorBrightness.Invalid; + + return TryGetCustomColor(colorId, out var custom) + ? custom.ColorBrightness + : ColorBrightnessMap[colorId]; + } + + internal static bool TryGetCustomColor(int colorId, [NotNullWhen(true)] out CustomColor? custom) + { + var index = colorId - originalLength; + + if ((uint)index >= (uint)CustomColors.Count) + { + custom = null; + return false; + } + + custom = CustomColors[index]; + return true; + } } diff --git a/MiraAPI/Colors/VariableColor.cs b/MiraAPI/Colors/VariableColor.cs new file mode 100644 index 0000000..bfb9717 --- /dev/null +++ b/MiraAPI/Colors/VariableColor.cs @@ -0,0 +1,72 @@ +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. +/// The default time that is used to automatically fill in the static colors when the latter is not provided. +public sealed class VariableColor(StringNames name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : CustomColor(name, mainColor ?? mainEvaluator(defaultTime), shadowColor ?? shadowEvaluator?.Invoke(defaultTime) ?? (mainColor ?? mainEvaluator(defaultTime)).GetShadowColor(60)) +{ + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic main color. + /// + public Func MainEvaluator { get; set; } = mainEvaluator; + + /// + /// Gets or sets a delegate that takes in a time value and returns the dynamic shadow color. + /// + public Func ShadowEvaluator { get; set; } = shadowEvaluator ?? (t => mainEvaluator(t).GetShadowColor(0.24f)); + + /// + public VariableColor(StringNames name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Color32? mainColor = null, float defaultTime = 0f) + : this(name, mainEvaluator, null, mainColor, null, defaultTime) + { + } + + /// + public VariableColor(string name, Func mainEvaluator, Func? shadowEvaluator, Color32? mainColor = null, Color32? shadowColor = null, float defaultTime = 0f) + : this(MiraLocaleManager.GetOrCreateLocaleString(name), mainEvaluator, shadowEvaluator, mainColor, shadowColor, defaultTime) + { + } + + /// + /// Evaluates the dynamic main color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateMainColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? MainEvaluator(time) + : InternalMainColor; + } + + /// + /// Evaluates the dynamic shadow color based on the provided time value. + /// + /// The time to evaluate the color against. + /// The dynamically evaluated color, or the static if static variable colors are enabled in the user's settings. + public Color EvaluateShadowColor(float time) + { + return LocalSettingsTabSingleton.Instance.EnableVariableColors.Value + ? ShadowEvaluator(time) + : InternalShadowColor; + } +} diff --git a/MiraAPI/Colors/VariableColorBehaviour.cs b/MiraAPI/Colors/VariableColorBehaviour.cs new file mode 100644 index 0000000..e0d4cea --- /dev/null +++ b/MiraAPI/Colors/VariableColorBehaviour.cs @@ -0,0 +1,102 @@ +using System.Diagnostics.CodeAnalysis; +using Il2CppInterop.Runtime.Attributes; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Attributes; +using UnityEngine; + +namespace MiraAPI; + +/// +/// The base class for variable color behaviour on s. +/// +[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 abstract class BaseVariableColorBehaviour : MonoBehaviour +{ + /// + /// The to apply. + /// + protected VariableColor? color; + + /// + /// Sets the current variable color. + /// + /// The new . + [HideFromIl2Cpp] + public void SetColor(VariableColor? newColor) + { + color = newColor; + } +} + +/// +/// A mono script that adjusts the material colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class VariableColorBehaviour : BaseVariableColorBehaviour +{ + private Renderer? rend; + + private static readonly Color VisorColor = Palette.VisorColor; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(Renderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color == null || rend == null) + return; + + var time = Time.time; + var material = rend.material; + + material.SetColor(ShaderID.VisorColor, VisorColor); + + material.SetColor(ShaderID.BodyColor, color.EvaluateMainColor(time)); + material.SetColor(ShaderID.BackColor, color.EvaluateShadowColor(time)); + } +} + +/// +/// A mono script that adjusts the graphic colors of a component that it's attached to based on the provided variable color. +/// +[RegisterInIl2Cpp] +[SuppressMessage("Style", "IDE0051:Remove unused private members", Justification = "Unity methods.")] +public sealed class BasicVariableColorBehaviour : BaseVariableColorBehaviour +{ + private SpriteRenderer? rend; + + /// + /// Sets the new target. + /// + /// The new that the script must target. + public void SetRend(SpriteRenderer? newRend) + { + rend = newRend; + } + + private void Awake() + { + rend = GetComponent(); + } + + private void Update() + { + if (color != null && rend != null) + rend.color = color.EvaluateMainColor(Time.time); + } +} diff --git a/MiraAPI/Hud/CustomPaginableMenu.cs b/MiraAPI/Hud/CustomPaginableMenu.cs index f24ccee..14f573b 100644 --- a/MiraAPI/Hud/CustomPaginableMenu.cs +++ b/MiraAPI/Hud/CustomPaginableMenu.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using MiraAPI.Patches.Stubs; using MiraAPI.Translation; +using MiraAPI.Utilities; using MiraAPI.Utilities.Assets; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -247,8 +248,7 @@ public abstract class CustomPaginableMenu : CustomPhoneMenu() - ?? searchTextbox.gameObject.AddComponent(); + var searchFocusButton = searchTextbox.gameObject.GetOrAddComponent(); if (wikiClickSound != null) { searchFocusButton.ClickSound = wikiClickSound; diff --git a/MiraAPI/MiraApiSettings.cs b/MiraAPI/MiraApiSettings.cs index 7db2c54..1047fcd 100644 --- a/MiraAPI/MiraApiSettings.cs +++ b/MiraAPI/MiraApiSettings.cs @@ -243,4 +243,10 @@ public class MiraApiSettings(ConfigFile config) : LocalSettingsTab(config) /// [LocalToggleSetting] public ConfigEntry EnableSabotageBlares { get; private set; } = config.Bind("MiraApi.Accessibility", "MiraApi.EnableSabotageBlare", true); + + /// + /// Gets whether variable 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/.editorconfig b/MiraAPI/Patches/.editorconfig index a019ed9..3b3da0b 100644 --- a/MiraAPI/Patches/.editorconfig +++ b/MiraAPI/Patches/.editorconfig @@ -13,4 +13,4 @@ dotnet_diagnostic.SA1611.severity = none dotnet_diagnostic.SA1615.severity = none # SA1602: Enumeration items should be documented. -dotnet_diagnostic.SA1602.severity = none \ No newline at end of file +dotnet_diagnostic.SA1602.severity = none diff --git a/MiraAPI/Patches/Colors/VariableColorPatches.cs b/MiraAPI/Patches/Colors/VariableColorPatches.cs new file mode 100644 index 0000000..98bed64 --- /dev/null +++ b/MiraAPI/Patches/Colors/VariableColorPatches.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using AmongUs.Data; +using HarmonyLib; +using MiraAPI.Colors; +using MiraAPI.Utilities; +using Reactor.Utilities.Extensions; +using UnityEngine; + +namespace MiraAPI.Patches.Colors; + +[HarmonyPatch(typeof(PlayerMaterial))] +internal static class SetPlayerMaterialPatch +{ + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(int), typeof(Renderer))] + public static bool Prefix(int colorId, Renderer rend) + { + rend.gameObject.GetOrAddComponent().SetColor( + PaletteManager.TryGetCustomColor(colorId, out var color) && color is VariableColor variable + ? variable + : null); + return !PaletteManager.IsVariable(colorId); + } + + [HarmonyPatch(nameof(PlayerMaterial.SetColors), typeof(Color), typeof(Renderer))] + public static void Prefix(Renderer rend) + { + rend.gameObject.GetOrAddComponent().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 +{ + private static readonly Regex RichTags = new(@"<[^>]*>", default, Regex.InfiniteMatchTimeout); + + public static void Postfix(ChatNotification __instance) + { + if (!__instance.gameObject.active) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var color = PaletteManager.GetMainColor(colorId); + var str = color.ToHtmlStringRGBA(); + var nameText = __instance.playerNameText; + nameText.text = "" + RichTags.Replace(nameText.text, string.Empty); + nameText.outlineColor = color.GetShadowColor(0.5f); + } +} + +[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) + return; + + var colorId = __instance.player.cosmetics.ColorId; + + if (!PaletteManager.IsVariable(colorId)) + return; + + var host = GameData.Instance.GetHost(); + + if (!host) + return; + + var text = PaletteManager.GetMainColor(colorId).ToHtmlStringRGBA(); + var label = __instance.hostLabel; + label.text = TranslationController.Instance.GetString(StringNames.HostNounLabel); + + if (__instance.ShouldBoldenHostLabel(DataManager.Settings.Language.CurrentLanguage)) + { + label.text = "" + label.text + ""; + } + + 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..b46d155 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 7d27154..3cff5f4 100644 --- a/MiraAPI/Resources/Locale/en_US.xml +++ b/MiraAPI/Resources/Locale/en_US.xml @@ -13,6 +13,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 diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index 7a3381c..fd2b591 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -620,7 +620,23 @@ public static class Extensions (byte)Mathf.Clamp(color.r - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.g - darknessAmount, 0, 255), (byte)Mathf.Clamp(color.b - darknessAmount, 0, 255), - byte.MaxValue); + color.a); + } + + /// + /// Gets a darkened version of a . + /// + /// The original . + /// A darkness amount between 0 and 1. + /// The darkened . + public static Color GetShadowColor(this Color color, float darknessAmount) + { + return + new Color( + MathUtilities.Clamp01(color.r - darknessAmount), + MathUtilities.Clamp01(color.g - darknessAmount), + MathUtilities.Clamp01(color.b - darknessAmount), + color.a); } /// @@ -796,7 +812,7 @@ public static class Extensions } /// - /// Fixed version of . + /// Fixed version of . /// /// The you want to update the outline for. /// The outline . @@ -855,4 +871,17 @@ public static class Extensions userData.joystickMaps[0].actionElementMaps.Add(map); return action; } + + /// + /// Gets a component that is attached to the provided , attaching and returning the component if it wasn't found. + /// + /// The component type. + /// The to get the component from. + /// The component. + public static T GetOrAddComponent(this GameObject go) + where T : Component + { + var comp = go.GetComponent(); + return comp ? comp : go.AddComponent(); + } } diff --git a/MiraAPI/Utilities/MathUtilities.cs b/MiraAPI/Utilities/MathUtilities.cs new file mode 100644 index 0000000..a505a36 --- /dev/null +++ b/MiraAPI/Utilities/MathUtilities.cs @@ -0,0 +1,82 @@ +using System; + +namespace MiraAPI.Utilities; + +/// +/// Provides mathematical utility methods. +/// +public static class MathUtilities +{ + /// + /// Linearly interpolates between two values, clamping the interpolant between 0 and 1. + /// + /// The starting value. + /// The ending value. + /// The interpolation value, automatically clamped to a 0 to 1 range. + /// The interpolated float. + public static float Lerp(float a, float b, float t) + { + return a + ((b - a) * Clamp01(t)); + } + + /// + /// Linearly interpolates between two values without clamping the interpolant. + /// + /// The starting value. + /// The ending value. + /// The interpolation value. + /// The interpolated float. + public static float LerpUnclamped(float a, float b, float t) + { + return a + ((b - a) * t); + } + + /// + /// Clamps a floating-point value strictly between 0 and 1. + /// + /// The floating-point value to clamp. + /// The clamped value. + public static float Clamp01(float value) + { + return Math.Clamp(value, 0f, 1f); + } + + /// + /// Ping-pongs a value back and forth between a minimum and maximum value. + /// + /// The input time or interpolant. + /// The minimum boundary of the ping-pong wave. + /// The maximum boundary of the ping-pong wave. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between and . + public static float PingPong(float t, float min, float max, float speed = 1f) + { + return min + PingPong(t, max - min, speed); + } + + /// + /// Ping-pongs a value back and forth between 0 and a specified length. + /// + /// The input time or interpolant. + /// The maximum value to bounce against, starting from 0. + /// The multiplier for the speed of the ping-pong effect. + /// A value oscillating continuously between 0 and . + public static float PingPong(float t, float length, float speed = 1f) + { + var dx = speed * t; + var floor = (int)MathF.Floor(dx); + var phase = floor % 2; // 0 = forward, 1 = backward + return Clamp01(((dx - floor) * ((2 * phase) - 1)) + 1 - phase) * length; + } + + /// + /// Loops a value strictly between 0 and a maximum length, behaving like a standard float modulo. + /// + /// The input value to loop. + /// The maximum length of the repeating sequence. + /// The wrapped value smoothly looping back to 0 upon reaching . + public static float Repeat(float t, float length) + { + return Math.Clamp(t - (MathF.Floor(t / length) * length), 0f, length); + } +} diff --git a/README.md b/README.md index 84823f4..59dfa6e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) - [English] | [简体中文](Translated/README_zh.md) +[![Starlight badge](https://img.shields.io/badge/Download%20On-Starlight-d3b65e?style=for-the-badge)](https://starlight.allofus.dev/mods/mira.api) +[![](https://dcbadge.limes.pink/api/server/AEfHJGwggC)](https://discord.gg/AEfHJGwggC) + > This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC. # Mira API