view mde/options.d @ 25:2c28ee04a4ed

Some minor and some futile efforts. Played around with init functions, had problems, gave up and put them back. Removed idea for multiple init stages; it's not good for performance or simplicity. Adjusted exception messages. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 03 Apr 2008 17:26:52 +0100
parents 32eff0e01c05
children 611f7b9063c6
line wrap: on
line source

/* LICENSE BLOCK
Part of mde: a Modular D game-oriented Engine
Copyright © 2007-2008 Diggory Hardy

This program is free software; you can redistribute it and/or modify it under the terms of
the GNU General Public License, version 2, as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */

/** This module handles stored options, currently all except input maps.
*
* The purpose of having all options centrally controlled is to allow generic handling by the GUI
* and ease saving and loading of values. The Options class is only really designed around handling
* small numbers of variables for now.
*/
module mde.options;

import mde.exception;

import mde.mergetag.Reader;
import mde.mergetag.Writer;
import mde.mergetag.DataSet;
import mde.mergetag.exception;
import mde.resource.paths;

import tango.scrapple.text.convert.parseTo : parseTo;
import tango.scrapple.text.convert.parseFrom : parseFrom;

import tango.core.Exception : ArrayBoundsException;
import tango.util.log.Log : Log, Logger;

/** Base class for handling options.
*
* This class itself handles no options and should not be instantiated, but provides a sub-classable
* base for generic options handling. Also, the static portion of this class tracks sub-class
* instances and provides loading and saving methods.
*
* Each sub-class provides named variables for maximal-speed reading. Local sub-class references
* should be used for reading variables, and via the addOptionsClass() hook will be loaded from
* files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will
* not be saved; use, for example, Options.setBool(...).
*
* Details: Options sub-classes hold associative arrays of pointers to all option variables, with a
* char[] id. This list is used for saving, loading and to provide generic GUI options screens. The
* built-in support in Options is only for bool, int and char[] types (a float type may get added).
* Further to this, a generic class is used to store all options which have been changed, and if any
* have been changed, is merged with options from the user conf dir and saved on exit.
*/
class Options : IDataSection
{
    // No actual options are stored by this class. However, much of the infrastructure is
    // present since it need not be redefined in sub-classes.
    
    // The "pointer lists":
    protected bool*  [ID]   optsBool;
    protected char[]*[ID]   optsCharA;
    protected int*   [ID]   optsInt;
    
    //BEGIN Mergetag loading/saving code
    void addTag (char[] tp, ID id, char[] dt) {
        if (tp == "bool") {
            bool** p = id in optsBool;
            if (p !is null) **p = parseTo!(bool) (dt);
        } else if (tp == "char[]") {
            char[]** p = id in optsCharA;
            if (p !is null) **p = parseTo!(char[]) (dt);
        } else if (tp == "int") {
            int** p = id in optsInt;
            if (p !is null) **p = parseTo!(int) (dt);
        }
    }
    void writeAll (ItemDelg dlg) {
        foreach (ID id, bool*   val; optsBool)  dlg ("bool"  , id, parseFrom!(bool  ) (*val));
        foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val));
        foreach (ID id, int*    val; optsInt)   dlg ("int"   , id, parseFrom!(int   ) (*val));
    }
    //END Mergetag loading/saving code
    
    //BEGIN Static
    /** Add an options sub-class to the list for loading and saving.
    *
    * Call from static this() (before Init calls load()). */
    static void addOptionsClass (Options c, char[] i)
    in {    // Trap a couple of potential coding errors:
        assert (c !is null);    // Instance must be created before calling addOptionsClass
        assert (((cast(ID) i) in subClasses) is null);  // Don't allow a silent replacement
    } body {
        subClasses[cast(ID) i] = c;
        subClassChanges[cast(ID) i] = new OptionsGeneric;
    }
    
    /** Set option symbol of Options class subClass to val.
    *
    * Due to the way options are handled generically, string IDs must be used to access the options
    * via hash-maps, which is a little slower than direct access but necessary since the option
    * must be changed in two separate places. */
    private static const ERR_MSG = "Options.setXXX called with incorrect parameters!";
    static void setBool (char[] subClass, char[] symbol, bool val) {
        changed = true;     // something got set (don't bother checking this isn't what it already was)
        
        try {
            *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val;
            subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val);
        } catch (ArrayBoundsException) {
            // log and ignore:
            logger.error (ERR_MSG);
        }
    }
    static void setInt (char[] subClass, char[] symbol, int val) {
        changed = true;     // something got set (don't bother checking this isn't what it already was)
        
        try {
            *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val;
            subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val);
        } catch (ArrayBoundsException) {
            // log and ignore:
            logger.error (ERR_MSG);
        }
    }
    static void setCharA (char[] subClass, char[] symbol, char[] val) {
        changed = true;     // something got set (don't bother checking this isn't what it already was)
        
        try {
            *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val;
            subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val);
        } catch (ArrayBoundsException) {
            // log and ignore:
            logger.error (ERR_MSG);
        }
    }
        
    // Track all sections for saving/loading/other generic handling.
    static Options[ID] subClasses;
    static OptionsGeneric[ID] subClassChanges;
    static bool changed = false;    // any changes at all, i.e. do we need to save?
    
    /* Load/save options from file.
    *
    * If the file doesn't exist, no reading is attempted (options are left at default values).
    */
    private static const fileName = "options";
    private static const MT_LOAD_EXC = "Loading options aborted:";
    static void load () {
        // Check it exists (if not it should still be created on exit).
        // Don't bother checking it's not a folder, because it could still be a block or something.
        if (!confDir.exists (fileName)) return;
        
        try {
            IReader reader;
            reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH);
            reader.dataSecCreator = delegate IDataSection(ID id) {
                /* Recognise each defined section, and return null for unrecognised sections. */
                Options* p = id in subClasses;
                if (p !is null) return *p;
                else return null;
            };
            reader.read;
        } catch (MTException e) {
            logger.fatal (MT_LOAD_EXC);
            logger.fatal (e.msg);
            throw new optionsLoadException ("Mergetag exception (see above message)");
        }
    }
    static void save () {
        if (!changed) return;   // no changes to save
        
        DataSet ds = new DataSet();
        foreach (id, sec; subClassChanges) ds.sec[id] = sec;
        
        // Read locally-stored options
        try {
            IReader reader;
            reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
            reader.dataSecCreator = delegate IDataSection(ID id) {
                return null;    // All recognised sections are already in the dataset.
            };
            reader.read;
        } catch (MTFileIOException) {
            // File either didn't exist or couldn't be opened.
            // Presuming the former, this is not a problem.
        } catch (MTException e) {
            // Log a message and continue, overwriting the file:
            logger.error (MT_LOAD_EXC);
            logger.error (e.msg);
        }
        
        try {
            IWriter writer;
            writer = confDir.makeMTWriter (fileName, ds);
            writer.write();
        } catch (MTException e) {
            logger.error ("Saving options aborted! Reason:");
            logger.error (e.msg);
        }
    }
    
    private static Logger logger;
    static this() {
        logger = Log.getLogger ("mde.options");
    }
    //END Static
    
    //BEGIN Templates
    template store(A...) {
        alias A a;
    }
    template decRecurse(char[] A, B...) {
        static if (B.length) const char[] decRecurse = A ~ ", " ~ decRecurse!(B);
        else                 const char[] decRecurse = A;
    }
    template decBool(A...) {
        const char[] decBool = "bool " ~ decRecurse!(A) ~ ";\n";
    }
    template decCharA(A...) {
        const char[] decCharA = "char[] " ~ decRecurse!(A) ~ ";\n";
    }
    template decInt(A...) {
        const char[] decInt = "int " ~ decRecurse!(A) ~ ";\n";
    }
    template aaRecurse(char[] A, B...) {
        static if (B.length) const char[] aaRecurse = "\""~A~"\"[]:&"~A ~ ", " ~ aaRecurse!(B);
        else                 const char[] aaRecurse = "\""~A~"\"[]:&"~A;
    }
    template aaBool(A...) {
        const char[] aaBool = "optsBool = [" ~ aaRecurse!(A) ~ "];\n";
    }
    template aaInt(A...) {
        const char[] aaInt = "optsInt = [" ~ aaRecurse!(A) ~ "];\n";
    }
    template aaCharA(A...) {
        const char[] aaCharA = "optsCharA = [" ~ aaRecurse!(A) ~ "];\n";
    }
    //END Templates
}

