From 1ceafbb8990f11b9ed6b858241317ad230dc157f Mon Sep 17 00:00:00 2001 From: cavesomething Date: Mon, 26 Apr 2010 15:34:26 +0000 Subject: [PATCH] Add CFNPCDialog.py and the wrapper script dialog/npc_dialog.py, a wrapper script for advanced dialog handling. git-svn-id: svn://svn.code.sf.net/p/crossfire/code/maps/trunk@12970 282e977c-c81d-0410-88c4-b93c2d0d6712 --- python/CFNPCDialog.py | 610 ++++++++++++++++++++++++++++++++++++ python/dialog/npc_dialog.py | 139 ++++++++ 2 files changed, 749 insertions(+) create mode 100644 python/CFNPCDialog.py create mode 100644 python/dialog/npc_dialog.py diff --git a/python/CFNPCDialog.py b/python/CFNPCDialog.py new file mode 100644 index 000000000..6a70e2072 --- /dev/null +++ b/python/CFNPCDialog.py @@ -0,0 +1,610 @@ +# CFDialog.py - Dialog helper class +# +# Copyright (C) 2007 Yann Chachkoff +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# The author can be reached via e-mail at lauwenmark@gmail.com + +# What is CFDialog? +# ================= +# +# This is a small set of utility classes, to help you create complex dialogs. +# It is made for those who do not want to bother about complex programming, +# but just want to make a few dialogs that are better than the @match system +# used in the server. +# +# How to use CFDialog +# =================== +# +# First, create a script that imports the DialogRule and Dialog classes. Add +# the following line at the beginning of your script: +# +# from CFDialog import DialogRule, Dialog +# +# Next, build the dialog by creating a sequence of several rules made up of +# keywords, answers, preconditions, and postconditions. Optionally, define +# prefunctions or postfunctions to enhance the capabilities of the rule. +# +# - Keywords are what the rule answers to. For example, if you want a rule to +# trigger when the player says "hi", then "hi" must appear in the keyword +# list. One or more keywords are specified in a string list in the form +# ["keyword1", "keyword2" ...]. A "*" character is a special keyword that +# means: "match everything", and is useful to create rules that provide +# generic answers no matter what the player character says. +# +# NOTE: Like the @match system, CFDialog converts both keywords and the +# things the player says to lowercase before checking for a match, +# so it is never necessary to include multiple keywords that only +# differ in case. +# +# - Answers are what the rule will respond, or say, to the player when it is +# triggered. This is what the NPC replies to the player. Answers are stored +# in a list of one or more strings in the form ["Answer1", "Answer2" ...]. +# When there is more than one answer in that list, each time the rule is +# triggered, a single random reply will be selected from the list. +# +# NOTE: Answers may contain line breaks. To insert one, use "\n". +# +# - Preconditions are flags that must match specific values in order for a +# rule to be triggered. These flags persist across gaming sessions and are +# useful for tracking the state of a conversation with an NPC. Because of +# this, it is possible for the same word to elicit different NPC responses +# depending on how flags have been set. If dialogs are set to use identical +# locations, the flags and preconditions can be used by other NPC dialogs so +# that other NPCs can detect that the player heard specific information from +# another NPC. The flags can also be used to help an individual NPC +# remember what he has said to the player in the past. Flag settings are +# stored in the player file, so they persist as long as the character exists +# in the game. Each rule contains a list of one or more preconditions, if +# any. Supply an empty list [] if no preconditions exist, but otherwise, +# each of the preconditions is required to be a list that contains at least +# a flag name and one or more values in the following format: [["flag1", +# "value1", "value2" ...], ["flag2", "value3"] ...] where "..." indicates +# that the pattern may be repeated. The flag name is always the first item +# in a precondition list. ":" and ";" characters are forbidden in the flag +# names and values. For a rule to be triggered, all its preconditions must +# be satisfied by settings in the player file. To satisfy a precondition, +# one of the precondition values must match the identified flag setting in +# the player file. The default value of any precondition that has not been +# specifically set in the player file is "0". If one of the precondition +# values is set to "*", a match is not required. +# +# - Postconditions are state changes to apply to the player file flags after +# the rule triggers. If a rule is not intended to set a flag, supply an +# empty list [] when specifying postconditions, otherwise, postconditions +# are supplied in a nested list that has the same format as the precondition +# list except that each postcondition list only contains one value. This is +# because the other main difference is that whereas a precondition checks a +# player file to see if a flag has a certain value, the postcondition causes +# a value to be stored into the player file, and it does not make sense to +# store more than one value into a single flag. A value of "*" means that +# the player file flag will not be changed. +# +# - A prefunction is an optional callback function that will be called when a +# rule's preconditions are all matched, but before the rule is validated. +# The callback can do additional tests, and should return 1 to allow the +# rule to be selected, or 0 to block the rule. The function arguments are +# the player and the actual rule being tested. +# +# - A postfunction is an optional callback that is called when a rule has been +# applied, and after the message is said. It can do additional custom +# processing. The function arguments are the player and the actual rule +# having been used. +# +# Once the rules are all defined, assemble them into a dialog. Each dialog +# involves somebody who triggers it, somebody who answers, and also a unique +# name so it cannot be confused with other dialogs. Typically, the "one who +# triggers" will be the player, and the "one who answers" is an NPC the player +# was taking to. You are free to choose whatever you want for the dialog name, +# as long as it contains no whitespace or special characters, and as long as +# it is not used by another dialog. You can then add the rules you created to +# the dialog. Rules are parsed in a given order, so you must add the most +# generic answer last. +# +# A simple example +# ================ +# +# If I want to create a dialog for an old man, I might want him to respond to +# "hello" or "hi" differently the first time the player meets the NPC, and +# differently for subsequent encounters. In this example, grandpa greets the +# player cordially the first time, but grumbles subequent times (because he's +# like that, you know :)). This example grandpa also has a generic answer for +# what ever else is said to him. In the example, the player is stored in +# 'player', and the old man in 'grandpa', and the player said is in 'message'. +# +## Dialog creation: +# speech = Dialog(player, grandpa, "test_grandpa_01") +# +## The first rule is the "hello" answer, so we place it at index 0 of the +## rules list. The precondition is that we never said hello before. The +## postcondition saves a value of "1" into a player file flag named "hello" +## so grandpa remembers he has already met this player before. +# +# prer = [["hello","0"]] +# postr = [["hello", "1"]] +# rmsg = ["Hello, lad!","Hi, young fellow!","Howdy!"] +# speech.addRule(DialogRule(["hello","hi"], prer, rmsg, postr),0) +# +## The second rule is the answer to a greeting if he as already met the player +## before. Notice that "*" is used for the postcondition value, meaning that +## the flag will remain set as it was prior to the rule triggering. +# +# prer = [["hello","1"]] +# postr = [["hello", "*"]] +# rmsg = ["I've heard, you know, I'm not deaf *grmbl*"] +# speech.addRule(DialogRule(["hello","hi"], prer, rmsg, postr),1) +# +## Finally, the generic answer is written. This is the last rule of the list. +## We don't need to match any condition, and don't need to change any flags, +## so we use "*" in both cases this time. +# +# prer = [["hello","*"]] +# postr = [["hello", "*"]] +# rmsg = ["What ?", "Huh ?", "What do you want ?"] +# speech.addRule(DialogRule(["*"], prer, rmsg, postr),2) +# +# The following link points to a page on the Crossfire Wiki shows all the +# details needed to actually place this example in an actual game map: +# +# http://wiki.metalforge.net/doku.php/cfdialog?s=cfdialog#a_simple_example +# +# A more complex example +# ====================== +# +# A ./misc/npc_dialog.py script has been written that uses CFDialog, but +# allows the dialog data to be written in a slightly different format. +# ../scorn/kar/gork.msg is an example that uses multiple keywords and multiple +# precondition values. Whereas the above example has a linear and predicable +# conversation paths, note how a conversation with Gork can fork, merge, and +# loop back on itself. The example also illustrates how CFDialog can allow +# dialogs to affect how other NPCs react to a player. ../scorn/kar/mork.msg +# is a completely different dialog, but it is part of a quest that requires +# the player to interact with both NPCs in a specific way before the quest +# prize can be obtained. With the old @match system, once the player knew +# the key words, he could short-circuit the conversation the map designer +# intended to occur. CFDialog constrains the player to follow the proper +# conversation thread to qualify to receive the quest reward. +# +# Debugging +# ========= +# +# When debugging, if changes are made to this file, the Crossfire Server must +# be restarted for it to register the changes. + +import Crossfire +import string +import random +import sys +import CFItemBroker + +class DialogRule: + def __init__(self, keywords, presemaphores, messages, postsemaphores, suggested_response = None, required_response = None): + self.__keywords = keywords + self.__presems = presemaphores + self.__messages = messages + self.__postsems = postsemaphores + self.__suggestions = suggested_response + self.__requirements = required_response + + # The keyword is a string. Multiple keywords may be defined in the string + # by delimiting them with vertical bar (|) characters. "*" is a special + # keyword that matches anything. + def getKeyword(self): + return self.__keywords + + # Messages are stored in a list of strings. One or more messages may be + # defined in the list. If more than one message is present, a random + # string is returned. + def getMessage(self): + msg = self.__messages + l = len(msg) + r = random.randint(0, l - 1) + return msg[r] + + # Return the preconditions of a rule. They are a list of one or more lists + # that specify a flag name to check, and one or more acceptable values it + # may have in order to allow the rule to be triggered. + def getPreconditions(self): + return self.__presems + + # Return the postconditions for a rule. They are a list of one or more + # lists that specify a flag to be set in the player file and what value it + # should be set to. + def getPostconditions(self): + return self.__postsems + + # Return the possible responses to this rule + # This is when a message is sent. + def getSuggests(self): + return self.__suggestions + + # Return the required responses to this rule, this is like a suggestion, + # except that no other response will make sense in context (eg, a yes/no question) + def getRequires(self): + return self.__requirements + +class Dialog: + # A character is the source that supplies keywords that drive the dialog. + # The speaker is the NPC that responds to the keywords. A location is an + # unique identifier that is used to distinguish dialogs from each other. + def __init__(self, character, speaker, location): + self.__character = character + self.__location = location + self.__speaker = speaker + self.__rules = [] + + # Create rules of the DialogRule class that define dialog flow. An index + # defines the order in which rules are processed. FIXME: addRule could + # very easily create the index. It is unclear why this mundane activity + # is left for the dialog maker. + def addRule(self, rule, index): + self.__rules.insert(index, rule) + + # A function to call when saying something to an NPC to elicit a response + # based on defined rules. It iterates through the rules and determines if + # the spoken text matches a keyword. If so, the rule preconditions and/or + # prefunctions are checked. If all conditions they define are met, then + # the NPC responds, and postconditions, if any, are set. Postfunctions + # also execute if present. + # some variable substitution is done on the message here, $me and $you + # are replaced by the names of the npc and the player respectively + def speak(self, msg): + for rule in self.__rules: + if self.isAnswer(msg, rule.getKeyword()) == 1: + print "Checking whether to say: ", rule.getMessage() + if self.matchConditions(rule) == 1: + message = rule.getMessage() + message = message.replace('$me', self.__speaker.QueryName()) + message = message.replace('$you', self.__character.QueryName()) + + self.__speaker.Say(message) + if rule.getRequires() == None: + if rule.getSuggests() != None: + self.__speaker.Say(rule.getSuggests()) + else: + self.__speaker.Say(rule.getRequires()) + self.setConditions(rule) + return 0 + return 1 + + # Determine if the message sent to an NPC matches a string in the keyword + # list. The match check is case-insensitive, and succeeds if a keyword + # string is found in the message. This means that the keyword string(s) + # only need to be a substring of the message in order to trigger a reply. + def isAnswer(self, msg, keywords): + for ckey in keywords: + if ckey == "*" or msg.lower().find(ckey.lower()) != -1: + return 1 + return 0 + + # Check the preconditions specified in rule have been met. Preconditions + # are lists of one or more conditions to check. Each condition specifies + # a check to perform and the options it should act on. + # separate functions are called for each type of check. + def matchConditions(self, rule): + + for condition in rule.getPreconditions(): + #try: + print 'attempting to match rule', condition + if condition[0] == 'quest': + print self.matchquest(condition[1:]) + if self.matchquest(condition[1:]) == 0: + return 0 + elif condition[0] == 'item': + print self.matchitem(condition[1:]) + if self.matchitem(condition[1:]) == 0: + return 0 + elif condition[0] == 'level': + print self.matchlevel(condition[1:]) + if self.matchlevel(condition[1:]) == 0: + return 0 + elif condition[0] == 'age': + print self.checkage(condition[1:]) + if self.checkage(condition[1:]) == 0: + return 0 + elif condition[0] == 'token': + print self.checktoken(condition[1:]) + if self.checktoken(condition[1:]) == 0: + return 0 + else: + Crossfire.Log(Crossfire.LogError, "CFDialog: Preconditon called with unknown action.") + #except: + # Crossfire.Log(Crossfire.LogDebug, "CFDialog: Bad Precondition") + # return 0 + return 1 + + # Checks whether the token arg[0] has been set to any of the values in args[1] onwards + def checktoken(self, args): + status = self.getStatus(args[0]) + for value in args[1:]: + if (status == value) or (value == "*"): + return 1 + + return 0 + + # This returns 1 if the token in args[0] was set at least + # args[1] years + # args[2] months + # args[3] days + # args[4] hours + # args[5] minutes + # ago (in game time). + def checkage(self, args): + # maximum months, days, hours, as defined by the server. + # minutes per hour is hardcoded to approximately 60 + MAXTIMES = [Crossfire.Time.MONTHS_PER_YEAR, Crossfire.Time.WEEKS_PER_MONTH*Crossfire.Time.DAYS_PER_WEEK, + Crossfire.Time.HOURS_PER_DAY, 60] + # we have three times to consider, the old time, the current time, and the desired time difference. + if len(args) != 6: + return 0 + markername = args[0] + oldtime = self.getStatus(markername).split("-") + oldtime = map(int, oldtime) + if len(oldtime) !=5: + + # The marker hasn't been set yet + return 0 + + desireddiff = map(int, args[1:]) + currenttime = (Crossfire.GetTime())[:5] + actualdiff = [] + + for i in range(5): + actualdiff.append(currenttime[i]-oldtime[i]) + + for i in range(4,0,-1): + # let's tidy up desireddiff first + if desireddiff[i] > MAXTIMES[i-1]: + desireddiff[i-1] += desireddiff[i] // MAXTIMES[i-1] + desireddiff[i] %= MAXTIMES[i-1] + # Then actualdiff + if actualdiff[i] < 0: + actualdiff[i] += MAXTIMES[i-1] + actualdiff[i-1] -=1 + + print 'tidied up desired difference', desireddiff + print 'actual difference', actualdiff + for i in range(5): + if actualdiff[i] < desireddiff[i]: + return 0 + return 1 + + + # is passed a list, returns 1 if the character has at least args[1] of an item called args[0] + # if args[1] isn't given, then looks for 1 of the item. + def matchitem(self, args): + itemname = args[0] + if len(args) == 2: + quantity = args[1] + else: + quantity = 1 + if itemname == "money": + if self.__character.Money >= int(quantity): + return 1 + else: + return 0 + inv = self.__character.CheckInventory(itemname) + if inv: + if inv.Quantity >= int(quantity): + return 1 + return 0 + + # is passed a list, returns 1 if the player is at least at stage args[1] in quest args[0] + def matchquest(self, args): + questname = args[0] + stage = args[1] + print 'I am looking for stage ', stage, ' current stage is ', self.__character.QuestGetState(questname) + if stage == "complete": + # todo: implement this + pass + if self.__character.QuestGetState(questname) < int(stage): + return 0 + return 1 + + # is passed a list, returns 1 if the player is at least at level args[0] either overall or in the skill corresponding to any of the following arguments. + def matchlevel(self, args): + targetlevel = int(args[0]) + if len(args) == 1: + if self.__character.Level >= targetlevel: + return 1 + else: + return 0 + else: + pass + #TODO + + # If a rule triggers, this function is called to make identified player + # file changes, and to call any declared postfunctions to implement more + # dramatic effects than the setting of a flag in the player file. + def setConditions(self, rule): + for condition in rule.getPostconditions(): + # try: + if 1: + print 'trying to apply', condition + action = condition[0] + if action == 'quest': + self.setquest(condition[1:]) + elif action == 'connection': + self.__speaker.Map.TriggerConnected(int(condition[1]), 1, self.__speaker) + elif action == 'takeitem': + self.takeitem(condition[1:]) + elif action == 'giveitem': + self.giveitem(condition[1:], False) + elif action == 'givecontents': + self.giveitem(condition[1:], True) + elif action == 'marktime': + self.marktime(condition[1:]) + elif action == 'settoken': + self.setStatus(condition[1],condition[2]) + else: + Crossfire.Log(Crossfire.LogError, "CFDialog: Post Block called with unknown action.") + else: + #except: + Crossfire.Log(Crossfire.LogError, "CFDialog: Bad Postcondition") + Crossfire.Log(Crossfire.LogError, sys.exc_info()[0]) + return 0 + + def marktime(self, args): + markername = args[0] + timestamp = map(str, (Crossfire.GetTime())[:5]) + self.setStatus(markername, "-".join(timestamp)) + + # moves player to stage args[1] of quest args[0] + def setquest(self, args): + questname = args[0] + stage = args[1] + if self.__character.QuestGetState(questname) == 0: + print 'starting quest', questname, ' at stage ', stage + self.__character.QuestStart(questname, int(stage)) + elif int(stage) > self.__character.QuestGetState(questname): + print 'advancing quest', questname, 'to stage ', stage + self.__character.QuestSetState(questname, int(stage)) + else: + Crossfire.Log(Crossfire.LogError, "CFDialog: Tried to advance a quest backwards.") + + # places args[1] copies of item called args[0], into the inventory of the player. + # the item must be in the inventory of the NPC first for this to work. + # if args[1] is not specified, assume this means 1 copy of the item. + # if contents is 'true' then we don't give the player the item, but treat this item as a container, + # and give the player the exact contents of the container. + def giveitem(self, args, contents): + itemname = args[0] + if len(args) == 2: + quantity = int(args[1]) + else: + quantity = 1 + if itemname == "money": + # we can't guarentee that the player has any particular type of coin already + # so create the object first, then add 1 less than the total. + if quantity >= 50: + id = self.__character.CreateObject('platinum coin') + CFItemBroker.Item(id).add(int(quantity/50)) + if quantity % 50 > 0: + id = self.__character.CreateObject('gold coin') + CFItemBroker.Item(id).add(int((quantity % 50)/10)) + if quantity % 50 > 0: + id = self.__character.CreateObject('silver coin') + CFItemBroker.Item(id).add(int(quantity % 10)) + else: + # what we will do, is increase the number of items the NPC is holding, then + # split the stack into the players inventory. + # first we will check if there is an NPC_Gift_Box, and look in there. + lookin = self.__speaker.CheckInventory("NPC_Gift_Box") + if lookin: + inv = lookin.CheckInventory(itemname) + if not inv: + # ok, the NPC has no 'Gift Box', we'll check the other items. + inv = self.__speaker.CheckInventory(itemname) + else: + inv = self.__speaker.CheckInventory(itemname) + + if inv: + if contents: + nextob=inv.Inventory + while nextob: + # when giving the contents of a container, always give the + # number of items in the container, not the quantity number. + quantity = nextob.Quantity + if quantity == 0: + # if quantity is 0, then we need to set it to one, otherwise bad things happen. + nextob.Quantity = 1 + quantity = 1 + newob = nextob.Clone(0) + newob.Quantity = quantity + newob.InsertInto(self.__character) + nextob=nextob.Below + else: + if quantity == 0: + nextob.Quantity = 2 + quantity = 1 + else: + CFItemBroker.Item(inv).add(quantity+1) + newob = inv.Split(quantity) + + newob.InsertInto(self.__character) + else: + # ok, we didn't find any + Crossfire.Log(Crossfire.LogError, "Dialog script tried to give a non-existant item to a player") + + # removes args[1] copies of item called args[0], this should only be used if + # you have checked the player has those items beforehand. + # if args[1] is zero, will take all copies of the item from the first matching stack. + # if it is not specified, take 1 copy of the item. + def takeitem(self, args): + itemname = args[0] + if len(args) == 2: + quantity = args[1] + else: + quantity = 1 + print 'trying to take ', quantity, ' of item ', itemname + if itemname == "money": + paid = self.__character.PayAmount(int(quantity)) + if paid == 0: + Crossfire.Log(Crossfire.LogError, "Tried to make player pay more than they had") + else: + inv = self.__character.CheckInventory(itemname) + if inv: + if quantity == 0: + CFItemBroker.Item(inv).subtract(inv.Quantity) + else: + CFItemBroker.Item(inv).subtract(int(quantity)) + # we might have been wearing an item that was taken. + self.__character.Fix() + else: + Crossfire.Log(Crossfire.LogError, "Dialog script tried to remove non-existant item from player") + + + # Search the player file for a particular flag, and if it exists, return + # its value. Flag names are combined with the unique dialog "location" + # identifier, and are therefore are not required to be unique. This also + # prevents flags from conflicting with other non-dialog-related contents + # in the player file. + def getStatus(self, key): + character_status=self.__character.ReadKey("dialog_"+self.__location); + if character_status == "": + return "0" + pairs=character_status.split(";") + for i in pairs: + subpair=i.split(":") + if subpair[0] == key: + return subpair[1] + return "0" + + # Store a flag in the player file and set it to the specified value. Flag + # names are combined with the unique dialog "location" identifier, and are + # therefore are not required to be unique. This also prevents flags from + # conflicting with other non-dialog-related contents in the player file. + def setStatus(self, key, value): + if value == "*": + return + ishere = 0 + finished = "" + character_status = self.__character.ReadKey("dialog_"+self.__location); + if character_status != "": + pairs = character_status.split(";") + for i in pairs: + subpair = i.split(":") + if subpair[0] == key: + subpair[1] = value + ishere = 1 + if finished != "": + finished = finished+";" + finished = finished + subpair[0] + ":" + subpair[1] + if ishere == 0: + if finished != "": + finished = finished + ";" + finished = finished + key + ":" + value + self.__character.WriteKey("dialog_" + self.__location, finished, 1) diff --git a/python/dialog/npc_dialog.py b/python/dialog/npc_dialog.py new file mode 100644 index 000000000..22b074646 --- /dev/null +++ b/python/dialog/npc_dialog.py @@ -0,0 +1,139 @@ +# npc_dialog.py - Dialog helper class +# +# Copyright (C) 2007 David Delbecq +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# +# This is a simple script that makes use of CFDialog.py and that receives +# parameters from a JSON inside the event message. Alternatively, the JSON +# parameters, if >= 4096 characters, can be stored in a separate file. +# Use the classical script parameter to specify relative location of dialog. +# +# An example of a map file entry is: +# +# arch guildmaster +# name Sigmund +# msg +# +# endmsg +# x 11 +# y 7 +# resist_physical 100 +# resist_magic 100 +# weight 50000000 +# friendly 1 +# stand_still 1 +# arch event_say +# name start/sigmund.msg +# title Python +# slaying /python/misc/npc_dialog.py +# end +# end +# +# An example of a JSON dialog similar to the one described in CFDialog.py is: +# +# { +# "location" : "test_grandpa_01", +# "rules": [ +# { +# "match" : ["hello","hi"], +# "pre" : [["hello","0"]], +# "post" : [["hello","1"]], +# "msg" : ["Hello, lad!","Hi, young fellow!","Howdy!"] +# }, +# { +# "match": ["hello","hi"], +# "pre" :[["hello","1"]], +# "post" :[["hello", "*"]], +# "msg" : ["I've heard, you know, I'm not deaf *grmbl*"] +# }, +# { +# "match" : ["*"], +# "pre" : [["hello","*"]], +# "post" : [["hello", "*"]], +# "msg" : ["What ?", "Huh ?", "What do you want ?"] +# } +# ]} +# +# For detailed descriptions of the match, pre, post, and msg formats, see the +# ../CFDialog.py script. +# +# "match" is a list of keyword strings, and corresponds to what the player says +# that the dialog will respond to. +# +# In the above example, the first rule is applied if the player/character says +# "hello" or "hi" and if the "hello" flag is set to "0" (default). When the +# rule is applied, the "hello" flag is then set to "1". +# +# "pre" is a list of preconditions that identifies flags that must be set to a +# particular value in order to trigger a response if a match is detected. +# +# "post" is a list of postconditions that specify flags that are to be set if a +# response is triggered. +# +# All of the rule values are lists, and must be enclosed by square braces, but +# pre and post are lists of lists, so the nested square braces ([[]]) are +# required except that using an empty list [] is the best way to indicate when +# the rule does not need to check preconditions or set postconditions. +# +# "msg" defines one or more responses that will be given if the rule triggers. +# When more than one "msg" value is set up, the NPC randomly selects which one +# to say each time the rule is applied. +# +# A relatively complex example of an npc_dialog.py dialog is given in the Gork +# treasure room quest. See ../scorn/kar/gork.msg in particular as it +# demonstrates how multiple precondition flag values may be exploited to +# produce non-linear and variable-path conversations that are less likely to +# frustrate a player. Refer also to ../scorn/kar/mork.msg to see how more +# than one dialog can reference the same dialog flags. + +import Crossfire +import os +from CFNPCDialog import DialogRule, Dialog +import cjson + +npc = Crossfire.WhoAmI() +event = Crossfire.WhatIsEvent() +player = Crossfire.WhoIsActivator() +if (Crossfire.ScriptParameters() != None): + filename = os.path.join(Crossfire.DataDirectory(), + Crossfire.MapDirectory(), + Crossfire.ScriptParameters()) + try: + f = open(filename,'rb') + except: + Crossfire.Log(Crossfire.LogDebug, "Error loading NPC dialog %s" % filename) + raise + else: + Crossfire.Log(Crossfire.LogDebug, "Loading NPC dialog %s" % filename) + parameters = cjson.decode(f.read()) + f.close() +else: + parameters = cjson.decode(event.Message) +location = parameters["location"]; +speech = Dialog(player, npc, location) +index = 0; + +for jsonRule in parameters["rules"]: + rule = DialogRule(jsonRule["match"], + jsonRule["pre"], + jsonRule["msg"], + jsonRule["post"]) + speech.addRule(rule, index) + index = index + 1 + +if speech.speak(Crossfire.WhatIsMessage()) == 0: + Crossfire.SetReturnValue(1)