comparison src/parpg/settings.py @ 0:1fd2201f5c36

Initial commit of parpg-core.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 14 May 2011 01:12:35 -0700
parents
children 4706e0194af3
comparison
equal deleted inserted replaced
-1:000000000000 0:1fd2201f5c36
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
42 import os
43 import sys
44 import platform
45
46 #TODO: add logging to replace print statements
47 class 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
128 class 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
332 DEFAULT_SETTINGS = """\
333 [fife]
334 #------------------------------------------------------------------------------
335 # Options marked with ? are untested/unknown
336
337 # Game window's title (string) DO NOT EDIT!
338 WindowTitle = PARPG Techdemo 2
339
340 # Icon to use for the game window's border (filename) DO NOT EDIT!
341 WindowIcon = window_icon.png
342
343 # Video driver to use. (?)
344 VideoDriver = ""
345
346 # Backend to use for graphics (OpenGL|SDL)
347 RenderBackend = OpenGL
348
349 # Run the game in fullscreen mode or not. (True|False)
350 FullScreen = False
351
352 # Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc)
353 ScreenWidth = 1024
354
355 # Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc)
356 ScreenHeight = 768
357
358 # Screen DPI? (?)
359 BitsPerPixel = 0
360
361 # ? (?)
362 SDLRemoveFakeAlpha = 1
363
364 # Subdirectory to load icons from (path)
365 IconsPath = icons
366
367 # ? ([R, G, B])
368 ColorKey = [250, 0, 250]
369
370 # ? (True|False)
371 ColorKeyEnabled = False
372
373 # Turn on sound effects and music (True|False)
374 EnableSound = True
375
376 # Initial volume of sound effects and music (0.0-100.0?)
377 InitialVolume = 5.0
378
379 # Characters to use to render fonts. DO NOT EDIT!
380 FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\""
381
382 # Subdirectory to load fronts from (path)
383 FontsPath = fonts
384
385 # Font to load when game starts
386 Font = oldtypewriter.ttf
387
388 # Size of in-game fonts
389 DefaultFontSize = 12
390
391 # ? (?)
392 LogModules = [controller]
393
394 # ? (?)
395 PychanDebug = False
396
397 # use Psyco Acceperation (True|False)
398 UsePsyco = False
399
400 # ? (?)
401 ProfilingOn = False
402
403 # Lighting Model to use (0-2)
404 Lighting = 0
405
406 [parpg]
407 #------------------------------------------------------------------------------
408
409 # System subdirectory to load maps from (path)
410 MapsPath = maps
411
412 # YAML file that contains the available maps (filename)
413 MapsFile = maps.yaml
414
415 # Map to load when game starts (filename)
416 Map = Mall
417
418 # ? (filename)
419 AllAgentsFile = all_agents.yaml
420
421 # System subdirectory to load objects from (path)
422 ObjectsPath = objects
423
424 # YAML file that contains the database of availabel objects (filename)
425 ObjectDatabaseFile = object_database.yaml
426
427 # System subdirectory to load dialogues from (path)
428 DialoguesPath = dialogue
429
430 # System subdirectory to load quests from (path)
431 QuestsPath = quests
432
433 # User subdirectory to save screenshots to
434 ScreenshotsPath = screenshots
435
436 # User subdirectory to save games to
437 SavesPath = saves
438
439 # System subdirectory where gui files are loaded from (path)
440 GuiPath = gui
441
442 # System subdirectory where cursors are loaded from (path)
443 CursorPath = cursors
444
445 # File to use for default cursor (filename)
446 CursorDefault = cursor_plain.png
447
448 # File to use for up cursor (filename)
449 CursorUp = cursor_up.png
450
451 # File to use for right cursor (filename)
452 CursorRight = cursor_right.png
453
454 # File to use for down cursor (filename)
455 CursorDown = cursor_down.png
456
457 # File to use for left cursor (filename)
458 CursorLeft = cursor_left.png
459
460 # Player walk speed (digit)
461 PCSpeed = 3\
462 """
463
464 if __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)