""" Code to handle EMULATED GAME INFO. Game info is a way to assign names to memory locations and their values, so that quest definitions are easier to write. A quest might say "Watch for SCORE > 1000"; the game info would define what memory location SCORE corresponds to and how scores are stored. (If we didn't have GameInfo, every quest would have to specify the address of every memory location of interest; GameInfo makes it easy to define multiple quests for a game) """ import os import sys import traceback import xml.dom.minidom from Utils import * import Global import Config from Global import Log class GameType: MAME = 1 NES = 2 GBA = 3 Names = {MAME:"MAME", NES:"NES", GBA:"GBA", } PrettyNames = {MAME:"Arcade", NES:"NES", GBA:"GBA" } NamesToTypes = {"mame":MAME, "nes":NES, "gba":GBA } class StorageMethod: """ StorageMethod enumerates the various ways that values can be stored / encoded in memory. Simplest is a single byte. The "score" storage method is a number stored such that its HEXADECIMAL digits read out the BASE-10 SCORE. For instance, if your score is 2560, the value stored in memory could be 0x2560, or a 37 byte and a 96 byte. The ScoreLE (score-little-endian) storage method would store a score of 2560 as 0x6025. The "ScoreOnes" storage method indicates that each byte stores a SINGLE digit of the score. For instance, a score of 2560 would be stored in ScoreOnes as 0x02050600. Or in ScoreOnesLE as 0x00060502. The "BitFlag" storage method indicates that we're treating our byte as a bit-vector, and looking for one particular bit. For instance, if the game has 8 bosses that can be defeated in any order, it might keep track of defeated bosses by toggling 8 flags; the boss of interest for our quest might correspond to the flag 1 (the 1s digit), or flag2 (the 2s position in the base-2 number), ..., or flag128. """ Default = 0 Byte = 0 Score = 1 ScoreLE = 2 ScoreOnes = 3 ScoreOnesLE = 4 ScoreOnesOnly = 5 ScoreOnesOnlyLE = 6 TwoByte = 7 TwoByteLE = 8 BitFlag = 9 ScoreReverse = 10 FourByte = 11 FourByteLE = 12 WidthRequiredMethods = (Score, ScoreLE, ScoreOnes, ScoreOnesLE, ScoreOnesOnly, ScoreOnesOnlyLE, ScoreReverse) Names = {Byte: "Byte", Score: "Score", ScoreLE: "ScoreLE", ScoreOnes: "ScoreOnes", ScoreOnesLE: "ScoreOnesLE", ScoreOnesOnly: "ScoreOnesOnly", ScoreOnesOnlyLE: "ScoreOnesOnlyLE", TwoByte: "2Byte", TwoByteLE: "2ByteLE", BitFlag: "BitFlag", ScoreReverse: "ScoreReverse", FourByte:"4Byte", FourByteLE:"4ByteLE", } NamesToTypes = {"byte": Byte, "score": Score, "scorele": ScoreLE, "scoreones": ScoreOnes, "scoreonesle": ScoreOnesLE, "scoreonesonly": ScoreOnesOnly, "scoreonesonlyle": ScoreOnesOnlyLE, "2byte": TwoByte, "4byte": FourByte, "2bytele": TwoByteLE, "4bytele": FourByteLE, "flag": BitFlag, "scorereverse": ScoreReverse, } def ParseStorageMethod(Type): Type = Type.lower() return StorageMethod.NamesToTypes.get(Type, None) class MemlocClass: def __init__(self): self.Name = None self.Location = None self.Width = None # used by Score storage type self.StorageMethod = StorageMethod.Default self.CPU = 0 self.DefaultValue = 0 self.Multiplier = 1 self.Offset = 0 # used for lives def __str__(self): StorageName = StorageMethod.Names.get(self.StorageMethod, "") if self.Location == None: LocStr = "" else: LocStr = "0x%x"%self.Location Str = "") class GameInfoClass: ExpectedXMLAttributes = {"name": 1, "memory": 1, "type": 1, "cpu": 1, "value": 1, "width": 1, "multiplier": 1, "offset": 1} def __init__(self): self.GameName = None self.SubDirectory = None # for ROMs divided into a directory tree. self.GameType = GameType.MAME # default self.DriverName = None # name of the ROM or DRIVER self.Instructions = None self.Memlocs = {} def GetMemlocList(self): return self.Memlocs.keys() def GetMemloc(self, Name): return self.Memlocs.get(Name.lower(), None) def DebugPrint(self): TypeName = GameType.Names.get(self.GameType, "???") Log("Game %s:%s:"%(TypeName, self.GameName)) for (Key, Memloc) in self.Memlocs.items(): Log(" %s"%(Memloc)) def ParseInfoXML(self, InfoNode): """ Parse game-info from an XML node (and subnodes). Return ErrorCount (0 is success) """ ErrorCount = 0 ################################### Name = str(InfoNode.getAttribute("name")) if not Name: Log("* Warning: Unnamed info-node found for game %s"%self.GameName) return 1 Memloc = MemlocClass() Memloc.Name = Name.lower() Memory = str(InfoNode.getAttribute("memory")) if not Memory: Log("* Warning: Info-node '%s' for game %s has no memory location"%(Memloc.Name, self.GameName)) return 1 MemoryLocation = None if Memory[:2].lower() == "0x": Base = 16 else: Base = 10 try: MemoryLocation = int(Memory, Base) except: Log("* Warning: Info-node '%s' for game %s has illegal memory location '%s'"%(Memloc.Name, self.GameName, Memory)) return 1 Memloc.Location = MemoryLocation # Offset, for tracking lives Offset = str(InfoNode.getAttribute("offset")) if Offset: try: Memloc.Offset = int(Offset) except: Log("* Warning: Info-node '%s' for game %s has illegal offset '%s'"%(Memloc.Name, self.GameName, Offset)) ErrorCount += 1 ################################### ValueType = str(InfoNode.getAttribute("type")) if ValueType: Memloc.StorageMethod = ParseStorageMethod(ValueType) if Memloc.StorageMethod == None: Log("* Warning: Info-node '%s' for game %s has illegal storage type '%s'"%(Memloc.Name, self.GameName, ValueType)) ErrorCount += 1 ################################### CPUNumber = str(InfoNode.getAttribute("cpu")) if CPUNumber: Memloc.CPU = int(CPUNumber) if Memloc.CPU < 0 or Memloc.CPU > 10: Log("* Warning: Info-node '%s' for game %s has illegal cpu '%s'"%(Memloc.Name, self.GameName, Memloc.CPU)) ErrorCount += 1 ################################### Multiplier = str(InfoNode.getAttribute("multiplier")) if Multiplier: Memloc.Multiplier = int(Multiplier) if Memloc.Multiplier == 0: Log("* Warning: Info-node '%s' for game %s has silly multiplier cpu '%s'"%(Memloc.Name, self.GameName, Memloc.Multiplier)) ErrorCount += 1 ################################### DefaultValue = str(InfoNode.getAttribute("value")) if DefaultValue: if DefaultValue[:2].lower() == "0x": Base = 16 else: Base = 10 try: Memloc.DefaultValue = int(DefaultValue, Base) except: Log("* Warning: Info-node '%s' for game %s has illegal default value '%s'"%(Memloc.Name, self.GameName, DefaultValue)) ErrorCount += 1 ################################### Width = str(InfoNode.getAttribute("width")) if Width: try: Memloc.Width = int(Width) except: Log("* Warning: Info-node '%s' for game %s has illegal width '%s'"%(Memloc.Name, self.GameName, Width)) ErrorCount += 1 ################################## # Sanity checking: if Memloc.StorageMethod in StorageMethod.WidthRequiredMethods and not Memloc.Width: Log("* Warning: Info-node '%s' for game %s has no width!"%(Memloc.Name, self.GameName)) ErrorCount += 1 ################################## # Complain about UNEXPECTED child nodes: for ChildNode in InfoNode.childNodes: if ChildNode.nodeType == ChildNode.ELEMENT_NODE: Log("* Warning: Info-node '%s' for game %s has unexpected child tag '%s'"%(Memloc.Name, self.GameName, ChildNode.tagName)) ErrorCount += 1 # Complain about UNEXPECTED attributes: for Attribute in InfoNode.attributes.keys(): if not GameInfoClass.ExpectedXMLAttributes.has_key(str(Attribute)): Log("* Warning: Info-node '%s' for game %s has unexpected attribute %s"%(Memloc.Name, self.GameName, Attribute)) ErrorCount += 1 if self.Memlocs.has_key(Name.lower()): Log("* Warning: Info-node '%s' duplicated for game %s"%(Memloc.Name, self.GameName)) ErrorCount += 1 self.Memlocs[Name.lower()] = Memloc return ErrorCount def GetGBAPath(self): if Global.Config.Directories[Config.DirectoryConfig.GBA]: # Look in GoodNES/XXX/DRIVER.gba Log("SubDirectory: %s"%self.SubDirectory) if self.SubDirectory: Dir = os.path.join(Global.Config.Directories[Config.DirectoryConfig.GBA], self.SubDirectory) Path = self.CheckNESROMPath(Dir) if Path: return Path # Look for GoodNES/DRIVER.nes, GoodNES/DRIVER.zip: Path = self.CheckNESROMPath(Global.Config.Directories[Config.DirectoryConfig.GBA]) if Path: return Path # Last resort - look in the GBA subdirectory: return self.CheckNESROMPath("GBA") def GetNESPath(self): if Global.Config.Directories[Config.DirectoryConfig.NES]: # Look in GoodNES/XXX/DRIVER.nes Log("SubDirectory: %s"%self.SubDirectory) if self.SubDirectory: Dir = os.path.join(Global.Config.Directories[Config.DirectoryConfig.NES], self.SubDirectory) Path = self.CheckNESROMPath(Dir) if Path: return Path # Look for GoodNES/DRIVER.nes, GoodNES/DRIVER.zip: Path = self.CheckNESROMPath(Global.Config.Directories[Config.DirectoryConfig.NES]) if Path: return Path # Last resort - look in the NES subdirectory: return self.CheckNESROMPath("NES") def CheckNESROMPath(self, Dir): (Stub, Extension) = os.path.splitext(self.DriverName) Path = os.path.join(Dir, self.DriverName) Log("Check '%s'"%Path) if os.path.exists(Path): return Path Path = os.path.join(Dir, "%s.zip"%Stub) Log("Check '%s'"%Path) if os.path.exists(Path): return Path def GetDisplayName(self): if self.GameName[-4:] == ".nes": return self.GameName[:-4] return self.GameName GameNodeXMLAttributes = {"driver": 1, "name": 1, "type": 1, "subdir": 1, "SaveStatus": 1, "LastCheck": 1} def ParseGameFromNode(GameNode): """ Parse a tag from the game info .xml file. Populate Global.GameInfo[(GameType, Driver)] -> GameInfo """ ErrorCount = 0 Driver = str(GameNode.getAttribute("driver")) Name = str(GameNode.getAttribute("name")) if not Name: Name = Driver TypeStr = str(GameNode.getAttribute("type")) if TypeStr: Type = GameType.NamesToTypes.get(TypeStr.lower(), None) if Type == None: Log("* Warning: Game has unknown type '%s'"%TypeStr) ErrorCount += 1 else: Type = GameType.MAME # default, for now ################################### Subdir = str(GameNode.getAttribute("subdir")) if Subdir: SubDirectory = Subdir else: SubDirectory = None Nodes = GameNode.getElementsByTagName("instructions") Instructions = None if Nodes: Instructions = GetXMLNodeText(Nodes[0]).strip().replace("\r","").replace("\n", " ") Key = (Type, Driver.lower()) if Global.Games.has_key(Key): GameInfo = Global.Games[Key] else: GameInfo = GameInfoClass() GameInfo.GameName = Name GameInfo.DriverName = Driver GameInfo.GameType = Type GameInfo.SubDirectory = SubDirectory GameInfo.Instructions = Instructions Global.Games[Key] = GameInfo for InfoNode in GameNode.getElementsByTagName("info"): ErrorCount += GameInfo.ParseInfoXML(InfoNode) # Complain about UNEXPECTED attributes: for Attribute in GameNode.attributes.keys(): if not GameNodeXMLAttributes.has_key(str(Attribute)): Log("* Warning: Game %s has unexpected attribute '%s'"%(GameInfo.GameName, Attribute)) ErrorCount += 1 # Complain about UNEXPECTED child nodes: for ChildNode in GameNode.childNodes: if (ChildNode.nodeType == ChildNode.ELEMENT_NODE) and (ChildNode.tagName not in ("info", "instructions")): Log("* Warning: Game %s has unexpected child-node %s"%(GameInfo.GameName, ChildNode.tagName)) ErrorCount += 1 return ErrorCount def ParseGameInfoFromFile(FileName): Log("ParseGameInfoFromFile(%s)"%FileName) try: File = open(FileName, "rb") except: Log("* Error: Unable to parse game info from '%s'; can't open file"%FileName) return Text = File.read() File.close() try: DOMRoot = xml.dom.minidom.parseString(Text) except: Log("Error parsing game info XML from '%s'..."%FileName) traceback.print_exc() return ErrorCount = 0 for GameNode in DOMRoot.getElementsByTagName("game"): ErrorCount += ParseGameFromNode(GameNode) # Complain about any unexpected tags: GameDefNode = DOMRoot.firstChild if str(GameDefNode.tagName) != "GameDefinitions": Log("* Warning: Root tag of the game definitions file should be GameDefinitions") ErrorCount += 1 for ChildNode in GameDefNode.childNodes: if ChildNode.nodeType == ChildNode.ELEMENT_NODE: if ChildNode.tagName != "game": Log("* Warning: GameDefinitions tag has unexpected child tag '%s'"%ChildNode.tagName) if ErrorCount: Log("* %s errors encountered while parsing game info from %s"%(ErrorCount, FileName)) else: Log("(No errors in game info file)") def DebugPrintAllGameInfo(): Keys = Global.Games.keys() Keys.sort() for Key in Keys: GameInfo = Global.Games[Key] GameInfo.DebugPrint() def UnitTest(): FileName = os.path.join("quests", "Games.xml") ParseGameInfoFromFile(FileName) #DebugPrintAllGameInfo() if __name__ == "__main__": Log("Running unit tests of GameInfo.py!") UnitTest()