view mde/lookup/Options.d @ 63:66d555da083e

Moved many modules/packages to better reflect usage.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 27 Jun 2008 18:35:33 +0100
parents mde/Options.d@960206198cbd
children cc3763817b8a
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.lookup.Options;

import mde.exception;

import mde.mergetag.Reader;
import mde.mergetag.Writer;
import mde.mergetag.DataSet;
import mde.mergetag.exception;
import mde.setup.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(...). Use an example like OptionsMisc as a
* template for creating a new Options sub-class.
*
* 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 int*   [ID]   optsInt;
    protected double*[ID]   optsDouble;
    protected char[]*[ID]   optsCharA;
    
    //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 == "double") {
            double** p = id in optsDouble;
            if (p !is null) **p = parseTo!(double) (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, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*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 setDouble (char[] subClass, char[] symbol, double val) {
        changed = true;     // something got set (don't bother checking this isn't what it already was)
        
        try {
            *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val;
            subClassChanges[cast(ID) subClass].setDouble (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
    private {
        // Return index of first comma, or halts if not found.
        template cIndex(char[] A) {
            static if (A.length == 0)
                static assert (false, "Error in implementation");
            else static if (A[0] == ',')
                const size_t cIndex = 0;
            else
                const size_t cIndex = 1 + cIndex!(A[1..$]);
        }
        // Return index of first semi-colon, or halts if not found.
        template scIndex(char[] A) {
            static if (A.length == 0)
                static assert (false, "Error: no trailing semi-colon");
            else static if (A[0] == ';')
                const size_t scIndex = 0;
            else
                const size_t scIndex = 1 + scIndex!(A[1..$]);
        }
        // Look for "type symbols;" in A and return symbols as a comma separated list of names
        // (even if type is encountered more than once). Output may contain spaces and, if
        // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$.
        template parseT(char[] type, char[] A) {
            static if (A.length < type.length + 1)	// end of input, no match
                const char[] parseT = "";
            else static if (A[0] == ' ')		// leading whitespace: skip
                const char[] parseT = parseT!(type, A[1..$]);
            else static if (A[0..type.length] == type && A[type.length] == ' ')	// match
                const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~
                        parseT!(type, A[scIndex!(A)+1 .. $]);
            else					// no match
                const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]);
        }
        // May have a trailing comma. Assumes cIndex always returns less than A.$.
        template aaVars(char[] A) {
            static if (A.length == 0)
                const char[] aaVars = "";
            else static if (A[0] == ' ')
                const char[] aaVars = aaVars!(A[1..$]);
            else
                const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~
                        aaVars!(A[cIndex!(A)+1..$]);
        }
        // strip Trailing Comma
        template sTC(char[] A) {
            static if (A.length && A[$-1] == ',')
                const char[] sTC = A[0..$-1];
            else
                const char[] sTC = A;
        }
        // if string is empty (other than space) return null, otherwise enclose: [A]
        template listOrNull(char[] A) {
            static if (A.length == 0)
                const char[] listOrNull = "null";
            else static if (A[0] == ' ')
                const char[] listOrNull = listOrNull!(A[1..$]);
            else
                const char[] listOrNull = "["~A~"]";
        }
    } protected {
        /** Produces the implementation code to go in the constuctor. */
        template aaDefs(char[] A) {
            const char[] aaDefs =
                    "optsBool = "  ~ listOrNull!(sTC!(aaVars!(parseT!("bool"  , A)))) ~ ";\n" ~
                    "optsInt = "   ~ listOrNull!(sTC!(aaVars!(parseT!("int"   , A)))) ~ ";\n" ~
                    "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~
                    "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n";
        }
        /+/** Produces the implementation code to go in the static constuctor. */
        template optClassAdd(char[] symb) {
            const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n";
        }+/
        /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
         *
         * Where type is one of bool, int, double, char[]. E.g.
         * ---
         * mixin (impl ("bool a, b; int i;"));
         * ---
         *
         * In case this() needs to be customized, mixin(impl!(A)) is equivalent to:
         * ---
         * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}");
         * ---
         *
         * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing
         * semi-colon (;) or you'll get told off! :D
         *
         * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if
         * necessary.
         *
         * Extending: mixins could also be used for the static this() {...} or even the whole
         * class, but doing so would rather decrease readability of any implementation. */
        template impl(char[] A /+, char[] symb+/) {
            const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}";
            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
        }
    }
    /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
     *
     * E.g.
     * ---
     * mixin (impl ("bool a, b; int i;"));
     * ---
     * The parser isn't highly accurate. */
    // Try using templates instead? See std.metastrings
    static char[] impl (char[] A) {
        char[] bools;
        char[] ints;
        
        while (A.length) {
            // Trim whitespace
            {
                size_t i = 0;
                while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)))
                    ++i;
                A = A[i..$];
            }
            if (A.length == 0) break;
            
            char[] type;
            for (size_t i = 0; i < A.length; ++i) {
                if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) {
                    type = A[0..i];
                    A = A[i+1..$];
                    break;
                }
            }
            
            char[] symbols;
            for (size_t i = 0; i < A.length; ++i) {
                if (A[i] == ';') {
                    symbols = A[0..i];
                    A = A[i+1..$];
                    break;
                }
            }
            
            if (type == "bool") {
                if (bools.length)
                    bools = bools ~ "," ~ symbols;
                else
                    bools = symbols;
            }
            else if (type == "int") {
                if (ints.length)
                    ints = ints ~ "," ~ symbols;
                else
                    ints = symbols;
            }
            else {
                // Unfortunately, we cannot output non-const strings (even though func is compile-time)
                // We also cannot use pragma(msg) because the message gets printed even if the code isn't run.
                //pragma(msg, "Warning: impl failed to parse whole input string");
                // Cannot use Cout / logger either.
                break;
            }
        }
        
        char[] ret;
        if (bools.length)
            ret = "bool "~bools~";\n";
        if (ints.length)
            ret = ret ~ "int "~ints~";\n";
        
        
        
        return ret;
    }+/
    //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;
    double[] doubles;
    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 setDouble (ID id, double x) {
        double** p = id in optsDouble;
        if (p !is null) **p = x;
        else {
            doubles ~= x;
            optsDouble[id] = &doubles[$-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: Options sub-classes are expected to use a template to ease inserting contents and
* hide some of the "backend" functionality. Use impl as below, or read the documentation for impl.
*
* Each entry should have a Translation entry with humanized names and descriptions in
* data/L10n/ClassName.mtt
*
* To create a new Options sub-class, just copy, paste and adjust.
*/

/** A home for all miscellaneous options, at least for now. */
OptionsMisc miscOpts;
class OptionsMisc : Options {
    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;"));
    
    static this() {
        miscOpts = new OptionsMisc;
        Options.addOptionsClass (miscOpts, "misc");
    }
}