Mercurial > projects > mde
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 } |