From bb1f5d66cf58d0571ba433f51a7d5fcc2f460e28 Mon Sep 17 00:00:00 2001 From: cavesomething Date: Tue, 27 Apr 2010 23:43:20 +0000 Subject: [PATCH] Break out the CFDialog python script into a dispatcher for a number of smaller, action oriented scripts. git-svn-id: svn://svn.code.sf.net/p/crossfire/code/maps/trunk@13014 282e977c-c81d-0410-88c4-b93c2d0d6712 --- python/CFDialog.py | 406 ++++------------------------- python/dialog/commongive.py | 61 +++++ python/dialog/post/connection.py | 9 + python/dialog/post/givecontents.py | 15 ++ python/dialog/post/giveitem.py | 13 + python/dialog/post/marktime.py | 11 + python/dialog/post/quest.py | 18 ++ python/dialog/post/settoken.py | 9 + python/dialog/post/takeitem.py | 33 +++ python/dialog/pre/README | 21 ++ python/dialog/pre/age.py | 50 ++++ python/dialog/pre/item.py | 27 ++ python/dialog/pre/level.py | 14 + python/dialog/pre/quest.py | 17 ++ python/dialog/pre/token.py | 17 ++ 15 files changed, 364 insertions(+), 357 deletions(-) create mode 100644 python/dialog/commongive.py create mode 100644 python/dialog/post/connection.py create mode 100644 python/dialog/post/givecontents.py create mode 100644 python/dialog/post/giveitem.py create mode 100644 python/dialog/post/marktime.py create mode 100644 python/dialog/post/quest.py create mode 100644 python/dialog/post/settoken.py create mode 100644 python/dialog/post/takeitem.py create mode 100644 python/dialog/pre/README create mode 100644 python/dialog/pre/age.py create mode 100644 python/dialog/pre/item.py create mode 100644 python/dialog/pre/level.py create mode 100644 python/dialog/pre/quest.py create mode 100644 python/dialog/pre/token.py diff --git a/python/CFDialog.py b/python/CFDialog.py index 6a70e2072..6ba983fdc 100644 --- a/python/CFDialog.py +++ b/python/CFDialog.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # CFDialog.py - Dialog helper class # # Copyright (C) 2007 Yann Chachkoff @@ -25,6 +26,8 @@ # 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. +# You will not normally use this directly, but will instead want to call +# dialog/npc_dialog.py which will handle most common uses for dialogs. # # How to use CFDialog # =================== @@ -35,8 +38,7 @@ # 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, answers, preconditions, and postconditions. # # - 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 @@ -58,51 +60,14 @@ # # 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. +# - Preconditions are checks that must pass in order for a rule to be +# triggered. The checks that can be used are to be found in dialog/pre/*.py +# Each file describes how to use the check in question. # -# - 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. +# - Postconditions are changes that should be made to the player and/or the +# game world after the rule triggers. The effects that are available are to +# be found in dialog/post/*.py Each file describes how to use the effect in +# question. # # 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 @@ -114,58 +79,19 @@ # 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. +# allows the dialog data to be written in JSON format. +# This also permits the inclusion of additional files to take in more rules +# (this is mostly useful when you have a character who has some specific lines +# of dialog but also some other lines that are shared with other characters +# - the character can reference their specific lines of dialog directly and +# include the general ones. +# # ../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 @@ -188,6 +114,7 @@ import Crossfire import string import random import sys +import os import CFItemBroker class DialogRule: @@ -264,7 +191,6 @@ class Dialog: 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()) @@ -293,278 +219,44 @@ class Dialog: # 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. + # separate files are used for each type of check. def matchConditions(self, rule): - + character = self.__character + location = self.__location + speaker = self.__speaker + verdict = True 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 + Crossfire.Log(Crossfire.LogDebug, "CFDialog: Trying to test %s." % condition) + action = condition[0] + args = condition[1:] + path = os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/pre/', action + '.py') + if os.path.isfile(path): + Crossfire.Log(Crossfire.LogDebug, "CFDialog: performing test %s." % action) + exec(open(path).read()) + if verdict == False: + return 0 + else: + Crossfire.Log(Crossfire.LogError, "CFDialog: Pre Block called with unknown action %s." % action) + 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. + # If a rule triggers, this function goes through each condition and runs the file that handles it. def setConditions(self, rule): + character = self.__character + location = self.__location + speaker = self.__speaker + 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.") + Crossfire.Log(Crossfire.LogDebug, "CFDialog: Trying to apply %s." % condition) + action = condition[0] + args = condition[1:] + path = os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/post/', action + '.py') + if os.path.isfile(path): + Crossfire.Log(Crossfire.LogDebug, "CFDialog: implementing action %s." % action) + exec(open(path).read()) 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") + Crossfire.Log(Crossfire.LogError, "CFDialog: Post Block called with unknown action %s." % action) # Search the player file for a particular flag, and if it exists, return diff --git a/python/dialog/commongive.py b/python/dialog/commongive.py new file mode 100644 index 000000000..d569c6bc7 --- /dev/null +++ b/python/dialog/commongive.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# commongive.py +# This is a common block of 'give' code to handle give item and give 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 = character.CreateObject('platinum coin') + CFItemBroker.Item(id).add(int(quantity/50)) + if quantity % 50 > 0: + id = character.CreateObject('gold coin') + CFItemBroker.Item(id).add(int((quantity % 50)/10)) + if quantity % 50 > 0: + id = 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 = 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 = speaker.CheckInventory(itemname) + else: + inv = 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(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(character) + else: + # ok, we didn't find any + Crossfire.Log(Crossfire.LogError, "Dialog script tried to give a non-existant item to a player") \ No newline at end of file diff --git a/python/dialog/post/connection.py b/python/dialog/post/connection.py new file mode 100644 index 000000000..327677d6c --- /dev/null +++ b/python/dialog/post/connection.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# connection.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'connection' +# The syntax is ["connection", "numberofconnection"] +# Triggers the numbered connection on the local map in the same way as +# if button with that connection value had been pressed. + +speaker.Map.TriggerConnected(int(condition[1]), 1, speaker) \ No newline at end of file diff --git a/python/dialog/post/givecontents.py b/python/dialog/post/givecontents.py new file mode 100644 index 000000000..24cdf90ea --- /dev/null +++ b/python/dialog/post/givecontents.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# givecontents.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'givecontents' +# The syntax is ["givecontents", "nameofcontainer"] +# The NPC must have a container with the name "nameofcontainer", either in their +# inventory or in their NPC_Gift_Box if they have one. +# If a suitable container exists, then a complete copy of the entire contents +# of that container is given to the player. The container itself is *not* given +# to the player +# containers are matched by item name, not arch name. + +contents=True +# commongive.py does all of the heavy lifting here. +exec(open(os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/commongive.py')).read()) \ No newline at end of file diff --git a/python/dialog/post/giveitem.py b/python/dialog/post/giveitem.py new file mode 100644 index 000000000..16856717b --- /dev/null +++ b/python/dialog/post/giveitem.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# giveitem.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'giveitem' +# The syntax is ["giveitem", "itemtogive", "quantitytogive"] +# "quantitytogive" is optional, if it is missing, then 1 is assumed. +# The NPC must have at least one of the item being given, either in their +# inventory or in their NPC_Gift_Box if they have one. +# items are matched by item name, not arch name. + +contents=False +# commongive.py does all of the heavy lifting here. +exec(open(os.path.join(Crossfire.DataDirectory(), Crossfire.MapDirectory(), 'python/dialog/commongive.py')).read()) \ No newline at end of file diff --git a/python/dialog/post/marktime.py b/python/dialog/post/marktime.py new file mode 100644 index 000000000..fe84ac380 --- /dev/null +++ b/python/dialog/post/marktime.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# marktime.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'marktime' +# The syntax is ["marktime", "nameofmarker"] +# this can then be checked by an age condition that looks for the age +# of "nameofmarker" + +markername = args[0] +timestamp = map(str, (Crossfire.GetTime())[:5]) +self.setStatus(markername, "-".join(timestamp)) diff --git a/python/dialog/post/quest.py b/python/dialog/post/quest.py new file mode 100644 index 000000000..34d20c6f0 --- /dev/null +++ b/python/dialog/post/quest.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +#quest.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'quest' +# The syntax is ["quest", "questname", "queststage"] +# All arguments are required, questname must be a quest that is +# defined by one of the .quests files queststage must be a step +# number in that quest +questname = args[0] +stage = args[1] +if character.QuestGetState(questname) == 0: + Crossfire.Log(Crossfire.LogDebug, "CFDialog: starting quest: %s at stage %s for character %s" %(questname, stage, character.Name)) + character.QuestStart(questname, int(stage)) +elif int(stage) > character.QuestGetState(questname): + Crossfire.Log(Crossfire.LogDebug, "CFDialog: advancing quest: %s to stage %s for character %s" %(questname, stage, character.Name )) + character.QuestSetState(questname, int(stage)) +else: + Crossfire.Log(Crossfire.LogError, "CFDialog: Tried to advance a quest backwards.") \ No newline at end of file diff --git a/python/dialog/post/settoken.py b/python/dialog/post/settoken.py new file mode 100644 index 000000000..142560942 --- /dev/null +++ b/python/dialog/post/settoken.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# settoken.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'settoken' +# The syntax is ["settoken", "tokenname", "valuetosetto"] +# this can then be checked by a token condition that looks for the +# value of the token + +self.setStatus(args[0],args[1]) \ No newline at end of file diff --git a/python/dialog/post/takeitem.py b/python/dialog/post/takeitem.py new file mode 100644 index 000000000..85eddef2f --- /dev/null +++ b/python/dialog/post/takeitem.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# takeitem.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a post rule of 'takeitem' +# The syntax is ["takeitem", "itemtotake", "quantitytotake"] +# "quantitytotake" is optional, if it is missing, then 1 is assumed. +# if it is 0, then *all* instances of the item are taken. +# The player must have a sufficiant quantity of the item being taken +# This should normally be determined by doing an "item" check in the +# pre-block of the rule +# items are matched by item name, not arch name. + +itemname = args[0] +if len(args) == 2: + quantity = args[1] +else: + quantity = 1 +Crossfire.Log(Crossfire.LogDebug, "CFDialog: trying to take: %s of item %s from character %s" %(quantity, itemname, character.Name )) +if itemname == "money": + paid = character.PayAmount(int(quantity)) + if paid == 0: + Crossfire.Log(Crossfire.LogError, "Tried to make player %s pay more than they had" %(character.Name)) +else: + inv = 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. + character.Fix() + else: + Crossfire.Log(Crossfire.LogError, "Dialog script tried to remove non-existant item from player %s" %(character.Name)) diff --git a/python/dialog/pre/README b/python/dialog/pre/README new file mode 100644 index 000000000..477738585 --- /dev/null +++ b/python/dialog/pre/README @@ -0,0 +1,21 @@ +Dialog 'Pre' block check handlers: + +Every .py file in this directory corresponds to a handler for a class of checks in a dialog file. +Each file describes in its comments the check that it performs. + +When writing a new check, you have the variable 'verdict' which you are able to write to. +It is passed in with the value 'True' In order to have a check be treated as 'passed' then +ensure that this variable is still true when you are done, if you want to have your check fail +then set the verdict to 'False' at the point where control goes back to your caller. + +The following variables are also avaiable to you: + +character, speaker, location +which hold the CFObject of the player and the NPC and the location where tokens are being +written to and read from. + +These should probably be treated as read-only by your check, otherwise you might cause strange +side-effects on other checks. + +Other than that, there are no technical limits on what you can do with a check, although it is +probably good form not to make drastic changes to the player's character. diff --git a/python/dialog/pre/age.py b/python/dialog/pre/age.py new file mode 100644 index 000000000..ddc600e8a --- /dev/null +++ b/python/dialog/pre/age.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +#age.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a pre rule of 'age' +# The syntax is +# ["age", "agetoken", "years", "months", "days", "hours", "minutes"] +# To deliver a True verdict, the agetoken must correspond to a date +# that is was at least as long ago as the duration specified in the options. + +class checkfailed(Exception): + pass + +# maximum months, days, hours, as defined by the server. +# minutes per hour is hardcoded to approximately 60 + +try: + 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: + raise checkfailed() + markername = args[0] + oldtime = self.getStatus(markername).split("-") + oldtime = map(int, oldtime) + if len(oldtime) !=5: + # The marker hasn't been set yet + raise checkfailed() + + 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 + Crossfire.Log(Crossfire.LogDebug, "CFDialog: tidied up desired difference: %s actual difference %s" %(desireddiff, actualdiff)) + for i in range(5): + if actualdiff[i] < desireddiff[i]: + raise checkfailed() +except checkfailed: + verdict = False \ No newline at end of file diff --git a/python/dialog/pre/item.py b/python/dialog/pre/item.py new file mode 100644 index 000000000..292845f47 --- /dev/null +++ b/python/dialog/pre/item.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +#item.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a pre rule of 'item' +# The syntax is ["item", "itemname", "numberrequired"] +# numberrequired is optional, if it is missing then 1 is assumed. +# To deliver a True verdict, the player must have at least numberrequired +# copies of an item with name 'itemname' +# if the itemname is 'money' then as a special case, the check +# is against the total value of coin held, in silver. In this +# case the value of the coin must exceed numberrequired silver, +# in any denominations +itemname = args[0] +if len(args) == 2: + quantity = args[1] +else: + quantity = 1 +if itemname == "money": + if character.Money < int(quantity): + verdict = False +else: + inv = character.CheckInventory(itemname) + if inv: + if inv.Quantity < int(quantity): + verdict = False + else: + verdict = False \ No newline at end of file diff --git a/python/dialog/pre/level.py b/python/dialog/pre/level.py new file mode 100644 index 000000000..30f3cff8d --- /dev/null +++ b/python/dialog/pre/level.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +#level.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a pre rule of 'level' +# The syntax is ["level", "levelrequired"] +# To deliver a True verdict, the player must be at or above level levelrequired + +targetlevel = int(args[0]) +if len(args) == 1: + if character.Level < targetlevel: + verdict = False +else: + verdict = False + #TODO - add support for checking the level of individual skills diff --git a/python/dialog/pre/quest.py b/python/dialog/pre/quest.py new file mode 100644 index 000000000..1f1f2bdbf --- /dev/null +++ b/python/dialog/pre/quest.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +#quest.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a pre rule of 'quest' +# The syntax is ["quest", "questname", "queststage"] +# All arguments are required, questname must be a quest that is +# defined by one of the .quests files queststage must be a step +# number in that quest +# To deliver a True verdict, the player must be at or past queststage on questname + +questname = args[0] +stage = args[1] +if stage == "complete": + # todo: implement this + pass +if character.QuestGetState(questname) < int(stage): + verdict = False \ No newline at end of file diff --git a/python/dialog/pre/token.py b/python/dialog/pre/token.py new file mode 100644 index 000000000..152f9dcab --- /dev/null +++ b/python/dialog/pre/token.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +#token.py +# This is one of the files that can be called by an npc_dialog, +# The following code runs when a dialog has a pre rule of 'token' +# The syntax is +# ["token", "tokenname", "possiblevalue1", "possiblevalue2", etc] +# To deliver a True verdict, the token tokenname must be set to one of the +# 'possiblevalue' arguments. This will normally have been done +# with a previous use of settoken + +status = self.getStatus(args[0]) +for value in args[1:]: + if (status == value) or (value == "*"): + pass + else: + verdict = False + break