Files
LowPolyRPG/Assets/Plugins/Nullframes/Intrigues/Scripts/Utils/NullUtils.cs

726 lines
27 KiB
C#
Raw Normal View History

2025-06-25 11:10:11 +01:00
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Nullframes.Intrigues.Graph;
using Nullframes.Intrigues.Utils;
using UnityEngine;
using Object = UnityEngine.Object;
using Random = System.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Nullframes.Intrigues {
public class CoroutineData {
public Coroutine Coroutine { get; set; }
public float Delay { get; set; }
public float RemainingTime { get; set; }
public int LoopCount { get; set; }
public Action Call { get; }
public Func< bool > WaitUntil { get; }
public Action< float > OnUpdate { get; set; }
public bool UnscaledTime { get; }
public bool Paused { get; set; }
public CoroutineData(Coroutine coroutine, float delay, Func< bool > waitUntil, int loopCount, Action call,
bool unscaledTime) {
Coroutine = coroutine;
Delay = delay;
RemainingTime = delay;
WaitUntil = waitUntil;
LoopCount = loopCount;
Call = call;
UnscaledTime = unscaledTime;
Paused = false;
}
}
public class DelayedCallParams {
public string DelayName;
public float Delay = 0f;
public Func< bool > WaitUntil;
public Action Call;
public Action< float > OnUpdate;
public int LoopCount = 0;
public bool UnscaledTime = false;
}
public static class NullUtils {
public static float Round(this float number) {
return Mathf.Round(number * 100f) * .01f;
}
public static bool TryPop<T>(Stack<T> stack, out T value)
{
if (stack.Count > 0)
{
value = stack.Pop();
return true;
}
value = default;
return false;
}
// For List<T> using UnityEngine.Random
public static T PickRandom< T >(this List< T > list) {
if ( list == null || list.Count == 0 )
throw new InvalidOperationException("The list cannot be empty or null.");
int index = UnityEngine.Random.Range(0, list.Count);
return list[ index ];
}
// For IReadOnlyList<T> using UnityEngine.Random
public static T PickRandom< T >(this IReadOnlyList< T > list) {
if ( list == null || list.Count == 0 )
throw new InvalidOperationException("The list cannot be empty or null.");
int index = UnityEngine.Random.Range(0, list.Count);
return list[ index ];
}
// For IEnumerable<T> using reservoir sampling
public static T PickRandom< T >(this IEnumerable< T > sequence) {
if ( sequence == null )
throw new ArgumentNullException(nameof( sequence ), "The sequence cannot be null.");
int count = 0;
T current = default;
foreach ( var element in sequence ) {
count++;
if ( UnityEngine.Random.Range(0, count) == 0 )
current = element;
}
if ( count == 0 )
throw new InvalidOperationException("The sequence cannot be empty.");
return current;
}
public static float Remap(this float value, float fromMin, float fromMax, float toMin, float toMax) {
return ( value - fromMin ) / ( fromMax - fromMin ) * ( toMax - toMin ) + toMin;
}
private static readonly Dictionary< string, CoroutineData > coroutines = new();
public static Dictionary< string, CoroutineData > Coroutines => coroutines;
public static async Task WaitUntilAsync(Func<bool> condition, int checkIntervalMs = 10, int timeoutMs = -1)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
while (!condition())
{
await Task.Delay(checkIntervalMs);
if (timeoutMs > 0 && stopwatch.ElapsedMilliseconds > timeoutMs)
break;
}
}
public static string DelayedCall(DelayedCallParams config) {
string delayName = config.DelayName ?? GenerateID();
if ( coroutines.ContainsKey(delayName) )
StopCall(delayName);
var data = new CoroutineData(
coroutine: null,
delay: config.Delay,
waitUntil: config.WaitUntil,
loopCount: config.LoopCount,
call: config.Call,
unscaledTime: config.UnscaledTime
);
if ( config.OnUpdate != null )
data.OnUpdate += config.OnUpdate;
coroutines[ delayName ] = data;
coroutines[ delayName ].Coroutine = CoroutineManager.StartRoutine(delayedCall(delayName));
return delayName;
}
private static IEnumerator delayedCall(string delayName) {
if ( !coroutines.TryGetValue(delayName, out var coroutineData) )
yield break;
// Optional: wait until a custom condition is met
if ( coroutineData.WaitUntil != null )
yield return new WaitUntil(coroutineData.WaitUntil);
// Delay of -1 means wait forever
if ( coroutineData.Delay.Equals(-1) )
yield return new WaitUntil(() => false);
// Handle time-based delay (scaled vs unscaled)
while ( coroutineData.RemainingTime > 0 ) {
float delta = coroutineData.UnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
coroutineData.RemainingTime -= delta;
if ( coroutineData.RemainingTime < 0 )
coroutineData.RemainingTime = 0;
coroutineData.OnUpdate?.Invoke(coroutineData.RemainingTime);
yield return null;
}
// Reset remaining time for next loop
coroutineData.RemainingTime = coroutineData.Delay;
// Execute callback
coroutineData.Call?.Invoke();
// Decrease loop count only if greater than 0
if ( coroutineData.LoopCount > 0 )
coroutineData.LoopCount--;
// Loop if infinite or still has remaining cycles
if ( coroutineData.LoopCount is -1 or > 0 ) {
coroutineData.Coroutine = CoroutineManager.StartRoutine(delayedCall(delayName));
} else {
StopCall(delayName);
}
}
public static bool StopCall(string delayName) {
if ( string.IsNullOrEmpty(delayName) || !coroutines.ContainsKey(delayName) ) return false;
CoroutineManager.StopRoutine(coroutines[ delayName ].Coroutine);
coroutines.Remove(delayName);
return true;
}
public static bool PauseCall(string delayName) {
if ( !coroutines.ContainsKey(delayName) || coroutines[ delayName ].Paused ) return false;
coroutines[ delayName ].Paused = true;
CoroutineManager.StopRoutine(coroutines[ delayName ].Coroutine);
return true;
}
public static bool ResumeCall(string delayName) {
if ( !coroutines.ContainsKey(delayName) || !coroutines[ delayName ].Paused ) return false;
coroutines[ delayName ].Paused = false;
coroutines[ delayName ].Coroutine = CoroutineManager.StartRoutine(delayedCall(delayName));
return true;
}
public static CoroutineData CoroutineData(string delayName) {
if ( !coroutines.ContainsKey(delayName) ) return null;
return coroutines[ delayName ];
}
public static void SetDelay(string delayName, float newDelay) {
if ( !coroutines.ContainsKey(delayName) ) return;
coroutines[ delayName ].RemainingTime = newDelay;
coroutines[ delayName ].Delay = newDelay;
}
public static void SetLoop(string delayName, int loopCount) {
if ( coroutines.ContainsKey(delayName) ) coroutines[ delayName ].LoopCount = loopCount;
}
public static bool IsRunning(string callName) {
return coroutines.ContainsKey(callName);
}
public static void SaveAsset(params Object[ ] assets) {
#if UNITY_EDITOR
foreach ( var asset in assets ) {
if ( asset == null ) return;
EditorUtility.SetDirty(asset);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
#endif
}
public static string BracesIn(this string text, string keyword) {
var replaceText = (string)IM.Variables.FirstOrDefault(v => v.name == keyword);
replaceText ??= $"{{{keyword}}}";
return Regex.Replace(text, $"{{{keyword}}}", replaceText);
}
public static void AddItem< T, Y >(this SerializableDictionary< T, List< Y > > serializableDictionary, T key,
Y value) {
if ( serializableDictionary.ContainsKey(key) ) {
serializableDictionary[ key ].Add(value);
return;
}
serializableDictionary.Add(key, new List< Y >() { value });
}
public static string GenerateID() {
return Guid.NewGuid().ToString();
}
public static bool ExistsClan(this IEnumerable< Clan > clans, string clanName, out Clan clan) {
clan = clans.FirstOrDefault(d => d.ClanName == clanName);
return clan != null;
}
public static void Trigger(this Scheme scheme, string triggerName, bool value = true) {
IM.Trigger(scheme, triggerName, value);
}
public static string Shortener(this string actorName, int length = 8) {
if ( actorName.Length > length ) {
return actorName[ ..length ] + "..";
}
return actorName;
}
private static bool IsWhitespace(this char character) {
switch ( character ) {
case '\u0020':
case '\u00A0':
case '\u1680':
case '\u2000':
case '\u2001':
case '\u2002':
case '\u2003':
case '\u2004':
case '\u2005':
case '\u2006':
case '\u2007':
case '\u2008':
case '\u2009':
case '\u200A':
case '\u202F':
case '\u205F':
case '\u3000':
case '\u2028':
case '\u2029':
case '\u0009':
case '\u000A':
case '\u000B':
case '\u000C':
case '\u000D':
case '\u0085': {
return true;
}
default: {
return false;
}
}
}
public static string RemoveWhitespaces(this string text) {
var textLength = text.Length;
var textCharacters = text.ToCharArray();
var currentWhitespacelessTextLength = 0;
for ( var currentCharacterIndex = 0; currentCharacterIndex < textLength; ++currentCharacterIndex ) {
var currentTextCharacter = textCharacters[ currentCharacterIndex ];
if ( currentTextCharacter.IsWhitespace() ) continue;
textCharacters[ currentWhitespacelessTextLength++ ] = currentTextCharacter;
}
return new string(textCharacters, 0, currentWhitespacelessTextLength);
}
public static string RemoveSpecialCharacters(this string text) {
var textLength = text.Length;
var textCharacters = text.ToCharArray();
var currentWhitespacelessTextLength = 0;
for ( var currentCharacterIndex = 0; currentCharacterIndex < textLength; ++currentCharacterIndex ) {
var currentTextCharacter = textCharacters[ currentCharacterIndex ];
if ( !char.IsLetterOrDigit(currentTextCharacter) && !currentTextCharacter.IsWhitespace() ) continue;
textCharacters[ currentWhitespacelessTextLength++ ] = currentTextCharacter;
}
return new string(textCharacters, 0, currentWhitespacelessTextLength);
}
public static List< int > DivideNumber(this int number, int parts) {
var remainder = number % parts;
var partSize = number / parts;
var numbers = new List< int >();
if ( remainder == 0 ) {
for ( var i = 0; i < parts; i++ ) numbers.Add(partSize);
return numbers;
}
var largerParts = remainder;
var smallerParts = parts - remainder;
var largerPartSize = partSize + 1;
var smallerPartSize = partSize;
for ( var i = 0; i < largerParts; i++ ) numbers.Add(largerPartSize);
for ( var i = 0; i < smallerParts; i++ ) numbers.Add(smallerPartSize);
return numbers;
}
public static List< string > GetLanguageCodes() {
var cultureCodes = new List< string >();
var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
foreach ( var culture in cultures )
if ( !cultureCodes.Contains(culture.Name) )
cultureCodes.Add(culture.Name);
return cultureCodes;
}
public static string LocaliseText(this string source) {
if ( !IM.Exists || string.IsNullOrEmpty(source) ) return source;
var text = source;
foreach ( Match titleLocalisation in Regex.Matches(text, STATIC.LOCALISATION) )
if ( titleLocalisation.Success )
text = Regex.Replace(text, "{l:" + titleLocalisation.Value + "}",
IM.GetText(titleLocalisation.Value));
return text;
}
public static string SchemeFormat(this string source, Actor conspirator, Actor target) {
target ??= conspirator;
var replacements = new Dictionary< string, string > {
{ "{t*Name}", $"<link=\"{target.ID}\">{target.Name}</link>" },
{ "{c*Name}", $"<link=\"{conspirator.ID}\">{conspirator.Name}</link>" },
{ "{t*FullName}", $"<link=\"{target.ID}\">{target.FullName}</link>" },
{ "{c*FullName}", $"<link=\"{conspirator.ID}\">{conspirator.FullName}</link>" },
{ "{c*Gender}", conspirator.Gender.ToString() },
{ "{t*Gender}", target.Gender.ToString() },
{ "{c*Age}", conspirator.Age.ToString() },
{ "{t*Age}", target.Age.ToString() }
};
if ( conspirator.Culture != null ) replacements[ "{c*Culture}" ] = conspirator.Culture.CultureName;
if ( target.Culture != null ) replacements[ "{t*Culture}" ] = target.Culture.CultureName;
if ( conspirator.Family != null ) replacements[ "{c*Family}" ] = conspirator.Family.FamilyName;
if ( target.Family != null ) replacements[ "{t*Family}" ] = target.Family.FamilyName;
if ( conspirator.Role != null ) {
replacements[ "{c*Role}" ] = conspirator.Role.RoleName;
replacements[ "{c*Title}" ] = conspirator.Title;
}
if ( target.Role != null ) {
replacements[ "{t*Role}" ] = target.Role.RoleName;
replacements[ "{t*Title}" ] = target.Title;
}
if ( conspirator.Clan != null ) replacements[ "{c*Clan}" ] = conspirator.Clan.ClanName;
if ( target.Clan != null ) replacements[ "{t*Clan}" ] = target.Clan.ClanName;
// Replace fixed patterns (non-regex)
foreach ( var kvp in replacements )
source = source.Replace(kvp.Key, kvp.Value);
// Dynamic {c:...} and {t:...} variables
source = Regex.Replace(source, @"{c:(\w+)}", match => {
var value = conspirator.GetVariable(match.Groups[ 1 ].Value);
return value?.ToString() ?? string.Empty;
});
source = Regex.Replace(source, @"{t:(\w+)}", match => {
var value = target.GetVariable(match.Groups[ 1 ].Value);
return value?.ToString() ?? string.Empty;
});
return source;
}
public static float CalculatePercentage(this float number, float percentage) {
return number * percentage / 100f;
}
public static Color HTMLColor(string htmlString) {
return ColorUtility.TryParseHtmlString($"{htmlString}", out var clr) ? clr : new Color(1f, 1f, 1f, 1f);
}
public static Color RandomHTMLColor(params string[ ] htmlString) {
return ColorUtility.TryParseHtmlString($"{htmlString[ UnityEngine.Random.Range(0, htmlString.Length) ]}",
out var clr)
? clr
: new Color(1f, 1f, 1f, 1f);
}
public static string ToUpperPreserveTags(string input) {
if ( string.IsNullOrEmpty(input) ) return input;
var result = new System.Text.StringBuilder();
bool insideTag = false;
foreach ( char c in input ) {
if ( c == '<' ) insideTag = true;
if ( !insideTag )
result.Append(char.ToUpper(c, CultureInfo.InvariantCulture));
else
result.Append(c);
if ( c == '>' ) insideTag = false;
}
return result.ToString();
}
#if UNITY_EDITOR
public static void PlayClip(AudioClip clip, int startSample = 0, bool loop = false) {
if ( IsClipPlaying() ) return;
if ( clip == null ) return;
Assembly unityEditorAssembly = typeof( AudioImporter ).Assembly;
Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
MethodInfo method = audioUtilClass.GetMethod(
"PlayPreviewClip",
BindingFlags.Static | BindingFlags.Public,
null,
new[ ] { typeof( AudioClip ), typeof( int ), typeof( bool ) },
null
);
if ( method != null )
method.Invoke(
null,
new object[ ] { clip, startSample, loop }
);
}
public static bool IsClipPlaying() {
Assembly unityEditorAssembly = typeof( AudioImporter ).Assembly;
Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
MethodInfo method = audioUtilClass.GetMethod(
"IsPreviewClipPlaying",
BindingFlags.Static | BindingFlags.Public
);
bool playing = method != null && (bool)method.Invoke(null, null);
return playing;
}
public static void StopAllClips() {
Assembly unityEditorAssembly = typeof( AudioImporter ).Assembly;
Type audioUtilClass = unityEditorAssembly.GetType("UnityEditor.AudioUtil");
MethodInfo method = audioUtilClass.GetMethod(
"StopAllPreviewClips",
BindingFlags.Static | BindingFlags.Public,
null,
new Type[ ] { },
null
);
if ( method != null )
method.Invoke(
null,
new object[ ] { }
);
}
public static TextureImporterType GetTextureType(this Texture2D texture) {
var path = AssetDatabase.GetAssetPath(texture);
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if ( importer != null ) return importer.textureType;
return TextureImporterType.Default;
}
public static void SerializedProperty(this Object @object, Action< SerializedObject > @event = null,
bool ApplyModifiedProperties = true) {
var serializedObject = new SerializedObject(@object);
if ( @event != null ) {
@event.Invoke(serializedObject);
if ( ApplyModifiedProperties ) serializedObject.ApplyModifiedProperties();
}
}
public static SerializedObject Serialize(this Object @object) {
return new SerializedObject(@object);
}
public static T GetValue< T >(this SerializedProperty property) where T : class {
object obj = property.serializedObject.targetObject;
var path = property.propertyPath.Replace(".Array.data", "");
var fieldStructure = path.Split('.');
var rgx = new Regex(@"\[\d+\]");
foreach ( var t in fieldStructure )
if ( t.Contains("[") ) {
var index = Convert.ToInt32(new string(t.Where(char.IsDigit).ToArray()));
obj = GetFieldValueWithIndex(rgx.Replace(t, ""), obj, index);
} else {
obj = GetFieldValue(t, obj);
}
return (T)obj;
}
public static bool SetValue< T >(this SerializedProperty property, T value) where T : class {
object obj = property.serializedObject.targetObject;
var path = property.propertyPath.Replace(".Array.data", "");
var fieldStructure = path.Split('.');
var rgx = new Regex(@"\[\d+\]");
for ( var i = 0; i < fieldStructure.Length - 1; i++ )
if ( fieldStructure[ i ].Contains("[") ) {
var index = Convert.ToInt32(new string(fieldStructure[ i ].Where(char.IsDigit).ToArray()));
obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[ i ], ""), obj, index);
} else {
obj = GetFieldValue(fieldStructure[ i ], obj);
}
var fieldName = fieldStructure.Last();
if ( fieldName.Contains("[") ) {
var index = Convert.ToInt32(new string(fieldName.Where(char.IsDigit).ToArray()));
return SetFieldValueWithIndex(rgx.Replace(fieldName, ""), obj, index, value);
}
return SetFieldValue(fieldName, obj, value);
}
private static object GetFieldValue(string fieldName, object obj,
BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic) {
var field = obj.GetType().GetField(fieldName, bindings);
if ( field != null ) return field.GetValue(obj);
return default;
}
private static object GetFieldValueWithIndex(string fieldName, object obj, int index,
BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic) {
var field = obj.GetType().GetField(fieldName, bindings);
if ( field != null ) {
var list = field.GetValue(obj);
if ( list.GetType().IsArray ) return ( (object[ ])list )[ index ];
if ( list is IEnumerable ) return ( (IList)list )[ index ];
}
return default;
}
private static FieldInfo FindFieldRecursively(Type type, string fieldName, BindingFlags bindings) {
FieldInfo field = null;
while ( type != null && field == null ) {
field = type.GetField(fieldName, bindings);
type = type.BaseType;
}
return field;
}
private static bool SetFieldValue(string fieldName, object obj, object value,
BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic) {
var field = obj.GetType().GetField(fieldName, bindings);
if ( field != null ) {
field.SetValue(obj, value);
return true;
}
return false;
}
private static bool SetFieldValueWithIndex(string fieldName, object obj, int index, object value,
BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic) {
var field = obj.GetType().GetField(fieldName, bindings);
Debug.Log(field?.Name);
if ( field == null ) return false;
var list = field.GetValue(obj);
if ( list.GetType().IsArray ) {
( (object[ ])list )[ index ] = value;
return true;
}
if ( list is not IEnumerable ) return false;
( (IList)list )[ index ] = value;
return true;
}
#endif
public static bool CheckProbability(float percentage) {
percentage = Math.Max(0f, Math.Min(100f, percentage));
Random random = new Random();
int randomValue = random.Next(10000);
return randomValue < percentage * 100;
}
public static void Adds< T >(this List< T > list, params T[ ] item) {
list.AddRange(item);
}
public static NVar GetVariable(this Actor actor, string variableNameOrId) =>
actor.GetVariable(variableNameOrId);
public static void SetVariable(this Actor actor, string variableNameOrId, object value) {
actor.SetVariable(variableNameOrId, value);
}
public static void SetVariable(this Schemer schemer, string variableName, object value) {
var variable = schemer.GetVariable(variableName);
if ( variable == null ) return;
variable.value = value;
}
public static T Duplicate< T >(this GameObject @object) {
return Object.Instantiate(@object, @object.transform.parent).GetComponent< T >();
}
public static T Duplicate< T >(this GameObject @object, Transform parent) {
return Object.Instantiate(@object, parent).GetComponent< T >();
}
public static readonly KeyCode[ ] _keyCodes =
Enum.GetValues(typeof( KeyCode ))
.Cast< KeyCode >()
.ToArray();
public static readonly string[ ] _keyNames =
Enum.GetNames(typeof( KeyCode ))
.ToArray();
public static void GetCurrentKeysDown(List< KeyCode > buffer) {
foreach ( var key in _keyCodes ) {
if ( Input.GetKeyDown(key) )
buffer.Add(key);
}
}
public static void GetCurrentKeysUp(List< KeyCode > buffer) {
foreach ( var key in _keyCodes ) {
if ( Input.GetKeyUp(key) )
buffer.Add(key);
}
}
public static void GetCurrentKeys(List< KeyCode > buffer) {
foreach ( var key in _keyCodes ) {
if ( Input.GetKey(key) )
buffer.Add(key);
}
}
}
}