Skip to content

Instantly share code, notes, and snippets.

@negative-seven
Last active January 21, 2024 00:20
Show Gist options
  • Save negative-seven/c03ac3b0cf7ff8493d9e5bd3529ba204 to your computer and use it in GitHub Desktop.
Save negative-seven/c03ac3b0cf7ff8493d9e5bd3529ba204 to your computer and use it in GitHub Desktop.
NES Tetris cycle time calculator for reaching corruptable indirect jumps
--[[
script for the NES game Tetris with support for the NTSC, PAL, and three game cartridge versions, compatible with BizHawk 2.9.1, Mesen 0.9.9, and Mesen 2
on frames where the score addition routine gets run, calculates after how many cycles the "switch_s_plus_2a" subroutine is reached each time
intended to help analyze program counter corruption (game crash/ACE)
the script displays a table on screen showing the number of cycles between reaching the NMI handler and reaching "switch_s_plus_2a", on frames where score is calculated
columns "sw0" to "sw7" refer to the 8 times "switch_s_plus_2a" is reached
the "real" column shows the real cycle times, as measured with breakpoints
the "pred" column shows predicted cycle times, calculated at the start of the NMI based on the console state; the function calculating these contains comments detailing cycle times for particular parts of code
the "simp" column shows simplified cycle times, also calculated at the start of the NMI; the result should be the same as in the "pred" column, but the function has more compact code
the "aprx" column shows approximated cycle times, also calculated at the start of the NMI; the corresponding function is very simplified and makes certain assumptions helpful for crash/ACE analysis purposes
a green number means a cycle time is exactly equal to the real cycle time (non-"real" columns only); an orange number means the approximated time is within the expected error tolerance ("aprx" column only)
things that are not taken into account and may cause variance in game behavior despite numbers being the same:
- the NMI does not happen at a consistent time relative to the "start" of the frame (the current instruction finishes executing before the jump to NMI occurs)
- the length of a frame varies by 1 cycle depending on its parity
not intended to be handled correctly:
- A + B + start + select combo
- left + right + down bug
- pausing
- B-type game
]]
if tastudio then -- bizhawk
event.onexit(function() gui.clearGraphics() end)
rom_hash = gameinfo.getromhash()
read_byte = memory.read_u8
read_word = memory.read_u16_le
get_frame_count = emu.framecount
get_cycles = emu.totalexecutedcycles
function get_stack_pointer()
return emu.getregister('S')
end
function get_select_held()
return joypad.get(1)['Select']
end
function draw_rectangle(x, y, width, height, color)
local color_with_alpha = 0xff000000 | color
gui.drawRectangle(x, y, width, height, color_with_alpha, color_with_alpha)
end
function draw_text(x, y, text, text_color, background_color)
local text_color_with_alpha = 0xff000000 | text_color
local background_color_with_alpha = 0xff000000 | background_color
gui.pixelText(x, y, text, text_color_with_alpha, background_color_with_alpha, 'fceux')
end
function register_callback_execute(address, function_)
event.onmemoryexecute(function_, address)
end
function run_loop(function_)
while true do
function_()
emu.frameadvance()
end
end
else -- mesen
if emu.memCallbackType then -- mesen 0.9.9
function read_byte(address)
return emu.read(address, emu.memType.cpuDebug)
end
function read_word(address)
return emu.readWord(address, emu.memType.cpuDebug)
end
function get_frame_count()
return emu.getState().ppu.frameCount
end
function get_cycles()
return emu.getState().cpu.cycleCount
end
function get_stack_pointer()
return emu.getState().cpu.sp
end
function register_callback_execute(address, function_)
emu.addMemoryCallback(function_, emu.memCallbackType.cpuExec, address)
end
else -- mesen 2
function read_byte(address)
return emu.read(address, emu.memType.nesDebug)
end
function read_word(address)
return emu.readWord(address, emu.memType.nesDebug)
end
function get_frame_count()
return emu.getState()['ppu.frameCount']
end
function get_cycles()
-- in mesen 2, cpu cycle count parity as tracked by oam dma is flipped compared to the other emulators
return emu.getState()['cpu.cycleCount'] + 1
end
function get_stack_pointer()
return emu.getState()['cpu.sp']
end
function register_callback_execute(address, function_)
emu.addMemoryCallback(function_, emu.callbackType.exec, address)
end
end
rom_hash = emu.getRomInfo().fileSha1Hash
function get_select_held()
return emu.getInput(0).select
end
print = emu.log
draw_rectangle = emu.drawRectangle
draw_text = emu.drawString
function run_loop(function_)
emu.addEventCallback(function_, emu.eventType.endFrame)
end
end
if rom_hash == '77747840541BFC62A28A5957692A98C550BD6B2B' then
version = 'ntsc'
elseif rom_hash == '817169B819AADAAE52CCE6B3D8D2FC24270566D7' then
version = 'pal'
elseif rom_hash == 'AAC09B6E7B826276A44B1829EE28EAC32075185B' then
version = 'triple'
else
print('unknown rom hash; ntsc tetris assumed')
version = 'ntsc'
end
RAM_rng_seed = 0x17
RAM_tetriminoY = 0x41
RAM_player1_currentPiece = 0x62
RAM_player1_levelNumber = 0x64
RAM_player1_playState = 0x68
RAM_player1_completedRow = 0x6a
RAM_player1_holdDownPoints = 0x6f
RAM_player1_lines = 0x70
RAM_player1_rowY = 0x72
RAM_player1_score = 0x73
RAM_player1_completedLines = 0x76
RAM_gameModeState = 0xa7
RAM_frameCounter = 0xb1
RAM_allegro = 0xba
RAM_lineClearStatsByType = 0xd8
RAM_display_next_piece = 0xdf
RAM_heldButtons_player1 = 0xf7
RAM_stack = 0x100
RAM_playfield = 0x400
if version == 'triple' then
ROM_orientationTable = 0x8a99
else
ROM_orientationTable = 0x8a9c
end
ROWS = { 'sw0', 'sw1', 'sw2', 'sw3', 'sw4', 'sw5', 'sw6', 'sw7' }
line_clear_frame = false
cycle_count_check = false
real_cycle_counts = {}
function log(string)
print(string.format('f%d: %s', get_frame_count(), string))
end
function callback_nmi()
if cycle_count_check then
local real_pred_equal = true
local pred_simp_equal = true
for _, row in ipairs(ROWS) do
if real_cycle_counts[row] ~= predicted_cycle_counts[row] then
real_pred_equal = false
end
if predicted_cycle_counts[row] ~= simplified_cycle_counts[row] then
pred_simp_equal = false
end
end
if not real_pred_equal then
log('real != pred')
end
if not pred_simp_equal then
log('pred != simp')
end
cycle_count_check = false
end
line_clear_frame =
read_byte(RAM_player1_playState) == 5 -- 0 lines cleared
or ( -- 1-4 lines cleared
read_byte(RAM_player1_playState) == 4
and read_byte(RAM_player1_rowY) == 4
and read_byte(RAM_frameCounter) % 4 == 0
)
if not line_clear_frame then
return
end
real_cycle_counts = { offset = get_cycles() }
predicted_cycle_counts = calculate_predicted_cycle_counts()
simplified_cycle_counts = calculate_simplified_cycle_counts()
approximated_cycle_counts = calculate_approximated_cycle_counts()
cycle_count_check = true
end
function callback_switch_s_plus_2a()
if not line_clear_frame then return end
local jump_table_address = read_word(RAM_stack + get_stack_pointer() + 1)
local game_mode_state = read_byte(RAM_gameModeState)
local row
if
(version == 'ntsc' and jump_table_address == 0x8165)
or (version == 'pal' and jump_table_address == 0x8165)
or (version == 'triple' and jump_table_address == 0x816c)
then -- branchOnGameMode
if game_mode_state == 5 then
row = 'sw0'
elseif game_mode_state == 6 then
row = 'sw2'
elseif game_mode_state == 7 then
row = 'sw4'
elseif game_mode_state == 8 then
row = 'sw6'
end
elseif
(version == 'ntsc' and jump_table_address == 0x819f)
or (version == 'pal' and jump_table_address == 0x819f)
or (version == 'triple' and jump_table_address == 0x81a6)
then -- gameMode_playAndEndingHighScore
if game_mode_state == 5 then
row = 'sw1'
elseif game_mode_state == 6 then
row = 'sw3'
elseif game_mode_state == 7 then
row = 'sw5'
elseif game_mode_state == 8 then
row = 'sw7'
end
elseif
(version == 'ntsc' and jump_table_address == 0x804f)
or (version == 'pal' and jump_table_address == 0x804f)
or (version == 'triple' and jump_table_address == 0x8056)
then -- render
-- ignore
elseif
(version == 'ntsc' and jump_table_address == 0x81b6)
or (version == 'pal' and jump_table_address == 0x81b6)
or (version == 'triple' and jump_table_address == 0x81bd)
then -- branchOnPlayStatePlayer1
-- ignore
else
log(string.format('unexpected jump table address $%0x', jump_table_address))
end
if row then
real_cycle_counts[row] = get_cycles() - real_cycle_counts.offset
end
end
function calculate_predicted_cycle_counts()
local predicted_cycle_counts = {}
local nmi_start_cycle_parity = get_cycles() % 2
local nmi_return_address = read_word(RAM_stack + get_stack_pointer() + 2)
local display_next_piece = read_byte(RAM_display_next_piece) == 0
local completed_row_indices = {
read_byte(RAM_player1_completedRow + 0),
read_byte(RAM_player1_completedRow + 1),
read_byte(RAM_player1_completedRow + 2),
read_byte(RAM_player1_completedRow + 3),
}
local completed_lines = read_byte(RAM_player1_completedLines)
local level = read_byte(RAM_player1_levelNumber)
local rng_0 = read_byte(RAM_rng_seed)
local rng_1 = read_byte(RAM_rng_seed + 1)
local frame_counter = read_byte(RAM_frameCounter)
local hold_down_points = read_byte(RAM_player1_holdDownPoints)
local line_clear_stats = {
read_byte(RAM_lineClearStatsByType + 0),
read_byte(RAM_lineClearStatsByType + 1),
read_byte(RAM_lineClearStatsByType + 2),
read_byte(RAM_lineClearStatsByType + 3),
}
local lines = read_word(RAM_player1_lines)
local score = read_word(RAM_player1_score) + 0x10000 * read_byte(RAM_player1_score + 2)
local current_piece = read_byte(RAM_player1_currentPiece) -- always dummy piece 19 during line clear
local allegro = read_byte(RAM_allegro) ~= 0
local pressed_select = read_byte(RAM_heldButtons_player1) & 0x20 == 0 and get_select_held()
local visible_piece_tiles = 0
for index = 0, 3 do
local y = (read_byte(RAM_tetriminoY) + read_byte(ROM_orientationTable + current_piece * 12 + index * 3)) & 0xff
if y < 0x80 then -- equivalent to y >= 0 where y is a signed byte
visible_piece_tiles = visible_piece_tiles + 1
end
end
local allegro_trigger = nil
for index = 0, 9 do
if read_byte(RAM_playfield + 50 + index) ~= 0xef then
allegro_trigger = index
break
end
end
local cycle = 0 -- count starts at first instruction of nmi handler
if version == 'triple' then
cycle = cycle + 5 -- nmi_jmp: to nmi
end
cycle = cycle + 24 -- nmi: up to and including render call
cycle = cycle + 9 -- render: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
-- render_mode_play_and_demo: to @renderPlayer2Playfield
if completed_lines == 0 then
cycle = cycle + 9 -- render_mode_play_and_demo: to @playStateNotDisplayLineClearingAnimation
cycle = cycle + 965 -- @playStateNotDisplayLineClearingAnimation: to @renderPlayer2Playfield
else
cycle = cycle + 54 -- render_mode_play_and_demo: to jsr updateLineClearingAnimation
-- updateLineClearingAnimation
cycle = cycle + 12 -- to first time to @whileCounter3LessThan4
for i = 0, 3 do
local row = completed_row_indices[i + 1]
if row == 0 then
cycle = cycle + 20
else
if version == 'triple' then
cycle = cycle + 17
if row >= 13 then
cycle = cycle + 1 -- indexed addressing crosses page boundary
end
cycle = cycle + 29
if row >= 12 then
cycle = cycle + 1 -- indexed addressing crosses page boundary
end
cycle = cycle + 68
else
cycle = cycle + 17
if row >= 11 then
cycle = cycle + 1 -- indexed addressing crosses page boundary
end
cycle = cycle + 29
if row >= 11 then
cycle = cycle + 1 -- indexed addressing crosses page boundary
end
cycle = cycle + 69
end
end
if i == 3 then
cycle = cycle + 2 -- bne @whileCounter3LessThan4 branch not taken
cycle = cycle + 23 -- @nextRow: to rts
else
cycle = cycle + 3 -- bne @whileCounter3LessThan4 branch taken
end
end
cycle = cycle + 20 -- render_mode_play_and_demo: to jmp
end
cycle = cycle + 8 -- @renderPlayer2Playfield
cycle = cycle + 8 -- @renderLines
cycle = cycle + 9 -- @renderLevel
cycle = cycle + 15 -- @renderScore
cycle = cycle + 15 -- @renderStats
-- @renderTetrisFlashAndSound: to @setPaletteColor
if completed_lines ~= 4 then
cycle = cycle + 22
else
cycle = cycle + 38
if frame_counter % 8 == 0 then
cycle = cycle + 5
end
end
cycle = cycle + 28 -- @setPaletteColor with return
cycle = cycle + 17 -- nmi: rest of it
cycle = cycle + 6 -- @jumpOverIncrement: jsr copyOamStagingToOam
-- copyOamStagingToOam
cycle = cycle + 13
if cycle % 2 == nmi_start_cycle_parity then
cycle = cycle + 1 -- quirk of $4014 write
end
cycle = cycle + 518
cycle = cycle + 28 -- @jumpOverIncrement: to jsr generateNextPseudorandomNumber
-- generateNextPseudorandomNumber
if (rng_0 ~ rng_1) & 0x2 ~= 0 then
cycle = cycle + 55
else
cycle = cycle + 54
end
-- @jumpOverIncrement: to jsr pollControllerButtons
if version == 'triple' then
cycle = cycle + 9
end
cycle = cycle + 27
cycle = cycle + 882 -- pollControllerButtons
cycle = cycle + 22 -- @jumpOverIncrement: to rti
-- @checkForNmi: to rts
if
(version == 'ntsc' and nmi_return_address == 0xaa37)
or (version == 'pal' and nmi_return_address == 0xaa51)
or (version == 'triple' and nmi_return_address == 0xaa4e)
then
cycle = cycle + 2877
elseif
(version == 'ntsc' and nmi_return_address == 0xaa39)
or (version == 'pal' and nmi_return_address == 0xaa53)
or (version == 'triple' and nmi_return_address == 0xaa50)
then
cycle = cycle + 2880
else
log(string.format('unexpected nmi return address $%0x', nmi_return_address))
end
cycle = cycle + 8 -- @checkForDemoDataExhaustion: to bne @continue
cycle = cycle + 3 -- @continue: jmp
cycle = cycle + 6 -- @mainLoop: jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
-- gameModeState_updateCountersAndNonPlayerState
if version ~= 'triple' then
cycle = cycle + 84
end
cycle = cycle + 21
-- @checkSelectButtonPressed
if pressed_select then -- pressed select
cycle = cycle + 26
display_next_piece = not display_next_piece
else
cycle = cycle + 19
end
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 32 -- gameModeState_handleGameOver
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameModeState_updatePlayer1: jsr makePlayer1Active
cycle = cycle + 509 -- makePlayer1Active
cycle = cycle + 6 -- gameModeState_updatePlayer1: jsr branchOnPlayStatePlayer1
cycle = cycle + 9 -- branchOnPlayStatePlayer1: reach switch_s_plus_2a
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- playState_updateLinesAndStatistics: jsr updateMusicSpeed
cycle = cycle + 10 -- updateMusicSpeed: to first time to @checkForBlockInRow
-- @checkForBlockInRow: to rts
if not allegro_trigger then
if allegro then
cycle = cycle + 209
else
cycle = cycle + 171
end
else
cycle = cycle + 16 * allegro_trigger + 10 -- @checkForBlockInRow: to @foundBlockInRow
-- @foundBlockInRow: to rts
if allegro then
cycle = cycle + 12
else
cycle = cycle + 53
end
end
if completed_lines == 0 then
cycle = cycle + 8 -- playState_updateLinesAndStatistics: to addHoldDownPoints
else
cycle = cycle + 6 -- playState_updateLinesAndStatistics: to @linesCleared
-- @linesCleared: to @noCarry
if line_clear_stats[completed_lines] & 0xf == 0x9 then
cycle = cycle + 34
else
cycle = cycle + 23
end
cycle = cycle + 14 -- @noCarry: to @gameTypeA
cycle = cycle + 3 -- @gameTypeA: to incrementLines
-- incrementLines: to addHoldDownPoints
for i = 0, completed_lines - 1 do
-- incrementLines: to L9BC7
cycle = cycle + 12
lines = lines + 1
if lines & 0xf == 0xa then
cycle = cycle + 16
lines = lines + 6
if lines & 0xf0 == 0xa0 then
cycle = cycle + 15
lines = (lines & 0xff0f) + 0x100
else
cycle = cycle + 3
end
else
cycle = cycle + 3
end
-- L9BC7: to L9BFB
cycle = cycle + 5
if lines & 0xf == 0x0 then
cycle = cycle + 5
-- L9BD0: to L9BFB
cycle = cycle + 58
local target_level = lines >> 4
if (level - target_level) & 0x80 ~= 0 then
cycle = cycle + 21
level = level + 1
else
cycle = cycle + 3
end
else
cycle = cycle + 3
end
-- L9BFB
if i == completed_lines - 1 then
cycle = cycle + 4
else
cycle = cycle + 5
end
end
end
-- addHoldDownPoints: to addLineClearPoints
cycle = cycle + 5
if hold_down_points >= 2 then
score = score + hold_down_points - 1
-- addHoldDownPoints: to L9C18
cycle = cycle + 19
if score & 0xf >= 0xa then
cycle = cycle + 12
score = score + 6
else
cycle = cycle + 3
end
-- L9C18: to L9C27
cycle = cycle + 7
if score & 0xf0 >= 0xa0 then
cycle = cycle + 14
score = score + 0x60
else
cycle = cycle + 3
end
cycle = cycle + 8 -- L9C27: to addLineClearPoints
else
cycle = cycle + 3 -- addHoldDownPoints: to addLineClearPoints
end
cycle = cycle + 16 -- addLineClearPoints: to L9C37
for i = 0, level do
local LINE_CLEAR_POINTS = { 0x0000, 0x0040, 0x0100, 0x0300, 0x1200 }
score = score + LINE_CLEAR_POINTS[completed_lines + 1]
-- L9C37: to L9C4E
cycle = cycle + 21
if score & 0xff >= 0xa0 then
cycle = cycle + 14
score = score + 0x60
else
cycle = cycle + 3
end
-- L9C4E: to L9C64
cycle = cycle + 18
if score & 0xf00 >= 0xa00 then
cycle = cycle + 12
score = score + 0x600
else
cycle = cycle + 3
end
-- L9C64: to L9C75
cycle = cycle + 7
if score & 0xf000 >= 0xa000 then
cycle = cycle + 17
score = score + 0x6000
else
cycle = cycle + 3
end
-- L9C75: to L9C84
cycle = cycle + 7
if score & 0xf0000 >= 0xa0000 then
cycle = cycle + 12
score = score + 0x60000
else
cycle = cycle + 3
end
-- L9C84: to L9C94
cycle = cycle + 7
if score & 0xf00000 >= 0xa00000 then
cycle = cycle + 13
score = 0x999999
else
cycle = cycle + 3
end
-- L9C94
cycle = cycle + 5
if i == level then
cycle = cycle + 26
else
cycle = cycle + 3
end
end
cycle = cycle + 6 -- gameModeState_updatePlayer1: jsr stageSpriteForCurrentPiece
-- stageSpriteForCurrentPiece
cycle = cycle + 512
if current_piece >= 8 then
-- all extra cycles below come from indexed addressing crossing page boundary
if current_piece == 8 then
if version == 'triple' then
cycle = cycle + 1
else
cycle = cycle + 4
end
else
cycle = cycle + 8
end
for i = 1, visible_piece_tiles do
cycle = cycle + 1 -- branching to @validYCoordinate takes 1 cycle more than not branching only in this case, due to indexed addressing crossing a page boundary
end
end
cycle = cycle + 6 -- gameModeState_updatePlayer1: jsr savePlayer1State
cycle = cycle + 495 -- savePlayer1State
cycle = cycle + 6 -- gameModeState_updatePlayer1: jsr stageSpriteForNextPiece
-- stageSpriteForNextPiece
if display_next_piece then
cycle = cycle + 406
else
cycle = cycle + 12
end
cycle = cycle + 11 -- gameModeState_updatePlayer1: to rts
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
predicted_cycle_counts.sw0 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
predicted_cycle_counts.sw1 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 19 -- gameModeState_updatePlayer2
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
predicted_cycle_counts.sw2 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
predicted_cycle_counts.sw3 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 18 -- gameModeState_checkForResetKeyCombo
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
predicted_cycle_counts.sw4 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
predicted_cycle_counts.sw5 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 36 -- gameModeState_startButtonHandling; assumes start is not pressed
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: rts
cycle = cycle + 23 -- @mainLoop: loop back around to jsr branchOnGameMode
cycle = cycle + 9 -- branchOnGameMode: reach switch_s_plus_2a
predicted_cycle_counts.sw6 = cycle
cycle = cycle + 45 -- switch_s_plus_2a
cycle = cycle + 6 -- gameMode_playAndEndingHighScore_jmp: jsr
cycle = cycle + 9 -- gameMode_playAndEndingHighScore: reach switch_s_plus_2a
predicted_cycle_counts.sw7 = cycle
return predicted_cycle_counts
end
function calculate_simplified_cycle_counts()
local simplified_cycle_counts = {}
local nmi_start_cycle_parity = get_cycles() % 2
local nmi_return_address = read_word(RAM_stack + get_stack_pointer() + 2)
local display_next_piece = read_byte(RAM_display_next_piece) == 0
local completed_row_indices = {
read_byte(RAM_player1_completedRow + 0),
read_byte(RAM_player1_completedRow + 1),
read_byte(RAM_player1_completedRow + 2),
read_byte(RAM_player1_completedRow + 3),
}
local completed_lines = read_byte(RAM_player1_completedLines)
local level = read_byte(RAM_player1_levelNumber)
local rng_0 = read_byte(RAM_rng_seed)
local rng_1 = read_byte(RAM_rng_seed + 1)
local frame_counter = read_byte(RAM_frameCounter)
local hold_down_points = read_byte(RAM_player1_holdDownPoints)
local line_clear_stats = {
read_byte(RAM_lineClearStatsByType + 0),
read_byte(RAM_lineClearStatsByType + 1),
read_byte(RAM_lineClearStatsByType + 2),
read_byte(RAM_lineClearStatsByType + 3),
}
local lines = read_word(RAM_player1_lines)
local score = read_word(RAM_player1_score) + 0x10000 * read_byte(RAM_player1_score + 2)
local current_piece = read_byte(RAM_player1_currentPiece) -- always dummy piece 19 during line clear
local allegro = read_byte(RAM_allegro) ~= 0
local pressed_select = read_byte(RAM_heldButtons_player1) & 0x20 == 0 and get_select_held()
local visible_piece_tiles = 0
for index = 0, 3 do
local y = (read_byte(RAM_tetriminoY) + read_byte(ROM_orientationTable + current_piece * 12 + index * 3)) & 0xff
if y < 0x80 then
visible_piece_tiles = visible_piece_tiles + 1
end
end
local allegro_trigger = nil
for index = 0, 9 do
if read_byte(RAM_playfield + 50 + index) ~= 0xef then
allegro_trigger = index
break
end
end
local cycle
if version == 'triple' then
cycle = 7083
else
cycle = 7154
end
if completed_lines == 0 then
cycle = cycle + 774
else
for i = 0, 3 do
local row = completed_row_indices[i + 1]
if row ~= 0 then
if version == 'triple' then
cycle = cycle + 94
if row == 12 then
cycle = cycle + 1
elseif row >= 13 then
cycle = cycle + 2
end
else
cycle = cycle + 95
if row >= 11 then
cycle = cycle + 2
end
end
end
end
if completed_lines == 4 then
cycle = cycle + 16
if frame_counter % 8 == 0 then
cycle = cycle + 5
end
end
end
if cycle % 2 ~= nmi_start_cycle_parity then
cycle = cycle + 1
end
if version == 'triple' then
cycle = cycle + 1
end
if (rng_0 ~ rng_1) & 0x2 ~= 0 then
cycle = cycle + 1
end
if
(version == 'ntsc' and nmi_return_address == 0xaa39)
or (version == 'pal' and nmi_return_address == 0xaa53)
or (version == 'triple' and nmi_return_address == 0xaa50)
then
cycle = cycle + 3
end
if pressed_select then
cycle = cycle + 7
display_next_piece = not display_next_piece
end
if display_next_piece then
cycle = cycle + 394
end
if not allegro_trigger then
cycle = cycle + 149
if allegro then
cycle = cycle + 38
end
else
cycle = cycle + 16 * allegro_trigger
if not allegro then
cycle = cycle + 41
end
end
if completed_lines > 0 then
cycle = cycle + 37
cycle = cycle + completed_lines * 28
lines = lines + completed_lines
if lines & 0xf >= 0xa then
cycle = cycle + 79
lines = lines + 6
if lines & 0xf0 == 0xa0 then
cycle = cycle + 12
lines = (lines & 0xff0f) + 0x100
end
local target_level = lines >> 4
if (level - target_level) & 0x80 ~= 0 then
cycle = cycle + 18
level = level + 1
end
end
if line_clear_stats[completed_lines] & 0xf == 0x9 then
cycle = cycle + 11
end
end
if hold_down_points >= 2 then
cycle = cycle + 37
score = score + hold_down_points - 1
if score & 0xf >= 0xa then
cycle = cycle + 9
score = score + 6
end
if score & 0xf0 >= 0xa0 then
cycle = cycle + 11
score = score + 0x60
end
end
cycle = cycle + 83 * (level + 1)
for i = 0, level do
local LINE_CLEAR_POINTS = { 0x0000, 0x0040, 0x0100, 0x0300, 0x1200 }
score = score + LINE_CLEAR_POINTS[completed_lines + 1]
if score & 0xf0 >= 0xa0 then
cycle = cycle + 11
score = score + 0x60
end
if score & 0xf00 >= 0xa00 then
cycle = cycle + 9
score = score + 0x600
end
if score & 0xf000 >= 0xa000 then
cycle = cycle + 14
score = score + 0x6000
end
if score & 0xf0000 >= 0xa0000 then
cycle = cycle + 9
score = score + 0x60000
end
if score & 0xf00000 >= 0xa00000 then
cycle = cycle + 10
score = 0x999999
end
end
if current_piece == 8 then
if version == 'triple' then
cycle = cycle + 5
else
cycle = cycle + 8
end
elseif current_piece >= 9 then
cycle = cycle + 8 + visible_piece_tiles
end
simplified_cycle_counts.sw0 = cycle
cycle = cycle + 60
simplified_cycle_counts.sw1 = cycle
cycle = cycle + 102
simplified_cycle_counts.sw2 = cycle
cycle = cycle + 60
simplified_cycle_counts.sw3 = cycle
cycle = cycle + 101
simplified_cycle_counts.sw4 = cycle
cycle = cycle + 60
simplified_cycle_counts.sw5 = cycle
cycle = cycle + 119
simplified_cycle_counts.sw6 = cycle
cycle = cycle + 60
simplified_cycle_counts.sw7 = cycle
return simplified_cycle_counts
end
function calculate_approximated_cycle_counts()
--[[
assumptions/simplifications:
- 1 cycle difference caused by nmi start cycle parity ignored
- 1 cycle difference caused by rng function ignored
- 3 cycle difference caused by nmi_return_address ignored
- score is 999999
- either entire placed piece is visible or line is being cleared
]]
local approximated_cycle_counts = {}
local completed_row_indices = {
read_byte(RAM_player1_completedRow + 0),
read_byte(RAM_player1_completedRow + 1),
read_byte(RAM_player1_completedRow + 2),
read_byte(RAM_player1_completedRow + 3),
}
local completed_lines = read_byte(RAM_player1_completedLines)
local frame_counter = read_byte(RAM_frameCounter)
local hold_down_points = read_byte(RAM_player1_holdDownPoints)
local line_clear_stats = {
read_byte(RAM_lineClearStatsByType + 0),
read_byte(RAM_lineClearStatsByType + 1),
read_byte(RAM_lineClearStatsByType + 2),
read_byte(RAM_lineClearStatsByType + 3),
}
local current_piece = read_byte(RAM_player1_currentPiece) -- always dummy piece 19 during line clear
local allegro = read_byte(RAM_allegro) ~= 0
local pressed_select = read_byte(RAM_heldButtons_player1) & 0x20 == 0 and get_select_held()
local display_next_piece = read_byte(RAM_display_next_piece) == 0
if pressed_select then
display_next_piece = not display_next_piece
end
local level = read_byte(RAM_player1_levelNumber)
local lines = read_word(RAM_player1_lines) + completed_lines
local lines_changed_digit_1 = false
local lines_changed_digit_2 = false
local new_level = false
if lines & 0xf >= 0xa then
lines = lines + 6
lines_changed_digit_1 = true
if lines & 0xf0 == 0xa0 then
lines = (lines & 0xff0f) + 0x100
lines_changed_digit_2 = true
end
local target_level = lines >> 4
if (level - target_level) & 0x80 ~= 0 then
level = level + 1
new_level = true
end
end
local allegro_trigger = nil
for index = 0, 9 do
if read_byte(RAM_playfield + 50 + index) ~= 0xef then
allegro_trigger = index
break
end
end
local cycle
if version == 'triple' then
cycle = 7209
else
cycle = 7279
end
if completed_lines > 0 then
for i = 0, 3 do
local row = completed_row_indices[i + 1]
if version == 'triple' then
if row >= 1 and row < 12 then
cycle = cycle + 94
elseif row == 12 then
cycle = cycle + 95
elseif row >= 13 then
cycle = cycle + 96
end
else
if row >= 1 and row < 11 then
cycle = cycle + 95
elseif row >= 11 then
cycle = cycle + 97
end
end
end
end
if pressed_select then
cycle = cycle + 7
end
if display_next_piece then
cycle = cycle + 394
end
if not allegro_trigger then
cycle = cycle + 149
if allegro then
cycle = cycle + 38
end
else
cycle = cycle + 16 * allegro_trigger
if not allegro then
cycle = cycle + 41
end
end
if lines_changed_digit_1 then
cycle = cycle + 79
end
if lines_changed_digit_2 then
cycle = cycle + 12
end
if new_level then
cycle = cycle + 18
end
if completed_lines > 0 and line_clear_stats[completed_lines] & 0xf == 0x9 then
cycle = cycle + 11
end
if hold_down_points >= 2 then
cycle = cycle + 90
if hold_down_points & 0xf >= 0x2 and hold_down_points & 0xf < 0x8 then
cycle = cycle + 9
end
elseif completed_lines == 1 then
cycle = cycle + 53
elseif completed_lines > 1 then
cycle = cycle + 42
end
if completed_lines == 0 then
cycle = cycle + 83 * level + 737
elseif completed_lines == 1 then
cycle = cycle + 136 * level + 28
else
cycle = cycle + 125 * level + completed_lines * 28
if completed_lines == 4 then
cycle = cycle + 16
if frame_counter % 8 == 0 then
cycle = cycle + 5
end
end
end
if current_piece == 8 then
if version == 'triple' then
cycle = cycle + 5
else
cycle = cycle + 8
end
elseif current_piece > 8 then
cycle = cycle + 12
end
approximated_cycle_counts.sw0 = cycle
cycle = cycle + 60
approximated_cycle_counts.sw1 = cycle
cycle = cycle + 102
approximated_cycle_counts.sw2 = cycle
cycle = cycle + 60
approximated_cycle_counts.sw3 = cycle
cycle = cycle + 101
approximated_cycle_counts.sw4 = cycle
cycle = cycle + 60
approximated_cycle_counts.sw5 = cycle
cycle = cycle + 119
approximated_cycle_counts.sw6 = cycle
cycle = cycle + 60
approximated_cycle_counts.sw7 = cycle
return approximated_cycle_counts
end
function draw()
local COLUMNS = {
{
name = 'real',
x = 25,
cycle_counts = real_cycle_counts,
},
{
name = 'pred',
x = 60,
cycle_counts = predicted_cycle_counts,
},
{
name = 'simp',
x = 185,
cycle_counts = simplified_cycle_counts,
},
{
name = 'aprx',
x = 220,
cycle_counts = approximated_cycle_counts,
},
}
if not predicted_cycle_counts then return end
draw_rectangle(2, 30, COLUMNS[2].x - 2 + 30, (#ROWS + 1) * 10, 0x000000, true)
draw_rectangle(COLUMNS[3].x - 3, 30, COLUMNS[4].x - COLUMNS[3].x + 33, (#ROWS + 1) * 10, 0x000000, true)
for _, column in ipairs(COLUMNS) do
draw_text(column.x, 30, column.name, 0xffffff, 0x000000)
end
for i = 0, #ROWS - 1 do
draw_text(2, 40 + i * 10, ROWS[i + 1], 0xffffff, 0x000000)
for _, column in ipairs(COLUMNS) do
if column.cycle_counts == real_cycle_counts then
text_color = 0xffffff
else
local count = column.cycle_counts[ROWS[i + 1]]
local real_count = real_cycle_counts[ROWS[i + 1]]
if count == real_count then
text_color = 0x00ff00
elseif not count or not real_count then
text_color = 0xff0000
elseif column.cycle_counts == approximated_cycle_counts and real_count < count and count - real_count <= 5 then
text_color = 0xf0a030
else
text_color = 0xff0000
end
end
if column.cycle_counts[ROWS[i + 1]] then
text = column.cycle_counts[ROWS[i + 1]]
else
text = '-'
end
draw_text(column.x, 40 + 10 * i, text, text_color, 0x000000)
end
end
end
if version == 'ntsc' then
register_callback_execute(0x8005, callback_nmi)
register_callback_execute(0xac82, callback_switch_s_plus_2a)
elseif version == 'pal' then
register_callback_execute(0x8005, callback_nmi)
register_callback_execute(0xac9c, callback_switch_s_plus_2a)
elseif version == 'triple' then
register_callback_execute(0xd703, callback_nmi)
register_callback_execute(0xac99, callback_switch_s_plus_2a)
end
run_loop(draw)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment