Utils.cs

Utility class and extensions for a game project. Provides JSON-safe filesystem read extension, random selection and weighted random, JSON-to-primitive helpers, time formatting, math and vector helpers, shuffling, easing and mapping functions, and many easing implementations.

File AccessHttp Calls
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

public enum EasingType
{
	None = -1,
	Linear = 0,
	SineIn, SineOut, SineInOut,
	QuadIn, QuadOut, QuadInOut,
	CubicIn, CubicOut, CubicInOut,
	QuartIn, QuartOut, QuartInOut,
	QuintIn, QuintOut, QuintInOut,
	ExpoIn, ExpoOut, ExpoInOut,
	ExtremeIn, ExtremeOut, ExtremeInOut,
	ElasticIn, ElasticOut, ElasticInOut,
	ElasticSoftIn, ElasticSoftOut, ElasticSoftInOut,
	BackIn, BackOut, BackInOut,
	BounceIn, BounceOut, BounceInOut
};

public static class FileSystemExtensions
{
	public static T ReadJsonSafe<T>( this BaseFileSystem fs, string path, T defaultValue )
	{
		try
		{
			return fs.ReadJson<T>( path, defaultValue );
		}
		catch ( System.Text.Json.JsonException ex )
		{
			Log.Warning( $"Failed to read JSON from '{path}', using default: {ex.Message}" );
			return defaultValue;
		}
	}
}

public static class Utils
{
	public static T Select<T>( int input, T value0, T value1, T value2 )
	{
		switch ( input )
		{
			case 0: default: return value0;
			case 1: return value1;
			case 2: return value2;
		}
	}

	public static T GetWeightedRandom<T>( Dictionary<T, float> weights )
	{
		if ( weights == null || weights.Count == 0 )
			return default;

		float totalWeight = 0f;
		foreach ( var pair in weights )
		{
			if ( pair.Value > 0f )
				totalWeight += pair.Value;
		}

		if ( totalWeight <= 0f )
			return default;

		float rand = Game.Random.Float( 0f, totalWeight );
		float cumulative = 0f;

		foreach ( var pair in weights )
		{
			if ( pair.Value <= 0f )
				continue;

			cumulative += pair.Value;
			if ( rand < cumulative )
				return pair.Key;
		}

		// Fallback in case of floating point errors
		foreach ( var pair in weights )
		{
			if ( pair.Value > 0f )
				return pair.Key;
		}

		return default;
	}

	//public static void DrawCircle( Vector2 pos, float radius, int num_segments, float starting_angle, Color color, bool depthTest = false, float duration = 0f, float zHeight = 0f )
	//{
	//	var step = 2f * MathF.PI / (float)num_segments;
	//	for ( int i = 1; i < num_segments + 1; i++ )
	//	{
	//		var to_angle = i * step + starting_angle;
	//		var to_point = new Vector3( pos.x + radius * MathF.Cos( to_angle ), pos.y + radius * MathF.Sin( to_angle ), zHeight );

	//		var from_angle = (i - 1) * step + starting_angle;
	//		var from_point = new Vector3( pos.x + radius * MathF.Cos( from_angle ), pos.y + radius * MathF.Sin( from_angle ), zHeight );

	//		DebugOverlay.Line( from_point, to_point, color, duration, depthTest );
	//	}
	//}

	public static int GetIntFromDictionary( Dictionary<string, object> data, string key, int fallback = -1)
	{
		if ( data == null )
			return fallback;

		int output = fallback;
		if ( data.TryGetValue( key, out var outputObj ) )
		{
			if ( outputObj is JsonElement element )
			{
				if ( element.ValueKind == JsonValueKind.Number )
				{
					output = element.GetInt32();
				}
				else
				{
					Log.Warning(
						$"GetIntFromDictionary: Key '{key}' is not a number (type: {element.ValueKind}), value: {element.GetRawText()}, returning fallback {fallback}."
					);
				}
			}
			else
			{
				Log.Warning( $"GetIntFromDictionary: Key '{key}' is not a JsonElement (type: {outputObj?.GetType().Name}), returning fallback {fallback}." );
			}
		}
		return output;
	}

