view mde/lookup/Options.d @ 134:7ababdf97748

Moved mde.setup.paths to mde.file.paths and paths.mdeReader to mde.file.mergetag.Reader.MTMultiReader.
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 29 Jan 2009 14:59:45 +0000
parents 1b1e2297e2fc
children
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 loading and saving of, and allows generic access to named option variables
 * of a simple type (see Options.TYPES). */
module mde.lookup.Options;

import mde.file.paths;
import mde.exception;

public import mde.content.AStringContent;
import mde.content.miscContent;	// ContentLists used by content.Items

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

/*************************************************************************************************
 * This class and the OptionChanges class contain all the functionality.
 * 
 * Options are stored in derived class instances, tracked by the static portion of Options. Each
 * value is stored in a ValueContent class, whose value can be accessed with opCall, opCast and
 * opAssign. These class objects can be given callbacks called whenever their value is changed.
 * 
 * Public static methods allow getting the list of tracked sub-class instances, and loading and saving
 * option values. A public non-static method allows generic access to option variables.
 * 
 * Generic access to Options is of most use to a gui, allowing Options to be edited generically.
 * 
 * The easiest way to use Options is to use an existing sub-class as a template, e.g. MiscOptions.
 *************************************************************************************************/
class Options : IDataSection
{
    /** Do not instantiate directly; use a sub-class.
     *
     * CTOR adds any created instance to the list of classes tracked statically for loading/saving
     * and generic access.
     * 
     * Normally instances are created by a static CTOR. */
    protected this(char[] name)
    in {
	assert (((cast(ID) name) in subClasses) is null);  // Don't allow a silent replacement
    } body {
	subClasses[cast(ID) name] = this;
    }
    
