diff settings.py @ 0:7a89ea5404b1

Initial commit of parpg-core.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 14 May 2011 01:12:35 -0700
parents
children 4912a6f97c52
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/settings.py	Sat May 14 01:12:35 2011 -0700
@@ -0,0 +1,477 @@
+#!/usr/bin/env python2
+
+#  Copyright (C) 2011  Edwin Marshall <emarshall85@gmail.com>
+
+#   This file is part of PARPG.
+#
+#   PARPG is free software: you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation, either version 3 of the License, or
+#   (at your option) any later version.
+#
+#   PARPG is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
+
+""" Provides a class used for reading and writing various configurable options
+    throughout the game
+
+    This class produces an INI formated settings file as opposed to an XML
+    formatted one. The reason that python's built-in ConfigurationParser isn't 
+    sufficient is because comments aren't preserved when writing a settings
+    file, the order in which the options are written isn't preserved, and the 
+    interface used with this class is arguably more convenient that
+    ConfigParser's.
+
+    Default Settings may be generated by envoking this module from the
+    command line:
+        python -m settings.py [system] [data_directory]
+
+    where [system] is one of local, windows, or linux (mac coming soon),
+    and data_directory is the base path for the data files to be loaded.
+
+    Both [system] and [data_directory] are option. If omitted, both
+    default to whichever what is reasonable based on the system settings.py
+    is run on
+"""
+
+import os
+import sys
+import platform
+
+#TODO: add logging to replace print statements
+class Section(object):
+    """ An object that represents a section in a settings file.
+
+        Options can be added to a section by simply assigning a value to an 
+        attribute:
+            section.foo = baz
+        would produce:
+            [section]
+            foo = baz
+        in the settings file. Options that do not exist on assignment
+        are created dynamcially.
+
+        Values are automatically converted to the appropriate python type. 
+        Options that begin and end with brackets([, ]) are converted to lists,
+        and options that are double-quoted (") are converted to strings. 
+        Section also recognizes booleans regardless of case, in addition to the
+        literals 'yes' and 'no' of any case. Except in the case of 
+        double-quoted strings, extra white-space is trimmed, so you need not 
+        worry. For example:
+            foo = bar
+        is equivalent to :
+            foo    =         baz
+    """
+    def __init__(self, name):
+        """ Initialize a new section.
+
+            @param name: name of the section. In the INI file, sections are surrounded
+                         by brackets ([name])
+            @type name: string
+        """
+        self.name = name
+
+    def __setattr__(self, option, value):
+        """ Assign a value to an option, converting types when appropriate.
+
+            @param option: name of the option to assign a value to.
+            @type option: string @param value: value to be assigned to the option.
+            @type value: int, float, string, boolean, or list
+        """
+        value = str(value)
+        if value.startswith('[') and value.endswith(']'):
+            value = [item.strip() for item in value[1:-1].split(',')]
+        elif value.lower() == 'true' or value.lower() == 'yes':
+            value = True
+        elif value.lower() == 'false' or value.lower() == 'no':
+            value = False
+        elif value.isdigit():
+            value = int(value)
+        else:
+            try:
+                value = float(value)
+            except ValueError:
+                # leave as string
+                pass
+
+        self.__dict__[option] = value
+
+    def __getattribute__(self, option):
+        """ Returns the option's value"""
+        # Remove leading and trailing quotes from strings that have them
+        return_value = object.__getattribute__(self, option)
+        try:
+            for key, value in return_value.iteritems():
+                if (hasattr(value, 'split') and 
+                    value.startswith("\"") and value.endswith("\"")):
+                    return_value[key] = value[1:-1]
+        except AttributeError:
+            pass
+
+        return return_value
+
+    @property
+    def options(self):
+        """ Returns a dictionary of existing options """
+        options = self.__dict__
+        # get rid of properties that aren't actually options
+        if options.has_key('name'):
+            options.pop('name')
+
+        return options
+
+class Settings(object):
+    """ An object that represents a settings file, its sectons,
+        and the options defined within those sections.
+    """
+    def __init__(self, settings_path='', system_path='', user_path='', suffix='.cfg'):
+        """ initializes a new settings object. If no paths are given, they are
+            guessed based on whatever platform the script was run on.
+
+            Examples:
+                paths = ['/etc/parpg', '/home/user_name/.config/parpg']
+                settings = Settings(*paths)
+                
+                paths = {'system': '/etc/parpg', 
+                         'user': '/home/user_name/.config/parpg'}
+                settings = Settings(**paths)
+
+                settings = Settings('.')
+
+                settigns = Settings()
+
+            @param system_path: Path to the system settings file.
+            @type system_path: string (must be a valid path)
+
+            @param user_path: Path to the user settings file. Options that
+                              are missing from this file are propogated 
+                              from the system settings file and saved on
+                              request
+            @type user_path: string (must be a valid path)
+            
+            @param suffix: Suffix of the settings file that will be generated.
+            @type suffix: string
+        """
+        if not suffix.startswith('.'):
+            suffix = '.' + suffix
+
+        self.suffix = suffix
+        self.settings_file = ''
+
+
+        self.paths = {}
+        if not system_path and not user_path and not settings_path:
+            # use platform-specific values as paths
+            (self.paths['system'], self.paths['user'], 
+             self.paths['settings']) = self.platform_paths()
+        else:
+            # convert supplied paths to absolute paths
+            abs_paths = [os.path.expanduser(path)
+                         for path in [system_path, user_path, settings_path]]
+            (self.paths['system'], self.paths['user'],
+             self.paths['settings']) = abs_paths
+
+        self.read()
+
+
+    def __getattr__(self, name):
+        """ Returns a Section object to be used for assignment, creating one
+            if it doesn't exist.
+
+            @param name: name of section to be retrieved
+            @type name: string
+        """
+        if name in ['get', 'set']:
+            raise AttributeError("{0} is deprecated. Please consult Settings' "
+                                  "documentation for information on how to "
+                                  "create/modify sections and their respective "
+                                  "options".format(name))
+        else:
+            if not self.__dict__.has_key(name):
+                setattr(self, name, Section(name))
+
+        return getattr(self, name)
+
+    def platform_paths(self, system=None):
+        if system is None:
+            system = platform.system().lower()
+        
+        if system == 'linux':
+            return (os.path.join(os.sep, 'usr', 'share', 'parpg'),
+                    os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'),
+                    os.path.join(os.sep, 'etc', 'parpg'))
+        elif system == 'windows':
+            return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'),
+                    os.path.join(os.environ['USERDATA'], 'PARPG'),
+                    os.path.join(os.environ['PROGRAMFILES'], 'PARPG'))
+        else:
+            # TODO: determine values for Mac
+            return None
+
+    def read(self, filenames=None):
+        """ Reads a settings file and populates the settings object 
+            with its sections and options. Calling this method without
+            any arguments simply re-reads the previously defined filename
+            and paths
+
+            @param filenames: name of files to be parsed. 
+            @type path: string or list
+        """
+        
+        if filenames is None:
+            filenames = [os.path.join(self.paths['settings'], 
+                                      'system{0}'.format(self.suffix)),
+                         os.path.join(self.paths['user'],
+                                      'user{0}'.format(self.suffix))]
+        elif hasattr(filenames, 'split'):
+            filenames = [filenames]
+
+        for filename in filenames:
+            section = None
+
+            try:
+                self.settings_file = open(filename, 'r').readlines()
+            except IOError as (errno, strerror):
+                if errno == 2:
+                    if os.path.basename(filename).startswith('system'):
+                        print ('{0} could not be found. Please supply a '
+                               'different path or generate a system settings '
+                               'file with:\n'
+                               'python2 -m parpg.settings').format(filename)
+                        sys.exit(1)
+                else:
+                    print 'Error No. {0}: {1} {2}'.format(errno, filename, strerror)
+                    sys.exit(1)
+
+            for line in self.settings_file:
+                if line.startswith('#') or line.strip() == '':
+                    continue
+                elif line.startswith('[') and line.endswith(']\n'):
+                    getattr(self, line[1:-2])
+                    section = line[1:-2]
+                else:
+                    option, value = [item.strip() 
+                                     for item in line.split('=', 1)]
+                    setattr(getattr(self, section), option, value)
+
+    def write(self, filename=None):
+        """ Writes a settings file based on the settings object's 
+            sections and options
+
+            @param filename: Name of file to save to. By default, this is
+                             the user settings file.
+            @type path: string
+        """
+        if filename is None:
+            filename = os.path.join(self.paths['user'], 
+                                    'user{0}'.format(self.suffix))
+
+        for section in self.sections:
+            if '[{0}]\n'.format(section) not in self.settings_file:
+                self.settings_file.append('\n[{0}]\n'.format(section))
+                for option, value in getattr(self, section).options.iteritems():
+                    template = '{0} = {1}\n'.format(option, value)
+                    self.settings_file.append(template)
+            else:
+                start_of_section = (self.settings_file
+                                        .index('[{0}]\n'.format(section)) + 1)
+
+                for option, value in getattr(self, 
+                                             section).options.iteritems():
+                    if hasattr(value, 'sort'):
+                        value = '[{0}]'.format(', '.join(value))
+
+                    new_option = False
+                    template = '{0} = {1}\n'.format(option, value)
+                    for index, line in enumerate(self.settings_file[:]):
+                        if option in line:
+                            new_option = False
+                            if str(value) not in line:
+                                self.settings_file[index] = template
+
+                            break
+                        else:
+                            new_option = True
+                    if new_option:
+                        while self.settings_file[start_of_section].startswith('#'):
+                            start_of_section += 1
+
+                        self.settings_file.insert(start_of_section, template)
+
+        with open(filename, 'w') as out_stream:
+            for line in self.settings_file:
+                out_stream.write(line)
+
+    @property
+    def sections(self):
+        """ Returns a list of existing sections"""
+        sections = self.__dict__.keys()
+        sections.pop(sections.index('settings_file'))
+        sections.pop(sections.index('paths'))
+        sections.pop(sections.index('suffix'))
+        
+        return sections
+
+    @property
+    def system_path(self):
+        return self.paths['system']
+
+    @property
+    def user_path(self):
+        return self.paths['user']
+
+    @property
+    def settings_path(self):
+        return self.paths['settings']
+
+DEFAULT_SETTINGS = """\
+[fife]
+#------------------------------------------------------------------------------
+# Options marked with ? are untested/unknown
+
+# Game window's title (string) DO NOT EDIT!
+WindowTitle = PARPG Techdemo 2
+
+# Icon to use for the game window's border (filename) DO NOT EDIT!
+WindowIcon = window_icon.png
+
+# Video driver to use. (?)
+VideoDriver = ""
+
+# Backend to use for graphics (OpenGL|SDL)
+RenderBackend = OpenGL 
+
+# Run the game in fullscreen mode or not. (True|False)
+FullScreen = False
+
+# Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
+ScreenWidth = 1024
+
+# Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
+ScreenHeight = 768
+
+# Screen DPI? (?)
+BitsPerPixel = 0
+
+# ? (?)
+SDLRemoveFakeAlpha = 1
+
+# Subdirectory to load icons from (path)
+IconsPath = icons
+
+# ? ([R, G, B])
+ColorKey = [250, 0, 250]
+
+# ? (True|False)
+ColorKeyEnabled = False
+
+# Turn on sound effects and music (True|False)
+EnableSound = True
+
+# Initial volume of sound effects and music (0.0-100.0?)
+InitialVolume = 5.0
+
+# Characters to use to render fonts. DO NOT EDIT!
+FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\""
+
+# Subdirectory to load fronts from (path)
+FontsPath = fonts
+
+# Font to load when game starts
+Font = oldtypewriter.ttf
+
+# Size of in-game fonts
+DefaultFontSize = 12
+
+# ? (?)
+LogModules = [controller]
+
+# ? (?)
+PychanDebug = False
+
+# use Psyco Acceperation (True|False)
+UsePsyco = False
+
+# ? (?)
+ProfilingOn = False
+
+# Lighting Model to use (0-2)
+Lighting = 0
+
+[parpg]
+#------------------------------------------------------------------------------
+
+# System subdirectory to load maps from (path)
+MapsPath = maps
+
+# YAML file that contains the available maps (filename)
+MapsFile = maps.yaml
+
+# Map to load when game starts (filename)
+Map = Mall
+
+# ? (filename)
+AllAgentsFile = all_agents.yaml
+
+# System subdirectory to load objects from (path)
+ObjectsPath = objects
+
+# YAML file that contains the database of availabel objects (filename)
+ObjectDatabaseFile = object_database.yaml
+
+# System subdirectory to load dialogues from (path)
+DialoguesPath = dialogue
+
+# System subdirectory to load quests from (path)
+QuestsPath = quests
+
+# User subdirectory to save screenshots to
+ScreenshotsPath = screenshots
+
+# User subdirectory to save games to
+SavesPath = saves
+
+# System subdirectory where gui files are loaded from (path)
+GuiPath = gui
+
+# System subdirectory where cursors are loaded from (path)
+CursorPath = cursors
+
+# File to use for default cursor (filename)
+CursorDefault = cursor_plain.png
+
+# File to use for up cursor (filename)
+CursorUp = cursor_up.png
+
+# File to use for right cursor (filename)
+CursorRight = cursor_right.png
+
+# File to use for down cursor (filename)
+CursorDown = cursor_down.png
+
+# File to use for left cursor (filename)
+CursorLeft = cursor_left.png
+
+# Player walk speed (digit)
+PCSpeed = 3\
+"""
+
+if __name__ == '__main__':
+    from optparse import OptionParser
+
+    usage = "usage: %prog [options] system[, system, ...]"
+    parser = OptionParser(usage=usage)
+
+    parser.add_option('-f', '--filename', default='system.cfg',
+                      help='Filename of output configuration file')
+
+    opts, args = parser.parse_args()
+    
+    with open(opts.filename, 'w') as f:
+        for line in DEFAULT_SETTINGS:
+            f.write(line)