-- disassembly used for reference: -- https://gist.githubusercontent.com/1wErt3r/4048722/raw/59e88c0028a58c6d7b9156749230ccac647bc7d4/SMBDIS.ASM local band = bit.band local floor = math.floor local emu = emu local gui = gui local util = require("util") local R = memory.readbyteunsigned local W = memory.writebyte local function S(addr) return util.signbyte(R(addr)) end local valid_tiles = { 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x51, 0x52, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6B, 0x6C, 0x89, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5 } local tile_lut = {} for i, v in ipairs(valid_tiles) do tile_lut[v] = i - 1 end local area_lut = { -- first digit: world number. -- second digit: level number. -- note: excludes pipe intros. [11] = 0, [12] = 2, [13] = 3, [14] = 4, [21] = 0, [22] = 2, [23] = 3, [24] = 4, [31] = 0, [32] = 1, [33] = 2, [34] = 3, [41] = 0, [42] = 2, [43] = 3, [44] = 4, [51] = 0, [52] = 1, [53] = 2, [54] = 3, [61] = 0, [62] = 1, [63] = 2, [64] = 3, [71] = 0, [72] = 2, [73] = 3, [74] = 4, [81] = 0, [82] = 1, [83] = 2, [84] = 3, } local rotation_offsets = { -- FIXME: not all of these are pixel-perfect. 0, -40, -- 0x00 6, -38, 15, -37, 22, -32, 28, -28, 32, -22, 37, -14, 39, -6, 40, 0, -- 0x08 38, 7, 37, 15, 33, 23, 27, 29, 22, 33, 14, 37, 6, 39, 0, 41, -- 0x10 -7, 40, -16, 38, -22, 34, -28, 28, -34, 23, -38, 16, -40, 8, -40, -0, -- 0x18 -40, -6, -38, -14, -34, -22, -28, -28, -22, -32, -16, -36, -8, -38, } -- TODO: reinterface to one "input" array visible to main.lua. local sprite_input = {} local tile_input = {} local extra_input = {} local overlay = false local function get_timer() return R(0x7F8) * 100 + R(0x7F9) * 10 + R(0x7FA) end local function get_score() return R(0x7DE) * 10000 + R(0x7DF) * 1000 + R(0x7E0) * 100 + R(0x7E1) * 10 + R(0x7E2) end local function set_timer(time) W(0x7F8, floor(time / 100)) W(0x7F9, floor((time / 10) % 10)) W(0x7FA, floor(time % 10)) end local function mark_sprite(x, y, t) if t == 0 or x <= -8 or x >= 0xFF or y <= 0 or y >= 0xE8 then -- place unused/unseen sprites x = -8 -- just off the left side of the screen y = 0xC8 -- at ground level. end local cx = 2 * (x - 0x80) -- relative to center of screen. local cy = 2 * (y - 0x88) -- relative to standing on 4th block from floor. if x < 0 or x >= 256 or y < 0 or y > 224 then sprite_input[#sprite_input+1] = 0 sprite_input[#sprite_input+1] = 0 --sprite_input[#sprite_input+1] = 0 else sprite_input[#sprite_input+1] = cx sprite_input[#sprite_input+1] = cy --sprite_input[#sprite_input+1] = t end if overlay and t ~= 0 then gui.box(x-4, y-4, x+4, y+4) gui.text(x-13, y-3-9, ("%+04i"):format(t), '#FFFFFF', '#0000003F') gui.text(x-13, y-3+9, ("%+04i"):format(cx), '#FFFFFF', '#0000003F') end end local function mark_tile(x, y, t) tile_input[#tile_input+1] = tile_lut[t] if t == 0 then return end if overlay then gui.box(x-8, y-8, x+8, y+8) gui.text(x-5, y-3, ("%02X"):format(t), '#FFFFFF', '#00000000') end end local function getxy(i, x_addr, y_addr, pageloc_addr, hipos_addr) local spl_l = R(0x71A) local spl_r = R(0x71B) local sx_l = R(0x71C) local sx_r = R(0x71D) local x = R(x_addr + i) local y = R(y_addr + i) local sx, sy = x, y if pageloc_addr ~= nil then local page = R(pageloc_addr + i) sx = sx - sx_l - (spl_l - page) * 256 else sx = sx - sx_l end if hipos_addr ~= nil then local hipos = S(hipos_addr + i) sy = sy + (hipos - 1) * 256 end return sx, sy end local function paused() return band(R(0x776), 1) end local function get_state() if R(0xE) == 0xFF then return 'power' end if R(0x774) > 0 then return 'lagging' end if R(0x7A2) > 0 then return 'waiting_demo' end if R(0x717) > 0 then return 'playing_demo' end -- if R(0x770) == 0xFF then return 'power' end if paused() ~= 0 then return 'paused' end if R(0xE) == 0 then return 'world_screen' end -- if R(0x712) == 1 then return 'deadmusic' end if R(0x7CA) == 0x94 then return 'dead' end if R(0xE) == 4 then return 'win_flagpole' end if R(0xE) == 5 then return 'win_walking' end if R(0xE) == 6 then return 'lose' end -- if R(0x770) == 0 then return 'not_playing' end if R(0x770) == 2 then return 'win_castle' end if R(0x772) == 2 then return 'no_control' end if R(0x772) == 3 then return 'playing' end if R(0x770) == 1 then return 'loading' end if R(0x770) == 3 then return 'lose' end return 'unknown' end local function advance() emu.frameadvance() while emu.lagged() do emu.frameadvance() end -- skip lag frames. while R(0x774) > 0 do emu.frameadvance() end -- also lag frames. end local function handle_enemies() -- enemies, flagpole for i = 0, 5 do local x, y = getxy(i, 0x87, 0xCF, 0x6E, 0xB6) x, y = x + 8, y + 16 local tid = R(0x16 + i) local flags = R(0xF + i) --local offscr = R(0x3D8 + i) local invisible = tid < 0x10 and flags == 0 if tid == 0x30 then y = y - 8 end -- flagpole flag if tid == 0x31 then y = y - 8 end -- castle flag if tid == 0x16 then x, y = x - 4, y - 12 end -- fireworks if tid >= 0x24 and tid <= 0x29 then x, y = x + 16, y - 12 end -- moving platforms if tid == 0x2D then x, y = x, y end -- bowser (TODO: determine head or body) if tid == 0x15 then x, y = x, y - 12 end -- bowser fire if tid == 0x32 then x, y = x, y - 8 end -- spring -- tid == 0x35 -- toad if tid == 0x1D or tid == 0x1B then -- rotating fire bars x, y = x - 4, y - 12 -- this is a mess... gotta find out its rotation and then project. -- TODO: handle long fire bars too local rot = R(0xA0 + i) --* 0x100 + R(0x58 + i) if overlay then gui.text(x-13, y-3+9, ("%04X"):format(rot), '#FFFFFF', '#0000003F') end local x_off, y_off = rotation_offsets[rot*2+1], rotation_offsets[rot*2+2] x, y = x + x_off, y + y_off end if invisible then mark_sprite(0, 0, 0) else mark_sprite(x, y, tid + 1) end end end local function handle_fireballs() for i = 0, 1 do local x, y = getxy(i, 0x8D, 0xD5, 0x74, 0xBC) x, y = x + 4, y + 4 local state = R(0x24 + i) local invisible = state == 0 if invisible then mark_sprite(0, 0, 0) else mark_sprite(x, y, 257) end end end local function handle_blocks() for i = 0, 3 do local x, y = getxy(i, 0x8F, 0xD7, 0x76, 0xBE) x, y = x + 8, y + 8 local state = R(0x26 + i) local invisible = state == 0 if invisible then mark_sprite(0, 0, 0) else mark_sprite(x, y, 258) end end end local function handle_hammers() -- hammers, coins, score bonus text... for i = 0, 8 do local x, y = getxy(i, 0x93, 0xDB, 0x7A, 0xC2) x, y = x + 8, y + 8 local state = R(0x2A + i) -- skip coin effect states. not interactable; we don't care! if state ~= 0 and state >= 0x30 then mark_sprite(x, y, state + 1) else mark_sprite(0, 0, 0) end end end local function handle_misc() for i = 0, 0 do local x, y = getxy(i, 0x9C, 0xE4, 0x83, 0xCB) x, y = x + 8, y + 8 local state = R(0x33 + i) if state ~= 0 then mark_sprite(x, y, state + 1) else mark_sprite(0, 0, 0) end end end local function handle_tiles() --local tile_col = R(0x6A0) local tile_scroll = floor(R(0x73F) / 16) + R(0x71A) * 16 local tile_scroll_remainder = R(0x73F) % 16 extra_input[#extra_input+1] = tile_scroll_remainder -- for y = 0, 12 do -- afaik the bottom row is always a copy of the second to bottom, -- and the top is always air, so drop those from the inputs: for y = 1, 11 do for x = 0, 16 do local col = (x + tile_scroll) % 32 local t if col < 16 then t = R(0x500 + y * 16 + (col % 16)) else t = R(0x5D0 + y * 16 + (col % 16)) end local sx = x * 16 + 8 - tile_scroll_remainder local sy = y * 16 + 40 mark_tile(sx, sy, t) end end end return { -- TODO: don't expose these; provide interfaces for everything needed. R=R, W=W, S=S, overlay=overlay, valid_tiles=valid_tiles, area_lut=area_lut, sprite_input=sprite_input, tile_input=tile_input, extra_input=extra_input, get_timer=get_timer, get_score=get_score, set_timer=set_timer, mark_sprite=mark_sprite, mark_tile=mark_tile, getxy=getxy, paused=paused, get_state=get_state, advance=advance, handle_enemies=handle_enemies, handle_fireballs=handle_fireballs, handle_blocks=handle_blocks, handle_hammers=handle_hammers, handle_misc=handle_misc, handle_tiles=handle_tiles, }