    //BEGIN Templates: internal
    package {
	// All supported types, for generic handling via templates. It should be possible to change
	// the supported types simply by changing this list.
	template store(A...) { alias A store; }
	alias store!(bool, int, double, char[]) TYPES;   // types handled
	
	// 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;
        }
    }
    private {
        // 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...) {
            const char[] ifBlock = `if (tp == "`~T.stringof~`") {
    auto p = id in opts;
    if (p) {
        auto q = cast(`~ContentN!(T)~`) (*p);
        if (q) q.assignNoCb = parseTo!(`~T.stringof~`) (dt);
    }
}`;
            static if (A.length)
                const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin;
            else
                const char[] addTagMixin = ifBlock;
        }
    }
    //END Templates: internal
    
    
    //BEGIN Static
    static {
	/** Get the hash map of Options classes. READ-ONLY. */
	Options[ID] optionsClasses () {
	    return subClasses;
	}
	
	// Track all sections for saving/loading/other generic handling.
        private Options[ID] subClasses;
        private bool changed = false;	// any changes at all, i.e. do we need to save?
	
	ContentList allContentList;	/// Initially null; created by mde.content.Items on use.
	
    	/* 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
    /// Get all Options stored with a ValueContent.
    Content[char[]] content() {
        return opts;
    }
    
    /** Variable validate function, called when options are loaded from file.
     *
     * This can be overridden to enforce limits on option variables, etc. */
    protected void validate() {}
    
    /** All content in a ContentList. Initially null; mde.content.Items creates this and loads the
    * translation strings of all sub-content upon first request involving this Options instance. */
    ContentList contentList;
    
    protected {
        OptionChanges optionChanges;	// all changes to options (for saving)
	Content[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 {
        // 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 .. $]);
        }
        // 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 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 "~ContentN!(T)~" (\""~A[0..cIndex!(A)]~"\")).addCallback (&optionChanges.set);\n"~
                createContents!(T,A[cIndex!(A)+1..$]);
        }
        // for recursing on TYPES
        template optionsThisInternal(char[] A, B...) {
            static if (B.length) {
                const char[] optionsThisInternal = createContents!(B[0],parseT!(B[0].stringof,A))~
		    optionsThisInternal!(A,B[1..$]);
	    } else
                const char[] optionsThisInternal = ``;
        }
        template declValsInternal(char[] A, B...) {
            static if (B.length) {
                const char[] declValsInternal = catOrNothing!(ContentN!(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" ~
                    "super (name);\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 (char[] name) {
	`~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(char[] name){\n"~optionsThis!(A)~"}";
            // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
        }
    }
    //END Templates: impl & optionsThis
}

/*************************************************************************************************
 * Special class to store all locally changed options.
 * 
 * This allows only changed options and those already stored in the user directory to be saved, so
 * that other options can be merged in from a global directory, allowing any options not locally
 * set to be changed globally.
 *************************************************************************************************/
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 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));
    
    void set (Content c) {
	union U {
	    BoolContent bc;
	    StringContent sc;
	    IntContent ic;
	    DoubleContent dc;
	}
	U u;
	if ((u.bc = cast(BoolContent) c) !is null)
	    bools[u.bc.symbol] = u.bc();
	else if ((u.sc = cast(StringContent) c) !is null)
	    charAs[u.sc.symbol] = u.sc();
	else if ((u.ic = cast(IntContent) c) !is null)
	    ints[u.ic.symbol] = u.ic();
	else if ((u.dc = cast(DoubleContent) c) !is null)
	    doubles[u.dc.symbol] = u.dc();
	Options.changed = true;
    }
	
    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
}

/** A home for all miscellaneous options.
 *
 * Also a template for deriving Options; comments explain what does what.
 * 
 * Translation strings for the options are looked for in data/L10n/SectionName.mtt where
 * this ("SectionName") names the instance. */
MiscOptions miscOpts;
class MiscOptions : Options {
    /* The key step is to mixin impl.
    The syntax is just as simple variables are declared, which is how these options used to be
    stored. Now they're enclosed in ValueContent classes; e.g. "char[] L10n;" is replaced with
    "TextContent L10n;". The pragma statement can be uncommented to see what code gets injected
    (note: pragma () gets called each time the module is imported as well as when it's compiled).
    impl creates a this() function; if you want to include your own CTOR see impl's ddoc. */
    const A = "bool exitImmediately; int maxThreads, logLevel, logOutput; double pollInterval; char[] L10n;";
    //pragma (msg, impl!(A));
    //mixin (impl!(A));
    BoolContent exitImmediately;
    IntContent maxThreads, logOutput;
    EnumContent logLevel;
    DoubleContent pollInterval;
    StringContent L10n;
    
    this(char[] name){
	optionChanges = new OptionChanges;
	super (name);
	opts["exitImmediately"] = (exitImmediately = new BoolContent ("exitImmediately")).addCallback (&optionChanges.set);
	opts["maxThreads"] = (maxThreads = new IntContent ("maxThreads")).addCallback (&optionChanges.set);
	opts["logLevel"] = (logLevel = new EnumContent ("logLevel", ["Trace", "Info", "Warn", "Error", "Fatal", "None"])).addCallback (&optionChanges.set);
	opts["logOutput"] = (logOutput = new IntContent ("logOutput")).addCallback (&optionChanges.set);
	opts["pollInterval"] = (pollInterval = new DoubleContent ("pollInterval")).addCallback (&optionChanges.set);
	opts["L10n"] = (L10n = new StringContent ("L10n")).addCallback (&optionChanges.set);
    }
    
    // Overriding validate allows limits to be enforced on variables at load time. Currently
    // there's no infrastructure for enforcing limits when options are set at run-time.
    override 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() !<= 0.1 || pollInterval() !>= 0.0)
            pollInterval = 0.01;
    }
    
    // A static CTOR is a good place to create the instance (it must be created before init runs).
    static this() {
	// Adds instance to Options's tracking; the string is the section name in the config files.
	miscOpts = new MiscOptions ("MiscOptions");
    }
}