Crossfire Server, Trunk
CFDialog.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 # CFDialog.py - Dialog helper class
3 #
4 # Copyright (C) 2007 Yann Chachkoff
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19 #
20 # The author can be reached via e-mail at lauwenmark@gmail.com
21 
22 # What is CFDialog?
23 # =================
24 #
25 # This is a small set of utility classes, to help you create complex dialogs.
26 # It is made for those who do not want to bother about complex programming,
27 # but just want to make a few dialogs that are better than the @match system
28 # used in the server.
29 # You will not normally use this directly, but will instead want to call
30 # dialog/npc_dialog.py which will handle most common uses for dialogs.
31 #
32 # How to use CFDialog
33 # ===================
34 #
35 # First, create a script that imports the DialogRule and Dialog classes. Add
36 # the following line at the beginning of your script:
37 #
38 # from CFDialog import DialogRule, Dialog
39 #
40 # Next, build the dialog by creating a sequence of several rules made up of
41 # keywords, answers, preconditions, and postconditions.
42 #
43 # - Keywords are what the rule answers to. For example, if you want a rule to
44 # trigger when the player says "hi", then "hi" must appear in the keyword
45 # list. One or more keywords are specified in a string list in the form
46 # ["keyword1", "keyword2" ...]. A "*" character is a special keyword that
47 # means: "match everything", and is useful to create rules that provide
48 # generic answers no matter what the player character says.
49 #
50 # NOTE: Like the @match system, CFDialog converts both keywords and the
51 # things the player says to lowercase before checking for a match,
52 # so it is never necessary to include multiple keywords that only
53 # differ in case.
54 #
55 # - Answers are what the rule will respond, or say, to the player when it is
56 # triggered. This is what the NPC replies to the player. Answers are stored
57 # in a list of one or more strings in the form ["Answer1", "Answer2" ...].
58 # When there is more than one answer in that list, each time the rule is
59 # triggered, a single random reply will be selected from the list.
60 #
61 # NOTE: Answers may contain line breaks. To insert one, use "\n".
62 #
63 # - Preconditions are checks that must pass in order for a rule to be
64 # triggered. The checks that can be used are to be found in dialog/pre/*.py
65 # Each file describes how to use the check in question.
66 #
67 # - Postconditions are changes that should be made to the player and/or the
68 # game world after the rule triggers. The effects that are available are to
69 # be found in dialog/post/*.py Each file describes how to use the effect in
70 # question.
71 #
72 # - Replies are what the player will be informed of possible replies.
73 # Each should be an array in the form [word, text, type], with
74 # 'word' the actual word the player should say, 'text' the text the player
75 # will actually say if she says the word, 'type' an optional integer
76 # to specify if the text is a regular sentence (0), a reply (1) or a question
77 # to ask (2).
78 #
79 #
80 # Once the rules are all defined, assemble them into a dialog. Each dialog
81 # involves somebody who triggers it, somebody who answers, and also a unique
82 # name so it cannot be confused with other dialogs. Typically, the "one who
83 # triggers" will be the player, and the "one who answers" is an NPC the player
84 # was taking to. You are free to choose whatever you want for the dialog name,
85 # as long as it contains no whitespace or special characters, and as long as
86 # it is not used by another dialog. You can then add the rules you created to
87 # the dialog. Rules are parsed in a given order, so you must add the most
88 # generic answer last.
89 #
90 # http://wiki.metalforge.net/doku.php/cfdialog?s=cfdialog#a_simple_example
91 #
92 # A more complex example
93 # ======================
94 #
95 # A ./misc/npc_dialog.py script has been written that uses CFDialog, but
96 # allows the dialog data to be written in JSON format.
97 # This also permits the inclusion of additional files to take in more rules
98 # (this is mostly useful when you have a character who has some specific lines
99 # of dialog but also some other lines that are shared with other characters
100 # - the character can reference their specific lines of dialog directly and
101 # include the general ones.
102 #
103 # ../scorn/kar/gork.msg is an example that uses multiple keywords and multiple
104 # precondition values. Whereas the above example has a linear and predicable
105 # conversation paths, note how a conversation with Gork can fork, merge, and
106 # loop back on itself. The example also illustrates how CFDialog can allow
107 # dialogs to affect how other NPCs react to a player. ../scorn/kar/mork.msg
108 # is a completely different dialog, but it is part of a quest that requires
109 # the player to interact with both NPCs in a specific way before the quest
110 # prize can be obtained. With the old @match system, once the player knew
111 # the key words, he could short-circuit the conversation the map designer
112 # intended to occur. CFDialog constrains the player to follow the proper
113 # conversation thread to qualify to receive the quest reward.
114 #
115 # Debugging
116 # =========
117 #
118 # When debugging, if changes are made to this file, the Crossfire Server must
119 # be restarted for it to register the changes.
120 
121 import Crossfire
122 import string
123 import random
124 import sys
125 import os
126 import CFItemBroker
127 
129  def __init__(self, keywords, presemaphores, messages, postsemaphores, suggested_response = None):
130  self.__keywords = keywords
131  self.__presems = presemaphores
132  self.__messages = messages
133  self.__postsems = postsemaphores
134  self.__suggestions = suggested_response
135  self.__prefunction = None
136 
137  # The keyword is a string. Multiple keywords may be defined in the string
138  # by delimiting them with vertical bar (|) characters. "*" is a special
139  # keyword that matches anything.
140  def getKeyword(self):
141  return self.__keywords
142 
143  # Messages are stored in a list of strings. One or more messages may be
144  # defined in the list. If more than one message is present, a random
145  # string is returned.
146  def getMessage(self):
147  msg = self.__messages
148  l = len(msg)
149  r = random.randint(0, l - 1)
150  return msg[r]
151 
152  # Return the preconditions of a rule. They are a list of one or more lists
153  # that specify a flag name to check, and one or more acceptable values it
154  # may have in order to allow the rule to be triggered.
155  def getPreconditions(self):
156  return self.__presems
157 
158  # Return the postconditions for a rule. They are a list of one or more
159  # lists that specify a flag to be set in the player file and what value it
160  # should be set to.
161  def getPostconditions(self):
162  return self.__postsems
163 
164  # Return the possible responses to this rule
165  # This is when a message is sent.
166  def getSuggests(self):
167  return self.__suggestions
168 
169  # Return a possible pre function, that will be called to ensure the rule matches.
170  def getPreFunction(self):
171  return self.__prefunction
172 
173  # Define a prefunction that will be called to match the rule.
174  def setPreFunction(self, function):
175  self.__prefunction = function
176 
177 # This is a subclass of the generic dialog rule that we use for determining whether to
178 # 'include' additional rules.
180  def __init__(self, presemaphores):
181  DialogRule.__init__(self, None, presemaphores, None, None, None )
182 
183 class Dialog:
184  # A character is the source that supplies keywords that drive the dialog.
185  # The speaker is the NPC that responds to the keywords. A location is an
186  # unique identifier that is used to distinguish dialogs from each other.
187  def __init__(self, character, speaker, location):
188  self.__character = character
189  self.__location = location
190  self.__speaker = speaker
191  self.__rules = []
192 
193  # Create rules of the DialogRule class that define dialog flow. An index
194  # defines the order in which rules are processed. FIXME: addRule could
195  # very easily create the index. It is unclear why this mundane activity
196  # is left for the dialog maker.
197  def addRule(self, rule, index):
198  self.__rules.insert(index, rule)
199 
200  # A function to call when saying something to an NPC to elicit a response
201  # based on defined rules. It iterates through the rules and determines if
202  # the spoken text matches a keyword. If so, the rule preconditions and/or
203  # prefunctions are checked. If all conditions they define are met, then
204  # the NPC responds, and postconditions, if any, are set. Postfunctions
205  # also execute if present.
206  # some variable substitution is done on the message here, $me and $you
207  # are replaced by the names of the npc and the player respectively
208  def speak(self, msg):
209  # query the animation system in case the NPC is playing an animation
210  if self.__speaker.Event(self.__speaker, self.__speaker, "query_object_is_animated", 1):
211  return 0
212 
213  if self.__character.DungeonMaster and msg == 'resetdialog':
214  self.__character.WriteKey(self.keyName(), "", 0)
215  Crossfire.NPCSay(self.__speaker, "Dialog state reset!")
216  return 0
217 
218  key = self.uniqueKey()
219  replies = None
220  if key in Crossfire.GetPrivateDictionary():
221  replies = Crossfire.GetPrivateDictionary()[key]
222  Crossfire.GetPrivateDictionary()[key] = None
223 
224  for rule in self.__rules:
225  if self.isAnswer(msg, rule.getKeyword()) == 1:
226  if self.matchConditions(rule) == 1:
227  message = rule.getMessage()
228  message = message.replace('$me', self.__speaker.QueryName())
229  message = message.replace('$you', self.__character.QueryName())
230 
231  Crossfire.NPCSay(self.__speaker, message)
232  if rule.getSuggests() != None:
233  for reply in rule.getSuggests():
234  Crossfire.AddReply(reply[0], reply[1])
235  Crossfire.GetPrivateDictionary()[key] = rule.getSuggests()
236  self.setConditions(rule)
237 
238  # change the player's text if found
239  if replies != None:
240  for reply in replies:
241  if reply[0] == msg:
242  type = Crossfire.ReplyType.SAY
243  if len(reply) > 2:
244  type = int(reply[2])
245  Crossfire.SetPlayerMessage(reply[1], type)
246  break
247 
248  return 0
249  return 1
250 
251  def uniqueKey(self):
252  return self.__location + '_' + self.__character.QueryName()
253 
254  # Determine if the message sent to an NPC matches a string in the keyword
255  # list. The match check is case-insensitive, and succeeds if a keyword
256  # string is found in the message. This means that the keyword string(s)
257  # only need to be a substring of the message in order to trigger a reply.
258  def isAnswer(self, msg, keywords):
259  for ckey in keywords:
260  if ckey == "*" or msg.lower().find(ckey.lower()) != -1:
261  return 1
262  return 0
263 
264  # Check the preconditions specified in rule have been met. Preconditions
265  # are lists of one or more conditions to check. Each condition specifies
266  # a check to perform and the options it should act on.
267  # separate files are used for each type of check.
268  def matchConditions(self, rule):
269  character = self.__character
270  location = self.__location
271  speaker = self.__speaker
272  verdict = True
273  for condition in rule.getPreconditions():
274  action = condition[0]
275  args = condition[1:]
276  script_args = {'args': args, 'character': character, 'location': location, 'action': action, 'self': self, 'verdict': verdict}
277  path = os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/pre/', action + '.py')
278  if os.path.isfile(path):
279  try:
280  exec(open(path).read(), {}, script_args)
281  verdict = script_args['verdict']
282  except Exception as ex:
283  Crossfire.Log(Crossfire.LogError, "CFDialog: Failed to evaluate condition %s: %s." % (condition, str(ex)))
284  verdict = False
285  if verdict == False:
286  return 0
287  else:
288  Crossfire.Log(Crossfire.LogError, "CFDialog: Pre Block called with unknown action %s." % action)
289  return 0
290 
291  if rule.getPreFunction() != None:
292  if rule.getPreFunction()(self.__character, rule) != True:
293  return 0
294  return 1
295 
296 
297  # If a rule triggers, this function goes through each condition and runs the file that handles it.
298  def setConditions(self, rule):
299  character = self.__character
300  location = self.__location
301  speaker = self.__speaker
302 
303  for condition in rule.getPostconditions():
304  Crossfire.Log(Crossfire.LogDebug, "CFDialog: Trying to apply %s." % condition)
305  action = condition[0]
306  args = condition[1:]
307  path = os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/post/', action + '.py')
308  if os.path.isfile(path):
309  try:
310  exec(open(path).read())
311  except:
312  Crossfire.Log(Crossfire.LogError, "CFDialog: Failed to set post-condition %s." %condition)
313  else:
314  Crossfire.Log(Crossfire.LogError, "CFDialog: Post Block called with unknown action %s." % action)
315 
316  def keyName(self):
317  return "dialog_" + self.__location
318 
319  # Search the player file for a particular flag, and if it exists, return
320  # its value. Flag names are combined with the unique dialog "location"
321  # identifier, and are therefore are not required to be unique. This also
322  # prevents flags from conflicting with other non-dialog-related contents
323  # in the player file.
324  def getStatus(self, key):
325  character_status=self.__character.ReadKey(self.keyName())
326  if character_status == "":
327  return "0"
328  pairs=character_status.split(";")
329  for i in pairs:
330  subpair=i.split(":")
331  if subpair[0] == key:
332  return subpair[1]
333  return "0"
334 
335  # Store a flag in the player file and set it to the specified value. Flag
336  # names are combined with the unique dialog "location" identifier, and are
337  # therefore are not required to be unique. This also prevents flags from
338  # conflicting with other non-dialog-related contents in the player file.
339  def setStatus(self, key, value):
340  if value == "*":
341  return
342  ishere = 0
343  finished = ""
344  character_status = self.__character.ReadKey(self.keyName())
345  if character_status != "":
346  pairs = character_status.split(";")
347  for i in pairs:
348  subpair = i.split(":")
349  if subpair[0] == key:
350  subpair[1] = value
351  ishere = 1
352  if finished != "":
353  finished = finished+";"
354  finished = finished + subpair[0] + ":" + subpair[1]
355  if ishere == 0:
356  if finished != "":
357  finished = finished + ";"
358  finished = finished + key + ":" + value
359  self.__character.WriteKey(self.keyName(), finished, 1)
360 
361  # Search the NPC for a particular flag, and if it exists, return
362  # its value. Flag names are combined with the unique dialog "location"
363  # identifier and the player's name, and are therefore are not required
364  # to be unique. This also prevents flags from conflicting with other
365  # non-dialog-related contents in the NPC.
366  def getNPCStatus(self, key):
367  npc_status=self.__speaker.ReadKey(self.keyName() + "_" + self.__character.Name)
368  if npc_status == "":
369  return "0"
370  pairs=npc_status.split(";")
371  for i in pairs:
372  subpair=i.split(":")
373  if subpair[0] == key:
374  return subpair[1]
375  return "0"
376 
377  # Store a flag in the NPC and set it to the specified value. Flag
378  # names are combined with the unique dialog "location" identifier
379  # and the player's name, and are therefore are not required to be unique.
380  # This also prevents flags from conflicting with other non-dialog-related
381  # contents in the player file.
382  def setNPCStatus(self, key, value):
383  if value == "*":
384  return
385  ishere = 0
386  finished = ""
387  npc_status = self.__speaker.ReadKey(self.keyName() + "_" + self.__character.Name)
388  if npc_status != "":
389  pairs = npc_status.split(";")
390  for i in pairs:
391  subpair = i.split(":")
392  if subpair[0] == key:
393  subpair[1] = value
394  ishere = 1
395  if finished != "":
396  finished = finished+";"
397  finished = finished + subpair[0] + ":" + subpair[1]
398  if ishere == 0:
399  if finished != "":
400  finished = finished + ";"
401  finished = finished + key + ":" + value
402  self.__speaker.WriteKey(self.keyName() + "_" + self.__character.Name, finished, 1)
CFDialog.DialogRule.getKeyword
def getKeyword(self)
Definition: CFDialog.py:140
CFDialog.Dialog.setStatus
def setStatus(self, key, value)
Definition: CFDialog.py:339
CFDialog.Dialog.addRule
def addRule(self, rule, index)
Definition: CFDialog.py:197
CFDialog.Dialog.matchConditions
def matchConditions(self, rule)
Definition: CFDialog.py:268
CFBank.open
def open()
Definition: CFBank.py:69
CFDialog.DialogRule.__presems
__presems
Definition: CFDialog.py:131
CFDialog.Dialog.keyName
def keyName(self)
Definition: CFDialog.py:316
CFDialog.Dialog.speak
def speak(self, msg)
Definition: CFDialog.py:208
CFDialog.DialogRule.__prefunction
__prefunction
Definition: CFDialog.py:135
CFDialog.DialogRule.setPreFunction
def setPreFunction(self, function)
Definition: CFDialog.py:174
CFDialog.IncludeRule
Definition: CFDialog.py:179
CFDialog.DialogRule.getPreFunction
def getPreFunction(self)
Definition: CFDialog.py:170
CFDialog.DialogRule.getMessage
def getMessage(self)
Definition: CFDialog.py:146
make_face_from_files.str
str
Definition: make_face_from_files.py:30
CFDialog.Dialog.uniqueKey
def uniqueKey(self)
Definition: CFDialog.py:251
CFDialog.Dialog.__rules
__rules
Definition: CFDialog.py:191
CFDialog.DialogRule.__suggestions
__suggestions
Definition: CFDialog.py:134
CFDialog.DialogRule.getSuggests
def getSuggests(self)
Definition: CFDialog.py:166
CFDialog.Dialog.__location
__location
Definition: CFDialog.py:189
CFDialog.Dialog.getStatus
def getStatus(self, key)
Definition: CFDialog.py:324
CFDialog.Dialog.setNPCStatus
def setNPCStatus(self, key, value)
Definition: CFDialog.py:382
CFDialog.Dialog.setConditions
def setConditions(self, rule)
Definition: CFDialog.py:298
CFDialog.DialogRule.getPostconditions
def getPostconditions(self)
Definition: CFDialog.py:161
CFDialog.DialogRule
Definition: CFDialog.py:128
make_face_from_files.int
int
Definition: make_face_from_files.py:32
CFDialog.DialogRule.__keywords
__keywords
Definition: CFDialog.py:130
CFDialog.Dialog.__character
__character
Definition: CFDialog.py:188
CFDialog.Dialog.__speaker
__speaker
Definition: CFDialog.py:190
CFDialog.Dialog
Definition: CFDialog.py:183
CFDialog.DialogRule.__messages
__messages
Definition: CFDialog.py:132
CFDialog.Dialog.isAnswer
def isAnswer(self, msg, keywords)
Definition: CFDialog.py:258
CFDialog.DialogRule.getPreconditions
def getPreconditions(self)
Definition: CFDialog.py:155
CFDialog.IncludeRule.__init__
def __init__(self, presemaphores)
Definition: CFDialog.py:180
CFDialog.Dialog.__init__
def __init__(self, character, speaker, location)
Definition: CFDialog.py:187
CFDialog.DialogRule.__init__
def __init__(self, keywords, presemaphores, messages, postsemaphores, suggested_response=None)
Definition: CFDialog.py:129
CFDialog.DialogRule.__postsems
__postsems
Definition: CFDialog.py:133
CFDialog.Dialog.getNPCStatus
def getNPCStatus(self, key)
Definition: CFDialog.py:366