Mercurial > parpg-source
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)