Mercurial > projects > mde
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"); + } +}