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
master
cavesomething 2010-04-27 23:43:20 +00:00
parent c5899c93a0
commit bb1f5d66cf
15 changed files with 364 additions and 357 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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))

View File

@ -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.")

View File

@ -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])

View File

@ -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))

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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