Add dialog_check.py - a script to check dialog files for common errors.

Add ## DIALOGCHECK blocks to all existing action scripts
Allow the pre 'quest' check to express ranges of stages or an exact 
stage to be at


git-svn-id: svn://svn.code.sf.net/p/crossfire/code/maps/trunk@13124 282e977c-c81d-0410-88c4-b93c2d0d6712
master
cavesomething 2010-05-05 11:11:23 +00:00
parent 1422f597ae
commit 9f159a519a
13 changed files with 258 additions and 4 deletions

View File

@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
# dialog_check.py
# This script is *not* intended to be used by the crossfire plugin, it is
# designed to verify the correctness of a dialog script -independantly of the crossfire server.
import cjson
import sys
import os
import re
def checkactionfile(filename, condition):
args = condition[1:]
checkstatus = 0
if not os.path.isfile(filename):
print "Error: No script to support action: ", condition[0], "Expected: ", filename
return False
else:
actf = open(filename,"r")
argnum = 0
for actline in actf.readlines():
if checkstatus == 0:
if actline.find("## DIALOGCHECK") != -1:
checkstatus = 1
elif checkstatus == 1:
if actline.find("## ENDDIALOGCHECK") != -1:
checkstatus = 2
elif actline.find("## MINARGS") != -1:
num = actline.split()[2]
if not num.isdigit():
print "ERROR: Action definition for script ", filename, " MINARGS not defined"
return False
else:
if len(args)<int(num):
print "ERROR: Insufficiant options passed to script ", filename, "expected ", int(num), " recieved ", len(args)
return False
elif actline.find("## MAXARGS") != -1:
num = actline.split()[2]
if not num.isdigit():
print "ERROR: Action definition for script ", filename, " MAXARGS not defined"
return False
elif int(num) == 0:
# zero means there is no upper limit to the number of arguments
pass
else:
if len(args)>int(num):
print "ERROR: Too many options passed to script ", filename, "expected ", int(num), " recieved ", len(args)
return False
elif actline.find("##") != 1:
# This is a regexp for one of the arguments
if argnum < len(args):
argmatch = re.compile(actline.split()[1])
if not argmatch.match(args[argnum]):
print "ERROR: Argument ", argnum+2, "of rule: ", condition, " doesn't match regexp ", actline.split()[1]
return False
argnum+=1
if checkstatus != 2:
print "Warning: No dialogcheck block for file ", filename, " Unable to check condition ", condition
return True
return True
def checkdialoguefile(msgfile, location):
rulenumber = 0
errors = 0
warnings = 0
extrafiles =[]
params = {}
try:
f = open(msgfile,"rb")
except:
print "ERROR: Can't open file, ", msgfile
errors +=1
else:
try:
params = cjson.decode(f.read())
except:
print "ERROR: Failed to parse file, ", msgfile, "not a valid json file"
errors +=1
f.close()
if "location" in params:
if not location == '':
print "Warning: Location defined multiple times in included files"
warnings+=1
location = params["location"]
if location =='':
print "Warning: no location was specified"
warnings +=1
rulenumber =0
for jsonRule in params["rules"]:
rulenumber +=1
include = 0
msg=0
post=0
match=0
pre =0
for action in jsonRule:
if action == "pre":
pre+=1
for condition in jsonRule["pre"]:
action = condition[0]
path = os.path.join("pre/", action + ".py")
if not checkactionfile(path, condition):
print "ERROR: verification of action file ", path, " failed for rule ", rulenumber, " condition ", condition
errors+=1
elif action == "msg":
msg+=1
elif action == "post":
post+=1
for condition in jsonRule["post"]:
action = condition[0]
path = os.path.join("post/", action + ".py")
if not checkactionfile(path, condition):
print "ERROR: verification of action file ", path, " failed for rule ", rulenumber, " condition ", condition
errors +=1
elif action == "match":
match+=1
elif action == "requires":
pass
elif action == "suggests":
pass
elif action == "comment":
pass
elif action == "include":
include+=1
for condition in jsonRule["include"]:
if condition[0] == "/":
inclname = os.path.join("../..", condition[1:])
else:
inclname = os.path.join(os.path.dirname(msgfile), condition)
extrafiles.append(inclname)
else:
print "Warning: Ignoring unknown rule:", action
warnings+=1
if (include == 1 and msg+post+match == 0) or (msg == 1 and post == 1 and match ==1 and pre == 1):
pass
else:
print "ERROR: Rule created with an invalid combination of actions, actions are: ", jsonRule.keys()
errors +=1
newfiles =0
newrules =0
newwarnings=0
newerrors=0
if len(extrafiles) > 0:
for extrapath in extrafiles:
newfiles, newrules, newwarnings, newerrors = checkdialoguefile(extrapath, location)
print "checked ", newrules, "rules from file", extrapath, "Found ", newerrors, " errors and ", newwarnings,"warnings"
extrafiles = []
return (1+newfiles, rulenumber+newrules, warnings+newwarnings, errors+newerrors)
if len(sys.argv) < 2:
print "usage: python dialog_check.py path/to/dialogfile.msg"
exit()
for arg in sys.argv[1:]:
newfiles, rulecount, newwarnings, newerrors = checkdialoguefile(arg, '')
print "checked ", rulecount, "rules from file", arg, "Found ", newerrors, " errors and ", newwarnings,"warnings"

