source: branches/active/character_customization/game/parpg/settings.py @ 833

Revision 827, 15.0 KB checked in by technomage, 8 years ago (diff)

Patch by Technomage

  • Edited settings.py to expand any "~" (home directory) characters in configuration file paths;
  • Settings.read now raises an appropriate IOError when it cannot read a configuration file;
  • Property svn:executable set to *
Line 
1#!/usr/bin/env python2
2
3#  Copyright (C) 2011  Edwin Marshall <emarshall85@gmail.com>
4
5#   This file is part of PARPG.
6#
7#   PARPG is free software: you can redistribute it and/or modify
8#   it under the terms of the GNU General Public License as published by
9#   the Free Software Foundation, either version 3 of the License, or
10#   (at your option) any later version.
11#
12#   PARPG is distributed in the hope that it will be useful,
13#   but WITHOUT ANY WARRANTY; without even the implied warranty of
14#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#   GNU General Public License for more details.
16#
17#   You should have received a copy of the GNU General Public License
18#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
19
20""" Provides a class used for reading and writing various configurable options
21    throughout the game
22
23    This class produces an INI formated settings file as opposed to an XML
24    formatted one. The reason that python's built-in ConfigurationParser isn't
25    sufficient is because comments aren't preserved when writing a settings
26    file, the order in which the options are written isn't preserved, and the
27    interface used with this class is arguably more convenient that
28    ConfigParser's.
29
30    Default Settings may be generated by envoking this module from the
31    command line:
32        python -m settings.py [system] [data_directory]
33
34    where [system] is one of local, windows, or linux (mac coming soon),
35    and data_directory is the base path for the data files to be loaded.
36
37    Both [system] and [data_directory] are option. If omitted, both
38    default to whichever what is reasonable based on the system settings.py
39    is run on
40"""
41
42import os
43import sys
44import platform
45
46class Section(object):
47    """ An object that represents a section in a settings file.
48
49        Options can be added to a section by simply assigning a value to an
50        attribute:
51            section.foo = baz
52        would produce:
53            [section]
54            foo = baz
55        in the settings file. Options that do not exist on assignment
56        are created dynamcially.
57
58        Values are automatically converted to the appropriate python type.
59        Options that begin and end with brackets([, ]) are converted to lists,
60        and options that are double-quoted (") are converted to strings.
61        Section also recognizes booleans regardless of case, in addition to the
62        literals 'yes' and 'no' of any case. Except in the case of
63        double-quoted strings, extra white-space is trimmed, so you need not
64        worry. For example:
65            foo = bar
66        is equivalent to :
67            foo    =         baz
68    """
69    def __init__(self, name):
70        """ Initialize a new section.
71
72            @param name: name of the section. In the INI file, sections are surrounded
73                         by brackets ([name])
74            @type name: string
75        """
76        self.name = name
77
78    def __setattr__(self, option, value):
79        """ Assign a value to an option, converting types when appropriate.
80
81            @param option: name of the option to assign a value to.
82            @type option: string @param value: value to be assigned to the option.
83            @type value: int, float, string, boolean, or list
84        """
85        value = str(value)
86        if value.startswith('[') and value.endswith(']'):
87            value = [item.strip() for item in value[1:-1].split(',')]
88        elif value.lower() == 'true' or value.lower() == 'yes':
89            value = True
90        elif value.lower() == 'false' or value.lower() == 'no':
91            value = False
92        elif value.isdigit():
93            value = int(value)
94        else:
95            try:
96                value = float(value)
97            except ValueError:
98                # leave as string
99                pass
100
101        self.__dict__[option] = value
102
103    def __getattribute__(self, option):
104        """ Returns the option's value"""
105        # Remove leading and trailing quotes from strings that have them
106        return_value = object.__getattribute__(self, option)
107        try:
108            for key, value in return_value.iteritems():
109                if (hasattr(value, 'split') and 
110                    value.startswith("\"") and value.endswith("\"")):
111                    return_value[key] = value[1:-1]
112        except AttributeError:
113            pass
114
115        return return_value
116
117    @property
118    def options(self):
119        """ Returns a dictionary of existing options """
120        options = self.__dict__
121        # get rid of properties that aren't actually options
122        if options.has_key('name'):
123            options.pop('name')
124
125        return options
126
127class Settings(object):
128    """ An object that represents a settings file, its sectons,
129        and the options defined within those sections.
130    """
131    def __init__(self, system_path=None, user_path=None, suffix='.cfg'):
132        """ initializes a new settings object. If no paths are given, they are
133            guessed based on whatever platform the script was run on.
134
135            Examples:
136                paths = ['/etc/parpg', '/home/user_name/.config/parpg']
137                settings = Settings(*paths)
138               
139                paths = {'system': '/etc/parpg',
140                         'user': '/home/user_name/.config/parpg'}
141                settings = Settings(**paths)
142
143                settings = Settings('.')
144
145                settigns = Settings()
146
147            @param system_path: Path to the system settings file.
148            @type system_path: string (must be a valid path)
149
150            @param user_path: Path to the user settings file. Options that
151                              are missing from this file are propogated
152                              from the system settings file and saved on
153                              request
154            @type user_path: string (must be a valid path)
155           
156            @param suffix: Suffix of the settings file that will be generated.
157            @type suffix: string
158        """
159        if not suffix.startswith('.'):
160            suffix = '.' + suffix
161
162        self.suffix = suffix
163        self.settings_file = ''
164
165        if system_path is None and user_path is None:
166            # use platform-specific values as paths
167            self.paths = {}
168            self.paths['system'], self.paths['user'] = self.platform_paths()
169        else:
170            absolute_system_path = os.path.abspath(
171                os.path.expanduser(system_path)
172            )
173            absolute_user_path = os.path.abspath(
174                os.path.expanduser(user_path)
175            )
176            self.paths = {'system': absolute_system_path,
177                          'user': absolute_user_path}
178
179        self.read()
180
181
182    def __getattr__(self, name):
183        """ Returns a Section object to be used for assignment, creating one
184            if it doesn't exist.
185
186            @param name: name of section to be retrieved
187            @type name: string
188        """
189        if name in ['get', 'set']:
190            raise AttributeError("{0} is deprecated. Please consult Settings' "
191                                  "documentation for information on how to "
192                                  "create/modify sections and their respective "
193                                  "options".format(name))
194        else:
195            if not self.__dict__.has_key(name):
196                setattr(self, name, Section(name))
197
198        return getattr(self, name)
199
200    def platform_paths(self, system=None):
201        if system is None:
202            system = platform.system()
203       
204        if system.lower() == 'linux':
205            return (os.path.join(os.environ['XDG_DATA_DIRS'].split(':')[0], 
206                                 'parpg'),
207                    os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'))
208        elif system.lower() == 'windows':
209            return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'),
210                    os.path.join(os.environ['USERDATA'], 'PARPG'))
211        else:
212            return None
213
214    def read(self, filenames=None):
215        """ Reads a settings file and populates the settings object
216            with its sections and options. Calling this method without
217            any arguments simply re-reads the previously defined filename
218            and paths
219
220            @param filenames: name of files to be parsed.
221            @type path: string or list
222        """
223       
224        if filenames is None:
225            filenames = [os.path.join(self.paths[key], 
226                                      '{0}{1}'.format(key, self.suffix))
227                         for key in self.paths.keys() 
228                         if self.paths[key] is not None]
229        elif hasattr(filenames, 'split'):
230            filenames = [filenames]
231
232        for filename in filenames:
233            section = None
234            with open(filename, 'r') as settings_file:
235                self.settings_file = settings_file.readlines()
236            for line in self.settings_file:
237                if line.startswith('#') or line.strip() == '':
238                    continue
239                elif line.startswith('[') and line.endswith(']\n'):
240                    getattr(self, line[1:-2])
241                    section = line[1:-2]
242                else:
243                    option, value = [item.strip() 
244                                     for item in line.split('=', 1)]
245                    setattr(getattr(self, section), option, value)
246
247    def write(self, filename=None):
248        """ Writes a settings file based on the settings object's
249            sections and options
250
251            @param filename: Name of file to save to. By default, this is
252                             the user settings file.
253            @type path: string
254        """
255        if filename is None:
256            filename = os.path.join(self.paths['user'], 
257                                    'user{0}'.format(self.suffix))
258
259        for section in self.sections:
260            if '[{0}]\n'.format(section) not in self.settings_file:
261                self.settings_file.append('\n[{0}]\n'.format(section))
262                for option, value in getattr(self, section).options.iteritems():
263                    template = '{0} = {1}\n'.format(option, value)
264                    self.settings_file.append(template)
265            else:
266                start_of_section = (self.settings_file
267                                        .index('[{0}]\n'.format(section)) + 1)
268
269                for option, value in getattr(self, 
270                                             section).options.iteritems():
271                    if hasattr(value, 'sort'):
272                        value = '[{0}]'.format(', '.join(value))
273
274                    new_option = False
275                    template = '{0} = {1}\n'.format(option, value)
276                    for index, line in enumerate(self.settings_file[:]):
277                        if option in line:
278                            new_option = False
279                            if str(value) not in line:
280                                self.settings_file[index] = template
281
282                            break
283                        else:
284                            new_option = True
285                    if new_option:
286                        while self.settings_file[start_of_section].startswith('#'):
287                            start_of_section += 1
288
289                        self.settings_file.insert(start_of_section, template)
290
291        with open(filename, 'w') as out_stream:
292            for line in self.settings_file:
293                out_stream.write(line)
294
295    @property
296    def sections(self):
297        """ Returns a list of existing sections"""
298        sections = self.__dict__.keys()
299        sections.pop(sections.index('settings_file'))
300        sections.pop(sections.index('paths'))
301        sections.pop(sections.index('suffix'))
302       
303        return sections
304
305    @property
306    def system_path(self):
307        return self.paths['system']
308
309    @property
310    def user_path(self):
311        return self.paths['user']
312
313DEFAULT_SETTINGS = """\
314[fife]
315# Options marked with ? are untested/unknown
316
317# Game window's title (string) DO NOT EDIT!
318WindowTitle = PARPG Techdemo 2
319
320# Icon to use for the game window's border (filename) DO NOT EDIT!
321WindowIcon = window_icon.png
322
323# Video driver to use. (?)
324VideoDriver = ""
325
326# Backend to use for graphics (OpenGL|SDL)
327RenderBackend = OpenGL
328
329# Run the game in fullscreen mode or not. (True|False)
330FullScreen = False
331
332# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
333ScreenWidth = 1024
334
335# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
336ScreenHeight = 768
337
338# Screen DPI? (?)
339BitsPerPixel = 0
340
341# ? (?)
342SDLRemoveFakeAlpha = 1
343
344# Subdirectory to load icons from (path)
345IconsPath = icons
346
347# ? ([R, G, B])
348ColorKey = [250, 0, 250]
349
350# ? (True|False)
351ColorKeyEnabled = False
352
353# Turn on sound effects and music (True|False)
354EnableSound = True
355
356# Initial volume of sound effects and music (0.0-100.0?)
357InitialVolume = 5.0
358
359# Characters to use to render fonts. DO NOT EDIT!
360FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\""
361
362# Subdirectory to load fronts from (path)
363FontsPath = fonts
364
365# Font to load when game starts
366Font = oldtypewriter.ttf
367
368# Size of in-game fonts
369DefaultFontSize = 12
370
371# ? (?)
372LogModules = [controller]
373
374# ? (?)
375PychanDebug = False
376
377# use Psyco Acceperation (True|False)
378UsePsyco = False
379
380# ? (?)
381ProfilingOn = False
382
383# Lighting Model to use (0-2)
384Lighting = 0
385
386[parpg]
387
388# System subdirectory to load maps from (path)
389MapsPath = maps
390
391# YAML file that contains the available maps (filename)
392MapsFile = maps.yaml
393
394# Map to load when game starts (filename)
395Map = Mall
396
397# ? (filename)
398AllAgentsFile = all_agents.yaml
399
400# System subdirectory to load objects from (path)
401ObjectsPath = objects
402
403# YAML file that contains the database of availabel objects (filename)
404ObjectDatabaseFile = object_database.yaml
405
406# System subdirectory to load dialogues from (path)
407DialoguesPath = dialogue
408
409# System subdirectory to load quests from (path)
410QuestsPath = quests
411
412# User subdirectory to save screenshots to
413ScreenshotsPath = screenshots
414
415# User subdirectory to save games to
416SavesPath = saves
417
418# System subdirectory where gui files are loaded from (path)
419GuiPath = gui
420
421# System subdirectory where cursors are loaded from (path)
422CursorPath = cursors
423
424# File to use for default cursor (filename)
425CursorDefault = cursor_plain.png
426
427# File to use for up cursor (filename)
428CursorUp = cursor_up.png
429
430# File to use for right cursor (filename)
431CursorRight = cursor_right.png
432
433# File to use for down cursor (filename)
434CursorDown = cursor_down.png
435
436# File to use for left cursor (filename)
437CursorLeft = cursor_left.png
438
439# Player walk speed (digit)
440PCSpeed = 3\
441"""
442
443if __name__ == '__main__':
444    from optparse import OptionParser
445
446    usage = "usage: %prog [options] system[, system, ...]"
447    parser = OptionParser(usage=usage)
448
449    parser.add_option('-f', '--filename', default='system.cfg',
450                      help='Filename of output configuration file')
451
452    opts, args = parser.parse_args()
453   
454    with open(opts.filename, 'w') as f:
455        for line in DEFAULT_SETTINGS:
456            f.write(line)
Note: See TracBrowser for help on using the repository browser.