comparison src/parpg/dialogueparsers.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 d60f1dab8469
comparison
equal deleted inserted replaced
-1:000000000000 0:1fd2201f5c36
1 # This file is part of PARPG.
2 #
3 # PARPG is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # PARPG is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with PARPG. If not, see <http://www.gnu.org/licenses/>.
15 """
16 Contains classes for parsing and validating L{Dialogues<Dialogue>} and other
17 dialogue-related data.
18
19 @TODO Technomage 2010-11-13: Exception handling + validation needs work.
20 Currently YAML files are only crudely validated - the code assumes that
21 the file contains valid dialogue data, and if that assumption is
22 violated and causes the code to raise any TypeErrors, AttributeErrors or
23 ValueErrors the code then raises a DialogueFormatError with the
24 original (and mostly unhelpful) error message.
25 @TODO Technomage 2010-11-13: Support reading and writing unicode.
26 """
27 try:
28 from cStringIO import StringIO
29 except ImportError:
30 from StringIO import StringIO
31 from collections import Sequence
32 try:
33 from collections import OrderedDict
34 except ImportError:
35 # Python version 2.4-2.6 doesn't have the OrderedDict
36 from parpg.common.ordereddict import OrderedDict
37 import re
38 import textwrap
39
40 import yaml
41
42 from parpg import COPYRIGHT_HEADER
43 from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse,
44 DialogueGreeting)
45 from parpg.dialogueactions import DialogueAction
46
47 import logging
48 logger = logging.getLogger('dialogueparser')
49
50 class DialogueFormatError(Exception):
51 """Exception thrown when the DialogueParser has encountered an error."""
52
53
54 class AbstractDialogueParser(object):
55 """
56 Abstract base class defining the interface for parsers responsible for
57 constructing a L{Dialogue} from its serialized representation.
58 """
59 def load(self, stream):
60 """
61 Parse a stream and attempt to construct a new L{Dialogue} instance from
62 its serialized representation.
63
64 @param stream: open stream containing the serialized representation of
65 a Dialogue.
66 @type stream: BufferType
67 """
68 raise NotImplementedError('AbstractDialogueParser subclasses must '
69 'override the load method.')
70
71 def dump(self, dialogue, stream):
72 """
73 Serialize a L{Dialogue} instance and dump it to an open stream.
74
75 @param dialogue: dialogue to serialize.
76 @type dialogue: L{Dialogue}
77 @param stream: open stream into which the serialized L{Dialogue} should
78 be dumped.
79 @type stream: BufferType
80 """
81 raise NotImplementedError('AbstractDialogueParser subclasses must '
82 'override the dump method.')
83
84 def validate(self, stream):
85 """
86 Parse a stream and verify that it contains a valid serialization of a
87 L{Dialogue instance}.
88
89 @param stream: stream containing the serialized representation of a
90 L{Dialogue}
91 @type stream: BufferType
92 """
93 raise NotImplementedError('AbstractDialogueParser subclasses must '
94 'override the validate method.')
95
96
97 class YamlDialogueParser(AbstractDialogueParser):
98 """
99 L{AbstractDialogueParser} subclass responsible for parsing dialogues
100 serialized in YAML.
101 """
102 logger = logging.getLogger('dialogueparser.OldYamlDialogueParser')
103
104 def load(self, stream, loader_class=yaml.Loader):
105 """
106 Parse a YAML stream and attempt to construct a new L{Dialogue}
107 instance.
108
109 @param stream: stream containing the serialized YAML representation of
110 a L{Dialogue}.
111 @type stream: BufferType
112 @param loader_class: PyYAML loader class to use for reading the
113 serialization.
114 @type loader_class: yaml.BaseLoader subclass
115 """
116 loader = loader_class(stream)
117 try:
118 dialogue = \
119 self._constructDialogue(loader, loader.get_single_node())
120 except (AssertionError,) as error:
121 raise DialogueFormatError(str(error))
122 return dialogue
123
124 def dump(self, dialogue, output_stream, dumper_class=yaml.Dumper):
125 """
126 Serialize a L{Dialogue} instance as YAML and dump it to an open stream.
127
128 @param dialogue: dialogue to serialize.
129 @type dialogue: L{Dialogue}
130 @param stream: open stream into which the serialized L{Dialogue} should
131 be dumped.
132 @type stream: BufferType
133 @param dumper_class: PyYAML dumper class to use for formatting the
134 serialization.
135 @type dumper_class: yaml.BaseDumper subclass
136 """
137 intermediate_stream = StringIO()
138 # KLUDE Technomage 2010-11-16: The "width" argument seems to be broken,
139 # as it doesn't take into about current line indentation and fails
140 # to correctly wrap at word boundaries.
141 dumper = dumper_class(intermediate_stream, default_flow_style=False,
142 indent=4, width=99999, line_break='\n',
143 allow_unicode=True, explicit_start=True,
144 explicit_end=True, tags=False)
145 dialogue_node = self._representDialogue(dumper, dialogue)
146 dumper.open()
147 dumper.serialize(dialogue_node)
148 dumper.close()
149 file_contents = intermediate_stream.getvalue()
150
151 file_contents = re.sub(r'(\n|\r|\r\n)(\s*)(GOTO: .*)', r'\1\2\3\1\2',
152 file_contents)
153 lines = file_contents.splitlines()
154 max_line_length = 76 # 79 - 3 chars for escaping newlines
155 for i in range(len(lines)):
156 line = lines[i]
157 match = re.match(
158 r'^(\s*(?:-\s+)?)(SAY|REPLY|CONDITION):\s+"(.*)"$',
159 line
160 )
161 if (match and len(line) > max_line_length):
162 # Wrap long lines for readability.
163 initial_indent = len(match.group(1))
164 subsequent_indent = initial_indent + 4
165 text_wrapper = textwrap.TextWrapper(
166 max_line_length,
167 subsequent_indent=' ' * subsequent_indent,
168 break_long_words=False,
169 break_on_hyphens=False
170 )
171 new_lines = text_wrapper.wrap(line)
172 new_lines = (
173 new_lines[:1] + [re.sub(r'^(\s*) (.*)$', r'\1\ \2', l)
174 for l in new_lines[1:]]
175 )
176 lines[i] = '\\\n'.join(new_lines)
177
178 output_stream.write(COPYRIGHT_HEADER)
179 output_stream.write('\n'.join(lines))
180
181
182 def _representDialogue(self, dumper, dialogue):
183 dialogue_node = dumper.represent_dict({})
184 dialogue_dict = OrderedDict()
185 dialogue_dict['NPC_NAME'] = dialogue.npc_name
186 dialogue_dict['AVATAR_PATH'] = dialogue.avatar_path
187 dialogue_dict['DEFAULT_GREETING'] = \
188 self._representDialogueSection(dumper,
189 dialogue.default_greeting)
190 # NOTE Technomage 2010-11-16: Dialogue stores its sections in an
191 # OrderedDict, so a round-trip load, dump, and load will preserve
192 # the order of DialogueSections.
193 if (len(dialogue.greetings) > 0):
194 greetings_list_node = dumper.represent_list([])
195 greetings_list = greetings_list_node.value
196 for greeting in dialogue.greetings:
197 greeting_node = \
198 self._representRootDialogueSection(dumper, greeting)
199 greetings_list.append(greeting_node)
200 dialogue_dict['GREETINGS'] = greetings_list_node
201 if (len(dialogue.setions) > 0):
202 sections_list_node = dumper.represent_list([])
203 sections_list = sections_list_node.value
204 for section in dialogue.sections.values():
205 section_node = self._representDialogueSection(dumper, section)
206 sections_list.append(section_node)
207 dialogue_dict['SECTIONS'] = sections_list_node
208
209 for key, value in dialogue_dict.items():
210 if (isinstance(key, yaml.Node)):
211 key_node = key
212 else:
213 key_node = dumper.represent_data(key)
214 if (isinstance(value, yaml.Node)):
215 value_node = value
216 else:
217 value_node = dumper.represent_data(value)
218 dialogue_node.value.append((key_node, value_node))
219 return dialogue_node
220
221 def _representRootDialogueSection(self, dumper, greeting):
222 greeting_node = dumper.represent_dict({})
223 greeting_dict = OrderedDict()
224 greeting_dict['ID'] = greeting.id
225 greeting_dict['CONDITION'] = dumper.represent_scalar(
226 'tag:yaml.org,2002:str',
227 greeting.condition,
228 style='"'
229 )
230 for key, value in greeting_dict.items():
231 if (isinstance(key, yaml.Node)):
232 key_node = key
233 else:
234 key_node = dumper.represent_data(key)
235 if (isinstance(value, yaml.Node)):
236 value_node = value
237 else:
238 value_node = dumper.represent_data(value)
239 greeting_node.value.append((key_node, value_node))
240 return greeting_node
241
242 def _representDialogueSection(self, dumper, dialogue_section):
243 section_node = dumper.represent_dict({})
244 section_dict = OrderedDict() # OrderedDict is required to preserve
245 # the order of attributes.
246 section_dict['ID'] = dialogue_section.id
247 # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be
248 # a problem when writing unicode.
249 section_dict['SAY'] = dumper.represent_scalar('tag:yaml.org,2002:str',
250 dialogue_section.text,
251 style='"')
252 actions_list_node = dumper.represent_list([])
253 actions_list = actions_list_node.value
254 for action in dialogue_section.actions:
255 action_node = self._representDialogueAction(dumper, action)
256 actions_list.append(action_node)
257 if (actions_list):
258 section_dict['ACTIONS'] = actions_list_node
259 responses_list_node = dumper.represent_list([])
260 responses_list = responses_list_node.value
261 for response in dialogue_section.responses:
262 response_node = self._representDialogueResponse(dumper, response)
263 responses_list.append(response_node)
264 section_dict['RESPONSES'] = responses_list_node
265
266 for key, value in section_dict.items():
267 if (isinstance(key, yaml.Node)):
268 key_node = key
269 else:
270 key_node = dumper.represent_data(key)
271 if (isinstance(value, yaml.Node)):
272 value_node = value
273 else:
274 value_node = dumper.represent_data(value)
275 section_node.value.append((key_node, value_node))
276 return section_node
277
278 def _representDialogueResponse(self, dumper, dialogue_response):
279 response_node = dumper.represent_dict({})
280 response_dict = OrderedDict()
281 # KLUDGE Technomage 2010-11-16: Hard-coding the tag like this could be
282 # a problem when writing unicode.
283 response_dict['REPLY'] = dumper.represent_scalar(
284 'tag:yaml.org,2002:str',
285 dialogue_response.text,
286 style='"')
287 if (dialogue_response.condition is not None):
288 response_dict['CONDITION'] = dumper.represent_scalar(
289 'tag:yaml.org,2002:str',
290 dialogue_response.condition,
291 style='"'
292 )
293 actions_list_node = dumper.represent_list([])
294 actions_list = actions_list_node.value
295 for action in dialogue_response.actions:
296 action_node = self._representDialogueAction(dumper, action)
297 actions_list.append(action_node)
298 if (actions_list):
299 response_dict['ACTIONS'] = actions_list_node
300 response_dict['GOTO'] = dialogue_response.next_section_id
301
302 for key, value in response_dict.items():
303 if (isinstance(key, yaml.Node)):
304 key_node = key
305 else:
306 key_node = dumper.represent_data(key)
307 if (isinstance(value, yaml.Node)):
308 value_node = value
309 else:
310 value_node = dumper.represent_data(value)
311 response_node.value.append((key_node, value_node))
312 return response_node
313
314 def _representDialogueAction(self, dumper, dialogue_action):
315 action_node = dumper.represent_dict({})
316 action_dict = OrderedDict()
317 args, kwargs = dialogue_action.arguments
318 if (args and not kwargs):
319 arguments = list(args)
320 elif (kwargs and not args):
321 arguments = kwargs
322 else:
323 arguments = [list(args), kwargs]
324 action_dict[dialogue_action.keyword] = arguments
325
326 for key, value in action_dict.items():
327 if (isinstance(key, yaml.Node)):
328 key_node = key
329 else:
330 key_node = dumper.represent_data(key)
331 if (isinstance(value, yaml.Node)):
332 value_node = value
333 else:
334 value_node = dumper.represent_data(value)
335 action_node.value.append((key_node, value_node))
336 return action_node
337
338 def _constructDialogue(self, loader, yaml_node):
339 npc_name = None
340 avatar_path = None
341 default_greeting = None
342 greetings = []
343 sections = []
344
345 try:
346 for key_node, value_node in yaml_node.value:
347 key = key_node.value
348 if (key == u'NPC_NAME'):
349 npc_name = loader.construct_object(value_node)
350 elif (key == u'AVATAR_PATH'):
351 avatar_path = loader.construct_object(value_node)
352 elif (key == u'DEFAULT_GREETING'):
353 default_greeting = \
354 self._constructDialogueSection(loader, value_node)
355 elif (key == u'GREETINGS'):
356 for greeting_node in value_node.value:
357 greeting = self._constructRootDialogueSection(
358 loader,
359 greeting_node
360 )
361 greetings.append(
362 greeting
363 )
364 elif (key == u'SECTIONS'):
365 for section_node in value_node.value:
366 dialogue_section = self._constructDialogueSection(
367 loader,
368 section_node
369 )
370 sections.append(dialogue_section)
371 except (AttributeError, TypeError, ValueError) as e:
372 raise DialogueFormatError(e)
373
374 dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path,
375 default_greeting=default_greeting,
376 greetings=greetings,
377 sections=sections)
378 return dialogue
379
380 def _constructRootDialogueSection(self, loader, greeting_node):
381 id = None
382 text = None
383 condition = None
384 responses = []
385 actions = []
386 greeting = None
387
388 try:
389 for key_node, value_node in greeting_node.value:
390 key = key_node.value
391 if (key == u'ID'):
392 id = loader.construct_object(value_node)
393 elif (key == u'SAY'):
394 text = loader.construct_object(value_node)
395 elif (key == u'CONDITION'):
396 condition = loader.construct_object(value_node)
397 elif (key == u'RESPONSES'):
398 for response_node in value_node.value:
399 dialogue_response = self._constructDialogueResponse(
400 loader,
401 response_node
402 )
403 responses.append(dialogue_response)
404 elif (key == u'ACTIONS'):
405 for action_node in value_node.value:
406 action = self._constructDialogueAction(loader,
407 action_node)
408 actions.append(action)
409 except (AttributeError, TypeError, ValueError) as e:
410 raise DialogueFormatError(e)
411 else:
412 greeting = DialogueSection(id=id, text=text,
413 condition=condition,
414 responses=responses,
415 actions=actions)
416
417 return greeting
418
419 def _constructDialogueSection(self, loader, section_node):
420 id_ = None
421 text = None
422 responses = []
423 actions = []
424 dialogue_section = None
425
426 try:
427 for key_node, value_node in section_node.value:
428 key = key_node.value
429 if (key == u'ID'):
430 id_ = loader.construct_object(value_node)
431 elif (key == u'SAY'):
432 text = loader.construct_object(value_node)
433 elif (key == u'RESPONSES'):
434 for response_node in value_node.value:
435 dialogue_response = self._constructDialogueResponse(
436 loader,
437 response_node
438 )
439 responses.append(dialogue_response)
440 elif (key == u'ACTIONS'):
441 for action_node in value_node.value:
442 action = self._constructDialogueAction(loader,
443 action_node)
444 actions.append(action)
445 except (AttributeError, TypeError, ValueError) as e:
446 raise DialogueFormatError(e)
447 else:
448 dialogue_section = DialogueSection(id_=id_, text=text,
449 responses=responses,
450 actions=actions)
451
452 return dialogue_section
453
454 def _constructDialogueResponse(self, loader, response_node):
455 text = None
456 next_section_id = None
457 actions = []
458 condition = None
459
460 try:
461 for key_node, value_node in response_node.value:
462 key = key_node.value
463 if (key == u'REPLY'):
464 text = loader.construct_object(value_node)
465 elif (key == u'ACTIONS'):
466 for action_node in value_node.value:
467 action = self._constructDialogueAction(loader,
468 action_node)
469 actions.append(action)
470 elif (key == u'CONDITION'):
471 condition = loader.construct_object(value_node)
472 elif (key == u'GOTO'):
473 next_section_id = loader.construct_object(value_node)
474 except (AttributeError, TypeError, ValueError) as e:
475 raise DialogueFormatError(e)
476
477 dialogue_response = DialogueResponse(text=text,
478 next_section_id=next_section_id,
479 actions=actions,
480 condition=condition)
481 return dialogue_response
482
483 def _constructDialogueAction(self, loader, action_node):
484 mapping = loader.construct_mapping(action_node, deep=True)
485 keyword, arguments = mapping.items()[0]
486 if (isinstance(arguments, dict)):
487 # Got a dictionary of keyword arguments.
488 args = ()
489 kwargs = arguments
490 elif (not isinstance(arguments, Sequence) or
491 isinstance(arguments, basestring)):
492 # Got a single positional argument.
493 args = (arguments,)
494 kwargs = {}
495 elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)):
496 # Got a list of positional arguments.
497 args = arguments
498 kwargs = {}
499 else:
500 self.logger.error(
501 '{0} is an invalid DialogueAction argument'.format(arguments)
502 )
503 return None
504
505 action_type = DialogueAction.registered_actions.get(keyword)
506 if (action_type is None):
507 self.logger.error(
508 'no DialogueAction with keyword "{0}"'.format(keyword)
509 )
510 dialogue_action = None
511 else:
512 dialogue_action = action_type(*args, **kwargs)
513 return dialogue_action
514
515
516 class OldYamlDialogueParser(YamlDialogueParser):
517 """
518 L{YAMLDialogueParser} that can read and write dialogues in the old
519 Techdemo1 dialogue file format.
520
521 @warning: This class is deprecated and likely to be removed in a future
522 version.
523 """
524 logger = logging.getLogger('dialogueparser.OldYamlDialogueParser')
525
526 def __init__(self):
527 self.response_actions = {}
528
529 def load(self, stream):
530 dialogue = YamlDialogueParser.load(self, stream)
531 # Place all DialogueActions that were in DialogueSections into the
532 # DialogueResponse that led to the action's original section.
533 for section in dialogue.sections.values():
534 for response in section.responses:
535 actions = self.response_actions.get(response.next_section_id)
536 if (actions is not None):
537 response.actions = actions
538 return dialogue
539
540 def _constructDialogue(self, loader, yaml_node):
541 npc_name = None
542 avatar_path = None
543 start_section_id = None
544 sections = []
545
546 try:
547 for key_node, value_node in yaml_node.value:
548 key = key_node.value
549 if (key == u'NPC'):
550 npc_name = loader.construct_object(value_node)
551 elif (key == u'AVATAR'):
552 avatar_path = loader.construct_object(value_node)
553 elif (key == u'START'):
554 start_section_id = loader.construct_object(value_node)
555 elif (key == u'SECTIONS'):
556 for id_node, section_node in value_node.value:
557 dialogue_section = self._constructDialogueSection(
558 loader,
559 id_node,
560 section_node
561 )
562 sections.append(dialogue_section)
563 except (AttributeError, TypeError, ValueError) as e:
564 raise DialogueFormatError(e)
565
566 dialogue = Dialogue(npc_name=npc_name, avatar_path=avatar_path,
567 start_section_id=start_section_id,
568 sections=sections)
569 return dialogue
570
571 def _constructDialogueSection(self, loader, id_node, section_node):
572 id = loader.construct_object(id_node)
573 text = None
574 responses = []
575 actions = []
576 dialogue_section = None
577
578 try:
579 for node in section_node.value:
580 key_node, value_node = node.value[0]
581 key = key_node.value
582 if (key == u'say'):
583 text = loader.construct_object(value_node)
584 elif (key == u'meet'):
585 action = self._constructDialogueAction(loader, node)
586 actions.append(action)
587 elif (key in [u'start_quest', u'complete_quest', u'fail_quest',
588 u'restart_quest', u'set_value',
589 u'decrease_value', u'increase_value',
590 u'give_stuff', u'get_stuff']):
591 action = self._constructDialogueAction(loader, node)
592 if (id not in self.response_actions.keys()):
593 self.response_actions[id] = []
594 self.response_actions[id].append(action)
595 elif (key == u'responses'):
596 for response_node in value_node.value:
597 dialogue_response = self._constructDialogueResponse(
598 loader,
599 response_node
600 )
601 responses.append(dialogue_response)
602 except (AttributeError, TypeError, ValueError) as e:
603 raise DialogueFormatError(e)
604 else:
605 dialogue_section = DialogueSection(id=id, text=text,
606 responses=responses,
607 actions=actions)
608
609 return dialogue_section
610
611 def _constructDialogueResponse(self, loader, response_node):
612 text = None
613 next_section_id = None
614 actions = []
615 condition = None
616
617 try:
618 text = loader.construct_object(response_node.value[0])
619 next_section_id = loader.construct_object(response_node.value[1])
620 if (len(response_node.value) == 3):
621 condition = loader.construct_object(response_node.value[2])
622 except (AttributeError, TypeError, ValueError) as e:
623 raise DialogueFormatError(e)
624
625 dialogue_response = DialogueResponse(text=text,
626 next_section_id=next_section_id,
627 actions=actions,
628 condition=condition)
629 return dialogue_response
630
631 def _constructDialogueAction(self, loader, action_node):
632 mapping = loader.construct_mapping(action_node, deep=True)
633 keyword, arguments = mapping.items()[0]
634 if (keyword == 'get_stuff'):
635 # Renamed keyword in new syntax.
636 keyword = 'take_stuff'
637 elif (keyword == 'set_value'):
638 keyword = 'set_quest_value'
639 elif (keyword == 'increase_value'):
640 keyword = 'increase_quest_value'
641 elif (keyword == 'decrease_value'):
642 keyword = 'decrease_quest_value'
643 if (isinstance(arguments, dict)):
644 # Got a dictionary of keyword arguments.
645 args = ()
646 kwargs = arguments
647 elif (not isinstance(arguments, Sequence) or
648 isinstance(arguments, basestring)):
649 # Got a single positional argument.
650 args = (arguments,)
651 kwargs = {}
652 elif (not len(arguments) == 2 or not isinstance(arguments[1], dict)):
653 # Got a list of positional arguments.
654 args = arguments
655 kwargs = {}
656 else:
657 self.logger.error(
658 '{0} is an invalid DialogueAction argument'.format(arguments)
659 )
660 return None
661 action_type = DialogueAction.registered_actions.get(keyword)
662 if (action_type is None):
663 self.logger.error(
664 'no DialogueAction with keyword "{0}"'.format(keyword)
665 )
666 dialogue_action = None
667 else:
668 dialogue_action = action_type(*args, **kwargs)
669 return dialogue_action