// Import GoNES XML cheat database // TODO: read Nestopia's NstCheat files, like those found on http://www.mightymo.net/downloads.html // TODO: read Nesticle .pat files, [+]<code> [<name>] per line, raw format. http://www.zophar.net/faq/nitrofaq/nesticle.readme.txt and some at http://jeff.tk:81/consoles/ func (db *Database) ImportXML(filename string) { fmt.Printf("Importing %s\n", filename) r, err := os.Open(filename, os.O_RDONLY, 0) if r == nil { panic(fmt.Sprintf("cannot open %s: %s", filename, err)) } cheats := Cheats{} xml.Unmarshal(r, &cheats) db.exec("INSERT INTO user(fullname) VALUES(?)", "Galoob") // TODO: pass as argument, search for existing, and stop hardcoding userID := db.handle.LastInsertRowID() gamesInserted := 0 cartsInserted := 0 effectsInserted := 0 codesInserted := 0 // Insert into database for _, game := range cheats.Game { db.exec("INSERT INTO game(name,galoob_id,galoob_name) VALUES(?,?,?)", game.Name, game.GaloobID, game.GaloobName) // LastInsertedRowID() requires gosqlite patch at http://code.google.com/p/gosqlite/issues/detail?id=7 // or for https://github.com/kless/go-sqlite, add to end of connection.go: /* func (c *Connection) LastInsertRowID() (int64) { return int64(C.sqlite3_last_insert_rowid(c.db)) } */ gameID := db.handle.LastInsertRowID() gamesInserted += 1 cartName2ID := make(map[string]int64) for _, cart := range game.Cartridge { db.exec("INSERT INTO cart(game_id,sha1,name,filename) VALUES(?,?,?,?)", gameID, cart.SHA1, cart.Name, cart.Filename) cartName2ID[cart.Name] = db.handle.LastInsertRowID() cartsInserted += 1 } for _, effect := range game.Effect { db.exec("INSERT INTO effect(game_id,number,source,title,hacker_id,create_date) VALUES(?,?,?,?,?,?)", gameID, effect.Number, effect.Source, effect.Title, userID, "1994") effectID := db.handle.LastInsertRowID() effectsInserted += 1 for _, code := range effect.Code { func() { defer func() { r := recover() if r != nil { fmt.Printf("Bad code for %s: %s: %s\n", game.Name, code.Genie, r) } }() decoded := gamegenie.Decode(code.Genie) db.exec("INSERT INTO code(effect_id,cpu_address,value) VALUES(?,?,?)", effectID, decoded.CPUAddress(), decoded.Value) codeID := db.handle.LastInsertRowID() codesInserted += 1 // Only set if has a compare value, otherwise leave NULL if decoded.HasKey { db.exec("UPDATE code SET compare=? WHERE id=?", decoded.Key, codeID) } // If code only applies to a specific cart, set it; otherwise, leave NULL to apply to all if code.Applies != "" { db.exec("UPDATE code SET cart_id=? WHERE id=?", cartName2ID[code.Applies], codeID) } }() } } } fmt.Printf("Imported %d games, %d carts, %d effects, and %d total codes\n", gamesInserted, cartsInserted, effectsInserted, codesInserted) }
// Run a command to do something with the unit func (deck *ControlDeck) RunCommand(cmd string) { // If given filename (no command), load and run it if strings.HasSuffix(cmd, ".nes") || strings.HasSuffix(cmd, ".NES") || strings.HasSuffix(cmd, ".unf") || strings.HasSuffix(cmd, ".UNF") || strings.HasSuffix(cmd, ".unif") || strings.HasSuffix(cmd, ".UNIF") { _, err := os.Stat(cmd) if err == nil { deck.Load(cmd) deck.Start() } // have to ignore non-existing files, since might be a command } // TODO: better cmd parsing. real scripting language? tokens := strings.Split(cmd, " ", -1) name := tokens[0] args := tokens[1:] switch name { // go (not to be confused with the programming language) case "g", "go": if len(args) > 0 { // optional start address to override RESET vector startInt, err := strconv.Btoui64(args[0], 0) if err != nil { fmt.Fprintf(os.Stderr, "unable to read start address: %s: %s\n", args[0], err) os.Exit(-1) } deck.CPU.PC = uint16(startInt) } // TODO: number of instructions, cycles, pixels, or frames to execute // (2i, 2c, 2p, 2f) deck.Start() // load case "l", "load": if len(args) > 0 { deck.Load(strings.Join(args, " ")) // TODO: use cmd directly instead of rejoining // TODO: verbose load } else { fmt.Printf("usage: l <filename>\n") } case "load-dir": if len(args) > 0 { deck.LoadDir(strings.Join(args, " ")) // TODO: use cmd directly instead of rejoining } // hide GUI case "h", "hide": deck.ShowGui = false // interactive case "i", "shell": deck.Shell() // registers case "r", "registers": deck.CPU.DumpRegisters() case "m", "memory": deck.CPU.DumpMemoryMap() case "show-carts": cartdb.Dump(cartdb.Load()) // TODO: search // TODO: unassemble // pattern table case "p", "show-pattern": if len(args) < 2 { fmt.Printf("usage: p <table-number> <tile-number>\n") return } // TODO: allow omitting to show all table, err := strconv.Btoui64(args[0], 0) if err == nil { table = 0 } tile, err := strconv.Btoui64(args[1], 0) if err == nil { tile = 0 } if table == 0 { table = 0 } else { table = 0x1000 } deck.PPU.PrintPattern(deck.PPU.GetPattern(uint16(table), int(tile))) /* TODO: be able to interact with shell when GUI is open, so could do this case "draw-patterns": ppu.ShowPatterns() */ // enter case "e", "enter": if len(args) < 2 { fmt.Printf("usage: e <address> <byte0> [<byte1> [<byteN...]]\n") return } baseAddress, err := strconv.Btoui64(args[0], 16) if err != nil { fmt.Printf("bad address: %s: %s\n", args[0], err) return } for offset, arg := range args[1:] { argInt, err := strconv.Btoui64(arg, 16) if err == nil { writeAddress := uint16(baseAddress) + uint16(offset) writeByte := uint8(argInt) fmt.Printf("%.4X = %X\n", writeAddress, writeByte) deck.CPU.WriteTo(writeAddress, writeByte) } else { fmt.Printf("bad value: %s: %s\n", arg, err) } } // assemble // TODO: get string -> Opcode/AddrMode working again /* case "a", "assemble": if len(args) != 2 { fmt.Printf("usage: a <opcode> <addrmode>\n") return } opcode := dis6502.Opcode(args[0]) addrMode := dis6502.AddrMode(args[1]) // TODO: show all matches fmt.Printf("%.2x\n", dis6502.OpcodeByteFor(opcode, addrMode)) // TODO: read operands and actually assemble and write */ // trace case "t", "trace": // TODO: optional argument of instructions to execute // TODO: Fix, really have to run PPU & CPU in sync..see 'g' deck.CPU.ExecuteInstruction() deck.CPU.DumpRegisters() case "import-cheats": db := cheatdb.Open() filename := strings.Join(args, " ") // TODO: use cmd directly instead of rejoining db.ImportXML(filename) case "analyze-cheats": db := cheatdb.Open() db.AllCarts(func(path string) (bool) { err, ok := deck.Load(path) if !ok { // Not everything is supported yet fmt.Printf("Skipping %s: %s", path, err) return false } return true }, func(code gamegenie.GameGenieCode) (found bool, romAddress uint32, romChip string, romBefore string, romAfter string) { romAddress, romChip = deck.CPU.Address2ROM(code.CPUAddress()) // TODO: refactor 'code' command below that also checks key currentValue := deck.InsertedCartridge.Prg[romAddress] if code.HasKey && currentValue != code.Key { // Some other bank is loaded currently fmt.Printf("This is NOT the ROM address: %.6X, since it currently contains %.2X, but key is %.2X\n", romAddress, currentValue, code.Key) } else { // Read bytes before and after the patched address, for context for i := uint32(0); i < 16; i += 1 { romAfter += fmt.Sprintf("%.2X ", deck.InsertedCartridge.Prg[romAddress + i]) romBefore = fmt.Sprintf("%.2X ", deck.InsertedCartridge.Prg[romAddress - i - 1]) + romBefore } //code.ROMBefore = romBefore //code.ROMAfter = romAfter // TODO: save it for jsdis, in patch table return true, romAddress, romChip, romBefore, romAfter } // Not found return false, romAddress, romChip, romBefore, romAfter }) case "serve-cheats": db := cheatdb.Open() db.Serve() // cheat code case "c", "code": for _, code := range(args) { // TODO: Pro Action Replay/PAR codes (RAM patches), FCEU ("emulator PAR"; 001234 FF) and PAR format (1234 FF) // TODO: ROM patches, complete addresses in PRG chip to patch, more precise than GG // since can specify entire address (XXXXX=XX?) // TODO: maybe CHR ROM patches too? (XXXXX=XX, but with higher address? PRG+CHR) // TODO: Pro Action Rocky (ROM patcher like Game Genie but for Famicom) - see http://www.disgruntleddesigner.com/chrisc/fcrocky.html - need more info patch := gamegenie.Decode(code) fmt.Printf("%s = %s\n", patch, patch.Encode()) fmt.Printf("CPU address: %.4X\n", patch.CPUAddress()) fmt.Printf("Value: %.2X\n", patch.Value) // TODO: apply code // Find out what the CPU address currently maps to in ROM // TODO: option to attempt to do this statically.. the code below will only find what is currently loaded. romAddress, romChip := deck.CPU.Address2ROM(patch.CPUAddress()) currentValue := deck.CPU.ReadFrom(patch.CPUAddress()) if !patch.HasKey || currentValue == patch.Key { fmt.Printf("ROM address found: %.6X\n", romAddress) fmt.Printf("ROM chip: %s\n", romChip) fmt.Printf("iNES offset: %.4X\n", romAddress + 0x10) } else { // The affected part of ROM is not currently loaded fmt.Printf("This is NOT the ROM address: %.6X, since it currently contains %.2X, but key is %.2X\n", romAddress, currentValue, patch.Key) } // TODO: should we also, after finding the ROM address, search for all CPU // addresses it affects, since it may be more than one? For example, 16 KB NROM // games (like Mario Bros.) have codes like SXTIEG = 5ce0:a5 = CPU address dce0, // which affects ROM address 1ce0, since CPU d000-dfff is mapped to ROM 1000-1fff; // however, paching ROM 1ce0 would ALSO affect CPU 9ce0, since CPU 8000-8fff is // mapped there, too. Game Genie, since it uses CPU addresses, can affect each of // these mirrors independently. The code for the mirrored bank is 1ce0:a5=SXTPEG, // but it has no effect because the program in this case runs from c000-ffff not // the mirrored 8000-bfff. // Point is, GG codes can not only be LESS specific than ROM patches (affecting multiple // addresses in the ROM), they can also be MORE specific, since they use the CPU address. // TODO: also should check compare value, to restrict banks } if len(args) == 0 { fmt.Printf("usage: c game-genie-code\n") } // verbose case "v", "show-cycles": deck.CPU.Verbose = true case "show-ppu": deck.PPU.Verbose = true case "b", "show-vblank": deck.PPU.Verbose = true case "V", "show-trace": deck.CPU.InstrTrace = true // TODO: breakpoints // TODO: watch default: fmt.Printf("unknown command: %s\n", cmd) } }