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.
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;
}
}