Tuesday, November 22, 2011

GameMaker - INI without an *.ini

So, say, you would like to load data from INI without creating an *.ini file somewhere.
There can be many reasons for doing so, be that security, performance, or simple fact of INI contents being defined inside of game's code.

There are two things to do about it - deciding on presentation of data, and creating a algorithm to load it. Presentation of data is relatively simple with INI - file is split into named sections, which contain named values. Since both use string index, using ds_map (sections) with other ds_map's inside (values) seems like an approriate choice. Below is my implementation.

Here, I have named main ds_map "fi" (Fake Ini). So, script to read a value from it would be as the following:

/* fi_get(section, key, default)
    Reads a value from loaded ini. */

if (!ds_map_exists(fi, argument0)) return argument2
local.map = ds_map_find_value(fi, argument0)
if (!ds_map_exists(local.map, argument1)) return argument2
return ds_map_find_value(local.map, argument1)
As can be seen, two checks ensure that both section and key exist, and return default value if they do not.

Writing value to "ini" requires a similar procedure, with exception of things being created when they do not exist yet:

/* fi_set(section, key, value)
    Writes a value to loaded ini. */

if (!ds_map_exists(fi, argument0)) ds_map_add(fi, argument0, ds_map_create())
local.map = ds_map_find_value(fi, argument0)
if (!ds_map_exists(local.map, argument1)) ds_map_add(local.map, argument1, argument2)
else ds_map_replace(local.map, argument1, argument2)

Then the most interesting part starts: reading data from "INI" string into such structure.
Without further thought, I have made a small 'parser' for basic INI format. With minor effort you could tell how it works, and maybe even improve it further.

/* fi_load(data: string, append: boolean)
    Loads data into fi* system
    Data must be in INI-like format
        [SectionName]
        stringVar = Text
        numberVar = 0
    Append indicates if new sections should be appended to currently
    loaded ones (true) or replace them (false).
    Script author: YellowAfterlife */

var i, j, s, c, z, w, m, v;
// remove existing maps:
if (!argument1) if (ds_map_size(fi) != 0)
for (i = ds_map_find_first(fi); ds_map_exists(fi, i); i = ds_map_find_next(fi, i)) {
    ds_map_destroy(ds_map_find_value(fi, i))
    ds_map_delete(fi, i)
}
s = ''
// parsing below:
for (i = 1; i <= string_length(argument0); i += 1) {
    c = string_char_at(argument0, i)
    v = (c == chr(13) || c == chr(10))
    if (!v) s += c
    if (v || i == string_length(argument0)) {
        if (string_length(s) > 0) {
            c = string_char_at(s, 1)
            if (c == '[') {
                z = string_copy(s, 2, string_pos(']', s) - 2)
                if (ds_map_exists(fi, z)) {
                    m = ds_map_find_value(fi, z)
                } else {
                    m = ds_map_create()
                    ds_map_add(fi, z, m)
                }
            } else if (string_letters(c) == c) {
                j = string_pos('=', s)
                w = string_copy(s, 1, j - 1)
                z = string_copy(s, j + 1, string_length(s))
                // delete unneeded spaces:
                while (string_char_at(w, string_length(w)) == ' ')
                w = string_copy(w, 1, string_length(w) - 1)
                while (string_char_at(z, 1) == ' ' && z != '')
                z = string_copy(z, 2, string_length(z) - 1)
                while (string_char_at(z, string_length(z)) == ' ' && z != '')
                z = string_copy(z, 1, string_length(z) - 1)
                // convert to number, if looks like one:
                c = string_char_at(z, 1)
                if (string_pos(c, '0123456789.-') != 0) z = real(z)
                if (c == '\') z = string_copy(z, 2, string_length(z))
                // add to submap:
                if ds_map_exists(m, w)
                then ds_map_replace(m, w, z)
                else ds_map_add(m, w, z)
            }
        }
        s = ''
    }
}
As can be seen, it even allows some freedom in form of leading\trailing spaces.

If you'd like to go a bit further and save INI data into a string, you can utilize the following function:

/* fi_save(): string
    Creates ini code from currently loaded data and returns it */

var s, m, i, j;
s = ''
for (i = ds_map_find_first(fi); ds_map_exists(fi, i); i = ds_map_find_next(fi, i)) {
    m = ds_map_find_value(fi, i)
    if (s != '') s += chr(13) + chr(10)
    s += '[' + i + ']'
    for (j = ds_map_find_first(m); ds_map_exists(m, j); j = ds_map_find_next(m, j)) {
        s += chr(13) + chr(10) + j + ' = ' + string(ds_map_find_value(m, j))
    }
}
return s

Obviously, ds_map isn't going to magically create itself on game start, so a small 'init' function is needed:

/* fi_init()
    Initializes system. Call at game start. */

globalvar fi;
fi = ds_map_create()

That's all. You can either copy & use above code, or download a GMK here.
Have a nice day.

No comments:

Post a Comment