diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/lookup/Options.d	Fri Jun 27 18:35:33 2008 +0100
@@ -0,0 +1,486 @@
+/* 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");
+    }
+}