	public static float GetFloatFromDictionary( Dictionary<string, object> data, string key, float fallback = 0f )
	{
		if ( data == null )
			return fallback;

		float output = fallback;
		if ( data.TryGetValue( key, out var outputObj ) )
		{
			if ( outputObj is JsonElement element )
			{
				if ( element.ValueKind == JsonValueKind.Number )
				{
					output = element.GetSingle();
				}
				else
				{
					Log.Warning(
						$"GetFloatFromDictionary: Key '{key}' is not a number (type: {element.ValueKind}), value: {element.GetRawText()}, returning fallback {fallback}."
					);
				}
			}
			else
			{
				Log.Warning( $"GetFloatFromDictionary: Key '{key}' is not a JsonElement (type: {outputObj?.GetType().Name}), returning fallback {fallback}." );
			}
		}
		return output;
	}

	public static long GetLongFromDictionary( Dictionary<string, object> data, string key, long fallback = 0 )
	{
		if ( data == null )
			return fallback;

		long output = fallback;
		if ( data.TryGetValue( key, out var outputObj ) )
		{
			if ( outputObj is JsonElement element )
			{
				if ( element.ValueKind == JsonValueKind.Number )
				{
					output = element.GetInt64();
				}
				else
				{
					Log.Warning(
						$"GetLongFromDictionary: Key '{key}' is not a number (type: {element.ValueKind}), value: {element.GetRawText()}, returning fallback {fallback}."
					);
				}
			}
			else
			{
				Log.Warning( $"GetLongFromDictionary: Key '{key}' is not a JsonElement (type: {outputObj?.GetType().Name}), returning fallback {fallback}." );
			}
		}
		return output;
	}

	public static string GetStringFromDictionary( Dictionary<string, object> data, string key, string fallback = "" )
	{
		if ( data == null )
			return fallback;

		string output = fallback;
		if ( data.TryGetValue( key, out var outputObj ) )
			output = ((JsonElement)outputObj).ToString();
		//output = ((JsonElement)outputObj).GetRawText();

		return output;
	}

	public static string FormatTime( double seconds, bool showMilliseconds = false )
	{
		// Convert the seconds to minutes, remaining seconds, and milliseconds
		int minutes = (int)seconds / 60;
		int remainingSeconds = (int)seconds % 60;

		if ( showMilliseconds )
		{
			int milliseconds = (int)((seconds - (int)seconds) * 1000);
			return $"{minutes}:{remainingSeconds:D2}.{milliseconds:D3}";
		}

		return $"{minutes}:{remainingSeconds:D2}";
	}

	public static bool HasSameMinutesAndSeconds( double time1, double time2 )
	{
		return (int)time1 / 60 == (int)time2 / 60 && (int)time1 % 60 == (int)time2 % 60;
	}

	public static string GetMillisecondsString( double seconds )
	{
		int milliseconds = (int)((seconds - (int)seconds) * 1000);
		return $".{milliseconds:D3}";
	}

	public static Dictionary<int, int> JsonObjectToIntDictionary( object jsonObject )
	{
		var dict = new Dictionary<int, int>();

		try
		{
			string jsonString = null;

			if ( jsonObject is JsonElement element )
			{
				if ( element.ValueKind == JsonValueKind.String )
				{
					jsonString = element.GetString();
					//Log.Info( $"JsonObjectToIntDictionary: Processing JSON string: {jsonString}" );
				}
				else
				{
					Log.Warning( $"JsonObjectToIntDictionary: Unsupported JsonElement type: {element.ValueKind}" );
					return dict;
				}
			}
			else if ( jsonObject is string str )
			{
				// Direct string input
				jsonString = str;
				//Log.Info( $"JsonObjectToIntDictionary: Processing direct string: {jsonString}" );
			}
			else
			{
				Log.Warning( $"JsonObjectToIntDictionary: Unsupported input type: {jsonObject?.GetType()}" );
				return dict;
			}

			// Deserialize the JSON string to Dictionary<int, int>
			if ( !string.IsNullOrEmpty( jsonString ) )
			{
				dict = JsonSerializer.Deserialize<Dictionary<int, int>>( jsonString );
				//Log.Info( $"JsonObjectToIntDictionary: Successfully deserialized {dict.Count} entries" );
			}
		}
		catch ( Exception ex )
		{
			Log.Error( $"JsonObjectToIntDictionary: Error deserializing: {ex.Message}" );
		}

		return dict;
	}

