Code/Config/ConfigController.cs
using System;
using System.Linq;

namespace SandbankDatabase;

internal class ConfigController
{
	public static bool STARTUP_SHUTDOWN_MESSAGES = true;
	public static bool WARNINGS_AS_EXCEPTIONS = false;
	public static bool CLIENTS_CAN_USE = false;
	public static bool INDENT_JSON = true;
	public static float PERSIST_EVERY_N_SECONDS = 10f;
	public static int PARTIAL_WRITES_PER_SECOND = 1;
	public static bool ENABLE_LOGGING = false;
	public static string DATABASE_NAME = "sandbank";
	public static float TICK_DELTA = 0.1f;
	public static int CLASS_INSTANCE_POOL_SIZE = 2000;
	public static bool MERGE_JSON = true;
	public static bool OBFUSCATE_FILES = false;
	public static string SBSERVER_USER_ID = "";
	public static string SBSERVER_PUBLIC_KEY = "";
	public static OnEndpointErrorBehaviour ON_ENDPOINT_ERROR_BEHAVIOUR = OnEndpointErrorBehaviour.LogWarning;
	public static BackupFrequency BACKUP_FREQUENCY = BackupFrequency.Hourly;
	public static int BACKUPS_TO_KEEP = 10;

	private const string DEFAULT_CONFIG_FILE =
@"# Whether to show the startup and shutdown messages in the console when the database stops and starts.
STARTUP_SHUTDOWN_MESSAGES=true
	
# If this is true then all warnings are thrown as exceptions. I probably wouldn't recommend this but you can enable it
# if you want. This is used in the unit tests to make life easier.
WARNINGS_AS_EXCEPTIONS=false

# Set this to true if you want clients to be able to use the database too. You probably don't want this - turning this on
# doesn't magically sync data between host and clients. But there might be some situations where you want to store data on
# the client for some reason.
CLIENTS_CAN_USE=false

# This controls whether the written JSON files are indented or not. Indentation makes them more human-readable, but
# probably makes saving to disk a little bit slower.
INDENT_JSON=true

# The database will try to make sure that all stale data is written to disk at most every this many seconds. In the event
# of a crash, all stale data is lost, so lower numbers are ""safer"". But lower numbers can lead to decreased performance
# under heavy loads due to increased disk writing.
PERSIST_EVERY_N_SECONDS=10

# We will only try to perform a partial write this many times per second. A partial write doesn't write everything, so
# changing this will not really change your performance. But it does increase performance by ensuring that are we not spamming
# writes every tick. You probably don't want to change this.
PARTIAL_WRITES_PER_SECOND=1

# Enables logging for helping to diagnose issues. This is mostly for development purposes.
ENABLE_LOGGING=false

# This is the name of the folder where your files are kept (e.g. ""sandbank/my_collection""). There's no reason to change it,
# but you're more than welcome to. If you're renaming an existing database, make sure to copy your files across to the new folder.
DATABASE_NAME=sandbank

# How often the database ticks in seconds. I don't recommend changing this as you are not necessarily making things any faster.
TICK_DELTA=0.1

# The number of instances of each class used by your database that will be cached in RAM for faster fetching. Increasing this
# will improve performance if you are selecting lots of records. The optimal value for this is roughly twice your peak per-second
# fetch rate. For example, if at your peak you are fetching 1,000 records per second, for optimal performance, the recommended
# value for this would be 2,000. You will probably see little-to-no performance gain by increasing this further.
#
# Increasing this will increase memory usage. The memory increase is not affected by the number of records in a collection,
# but it is affected by the complexity of your data class. As a very rough rule, a 100,000 pool size takes up around 200mb
# RAM for each collection. A very rough formula for estimating the total memory usage is:
#
# 1mb * number of collections * CLASS_INSTANCE_POOL_SIZE / 500
CLASS_INSTANCE_POOL_SIZE=2000

# This should always true unless you know what you are doing.
#
# If this is true and you rename a field in your data class, the renamed data will remain in the file. For example, if you
# renamed ""Name"" to ""PlayerName"", both properties would still be there. This is because any existing JSON is ""merged""
# with any updates.
#
# If this is false and you rename a field, the renamed data is destroyed. The new document simply overwrites the file and no
# merge is done. So if you renamed ""Name"" to ""PlayerName"", ""Name"" is destroyed.
#
# This is here to protect you, so the only time this should be set to false is if you're ready to remove the renamed data.
MERGE_JSON=true

# Enabling this option will obfuscate files stored on the local filesystem, making them (almost) impossible to edit. This is
# useful if you want to store data on the client that you don't want them to be able to change easily.
#
# Note that this is not secure. If someone really wanted to, they could reverse-engineer the data and change it to whatever
# they want. However most people will not have the skills or inclination to do this.
#
# Note that this will cause saving and loading files to become a bit slower and more CPU intensive.
#
# The database will work whether this is enabled or not, regardless of whether some or all of the files are obfuscated. Files
# are only obfuscated/unobfuscated when they are saved, so changing this will have no impact on files until those files
# are re-saved.
OBFUSCATE_FILES=false

# Your Sandbank Server user ID (if any). Most people can ignore this.
SBSERVER_USER_ID=

# Your Sandbank Server public key (if any). Most people can ignore this.
SBSERVER_PUBLIC_KEY=

# When an endpoint fails, it will return null. By default it will also log a warning. You can change that here. Most people can
# ignore this. Valid options are DoNothing and LogWarning.
ON_ENDPOINT_ERROR_BEHAVIOUR=LogWarning

# Controls how often the database should be backed up. Valid options are Never, Hourly, Daily and Weekly.
BACKUP_FREQUENCY=Hourly

# How many backups should be kept. If the number of backups is greater than this, the oldest backup is deleted.
#
# Make sure you have enough storage to cover your needs. For example, if your database is 10MB big, and you want 100 backups,
# then you need at least 1GB of free disk storage.
BACKUPS_TO_KEEP=10";

