smbot/smb.lua

339 lines
9.4 KiB
Lua

-- 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,
}