source: branches/active/character_customization/game/parpg/dialogueparsers.py @ 797

Revision 797, 27.5 KB checked in by aspidites, 8 years ago (diff)

Patch by Aspidites:

  • converted print statements to logging messages.
  • all of parpg's log messages go to the same file, but are differenciated by their class names
  • need to find a way to manipulate fife's log level
  • Property svn:eol-style set to native
Line 
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"""
16Contains classes for parsing and validating L{Dialogues<Dialogue>} and other
17dialogue-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"""
27try:
28    from cStringIO import StringIO
29except ImportError:
30    from StringIO import StringIO
31from collections import Sequence
32try:
33    from collections import OrderedDict
34except ImportError:
35    # Python version 2.4-2.6 doesn't have the OrderedDict
36    from parpg.common.ordereddict import OrderedDict
37import re
38import textwrap
39
40import yaml
41
42from parpg import COPYRIGHT_HEADER
43from parpg.dialogue import (Dialogue, DialogueSection, DialogueResponse,
44    DialogueGreeting)
45from parpg.dialogueactions import DialogueAction
46
47import logging
48logger = logging.getLogger('dialogueparser')
49
50class DialogueFormatError(Exception):
51    """Exception thrown when the DialogueParser has encountered an error."""
52
53
54class 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
97class 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
516class 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
Note: See TracBrowser for help on using the repository browser.