diff options
Diffstat (limited to 'pokemontools/vba/vba.py')
-rw-r--r-- | pokemontools/vba/vba.py | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py new file mode 100644 index 0000000..d7fdf1d --- /dev/null +++ b/pokemontools/vba/vba.py @@ -0,0 +1,527 @@ +# -*- coding: utf-8 -*- +""" +VBA automation +""" + +import os +import sys +import re +import string +from copy import copy + +import unittest + +# for converting bytes to readable text +from pokemontools.chars import chars + +from pokemontools.map_names import map_names + +import keyboard + +# just use a default config for now until the globals are removed completely +import pokemontools.config as conf +config = conf.Config() +project_path = config.path +save_state_path = config.save_state_path +rom_path = config.rom_path + +if not os.path.exists(rom_path): + raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + +import vba_wrapper + +vba = vba_wrapper.VBA(rom_path) +registers = vba_wrapper.core.registers.Registers(vba) + +button_masks = vba_wrapper.core.VBA.button_masks +button_combiner = vba_wrapper.core.VBA.button_combine + +def translate_chars(charz): + result = "" + for each in charz: + result += chars[each] + return result + +def press(buttons, holdsteps=1, aftersteps=1): + """ + Press a button. + + Use steplimit to say for how many steps you want to press + the button (try leaving it at the default, 1). + """ + if hasattr(buttons, "__len__"): + number = button_combiner(buttons) + elif isinstance(buttons, int): + number = buttons + else: + number = buttons + for step_counter in range(0, holdsteps): + Gb.step(number) + + # clear the button press + if aftersteps > 0: + for step_counter in range(0, aftersteps): + Gb.step(0) + +def call(bank, address): + """ + Jumps into a function at a certain address. + + Go into the start menu, pause the game and try call(1, 0x1078) to see a + string printed to the screen. + """ + push = [ + registers.pc, + registers.hl, + registers.de, + registers.bc, + registers.af, + 0x3bb7, + ] + + for value in push: + registers.sp -= 2 + set_memory_at(registers.sp + 1, value >> 8) + set_memory_at(registers.sp, value & 0xFF) + if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: + print "desired memory values: " + str([value & 0xFF, value >> 8] ) + print "actual memory values: " + str(get_memory_range(registers.sp , 2)) + print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(get_memory_at(registers.sp)) + + if bank != 0: + registers["af"] = (bank << 8) | (registers["af"] & 0xFF) + registers["hl"] = address + registers["pc"] = 0x2d63 # FarJump + else: + registers["pc"] = address + +def get_stack(): + """ + Return a list of functions on the stack. + """ + addresses = [] + sp = registers.sp + + for x in range(0, 11): + sp = sp - (2 * x) + hi = get_memory_at(sp + 1) + lo = get_memory_at(sp) + address = ((hi << 8) | lo) + addresses.append(address) + + return addresses + +class crystal: + """ + Just a simple namespace to store a bunch of functions for Pokémon Crystal. + """ + + @staticmethod + def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): + """ + Presses the "A" button when text is done being drawn to screen. + + The `debug` parameter is only useful when debugging this function. It + enables the `max_wait` feature, which causes the function to exit + instead of hanging around. + + The `sfx_limit` parameter is useful for when the player is given an + item during the text. Set it to 1 to not treat the sound as the end of + text. The next loop around it will return to the normal behavior of the + function. + + :param step_size: number of steps per wait loop + :param max_wait: number of wait loops to perform + """ + while max_wait > 0: + hi = get_memory_at(registers.sp + 1) + lo = get_memory_at(registers.sp) + address = ((hi << 8) | lo) + + if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): # 0xaef: + print "pressing, then breaking.. address is: " + str(hex(address)) + + # set CurSFX + set_memory_at(0xc2bf, 0) + + press("a", holdsteps=10, aftersteps=1) + + # check if CurSFX is SFX_READ_TEXT_2 + if get_memory_at(0xc2bf) == 0x8: + print "cursfx is set to SFX_READ_TEXT_2, looping.." + return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) + else: + if sfx_limit > 0: + sfx_limit = sfx_limit - 1 + print "decreasing sfx_limit" + else: + # probably the last textbox in a sequence + print "cursfx is not set to SFX_READ_TEXT_2, so: breaking" + + break + else: + stack = get_stack() + + # yes/no box or the name selection box + if address in range(0xa46, 0xaaf): + print "probably at a yes/no box.. exiting." + break + + # date/time box (day choice) + # 0x47ab is the one from the intro, 0x49ab is the one from mom. + elif 0x47ab in stack or 0x49ab in stack: # was any([x in stack for x in range(0x46EE, 0x47AB)]) + print "probably at a date/time box ? exiting." + break + + # "How many minutes?" selection box + elif 0x4826 in stack: + print "probably at a \"How many minutes?\" box ? exiting." + break + + else: + nstep(step_size) + + # if there is a callback, then call the callback and exit when the + # callback returns True. This is especially useful during the + # OakSpeech intro where textboxes are running constantly, and then + # suddenly the player can move around. One way to detect that is to + # set callback to a function that returns + # "vba.get_memory_at(0xcfb1) != 0". + if callback != None: + result = callback() + if result == True: + print "callback returned True, exiting" + return + + # only useful when debugging. When this is left on, text that + # takes a while to print to screen will cause this function to + # exit. + if debug == True: + max_wait = max_wait - 1 + + if max_wait == 0: + print "max_wait was hit" + + @staticmethod + def walk_through_walls_slow(): + memory = get_memory() + memory[0xC2FA] = 0 + memory[0xC2FB] = 0 + memory[0xC2FC] = 0 + memory[0xC2FD] = 0 + set_memory(memory) + + @staticmethod + def walk_through_walls(): + """ + Lets the player walk all over the map. + + These values are probably reset by some of the map/collision + functions when you move on to a new location, so this needs + to be executed each step/tick if continuous walk-through-walls + is desired. + """ + set_memory_at(0xC2FA, 0) + set_memory_at(0xC2FB, 0) + set_memory_at(0xC2FC, 0) + set_memory_at(0xC2FD, 0) + + #@staticmethod + #def set_enemy_level(level): + # set_memory_at(0xd213, level) + + @staticmethod + def nstep(steplimit=500): + """ + Steps the CPU forward and calls some functions in between each step. + + (For example, to manipulate memory.) This is pretty slow. + """ + for step_counter in range(0, steplimit): + crystal.walk_through_walls() + #call(0x1, 0x1078) + step() + + @staticmethod + def disable_triggers(): + set_memory_at(0x23c4, 0xAF) + set_memory_at(0x23d0, 0xAF); + + @staticmethod + def disable_callbacks(): + set_memory_at(0x23f2, 0xAF) + set_memory_at(0x23fe, 0xAF) + + @staticmethod + def get_map_group_id(): + """ + Returns the current map group. + """ + return get_memory_at(0xdcb5) + + @staticmethod + def get_map_id(): + """ + Returns the map number of the current map. + """ + return get_memory_at(0xdcb6) + + @staticmethod + def get_map_name(): + """ + Figures out the current map name. + """ + map_group_id = crystal.get_map_group_id() + map_id = crystal.get_map_id() + return map_names[map_group_id][map_id]["name"] + + @staticmethod + def get_xy(): + """ + (x, y) coordinates of player on map. + + Relative to top-left corner of map. + """ + x = get_memory_at(0xdcb8) + y = get_memory_at(0xdcb7) + return (x, y) + + @staticmethod + def menu_select(id=1): + """ + Sets the cursor to the given pokemon in the player's party. + + This is under Start -> PKMN. This is useful for selecting a + certain pokemon with fly or another skill. + + This probably works on other menus. + """ + set_memory_at(0xcfa9, id) + + @staticmethod + def is_in_battle(): + """ + Checks whether or not we're in a battle. + """ + return (get_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() + + @staticmethod + def is_in_link_battle(): + return get_memory_at(0xc2dc) != 0 + + @staticmethod + def unlock_flypoints(): + """ + Unlocks different destinations for flying. + + Note: this might start at 0xDCA4 (minus one on all addresses), but not + sure. + """ + set_memory_at(0xDCA5, 0xFF) + set_memory_at(0xDCA6, 0xFF) + set_memory_at(0xDCA7, 0xFF) + set_memory_at(0xDCA8, 0xFF) + + @staticmethod + def get_gender(): + """ + Returns 'male' or 'female'. + """ + gender = get_memory_at(0xD472) + if gender == 0: + return "male" + elif gender == 1: + return "female" + else: + return gender + + @staticmethod + def get_player_name(): + """ + Returns the 7 characters making up the player's name. + """ + bytez = get_memory_range(0xD47D, 7) + name = translate_chars(bytez) + return name + + @staticmethod + def warp(map_group_id, map_id, x, y): + set_memory_at(0xdcb5, map_group_id) + set_memory_at(0xdcb6, map_id) + set_memory_at(0xdcb7, y) + set_memory_at(0xdcb8, x) + set_memory_at(0xd001, 0xFF) + set_memory_at(0xff9f, 0xF1) + set_memory_at(0xd432, 1) + set_memory_at(0xd434, 0 & 251) + + @staticmethod + def warp_pokecenter(): + crystal.warp(1, 1, 3, 3) + crystal.nstep(200) + + @staticmethod + def masterballs(): + # masterball + set_memory_at(0xd8d8, 1) + set_memory_at(0xd8d9, 99) + + # ultraball + set_memory_at(0xd8da, 2) + set_memory_at(0xd8db, 99) + + # pokeballs + set_memory_at(0xd8dc, 5) + set_memory_at(0xd8dd, 99) + + @staticmethod + def get_text(): + """ + Returns alphanumeric text on the screen. + + Other characters will not be shown. + """ + output = "" + tiles = get_memory_range(0xc4a0, 1000) + for each in tiles: + if each in chars.keys(): + thing = chars[each] + acceptable = False + + if len(thing) == 2: + portion = thing[1:] + else: + portion = thing + + if portion in string.printable: + acceptable = True + + if acceptable: + output += thing + + # remove extra whitespace + output = re.sub(" +", " ", output) + output = output.strip() + + return output + + @staticmethod + def keyboard_apply(button_sequence): + """ + Applies a sequence of buttons to the on-screen keyboard. + """ + for buttons in button_sequence: + press(buttons) + nstep(2) + press([]) + + @staticmethod + def write(something="TrAiNeR"): + """ + Types out a word. + + Uses a planning algorithm to do this in the most efficient way possible. + """ + button_sequence = keyboard.plan_typing(something) + crystal.keyboard_apply([[x] for x in button_sequence]) + + @staticmethod + def set_partymon2(): + """ + This causes corruption, so it's not working yet. + """ + memory = get_memory() + memory[0xdcd7] = 2 + memory[0xdcd9] = 0x7 + + memory[0xdd0f] = 0x7 + memory[0xdd10] = 0x1 + + # moves + memory[0xdd11] = 0x1 + memory[0xdd12] = 0x2 + memory[0xdd13] = 0x3 + memory[0xdd14] = 0x4 + + # id + memory[0xdd15] = 0x1 + memory[0xdd16] = 0x2 + + # experience + memory[0xdd17] = 0x2 + memory[0xdd18] = 0x3 + memory[0xdd19] = 0x4 + + # hp + memory[0xdd1a] = 0x5 + memory[0xdd1b] = 0x6 + + # current hp + memory[0xdd31] = 0x10 + memory[0xdd32] = 0x25 + + # max hp + memory[0xdd33] = 0x10 + memory[0xdd34] = 0x40 + + set_memory(memory) + + @staticmethod + def wait_for_script_running(debug=False, limit=1000): + """ + Wait until ScriptRunning isn't -1. + """ + while limit > 0: + if get_memory_at(0xd438) != 255: + print "script is done executing" + return + else: + step() + + if debug: + limit = limit - 1 + + if limit == 0: + print "limit ran out" + + @staticmethod + def move(cmd): + """ + Attempt to move the player. + """ + press(cmd, holdsteps=10, aftersteps=0) + press([]) + + memory = get_memory() + #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: + while memory[0xd043] in [0, 1, 2, 3]: + #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: + nstep(10) + memory = get_memory() + +class TestEmulator(unittest.TestCase): + def test_PlaceString(self): + call(0, 0x1078) + + # where to draw the text + registers["hl"] = 0xc4a0 + + # what text to read from + registers["de"] = 0x1276 + + nstep(10) + + text = crystal.get_text() + + self.assertTrue("TRAINER" in text) + +class TestWriter(unittest.TestCase): + def test_very_basic(self): + button_sequence = keyboard.plan_typing("an") + expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] + + self.assertEqual(len(expected_result), len(button_sequence)) + self.assertEqual(expected_result, button_sequence) + +if __name__ == "__main__": + unittest.main() |