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