- Shows on-screen tiles +stairs
- Shows tile buffer
- Works in OOB rooms (mostly)
Demosntration at
--|-------------------------------------|--
--| CV3 On-Screen Map Viewer |--
--| For BizHawk 2.9.1 |--
--| NesHawk Core |--
--|-------------------------------------|--
--| Last modified: 2026-02-02 scrm |--
--|-------------------------------------|--
--| ROMs supported: |--
--|-------------------------------------|--
--| Akumajou Densetsu (J) |--
--| Castlevania 3 - Dracula's Curse (U) |--
--|-------------------------------------|--
-- Global Configuration -- Adjust these values to suit your needs --
show_tiles_onscreen = true
show_tile_buffer = true
show_tile_spawn_zone = true
show_replaced_tiles = true
show_stair_data = true
show_ghost_stairs = true
tile_w = 14
tile_h = 14
tile_origin_x = -16
tile_origin_y = 196
-- Functions
function get_game_type()
local board = gameinfo.getboardtype()
if board == "ExROM" then
game_type = "us"
elseif board == "VRC6" then
game_type = "jp"
else
error("Cannot determine game type " .. board)
end
print("Game type is " .. game_type .. ".")
end
function val(us, jp)
if game_type == "jp" then
return jp
else
return us
end
end
-- MODULE Lua Table Util --
function table_key_set(arg)
local result = {}
for _, v in pairs(arg) do
result[v] = true
end
return result
end
-- MODULE Stairs
-- Stair entrance format
--
-- Pointed to by $69 (U) / $66 (J)
--
-- Address Translation ADR - ROM:
-- $B88F -> $1F88F
--
-- Horizontal
-- aa__ yyyy: direction (UR, UL, DL, DR), Y tile
-- xxxx xxxx: X px
-- XXXX XXXX: X high
--
-- Vertical
-- aa__ YYYY: direction (UR, UL, DL, DR), Y high
-- yyyy yyyy: Y px
-- xxxx xxxx: X px
--
-- $FF as the first byte indicates sequence end
--
-- -- The game uses the camera position to find stairs in both horizontal and vertical rooms
local stair_data = {}
UP_RIGHT = 0
UP_LEFT = 1
DOWN_LEFT = 2
DOWN_RIGHT = 3
STAIR_PROPERTIES = {
[UP_RIGHT] = { dx = 1, dy = -1, x_offs = 0, y_offs = 0, marker = { { 1, 1 }, { 6, 1 }, { 6, 6 } } },
[UP_LEFT] = { dx = -1, dy = -1, x_offs = -8, y_offs = 0, marker = { { 2, 1 }, { 7, 1 }, { 2, 6 } } },
[DOWN_LEFT] = { dx = -1, dy = 1, x_offs = -8, y_offs = 8, marker = { { 1, 6 }, { 6, 6 }, { 1, 1 } } },
[DOWN_RIGHT] = { dx = 1, dy = 1, x_offs = 0, y_offs = 8, marker = { { 1, 6 }, { 6, 6 }, { 6, 1 } } }
}
-- Stair Types
STAIR_NORMAL = 0
STAIR_INVALID = 1
STAIR_V_DUPLICATE = 2
function read_byte(adr)
if adr < 0x8000 or adr >= 0xC000 then
return memory.readbyte(adr)
else
return memory.readbyte(0x1C000 | (adr & 0x3FFF), "PRG ROM")
end
end
function stair_data.read(vertical)
-- See https://github.com/vinheim3/castlevania3-disasm/blob/main/code/bank0f.s#L264
local stairs = {}
local stair_data_adr = memory.read_u16_le(val(0x69, 0x66))
if stair_data_adr >= 0x2000 and stair_data_adr < 0x8000 then
-- Stair Pointer points into open bus, no way to predict this
return stairs
end
-- In invalid rooms, the stair pointer may point to invalid stair data, which can cause the game to keep reading
-- the entire range accesssed by the pointer, and then wrap around, reading the whole range again, but offset by 1
-- After three loops through the whole sequence, a failsafe in the game kicks in, keeping the game from locking up
local offs = 0
local stair_type = STAIR_NORMAL
if offs >= 256 or stair_data_adr < 0x8000 then
stair_type = STAIR_INVALID
end
while read_byte(stair_data_adr + (offs & 0xFF)) ~= 0xFF and offs < 768 do
local byte_0 = read_byte(stair_data_adr + (offs & 0xFF))
local byte_1 = read_byte(stair_data_adr + ((offs + 1) & 0xFF))
local byte_2 = read_byte(stair_data_adr + ((offs + 2) & 0xFF))
local staircase = {
flags = (byte_0 & 0xC0) >> 6,
byte_0 = byte_0,
byte_1 = byte_1,
byte_2 = byte_2,
offs = offs & 0xFF,
stair_type = stair_type
}
staircase.down = staircase.flags == DOWN_LEFT or staircase.flags == DOWN_RIGHT
staircase.right = staircase.flags == UP_RIGHT or staircase.flags == DOWN_RIGHT
staircase.up = not staircase.down
staircase.left = not staircase.right
if vertical then
-- The screen is only 15 tiles tall, the camera skips pixels 240 - 256
-- Because of this, subtract 16 pixels for every full byte
staircase.y = ((byte_0 & 0x3F) * 240) + byte_1 + 51
staircase.x = byte_2
else
staircase.y = (byte_0 & 0x0F) * 16
staircase.x = byte_2 * 256 + byte_1
end
table.insert(stairs, staircase)
offs = offs + 3
end
if vertical then
-- A bug in the 16-bit comparison code in vertical rooms seems to cause duplicate
-- stairs one screen below the intended entrance
local duplicate_stairs = {}
for _, staircase in pairs(stairs) do
local duplicate_staircase = {
flags = staircase.flags,
byte_0 = staircase.byte_0,
byte_1 = staircase.byte_1,
byte_2 = staircase.byte_2,
offs = staircase.offs,
x = staircase.x,
y = staircase.y + 240,
stair_type = STAIR_V_DUPLICATE
}
table.insert(duplicate_stairs, duplicate_staircase)
end
for _, duplicate_staircase in pairs(duplicate_stairs) do
table.insert(stairs, duplicate_staircase)
end
end
return stairs
end
-- MODULE Tile val
local tile_val = {}
-- Get all relevant information from the game's RAM
function tile_val.get()
tile_val.gamestate = memory.readbyte(0x18)
tile_val.substate = memory.readbyte(val(0x2A, 0x2C))
tile_val.scroll_mode = memory.readbyte(val(0x68, 0x65))
tile_val.vertical = tile_val.scroll_mode ~= 0
tile_val.horizontal = not tile_val.vertical
tile_val.scroll_down = (tile_val.scroll_mode & 1) == 1
tile_val.scroll_up = not tile_val.scroll_down
tile_val.room_size = memory.read_u8(val(0x71, 0x6E))
tile_val.camera = memory.read_u16_le(val(0x56, 0x53))
tile_val.tile_buffer = {}
if tile_val.vertical then
tile_val.tile_buffer = { w = 16, h = 15, start_adr = 0x6E0, end_adr = 0x760 - 1 }
tile_val.cam_x = 0
-- Camera skips pixels 240 - 255 in vertical rooms, so we correct for this here
tile_val.cam_y = ((tile_val.camera >> 8) * 240) + (tile_val.camera & 0xFF)
else
tile_val.tile_buffer = { w = 24, h = 12, start_adr = 0x6E0, end_adr = 0x770 - 1 }
tile_val.cam_x = tile_val.camera
tile_val.cam_y = 0
end
local size = tile_val.tile_buffer.w * tile_val.tile_buffer.h
tile_val.tile_buffer.tiles = memory.read_bytes_as_dict(tile_val.tile_buffer.start_adr, size)
-- Stair data
tile_val.stair_ptr = memory.read_u16_le(val(0x69, 0x66))
if show_stair_data then
tile_val.stair_data = stair_data.read(tile_val.vertical)
end
tile_val.stair_v_offs = 0
if tile_val.vertical then
-- For some reason, the game adds 51 pixels to the Y position of a stair case in vertical room
-- but then, stairs are also placed one tile lower than expected. Don't ask me.
tile_val.stair_v_offs = -35
end
tile_val.spawn_l_col = memory.read_u8(val(0x59, 0x56))
tile_val.spawn_l_row = memory.read_u8(val(0x5B, 0x58))
tile_val.spawn_r_col = memory.read_u8(val(0x5A, 0x57))
tile_val.spawn_r_row = memory.read_u8(val(0x5C, 0x59))
end
-- MODULE Coords --
function tile_buffer_xy_to_offs(tx, ty)
if tile_val.vertical then
return ty * 8 + (tx // 2)
else
return (tx // 2) * 12 + ty
end
end
function tile_buffer_offs_to_xy(offs)
if tile_val.vertical then
return (offs % 8) * 2, offs // 8
else
return (offs // 12) * 2, offs % 12
end
end
function tile_buffer_xy_to_world(tx, ty)
if tile_val.vertical then
local ty_cam = (tile_val.cam_y // 16) % 15
local dty = ty - ty_cam
if ty_cam > ty then
dty = (15 - ty_cam) + ty
end
return tx * 16, (tile_val.cam_y & 0xFFF0) + dty * 16 + 42
else
local tx_cam = (tile_val.cam_x // 16) % 24
local dtx = tx - tx_cam
if tx_cam > tx then
dtx = (24 - tx_cam) + tx
end
return (tile_val.cam_x & 0xFFF0) + dtx * 16, ty * 16 + 24
end
end
function world_xy_to_tile_buffer_xy(wx, wy)
if tile_val.vertical then
if wy < tile_val.cam_y or wy >= tile_val.cam_y + 240 then
return nil
end
return (wx // 16), (wy // 16) % 15
else
if wy < 24 or wy >= 216 then
return nil
elseif wx < tile_val.cam_x or wx >= tile_val.cam_x + 256 then
return nil
end
return (wx // 16) % 24, (wy - 24) // 16
end
end
function world_xy_to_screen(wx, wy)
return wx - tile_val.cam_x, wy - tile_val.cam_y
end
-- MODULE map viewer
local map_viewer = {}
-- Gets the x, y position to draw on the screen
-- Negative values indicate from the right / bottom edge
function draw_get_pos(x, y, w, h)
if x < 0 then
x = client.screenwidth() + x - (w or 0)
end
if y < 0 then
y = client.screenheight() + y - (h or 0)
end
return x, y
end
-- Definitions
local tile_buffer = {}
local new_tiles = {}
local new_tiles_max_ttl = 4
local draw_x = 0
local draw_y = 0
-- Scans the current offset on the screen for tile collision, simulating the game's assembly code
-- We cannot rely on the tile buffer when the camera is out of bounds due to the LUT accesses
-- seen below
--
-- I am once again indebted to vinheim...
-- https://github.com/vinheim3/castlevania3-disasm/blob/main/code/bank1f.s#L2710
function tile_buffer.get_byte_offs_h(sx, sy)
if sy < 0x20 or sy >= 0xE0 then
return 0
end
local ty = (sy - 0x20) // 16
local wx = tile_val.camera + sx
-- The game wants to determine the row in the meta tile buffer, and does so by
-- by dividing the current screen position by 32 and ORing it with a times-8 table for room
-- If the camera is out of bounds, this can return garbage
local mtx = (wx & 0xFF) // 32
local room_mtx = mtx | memory.readbyte(val(0xFD61, 0xFD62) + (wx >> 8))
-- now the game finds the actual row in the meta tile buffer
-- mod 12 to stay in bounds of the tile buffer...
-- and times 12 to get the actual row offset. the game uses a lookup table for this again...
-- this shouldn't go out of bounds though
local mtx_buf = room_mtx % 12
local mt_buf_offs_row = memory.readbyte(val(0xFD4C, 0xFD4D) + mtx_buf)
-- finally, we can return the actual offs
return mt_buf_offs_row + ty
end
function tile_buffer.get_tile(tx, ty)
local t_offs = tile_buffer_xy_to_offs(tx, ty)
local tiles = tile_val.tile_buffer.tiles[tile_val.tile_buffer.start_adr + t_offs]
if tx % 2 == 0 then
return tiles >> 4
else
return tiles & 0xF
end
end
function tile_buffer.show_all(draw_fun)
-- Sample tile grid at regular intertile_val.
for t = tile_val.tile_buffer.start_adr, tile_val.tile_buffer.end_adr do
local tile = tile_val.tile_buffer.tiles[t]
local tile_l = tile >> 4
local tile_r = tile & 0x0F
local tx, ty = tile_buffer_offs_to_xy(t - tile_val.tile_buffer.start_adr)
draw_fun(tile_l, tx, ty)
draw_fun(tile_r, tx + 1, ty)
end
end
function tile_buffer._draw_tile(tile_type, x, y, fg, bg)
if tile_type ~= 0 then
local x_pos = draw_x + tile_w * x
local y_pos = draw_y + tile_h * y
if not fg and not bg then
fg = TILE_COLORS[tile_type]
end
gui.drawRectangle(x_pos, y_pos, tile_w, tile_h, fg, bg)
end
end
function tile_buffer._draw_grid_lines(x, y, w, h)
gui.drawRectangle(x, y, w, h, 0xFF666666)
if tile_val.vertical then
for i = 0, tile_val.tile_buffer.h / 2 do
local cur_y = y + (tile_h * i * 2)
gui.drawLine(x, cur_y, x + w, cur_y, 0xA0666666)
end
else
for i = 0, tile_val.tile_buffer.w / 2 do
local cur_x = x + (tile_w * i * 4)
gui.drawLine(cur_x, y, cur_x, y + h, 0xA0666666)
end
end
end
function tile_buffer._show_camera_area(x, y, w, h)
local x_end = draw_x + w
local y_end = draw_y + h
if tile_val.vertical then
local cam_start = (tile_val.cam_y % (tile_val.tile_buffer.h * 16)) / 16
local cam_end = (cam_start + 12) % tile_val.tile_buffer.h
if cam_start < cam_end then
gui.drawBox(draw_x, draw_y + cam_start * tile_h, x_end, draw_y + cam_end * tile_h, 0xFF666666, 0x4000CCCC)
else
gui.drawBox(draw_x, draw_y + cam_start * tile_h, x_end, y_end, 0xFF666666, 0x4000CCCC)
gui.drawBox(draw_x, draw_y, x_end, draw_y + cam_end * tile_h, 0xFF666666, 0x4000CCCC)
end
else
local cam_start = (tile_val.cam_x % (tile_val.tile_buffer.w * 16)) / 16
local cam_end = (cam_start + 16) % tile_val.tile_buffer.w
if cam_start < cam_end then
gui.drawBox(draw_x + cam_start * tile_w, draw_y, draw_x + cam_end * tile_w, y_end, 0xFF666666, 0x4000CCCC)
else
gui.drawBox(draw_x + cam_start * tile_w, draw_y, x_end, y_end, 0xFF666666, 0x4000CCCC)
gui.drawBox(draw_x, draw_y, draw_x + cam_end * tile_w, y_end, 0xFF666666, 0x4000CCCC)
end
end
end
function tile_buffer._show_spawn_zone(draw_x, draw_y, w, h)
function draw_block(col, row, color)
col = col % (tile_val.tile_buffer.w / 2)
tile_buffer._draw_tile(1, col * 2, row * 2, 0, color)
tile_buffer._draw_tile(1, col * 2 + 1, row * 2, 0, color)
if row > 0 and row < 6 then
tile_buffer._draw_tile(1, col * 2, row * 2 + 1, 0, color)
tile_buffer._draw_tile(1, col * 2 + 1, row * 2 + 1, 0, color)
end
end
if not show_tile_spawn_zone or not tile_val.horizontal then
return
end
draw_block(tile_val.spawn_l_col, tile_val.spawn_l_row, 0x40C0C0FF)
draw_block(tile_val.spawn_r_col, tile_val.spawn_r_row, 0x40C0FFC0)
end
function tile_buffer.show()
-- Set drawing coordinates
local w = tile_w * tile_val.tile_buffer.w
local h = tile_h * tile_val.tile_buffer.h
draw_x, draw_y = draw_get_pos(tile_origin_x, tile_origin_y, w, h)
-- Draw a dark backdrop
gui.drawRectangle(draw_x - 2, draw_y -2, w + 4, h + 4, nil, 0xC0000000)
tile_buffer._draw_grid_lines(draw_x, draw_y, w, h)
tile_buffer._show_camera_area(draw_x, draw_y, w, h)
tile_buffer._show_spawn_zone(draw_x, draw_y, w, h)
-- Draw stair entrances
if show_stair_data then
for _, staircase in pairs(tile_val.stair_data) do
local is_ghost_staircase = staircase.stair_type == STAIR_V_DUPLICATE
if not is_ghost_staircase or show_ghost_stairs then
local x = staircase.x + STAIR_PROPERTIES[staircase.flags].x_offs
local y = staircase.y + STAIR_PROPERTIES[staircase.flags].y_offs
local tx, ty = world_xy_to_tile_buffer_xy(x, y + tile_val.stair_v_offs)
if tx ~= nil and ty ~= nil then
if staircase.down or tile_val.vertical then
ty = (ty - 1) % tile_val.tile_buffer.h
end
local color = "lime"
if is_ghost_staircase then
color = "yellow"
end
gui.drawRectangle(draw_x + tx * tile_w, draw_y + ty * tile_h, tile_w, tile_h, color, 0)
end
end
end
end
-- Draw tiles
tile_buffer.show_all(tile_buffer._draw_tile)
end
function tile_buffer.get_replaced()
local write_offset = memory.readbyte(0x10)
local x, y = tile_buffer_offs_to_xy(write_offset)
local tiles = {
offset = write_offset,
x = x,
y = y,
ttl = new_tiles_max_ttl
}
new_tiles[write_offset] = tiles
end
function tile_buffer.show_new_tiles()
-- Display the new tiles we collected in the callback
local i, tiles = next(new_tiles, nil)
while i do
local alpha = math.floor((tiles.ttl / new_tiles_max_ttl) * 0x80)
local color = forms.createcolor(0xFF, 0x80, 0x80, alpha)
tile_buffer._draw_tile(1, tiles.x, tiles.y, 0, color)
tile_buffer._draw_tile(1, tiles.x + 1, tiles.y, 0, color)
if client.ispaused() then
tiles.ttl = tiles.ttl - 0.05
else
tiles.ttl = tiles.ttl - 1
end
if tiles.ttl < 0 then
new_tiles[tiles.offset] = nil
end
i, tiles = next(new_tiles, i)
end
end
-- MODULE on-screen-tiles
local on_screen_tiles = {}
local STAIR_COLORS = {
[STAIR_NORMAL] = 0x00FF00,
[STAIR_INVALID] = 0x20E000,
[STAIR_V_DUPLICATE] = 0xD0F000
}
function on_screen_tiles._draw_tile_screen(tile_type, sx, sy)
if tile_type ~= 0 then
gui.drawRectangle(sx, sy, 16, 16, TILE_COLORS[tile_type], nil, "emucore")
end
end
function on_screen_tiles._draw_tile(tile_type, tx, ty)
local wx, wy = tile_buffer_xy_to_world(tx, ty)
local sx, sy = world_xy_to_screen(wx, wy)
-- Depending on which way the game is scrolling, different parts of the screen have collision
-- Only draw tiles if the player can actually interact with them
-- I am not sure if the game checks the screen position or has more sophisticated logic internally
if tile_val.horizontal then
on_screen_tiles._draw_tile_screen(tile_type, sx, sy)
elseif tile_val.scroll_up and sy >= 224 then
on_screen_tiles._draw_tile_screen(tile_type, sx, sy - 240)
elseif not (tile_val.scroll_down and ((sy % 240) < 26)) then
on_screen_tiles._draw_tile_screen(tile_type, sx, sy % 240)
end
end
TILE_COLORS = {
[0] = 0x00000000, -- Empty
[1] = 0xFFB0E080, -- Mud
[2] = 0xFF80E0FF, -- Current Left
[3] = 0xFFE080FF, -- Current Right
[4] = 0xFFFFFFC0, -- Crumble
[5] = 0xFFFF0000, -- Spikes
[6] = 0xFFFFFFFF, -- Solid
[7] = 0xFFFF0000, -- Spikes
[8] = 0xFFFFFFFF, -- Solid
[9] = 0xFFFFFFFF, -- Solid
[10] = 0xFFFFFFFF, -- Solid
[11] = 0xFFFFFFFF, -- Solid
[12] = 0xFFFFFFA8, -- Crumble 0
[13] = 0xFFFFFF90, -- Crumble 1
[14] = 0xFFFFFF78, -- Crumble 2
[15] = 0xFFFFFF60 -- Crumble 3
}
function on_screen_tiles.show()
if tile_val.horizontal then
for wsx = 0, 256, 32 do
for wsy = 32, 224, 16 do
-- Horizontal rooms need special logic because they use LUTs, which can break at
-- high camera positions
local offs = tile_buffer.get_byte_offs_h(wsx, wsy)
local tile = memory.readbyte(tile_val.tile_buffer.start_adr + offs)
local tile_l = tile >> 4
local tile_r = tile & 0x0F
local sx = wsx - (tile_val.cam_x & 0x1F)
local sy = wsy - 8
on_screen_tiles._draw_tile_screen(tile_l, sx, sy)
on_screen_tiles._draw_tile_screen(tile_r, sx + 16, sy)
end
end
else
tile_buffer.show_all(on_screen_tiles._draw_tile)
end
end
function get_world_tile(staircase, wx, wy)
local tx, ty = world_xy_to_tile_buffer_xy(wx, wy + tile_val.stair_v_offs)
if tx == nil or ty == nil then
return nil
end
return tile_buffer.get_tile(tx, ty)
end
function draw_staircase(staircase, num_stairs)
local color_outline = 0xFF000000 | STAIR_COLORS[staircase.stair_type]
local color_fill = 0x30000000 | STAIR_COLORS[staircase.stair_type]
local is_duplicate_staircase = staircase.stair_type == STAIR_V_DUPLICATE
if is_duplicate_staircase and not show_ghost_stairs then
return
end
local stair_overload = num_stairs > 32
local props = STAIR_PROPERTIES[staircase.flags]
local x = staircase.x + props.x_offs
local y = staircase.y + props.y_offs
local start_x, start_y = world_xy_to_screen(x, y)
if stair_overload or is_duplicate_staircase then
start_y = start_y + 8
end
if start_y > 32 then
gui.drawPolygon(props.marker, start_x, start_y - 9, color_outline, color_fill, "emucore")
end
if tile_val.vertical then
if (tile_val.scroll_up and start_y >= 224) or (tile_val.scroll_down and ((start_y % 240) < 26)) then
return
end
end
-- Trace staircase until it hits an obstacle
-- Unless there's way more stairs in the room than there should be
-- In that case, peace out, and just draw the entrances. we do this for both performance and visual clutter
if is_duplicate_staircase then
-- For duplicate staircsaes, we just draw a small mark at the exact entrance height you need to line up your feet with to see it
local x_cur, y_cur = world_xy_to_screen(staircase.x, staircase.y + 6)
gui.drawLine(x_cur - 6, y_cur, x_cur + 6, y_cur, color_outline, "emucore")
gui.drawLine(x_cur, y_cur - 1, x_cur, y_cur + 1, color_outline, "emucore")
return
elseif stair_overload then
return
end
while true do
if tile_val.horizontal and (y < 0 or y >= 240) then
break
elseif tile_val.vertical and (x < 0 or x >= 256) then
break
end
local step_x, step_y = world_xy_to_screen(x, y)
if step_y > 32 then
gui.drawRectangle(step_x, step_y, 8, 8, color_outline, color_fill, "emucore")
gui.drawRectangle(step_x + props.dx * 8, step_y + props.dy * 8, 8, 8, color_outline, color_fill, "emucore")
end
x = x + props.dx * 16
y = y + props.dy * 16
-- check if the tile terminates
local y_check_offs = 0
if staircase.up then
y_check_offs = 8
end
local t_a = get_world_tile(staircase, x, y + y_check_offs)
local t_b = get_world_tile(staircase, x - props.dx * 16, y + y_check_offs)
if (t_a == nil or t_a ~= 0) or (t_b == nil or t_b ~= 0) then
break
end
end
end
function on_screen_tiles.show_stairs()
if not show_stair_data then
return
end
local num_stairs = 0
for _, staircase in pairs(tile_val.stair_data) do
if staircase.stair_type ~= STAIR_V_DUPLICATE then
num_stairs = num_stairs + 1
end
end
for _, staircase in pairs(tile_val.stair_data) do
draw_staircase(staircase, num_stairs)
end
-- Draw Stair Info
local stair_draw_x = 2
local stair_draw_y = 36
local draw_pos = client.transformPoint(stair_draw_x, stair_draw_y)
local open_bus_message = ""
local stair_count = #tile_val.stair_data .. " staircases"
if tile_val.stair_ptr > 0x2000 and tile_val.stair_ptr < 0x8000 then
stair_count = "? staircases (Open bus)"
end
local message = string.format("Stair PTR: $%04X, %s", tile_val.stair_ptr, stair_count)
gui.text(draw_pos.x, draw_pos.y, message)
end
-- Static Data
-- See https://datacrystal.tcrf.net/wiki/Castlevania_III:_Dracula%27s_Curse/RAM_map
-- The other game states don't show the map or do not align the scroll with the camera
local GAMEPLAY_SUBSTATES = table_key_set({ 0x03, 0x05, 0x0A, 0x0B, 0x0C, 0xF, 0x10, 0x11, 0x13, 0x16, 0x19, 0x1A, 0x1B, 0x1C })
-- Game Values
local replaced_tiles_cb_registered = false
-- Main script loop, execute every frame --
function map_viewer.show()
tile_val.get()
-- Check if ingame
if tile_val.gamestate ~= 4 or not GAMEPLAY_SUBSTATES[tile_val.substate] then
return
end
if show_tiles_onscreen then
on_screen_tiles.show()
on_screen_tiles.show_stairs()
end
if show_tile_buffer then
tile_buffer.show()
end
if show_replaced_tiles then
if not replaced_tiles_cb_registered then
event.onmemoryexecute(tile_buffer.get_replaced, val(0xD29E, 0xD273), "cv3_map_viewer_tile_buffer_show_replaced")
replaced_tiles_cb_registered = true
end
tile_buffer.show_new_tiles()
else
event.unregisterbyname("cv3_map_viewer_tile_buffer_show_replaced")
replaced_tiles_cb_registered = false
end
end
-- Game Values
local game_type = nil
-- Start Execution --
console.clear()
gui.clearGraphics("client")
gui.clearGraphics("emucore")
gui.cleartext()
print("Starting CV3 Map viewer...")
get_game_type()
gui.use_surface("client")
-- Main script loop, execute every frame --
while true do
map_viewer.show()
local cur_framecount = emu.framecount()
repeat
emu.yield()
until cur_framecount ~= emu.framecount() or client.ispaused()
gui.clearGraphics("client")
gui.clearGraphics("emucore")
gui.cleartext()
end