/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using HarmonyLib; using System; using System.Reflection; using System.Text; using UnityEngine; namespace PeterHan.PLib.Core { /// /// Extension methods to make life easier! /// public static class ExtensionMethods { /// /// Shorthand for string.Format() which can be invoked directly on the message. /// /// The format template message. /// The substitutions to be included. /// The formatted string. public static string F(this string message, params object[] args) { return string.Format(message, args); } /// /// Retrieves a component, but returns null if the GameObject is disposed. /// /// The component type to retrieve. /// The GameObject that hosts the component. /// The requested component, or null if it does not exist public static T GetComponentSafe(this GameObject obj) where T : Component { #pragma warning disable IDE0031 // Use null propagation // == operator is overloaded on GameObject to be equal to null if destroyed return (obj == null) ? null : obj.GetComponent(); #pragma warning restore IDE0031 // Use null propagation } /// /// Gets the assembly name of an assembly. /// /// The assembly to query. /// The assembly name, or null if assembly is null. public static string GetNameSafe(this Assembly assembly) { return assembly?.GetName()?.Name; } /// /// Gets the file version of the specified assembly. /// /// The assembly to query /// The AssemblyFileVersion of that assembly, or null if it could not be determined. public static string GetFileVersion(this Assembly assembly) { // Mod version var fileVersions = assembly.GetCustomAttributes(typeof( AssemblyFileVersionAttribute), true); string modVersion = null; if (fileVersions != null && fileVersions.Length > 0) { // Retrieves the "File Version" attribute var assemblyFileVersion = (AssemblyFileVersionAttribute)fileVersions[0]; if (assemblyFileVersion != null) modVersion = assemblyFileVersion.Version; } return modVersion; } /// /// Coerces a floating point number into the specified range. /// /// The original number. /// The minimum value (inclusive). /// The maximum value (inclusive). /// The nearest value between minimum and maximum inclusive to value. public static double InRange(this double value, double min, double max) { double result = value; if (result < min) result = min; if (result > max) result = max; return result; } /// /// Coerces a floating point number into the specified range. /// /// The original number. /// The minimum value (inclusive). /// The maximum value (inclusive). /// The nearest value between minimum and maximum inclusive to value. public static float InRange(this float value, float min, float max) { float result = value; if (result < min) result = min; if (result > max) result = max; return result; } /// /// Coerces an integer into the specified range. /// /// The original number. /// The minimum value (inclusive). /// The maximum value (inclusive). /// The nearest value between minimum and maximum inclusive to value. public static int InRange(this int value, int min, int max) { int result = value; if (result < min) result = min; if (result > max) result = max; return result; } /// /// Checks to see if an object is falling. /// /// The object to check. /// true if it is falling, or false otherwise. public static bool IsFalling(this GameObject obj) { int cell = Grid.PosToCell(obj); return obj.TryGetComponent(out Navigator navigator) && !navigator.IsMoving() && Grid.IsValidCell(cell) && Grid.IsValidCell(Grid.CellBelow(cell)) && !navigator.NavGrid.NavTable.IsValid(cell, navigator.CurrentNavType); } /// /// Checks to see if a floating point value is NaN or infinite. /// /// The value to check. /// true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise. public static bool IsNaNOrInfinity(this double value) { return double.IsNaN(value) || double.IsInfinity(value); } /// /// Checks to see if a floating point value is NaN or infinite. /// /// The value to check. /// true if it is NaN, PositiveInfinity, or NegativeInfinity, or false otherwise. public static bool IsNaNOrInfinity(this float value) { return float.IsNaN(value) || float.IsInfinity(value); } /// /// Checks to see if a building is usable. /// /// The building component to check. /// true if it is usable (enabled, not broken, not overheated), or false otherwise. public static bool IsUsable(this GameObject building) { return building.TryGetComponent(out Operational op) && op.IsFunctional; } /// /// Creates a string joining the members of an enumerable. /// /// The values to join. /// The delimiter to use between values. /// A string consisting of each value in order, with the delimiter in between. public static string Join(this System.Collections.IEnumerable values, string delimiter = ",") { var ret = new StringBuilder(128); bool first = true; // Append all, but skip comma if the first time foreach (var value in values) { if (!first) ret.Append(delimiter); ret.Append(value); first = false; } return ret.ToString(); } /// /// Patches a method manually. /// /// The Harmony instance. /// The class to modify. /// The method to patch. /// The prefix to apply, or null if none. /// The postfix to apply, or null if none. public static void Patch(this Harmony instance, Type type, string methodName, HarmonyMethod prefix = null, HarmonyMethod postfix = null) { if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(methodName)) throw new ArgumentNullException(nameof(methodName)); // Fetch the method try { var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS | BindingFlags. Static | BindingFlags.Instance); if (method != null) instance.Patch(method, prefix, postfix); else PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName, type.FullName)); } catch (AmbiguousMatchException e) { #if DEBUG PUtil.LogWarning("When patching candidate method {0}.{1}:".F(type.FullName, methodName)); #endif PUtil.LogException(e); } } /// /// Patches a constructor manually. /// /// The Harmony instance. /// The class to modify. /// The constructor's argument types. /// The prefix to apply, or null if none. /// The postfix to apply, or null if none. public static void PatchConstructor(this Harmony instance, Type type, Type[] arguments, HarmonyMethod prefix = null, HarmonyMethod postfix = null) { if (type == null) throw new ArgumentNullException(nameof(type)); // Fetch the constructor try { var cons = type.GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Static | BindingFlags.Instance, null, arguments, null); if (cons != null) instance.Patch(cons, prefix, postfix); else PUtil.LogWarning("Unable to find constructor on type {0}".F(type. FullName)); } catch (ArgumentException e) { PUtil.LogException(e); } } /// /// Patches a method manually with a transpiler. /// /// The Harmony instance. /// The class to modify. /// The method to patch. /// The transpiler to apply. public static void PatchTranspile(this Harmony instance, Type type, string methodName, HarmonyMethod transpiler) { if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(methodName)) throw new ArgumentNullException(nameof(methodName)); // Fetch the method try { var method = type.GetMethod(methodName, PPatchTools.BASE_FLAGS | BindingFlags.Static | BindingFlags.Instance); if (method != null) instance.Patch(method, null, null, transpiler); else PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName, type.FullName)); } catch (AmbiguousMatchException e) { PUtil.LogException(e); } catch (FormatException e) { PUtil.LogWarning("Unable to transpile method {0}: {1}".F(methodName, e.Message)); } } /// /// Sets a game object's parent. /// /// The game object to modify. /// The new parent object. /// The game object, for call chaining. public static GameObject SetParent(this GameObject child, GameObject parent) { if (child == null) throw new ArgumentNullException(nameof(child)); #pragma warning disable IDE0031 // Use null propagation child.transform.SetParent((parent == null) ? null : parent.transform, false); #pragma warning restore IDE0031 // Use null propagation return child; } } }