INI Based Settings in C#/XNA

One of my first issues to handle in this, the second phase of development for hades, has been how to store settings. Of course I could have gone an XML approach, but it is not at all what people are accustomed to – we like ini files. Section, Key = Value… NEXT! Well, while the horn may have sounded in terms of figuring this out, putting things into action is a bit more involved, but not too much so.

For those of you who have worked with Unreal or looked at the configuration files for most any linux application you likely found something similar to the following:

[Engine.GameInfo]
DefaultGame=Fulcrum.FulcrumGame
DefaultServerGame=Fulcrum.FulcrumGame
bAdminCanPause=false
MaxPlayers=32
GameDifficulty=+1.0
bChangeLevels=True

This example shows off all of the typical features of an INI file based approach for storing settings, and I am fully intending to use it in Hades, but how do we start? We have to establish a couple of requirements to paint the picture fully…

Requirements

  1. All requested settings must be defined
    1. If I request a setting I should not get an exception, or null value, otherwise it could crash the game if it is not defined in the ini file
  2. All such settings must have a default value
    1. A setting should be able to be reset to the default in the situation that a problem arises
  3. Settings in this INI file will be treated as strings, but converted to a type based upon a validation delegate
    1. Great flexibility is gained by leveraging anonymous functions
    2. A similar trick will be used to convert back to the string format that is being expected by the validation delegate
  4. Settings are looked up by passing a Section.Name formatted request
  5. Saving the settings should be straight forward, providing standardly formatted output
    1. Should be able to provide a source file location
  6. The settings should be accessible by any class needing to access or modify settings

A pretty stringent set of requirements, but we can achieve this with some pretty straight forward elements. To start with, we are going to have to register our settings, one way or another – without this we will have issues with the first two requirements. Because of the  sixth requirement we are likely looking at a static class / set of methods.

Lets get this started

To begin with, lets define our interface of interaction – the delegates. We have two, one for validation of the value, and one for converting the value to the save format, a string in this case but you can modify it to fit any number of situations.

public delegate object ValidatorDelegate(string readValue);
public delegate string SaveFormatDelegate(object readValue);

Note that there is symmetry here – we read in a string, convert it to the object we are expecting, mind you typically a value type so it will be boxed, and once we have finished working with it we save that object out to a string.

Because each of our settings have a different set of fields we will likely benefit from a structure at this point. Because we wont be using this structure anywhere but in this class (again, something you may want to change for your implementation) ill be showing it as an inner struct:

public static class HadesGameSettings
{
    struct setting
    {
        public object fileName;
        public string name;

        public object actualValue;
        public object defaultValue;

        public ValidatorDelegate validate;
        public SaveFormatDelegate saveFormat;
    }
}

We are well on our way, let’s go ahead and talk about the registration and a couple of enhancements we can add later – First, you wont likely want to store your settings files all in the same directory, especially as your game matures. For that reason ill be making a property to return the directory, for our purposes it is the “Settings” directory, in the same directory of the executable. Second, you may want to provide a different suffix. You will see a call to configSuffix in my example, I will be setting this to “.ini” for Hades.

registering a setting is basically about setting up this setting structure, which mind you should likely be stuck into a constructor for the structure, but it was pretty simple so I have simple left it in the method call.

public static class HadesGameSettings
{ 

[...]

    static Dictionary settings = 
        new Dictionary();

    public static void registerSetting(
        string file, string name, object defaultValue, 
        ValidatorDelegate validator, 
        SaveFormatDelegate saveFormat)
    {
        INIFile f;
        setting v = new setting();

        if (!Directory.Exists(SettingsDirectory()))
            Directory.CreateDirectory(SettingsDirectory());

        string[] taxonomy = name.Split(".".ToCharArray(), 2);

        v.name = name;
        v.fileName = file;

        v.validate = validator;
        v.saveFormat = saveFormat;

        v.defaultValue = defaultValue;

        f = new INIFile(
            SettingsDirectory() + v.fileName + configSuffix);
        v.actualValue = v.validate(f.ReadValue(taxonomy[0], 
            taxonomy[1], saveFormat(defaultValue)));

        settings.Add(name, v);
    }
}

