comparison 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
comparison
equal deleted inserted replaced
62:960206198cbd 63:66d555da083e
1 /* LICENSE BLOCK
2 Part of mde: a Modular D game-oriented Engine
3 Copyright © 2007-2008 Diggory Hardy
4
5 This program is free software: you can redistribute it and/or modify it under the terms
6 of the GNU General Public License as published by the Free Software Foundation, either
7 version 2 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */
15
16 /** This module handles stored options, currently all except input maps.
17 *
18 * The purpose of having all options centrally controlled is to allow generic handling by the GUI
19 * and ease saving and loading of values. The Options class is only really designed around handling
20 * small numbers of variables for now.
21 */
22 module mde.lookup.Options;
23
24 import mde.exception;
25
26 import mde.mergetag.Reader;
27 import mde.mergetag.Writer;
28 import mde.mergetag.DataSet;
29 import mde.mergetag.exception;
30 import mde.setup.paths;
31
32 import tango.scrapple.text.convert.parseTo : parseTo;
33 import tango.scrapple.text.convert.parseFrom : parseFrom;
34
35 import tango.core.Exception : ArrayBoundsException;
36 import tango.util.log.Log : Log, Logger;
37
38 /** Base class for handling options.
39 *
40 * This class itself handles no options and should not be instantiated, but provides a sub-classable
41 * base for generic options handling. Also, the static portion of this class tracks sub-class
42 * instances and provides loading and saving methods.
43 *
44 * Each sub-class provides named variables for maximal-speed reading. Local sub-class references
45 * should be used for reading variables, and via the addOptionsClass() hook will be loaded from
46 * files during pre-init (init0 stage). Do not write changes directly to the subclasses or they will
47 * not be saved; use, for example, Options.setBool(...). Use an example like OptionsMisc as a
48 * template for creating a new Options sub-class.
49 *
50 * Details: Options sub-classes hold associative arrays of pointers to all option variables, with a
51 * char[] id. This list is used for saving, loading and to provide generic GUI options screens. The
52 * built-in support in Options is only for bool, int and char[] types (a float type may get added).
53 * Further to this, a generic class is used to store all options which have been changed, and if any
54 * have been changed, is merged with options from the user conf dir and saved on exit.
55 */
56 class Options : IDataSection
57 {
58 // No actual options are stored by this class. However, much of the infrastructure is
59 // present since it need not be redefined in sub-classes.
60
61 // The "pointer lists":
62 protected bool* [ID] optsBool;
63 protected int* [ID] optsInt;
64 protected double*[ID] optsDouble;
65 protected char[]*[ID] optsCharA;
66
67 //BEGIN Mergetag loading/saving code
68 void addTag (char[] tp, ID id, char[] dt) {
69 if (tp == "bool") {
70 bool** p = id in optsBool;
71 if (p !is null) **p = parseTo!(bool) (dt);
72 } else if (tp == "char[]") {
73 char[]** p = id in optsCharA;
74 if (p !is null) **p = parseTo!(char[]) (dt);
75 } else if (tp == "double") {
76 double** p = id in optsDouble;
77 if (p !is null) **p = parseTo!(double) (dt);
78 } else if (tp == "int") {
79 int** p = id in optsInt;
80 if (p !is null) **p = parseTo!(int) (dt);
81 }
82 }
83
84 void writeAll (ItemDelg dlg) {
85 foreach (ID id, bool* val; optsBool) dlg ("bool" , id, parseFrom!(bool ) (*val));
86 foreach (ID id, char[]* val; optsCharA) dlg ("char[]", id, parseFrom!(char[]) (*val));
87 foreach (ID id, double* val; optsDouble)dlg ("double", id, parseFrom!(double) (*val));
88 foreach (ID id, int* val; optsInt) dlg ("int" , id, parseFrom!(int ) (*val));
89 }
90 //END Mergetag loading/saving code
91
92 //BEGIN Static
93 /** Add an options sub-class to the list for loading and saving.
94 *
95 * Call from static this() (before Init calls load()). */
96 static void addOptionsClass (Options c, char[] i)
97 in { // Trap a couple of potential coding errors:
98 assert (c !is null); // Instance must be created before calling addOptionsClass
99 assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement
100 } body {
101 subClasses[cast(ID) i] = c;
102 subClassChanges[cast(ID) i] = new OptionsGeneric;
103 }
104
105 /** Set option symbol of Options class subClass to val.
106 *
107 * Due to the way options are handled generically, string IDs must be used to access the options
108 * via hash-maps, which is a little slower than direct access but necessary since the option
109 * must be changed in two separate places. */
110 private static const ERR_MSG = "Options.setXXX called with incorrect parameters!";
111 static void setBool (char[] subClass, char[] symbol, bool val) {
112 changed = true; // something got set (don't bother checking this isn't what it already was)
113
114 try {
115 *(subClasses[cast(ID) subClass].optsBool[cast(ID) symbol]) = val;
116 subClassChanges[cast(ID) subClass].setBool (cast(ID) symbol, val);
117 } catch (ArrayBoundsException) {
118 // log and ignore:
119 logger.error (ERR_MSG);
120 }
121 }
122 static void setInt (char[] subClass, char[] symbol, int val) {
123 changed = true; // something got set (don't bother checking this isn't what it already was)
124
125 try {
126 *(subClasses[cast(ID) subClass].optsInt[cast(ID) symbol]) = val;
127 subClassChanges[cast(ID) subClass].setInt (cast(ID) symbol, val);
128 } catch (ArrayBoundsException) {
129 // log and ignore:
130 logger.error (ERR_MSG);
131 }
132 }
133 static void setDouble (char[] subClass, char[] symbol, double val) {
134 changed = true; // something got set (don't bother checking this isn't what it already was)
135
136 try {
137 *(subClasses[cast(ID) subClass].optsDouble[cast(ID) symbol]) = val;
138 subClassChanges[cast(ID) subClass].setDouble (cast(ID) symbol, val);
139 } catch (ArrayBoundsException) {
140 // log and ignore:
141 logger.error (ERR_MSG);
142 }
143 }
144 static void setCharA (char[] subClass, char[] symbol, char[] val) {
145 changed = true; // something got set (don't bother checking this isn't what it already was)
146
147 try {
148 *(subClasses[cast(ID) subClass].optsCharA[cast(ID) symbol]) = val;
149 subClassChanges[cast(ID) subClass].setCharA (cast(ID) symbol, val);
150 } catch (ArrayBoundsException) {
151 // log and ignore:
152 logger.error (ERR_MSG);
153 }
154 }
155
156 // Track all sections for saving/loading/other generic handling.
157 static Options[ID] subClasses;
158 static OptionsGeneric[ID] subClassChanges;
159 static bool changed = false; // any changes at all, i.e. do we need to save?
160
161 /* Load/save options from file.
162 *
163 * If the file doesn't exist, no reading is attempted (options are left at default values).
164 */
165 private static const fileName = "options";
166 private static const MT_LOAD_EXC = "Loading options aborted:";
167 static void load () {
168 // Check it exists (if not it should still be created on exit).
169 // Don't bother checking it's not a folder, because it could still be a block or something.
170 if (!confDir.exists (fileName)) return;
171
172 try {
173 IReader reader;
174 reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH);
175 reader.dataSecCreator = delegate IDataSection(ID id) {
176 /* Recognise each defined section, and return null for unrecognised sections. */
177 Options* p = id in subClasses;
178 if (p !is null) return *p;
179 else return null;
180 };
181 reader.read;
182 } catch (MTException e) {
183 logger.fatal (MT_LOAD_EXC);
184 logger.fatal (e.msg);
185 throw new optionsLoadException ("Mergetag exception (see above message)");
186 }
187 }
188 static void save () {
189 if (!changed) return; // no changes to save
190
191 DataSet ds = new DataSet();
192 foreach (id, sec; subClassChanges) ds.sec[id] = sec;
193
194 // Read locally-stored options
195 try {
196 IReader reader;
197 reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds);
198 reader.dataSecCreator = delegate IDataSection(ID id) {
199 return null; // All recognised sections are already in the dataset.
200 };
201 reader.read;
202 } catch (MTFileIOException) {
203 // File either didn't exist or couldn't be opened.
204 // Presuming the former, this is not a problem.
205 } catch (MTException e) {
206 // Log a message and continue, overwriting the file:
207 logger.error (MT_LOAD_EXC);
208 logger.error (e.msg);
209 }
210
211 try {
212 IWriter writer;
213 writer = confDir.makeMTWriter (fileName, ds);
214 writer.write();
215 } catch (MTException e) {
216 logger.error ("Saving options aborted! Reason:");
217 logger.error (e.msg);
218 }
219 }
220
221 private static Logger logger;
222 static this() {
223 logger = Log.getLogger ("mde.options");
224 }
225 //END Static
226
227 //BEGIN Templates
228 private {
229 // Return index of first comma, or halts if not found.
230 template cIndex(char[] A) {
231 static if (A.length == 0)
232 static assert (false, "Error in implementation");
233 else static if (A[0] == ',')
234 const size_t cIndex = 0;
235 else
236 const size_t cIndex = 1 + cIndex!(A[1..$]);
237 }
238 // Return index of first semi-colon, or halts if not found.
239 template scIndex(char[] A) {
240 static if (A.length == 0)
241 static assert (false, "Error: no trailing semi-colon");
242 else static if (A[0] == ';')
243 const size_t scIndex = 0;
244 else
245 const size_t scIndex = 1 + scIndex!(A[1..$]);
246 }
247 // Look for "type symbols;" in A and return symbols as a comma separated list of names
248 // (even if type is encountered more than once). Output may contain spaces and, if
249 // non-empty, will contain a trailing comma. Assumes scIndex always returns less than A.$.
250 template parseT(char[] type, char[] A) {
251 static if (A.length < type.length + 1) // end of input, no match
252 const char[] parseT = "";
253 else static if (A[0] == ' ') // leading whitespace: skip
254 const char[] parseT = parseT!(type, A[1..$]);
255 else static if (A[0..type.length] == type && A[type.length] == ' ') // match
256 const char[] parseT = A[type.length+1 .. scIndex!(A)] ~ "," ~
257 parseT!(type, A[scIndex!(A)+1 .. $]);
258 else // no match
259 const char[] parseT = parseT!(type, A[scIndex!(A)+1 .. $]);
260 }
261 // May have a trailing comma. Assumes cIndex always returns less than A.$.
262 template aaVars(char[] A) {
263 static if (A.length == 0)
264 const char[] aaVars = "";
265 else static if (A[0] == ' ')
266 const char[] aaVars = aaVars!(A[1..$]);
267 else
268 const char[] aaVars = "\""~A[0..cIndex!(A)]~"\"[]:&"~A[0..cIndex!(A)] ~ "," ~
269 aaVars!(A[cIndex!(A)+1..$]);
270 }
271 // strip Trailing Comma
272 template sTC(char[] A) {
273 static if (A.length && A[$-1] == ',')
274 const char[] sTC = A[0..$-1];
275 else
276 const char[] sTC = A;
277 }
278 // if string is empty (other than space) return null, otherwise enclose: [A]
279 template listOrNull(char[] A) {
280 static if (A.length == 0)
281 const char[] listOrNull = "null";
282 else static if (A[0] == ' ')
283 const char[] listOrNull = listOrNull!(A[1..$]);
284 else
285 const char[] listOrNull = "["~A~"]";
286 }
287 } protected {
288 /** Produces the implementation code to go in the constuctor. */
289 template aaDefs(char[] A) {
290 const char[] aaDefs =
291 "optsBool = " ~ listOrNull!(sTC!(aaVars!(parseT!("bool" , A)))) ~ ";\n" ~
292 "optsInt = " ~ listOrNull!(sTC!(aaVars!(parseT!("int" , A)))) ~ ";\n" ~
293 "optsDouble = "~ listOrNull!(sTC!(aaVars!(parseT!("double", A)))) ~ ";\n" ~
294 "optsCharA = " ~ listOrNull!(sTC!(aaVars!(parseT!("char[]", A)))) ~ ";\n";
295 }
296 /+/** Produces the implementation code to go in the static constuctor. */
297 template optClassAdd(char[] symb) {
298 const char[] optClassAdd = symb ~ "=new "~classinfo(this).name~";\n";//Options.addOptionsClass("~symb~", );\n";
299 }+/
300 /** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
301 *
302 * Where type is one of bool, int, double, char[]. E.g.
303 * ---
304 * mixin (impl ("bool a, b; int i;"));
305 * ---
306 *
307 * In case this() needs to be customized, mixin(impl!(A)) is equivalent to:
308 * ---
309 * mixin (A~"\nthis(){\n"~aaDefs!(A)~"}");
310 * ---
311 *
312 * Notes: Only use space as whitespace (no new-lines or tabs). Make sure to add a trailing
313 * semi-colon (;) or you'll get told off! :D
314 *
315 * In general errors aren't reported well. Trial with pragma (msg, impl!(...)); if
316 * necessary.
317 *
318 * Extending: mixins could also be used for the static this() {...} or even the whole
319 * class, but doing so would rather decrease readability of any implementation. */
320 template impl(char[] A /+, char[] symb+/) {
321 const char[] impl = A~"\nthis(){\n"~aaDefs!(A)~"}";
322 // ~"\nstatic this(){\n"~optClassAdd!(symb)~"}"
323 }
324 }
325 /+/** mixin impl("type symbol[, symbol[...]];[type symbol[...];][...]")
326 *
327 * E.g.
328 * ---
329 * mixin (impl ("bool a, b; int i;"));
330 * ---
331 * The parser isn't highly accurate. */
332 // Try using templates instead? See std.metastrings
333 static char[] impl (char[] A) {
334 char[] bools;
335 char[] ints;
336
337 while (A.length) {
338 // Trim whitespace
339 {
340 size_t i = 0;
341 while (i < A.length && (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)))
342 ++i;
343 A = A[i..$];
344 }
345 if (A.length == 0) break;
346
347 char[] type;
348 for (size_t i = 0; i < A.length; ++i) {
349 if (A[i] == ' ' || (A[i] >= 9u && A[i] <= 0xD)) {
350 type = A[0..i];
351 A = A[i+1..$];
352 break;
353 }
354 }
355
356 char[] symbols;
357 for (size_t i = 0; i < A.length; ++i) {
358 if (A[i] == ';') {
359 symbols = A[0..i];
360 A = A[i+1..$];
361 break;
362 }
363 }
364
365 if (type == "bool") {
366 if (bools.length)
367 bools = bools ~ "," ~ symbols;
368 else
369 bools = symbols;
370 }
371 else if (type == "int") {
372 if (ints.length)
373 ints = ints ~ "," ~ symbols;
374 else
375 ints = symbols;
376 }
377 else {
378 // Unfortunately, we cannot output non-const strings (even though func is compile-time)
379 // We also cannot use pragma(msg) because the message gets printed even if the code isn't run.
380 //pragma(msg, "Warning: impl failed to parse whole input string");
381 // Cannot use Cout / logger either.
382 break;
383 }
384 }
385
386 char[] ret;
387 if (bools.length)
388 ret = "bool "~bools~";\n";
389 if (ints.length)
390 ret = ret ~ "int "~ints~";\n";
391
392
393
394 return ret;
395 }+/
396 //END Templates
397 }
398
399 /* Special class to store all locally changed options, whatever the section. */
400 class OptionsGeneric : Options {
401 // These store the actual values, but are never accessed directly except when initially added.
402 // optsX store pointers to each item added along with the ID and are used for access.
403 bool[] bools;
404 int[] ints;
405 double[] doubles;
406 char[][] strings;
407
408 this () {}
409
410 void setBool (ID id, bool x) {
411 bool** p = id in optsBool;
412 if (p !is null) **p = x;
413 else {
414 bools ~= x;
415 optsBool[id] = &bools[$-1];
416 }
417 }
418 void setInt (ID id, int x) {
419 int** p = id in optsInt;
420 if (p !is null) **p = x;
421 else {
422 ints ~= x;
423 optsInt[id] = &ints[$-1];
424 }
425 }
426 void setDouble (ID id, double x) {
427 double** p = id in optsDouble;
428 if (p !is null) **p = x;
429 else {
430 doubles ~= x;
431 optsDouble[id] = &doubles[$-1];
432 }
433 }
434 void setCharA (ID id, char[] x) {
435 char[]** p = id in optsCharA;
436 if (p !is null) **p = x;
437 else {
438 strings ~= x;
439 optsCharA[id] = &strings[$-1];
440 }
441 }
442
443 //BEGIN Mergetag loading/saving code
444 // Reverse priority: only load symbols not currently existing
445 void addTag (char[] tp, ID id, char[] dt) {
446 if (tp == "bool") {
447 if ((id in optsBool) is null) {
448 bools ~= parseTo!(bool) (dt);
449 optsBool[id] = &bools[$-1];
450 }
451 } else if (tp == "char[]") {
452 if ((id in optsCharA) is null) {
453 strings ~= parseTo!(char[]) (dt);
454 optsCharA[id] = &strings[$-1];
455 }
456 char[]** p = id in optsCharA;
457 if (p !is null) **p = parseTo!(char[]) (dt);
458 } else if (tp == "int") {
459 if ((id in optsInt) is null) {
460 ints ~= parseTo!(int) (dt);
461 optsInt[id] = &ints[$-1];
462 }
463 }
464 }
465 //END Mergetag loading/saving code
466 }
467
468 /* NOTE: Options sub-classes are expected to use a template to ease inserting contents and
469 * hide some of the "backend" functionality. Use impl as below, or read the documentation for impl.
470 *
471 * Each entry should have a Translation entry with humanized names and descriptions in
472 * data/L10n/ClassName.mtt
473 *
474 * To create a new Options sub-class, just copy, paste and adjust.
475 */
476
477 /** A home for all miscellaneous options, at least for now. */
478 OptionsMisc miscOpts;
479 class OptionsMisc : Options {
480 mixin (impl!("bool useThreads, exitImmediately; int logOptions; double pollInterval; char[] L10n;"));
481
482 static this() {
483 miscOpts = new OptionsMisc;
484 Options.addOptionsClass (miscOpts, "misc");
485 }
486 }