view mde/options.d @ 26:611f7b9063c6

Changed the licensing and removed a few dead files. Changed licensing to "GPL version 2 or later" to avoid future compatibility issues. Also a unittest fix to the previous commit. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 03 Apr 2008 18:15:02 +0100
parents 2c28ee04a4ed
children 0aa621b3e070
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 as published by the Free Software Foundation, either
version 2 of the License, or (at your option) any later version.

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, see <http://www.gnu.org/licenses/>. */

/** 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");
    }
}