	public static string GetDefaultConfigFileContents()
	{
		if ( !TestHelpers.IsUnitTests )
			throw new SandbankException( "this can only be used during tests" );

		return DEFAULT_CONFIG_FILE;
	}

	public static void CreateConfigFileIfNone()
	{
		if ( FileController.FileExists( "sandbank_config.ini", "/" ) )
			return;

		FileController.WriteFile( "sandbank_config.ini", DEFAULT_CONFIG_FILE );
	}

	private static T InterpretConfigOption<T>( string[] fileLines, string key ) where T : notnull
	{
		var line = fileLines.Where( x => x.StartsWith( key ) ).LastOrDefault();

		if ( line == null )
			throw new SandbankException( $"sandbank_config.ini is corrupt - it is missing the \"{key}\" option" );

		var parts = line.Split( '=' );
		var value = "";

		if ( parts.Length == 2 )
			value = parts[1];

		return typeof( T ) switch
		{
			Type t when t == typeof( string ) => (T)(object)value,
			Type t when t == typeof( int ) => (T)(object)int.Parse( value ),
			Type t when t == typeof( float ) => (T)(object)float.Parse( value ),
			Type t when t == typeof( bool ) => (T)(object)bool.Parse( value ),
			Type t when t == typeof( OnEndpointErrorBehaviour ) => (T)Enum.Parse( typeof( OnEndpointErrorBehaviour ), value ),
			Type t when t == typeof( BackupFrequency ) => (T)Enum.Parse( typeof( BackupFrequency ), value ),
			_ => throw new SandbankException( "unsupported type" )
		};
	}

	public static void LoadConfigFile()
	{
		var lines = FileController.ReadFile( "sandbank_config.ini" ).Replace("\r", "").Split( '\n' );

		STARTUP_SHUTDOWN_MESSAGES = InterpretConfigOption<bool>( lines, "STARTUP_SHUTDOWN_MESSAGES" );
		WARNINGS_AS_EXCEPTIONS = InterpretConfigOption<bool>( lines, "WARNINGS_AS_EXCEPTIONS" );
		CLIENTS_CAN_USE = InterpretConfigOption<bool>( lines, "CLIENTS_CAN_USE" );
		INDENT_JSON = InterpretConfigOption<bool>( lines, "INDENT_JSON" );
		ENABLE_LOGGING = InterpretConfigOption<bool>( lines, "ENABLE_LOGGING" );
		MERGE_JSON = InterpretConfigOption<bool>( lines, "MERGE_JSON" );
		OBFUSCATE_FILES = InterpretConfigOption<bool>( lines, "OBFUSCATE_FILES" );
		PERSIST_EVERY_N_SECONDS = InterpretConfigOption<float>( lines, "PERSIST_EVERY_N_SECONDS" );
		TICK_DELTA = InterpretConfigOption<float>( lines, "TICK_DELTA" );
		PARTIAL_WRITES_PER_SECOND = InterpretConfigOption<int>( lines, "PARTIAL_WRITES_PER_SECOND" );
		CLASS_INSTANCE_POOL_SIZE = InterpretConfigOption<int>( lines, "CLASS_INSTANCE_POOL_SIZE" );
		BACKUPS_TO_KEEP = InterpretConfigOption<int>( lines, "BACKUPS_TO_KEEP" );
		DATABASE_NAME = InterpretConfigOption<string>( lines, "DATABASE_NAME" );
		SBSERVER_USER_ID = InterpretConfigOption<string>( lines, "SBSERVER_USER_ID" );
		SBSERVER_PUBLIC_KEY = InterpretConfigOption<string>( lines, "SBSERVER_PUBLIC_KEY" );
		ON_ENDPOINT_ERROR_BEHAVIOUR = InterpretConfigOption<OnEndpointErrorBehaviour>( lines, "ON_ENDPOINT_ERROR_BEHAVIOUR" );
		BACKUP_FREQUENCY = InterpretConfigOption<BackupFrequency>( lines, "BACKUP_FREQUENCY" );
	}
}