A couple of notes.

  • I am creating the directory for our settings file if it does not exist.This ensures that there is a place later for the settings to be saved when I am ready to do that, during execution.
  • The taxonomy split requires that there be at least 1 dot, (.) character. The first portion is considered the section, and the second is the name of the parameter. This allows you to pass Game.WindowTitle, or Game.Window.Title, to provide some flexibility in organizing your data.
  • INIFile is a class that is based on the example here [1]
  • The actualValue is read in from a call to the readValue method and exercises the saveFormat and validator methods.

We can add a couple further utility methods, one for saving and a few example validators for standard types:

public static class HadesGameSettings
{ 
    [...]

    internal static void saveSettings()
    {
        foreach (setting v in settings.Values)
        {
            Save(v);
        }
    }

    private static void Save(setting v)
    {
        INIFile f = new INIFile(
            SettingsDirectory() + v.fileName + configSuffix);

        string[] taxonomy = v.name.Split(".".ToCharArray(), 2);

        f.WriteValue(taxonomy[0], 
            taxonomy[1], v.saveFormat(v.actualValue));
    }

    public static string getString(string p)
    {
        return (string)settings[p].actualValue;
    }

    public static string getStringDef(string p)
    {
        return (string)settings[p].defaultValue;
    }
}

Continue defining a few more of these utility methods if you are so inspired. I have added bool, int and float. You can see the full implementations here [2]

Registering Settings

So okay, all of the heavy lifting is in place and the same pattern we go through to get an element out is being used to shove it back down into the file, not a whole lot of complication to note, What is left? To be blunt, not a whole lot. Ill show you an example of how to create a couple settings, how to retrieve their values and then we can bring this post to a close.

Registering a setting is as simple as a call to the static method by the same name:

HadesGameSettings.registerSetting("Game", "General.WindowTitle", 
    "Hades - The Game (XNA)",
    HadesGameSettings.ValidateString, 
    HadesGameSettings.SaveFormatStd);

The file is passed, its section and key name and then the default variable. Then I have a call to two methods. Because I am using delegates for something that needs to have a certain level of standardization I have created a few convenience defines to help lighten the load. These two methods, ValidateString and SaveFormatStd are static methods back in the Settings class, defined as follows:

public static string SaveFormatStd(object var) 
{ 
   	return var.ToString(); 
}

public static object ValidateString(string var) 
{ 
   	return var; 
}

These can be used to avoid having to write 12000 different implementations of a method that returns the string straight through, or converts an element to a string. In my linked example you will find that there are a few others, and that they use TryParse, and not Parse. [2] Note our requirement regarding exceptions and or crashing the game from above. TryParse attempts to parse the value and in the situation where it fails (like trying to convert “x” to an integer) it will return false and you can default to a given value.

But as I mentioned, you can provide any method that fits the signature of the delegate, and you couldn’t possibly be expected to provide conversion from any class or enumeration type. C# has anonymous functions that can be passed around and we can leverage them here to convert the initialized DepthFormat:

HadesGameSettings.registerSetting("Game",
    "GraphicsDevice.DepthFormat", 
    DepthFormat.Depth24Stencil8, 
    (val) => { return ((DepthFormat)int.Parse(val)); }, 
    (val) => { return ((int)val).ToString(); });

You can build off of this example to drive the conversion of your types. Note also that my examples are really quite simple and don’t take into account the details of validating the input. As with any situation where you allow the user the ability to change the internal workings of your application, you should always ensure that you get back data that does not break your system. The subtleties of user input validation are a topic that tomes have been written on, but for our example I don’t want to pull away from the example with cloudy code.

With our settings registered it is pretty damned simple to do the actual query to get the value and set it, using one of our getters.

Window.Title = HadesGameSettings.getString("General.WindowTitle");

With that, the overview is complete. You can feel free to use this as a method of saving your settings & initialization values.

As always – Please do let me know if any issues come up. If you run into problems compiling make sure you are using the most recent version, you can grab it from BitBucket if you are in question.

References

  1. CodeProject
  2. BitBucket – Hades/Src/Utilities/HadesGameSettings.cs