Lua script for the homebrew NES game Böbl. Must be used on BizHawk only!
-- ###############################
-- ## ##
-- ## Böbl (NES) Utility Script ##
-- ## ##
-- ## for BizHawk only! ##
-- ## ##
-- ###############################
--- GAME AND SCRIPT UTILITIES ---
-- Script options
local OPTIONS = {
left_gap = 120,
top_gap = 30,
right_gap = 10,
bottom_gap = 10,
}
-- RAM memory mapping
local RAM = {
x_pos = 0x02CF, -- or 0x0007
x_subpos = 0x02E4,
y_pos = 0x0323, -- or 0x0008
y_subpos = 0x0338,
x_speed = 0x02F9,
x_subspeed = 0x030E,
y_speed = 0x034D,
y_subspeed = 0x0362,
status = 0x040A,
game_mode = 0x0056, -- 01 = player active, 02 = paused, 03 = getting powerup, 04 = main menu, 05 = screen transition
room_x = 0x0049,
room_y = 0x004A,
powerup = 0x0057,
can_double_jump = 0x003D,
effective_frame_counter = 0x0011,
frame_counter = 0x005A,
respawn_room_x = 0x0447,
respawn_room_y = 0x0448,
respawn_x = 0x0449,
respawn_y = 0x044A,
controller_first_frame = 0x000C,
controller_curr = 0x000D,
on_water = 0x003B,
water_type = 0x003A,
powerup_timer = 0x044F,
palette = 0x0200, -- seems to go up to 0x021F, needs more investigation
some_tileset_pointer1 = 0x0027, -- 2 bytes?
some_tileset_pointer2 = 0x0029, -- 2 bytes?
some_tileset_pointer3 = 0x002B, -- 2 bytes?
some_tileset_pointer4 = 0x002D, -- 2 bytes?
some_tileset_pointer5 = 0x002F, -- 2 bytes?
some_tileset_pointer6 = 0x0031, -- 2 bytes?
music = 0x0435,
tilemap = 0x0510, -- 0xF0 bytes, up to 0x05FF
water_wave_offsets = 0x0600, -- 0x100 bytes, up to 0x06FF
--[[ --TODO: investigate these tables, these addresses are exactly for the Bubble, since it's the sprite in slot #00
C989: 9D BA 02 STA $02BA,X
C98C: 9D E4 02 STA $02E4,X
C98F: 9D F9 02 STA $02F9,X
C992: 9D 0E 03 STA $030E,X
C995: 9D 38 03 STA $0338,X
C998: 9D 4D 03 STA $034D,X
C99B: 9D 62 03 STA $0362,X
C99E: 9D 0A 04 STA $040A,X
C9A1: 9D 1F 04 STA $041F,X
C9A4: 9D 8C 03 STA $038C,X
C9A7: 9D CB 03 STA $03CB,X
C9AA: 9D E0 03 STA $03E0,X
C9AD: 9D F5 03 STA $03F5,X
]]
}
-- Some renaming
local fmt = string.format
local floor = math.floor
local ceil = math.ceil
local sqrt = math.sqrt
local sin = math.sin
local cos = math.cos
local pi = math.pi
-- General emulation info
local Movie_active, Readonly, Framecount, Lagcount, Rerecords, Is_lagged
local Lastframe_emulated, Nextframe
local function bizhawk_status()
Movie_active = movie.isloaded()
Readonly = movie.getreadonly()
Framecount = movie.length()
Lagcount = emu.lagcount()
Rerecords = movie.getrerecordcount()
Is_lagged = emu.islagged()
Lastframe_emulated = emu.framecount()
Nextframe = Lastframe_emulated + 1
end
-- Get screen dimensions of the game and emulator
local Scale_x, Scale_y
local Screen_width, Screen_height
local Buffer_width, Buffer_height, Buffer_middle_x, Buffer_middle_y
local Border_right_start, Border_bottom_start
local BizHawk_font_width, BizHawk_font_height = 10, 18
local function bizhawk_screen_info()
if client.borderwidth() == 0 then -- to avoid division by zero bug when borders are not yet ready when loading the script
Scale_x = 2
Scale_y = 2
else
Scale_x = math.min(client.borderwidth()/OPTIONS.left_gap, client.borderheight()/OPTIONS.top_gap) -- Pixel scale
Scale_y = Scale_x -- assumming square pixels only
end
Screen_width = client.screenwidth()/Scale_x -- Emu screen width CONVERTED to game pixels
Screen_height = client.screenheight()/Scale_y -- Emu screen height CONVERTED to game pixels
Buffer_width = client.bufferwidth() -- Game area width, in game pixels
Buffer_height = client.bufferheight() -- Game area height, in game pixels
Buffer_middle_x = OPTIONS.left_gap + Buffer_width/2 -- Game area middle x relative to emu window, in game pixels
Buffer_middle_y = OPTIONS.top_gap + Buffer_height/2 -- Game area middle y relative to emu window, in game pixels
Border_right_start = OPTIONS.left_gap + Buffer_width
Border_bottom_start = OPTIONS.top_gap + Buffer_height
BizHawk_font_width = 10/Scale_x -- to make compatible to the scale
BizHawk_font_height = 18/Scale_y
end
-- Scaling text drawing function
local function draw_text(x_pos, y_pos, text, text_color, bg_color)
gui.text(Scale_x*x_pos, Scale_y*y_pos, text, text_color, bg_color)
end
-- Drawing function renaming
local draw = {
box = gui.drawBox,
ellipse = gui.drawEllipse,
image = gui.drawImage,
image_region = gui.drawImageRegion,
line = gui.drawLine,
cross = gui.drawAxis,
pixel = gui.drawPixel,
polygon = gui.drawPolygon,
rectangle = gui.drawRectangle,
text = draw_text,
pixel_text = gui.pixelText,
}
-- Memory read/write functions
local u8 = mainmemory.read_u8
local s8 = mainmemory.read_s8
local w8 = mainmemory.write_u8
local u16 = mainmemory.read_u16_le
local s16 = mainmemory.read_s16_le
local w16 = mainmemory.write_u16_le
-- Returns frames-time conversion
local function frame_time(frame)
local total_seconds = frame/60.098813897441
local hours = floor(total_seconds/3600)
local tmp = total_seconds - 3600*hours
local minutes = floor(tmp/60)
tmp = tmp - 60*minutes
local seconds = floor(tmp)
local miliseconds = 1000* (total_seconds%1)
if hours == 0 then hours = "" else hours = string.format("%d:", hours) end
local str = string.format("%s%02d:%02d.%03.0f", hours, minutes, seconds, miliseconds)
return str
end
-- Display every useful info
local function display_info()
--- Player info
local x_pos = u8(RAM.x_pos)
local x_subpos = u8(RAM.x_subpos)
local y_pos = u8(RAM.y_pos)
local y_subpos = u8(RAM.y_subpos)
local x_speed = u8(RAM.x_speed)
local x_subspeed = u8(RAM.x_subspeed)
local y_speed = u8(RAM.y_speed)
local y_subspeed = u8(RAM.y_subspeed)
local status = u8(RAM.status)
local powerup = u8(RAM.powerup)
local on_water = u8(RAM.on_water)
local water_type = u8(RAM.water_type)
local can_double_jump = u8(RAM.can_double_jump)
local respawn_room_x = u8(RAM.respawn_room_x)
local respawn_room_y = u8(RAM.respawn_room_y)
local respawn_x = u8(RAM.respawn_x)
local respawn_y = u8(RAM.respawn_y)
local powerup_timer = u8(RAM.powerup_timer)
local i = 0
local table_x = 2
local table_y = OPTIONS.top_gap
local delta_x = BizHawk_font_width
local delta_y = BizHawk_font_height
draw.text(table_x, table_y + i*delta_y, fmt("Pos (%02X.%02x, %02X.%02x)", x_pos, x_subpos, y_pos, y_subpos))
draw.cross(OPTIONS.left_gap + x_pos, OPTIONS.top_gap + y_pos, 3, "red")
draw.cross(OPTIONS.left_gap + u8(0x007F), OPTIONS.top_gap + u8(0x0080), 3, "orange") -- TODO: figure out what's this
i = i + 1
local x_spd_str = ""
if bit.check(x_speed, 7) then -- then negative speed
x_spd_str = fmt("-%02X.%02x", 0xFF - x_speed + floor((0x100 - x_subspeed)/0x100), bit.band(0x100 - x_subspeed, 0xFF))
else -- then positive speed
x_spd_str = fmt("+%02X.%02x", x_speed, x_subspeed)
end
local y_spd_str = ""
if bit.check(y_speed, 7) then -- then negative speed
y_spd_str = fmt("-%02X.%02x", 0xFF - y_speed + floor((0x100 - y_subspeed)/0x100), bit.band(0x100 - y_subspeed, 0xFF))
else -- then positive speed
y_spd_str = fmt("+%02X.%02x", y_speed, y_subspeed)
end
draw.text(table_x, table_y + i*delta_y, fmt("Spd (%s, %s)", x_spd_str, y_spd_str))
i = i + 1
draw.text(table_x, table_y + i*delta_y, fmt("Status: %02X", status))
i = i + 1
draw.text(table_x, table_y + i*delta_y, "Powerup: | |")
draw.text(table_x + 9*BizHawk_font_width, table_y + i*delta_y, "Dive Iron Jump", 0x40FFFFFF)
if bit.check(powerup, 1) then draw.text(table_x + 9*BizHawk_font_width, table_y + i*delta_y, "Dive") end
if bit.check(powerup, 0) then draw.text(table_x + 14*BizHawk_font_width, table_y + i*delta_y, "Iron") end
if bit.check(powerup, 2) then draw.text(table_x + 19*BizHawk_font_width, table_y + i*delta_y, "Jump") end
i = i + 1
local water_type_str = {[00] = "no", [01] = "normal", [03] = "fall", [05] = "current (R)", [07] = "fall corner (R)", [09] = "current (L)", [0xB] = "fall corner (L)"}
draw.text(table_x, table_y + i*delta_y, fmt("On water: %s", on_water == 0 and "no" or water_type_str[water_type]))
i = i + 1
if bit.check(powerup, 2) then
draw.text(table_x, table_y + i*delta_y, fmt("Can double jump: %s", can_double_jump > 0 and "yes" or "no"))
i = i + 1
end
i = i + 1
draw.text(table_x, table_y + i*delta_y,fmt("Respawn (%02X, %02X)", respawn_room_x, respawn_room_y))
i = i + 1
if powerup_timer > 0 then
i = i + 1
draw.text(table_x, table_y + i*delta_y,fmt("Powerup timer: %d", powerup_timer))
i = i + 1
end
--- General info
local room_x = u8(RAM.room_x)
local room_y = u8(RAM.room_y)
local frame_counter = u8(RAM.frame_counter)
local effective_frame_counter = u8(RAM.effective_frame_counter)
local game_mode = u8(RAM.game_mode)
local room_str = fmt("Room (%02X, %02X)", room_x, room_y)
draw.text(Buffer_middle_x - string.len(room_str)*BizHawk_font_width/2, OPTIONS.top_gap - BizHawk_font_height, room_str)
local frame_mode_str = fmt("Frame (%02X, %02X) Mode (%02X)", effective_frame_counter, frame_counter, game_mode)
draw.text(Screen_width - string.len(frame_mode_str)*BizHawk_font_width, 4, frame_mode_str)
--- Tilemap info -- actually 16x16 meta-tiles, formed of 4 8x8 tiles, each region of the map has a specific tileset
--[[
local tile_id
for y = 0, 14 do
for x = 0, 15 do
draw.rectangle(OPTIONS.left_gap + x*16, OPTIONS.top_gap + y*16, 15, 15, 0x80FFFFFF)
tile_id = u8(RAM.tilemap + y*16 + x)
--w8(RAM.tilemap + y*16 + x, y*16 + x) -- REMOVE/TESTS
draw.text(OPTIONS.left_gap + x*16 + 2.5, OPTIONS.top_gap + y*16, fmt("%02X", tile_id), 0xA0FFFFFF)
end
end
]]
--- Movie info
local width = BizHawk_font_width
local x_text, y_text = 8*width, 4
local rec_color = (Readonly or not Movie_active) and "white" or "red"
-- Read-only or read-write?
local movie_type = (not Movie_active and "No movie ") or (Readonly and "Movie " or "REC")
draw.text(x_text, y_text, movie_type, rec_color)
x_text = x_text + width*(string.len(movie_type) + 1)
-- Frame count
local movie_info
if Readonly and Movie_active then
movie_info = fmt("%d/%d", Lastframe_emulated, Framecount)
else
movie_info = fmt("%d", Lastframe_emulated)
end
draw.text(x_text, y_text, movie_info) -- Shows the latest frame emulated, not the frame being run now
x_text = x_text + width*string.len(movie_info)
if Movie_active then
-- Rerecord count
local rr_info = fmt(" %d ", Rerecords)
draw.text(x_text, y_text, rr_info, 0x80FFFFFF)
x_text = x_text + width*string.len(rr_info)
-- Lag count
draw.text(x_text, y_text, Lagcount, "red")
x_text = x_text + width*string.len(Lagcount)
end
-- Time
local time_str = frame_time(Lastframe_emulated) -- Shows the latest frame emulated, not the frame being run now
draw.text(x_text, y_text, fmt(" (%s)", time_str))
end
--- MAIN ---
-- Create lateral gaps
client.SetGameExtraPadding(OPTIONS.left_gap, OPTIONS.top_gap, OPTIONS.right_gap, OPTIONS.bottom_gap)
-- Game check
local check = ""
for i = 0, 4 do
check = check .. string.char(memory.read_u8(0xFE11 + i, "PRG ROM"))
end
if check ~= "QUACK" then error("\n\nThis script is meant to be used with Bobl (NES) only!") end
-- Load confirmation
print("\n\nBobl script loaded successfully.\n")
-- Main loop
while true do
-- Load enviroment
bizhawk_status()
bizhawk_screen_info()
-- Drawings
display_info()
emu.frameadvance()
end