	private static Dictionary<int, int> HandleOldObjectFormat( JsonElement element )
	{
		var dict = new Dictionary<int, int>();

		// Handle the old broken format where values became arrays
		foreach ( var property in element.EnumerateObject() )
		{
			if ( int.TryParse( property.Name, out int key ) )
			{
				// For old format, we can't recover the values since they're empty arrays
				// Just skip them
				Log.Info( $"HandleOldObjectFormat: Skipping old format entry for key {key}" );
			}
		}

		return dict;
	}

	public static void Shuffle<T>( this IList<T> list )
	{
		int n = list.Count;
		while ( n > 1 )
		{
			n--;
			int k = Game.Random.Next( n + 1 );
			T value = list[k];
			list[k] = list[n];
			list[n] = value;
		}
	}

	public const float Deg2Rad = MathF.PI / 180f;
	public const float Rad2Deg = 360f / (MathF.PI * 2f);

	public const float Unit2Meter = 0.015f;
	public const float Meter2Unit = 1f / Unit2Meter;

	public static Vector2 RotateVector( Vector2 v, float degrees )
	{
		var rads = Deg2Rad * degrees;
		var ca = MathF.Cos( rads );
		var sa = MathF.Sin( rads );

		return new Vector2( ca * v.x - sa * v.y, sa * v.x + ca * v.y );
	}

	public static Vector2 GetReflectedVector( Vector2 vector, Vector2 normal )
	{
		return vector - 2 * Vector2.Dot( vector, normal ) * normal;
	}

	public static float GetAngleDegreesFromVector( Vector2 vector )
	{
		return Rad2Deg * ((float)(Math.Atan2( vector.x, vector.y ) - Math.Atan2( 1.0f, 0.0f )));
	}

	public static float GetAngleDegreesFromVectorAlt( Vector2 vector )
	{
		return MathF.Atan2( vector.y, vector.x ) * (180 / MathF.PI);
	}

	public static Vector2 GetVector2FromAngle( float radians )
	{
		return new Vector2( MathF.Cos( radians ), MathF.Sin( -radians ) );
	}

	public static Vector2 GetVector2FromAngleDegrees( float degrees )
	{
		return GetVector2FromAngle( degrees * Deg2Rad );
	}

	public static Vector2 GetPerpendicularVector( Vector2 vec )
	{
		return new Vector2( -vec.y, vec.x );
	}

	public static Vector2 GetRandomVector()
	{
		return GetVector2FromAngleDegrees( Game.Random.Float( 0f, 360f ) );
	}

	public static Vector2 GetRandomVectorInCone( Vector2 dir, float coneDegrees = 120f )
	{
		float angle = Game.Random.Float( -coneDegrees / 2f, coneDegrees / 2f );
		return RotateVector( dir, angle );
	}

	public static float FastSin( float input )
	{
		// wrap input angle to -PI..PI
		while ( input < -3.14159265f )
			input += 6.28318531f;
		while ( input > 3.14159265f )
			input -= 6.28318531f;

		return 1.27323954f * input + 0.405284735f * (input < 0f ? 1f : -1f) * input * input;
	}

	public static float DynamicEaseTo( float current, float goal, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goal;

		return current + (goal - current) * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
	}

	public static Vector2 DynamicEaseTo( Vector2 current, Vector2 goal, float factorPercent, float dt, float referenceFrameRate = 60f )
	{
		if ( float.IsPositiveInfinity( dt ) )
			return goal;
		var diff = goal - current;

		return current + diff * (1f - MathF.Pow( 1f - MathX.Clamp( factorPercent, 0f, 1f ), dt * referenceFrameRate ));
	}

	public static float EaseUnclamped( float value, EasingType easingType )
	{
		switch ( easingType )
		{
			case EasingType.SineIn: return SineIn( value );
			case EasingType.SineOut: return SineOut( value );
			case EasingType.SineInOut: return SineInOut( value );

			case EasingType.QuadIn: return QuadIn( value );
			case EasingType.QuadOut: return QuadOut( value );
			case EasingType.QuadInOut: return QuadInOut( value );

			case EasingType.CubicIn: return CubicIn( value );
			case EasingType.CubicOut: return CubicOut( value );
			case EasingType.CubicInOut: return CubicInOut( value );

			case EasingType.QuartIn: return QuartIn( value );
			case EasingType.QuartOut: return QuartOut( value );
			case EasingType.QuartInOut: return QuartInOut( value );

			case EasingType.QuintIn: return QuintIn( value );
			case EasingType.QuintOut: return QuintOut( value );
			case EasingType.QuintInOut: return QuintInOut( value );

			case EasingType.ExpoIn: return ExpoIn( value );
			case EasingType.ExpoOut: return ExpoOut( value );
			case EasingType.ExpoInOut: return ExpoInOut( value );

			case EasingType.ExtremeIn: return ExtremeIn( value );
			case EasingType.ExtremeOut: return ExtremeOut( value );
			case EasingType.ExtremeInOut: return ExtremeInOut( value );

			case EasingType.ElasticIn: return ElasticIn( value );
			case EasingType.ElasticOut: return ElasticOut( value );
			case EasingType.ElasticInOut: return ElasticInOut( value );

			case EasingType.ElasticSoftIn: return ElasticSoftIn( value );
			case EasingType.ElasticSoftOut: return ElasticSoftOut( value );
			case EasingType.ElasticSoftInOut: return ElasticSoftInOut( value );

			case EasingType.BackIn: return BackIn( value );
			case EasingType.BackOut: return BackOut( value );
			case EasingType.BackInOut: return BackInOut( value );

			case EasingType.BounceIn: return BounceIn( value );
			case EasingType.BounceOut: return BounceOut( value );
			case EasingType.BounceInOut: return BounceInOut( value );
			default: return value;
		}
	}

	public static float Map( float value, float inputMin, float inputMax, float outputMin, float outputMax, EasingType easingType = EasingType.Linear, bool clamp = true )
	{
		if ( inputMin.Equals( inputMax ) || outputMin.Equals( outputMax ) )
			return outputMin;

		//            if (inputMin.Equals(inputMax) || outputMin.Equals(outputMax))
		//                return outputMax;

		if ( clamp )
		{
			// clamp input
			if ( inputMax > inputMin )
			{
				if ( value < inputMin ) value = inputMin;
				else if ( value > inputMax ) value = inputMax;
			}
			else if ( inputMax < inputMin )
			{
				if ( value > inputMin ) value = inputMin;
				else if ( value < inputMax ) value = inputMax;
			}
		}

		var ratio = EaseUnclamped( (value - inputMin) / (inputMax - inputMin), easingType );

		var outVal = outputMin + ratio * (outputMax - outputMin);

		//            // clamp output
		//            if (outputMax < outputMin) {
		//                if (outVal < outputMax) outVal = outputMax;
		//                else if (outVal > outputMin) outVal = outputMin;
		//            } else {
		//                if (outVal > outputMax) outVal = outputMax;
		//                else if (outVal < outputMin) outVal = outputMin;
		//            }

		return outVal;
	}

	public static float MapReturn( float value, float inputMin, float inputMax, float outputMin, float outputMax, EasingType easingType = EasingType.Linear )
	{
		var halfway = inputMin + (inputMax - inputMin) * 0.5f;

		if ( inputMax > inputMin && value < halfway || inputMax < inputMin && value > halfway )
			return Map( value, inputMin, halfway, outputMin, outputMax, easingType );
		else
			return Map( value, halfway, inputMax, outputMax, outputMin, GetOppositeEasingType( easingType ) );
	}

	public static float EasePercent( float percent, EasingType easingType )
	{
		return Map( percent, 0f, 1f, 0f, 1f, easingType );
	}

