Files
MiraAPI/MiraAPI/Utilities/Extensions.cs
T
2025-03-01 18:34:18 -05:00

347 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using MiraAPI.GameOptions;
using MiraAPI.Networking;
using MiraAPI.Roles;
using Reactor.Utilities;
using TMPro;
using UnityEngine;
namespace MiraAPI.Utilities;
/// <summary>
/// Extension methods for various classes.
/// </summary>
public static class Extensions
{
internal static NetData GetNetData(this ICustomRole role)
{
var count = role.GetCount();
var chance = role.GetChance();
if (count == null)
{
Logger<MiraApiPlugin>.Error("Couldn't get role count for NetData, defaulting to zero.");
count = 0;
}
if (chance == null)
{
Logger<MiraApiPlugin>.Error("Couldn't get role chance for NetData, defaulting to zero.");
chance = 0;
}
return new NetData(
RoleId.Get(role.GetType()),
BitConverter.GetBytes(count.Value).AddRangeToArray(BitConverter.GetBytes(chance.Value)));
}
/// <summary>
/// Determines if a float is an integer.
/// </summary>
/// <param name="number">The float number.</param>
/// <returns>True if the float is an integer, false otherwise.</returns>
public static bool IsInteger(this float number)
{
return Mathf.Approximately(number, Mathf.Round(number));
}
/// <summary>
/// Gets the best constructor for a type based on the specified arguments.
/// </summary>
/// <param name="type">The type to get the constructor from.</param>
/// <param name="args">The arguments to pass into the constructor.</param>
/// <returns>The best constructor.</returns>
public static ConstructorInfo? GetBestConstructor(this Type type, params object[] args)
{
return type.GetValidConstructors(args)
.OrderBy(
ctor => ctor.GetParameters()
.Select((p, i) => GetInheritanceDistance(args[i].GetType(), p.ParameterType))
.Sum())
.FirstOrDefault();
}
/// <summary>
/// Gets the constructors of a type that match the specified arguments.
/// </summary>
/// <param name="type">The type to get constructors from.</param>
/// <param name="args">The arguments to pass into the constructor.</param>
/// <returns>A collection of valid constructors.</returns>
public static IEnumerable<ConstructorInfo> GetValidConstructors(this Type type, params object[] args)
{
return type.GetConstructors().Where(
x =>
{
var parameters = x.GetParameters();
return parameters.Length == args.Length && Array.TrueForAll(
parameters,
t => t.ParameterType.IsInstanceOfType(args[t.Position]));
});
}
/// <summary>
/// Calculates the inheritance distance from the given type to its target base type.
/// Lower values mean the type is a closer match.
/// </summary>
/// <param name="from">The derived type.</param>
/// <param name="to">The base type.</param>
/// <returns>The distance between the types.</returns>
public static int GetInheritanceDistance(Type from, Type to)
{
if (!from.IsAssignableFrom(to))
{
return int.MaxValue;
}
var type = from;
var distance = 0;
while (type != null && type != to)
{
type = type.BaseType;
distance++;
}
return type == to ? distance : int.MaxValue;
}
/// <summary>
/// Enables stencil masking on a TMP text object.
/// </summary>
/// <param name="text">The TMP text.</param>
public static void EnableStencilMasking(this TMP_Text text)
{
text.fontMaterial.SetFloat(ShaderID.Stencil, 1);
text.fontMaterial.SetFloat(ShaderID.StencilComp, 4);
}
/// <summary>
/// Checks if a type is static.
/// </summary>
/// <param name="type">The type being checked.</param>
/// <returns>True if the type is static, false otherwise.</returns>
public static bool IsStatic(this Type type)
{
return type is { IsClass: true, IsAbstract: true, IsSealed: true };
}
/// <summary>
/// Gets a darkened version of a color.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="darknessAmount">A darkness amount between 0 and 255.</param>
/// <returns>The darkened color.</returns>
public static Color32 GetShadowColor(this Color32 color, byte darknessAmount)
{
return
new Color32(
(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);
}
/// <summary>
/// Truncates a string to a specified length.
/// </summary>
/// <param name="value">The original string.</param>
/// <param name="maxLength">The maximum length.</param>
/// <param name="truncationSuffix">An option suffix to attach at the end of the truncated string.</param>
/// <returns>A truncated string of maxLength with the attached suffix.</returns>
public static string? Truncate(this string? value, int maxLength, string truncationSuffix = "…")
{
return value?.Length > maxLength
? value[..maxLength] + truncationSuffix
: value;
}
/// <summary>
/// Chunks a collection of NetData into smaller arrays.
/// </summary>
/// <param name="dataCollection">A collection of NetData objects.</param>
/// <param name="chunkSize">The max chunk size in bytes.</param>
/// <returns>A Queue of NetData arrays.</returns>
public static Queue<NetData[]> ChunkNetData(this IEnumerable<NetData> dataCollection, int chunkSize)
{
Queue<NetData[]> chunks = [];
List<NetData> current = [];
var count = 0;
foreach (var netData in dataCollection)
{
var length = netData.GetLength();
if (length > chunkSize)
{
Logger<MiraApiPlugin>.Info($"NetData length is greater than chunk size: {length} > {chunkSize}");
continue;
}
if (count + length > chunkSize)
{
chunks.Enqueue(current.ToArray());
current.Clear();
count = 0;
}
current.Add(netData);
}
if (current.Count > 0)
{
chunks.Enqueue([.. current]);
}
return chunks;
}
/// <summary>
/// Determines if a given OptionBehaviour is for a custom option.
/// </summary>
/// <param name="optionBehaviour">The OptionBehaviour to be tested.</param>
/// <returns>True if the OptionBehaviour is for a custom options, false otherwise.</returns>
public static bool IsCustom(this OptionBehaviour optionBehaviour)
{
return ModdedOptionsManager.ModdedOptions.Values.Any(
opt => opt.OptionBehaviour && opt.OptionBehaviour == optionBehaviour);
}
/// <summary>
/// Randomizes a list.
/// </summary>
/// <param name="list">The list object.</param>
/// <typeparam name="T">The type of object the list contains.</typeparam>
/// <returns>A randomized list made from the original list.</returns>
public static List<T> Randomize<T>(this List<T> list)
{
List<T> randomizedList = [];
System.Random rnd = new();
while (list.Count > 0)
{
var index = rnd.Next(0, list.Count);
randomizedList.Add(list[index]);
list.RemoveAt(index);
}
return randomizedList;
}
/// <summary>
/// Darkens a color by a specified amount.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">A float amount between 0 and 1.</param>
/// <returns>The darkened color.</returns>
public static Color DarkenColor(this Color color, float amount = 0.45f)
{
return new Color(color.r - amount, color.g - amount, color.b - amount);
}
/// <summary>
/// Gets an alternate color based on the original color.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">The amount to darken or lighten the original color by between 0.0 and 1.0.</param>
/// <returns>An alternate color that has been darkened or lightened.</returns>
public static Color GetAlternateColor(this Color color, float amount = 0.45f)
{
return color.IsColorDark() ? LightenColor(color, amount) : DarkenColor(color, amount);
}
/// <summary>
/// Lightens a color by a specified amount.
/// </summary>
/// <param name="color">The original color.</param>
/// <param name="amount">A float amount between 0.0 and 1.0.</param>
/// <returns>The lightened color.</returns>
public static Color LightenColor(this Color color, float amount = 0.45f)
{
return new Color(color.r + amount, color.g + amount, color.b + amount);
}
/// <summary>
/// Checks if a color is dark.
/// </summary>
/// <param name="color">The color to check.</param>
/// <returns>True if the color is dark, false otherwise.</returns>
public static bool IsColorDark(this Color color)
{
return color.r < 0.5f && color is { g: < 0.5f, b: < 0.5f };
}
/// <summary>
/// Gets the nearest dead body to a player.
/// </summary>
/// <param name="playerControl">The player object.</param>
/// <param name="radius">The radius to search within.</param>
/// <returns>The dead body if it is found, or null there is none within the radius.</returns>
public static DeadBody? GetNearestDeadBody(this PlayerControl playerControl, float radius)
{
return Helpers
.GetNearestDeadBodies(playerControl.GetTruePosition(), radius, Helpers.CreateFilter(Constants.NotShipMask))
.Find(component => component && !component.Reported);
}
/// <summary>
/// Finds the nearest object of a specified type to a player. It will only work if the object has a collider.
/// </summary>
/// <param name="playerControl">The player object.</param>
/// <param name="radius">The radius to search within.</param>
/// <param name="filter">The contact filter.</param>
/// <param name="colliderTag">An optional collider tag.</param>
/// <param name="predicate">Optional predicate to test if the object is valid.</param>
/// <typeparam name="T">The type of the object.</typeparam>
/// <returns>The object if it was found, or null if there is none within the radius.</returns>
public static T? GetNearestObjectOfType<T>(
this PlayerControl playerControl,
float radius,
ContactFilter2D filter,
string? colliderTag = null,
Predicate<T>? predicate = null) where T : Component
{
return Helpers.GetNearestObjectsOfType<T>(playerControl.GetTruePosition(), radius, filter, colliderTag)
.Find(predicate ?? (component => component));
}
/// <summary>
/// Gets the closest player that matches the given criteria.
/// </summary>
/// <param name="playerControl">The player object.</param>
/// <param name="includeImpostors">Whether impostors should be included in the search.</param>
/// <param name="distance">The radius to search within.</param>
/// <param name="ignoreColliders">Whether colliders should be ignored when searching.</param>
/// <param name="predicate">Optional predicate to test if the object is valid.</param>
/// <returns>The closest player if there is one, false otherwise.</returns>
public static PlayerControl? GetClosestPlayer(
this PlayerControl playerControl,
bool includeImpostors,
float distance,
bool ignoreColliders = false,
Predicate<PlayerControl>? predicate = null)
{
var filteredPlayers = Helpers.GetClosestPlayers(playerControl, distance, ignoreColliders)
.Where(
playerInfo => !playerInfo.Data.Disconnected &&
playerInfo.PlayerId != playerControl.PlayerId &&
!playerInfo.Data.IsDead &&
(includeImpostors || !playerInfo.Data.Role.IsImpostor))
.ToList();
return predicate != null ? filteredPlayers.Find(predicate) : filteredPlayers.FirstOrDefault();
}
/// <summary>
/// Fixed version of Reactor's SetOutline.
/// </summary>
/// <param name="renderer">The renderer you want to update the outline for.</param>
/// <param name="color">The outline color.</param>
public static void UpdateOutline(this Renderer renderer, Color? color)
{
renderer.material.SetFloat(ShaderID.Outline, color.HasValue ? 1 : 0);
renderer.material.SetColor(ShaderID.OutlineColor, color ?? Color.clear);
renderer.material.SetColor(ShaderID.AddColor, color ?? Color.clear);
}
}