/* Special class to store all locally changed options, whatever the section. */
class OptionsGeneric : Options {
    // These store the actual values, but are never accessed directly except when initially added.
    // optsX store pointers to each item added along with the ID and are used for access.
    bool[] bools;
    int[] ints;
    char[][] strings;
    
    this () {}
    
    void setBool (ID id, bool x) {
        bool** p = id in optsBool;
        if (p !is null) **p = x;
        else {
            bools ~= x;
            optsBool[id] = &bools[$-1];
        }
    }
    void setInt (ID id, int x) {
        int** p = id in optsInt;
        if (p !is null) **p = x;
        else {
            ints ~= x;
            optsInt[id] = &ints[$-1];
        }
    }
    void setCharA (ID id, char[] x) {
        char[]** p = id in optsCharA;
        if (p !is null) **p = x;
        else {
            strings ~= x;
            optsCharA[id] = &strings[$-1];
        }
    }
    
    //BEGIN Mergetag loading/saving code
    // Reverse priority: only load symbols not currently existing
    void addTag (char[] tp, ID id, char[] dt) {
        if (tp == "bool") {
            if ((id in optsBool) is null) {
                bools ~= parseTo!(bool) (dt);
                optsBool[id] = &bools[$-1];
            }
        } else if (tp == "char[]") {
            if ((id in optsCharA) is null) {
                strings ~= parseTo!(char[]) (dt);
                optsCharA[id] = &strings[$-1];
            }
            char[]** p = id in optsCharA;
            if (p !is null) **p = parseTo!(char[]) (dt);
        } else if (tp == "int") {
            if ((id in optsInt) is null) {
                ints ~= parseTo!(int) (dt);
                optsInt[id] = &ints[$-1];
            }
        }
    }
    //END Mergetag loading/saving code
}

/* NOTE: These Options classes use templates to ease inserting contents.
*
* Each entry has an I18nTranslation entry; see data/L10n/ClassName.mtt for descriptions.
*
* To create a new class, just copy and paste anywhere and adjust.
*/

/** A home for all miscellaneous options, at least for now. */
OptionsMisc miscOpts;
class OptionsMisc : Options {
    alias store!("useThreads") BOOL;
    alias store!("logLevel") INT;
    alias store!("L10n") CHARA;
    
    mixin (decBool!(BOOL.a));
    mixin (decInt!(INT.a));
    mixin (decCharA!(CHARA.a));
    
    this () {
        mixin (aaBool!(BOOL.a));
        mixin (aaInt!(INT.a));
        mixin (aaCharA!(CHARA.a));
    }
    
    static this() {
        miscOpts = new OptionsMisc;
        Options.addOptionsClass (miscOpts, "misc");
    }
}