	public static EasingType GetOppositeEasingType( EasingType easingType )
	{
		var opposite = EasingType.Linear;
		switch ( easingType )
		{
			case EasingType.SineIn: opposite = EasingType.SineOut; break;
			case EasingType.SineOut: opposite = EasingType.SineIn; break;
			case EasingType.SineInOut: opposite = EasingType.SineInOut; break;

			case EasingType.QuadIn: opposite = EasingType.QuadOut; break;
			case EasingType.QuadOut: opposite = EasingType.QuadIn; break;
			case EasingType.QuadInOut: opposite = EasingType.QuadInOut; break;

			case EasingType.CubicIn: opposite = EasingType.CubicOut; break;
			case EasingType.CubicOut: opposite = EasingType.CubicIn; break;
			case EasingType.CubicInOut: opposite = EasingType.CubicInOut; break;

			case EasingType.QuartIn: opposite = EasingType.QuartOut; break;
			case EasingType.QuartOut: opposite = EasingType.QuartIn; break;
			case EasingType.QuartInOut: opposite = EasingType.QuartInOut; break;

			case EasingType.QuintIn: opposite = EasingType.QuintOut; break;
			case EasingType.QuintOut: opposite = EasingType.QuintIn; break;
			case EasingType.QuintInOut: opposite = EasingType.QuintInOut; break;

			case EasingType.ExpoIn: opposite = EasingType.ExpoOut; break;
			case EasingType.ExpoOut: opposite = EasingType.ExpoIn; break;
			case EasingType.ExpoInOut: opposite = EasingType.ExpoInOut; break;

			case EasingType.ExtremeIn: opposite = EasingType.ExtremeOut; break;
			case EasingType.ExtremeOut: opposite = EasingType.ExtremeIn; break;
			case EasingType.ExtremeInOut: opposite = EasingType.ExtremeInOut; break;

			case EasingType.ElasticIn: opposite = EasingType.ElasticOut; break;
			case EasingType.ElasticOut: opposite = EasingType.ElasticIn; break;
			case EasingType.ElasticInOut: opposite = EasingType.ElasticInOut; break;

			case EasingType.ElasticSoftIn: opposite = EasingType.ElasticSoftOut; break;
			case EasingType.ElasticSoftOut: opposite = EasingType.ElasticSoftIn; break;
			case EasingType.ElasticSoftInOut: opposite = EasingType.ElasticSoftInOut; break;

			case EasingType.BackIn: opposite = EasingType.BackOut; break;
			case EasingType.BackOut: opposite = EasingType.BackIn; break;
			case EasingType.BackInOut: opposite = EasingType.BackInOut; break;

			case EasingType.BounceIn: opposite = EasingType.BounceOut; break;
			case EasingType.BounceOut: opposite = EasingType.BounceIn; break;
			case EasingType.BounceInOut: opposite = EasingType.BounceInOut; break;
		}

		return opposite;
	}

	public static float SineIn( float t ) { return 1f - MathF.Cos( t * MathF.PI * 0.5f ); }
	public static float SineOut( float t ) { return MathF.Sin( t * (MathF.PI * 0.5f) ); }
	public static float SineInOut( float t ) { return -0.5f * (MathF.Cos( MathF.PI * t ) - 1f); }

	public static float QuadIn( float t ) { return t * t; }
	public static float QuadOut( float t ) { return t * (2f - t); }
	public static float QuadInOut( float t ) { return t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t; }