View File

@ -5,5 +5,10 @@
# 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.
## DIALOGCHECK
## MINARGS 1
## MAXARGS 1
## \d+
## ENDDIALOGCHECK
speaker.Map.TriggerConnected(int(condition[1]), 1, speaker)

View File

@ -10,6 +10,12 @@
# to the player
# containers are matched by item name, not arch name.
## DIALOGCHECK
## MINARGS 1
## MAXARGS 1
## .*
## ENDDIALOGCHECK
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

@ -8,6 +8,13 @@
# inventory or in their NPC_Gift_Box if they have one.
# items are matched by item name, not arch name.
## DIALOGCHECK
## MINARGS 1
## MAXARGS 2
## .*
## \d
## ENDDIALOGCHECK
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

@ -6,6 +6,12 @@
# this can then be checked by an age condition that looks for the age
# of "nameofmarker"
## DIALOGCHECK
## MINARGS 1
## MAXARGS 1
## .*
## ENDDIALOGCHECK
markername = args[0]
timestamp = map(str, (Crossfire.GetTime())[:5])
self.setStatus(markername, "-".join(timestamp))

View File

@ -6,6 +6,13 @@
# 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
## DIALOGCHECK
## MINARGS 2
## MAXARGS 2
## .*
## \d+
## ENDDIALOGCHECK
questname = args[0]
stage = args[1]
if character.QuestGetState(questname) == 0:

View File

@ -5,5 +5,11 @@
# The syntax is ["settoken", "tokenname", "valuetosetto"]
# this can then be checked by a token condition that looks for the
# value of the token
## DIALOGCHECK
## MINARGS 2
## MAXARGS 2
## .*
## .*
## ENDDIALOGCHECK
self.setStatus(args[0],args[1])

View File

@ -9,6 +9,12 @@
# 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.
## DIALOGCHECK
## MINARGS 1
## MAXARGS 2
## .*
## \d+
## ENDDIALOGCHECK
itemname = args[0]
if len(args) == 2:

View File

@ -6,6 +6,17 @@
# ["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.
## DIALOGCHECK
## MINARGS 6
## MAXARGS 6
## .*
## \d
## \d
## \d
## \d
## \d
## ENDDIALOGCHECK
class checkfailed(Exception):
pass

View File

@ -10,6 +10,14 @@
# 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
#
## DIALOGCHECK
## MINARGS 1
## MAXARGS 2
## .*
## \d+
## ENDDIALOGCHECK
itemname = args[0]
if len(args) == 2:
quantity = args[1]

View File

@ -4,6 +4,11 @@
# 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
## DIALOGCHECK
## MINARGS 1
## MAXARGS 1
## \d
## ENDDIALOGCHECK
targetlevel = int(args[0])
if len(args) == 1:

View File

@ -4,14 +4,38 @@
# 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
# defined by one of the .quests files.
# Queststage can be in one of the following forms:
# 1) A number - eg "20" - The player must be at or past step 20 on questname
# 2) = A number - eg "=20" - The player must be at exactly step 20 on questname
# 3) = A range of numbers - eg "10-30" - The player must be betwen steps 10 and 30 on questname
# To deliver a True verdict, the player must be at the right stage in the quest.
## DIALOGCHECK
## MINARGS 2
## MAXARGS 2
## .*
## (=|)\d+(|-\d+)
## ENDDIALOGCHECK
questname = args[0]
stage = args[1]
if stage == "complete":
# todo: implement this
pass
if character.QuestGetState(questname) < int(stage):
if stage.find("-") == -1:
if stage[1] == "=":
startstep = int(stage[1:])
endstep = startstep
else:
startstep = int(stage)
endstep = -1
else:
startstep = int(condition.split("-")[0])
endstep= int(condition.split("-")[1])
currentstep = character.QuestGetState(questname)
if currentstep < startstep:
verdict = False
if endstep > -1 and currentstep > endstep:
verdict = False

View File

@ -7,6 +7,12 @@
# 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
## DIALOGCHECK
## MINARGS 2
## MAXARGS 0
## .*
## .*
## ENDDIALOGCHECK
status = self.getStatus(args[0])
for value in args[1:]: