DiamondSettings.cs
using System;
using Sandbox;

namespace Diamonds;

/// <summary>
/// Player preferences from the options overlay, persisted as JSON in the game's local data folder.
/// Volumes are slider positions from zero to one, applied as linear gain.
/// </summary>
public sealed class DiamondSettings
{
	public const string FilePath = "diamonds_settings.json";
	static DiamondSettings current;

	/// <summary>Use a dim static dye background instead of gameplay fluid. Keep the original JSON key for saved preferences.</summary>
	[System.Text.Json.Serialization.JsonPropertyName( "HideDye" )]
	public bool DisableFluid { get; set; }

	/// <summary>Overall level for every game sound, including music.</summary>
	public float MasterVolume { get => masterVolume; set => masterVolume = Clamp( value ); }
	float masterVolume = 1;

	/// <summary>Music level, applied on top of the master volume.</summary>
	public float MusicVolume { get => musicVolume; set => musicVolume = Clamp( value ); }
	float musicVolume = 1;

	/// <summary>Controller vibration strength, from zero (off) to one.</summary>
	public float ControllerRumble { get => controllerRumble; set => controllerRumble = Clamp( value ); }
	float controllerRumble = 1;

	public const float RestartDelayStep = 0.05f;

	/// <summary>Seconds the restart key must be held, from zero to one in 0.05 steps. Zero restarts instantly, without a fade.</summary>
	public float RestartDelay
	{
		get => restartDelay;
		set => restartDelay = float.IsFinite( value ) ? MathF.Round( Math.Clamp( value, 0, 1 ) / RestartDelayStep ) * RestartDelayStep : 0.5f;
	}
	float restartDelay = 0.5f;

	/// <summary>A delay as the options slider shows it: 0s, 0.05s, 0.5s, 1s.</summary>
	public static string FormatSeconds( float seconds )
	{
		// Whole hundredths, formatted by hand so the decimal point never follows the system culture.
		int hundredths = Math.Max( 0, (int)MathF.Round( seconds * 100 ) );
		int whole = hundredths / 100, fraction = hundredths % 100;
		return fraction == 0 ? $"{whole}s" : fraction % 10 == 0 ? $"{whole}.{fraction / 10}s" : $"{whole}.{fraction:00}s";
	}

	public static DiamondSettings Current => current ??= Load();

	/// <summary>Linear gain for master-scaled sound effects.</summary>
	public static float EffectsGain => Current.MasterVolume;

	/// <summary>Linear gain for music: the master and music sliders combined.</summary>
	public static float MusicGain => Current.MasterVolume * Current.MusicVolume;


	static DiamondSettings Load()
	{
		try { return FileSystem.Data?.ReadJsonOrDefault( FilePath, new DiamondSettings() ) ?? new DiamondSettings(); }
		catch ( Exception e )
		{
			Log.Warning( $"Could not read {FilePath}; using default options. {e.Message}" );
			return new DiamondSettings();
		}
	}

	public static void Save()
	{
		try { FileSystem.Data?.WriteJson( FilePath, Current ); }
		catch ( Exception e ) { Log.Warning( $"Could not save {FilePath}. {e.Message}" ); }
	}

	static float Clamp( float value ) => float.IsFinite( value ) ? Math.Clamp( value, 0, 1 ) : 1;
}