	public static float CubicIn( float t ) { return t * t * t; }
	public static float CubicOut( float t ) { var t1 = t - 1f; return t1 * t1 * t1 + 1f; }
	public static float CubicInOut( float t ) { return t < 0.5f ? 4f * t * t * t : (t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f; }

	public static float QuartIn( float t ) { return t * t * t * t; }
	public static float QuartOut( float t ) { var t1 = t - 1f; return 1f - t1 * t1 * t1 * t1; }
	public static float QuartInOut( float t ) { var t1 = t - 1f; return t < 0.5f ? 8f * t * t * t * t : 1f - 8f * t1 * t1 * t1 * t1; }

	public static float QuintIn( float t ) { return t * t * t * t * t; }
	public static float QuintOut( float t ) { var t1 = t - 1f; return 1f + t1 * t1 * t1 * t1 * t1; }
	public static float QuintInOut( float t ) { var t1 = t - 1f; return t < 0.5f ? 16f * t * t * t * t * t : 1f + 16f * t1 * t1 * t1 * t1 * t1; }

	public static float ExpoIn( float t ) { return MathF.Pow( 2f, 10f * (t - 1f) ); }
	public static float ExpoOut( float t ) { return 1f - MathF.Pow( 2f, -10f * t ); }
	public static float ExpoInOut( float t ) { return t < 0.5f ? ExpoIn( t * 2f ) * 0.5f : 1f - ExpoIn( 2f - t * 2f ) * 0.5f; }

	public static float ExtremeIn( float t ) { return MathF.Pow( 10f, 10f * (t - 1f) ); }
	public static float ExtremeOut( float t ) { return 1f - MathF.Pow( 10f, -10f * t ); }
	public static float ExtremeInOut( float t ) { return t < 0.5f ? ExtremeIn( t * 2f ) * 0.5f : 1f - ExtremeIn( 2f - t * 2f ) * 0.5f; }

	public static float ElasticIn( float t ) { return 1f - ElasticOut( 1f - t ); }
	public static float ElasticOut( float t ) { var p = 0.3f; return MathF.Pow( 2f, -10f * t ) * MathF.Sin( (t - p / 4f) * (2f * (float)Math.PI) / p ) + 1f; }
	public static float ElasticInOut( float t ) { return t < 0.5f ? ElasticIn( t * 2f ) * 0.5f : 1f - ElasticIn( 2f - t * 2f ) * 0.5f; }

	public static float ElasticSoftIn( float t ) { return 1f - ElasticSoftOut( 1f - t ); }
	public static float ElasticSoftOut( float t ) { var p = 0.5f; return MathF.Pow( 2f, -10f * t ) * MathF.Sin( (t - p / 4f) * (2f * (float)Math.PI) / p ) + 1f; }
	public static float ElasticSoftInOut( float t ) { return t < 0.5f ? ElasticSoftIn( t * 2f ) * 0.5f : 1f - ElasticSoftIn( 2f - t * 2f ) * 0.5f; }

	public static float BackIn( float t ) { var p = 1f; return t * t * ((p + 1f) * t - p); }
	public static float BackOut( float t ) { var p = 1f; var scaledTime = t / 1f - 1f; return scaledTime * scaledTime * ((p + 1f) * scaledTime + p) + 1f; }
	public static float BackInOut( float t )
	{
		var p = 1f;
		var scaledTime = t * 2f;
		var scaledTime2 = scaledTime - 2f;
		var s = p * 1.525f;

		if ( scaledTime < 1f ) return 0.5f * scaledTime * scaledTime * ((s + 1f) * scaledTime - s);
		else return 0.5f * (scaledTime2 * scaledTime2 * ((s + 1f) * scaledTime2 + s) + 2f);
	}

	public static float BounceIn( float t ) { return 1f - BounceOut( 1f - t ); }
	public static float BounceOut( float t )
	{
		var scaledTime = t / 1f;

		if ( scaledTime < 1 / 2.75f )
		{
			return 7.5625f * scaledTime * scaledTime;
		}
		else if ( scaledTime < 2 / 2.75 )
		{
			var scaledTime2 = scaledTime - 1.5f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.75f;
		}
		else if ( scaledTime < 2.5 / 2.75 )
		{
			var scaledTime2 = scaledTime - 2.25f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.9375f;
		}
		else
		{
			var scaledTime2 = scaledTime - 2.625f / 2.75f;
			return 7.5625f * scaledTime2 * scaledTime2 + 0.984375f;
		}
	}
	public static float BounceInOut( float t )
	{
		if ( t < 0.5 ) return BounceIn( t * 2f ) * 0.5f;
		else return BounceOut( t * 2f - 1f ) * 0.5f + 0.5f;
	}
}