cfad47cfa3/t3compiler/tads3/lib/adv3/settings.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library - settings file management
7
 *   
8
 *   This is a framework that the library uses to keep track of certain
9
 *   preference settings - things like the NOTIFY, FOOTNOTES, and EXITS
10
 *   settings. 
11
 *   
12
 *   The point of this framework is "global" settings - settings that apply
13
 *   not just to a particular game, but to all games that have a particular
14
 *   feature.  Things like NOTIFY, FOOTNOTES, and some other such features
15
 *   are part of the standard library, so they tend to be available in most
16
 *   games.  Furthermore, they tend to work more or less the same way in
17
 *   most games.  As a result, a given player will probably prefer to set
18
 *   the options a particular way for most or all games.  If a player
19
 *   doesn't like score notification, she'll probably dislike it across the
20
 *   board, not just in certain games.
21
 *   
22
 *   This module provides the internal, programmatic core for managing
23
 *   global preferences.  There's no UI in this part of the implementation;
24
 *   the adv3 library layers the UI on top via the settingsUI object, but
25
 *   other alternative UIs could be built using the API provided here.
26
 *   
27
 *   The framework is extensible - there's an easy, structured way for
28
 *   library extensions and games to add their own configuration variables
29
 *   that will be automatically managed by the framework.  All you have to
30
 *   do to create a new configuration variable is to create a SettingsItem
31
 *   object to represent it.  Once you've created the object, the library
32
 *   will automatically find it and manage it for you.
33
 *   
34
 *   This module is designed to be separable from the adv3 library, so that
35
 *   alternative libraries or stand-alone (non-library-based) games can
36
 *   reuse it.  This file has no dependencies on anything in adv3 (at
37
 *   least, it shouldn't).  
38
 */
39
40
#include <tads.h>
41
#include <file.h>
42
43
44
/* ------------------------------------------------------------------------ */
45
/*
46
 *   A settings item.  This encapsulates a single setting variable.  When
47
 *   we're saving or restoring default settings, we'll simply loop over all
48
 *   objects of this class to get or set the current settings.
49
 *   
50
 *   Note that we don't make any assumptions in this base class about the
51
 *   type of the value associated with this setting, how it's stored, or
52
 *   how it's represented in the external configuration file.  This means
53
 *   that each subclass has to provide the property or properties that
54
 *   store the item's value, and must also define the methods that operate
55
 *   on the value.
56
 *   
57
 *   If you want to force a particular default setting for a particular
58
 *   preference item, overriding the setting stored in the global
59
 *   preferences file, you can override that SettingsItem's
60
 *   settingFromText() method.  This is the method that interprets the
61
 *   information in the preferences file, so if you want to ignore the
62
 *   preferences file setting, override this method to set the hard-coded
63
 *   value of your choosing.  
64
 */
65
class SettingsItem: object
66
    /*
67
     *   The setting's identifier string.  This is the ID of the setting as
68
     *   it appears in the external configuration file.
69
     *   
70
     *   The ID should be chosen to ensure uniqueness.  To reduce the
71
     *   chances of name collisions, we suggest a convention of using a two
72
     *   part name: a prefix identifying the source of the name (an
73
     *   abbreviated version of the name of the library, library extension,
74
     *   or game), followed by a period as a separator, followed by a short
75
     *   descriptive name for the variable.  The library follows this
76
     *   convention by using names of the form "adv3.xxx" - the "adv3"
77
     *   prefix indicates the standard library.
78
     *   
79
     *   The ID should contain only letters, numbers, and periods.  Don't
80
     *   use spaces or punctuation marks (other than periods).
81
     *   
82
     *   Note that the ID string is for the program's use, not the
83
     *   player's, so this isn't something we translate to different
84
     *   languages.  Note, though, that the configuration file is a simple
85
     *   text file, so it wouldn't hurt to use a reasonably meaningful
86
     *   name, in case the user takes it upon herself to look at the
87
     *   contents of the file.  
88
     */
89
    settingID = ''
90
91
    /* 
92
     *   Display a message fragment that shows the current setting value.
93
     *   We use this to show the player exactly what we're saving or
94
     *   restoring in response to a SAVE DEFAULTS or RESTORE DEFAULTS
95
     *   command, so that there's no confusion about which settings are
96
     *   included.  In most cases, the best thing to show here is the
97
     *   command that selects the current setting: "NOTIFY ON," for
98
     *   example.  This is for the UI's convenience; it's not used by the
99
     *   settings manager itself.  
100
     */
101
    settingDesc = ""
102
103
    /* 
104
     *   Get the textual representation of the setting - returns a string
105
     *   representing the setting as it should appear in the external
106
     *   configuration file.  We use this to write the setting to the file.
107
     */
108
    settingToText() { /* subclasses must override */ }
109
110
    /* 
111
     *   Set the current value to the contents of the given string.  The
112
     *   string contains a textual representation of a setting value, as
113
     *   previously generated with settingToText().  
114
     */
115
    settingFromText(str) { /* subclasses must override */ }
116
117
    /* 
118
     *   My "factory default" setting.  At pre-init time, before we've
119
     *   loaded the settings file for the first time, we'll run through all
120
     *   SettingsItems and store their pre-defined source-code settings
121
     *   here, as though we were saving the values to a file.  Later, when
122
     *   we load a file, if we find the file lacks an entry for this
123
     *   setting item, we'll simply re-load the factory default from this
124
     *   property. 
125
     */
126
    factoryDefault = nil
127
;
128
129
/*
130
 *   A binary settings item - this is for variables that have simple
131
 *   true/nil values. 
132
 */
133
class BinarySettingsItem: SettingsItem
134
    /* convert to text - use ON or OFF as the representation */
135
    settingToText() { return isOn ? 'on' : 'off'; }
136
137
    /* parse text */
138
    settingFromText(str)
139
    {
140
        /* convert to lower-case and strip off spaces */
141
        if (rexMatch('<space>*(<alpha>+)', str.toLower()) != nil)
142
            str = rexGroup(1)[3];
143
144
        /* get the new setting */
145
        isOn = (str.toLower() == 'on');
146
    }
147
148
    /* our value is true (on) or nil (off) */
149
    isOn = nil
150
;
151
152
153
/* ------------------------------------------------------------------------ */
154
/*
155
 *   The settings manager.  This object gathers up some global methods for
156
 *   managing the saved settings.  This base class provides only a
157
 *   programmatic interface - it doesn't have a user interface.  
158
 */
159
settingsManager: object
160
    /*
161
     *   Save the current settings.  This writes out the current settings
162
     *   to the global settings file.  On any error, the method throws an
163
     *   exception:
164
     *   
165
     *   - FileCreationException indicates that the settings file couldn't
166
     *   be opened for writing.  
167
     */
168
    saveSettings()
169
    {
170
        local s;
171
        
172
        /* retrieve the current settings */
173
        s = retrieveSettings();
174
175
        /* if that failed, there's nothing more we can do */
176
        if (s == nil)
177
            return;
178
179
        /* 
180
         *   Update the file's contents with all of the current in-memory
181
         *   settings objects. 
182
         */
183
        forEachInstance(SettingsItem, {item: s.saveItem(item)});
184
185
        /* write out the settings */
186
        storeSettings(s);
187
    }
188
189
    /* 
190
     *   Restore all of the settings.  If an error occurs, we'll throw an
191
     *   exception:
192
     *   
193
     *   - SettingsNotSupportedException - this is an older interpreter
194
     *   that doesn't support the "special files" feature, so we can't save
195
     *   or restore the default settings.  
196
     */
197
    restoreSettings()
198
    {
199
        local s;
200
        
201
        /* retrieve the current settings */
202
        s = retrieveSettings();
203
204
        /* 
205
         *   update all of the in-memory settings objects with the values
206
         *   from the file 
207
         */
208
        forEachInstance(SettingsItem, {item: s.restoreItem(item)});
209
    }
210
211
    /* 
212
     *   Retrieve the settings from the global settings file.  This returns
213
     *   a SettingsFileData object that describes the file's contents.
214
     *   Note that if there simply isn't an existing settings file, we'll
215
     *   successfully return a SettingsFileData object with no data - the
216
     *   absence of a settings file isn't an error, but is merely
217
     *   equivalent to an empty settings file.  
218
     */
219
    retrieveSettings()
220
    {
221
        local f;
222
        local s = new SettingsFileData();
223
        local linePat = new RexPattern(
224
            '<space>*(<alphanum|.>+)<space>*=<space>*([^\n]*)\n?$');
225
        
226
        /* 
227
         *   Try opening the settings file.  Older interpreters don't
228
         *   support the "special files" feature; if the interpreter
229
         *   predates special file support, it'll throw a "string value
230
         *   required," since it won't recognize the special file ID value
231
         *   as a valid filename.  
232
         */
233
        try
234
        {
235
            /* open the "library defaults" special file */
236
            f = File.openTextFile(LibraryDefaultsFile, FileAccessRead);
237
        }
238
        catch (FileNotFoundException fnf)
239
        {
240
            /* 
241
             *   The interpreter supports the special file, but the file
242
             *   doesn't seem to exist.  Simply return the empty file
243
             *   contents object. 
244
             */
245
            return s;
246
        }
247
        catch (RuntimeError rte)
248
        {
249
            /* 
250
             *   if the error is "string value required," then we have an
251
             *   older interpreter that doesn't support special files -
252
             *   indicate this by returning nil 
253
             */
254
            if (rte.errno_ == 2019)
255
            {
256
                /* re-throw this as a SettingsNotSupportedException */
257
                throw new SettingsNotSupportedException();
258
            }
259
260
            /* other exceptions are unexpected, so re-throw them */
261
            throw rte;
262
        }
263
264
        /* read the file */
265
        for (;;)
266
        {
267
            local l;
268
            
269
            /* read the next line */
270
            l = f.readFile();
271
272
            /* stop if we've reached end of file */
273
            if (l == nil)
274
                break;
275
276
            /* parse the line */
277
            if (rexMatch(linePat, l) != nil)
278
            {
279
                /* 
280
                 *   it parsed - add the variable and its value to the
281
                 *   contents object 
282
                 */
283
                s.addItem(rexGroup(1)[3], rexGroup(2)[3]);
284
            }
285
            else
286
            {
287
                /* it doesn't parse, so just keep the line as a comment */
288
                s.addComment(l);
289
            }
290
        }
291
292
        /* done with the file - close it */
293
        f.closeFile();
294
295
        /* return the populated file contents object */
296
        return s;
297
    }
298
299
    /* store the given SettingsFileData to the global settings file */
300
    storeSettings(s)
301
    {
302
        local f;
303
        
304
        /* 
305
         *   Open the "library defaults" file.  Note that we don't have to
306
         *   worry here about the old-interpreter situation that we handle
307
         *   in retrieveSettings() - if the interpreter doesn't support
308
         *   special files, we won't ever get this far, because we always
309
         *   have to retrieve the current file's contents before we can
310
         *   store the new contents.  
311
         */
312
        f = File.openTextFile(LibraryDefaultsFile, FileAccessWrite);
313
314
        /* write each line of the file's contents */
315
        foreach (local item in s.lst_)
316
            item.writeToFile(f);
317
318
        /* done with the file - close it */
319
        f.closeFile();
320
    }
321
;
322
323
/* ------------------------------------------------------------------------ */
324
/*
325
 *   Exception: the settings file mechanism isn't supported on this
326
 *   interpreter.  This indicates that this is an older interpreter that
327
 *   doesn't support the "special files" feature, so we can't save or load
328
 *   the global settings file. 
329
 */
330
class SettingsNotSupportedException: Exception
331
;
332
333
/* ------------------------------------------------------------------------ */
334
/*
335
 *   SettingsFileData - this is an object we use to represent the contents
336
 *   of the configuration file. 
337
 */
338
class SettingsFileData: object
339
    construct()
340
    {
341
        /* 
342
         *   We store the contents of the file in two ways: as a list, in
343
         *   the same order in which the contents appear in the file; and
344
         *   as a lookup table keyed by variable name.  The list lets us
345
         *   preserve the parts of the file's contents that we don't need
346
         *   to change when we read it in and write it back out.  The
347
         *   lookup table makes it easy to look up particular variable
348
         *   values.  
349
         */
350
        tab_ = new LookupTable(16, 32);
351
        lst_ = new Vector(16);
352
    }
353
354
    /* add a variable */
355
    addItem(id, val)
356
    {
357
        local item;
358
        
359
        /* create the item descriptor object */
360
        item = new SettingsFileItem(id, val);
361
362
        /* append it to our file-contents-ordered list */
363
        lst_.append(item);
364
365
        /* add it to the lookup table, keyed by the variable ID */
366
        tab_[id] = item;
367
    }
368
369
    /* add a comment line */
370
    addComment(str)
371
    {
372
        /* append a comment descriptor to the contents list */
373
        lst_.append(new SettingsFileComment(str));
374
    }
375
376
    /*
377
     *   Save an item.  This takes the current value from the given
378
     *   SettingsItem, and saves it to the in-memory representation of the
379
     *   file.  
380
     */
381
    saveItem(memItem)
382
    {
383
        local id;
384
        local val;
385
        local fileItem;
386
387
        /* get the item's ID */
388
        id = memItem.settingID;
389
390
        /* get the string representation of the item's value */
391
        val = memItem.settingToText();
392
        
393
        /* 
394
         *   look for a SettingsFileItem with the ID of the memory item
395
         *   we're saving 
396
         */
397
        fileItem = tab_[id];
398
399
        /* 
400
         *   If the file item exists, update its value with the value from
401
         *   the in-memory item.  Otherwise, simply add a new file item
402
         *   with the given ID and value. 
403
         */
404
        if (fileItem != nil)
405
        {
406
            /* 
407
             *   this variable was already in the file, so update it with
408
             *   the new value 
409
             */
410
            fileItem.val_ = val;
411
        }
412
        else
413
        {
414
            /* this variable wasn't previously in the file, so add it */
415
            addItem(id, val);
416
        }
417
    }
418
419
    /*
420
     *   Restore an item.  We'll look for a value for the given item in the
421
     *   file contents.  If we find the file item, we'll restore its value
422
     *   to the in-memory item.  If we don't find the file item, we'll
423
     *   restore the factory default.  
424
     */
425
    restoreItem(memItem)
426
    {
427
        local fileItem;
428
        
429
        /* look up the file item by ID */
430
        fileItem = tab_[memItem.settingID];
431
432
        /* 
433
         *   if this item appears in the file, restore its value; if not,
434
         *   restore it to its factory default setting 
435
         */
436
        memItem.settingFromText(fileItem != nil
437
                                ? fileItem.val_
438
                                : memItem.factoryDefault);
439
    }
440
441
    /* lookup table of values, keyed by variable name */
442
    tab_ = nil
443
444
    /* a list of SettingsFileItem objects giving the contents of the file */
445
    lst_ = nil
446
;
447
448
/*
449
 *   SettingsFileItem - this object describes a single item within an
450
 *   external settings file. 
451
 */
452
class SettingsFileItem: object
453
    construct(id, val)
454
    {
455
        id_ = id;
456
        val_ = val;
457
    }
458
459
    /* write this value to a file */
460
    writeToFile(f) { f.writeFile(id_ + ' = ' + val_ + '\n'); }
461
462
    /* the variable's ID */
463
    id_ = nil
464
465
    /* the string representation of the value */
466
    val_ = nil
467
;
468
469
/*
470
 *   SettingsFileComment - this object describes an unparsed line in the
471
 *   settings file.  We treat lines that don't match our parsing rules as
472
 *   comments.  We preserve the contents and order of these lines, but we
473
 *   don't otherwise try to interpret them. 
474
 */
475
class SettingsFileComment: object
476
    construct(str)
477
    {
478
        /* if it doesn't end in a newline, add a newline */
479
        if (!str.endsWith('\n'))
480
            str += '\n';
481
482
        /* remember the string */
483
        str_ = str;
484
    }
485
486
    /* write the comment line to a file */
487
    writeToFile(f) { f.writeFile(str_); }
488
489
    /* the text from the file */
490
    str_ = nil
491
;
492
493