Last active
January 21, 2024 00:20
-
-
Save negative-seven/c03ac3b0cf7ff8493d9e5bd3529ba204 to your computer and use it in GitHub Desktop.
NES Tetris cycle time calculator for reaching corruptable indirect jumps
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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