| 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; |
|---|
| | 58 | // All supported types, for generic handling via templates. It should be possible to change |
|---|
| | 59 | // the supported types simply by changing this list now (untested). |
|---|
| | 60 | template store(A...) { alias A store; } |
|---|
| | 61 | alias store!(bool, int, double, char[]) TYPES; |
|---|
| | 62 | //BEGIN Templates: internal |
|---|
| | 63 | private { |
|---|
| | 64 | // Get name of a type. Basically just stringof, but special handling for arrays. |
|---|
| | 65 | // Use TName!(T) for a valid symbol name, and T.stringof for a type. |
|---|
| | 66 | template TName(T : T[]) { |
|---|
| | 67 | const char[] TName = TName!(T) ~ "A"; |
|---|
| | 68 | } |
|---|
| | 69 | template TName(T) { |
|---|
| | 70 | const char[] TName = T.stringof; |
|---|
| | 71 | } |
|---|
| | 72 | |
|---|
| | 73 | // Pointer lists |
|---|
| | 74 | template PLists(T, A...) { |
|---|
| | 75 | static if (A.length) { |
|---|
| | 76 | const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n" ~ PLists!(A); |
|---|
| | 77 | } else |
|---|
| | 78 | const char[] PLists = T.stringof~"*[ID] opts"~TName!(T)~";\n"; |
|---|
| | 79 | } |
|---|
| | 80 | |
|---|
| | 81 | // True if type is one of A |
|---|
| | 82 | template TIsIn(T, A...) { |
|---|
| | 83 | static if (A.length) { |
|---|
| | 84 | static if (is(T == A[0])) |
|---|
| | 85 | const bool TIsIn = true; |
|---|
| | 86 | else |
|---|
| | 87 | const bool TIsIn = TIsIn!(T,A[1..$]); |
|---|
| | 88 | } else |
|---|
| | 89 | const bool TIsIn = false; // no more possibilities |
|---|
| | 90 | } |
|---|
| | 91 | |
|---|
| | 92 | // For addTag |
|---|
| | 93 | template addTagMixin(T, A...) { |
|---|
| | 94 | const char[] ifBlock = `if (tp == "`~T.stringof~`") { |
|---|
| | 95 | `~T.stringof~`** p = id in opts`~TName!(T)~`; |
|---|
| | 96 | if (p !is null) **p = parseTo!(`~T.stringof~`) (dt); |
|---|
| | 97 | }`; |
|---|
| | 98 | static if (A.length) |
|---|
| | 99 | const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin; |
|---|
| | 100 | else |
|---|
| | 101 | const char[] addTagMixin = ifBlock; |
|---|
| | 102 | } |
|---|
| | 103 | |
|---|
| | 104 | // For writeAll |
|---|
| | 105 | template writeAllMixin(A...) { |
|---|
| | 106 | static if (A.length) { |
|---|
| | 107 | const char[] writeAllMixin = |
|---|
| | 108 | `foreach (ID id, `~A[0].stringof~`* val; opts`~TName!(A[0])~`) dlg ("`~A[0].stringof~`", id, parseFrom!(`~A[0].stringof~`) (*val));` ~ writeAllMixin!(A[1..$]); |
|---|
| | 109 | } else |
|---|
| | 110 | const char[] writeAllMixin = ``; |
|---|
| | 111 | } |
|---|
| | 112 | } |
|---|
| | 113 | //END Templates: internal |
|---|
| | 114 | |
|---|
| | 115 | |
|---|
| | 116 | //BEGIN Static |
|---|
| | 117 | static { |
|---|
| | 118 | /** Add an options sub-class to the list for loading and saving. |
|---|
| | 119 | * |
|---|
| | 120 | * Call from static this() (before Init calls load()). */ |
|---|
| | 121 | void addOptionsClass (Options c, char[] i) |
|---|
| | 122 | in { // Trap a couple of potential coding errors: |
|---|
| | 123 | assert (c !is null); // Instance must be created before calling addOptionsClass |
|---|
| | 124 | assert (((cast(ID) i) in subClasses) is null); // Don't allow a silent replacement |
|---|
| | 125 | } body { |
|---|
| | 126 | subClasses[cast(ID) i] = c; |
|---|
| | 127 | } |
|---|
| | 128 | |
|---|
| | 129 | // Track all sections for saving/loading/other generic handling. |
|---|
| | 130 | Options[ID] subClasses; |
|---|
| | 131 | bool changed = false; // any changes at all, i.e. do we need to save? |
|---|
| | 132 | |
|---|
| | 133 | /* Load/save options from file. |
|---|
| | 134 | * |
|---|
| | 135 | * If the file doesn't exist, no reading is attempted (options are left at default values). |
|---|
| | 136 | */ |
|---|
| | 137 | private const fileName = "options"; |
|---|
| | 138 | private const MT_LOAD_EXC = "Loading options aborted:"; |
|---|
| | 139 | void load () { |
|---|
| | 140 | // Check it exists (if not it should still be created on exit). |
|---|
| | 141 | // Don't bother checking it's not a folder, because it could still be a block or something. |
|---|
| | 142 | if (!confDir.exists (fileName)) return; |
|---|
| | 143 | |
|---|
| | 144 | try { |
|---|
| | 145 | IReader reader; |
|---|
| | 146 | reader = confDir.makeMTReader (fileName, PRIORITY.LOW_HIGH); |
|---|
| | 147 | reader.dataSecCreator = delegate IDataSection(ID id) { |
|---|
| | 148 | /* Recognise each defined section, and return null for unrecognised sections. */ |
|---|
| | 149 | Options* p = id in subClasses; |
|---|
| | 150 | if (p !is null) return *p; |
|---|
| | 151 | else return null; |
|---|
| | 152 | }; |
|---|
| | 153 | reader.read; |
|---|
| | 154 | } catch (MTException e) { |
|---|
| | 155 | logger.fatal (MT_LOAD_EXC); |
|---|
| | 156 | logger.fatal (e.msg); |
|---|
| | 157 | throw new optionsLoadException ("Mergetag exception (see above message)"); |
|---|
| | 158 | } |
|---|
| | 159 | } |
|---|
| | 160 | void save () { |
|---|
| | 161 | if (!changed) return; // no changes to save |
|---|
| | 162 | |
|---|
| | 163 | DataSet ds = new DataSet(); |
|---|
| | 164 | foreach (id, subOpts; subClasses) ds.sec[id] = subOpts.optionChanges; |
|---|
| | 165 | |
|---|
| | 166 | // Read locally-stored options |
|---|
| | 167 | try { |
|---|
| | 168 | IReader reader; |
|---|
| | 169 | reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_ONLY, ds); |
|---|
| | 170 | reader.dataSecCreator = delegate IDataSection(ID id) { |
|---|
| | 171 | return null; // All recognised sections are already in the dataset. |
|---|
| | 172 | }; |
|---|
| | 173 | reader.read; |
|---|
| | 174 | } catch (MTFileIOException) { |
|---|
| | 175 | // File either didn't exist or couldn't be opened. |
|---|
| | 176 | // Presuming the former, this is not a problem. |
|---|
| | 177 | } catch (MTException e) { |
|---|
| | 178 | // Log a message and continue, overwriting the file: |
|---|
| | 179 | logger.error (MT_LOAD_EXC); |
|---|
| | 180 | logger.error (e.msg); |
|---|
| | 181 | } |
|---|
| | 182 | |
|---|
| | 183 | try { |
|---|
| | 184 | IWriter writer; |
|---|
| | 185 | writer = confDir.makeMTWriter (fileName, ds); |
|---|
| | 186 | writer.write(); |
|---|
| | 187 | } catch (MTException e) { |
|---|
| | 188 | logger.error ("Saving options aborted! Reason:"); |
|---|
| | 189 | logger.error (e.msg); |
|---|
| | 190 | } |
|---|
| | 191 | } |
|---|
| | 192 | |
|---|
| | 193 | private Logger logger; |
|---|
| | 194 | static this() { |
|---|
| | 195 | logger = Log.getLogger ("mde.options"); |
|---|
| | 196 | } |
|---|
| | 197 | } |
|---|
| | 198 | //END Static |
|---|
| | 199 | |
|---|
| | 200 | |
|---|
| | 201 | //BEGIN Non-static |
|---|
| | 202 | /** Set option symbol of an Options sub-class to val. |
|---|
| | 203 | * |
|---|
| | 204 | * Due to the way options are handled generically, string IDs must be used to access the options |
|---|
| | 205 | * via hash-maps, which is a little slower than direct access but necessary since the option |
|---|
| | 206 | * must be changed in two separate places. */ |
|---|
| | 207 | void set(T) (char[] symbol, T val) { |
|---|
| | 208 | static if (!TIsIn!(T,TYPES)) |
|---|
| | 209 | static assert (false, "Options.set does not currently support type "~T.stringof); |
|---|
| | 210 | |
|---|
| | 211 | mixin (`alias opts`~T.stringof~` optsVars;`); |
|---|
| | 212 | |
|---|
| | 213 | changed = true; // something got set (don't bother checking this isn't what it already was) |
|---|
| | 214 | |
|---|
| | 215 | try { |
|---|
| | 216 | *(optsVars[cast(ID) symbol]) = val; |
|---|
| | 217 | optionChanges.set!(T) (cast(ID) symbol, val); |
|---|
| | 218 | } catch (ArrayBoundsException) { |
|---|
| | 219 | // log and ignore: |
|---|
| | 220 | logger.error ("Options.set: unkw!"); |
|---|
| | 221 | } |
|---|
| | 222 | } |
|---|
| | 223 | |
|---|
| | 224 | protected { |
|---|
| | 225 | OptionChanges optionChanges; // all changes to options (for saving) |
|---|
| | 226 | |
|---|
| | 227 | // The "pointer lists": |
|---|
| | 228 | mixin (PLists!(TYPES)); |
|---|
| | 229 | } |
|---|
| 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 |
|---|
| | 240 | //END Non-static |
|---|
| | 241 | |
|---|
| | 242 | |
|---|
| | 243 | //BEGIN Templates: impl & optionsThis |
|---|
| 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 | | }+/ |
|---|
| | 349 | //END Templates: impl & optionsThis |
|---|
| | 350 | } |
|---|
| | 351 | |
|---|
| | 352 | /* Special class to store all locally changed options, whatever the section. */ |
|---|
| | 353 | class OptionChanges : Options { |
|---|
| | 354 | //BEGIN Templates |
|---|
| | 355 | private { |
|---|
| | 356 | template Vars(A...) { |
|---|
| | 357 | static if (A.length) { |
|---|
| | 358 | const char[] Vars = A[0].stringof~`[] `~TName!(A[0])~`s;` ~ Vars!(A[1..$]); |
|---|
| | 359 | } else |
|---|
| | 360 | const char[] Vars = ``; |
|---|
| | 361 | } |
|---|
| | 362 | |
|---|
| | 363 | // For addTag; different to Options.addTag(). |
|---|
| | 364 | // Reverse priority: only load symbols not currently existing |
|---|
| | 365 | template addTagMixin(T, A...) { |
|---|
| | 366 | const char[] ifBlock = `if (tp == "`~T.stringof~`") { |
|---|
| | 367 | if ((id in opts`~TName!(T)~`) is null) { |
|---|
| | 368 | `~TName!(T)~`s ~= parseTo!(`~T.stringof~`) (dt); |
|---|
| | 369 | opts`~TName!(T)~`[id] = &`~TName!(T)~`s[$-1]; |
|---|
| | 370 | } |
|---|
| | 371 | }`; |
|---|
| | 372 | static if (A.length) |
|---|
| | 373 | const char[] addTagMixin = ifBlock~` else `~addTagMixin!(A).addTagMixin; |
|---|
| | 374 | else |
|---|
| | 375 | const char[] addTagMixin = ifBlock; |
|---|
| | 376 | } |
|---|
| | 377 | |
|---|
| | 378 | } |
|---|