-- EarthBound spawn locator script maybe possibly hopefully (EBSLSMPH) -- -- Requires BizHawk 1.6.0a or newer -- Technically works in 1.5.3, but you don't want to do that -- IMPORTANT: Edit the following value to tell the script the location of -- your EarthBound ROM. If this is not set right, the blue spawn tile boxes -- will not be drawn. local ROM_PATH = "C:\\Users\\Public\\Earthbound (U) [!].smc" -- STILL IMPORTANT: Edit the following value with the size of the header on -- your ROM. If your ROM is 3145728 bytes in size, this should be 0. If your -- ROM is 3146240 bytes in size, this should be 512. If it has something other -- than those two sizes, you have a weird ROM. Anyway, if this value is not -- set right, the blue spawn tile boxes will appear in the wrong places. local HEADER_SIZE = 0 -- Version 0.0.1 - Initial release -- Known stability issues: -- * Due to a bug in BizHawk 1.6.0a, you need to specify the ROM filename and -- header size (this should be fixed in the next BizHawk release, at which -- point, a new version of this script should hopefully be released that -- won't make you set those values anymore) -- * You may see weird things if you rewind (in 1.6.0+) because BizHawk does -- not notify scripts when the emulator state is rewound -- * BizHawk 1.5.3 hangs if you create a savestate while the script is active -- * BizHawk 1.5.3 hangs if you start the script while rewind is active -- * Seriously, don't use this with BizHawk 1.5.3 -- Known accuracy/functional issues: -- * The spawn labels sometimes look weird when you enter a room, with some -- tiles showing as having 4x or 6x spawns. I haven't figured out if this is -- a bug in the script or if the spawns actually do work like that -- * Magic Butterflies sometimes don't follow the expected spawn workflow, -- which can result in some spawns turning red when they shouldn't and also -- in, "Surprise butterfly!" messages in the console -- * Graphics are drawn all the time, including during battles -- * Text is drawn in stupid places when you go through doors -- * Most enemy placement group names are dumb local DRAW_TIME = 181 local NO_SPAWN_FG = 0xFFFFFFFF local NO_SPAWN_BG = 0xFF202020 local SPAWN_FG = 0xFFFF6060 local SPAWN_BG = 0xFF200000 local ACTIVE_BORDER = 0x40404080 local ACTIVE_FILL = 0x40404080 local INACTIVE_BORDER = 0x00000000 local INACTIVE_FILL = 0x00000000 local MIN_X = 0 local MIN_Y = 0 local CENTER_X = 128 local CENTER_Y = 112 local MAX_X = 255 local MAX_Y = 223 local MIN_DRAW_X = 0 local MIN_DRAW_Y = 0 local MAX_DRAW_X = 200 local MAX_DRAW_Y = 214 local last_spawn = nil local spawns = {} -- Names for each of the enemy placement groups (EPGs) in the original EB ROM -- -- TODO Is there a better way to provide this than stuffing it in the script? -- >:/ local EPG_NAMES = { [0] = "3 Crows", "Onett 01", "Onett 02", "Onett 03", "Onett 04", "Onett 05", "Onett 06", "Crow/Snake", "Sharks", "Weak Aliens", "Winters 0a", "Winters 0b", "Goat 0c", "Goat 0d", "Cave Boy/Bear", "4 Antoids", "RShroom 10", "Ant/Shroom", "Twoson 12", "Twoson 13", "RShroom 14", "Shroom/Sprout 15", "Shroom/Sprout 16", "UFO/Sprout", "PRV 18", "PRV 19", "PRV 1a", "PRV 1b", "Oak", "PRV 1d", "PRV 1e", "Cultist 1f", "Cultist 20", "Cultist 21", "Crow", "Twoson 23", "Twoson 24", "Twoson 25", "Hippie", "Threed 27", "Threed 28", "Graveyard 29", "Graveyard 2a", "3 NG Flies", "Dog/Flies", "Frog/Zombie", "GFalls 2e", "GFalls 2f", "GFalls 30", "GFalls 31", "Duck/Roach", "Well 33", "(empty)", "DDD 35", "DDD 36", "DDD 37", "Lady/Reveler", "Fourside 39", "Fourside 3a", "Moonside 3b", "Moonside 3c", "Taxi/Sign", "Guy/Cop", "N Scaraba 3f", "N Scaraba 40", "S Scaraba 41", "S Scaraba 42", "S Scaraba 43", "Oak/4Slugs/Plant", "DDD 45", "DDD 46", "DDD 47", "DDD 48", "Barfs/Fishes", "Oak/6Slugs/Plant", "Wetnosaur", "Underworld 4c", "Underworld 4d", "Underworld 4e", "Underworld 4f", "Dice/Swoosh", "Dice/Swoosh/Kiss", "SwooshKiss/Mols", "Mols/Bombs", "Mouse/Slugs", "Mouse/Slugs/Shroom", "Shroom/Mouse", "Shroom", "Mouse/Slugs", "Mouse/Slugs/Ant", "Slugs/Ant", "Mouse/Ant", "Mouse", "Ant 5d", "Ant 5e", "Ant 5f", "Ant 60", "Ant/Slug/Mouse", "Mole", "Mole/Bat", "Lilliput 64", "Lilliput 65", "Posessor", "Tunnel 67", "Posessor/Zom 68", "Tunnel 69", "Posessor/Zom 6a", "Posessor/Zom 6b", "Roach", "3- Foppies", "6- Foppies", "Farm Zombies", "Belch 70", "Belch 71", "Piles/Flies", "Well 73", "Well 74", "TSprout/Shroom 75", "TSprout/Shroom 76", "TSprout/Shroom 77", "Snake/Noose/Duck 78", "Snake/Noose/Duck 79", "Snake/Ant 7a", "Snake/Ant 7b", "Noose/Duck/Snake 7c", "Noose/Duck/Snake 7d", "Noose/Duck/Snake 7e", "Noose/Ant", "Dept Store 80", "Coffee/Record", "Record/Musica", "(empty)", "Sewer 84", "Sewer 85", "Roaches/Mice", "Mite/Tangoo", "Mite+Tangoo", "Cloud 89", "2 Tangoos", "2 Mites", "Cloud 8c", "2 Menaces", "Cloud 8e", "Man/Spiders 8f", "Man/Spiders 90", "Pyramid 91", "Pyramid 92", "Pyramid 93", "Pyramid 94", "(empty)", "Coffee/Record/Proto", "(empty)", "Cute UFOs", "Clock", "Pump/Plug", "Lesser Mook 9b", "Lesser Mook 9c", "Lesser Mook 9d", "(empty)", "(empty)", "3-5 Protos", "Mook Senior", "Mook/Starman a2", "Mook/Starman a3", "Upper Base a4", "Upper Base a5", "Mid Base a6", "Mid Base a7", "Mid Base a8", "Lower Base a9", "Robot/Octobot", "Fobbies", "Fobby/Sphere", "Fobby/Robo", "Fobby/Spirit", "Spirit/Sphere", "Spirit/Robo", "Spirit/Robo/Sphere", "Robo/Sphere", "Flame/PP", "EE/Flame b4", "EE/Flame b5", "EE/Flame b6", "Flame", "PP/MPP", "EE/PP", "Flame/MPP ba", "Flame/MPP bb", "Early Past", "Late Past", "Sprout/Shroom be", "Sprout bf", "Coil Snake", "Cultist c1", "(empty)", "Skelpion", "(empty)", "Caterpillar", "Worm", "Butterfly", "Mole c8", "Mole c9", "Mole ca" } local DEFAULT_EPG_NAME = "Unknown :(" local function get_epg_name(epg_index) local name = EPG_NAMES[epg_index] if name == nil then name = DEFAULT_EPG_NAME end return name end local function tile_index_to_coords(tile_index) local tile_row = math.floor(tile_index / 256) local tile_column = math.floor((tile_index - (tile_row * 256)) / 2) return {x = (tile_column * 64) + 4, y = (tile_row * 64) + 29} end local function coords_to_tile_index(coords) local tile_row = math.floor(coords.y / 64) local tile_column = math.floor(coords.x / 64) return (256 * tile_row) + (2 * tile_column) end -- Gets the map position of the upper-left-hand corner of the screen. local function get_screen_coords() return {x = mainmemory.read_u16_le(0x9877) - CENTER_X, y = mainmemory.read_u16_le(0x987b) - CENTER_Y} end local function copy_coords(coords) return {x = coords.x, y = coords.y} end -- Buffers the tile-to-EPG mapping table in RAM as a workaround for issues with -- reading from CARTROM. Assumes the ROM is headered. local tile_table = nil local function read_tile_table(path) local num_tiles = coords_to_tile_index({x = 0, y = 10240}) local file = io.open(path, "rb") if file == nil then console.log("Could not open " .. path .. "; giving up on drawing tiles") return end tile_table = {} file:seek("set", 0x101880 + HEADER_SIZE) for i = 0, num_tiles-1, 1 do tile_table[i] = file:read(1) end file:close() end -- Blows up the spawn history when a savestate is loaded so we don't keep -- drawing spawns that haven't happened anymore. local function on_load_state() last_spawn = nil spawns = {} end event.onloadstate(on_load_state, "on_load_state_epg") local function get_last_tile_coords() local offset = emu.getregister("D") local big_x = mainmemory.read_u8(offset + 0x2c) local big_y = mainmemory.read_u8(offset + 0x2e) local tile_index = (big_y * 256) + (big_x * 2) local coords = tile_index_to_coords(tile_index) return {x = coords.x, y = coords.y} end local function get_spawn_key(spawn) return spawn.tile_coords.x + (spawn.tile_coords.y * 0x10000) end -- Triggered when the game is about to randomly decide if it should spawn -- something in an EPG. Records information about the potential spawn. local function on_spawn_test() local offset = emu.getregister("D") local epg_index = mainmemory.read_u8(offset + 0x30) local spawn = {} spawn.epg_index = epg_index spawn.tile_coords = get_last_tile_coords() spawn.spawned = false spawn.draw_time_left = DRAW_TIME local spawn_key = get_spawn_key(spawn) local spawn_queue = spawns[spawn_key] if spawn_queue == nil then spawn_queue = {} spawns[spawn_key] = spawn_queue end table.insert(spawn_queue, spawn) last_spawn = spawn end event.onmemoryexecute(on_spawn_test, 0xc02834, "on_spawn_test") -- Triggered when the game actually spawns something. Updates the most recent -- spawn record to indicate that there really was a spawn. local function on_enemy_spawn() local enemy_type = emu.getregister("A") if last_spawn == nil or last_spawn.draw_time_left <= 0 then -- TODO Figure out why this is sometimes triggered without a preceding -- locate or read. -- TODO Make sure this only happens for magic butterflies. If it's only -- butterflies that do this, it's not a big deal. if enemy_type == 0xe1 then console.log("Surprise butterfly!") else console.log("Unexpectedly spawned enemy type " .. enemy_type) end else last_spawn.spawned = true last_spawn.enemy_type = enemy_type end end event.onmemoryexecute(on_enemy_spawn, 0xc02a0c, "on_enemy_spawn") -- Returns x, y, modifiers. "modifiers" is a string indicating whether the -- given tile coordinates are offscreen. local function get_draw_coords(tile_coords, screen_coords) local x = tile_coords.x - screen_coords.x local y = tile_coords.y - screen_coords.y local modifiers = "" if x < MIN_X then modifiers = modifiers .. "<" elseif x > MAX_X then modifiers = modifiers .. ">" end if y < MIN_Y then modifiers = modifiers .. "^" elseif y > MAX_Y then modifiers = modifiers .. "v" end x = math.min(MAX_DRAW_X, math.max(MIN_DRAW_X, x)) y = math.min(MAX_DRAW_Y, math.max(MIN_DRAW_Y, y)) return x, y, modifiers end local function is_actual_spawn_in_queue(spawn_queue) for idx, spawn in pairs(spawn_queue) do if spawn.spawned then return true end end return false end local function get_draw_colors(spawned) if spawned then return SPAWN_FG, SPAWN_BG else return NO_SPAWN_FG, NO_SPAWN_BG end end local function update_draw_times_left(spawn_queue, spawn_queue_id, parent_set) for idx, spawn in pairs(spawn_queue) do spawn.draw_time_left = spawn.draw_time_left - 1 end while #spawn_queue > 0 do local spawn = spawn_queue[1] if spawn.draw_time_left <= 0 then table.remove(spawn_queue, 1) else break end end if #spawn_queue <= 0 then parent_set[spawn_queue_id] = nil end end local function draw_spawns(spawn_set, screen_coords) for id, spawn_queue in pairs(spawn_set) do local spawned = is_actual_spawn_in_queue(spawn_queue) local head = spawn_queue[1] if head == nil then -- TODO Does this happen anymore or is that bug fixed? console.log("spawn queue for broke for id " .. id) spawn_set[id] = nil else local fg_color, bg_color = get_draw_colors(spawned) local x, y, suffix = get_draw_coords(head.tile_coords, screen_coords) local name = get_epg_name(head.epg_index) local prefix = "" if #spawn_queue > 1 then prefix = tostring(#spawn_queue) .. "x" end gui.text(x, y, prefix .. name .. " " .. suffix, bg_color, fg_color) update_draw_times_left(spawn_queue, id, spawn_set) end end end local function draw_epg_tiles(screen_coords) -- We can't draw anything here if we didn't read the tile file. if tile_table == nil then return end -- TODO You should probably decide if you want to use magic constants or -- declared constants. Just saying. for tile_y = MIN_Y - (screen_coords.y % 64), MAX_Y, 64 do for tile_x = MIN_X - (screen_coords.x % 64), MAX_X, 64 do local left = math.max(tile_x, MIN_X) local top = math.max(tile_y, MIN_Y) local right = math.min(tile_x + 63, MAX_X) local bottom = math.min(tile_y + 63, MAX_Y) local x = tile_x + screen_coords.x local y = tile_y + screen_coords.y if left <= MAX_X and top <= MAX_Y and right >= MIN_X and bottom >= MIN_Y and x >= 0 and y >= 0 and x < 8192 and y < 10240 then local tile_index = coords_to_tile_index({x = x, y = y}) local has_tile = (tile_table[tile_index] ~= "\0") or (tile_table[tile_index + 1] ~= "\0") local border, fill = INACTIVE_BORDER, INACTIVE_FILL if has_tile then border, fill = ACTIVE_BORDER, ACTIVE_FILL end gui.drawBox(left, top, right, bottom, border, fill) end end end end read_tile_table(ROM_PATH) -- -----MAIN LOOP----- -- while true do local screen_coords = get_screen_coords() draw_spawns(spawns, screen_coords) draw_epg_tiles(screen_coords) emu.frameadvance() end