view src/parpg/settings.py @ 2:e2a8e3805b04

Made parpg-core the main repository to pull from and build. * Added parpg-assets as a subrepo under the data folder. * Moved the SConstruct script into parpg-core and modified it to build and install parpg-assets as well. * Made a few minor changes to the SConstruct file from the defunct parpg-main repository. * Modified unix parpg executable script template to be more forgiving of installing the parpg Python package outside of PYTHONPATH. * Updated the .hgignore file to include temporary SCons files.
author M. George Hansen <technopolitica@gmail.com>
date Sun, 15 May 2011 14:51:41 -0700
parents 1fd2201f5c36
children 4706e0194af3
line wrap: on
line source

#!/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)