From 9f159a519aea105f45e5fe72fcf32bb804f54128 Mon Sep 17 00:00:00 2001 From: cavesomething Date: Wed, 5 May 2010 11:11:23 +0000 Subject: [PATCH] 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 --- python/dialog/dialog_check.py | 157 +++++++++++++++++++++++++++++ python/dialog/post/connection.py | 5 + python/dialog/post/givecontents.py | 6 ++ python/dialog/post/giveitem.py | 7 ++ python/dialog/post/marktime.py | 6 ++ python/dialog/post/quest.py | 7 ++ python/dialog/post/settoken.py | 6 ++ python/dialog/post/takeitem.py | 6 ++ python/dialog/pre/age.py | 11 ++ python/dialog/pre/item.py | 8 ++ python/dialog/pre/level.py | 5 + python/dialog/pre/quest.py | 32 +++++- python/dialog/pre/token.py | 6 ++ 13 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 python/dialog/dialog_check.py diff --git a/python/dialog/dialog_check.py b/python/dialog/dialog_check.py new file mode 100644 index 000000000..1197664f8 --- /dev/null +++ b/python/dialog/dialog_check.py @@ -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: 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" \ No newline at end of file diff --git a/python/dialog/post/connection.py b/python/dialog/post/connection.py index 327677d6c..cd436c276 100644 --- a/python/dialog/post/connection.py +++ b/python/dialog/post/connection.py @@ -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) \ No newline at end of file diff --git a/python/dialog/post/givecontents.py b/python/dialog/post/givecontents.py index 24cdf90ea..aba7fc531 100644 --- a/python/dialog/post/givecontents.py +++ b/python/dialog/post/givecontents.py @@ -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()) \ No newline at end of file diff --git a/python/dialog/post/giveitem.py b/python/dialog/post/giveitem.py index 16856717b..c97f0fb77 100644 --- a/python/dialog/post/giveitem.py +++ b/python/dialog/post/giveitem.py @@ -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()) \ No newline at end of file diff --git a/python/dialog/post/marktime.py b/python/dialog/post/marktime.py index fe84ac380..6023da7f8 100644 --- a/python/dialog/post/marktime.py +++ b/python/dialog/post/marktime.py @@ -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)) diff --git a/python/dialog/post/quest.py b/python/dialog/post/quest.py index 34d20c6f0..2cd6ff894 100644 --- a/python/dialog/post/quest.py +++ b/python/dialog/post/quest.py @@ -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: diff --git a/python/dialog/post/settoken.py b/python/dialog/post/settoken.py index 142560942..3f366e7b2 100644 --- a/python/dialog/post/settoken.py +++ b/python/dialog/post/settoken.py @@ -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]) \ No newline at end of file diff --git a/python/dialog/post/takeitem.py b/python/dialog/post/takeitem.py index 85eddef2f..b70bf0fc4 100644 --- a/python/dialog/post/takeitem.py +++ b/python/dialog/post/takeitem.py @@ -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: diff --git a/python/dialog/pre/age.py b/python/dialog/pre/age.py index ddc600e8a..c203dd127 100644 --- a/python/dialog/pre/age.py +++ b/python/dialog/pre/age.py @@ -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 diff --git a/python/dialog/pre/item.py b/python/dialog/pre/item.py index 292845f47..bf3d84c89 100644 --- a/python/dialog/pre/item.py +++ b/python/dialog/pre/item.py @@ -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] diff --git a/python/dialog/pre/level.py b/python/dialog/pre/level.py index 30f3cff8d..383026601 100644 --- a/python/dialog/pre/level.py +++ b/python/dialog/pre/level.py @@ -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: diff --git a/python/dialog/pre/quest.py b/python/dialog/pre/quest.py index 1f1f2bdbf..2aa008140 100644 --- a/python/dialog/pre/quest.py +++ b/python/dialog/pre/quest.py @@ -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 \ No newline at end of file diff --git a/python/dialog/pre/token.py b/python/dialog/pre/token.py index 152f9dcab..49ec640a5 100644 --- a/python/dialog/pre/token.py +++ b/python/dialog/pre/token.py @@ -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:]: