view settings.py @ 1:4912a6f97c52

Various improvements to the build process including support for self-contained builds. * Note that despite all of these changes PARPG still does not run because asset paths are not standardized, * Modified the SCons script so that by default running `scons` with no arguments creates a self-contained "build" under a build subdirectory to make in-source testing easier. To install PARPG, use `scons install` instead. * Got rid of the binary launcher and replaced it with a shell script for unix and a batch script for Windows (batch script is untested). The binary turned out to be too much trouble to maintain. * Modified the parpg.settings module and parpg.main entry script so that PARPG searches through several default search paths for configuration file(s). PARPG thus no longer crashes if it can't find a configuration file in any particular search path, but will crash it if can't find any configuration files. * Paths supplied to parpg.main are now appended as search paths for the configuration file(s). * Changed the default configuration file name to "parpg.cfg" to simplify searches. * Created the site_scons directory tree where SCons extensions and tools should be placed. * Created a new SCons builder, CopyRecurse, which can copy only certain files and folders from a directory tree using filters (files and folders that start with a leading dot "." e.g. ".svn" are ignored by default). * Added the CPython SCons tool (stands for Compile-Python - I didn't name it!), which provides the InstallPython builder for pre-compiling python sources before they are installed. However, it is currently broken and only installs the python sources.
author M. George Hansen <technopolitica@gmail.com>
date Tue, 31 May 2011 02:46:20 -0700
parents 7a89ea5404b1
children bf1dd9c24a7e
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='',
                 filename='parpg.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
        """
        self.filename = filename
        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'], self.filename),
                         os.path.join(self.paths['user'], self.filename)]
        elif hasattr(filenames, 'split'):
            filenames = [filenames]

        for filename in filenames:
            section = None
            if os.path.exists(filename):
                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)