""" Class definitions for QUESTS. A QuestClass stores a quest definition. Helper classes are QuestNodeClass (a condition determining quest completion) and QuestStatClass (a record which is reported, like score or lives or time taken). """ import os import time import sys import cPickle import traceback import xml.dom.minidom from Utils import * import GameInfo import Global from Global import * import shutil # We could switch the pickle module to "pickle", but the pickle # file format is not much more human-readable than binary pickle, meh. PickleModule = cPickle class Bag: pass class QuestState: NoQuest = -4 Error = -3 Abort = -2 Running = -1 Failure = 0 Gold = 1 Silver = 2 Bronze = 3 Names = {Error: "Error", Abort: "Abort", Running: "Running", Failure: "Failure", Gold: "Gold", Silver: "Silver", Bronze: "Bronze", NoQuest: "NoQuest" } TopScoreOrdering = {Gold: 0, Silver: 1, Bronze: 2, Failure: 3, Abort: 4, Error: 4, Running: 4} class QuestNodeType: Default = 0 Equal = 0 LessThan = 1 GreaterThan = 2 LessOrEqual = 3 GreaterOrEqual = 4 NotEqual = 5 AND = 6 OR = 7 DiffEqual = 8 DiffLessThan = 9 DiffGreaterThan = 10 DiffLessOrEqual = 11 DiffGreaterOrEqual = 12 DiffNotEqual = 13 Names = {Equal: "Equal", LessThan: "Less", GreaterThan: "Greater", LessOrEqual: "LessEqual", GreaterOrEqual: "GreaterEqual", NotEqual: "NotEqual", AND: "AND", OR: "OR", DiffEqual: "DiffEqual", DiffLessThan: "DiffLess", DiffGreaterThan: "DiffGreater", DiffLessOrEqual: "DiffLessEqual", DiffGreaterOrEqual: "DiffGreaterEqual", DiffNotEqual: "DiffNotEqual", } Nicknames = {Equal: "==", LessThan: "<", GreaterThan: ">", LessOrEqual: "<=", GreaterOrEqual: ">=", NotEqual: "!=", AND: "AND", OR: "OR", DiffEqual: "- Original ==", DiffLessThan: "- Original <", DiffGreaterThan: "- Original >", DiffLessOrEqual: "- Original <=", DiffGreaterOrEqual: "- Original >=", DiffNotEqual: "- Original !=", } NamesToTypes = {"equal": Equal, "less": LessThan, "lessthan": LessThan, "greater": GreaterThan, "greaterthan": GreaterThan, "lessorequal": LessOrEqual, "lessequal": LessOrEqual, "greaterorequal": GreaterOrEqual, "greaterequal": GreaterOrEqual, "notequal": NotEqual, "and": AND, "or": OR, "diffequal": DiffEqual, "diffless": DiffLessThan, "difflessthan": DiffLessThan, "diffgreater": DiffGreaterThan, "diffgreaterthan": DiffGreaterThan, "difflessorequal": DiffLessOrEqual, "difflessequal": DiffLessOrEqual, "diffgreaterorequal": DiffGreaterOrEqual, "diffgreaterequal": DiffGreaterOrEqual, "diffnotequal": DiffNotEqual, } def QNodeNeedsChildren(Type): if Type in (QuestNodeType.AND, QuestNodeType.OR): return 1 return 0 def QNodeNeedsValue(Type): if Type in (QuestNodeType.AND, QuestNodeType.OR): return 0 return 1 class QuestSpecial: Time = 2 Names = {Time:"time", } NamesToTypes = {"time":Time } class QuestNodeClass: """ A QuestNode is a CONDITION, such as "score > 1000" or "lives == 0". A QuestNode can be an AND or OR of one or more child nodes. A quest is completed when the (gold) success QuestNode tests true; a quest is failed when the failure QuestNode tests true. """ ExpectedXMLAttributes = {"compare": 1, "special": 1, "info": 1, "value": 1, "type": 1} def __init__(self): # SpecialValue is either None or a member of QuestSpecial: self.SpecialValue = None # Memloc is either None or an instance of GameInfo.MemlocClass: self.Memloc = None self.Constant = 0 self.Children = [] self.Type = QuestNodeType.Equal def DebugPrint(self, IndentCount = 0): IndentString = " " * (IndentCount * 2) CompareName = QuestNodeType.Nicknames.get(self.Type, "??") if self.Type in (QuestNodeType.AND, QuestNodeType.OR): Log("%s%s:"%(IndentString, CompareName)) for Child in self.Children: Child.DebugPrint(IndentCount + 1) return if self.SpecialValue: ValueName = QuestSpecial.Names[self.SpecialValue] else: ValueName = self.Memloc.Name Log("%s%s %s %s"%(IndentString, ValueName, CompareName, self.Constant)) def ParseFromXML(self, XMLNode, Game, InfoString = "?"): """ Parse a tag from quest definition XML. Returns error count, 0 for success. """ ErrorCount = 0 # Parse "compare" attribute (equal, etc) ComparatorStr = str(XMLNode.getAttribute("compare")) if ComparatorStr: Comparator = QuestNodeType.NamesToTypes.get(ComparatorStr.lower(), None) if Comparator == None: Log("* Error: Condition in '%s' has invalid comparator '%s'"%(InfoString, ComparatorStr)) ErrorCount += 1 self.Type = Comparator # Parse SPECIAL VALUE: SpecialStr = str(XMLNode.getAttribute("special")) if SpecialStr: SpecialStr = SpecialStr.lower() if SpecialStr != "info": Special = QuestSpecial.NamesToTypes.get(SpecialStr, None) if Special == None: Log("* Error: Condition in '%s' has invlaid special value '%s'"%(InfoString, SpecialStr)) ErrorCount += 1 self.SpecialValue = Special # Parse INFO: MemlocStr = str(XMLNode.getAttribute("info")) if MemlocStr: #Log("Memlocstr '%s' game %s"%(MemlocStr, Game)) Memloc = None if Game: Memloc = Game.GetMemloc(MemlocStr) else: print "* Error: Unknown game!" self.Memloc = Memloc if not Memloc: Log("* Error: Condition in '%s' has unknown memloc '%s'"%(InfoString, MemlocStr)) Log("* Legal memlocs are as follows:") Log(" %s"%Game.GetMemlocList()) ErrorCount += 1 else: self.Constant = Memloc.DefaultValue # Parse constant-value (default is inherited from memloc!): ConstantStr = str(XMLNode.getAttribute("value")) if ConstantStr: if ConstantStr[:2].lower() == "0x": Base = 16 else: Base = 10 try: self.Constant = int(ConstantStr, Base) except: Log("* Error: Condition in '%s' has invalid constant '%s'"%(InfoString, ConstantStr)) ErrorCount += 1 # Parse child-nodes: ChildNodes = XMLNode.childNodes for ChildNode in ChildNodes: if str(ChildNode.localName) != "condition": continue SubCondition = QuestNodeClass() ErrorCount += SubCondition.ParseFromXML(ChildNode, Game, InfoString) self.Children.append(SubCondition) # Sanity checking: if QNodeNeedsChildren(self.Type) and not self.Children: Log("* Error: %s Condition in '%s' needs children!"%(QuestNodeType.Names[self.Type], InfoString)) ErrorCount += 1 if QNodeNeedsValue(self.Type): if not self.Memloc and not self.SpecialValue: Log("* Error: Condition in '%s' has no memory location specified"%(InfoString)) ErrorCount += 1 # Complain on unhandled attribute: for Attribute in XMLNode.attributes.keys(): if not QuestNodeClass.ExpectedXMLAttributes.has_key(str(Attribute)): Log("* Warning: Condition in '%s' has unexpected attribute '%s'"%(InfoString, Attribute)) ErrorCount += 1 # Complain on unhandled child-tag: for ChildNode in XMLNode.childNodes: if ChildNode.nodeType == ChildNode.ELEMENT_NODE and ChildNode.tagName != "condition": Log("* Warning: Condition in '%s' has unexpected child tag '%s'"%(InfoString, ChildNode.tagName)) ErrorCount += 1 return ErrorCount class QuestStatClass: def __init__(self): #self.Name = None self.SpecialValue = None self.HighFlag = 1 # by default, higher is better! self.Memloc = None def GetNiceName(self): String = str(self.GetName()) return String[0].upper() + String[1:] def GetName(self): #Log("Get name. Memloc %s, special value %s"%(self.Memloc, self.SpecialValue)) if self.Memloc: return self.Memloc.Name return QuestSpecial.Names.get(self.SpecialValue, None) def __str__(self): if self.SpecialValue: Name = QuestSpecial.Names.get(self.SpecialValue, "???") elif self.Memloc: Name = self.Memloc.Name Name += self.Memloc.GetStorageMethodName() Name += " 0x%x"%self.Memloc.Location if self.Memloc.Width: Name += " w%d"%self.Memloc.Width if self.HighFlag: return ""%Name else: return ""%Name class QuestRecordClass: def __init__(self): self.Result = None self.Ticks = None self.Timestamp = None self.MasterLivesLeft = None self.MasterTicksLeft = None self.StatValues = [] def DebugPrint(self, Quest, Name = ""): Log("%s"%(QuestState.Names.get(self.Result, "???"))) Log("Time: %s"%time.asctime(time.localtime(self.Timestamp))) PrintedTicks = 0 for StatIndex in range(len(Quest.Stats)): Stat = Quest.Stats[StatIndex] Log("%s: %s"%(Stat.GetName(), self.StatValues[StatIndex])) if Stat.SpecialValue == QuestSpecial.Time: PrintedTicks = 1 if not PrintedTicks: Log("Ticks: %s"%self.Ticks) class QuestOverallStats: """ A simple class for recording OVERALL stats on a quest, such as the total number of plays. (These are the numbers that aren't specific to a single play-through!) """ EBAWeightOld = 0.8 EBAWeightNew = 0.2 def __init__(self): self.Name = None self.TotalPlays = {} # state -> count self.TotalTicks = 0 # state -> tries, or None if not completed yet: self.TriesToComplete = {} # state -> ticks, or None if not completed yet: self.TicksToComplete = {} # EBA for gold, silver, bronze self.EBA = {} def Update(self, Record): """ Register this quest result. """ # Count plays: TotalPlays = self.TotalPlays.get(None, 0) + 1 self.TotalPlays[None] = TotalPlays self.TotalPlays[Record.Result] = self.TotalPlays.get(Record.Result, 0) + 1 # Use flags to track whehter we beat gold, silver-or-better, bronze-or-better: GoldFlag = 0 SilverFlag = 0 BronzeFlag = 0 if Record.Result == QuestState.Gold: GoldFlag = 1 SilverFlag = 1 BronzeFlag = 1 if Record.Result == QuestState.Silver: SilverFlag = 1 BronzeFlag = 1 if Record.Result == QuestState.Bronze: BronzeFlag = 1 # compute EXPONENTIAL BATTING AVERAGES: if not self.EBA.has_key(QuestState.Gold): self.EBA[QuestState.Gold] = GoldFlag else: self.EBA[QuestState.Gold] = self.EBA[QuestState.Gold] * self.EBAWeightOld + GoldFlag * self.EBAWeightNew if not self.EBA.has_key(QuestState.Silver): self.EBA[QuestState.Silver] = SilverFlag else: self.EBA[QuestState.Silver] = self.EBA[QuestState.Silver] * self.EBAWeightOld + SilverFlag * self.EBAWeightNew if not self.EBA.has_key(QuestState.Bronze): self.EBA[QuestState.Bronze] = BronzeFlag else: self.EBA[QuestState.Bronze] = self.EBA[QuestState.Bronze] * self.EBAWeightOld + BronzeFlag * self.EBAWeightNew # Count ticks: self.TotalTicks += Record.Ticks # Remember the number of plays/ticks required to get first completion: if not self.TriesToComplete.has_key(QuestState.Gold): if GoldFlag: self.TriesToComplete[QuestState.Gold] = TotalPlays self.TicksToComplete[QuestState.Gold] = self.TotalTicks if not self.TriesToComplete.has_key(QuestState.Silver): if SilverFlag: self.TriesToComplete[QuestState.Silver] = TotalPlays self.TicksToComplete[QuestState.Silver] = self.TotalTicks if not self.TriesToComplete.has_key(QuestState.Bronze): if BronzeFlag: self.TriesToComplete[QuestState.Bronze] = TotalPlays self.TicksToComplete[QuestState.Bronze] = self.TotalTicks class QuestClass: ExpectedXMLAttributes = {"name": 1, "ID": 1, "driver": 1, "type": 1, "savefile": 1, "patch": 1, "lifekeep": 1,} ExpectedChildTags = {"instructions": 1, "stat": 1, "condition": 1,} def __init__(self): self.Group = None self.ID = None # player-readable name; ASSUMED UNIQUE within its group. self.Name = None # GameType is a member of GameInfo.GameType self.GameType = GameInfo.GameType.MAME self.DriverName = None # MAME driver-name or ROM file-name self.PatchFileName = None self.FailureNode = None self.GoldNode = None self.SilverNode = None self.BronzeNode = None self.Instructions = None self.Stats = [] # self.Game is a reference to a GameInfo instance. # It should always be set! self.Game = None self.SaveFileName = None self.RecentRecords = [] self.TopRecords = [] self.OverallQStats = None self.LifekeepFlag = 0 self.DungeonFlag = -1 self.MasterLivesLeft = 1 self.MasterTicksLeft = 1 self.MasterLivesLoc = None self.MasterLivesOffset = 0 def __str__(self): return ""%self.Name def GetGameType(self): """ Viewable version of our game-type: """ return GameInfo.GameType.PrettyNames.get(self.GameType, "???") def GetDisplayDriverName(self): """ Get a prettified version of our game's name: """ if self.Game: return self.Game.GetDisplayName() return self.DriverName def UpdateOverallQStats(self, Record): self.OverallQStats.Update(Record) def RecordResult(self, ResultDict): """ Called when a player returns from a quest: Save this success (or failure!) """ if not ResultDict: return if not self.OverallQStats: self.OverallQStats = QuestOverallStats() self.OverallQStats.Name = self.Name Record = QuestRecordClass() Record.Result = ResultDict.get("Result", QuestState.Error) Record.Ticks = ResultDict.get("TotalTicks", 0) Record.Timestamp = time.time() StatList = ResultDict.get("Stats", []) MaxStatIndex = min(len(self.Stats), len(StatList)) for StatIndex in range(MaxStatIndex): Record.StatValues.append(StatList[StatIndex]) self.UpdateOverallQStats(Record) # Record RECENT records: self.RecentRecords.insert(0, Record) self.RecentRecords = self.RecentRecords[:Global.Options.KeepRecentRecords] # Record BEST records: self.TopRecords.append(Record) self.SortTopRecords() self.TopRecords = self.TopRecords[:Global.Options.KeepTopRecords] def SortTopRecords(self): SortedList = [] for Record in self.TopRecords: Entry = [] Entry.append(QuestState.TopScoreOrdering[Record.Result]) for StatIndex in range(len(self.Stats)): Stat = self.Stats[StatIndex] Value = Record.StatValues[StatIndex] if Stat.HighFlag: Entry.append(-Value) else: Entry.append(Value) Entry.append(Record) SortedList.append(Entry) SortedList.sort() self.TopRecords = [] for List in SortedList: self.TopRecords.append(List[-1]) def ParseStat(self, XMLNode): """ Parse a stat (a value that we save, and which the player tries to make as HIGH or LOW as possible) """ Stat = QuestStatClass() # Special stats (currently just TIME): TypeString = str(XMLNode.getAttribute("type")) if TypeString: TypeString = TypeString.lower() if TypeString != "info": Type = QuestSpecial.NamesToTypes.get(TypeString, None) if Type == None: Log("* Warning: Quest '%s' has stat of invalid type '%s'"%(self.Name, TypeString)) return Stat.SpecialValue = Type # HIGH or LOW stat types: HighString = str(XMLNode.getAttribute("high")) if HighString: try: HighFlag = int(HighString) Stat.HighFlag = HighFlag except: Log("* Warning: Quest '%s' has stat with illegal high='%s'"%(self.Name, HighString)) # Stats by name: NameString = str(XMLNode.getAttribute("name")) if NameString and self.Game: Memloc = self.Game.GetMemloc(NameString) if not Memloc: Log("* Warning: Quest '%s' has unknown stat '%s'"%(self.Name, NameString)) else: Stat.Memloc = Memloc # Sanity-checking: if Stat.SpecialValue and Stat.Memloc: Log("* Warning: Quest '%s' has stat with both special value and GameInfo!"%(self.Name)) self.Stats.append(Stat) def ParseFromNode(self, XMLNode): ErrorCount = 0 self.Name = str(XMLNode.getAttribute("name")) try: self.ID = int(XMLNode.getAttribute("ID")) except: Log("* Warning: Quest without ID! %s"%self.Name) ############################################## # Parse the GAME. self.DriverName = str(XMLNode.getAttribute("driver")) GameTypeStr = str(XMLNode.getAttribute("type")) if GameTypeStr: GameType = GameInfo.GameType.NamesToTypes.get(GameTypeStr.lower(), None) if not GameType: Log("* Warning: Quest '%s' has unknown game type '%s'"%(self.Name, GameTypeStr)) ErrorCount += 1 self.GameType = GameType self.Game = Global.GetGame(self.GameType, self.DriverName) if not self.Game: Log("* Warning: Quest '%s' has unknown game %s:%s"%(self.Name, GameInfo.GameType.Names.get(self.GameType, None), self.DriverName)) ErrorCount += 1 ############################################## # Parse the SAVEFILE NAME: self.SaveFileName = str(XMLNode.getAttribute("savefile")) # Parse the PATCH FILENAME: FileName = str(XMLNode.getAttribute("patch")) if FileName: self.PatchFileName = FileName Flag = str(XMLNode.getAttribute("lifekeep")) if Flag: self.LifekeepFlag = -1 try: self.LifekeepFlag = int(Flag) except: pass if self.LifekeepFlag < 0 or self.LifekeepFlag > 1: Log("* Warning: Quest '%s' has invalid Lifekeep flag"%self.Name) ErrorCount += 1 self.LifekeepFlag = 0 if self.LifekeepFlag: Memloc = self.Game.GetMemloc("Lives") if not Memloc: Log("* Warning: Quest '%s' has Lifekeep set, but the game has no Lives memloc"%self.Name) ErrorCount += 1 # Parse instructions: Nodes = XMLNode.getElementsByTagName("instructions") if Nodes: self.Instructions = GetXMLNodeText(Nodes[0]).strip().replace("\r","").replace("\n", " ") ############################################## # Parse stats: Nodes = XMLNode.getElementsByTagName("stat") for Node in Nodes: Stat = self.ParseStat(Node) if Stat: self.Stats.append(Stat) ############################################## # Parse victory/failure conditions: Nodes = XMLNode.childNodes ConditionNameToAttribute = {"gold": "GoldNode", "silver": "SilverNode", "bronze": "BronzeNode", "failure": "FailureNode" } for Node in Nodes: if str(Node.localName) != "condition": continue TypeString = str(Node.getAttribute("type")) Attribute = ConditionNameToAttribute.get(TypeString, None) if not Attribute: Log("* Warning: Quest '%s' has top-level condition node with no 'type' (gold/failure/etc)"%self.Name) ErrorCount += 1 continue if getattr(self, Attribute) != None: Log("* Warning: Quest '%s' has TWO top-level conditions of type '%s'"%(self.Name, TypeString)) ErrorCount += 1 continue Condition = QuestNodeClass() ConditionErrors = Condition.ParseFromXML(Node, self.Game, self.Name) ErrorCount += ConditionErrors if ConditionErrors == 0: setattr(self, Attribute, Condition) # Sanity-checking: if not self.FailureNode: Log("* Warning: Quest '%s' has no failure condition"%self.Name) ErrorCount += 1 if not self.GoldNode: Log("* Warning: Quest '%s' has no (gold) victory condition"%self.Name) ErrorCount += 1 if self.BronzeNode and not self.SilverNode: Log("* Warning: Quest '%s' has bronze but no silver victory condition"%self.Name) ErrorCount += 1 # Complain about unhandled attributes and tags: for Attribute in XMLNode.attributes.keys(): if not QuestClass.ExpectedXMLAttributes.has_key(str(Attribute)): Log("* Warning: Quest '%s' has unexpected attributes '%s'"%(self.Name, Attribute)) ErrorCount += 1 for ChildNode in XMLNode.childNodes: if ChildNode.nodeType == ChildNode.ELEMENT_NODE and not QuestClass.ExpectedChildTags.has_key(ChildNode.tagName): Log("* Warning: Quest '%s' has unexpected child tag '%s'"%(self.Name, ChildNode.tagName)) ErrorCount += 1 return ErrorCount def DebugPrint(self): Log("Quest '%s' type %s driver '%s'"%(self.Name, GameInfo.GameType.Names.get(self.GameType, None), self.DriverName)) for StatIndex in range(len(self.Stats)): Log(" Stat #%s: %s"%(StatIndex+1, self.Stats[StatIndex])) if self.GoldNode: Log("GOLD success condition:") self.GoldNode.DebugPrint(1) if self.SilverNode: Log("SILVER success condition:") self.SilverNode.DebugPrint(1) if self.BronzeNode: Log("BRONZE success condition:") self.BronzeNode.DebugPrint(1) if self.FailureNode: Log("FAILURE condition:") self.FailureNode.DebugPrint(1) if self.TopRecords: Log("Best records:") for Index in range(len(self.TopRecords)): Record = self.TopRecords[Index] Record.DebugPrint("#%d"%Index) def GetSaveFilePath(self): if self.Group and self.Group.Directory: Path = os.path.join(self.Group.Directory, self.SaveFileName) return Path return self.SaveFileName def GetPatchFilePath(self): if not self.PatchFileName: return None if self.Group and self.Group.Directory: Path = os.path.join(self.Group.Directory, self.PatchFileName) return Path return self.PatchFileName def GetBestCompletionString(self): """ Return our best completion as a string. """ if not self.OverallQStats: return "" if self.OverallQStats.TotalPlays.get(QuestState.Gold, 0): return "Gold" if self.OverallQStats.TotalPlays.get(QuestState.Silver, 0): return "Silver" if self.OverallQStats.TotalPlays.get(QuestState.Bronze, 0): return "Bronze" return "" def GetResultsFromDict(self, ResultDict): if not ResultDict: return None Record = QuestRecordClass() Record.Result = ResultDict.get("Result", QuestState.Error) Record.Ticks = ResultDict.get("TotalTicks", 0) Record.Timestamp = time.time() Record.MasterLivesLeft = ResultDict.get("MasterLivesLeft", 0) Record.MasterTicksLeft = ResultDict.get("MasterTicksLeft", 0) StatList = ResultDict.get("Stats", []) #Log("Stat list: %s"%StatList) MaxStatIndex = min(len(self.Stats), len(StatList)) for StatIndex in range(MaxStatIndex): Record.StatValues.append(StatList[StatIndex]) return Record def GetNiceInstructions(self): """ Translate our instructions, using the keystrokes configured in the keyconfig. """ return Config.GetFixedInstructions(self.Instructions) if __name__ == "__main__": pass