source: trunk/game/parpg/settings.py @ 833

Revision 833, 16.0 KB checked in by aspidites, 8 years ago (diff)

Patch by Aspidites: Game now loads if launched from a different directory. Just need to remove relative paths from xml files

  • 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
46#TODO: add logging to replace print statements
47class Section(object):
48    """ An object that represents a section in a settings file.
49
50        Options can be added to a section by simply assigning a value to an
51        attribute:
52            section.foo = baz
53        would produce:
54            [section]
55            foo = baz
56        in the settings file. Options that do not exist on assignment
57        are created dynamcially.
58
59        Values are automatically converted to the appropriate python type.
60        Options that begin and end with brackets([, ]) are converted to lists,
61        and options that are double-quoted (") are converted to strings.
62        Section also recognizes booleans regardless of case, in addition to the
63        literals 'yes' and 'no' of any case. Except in the case of
64        double-quoted strings, extra white-space is trimmed, so you need not
65        worry. For example:
66            foo = bar
67        is equivalent to :
68            foo    =         baz
69    """
70    def __init__(self, name):
71        """ Initialize a new section.
72
73            @param name: name of the section. In the INI file, sections are surrounded
74                         by brackets ([name])
75            @type name: string
76        """
77        self.name = name
78
79    def __setattr__(self, option, value):
80        """ Assign a value to an option, converting types when appropriate.
81
82            @param option: name of the option to assign a value to.
83            @type option: string @param value: value to be assigned to the option.
84            @type value: int, float, string, boolean, or list
85        """
86        value = str(value)
87        if value.startswith('[') and value.endswith(']'):
88            value = [item.strip() for item in value[1:-1].split(',')]
89        elif value.lower() == 'true' or value.lower() == 'yes':
90            value = True
91        elif value.lower() == 'false' or value.lower() == 'no':
92            value = False
93        elif value.isdigit():
94            value = int(value)
95        else:
96            try:
97                value = float(value)
98            except ValueError:
99                # leave as string
100                pass
101
102        self.__dict__[option] = value
103
104    def __getattribute__(self, option):
105        """ Returns the option's value"""
106        # Remove leading and trailing quotes from strings that have them
107        return_value = object.__getattribute__(self, option)
108        try:
109            for key, value in return_value.iteritems():
110                if (hasattr(value, 'split') and 
111                    value.startswith("\"") and value.endswith("\"")):
112                    return_value[key] = value[1:-1]
113        except AttributeError:
114            pass
115
116        return return_value
117
118    @property
119    def options(self):
120        """ Returns a dictionary of existing options """
121        options = self.__dict__
122        # get rid of properties that aren't actually options
123        if options.has_key('name'):
124            options.pop('name')
125
126        return options
127
128class Settings(object):
129    """ An object that represents a settings file, its sectons,
130        and the options defined within those sections.
131    """
132    def __init__(self, settings_path='', system_path='', user_path='', suffix='.cfg'):
133        """ initializes a new settings object. If no paths are given, they are
134            guessed based on whatever platform the script was run on.
135
136            Examples:
137                paths = ['/etc/parpg', '/home/user_name/.config/parpg']
138                settings = Settings(*paths)
139               
140                paths = {'system': '/etc/parpg',
141                         'user': '/home/user_name/.config/parpg'}
142                settings = Settings(**paths)
143
144                settings = Settings('.')
145
146                settigns = Settings()
147
148            @param system_path: Path to the system settings file.
149            @type system_path: string (must be a valid path)
150
151            @param user_path: Path to the user settings file. Options that
152                              are missing from this file are propogated
153                              from the system settings file and saved on
154                              request
155            @type user_path: string (must be a valid path)
156           
157            @param suffix: Suffix of the settings file that will be generated.
158            @type suffix: string
159        """
160        if not suffix.startswith('.'):
161            suffix = '.' + suffix
162
163        self.suffix = suffix
164        self.settings_file = ''
165
166
167        self.paths = {}
168        if not system_path and not user_path and not settings_path:
169            # use platform-specific values as paths
170            (self.paths['system'], self.paths['user'], 
171             self.paths['settings']) = self.platform_paths()
172        else:
173            # convert supplied paths to absolute paths
174            abs_paths = [os.path.expanduser(path)
175                         for path in [system_path, user_path, settings_path]]
176            (self.paths['system'], self.paths['user'],
177             self.paths['settings']) = abs_paths
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().lower()
203       
204        if system == 'linux':
205            return (os.path.join(os.sep, 'usr', 'share', 'parpg'),
206                    os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'),
207                    os.path.join(os.sep, 'etc', 'parpg'))
208        elif system == 'windows':
209            return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'),
210                    os.path.join(os.environ['USERDATA'], 'PARPG'),
211                    os.path.join(os.environ['PROGRAMFILES'], 'PARPG'))
212        else:
213            # TODO: determine values for Mac
214            return None
215
216    def read(self, filenames=None):
217        """ Reads a settings file and populates the settings object
218            with its sections and options. Calling this method without
219            any arguments simply re-reads the previously defined filename
220            and paths
221
222            @param filenames: name of files to be parsed.
223            @type path: string or list
224        """
225       
226        if filenames is None:
227            filenames = [os.path.join(self.paths['settings'], 
228                                      'system{0}'.format(self.suffix)),
229                         os.path.join(self.paths['user'],
230                                      'user{0}'.format(self.suffix))]
231        elif hasattr(filenames, 'split'):
232            filenames = [filenames]
233
234        for filename in filenames:
235            section = None
236
237            try:
238                self.settings_file = open(filename, 'r').readlines()
239            except IOError as (errno, strerror):
240                if errno == 2:
241                    if os.path.basename(filename).startswith('system'):
242                        print ('{0} could not be found. Please supply a '
243                               'different path or generate a system settings '
244                               'file with:\n'
245                               'python2 -m parpg.settings').format(filename)
246                        sys.exit(1)
247                else:
248                    print 'Error No. {0}: {1} {2}'.format(errno, filename, strerror)
249                    sys.exit(1)
250
251            for line in self.settings_file:
252                if line.startswith('#') or line.strip() == '':
253                    continue
254                elif line.startswith('[') and line.endswith(']\n'):
255                    getattr(self, line[1:-2])
256                    section = line[1:-2]
257                else:
258                    option, value = [item.strip() 
259                                     for item in line.split('=', 1)]
260                    setattr(getattr(self, section), option, value)
261
262    def write(self, filename=None):
263        """ Writes a settings file based on the settings object's
264            sections and options
265
266            @param filename: Name of file to save to. By default, this is
267                             the user settings file.
268            @type path: string
269        """
270        if filename is None:
271            filename = os.path.join(self.paths['user'], 
272                                    'user{0}'.format(self.suffix))
273
274        for section in self.sections:
275            if '[{0}]\n'.format(section) not in self.settings_file:
276                self.settings_file.append('\n[{0}]\n'.format(section))
277                for option, value in getattr(self, section).options.iteritems():
278                    template = '{0} = {1}\n'.format(option, value)
279                    self.settings_file.append(template)
280            else:
281                start_of_section = (self.settings_file
282                                        .index('[{0}]\n'.format(section)) + 1)
283
284                for option, value in getattr(self, 
285                                             section).options.iteritems():
286                    if hasattr(value, 'sort'):
287                        value = '[{0}]'.format(', '.join(value))
288
289                    new_option = False
290                    template = '{0} = {1}\n'.format(option, value)
291                    for index, line in enumerate(self.settings_file[:]):
292                        if option in line:
293                            new_option = False
294                            if str(value) not in line:
295                                self.settings_file[index] = template
296
297                            break
298                        else:
299                            new_option = True
300                    if new_option:
301                        while self.settings_file[start_of_section].startswith('#'):
302                            start_of_section += 1
303
304                        self.settings_file.insert(start_of_section, template)
305
306        with open(filename, 'w') as out_stream:
307            for line in self.settings_file:
308                out_stream.write(line)
309
310    @property
311    def sections(self):
312        """ Returns a list of existing sections"""
313        sections = self.__dict__.keys()
314        sections.pop(sections.index('settings_file'))
315        sections.pop(sections.index('paths'))
316        sections.pop(sections.index('suffix'))
317       
318        return sections
319
320    @property
321    def system_path(self):
322        return self.paths['system']
323
324    @property
325    def user_path(self):
326        return self.paths['user']
327
328    @property
329    def settings_path(self):
330        return self.paths['settings']
331
332DEFAULT_SETTINGS = """\
333[fife]
334#------------------------------------------------------------------------------
335# Options marked with ? are untested/unknown
336
337# Game window's title (string) DO NOT EDIT!
338WindowTitle = PARPG Techdemo 2
339
340# Icon to use for the game window's border (filename) DO NOT EDIT!
341WindowIcon = window_icon.png
342
343# Video driver to use. (?)
344VideoDriver = ""
345
346# Backend to use for graphics (OpenGL|SDL)
347RenderBackend = OpenGL
348
349# Run the game in fullscreen mode or not. (True|False)
350FullScreen = False
351
352# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
353ScreenWidth = 1024
354
355# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
356ScreenHeight = 768
357
358# Screen DPI? (?)
359BitsPerPixel = 0
360
361# ? (?)
362SDLRemoveFakeAlpha = 1
363
364# Subdirectory to load icons from (path)
365IconsPath = icons
366
367# ? ([R, G, B])
368ColorKey = [250, 0, 250]
369
370# ? (True|False)
371ColorKeyEnabled = False
372
373# Turn on sound effects and music (True|False)
374EnableSound = True
375
376# Initial volume of sound effects and music (0.0-100.0?)
377InitialVolume = 5.0
378
379# Characters to use to render fonts. DO NOT EDIT!
380FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\""
381
382# Subdirectory to load fronts from (path)
383FontsPath = fonts
384
385# Font to load when game starts
386Font = oldtypewriter.ttf
387
388# Size of in-game fonts
389DefaultFontSize = 12
390
391# ? (?)
392LogModules = [controller]
393
394# ? (?)
395PychanDebug = False
396
397# use Psyco Acceperation (True|False)
398UsePsyco = False
399
400# ? (?)
401ProfilingOn = False
402
403# Lighting Model to use (0-2)
404Lighting = 0
405
406[parpg]
407#------------------------------------------------------------------------------
408
409# System subdirectory to load maps from (path)
410MapsPath = maps
411
412# YAML file that contains the available maps (filename)
413MapsFile = maps.yaml
414
415# Map to load when game starts (filename)
416Map = Mall
417
418# ? (filename)
419AllAgentsFile = all_agents.yaml
420
421# System subdirectory to load objects from (path)
422ObjectsPath = objects
423
424# YAML file that contains the database of availabel objects (filename)
425ObjectDatabaseFile = object_database.yaml
426
427# System subdirectory to load dialogues from (path)
428DialoguesPath = dialogue
429
430# System subdirectory to load quests from (path)
431QuestsPath = quests
432
433# User subdirectory to save screenshots to
434ScreenshotsPath = screenshots
435
436# User subdirectory to save games to
437SavesPath = saves
438
439# System subdirectory where gui files are loaded from (path)
440GuiPath = gui
441
442# System subdirectory where cursors are loaded from (path)
443CursorPath = cursors
444
445# File to use for default cursor (filename)
446CursorDefault = cursor_plain.png
447
448# File to use for up cursor (filename)
449CursorUp = cursor_up.png
450
451# File to use for right cursor (filename)
452CursorRight = cursor_right.png
453
454# File to use for down cursor (filename)
455CursorDown = cursor_down.png
456
457# File to use for left cursor (filename)
458CursorLeft = cursor_left.png
459
460# Player walk speed (digit)
461PCSpeed = 3\
462"""
463
464if __name__ == '__main__':
465    from optparse import OptionParser
466
467    usage = "usage: %prog [options] system[, system, ...]"
468    parser = OptionParser(usage=usage)
469
470    parser.add_option('-f', '--filename', default='system.cfg',
471                      help='Filename of output configuration file')
472
473    opts, args = parser.parse_args()
474   
475    with open(opts.filename, 'w') as f:
476        for line in DEFAULT_SETTINGS:
477            f.write(line)
Note: See TracBrowser for help on using the repository browser.