#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 Michael J. Roberts. All Rights Reserved.
*
* TADS 3 Library - settings file management
*
* This is a framework that the library uses to keep track of certain
* preference settings - things like the NOTIFY, FOOTNOTES, and EXITS
* settings.
*
* The point of this framework is "global" settings - settings that apply
* not just to a particular game, but to all games that have a particular
* feature. Things like NOTIFY, FOOTNOTES, and some other such features
* are part of the standard library, so they tend to be available in most
* games. Furthermore, they tend to work more or less the same way in
* most games. As a result, a given player will probably prefer to set
* the options a particular way for most or all games. If a player
* doesn't like score notification, she'll probably dislike it across the
* board, not just in certain games.
*
* This module provides the internal, programmatic core for managing
* global preferences. There's no UI in this part of the implementation;
* the adv3 library layers the UI on top via the settingsUI object, but
* other alternative UIs could be built using the API provided here.
*
* The framework is extensible - there's an easy, structured way for
* library extensions and games to add their own configuration variables
* that will be automatically managed by the framework. All you have to
* do to create a new configuration variable is to create a SettingsItem
* object to represent it. Once you've created the object, the library
* will automatically find it and manage it for you.
*
* This module is designed to be separable from the adv3 library, so that
* alternative libraries or stand-alone (non-library-based) games can
* reuse it. This file has no dependencies on anything in adv3 (at
* least, it shouldn't).
*/
#include <tads.h>
#include <file.h>
/* ------------------------------------------------------------------------ */
/*
* A settings item. This encapsulates a single setting variable. When
* we're saving or restoring default settings, we'll simply loop over all
* objects of this class to get or set the current settings.
*
* Note that we don't make any assumptions in this base class about the
* type of the value associated with this setting, how it's stored, or
* how it's represented in the external configuration file. This means
* that each subclass has to provide the property or properties that
* store the item's value, and must also define the methods that operate
* on the value.
*
* If you want to force a particular default setting for a particular
* preference item, overriding the setting stored in the global
* preferences file, you can override that SettingsItem's
* settingFromText() method. This is the method that interprets the
* information in the preferences file, so if you want to ignore the
* preferences file setting, override this method to set the hard-coded
* value of your choosing.
*/
class SettingsItem: object
/*
* The setting's identifier string. This is the ID of the setting as
* it appears in the external configuration file.
*
* The ID should be chosen to ensure uniqueness. To reduce the
* chances of name collisions, we suggest a convention of using a two
* part name: a prefix identifying the source of the name (an
* abbreviated version of the name of the library, library extension,
* or game), followed by a period as a separator, followed by a short
* descriptive name for the variable. The library follows this
* convention by using names of the form "adv3.xxx" - the "adv3"
* prefix indicates the standard library.
*
* The ID should contain only letters, numbers, and periods. Don't
* use spaces or punctuation marks (other than periods).
*
* Note that the ID string is for the program's use, not the
* player's, so this isn't something we translate to different
* languages. Note, though, that the configuration file is a simple
* text file, so it wouldn't hurt to use a reasonably meaningful
* name, in case the user takes it upon herself to look at the
* contents of the file.
*/
settingID = ''
/*
* Display a message fragment that shows the current setting value.
* We use this to show the player exactly what we're saving or
* restoring in response to a SAVE DEFAULTS or RESTORE DEFAULTS
* command, so that there's no confusion about which settings are
* included. In most cases, the best thing to show here is the
* command that selects the current setting: "NOTIFY ON," for
* example. This is for the UI's convenience; it's not used by the
* settings manager itself.
*/
settingDesc = ""
/*
* Get the textual representation of the setting - returns a string
* representing the setting as it should appear in the external
* configuration file. We use this to write the setting to the file.
*/
settingToText() { /* subclasses must override */ }
/*
* Set the current value to the contents of the given string. The
* string contains a textual representation of a setting value, as
* previously generated with settingToText().
*/
settingFromText(str) { /* subclasses must override */ }
/*
* My "factory default" setting. At pre-init time, before we've
* loaded the settings file for the first time, we'll run through all
* SettingsItems and store their pre-defined source-code settings
* here, as though we were saving the values to a file. Later, when
* we load a file, if we find the file lacks an entry for this
* setting item, we'll simply re-load the factory default from this
* property.
*/
factoryDefault = nil
;
/*
* A binary settings item - this is for variables that have simple
* true/nil values.
*/
class BinarySettingsItem: SettingsItem
/* convert to text - use ON or OFF as the representation */
settingToText() { return isOn ? 'on' : 'off'; }
/* parse text */
settingFromText(str)
{
/* convert to lower-case and strip off spaces */
if (rexMatch('<space>*(<alpha>+)', str.toLower()) != nil)
str = rexGroup(1)[3];
/* get the new setting */
isOn = (str.toLower() == 'on');
}
/* our value is true (on) or nil (off) */
isOn = nil
;
/* ------------------------------------------------------------------------ */
/*
* The settings manager. This object gathers up some global methods for
* managing the saved settings. This base class provides only a
* programmatic interface - it doesn't have a user interface.
*/
settingsManager: object
/*
* Save the current settings. This writes out the current settings
* to the global settings file. On any error, the method throws an
* exception:
*
* - FileCreationException indicates that the settings file couldn't
* be opened for writing.
*/
saveSettings()
{
local s;
/* retrieve the current settings */
s = retrieveSettings();
/* if that failed, there's nothing more we can do */
if (s == nil)
return;
/*
* Update the file's contents with all of the current in-memory
* settings objects.
*/
forEachInstance(SettingsItem, {item: s.saveItem(item)});
/* write out the settings */
storeSettings(s);
}
/*
* Restore all of the settings. If an error occurs, we'll throw an
* exception:
*
* - SettingsNotSupportedException - this is an older interpreter
* that doesn't support the "special files" feature, so we can't save
* or restore the default settings.
*/
restoreSettings()
{
local s;
/* retrieve the current settings */
s = retrieveSettings();
/*
* update all of the in-memory settings objects with the values
* from the file
*/
forEachInstance(SettingsItem, {item: s.restoreItem(item)});
}
/*
* Retrieve the settings from the global settings file. This returns
* a SettingsFileData object that describes the file's contents.
* Note that if there simply isn't an existing settings file, we'll
* successfully return a SettingsFileData object with no data - the
* absence of a settings file isn't an error, but is merely
* equivalent to an empty settings file.
*/
retrieveSettings()
{
local f;
local s = new SettingsFileData();
local linePat = new RexPattern(
'<space>*(<alphanum|.>+)<space>*=<space>*([^\n]*)\n?$');
/*
* Try opening the settings file. Older interpreters don't
* support the "special files" feature; if the interpreter
* predates special file support, it'll throw a "string value
* required," since it won't recognize the special file ID value
* as a valid filename.
*/
try
{
/* open the "library defaults" special file */
f = File.openTextFile(LibraryDefaultsFile, FileAccessRead);
}
catch (FileNotFoundException fnf)
{
/*
* The interpreter supports the special file, but the file
* doesn't seem to exist. Simply return the empty file
* contents object.
*/
return s;
}
catch (RuntimeError rte)
{
/*
* if the error is "string value required," then we have an
* older interpreter that doesn't support special files -
* indicate this by returning nil
*/
if (rte.errno_ == 2019)
{
/* re-throw this as a SettingsNotSupportedException */
throw new SettingsNotSupportedException();
}
/* other exceptions are unexpected, so re-throw them */
throw rte;
}
/* read the file */
for (;;)
{
local l;
/* read the next line */
l = f.readFile();
/* stop if we've reached end of file */
if (l == nil)
break;
/* parse the line */
if (rexMatch(linePat, l) != nil)
{
/*
* it parsed - add the variable and its value to the
* contents object
*/
s.addItem(rexGroup(1)[3], rexGroup(2)[3]);
}
else
{
/* it doesn't parse, so just keep the line as a comment */
s.addComment(l);
}
}
/* done with the file - close it */
f.closeFile();
/* return the populated file contents object */
return s;
}
/* store the given SettingsFileData to the global settings file */
storeSettings(s)
{
local f;
/*
* Open the "library defaults" file. Note that we don't have to
* worry here about the old-interpreter situation that we handle
* in retrieveSettings() - if the interpreter doesn't support
* special files, we won't ever get this far, because we always
* have to retrieve the current file's contents before we can
* store the new contents.
*/
f = File.openTextFile(LibraryDefaultsFile, FileAccessWrite);
/* write each line of the file's contents */
foreach (local item in s.lst_)
item.writeToFile(f);
/* done with the file - close it */
f.closeFile();
}
;
/* ------------------------------------------------------------------------ */
/*
* Exception: the settings file mechanism isn't supported on this
* interpreter. This indicates that this is an older interpreter that
* doesn't support the "special files" feature, so we can't save or load
* the global settings file.
*/
class SettingsNotSupportedException: Exception
;
/* ------------------------------------------------------------------------ */
/*
* SettingsFileData - this is an object we use to represent the contents
* of the configuration file.
*/
class SettingsFileData: object
construct()
{
/*
* We store the contents of the file in two ways: as a list, in
* the same order in which the contents appear in the file; and
* as a lookup table keyed by variable name. The list lets us
* preserve the parts of the file's contents that we don't need
* to change when we read it in and write it back out. The
* lookup table makes it easy to look up particular variable
* values.
*/
tab_ = new LookupTable(16, 32);
lst_ = new Vector(16);
}
/* add a variable */
addItem(id, val)
{
local item;
/* create the item descriptor object */
item = new SettingsFileItem(id, val);
/* append it to our file-contents-ordered list */
lst_.append(item);
/* add it to the lookup table, keyed by the variable ID */
tab_[id] = item;
}
/* add a comment line */
addComment(str)
{
/* append a comment descriptor to the contents list */
lst_.append(new SettingsFileComment(str));
}
/*
* Save an item. This takes the current value from the given
* SettingsItem, and saves it to the in-memory representation of the
* file.
*/
saveItem(memItem)
{
local id;
local val;
local fileItem;
/* get the item's ID */
id = memItem.settingID;
/* get the string representation of the item's value */
val = memItem.settingToText();
/*
* look for a SettingsFileItem with the ID of the memory item
* we're saving
*/
fileItem = tab_[id];
/*
* If the file item exists, update its value with the value from
* the in-memory item. Otherwise, simply add a new file item
* with the given ID and value.
*/
if (fileItem != nil)
{
/*
* this variable was already in the file, so update it with
* the new value
*/
fileItem.val_ = val;
}
else
{
/* this variable wasn't previously in the file, so add it */
addItem(id, val);
}
}
/*
* Restore an item. We'll look for a value for the given item in the
* file contents. If we find the file item, we'll restore its value
* to the in-memory item. If we don't find the file item, we'll
* restore the factory default.
*/
restoreItem(memItem)
{
local fileItem;
/* look up the file item by ID */
fileItem = tab_[memItem.settingID];
/*
* if this item appears in the file, restore its value; if not,
* restore it to its factory default setting
*/
memItem.settingFromText(fileItem != nil
? fileItem.val_
: memItem.factoryDefault);
}
/* lookup table of values, keyed by variable name */
tab_ = nil
/* a list of SettingsFileItem objects giving the contents of the file */
lst_ = nil
;
/*
* SettingsFileItem - this object describes a single item within an
* external settings file.
*/
class SettingsFileItem: object
construct(id, val)
{
id_ = id;
val_ = val;
}
/* write this value to a file */
writeToFile(f) { f.writeFile(id_ + ' = ' + val_ + '\n'); }
/* the variable's ID */
id_ = nil
/* the string representation of the value */
val_ = nil
;
/*
* SettingsFileComment - this object describes an unparsed line in the
* settings file. We treat lines that don't match our parsing rules as
* comments. We preserve the contents and order of these lines, but we
* don't otherwise try to interpret them.
*/
class SettingsFileComment: object
construct(str)
{
/* if it doesn't end in a newline, add a newline */
if (!str.endsWith('\n'))
str += '\n';
/* remember the string */
str_ = str;
}
/* write the comment line to a file */
writeToFile(f) { f.writeFile(str_); }
/* the text from the file */
str_ = nil
;
TADS 3 Library Manual
Generated on 9/8/2006 from TADS version 3.0.11