view mde/lookup/Options.d @ 82:ac1e3fd07275

New ssi file format. (De)serializer now supports non-ascii wide characters (encoded to UTF-8) and no longer supports non-ascii 8-bit chars which would result in bad UTF-8. Moved/renamed a few things left over from the last commit.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 30 Aug 2008 09:37:35 +0100
parents d8fccaa45d5f
children e0f1ec7fe73a
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.setup.paths;
import mde.exception;

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;

/** 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 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
{
    // 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; }
    alias store!(bool, int, double, char[]) TYPES;
    //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(T, A...) {
            static if (A.length) {
                const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n" ~ PLists!(A);
            } else
                const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n";
        }
        
        // 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~`") {
                `~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 writeAll
        template writeAllMixin(A...) {
            static if (A.length) {
                const char[] writeAllMixin =
                        `foreach (ID id, `~A[0].stringof~`* val; opts`~TName!(A[0])~`) dlg ("`~A[0].stringof~`", id, parseFrom!(`~A[0].stringof~`) (*val));` ~ writeAllMixin!(A[1..$]);
            } else
                const char[] writeAllMixin = ``;
        }
    }
    //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 {
            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
            }
        }
        void save () {
            if (!changed) return;   // no changes to save
        
            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) {
                    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);
            }
        }
    
        private Logger logger;
        static this() {
            logger = Log.getLogger ("mde.lookup.Options");
        }
    }
    //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), "Options 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(T) () {
        static assert (TIsIn!(T,TYPES), "Options does not support type "~T.stringof);
        
        mixin (`alias opts`~TName!(T)~` optsVars;`);
        
        return optsVars.keys;
    }
    
    protected {
        OptionChanges optionChanges;	// all changes to options (for saving)
    	
        // The "pointer lists", e.g. char[]*[ID] optscharA;
        mixin (PLists!(TYPES));
    }
    
    //BEGIN Mergetag loading/saving code
    void addTag (char[] tp, ID id, char[] dt) {
        mixin(addTagMixin!(TYPES).addTagMixin);
    }

    void writeAll (ItemDelg dlg) {
        mixin(writeAllMixin!(TYPES));
    }
    //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, 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~"]";
        }
        // for recursing on TYPES
        template optionsThisInternal(char[] A, B...) {
            static if (B.length) {
                const char[] optionsThisInternal = `opts`~TName!(B[0])~` = `~listOrNull!(sTC!(aaVars!(parseT!(B[0].stringof,A))))~";\n" ~ optionsThisInternal!(A,B[1..$]);
            } else
                const char[] optionsThisInternal = ``;
        }
    } protected {
        /** 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 (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 = 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 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));
    
    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. */
OptionsMisc miscOpts;
class OptionsMisc : Options {
    mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n, a,b,c;"));
    
    static this() {
        miscOpts = new OptionsMisc;
        Options.addOptionsClass (miscOpts, "misc");
    }
}