view mde/lookup/Options.d @ 98:49e7cfed4b34

All types of Option have been converted to use ValueContent classes, and their values can be displayed.
author Diggory Hardy <diggory.hardy@gmail.com>
date Wed, 12 Nov 2008 13:18:51 +0000
parents 2a364c7d82c9
children 71f0f1f83620
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.
 *
 * Note: This module uses some non-spec functionality, which "works for me", but may need to be
 * changed if it throws up problems. Specifically: templated virtual functions (Options.set, get
 * and list), and accessing private templates from an unrelated class (Options.TName, TYPES).
 * OptionChanges used to have a templated set member function (used by Options.set), which caused
 * linker problems when the module wasn't compiled from scratch.
 */
module mde.lookup.Options;

import mde.setup.paths;
import mde.exception;

public import mde.gui.content.Content;

import mde.file.mergetag.Reader;
import mde.file.mergetag.Writer;
import mde.file.mergetag.DataSet;
import mde.file.serialize;

import tango.core.Exception : ArrayBoundsException;
import tango.util.log.Log : Log, Logger;
private Logger logger;
static this() {
    logger = Log.getLogger ("mde.lookup.Options");
}

/** 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; instead use set(), for example, miscOpts.set!(char[])("L10n","en-GB"). Use an
* example like MiscOptions as a template for creating a new Options sub-class.
*
* Optionally, overload the validate() function. This is called after loading, allowing conditions
* to be enforced on variables. Use set!()() to change the variables. If an exception is thrown,
* init will abort and the executable won't start.
*
* 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.
*/
/* An idea for potentially extending Options, but which doesn't seem necessary now:
Move static code from Options to an OptionSet class, which may be sub-classed. These sub-classes
may be hooked in to the master OptionSet class to shadow all Options classes and be notified of
changes, and may or may not have values loaded from files during init. Change-sets could be
rewritten to use this.
However, only the changesets should need to be notified of each change (gui interfaces could simply
be notified that a change occured and redraw everything; users of options can just re-take their
values every time they use them). */
class Options : IDataSection
{
    protected this() {}   /// Do not instantiate directly.
    
