User File #63367956419308241

Upload All User Files

#63367956419308241 - Böbl (NES) Lua script

Böbl Utility Script.lua
Game: Böbl ( NES, see all files )
336 downloads
Uploaded 5/15/2020 6:38 PM by brunovalads (see all 13)
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