    // All supported types, for generic handling via templates. It should be possible to change
    // the supported types simply by changing this list now (untested).
    template store(A...) { alias A store; }
    // NOTE: currently all types have transitioned to the new method, but the old method remains
    alias store!(bool, int, double, char[]) TYPES;    // all types
    alias store!(bool, int, double, char[]) CTYPES;   // types stored with a content
    //BEGIN Templates: internal
    private {
        // Get name of a type. Basically just stringof, but special handling for arrays.
        // Use TName!(T) for a valid symbol name, and T.stringof for a type.
        template TName(T : T[]) {
            const char[] TName = TName!(T) ~ "A";
        }
        template TName(T) {
            const char[] TName = T.stringof;
        }
        
        // Pointer lists
        template PLists(A...) {
            static if (A.length) {
                static if (TIsIn!(A[0], CTYPES)) {
                    const char[] PLists = PLists!(A[1..$]);
                } else
                    const char[] PLists = A[0].stringof~"*[ID] opts"~TName!(A[0])~";\n" ~ PLists!(A[1..$]);
            } else
                const char[] PLists = "";
        }
        
        // True if type is one of A
        template TIsIn(T, A...) {
            static if (A.length) {
                static if (is(T == A[0]))
                    const bool TIsIn = true;
                else
                    const bool TIsIn = TIsIn!(T,A[1..$]);
            } else
                const bool TIsIn = false;	// no more possibilities
        }
        
        // For addTag
        template addTagMixin(T, A...) {
            static if (TIsIn!(T, CTYPES)) {
                const char[] ifBlock = `if (tp == "`~T.stringof~`") {
    auto p = id in opts;
    if (p) {
        auto q = cast(`~VContentN!(T)~`) (*p);
        if (q) q.v = parseTo!(`~T.stringof~`) (dt);
    }
}`;
            } else
                const char[] ifBlock = `if (tp == "`~T.stringof~`") {
    `~T.stringof~`** p = id in opts`~TName!(T)~`;
    if (p !is null) **p = parseTo!(`~T.stringof~`) (dt);
}`;
            static if (A.length)
                const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
            else
                const char[] addTagMixin = ifBlock;
        }
        
        // For list
        template listMixin(A...) {
            static if (A.length) {
                static if (TIsIn!(A, CTYPES))
                    const char[] listMixin = listMixin!(A[1..$]);
                else
                    const char[] listMixin = `ret ~= opts`~TName!(A[0])~`.keys;` ~ listMixin!(A[1..$]);
            } else
                const char[] listMixin = ``;
        }
    }
    //END Templates: internal
    
    
    //BEGIN Static
    static {
    	/** Add an options sub-class to the list for loading and saving.
    	 *
    	 * Call from static this() (before Init calls load()). */
    	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 {
            c.secName = i;
            subClasses[cast(ID) i] = c;
        }
    
    	// Track all sections for saving/loading/other generic handling.
        Options[ID] subClasses;
        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 const fileName = "options";
        void load () {
            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 (NoFileException e) {
                // Just return. Options file will be created on exit.
            } catch (Exception e) {
                logger.warn ("Loading options failed: "~e.msg);
                logger.warn ("If warning persists, delete the offending file.");        // FIXME - delete the bad file somehow
            }
            foreach (opts; subClasses)
                opts.validate;  // post-load checking of variables
        }
        void save () {
            if (!changed) return;   // no changes to save
            debug logger.trace ("Saving options...");
            
            DataSet ds = new DataSet();
            foreach (id, subOpts; subClasses)
                ds.sec[id] = subOpts.optionChanges;
            
            // Read locally-stored options
            try {
                IReader reader;
                reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
                reader.dataSecCreator = delegate IDataSection(ID id) {
                    debug logger.warn ("New section appearing in options.mtt during save (ignored & overwritten): "~id);
                    return null;    // All recognised sections are already in the dataset.
                };
                reader.read;
            } catch (NoFileException) {
                // No user file exists; not an error.
            } catch (Exception e) {
            	// Log a message and continue, overwriting the file:
                logger.error ("Loading options aborted: " ~ e.msg);
            }
        
            try {
                IWriter writer;
                writer = confDir.makeMTWriter (fileName, ds);
                writer.write();
            } catch (Exception e) {
                logger.error ("Saving options aborted: "~e.msg);
            }
        }
    }
    //END Static
    
    
    //BEGIN Non-static
    /+ NOTE: according to spec: "Templates cannot be used to add non-static members or virtual
    functions to classes." However, this appears to work (but linking problems did occur).
    Alternative: use mixins. From OptionsChanges:
        // setT (used to be a template, but:
        // Templates cannot be used to add non-static members or virtual functions to classes. )
        template setMixin(A...) {
            static if (A.length) {
                const char[] setMixin = `void set`~TName!(A[0])~` (ID id, `~A[0].stringof~` x) {
                    `~TName!(T)~`s[id] = x;
                }
                ` ~ setMixin!(A[1..$]);
            } else
                const char[] setMixin = ``;
        }+/
    /** Set option symbol of an Options sub-class 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. */
    void set(T) (char[] symbol, T val) {
        static assert (TIsIn!(T,TYPES) && !TIsIn!(T, CTYPES), "Options.set does not support type "~T.stringof);
        
        changed = true;     // something got set (don't bother checking this isn't what it already was)
        
        try {
            mixin (`*(opts`~TName!(T)~`[cast(ID) symbol]) = val;`);
            mixin (`optionChanges.`~TName!(T)~`s[symbol] = val;`);
        } catch (ArrayBoundsException) {
            // log and ignore:
            logger.error ("Options.set: invalid symbol");
        }
    }
    /+
    /** Get option symbol of an Options sub-class.
     *
     * Using this method to read an option is not necessary, but allows for generic use.  */
    T get(T) (char[] symbol) {
        static assert (TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
        
        mixin (`alias opts`~TName!(T)~` optsVars;`);
        
        try {
            return *(optsVars[cast(ID) symbol]);
        } catch (ArrayBoundsException) {
            // log and ignore:
            logger.error ("Options.get: invalid symbol");
        }
    }+/
    
    /** List the names of all options of a specific type. */
    char[][] list () {
        char[][] ret;
        mixin (listMixin!(TYPES));
        return ret;
    }
    
    /// Get all Options stored with a ValueContent.
    ValueContent[char[]] content() {
        return opts;
    }
    
    /// Variable validate function. This implementation does nothing.
    void validate() {}
    
    protected {
        char[] secName;         // name of this option setting; set null after translation is loaded
        OptionChanges optionChanges;	// all changes to options (for saving)
    	
        // The "pointer lists", e.g. char[]*[ID] optscharA;
        mixin (PLists!(TYPES));
        ValueContent[char[]] opts;      // generic list of option values
    }
    
    //BEGIN Mergetag loading/saving code
    void addTag (char[] tp, ID id, char[] dt) {
        mixin(addTagMixin!(TYPES).addTagMixin);
    }
    // Only OptionChanges writes stuff
    void writeAll (ItemDelg dlg) {}
    //END Mergetag loading/saving code
    //END Non-static
    
    
    //BEGIN Templates: impl & optionsThis
    private {
        // Replace, e.g., bool, with BoolContent
        template contentName(A) {
            static if (TIsIn!(A, CTYPES)) {
                const char[] contentName = VContentN!(A);
            } else
                const char[] contentName = A.stringof;
        }
        // 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 will have a
         trailing comma unless no match was found in which case an empty string is returned.
         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..$]);
        }
        // May have a trailing comma. Assumes cIndex always returns less than A.$ .
        template aaVarsContent(char[] A) {//FIXME
            static if (A.length == 0)
            const char[] aaVarsContent = "";
            else static if (A[0] == ' ')
                const char[] aaVarsContent = aaVarsContent!(A[1..$]);
            else
                const char[] aaVarsContent = "\""~A[0..cIndex!(A)]~"\"[]:cast(ValueContent)"~A[0..cIndex!(A)] ~ "," ~
                aaVarsContent!(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~"]";
        }
        // if B is empty return an empty string otherswise return what's below:
        template catOrNothing(char[] A,char[] B) {
            static if (B.length)
                const char[] catOrNothing = A~` `~sTC!(B)~";\n";
            else
                const char[] catOrNothing = ``;
        }
        // foreach decl...
        template createContents(T, char[] A) {
            static if (A.length == 0)
                const char[] createContents = "";
            else static if (A[0] == ' ')
                const char[] createContents = createContents!(T,A[1..$]);
            else
                const char[] createContents = "opts[\""~A[0..cIndex!(A)]~"\"] = " ~ A[0..cIndex!(A)]~ " = (new "~VContentN!(T)~" (\""~A[0..cIndex!(A)]~"\")).addChangeCb (&optionChanges.set);\n"~
                createContents!(T,A[cIndex!(A)+1..$]);
        }
        // for recursing on TYPES
        template optionsThisInternal(char[] A, B...) {
            static if (B.length) {
                static if (TIsIn!(B[0], CTYPES)) {
                    const char[] optionsThisInternal = createContents!(B[0],parseT!(B[0].stringof,A))~
                    optionsThisInternal!(A,B[1..$]);
                } else
                const char[] optionsThisInternal = `opts`~TName!(B[0])~` = `~listOrNull!(sTC!(aaVars!(parseT!(B[0].stringof,A))))~";\n" ~ optionsThisInternal!(A,B[1..$]);
            } else
                const char[] optionsThisInternal = ``;
        }
        template declValsInternal(char[] A, B...) {
            static if (B.length) {
                const char[] declValsInternal = catOrNothing!(contentName!(B[0]),parseT!(B[0].stringof,A)) ~ declValsInternal!(A,B[1..$]);
            } else
                const char[] declValsInternal = ``;
        }
    } protected {
        /** Declares the values.
         *
         * Basic types are replaced with a ValueContent class to keep the option synchronized and
         * generalize use. */
        template declVals(char[] A) {
            const char[] declVals = declValsInternal!(A, TYPES);
        }
        /** Produces the implementation code to go in the constuctor. */
        template optionsThis(char[] A) {
            const char[] optionsThis =
                    "optionChanges = new OptionChanges;\n" ~
                    optionsThisInternal!(A,TYPES);
        }
        /+ Needs too many custom parameters to be worth it? Plus makes class less readable.
        /** 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 (declVals!(A)~`
        this () {
    	`~optionsThis!(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 = declVals!(A)~"\nthis(){\n"~optionsThis!(A)~"}";
            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
        }
    }
    //END Templates: impl & optionsThis
}

/** Special class to store all locally changed options, whatever the section. */
class OptionChanges : IDataSection
{
    //BEGIN Templates
    private {
        alias Options.TName TName;
        alias Options.TYPES TYPES;
        template Vars(A...) {
            static if (A.length) {
                const char[] Vars = A[0].stringof~`[ID] `~TName!(A[0])~`s;` ~ Vars!(A[1..$]);
            } else
                const char[] Vars = ``;
        }
        // For set
        template Set(A...) {
            static if (A.length) {
                const char[] Set= `void set (char[] s,`~A[0].stringof~` v) {
   Options.changed = true;
   `~TName!(A[0])~`s[s] = v;
}`~ Set!(A[1..$]);
            } else
                const char[] Set = ``;
        }
        
        // For addTag
        template addTagMixin(T, A...) {
            const char[] ifBlock = `if (tp == "`~T.stringof~`") {
                if ((id in `~TName!(T)~`s) is null)
                    `~TName!(T)~`s[id] = parseTo!(`~T.stringof~`) (dt);
            }`;
            static if (A.length)
                const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
            else
                const char[] addTagMixin = ifBlock;
        }
        // For writeAll
        template writeAllMixin(A...) {
            static if (A.length) {
                const char[] writeAllMixin =
                `foreach (id, val; `~TName!(A[0])~`s)
                    dlg ("`~A[0].stringof~`", id, serialize (val));
                ` ~ writeAllMixin!(A[1..$]);
            } else
                const char[] writeAllMixin = ``;
        }
    }
    //END Templates
    // 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.
    mixin(Vars!(TYPES));
    // set (char[] symbol, T value);    (not templates but for each type T)
    mixin(Set!(TYPES));
    
    this () {}
    
    //BEGIN Mergetag loading/saving code
    // HIGH_LOW priority: only load symbols not currently existing
    void addTag (char[] tp, ID id, char[] dt) {
        mixin (addTagMixin!(TYPES).addTagMixin);
    }
    void writeAll (ItemDelg dlg) {
        mixin(writeAllMixin!(TYPES));
    }
    //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. */
MiscOptions miscOpts;
class MiscOptions : Options {
    const A = "bool exitImmediately; int maxThreads, logLevel, logOutput; double pollInterval; char[] L10n;";
    //pragma (msg, impl!(A));
    mixin (impl!(A));
    
    void validate() {
        // Try to enforce sensible values, whilst being reasonably flexible:
        if (maxThreads() < 1 || maxThreads() > 64) {
            logger.warn ("maxThreads must be in the range 1-64. Defaulting to 4.");
            maxThreads = 4;
        }
        if (pollInterval() !<= 1.0 || pollInterval() !>= 0.0)
            pollInterval = 0.01;
    }
    
    static this() {
        miscOpts = new MiscOptions;
        Options.addOptionsClass (miscOpts, "MiscOptions");
    }
}