From b477373b5b1b4daabdce59ab08805df06b26eae3 Mon Sep 17 00:00:00 2001 From: Michael Miceli Date: Tue, 13 May 2025 21:32:51 -0400 Subject: [PATCH] document 2 player cart scroll bug. Thanks @archycoffee Additionally, added mesen2-compatible lua scripts. --- docs/Bugs.md | 34 +++++- docs/lua_scripts/mesen/Enemy Debug.lua | 6 +- .../mesen/Show Background Collisions.lua | 4 +- docs/lua_scripts/mesen2/Always Invincible.lua | 9 ++ docs/lua_scripts/mesen2/Enemy Debug.lua | 33 ++++++ .../mesen2/Show Background Collisions.lua | 72 ++++++++++++ .../mesen2/Show Enemy Positions.lua | 106 ++++++++++++++++++ docs/lua_scripts/mesen2/Show HUD Info.lua | 27 +++++ src/bank7.asm | 7 +- 9 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 docs/lua_scripts/mesen2/Always Invincible.lua create mode 100644 docs/lua_scripts/mesen2/Enemy Debug.lua create mode 100644 docs/lua_scripts/mesen2/Show Background Collisions.lua create mode 100644 docs/lua_scripts/mesen2/Show Enemy Positions.lua create mode 100644 docs/lua_scripts/mesen2/Show HUD Info.lua diff --git a/docs/Bugs.md b/docs/Bugs.md index 0ab2cd6..c4db8de 100644 --- a/docs/Bugs.md +++ b/docs/Bugs.md @@ -222,4 +222,36 @@ updating each orb on the arm to be begin its self destruct routine. However, that never happens. So, the shoulder doesn't know to destroy itself. Instead the shoulder operates as if it wasn't destroyed and when it decides that a projectile should be created, that overwrites the hand with a different enemy -type, and clears all the links between the hand and the arm. \ No newline at end of file +type, and clears all the links between the hand and the arm. + +# 5. 2 Player Moving Cart Scroll Bug + +When playing a 2 player game, _Contra_ will ensure that if one player is causing +a scroll, the other player's position on the screen is compensated so that, +relatively speaking, the player stays in the same location in the level. The +game logic assumes that for horizontal scrolling, the scroll speed will always +be 1px per frame. However, this isn't true when a player is moving to the right +while standing on a moving cart in level 7 hangar. When a player is moving +right while on a moving cart and causing scroll, the scroll speed will actually +be 2px per frame. When this happens, the player that is not causing scroll will +not have their X position properly adjusted and will be 'dragged' forward at a +rate of 1px per frame. Thanks to @archycoffee for reporting this bug. + +This bug occurs in the `scroll_player` logic. + +``` +scroll_player: + ... + dec SPRITE_X_POS,x ; move player that isn't causing scroll back +``` + +Below is the corrected implementation. + +``` +scroll_player: + ... + lda SPRITE_X_POS,x + sec + sbc FRAME_SCROLL + sta SPRITE_X_POS,x +``` \ No newline at end of file diff --git a/docs/lua_scripts/mesen/Enemy Debug.lua b/docs/lua_scripts/mesen/Enemy Debug.lua index 327d2b4..aa951ba 100644 --- a/docs/lua_scripts/mesen/Enemy Debug.lua +++ b/docs/lua_scripts/mesen/Enemy Debug.lua @@ -1,6 +1,6 @@ -- used for testing to show enemy data -function display_enemy_data() +function Main() for i = 0,0xf do local ENEMY_X_POS = emu.read(0x033e + i, emu.memType.cpu) local ENEMY_Y_POS = emu.read(0x0324 + i, emu.memType.cpu) @@ -30,8 +30,4 @@ function display_enemy_data() end end -function Main() - display_enemy_data() -end - emu.addEventCallback(Main, emu.eventType.endFrame) \ No newline at end of file diff --git a/docs/lua_scripts/mesen/Show Background Collisions.lua b/docs/lua_scripts/mesen/Show Background Collisions.lua index 5635e23..d621adb 100644 --- a/docs/lua_scripts/mesen/Show Background Collisions.lua +++ b/docs/lua_scripts/mesen/Show Background Collisions.lua @@ -43,8 +43,8 @@ function get_bg_collision(x, y) collisionCode = collisionCode & 0x03; local floorColor = 0x508fbc8f - local waterColor = 0x500096FF - local solidColor = 0x50A9A9A9 + local waterColor = 0x500096ff + local solidColor = 0x50a9a9a9 local tileColor = 0x0 if collisionCode == 0x01 then tileColor = floorColor diff --git a/docs/lua_scripts/mesen2/Always Invincible.lua b/docs/lua_scripts/mesen2/Always Invincible.lua new file mode 100644 index 0000000..4724e58 --- /dev/null +++ b/docs/lua_scripts/mesen2/Always Invincible.lua @@ -0,0 +1,9 @@ +function Main() + -- 0xb0/b1 in memory store the amount of time the B weapon (barrier) lasts. + -- By continually overwriting the timer to the max value (FF), the players + -- will always be invincible + emu.write(0x00b0, 0xff, emu.memType.nesMemory) -- player 1 + emu.write(0x00b1, 0xff, emu.memType.nesMemory) -- player 1 +end + +emu.addEventCallback(Main, emu.eventType.endFrame) \ No newline at end of file diff --git a/docs/lua_scripts/mesen2/Enemy Debug.lua b/docs/lua_scripts/mesen2/Enemy Debug.lua new file mode 100644 index 0000000..9343a64 --- /dev/null +++ b/docs/lua_scripts/mesen2/Enemy Debug.lua @@ -0,0 +1,33 @@ +-- used for testing to show enemy data + +function Main() + for i = 0,0xf do + local ENEMY_X_POS = emu.read(0x033e + i, emu.memType.nesMemory) + local ENEMY_Y_POS = emu.read(0x0324 + i, emu.memType.nesMemory) + local ENEMY_ANIMATION_DELAY = emu.read(0x0538 + i, emu.memType.nesMemory) + local ENEMY_ATTRIBUTES = emu.read(0x05a8 + i, emu.memType.nesMemory) + local ENEMY_FRAME = emu.read(0x0568 + i, emu.memType.nesMemory) + local ENEMY_HP = emu.read(0x0578 + i, emu.memType.nesMemory) + local ENEMY_STATE_WIDTH = emu.read(0x0598 + i, emu.memType.nesMemory) + local ENEMY_VAR_A = emu.read(0x0548 + i, emu.memType.nesMemory) + local ENEMY_VAR_B = emu.read(0x0558 + i, emu.memType.nesMemory) + local ENEMY_VAR_1 = emu.read(0x05b8 + i, emu.memType.nesMemory) + local ENEMY_VAR_2 = emu.read(0x05c8 + i, emu.memType.nesMemory) + local ENEMY_VAR_3 = emu.read(0x05d8 + i, emu.memType.nesMemory) + local ENEMY_VAR_4 = emu.read(0x05e8 + i, emu.memType.nesMemory) + local ENEMY_SPRITES = emu.read(0x030a + i, emu.memType.nesMemory) + local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.nesMemory) + local ENEMY_ATTACK_DELAY = ENEMY_VAR_B + local ENEMY_X_VELOCITY_FAST = emu.read(0x0508 + i, emu.memType.nesMemory) + local ENEMY_X_VELOCITY_FRACT = emu.read(0x0518 + i, emu.memType.nesMemory) + local ENEMY_X_VEL_ACCUM = emu.read(0x04d8 + i, emu.memType.nesMemory) + local ENEMY_Y_VELOCITY_FAST = emu.read(0x04e8 + i, emu.memType.nesMemory) + local ENEMY_Y_VELOCITY_FRACT = emu.read(0x04f8 + i, emu.memType.nesMemory) + local ENEMY_Y_VEL_ACCUM = emu.read(0x04c8 + i, emu.memType.nesMemory) + + -- change variable to interested variable for studying + emu.drawString(ENEMY_X_POS, ENEMY_Y_POS, string.format("%x", ENEMY_HP)) + end +end + +emu.addEventCallback(Main, emu.eventType.endFrame) diff --git a/docs/lua_scripts/mesen2/Show Background Collisions.lua b/docs/lua_scripts/mesen2/Show Background Collisions.lua new file mode 100644 index 0000000..1609719 --- /dev/null +++ b/docs/lua_scripts/mesen2/Show Background Collisions.lua @@ -0,0 +1,72 @@ +-- shows the background collision codes +-- floor, water, solid, or empty + +function get_bg_collision(x, y) + local VERTICAL_SCROLL = emu.read(0xfc, emu.memType.nesMemory) + local HORIZONTAL_SCROLL = emu.read(0xfd, emu.memType.nesMemory) + local PPUCTRL_SETTINGS = emu.read(0xff, emu.memType.nesMemory) + local adjusted_y = y + VERTICAL_SCROLL + local adjusted_x = x + HORIZONTAL_SCROLL + + if adjusted_y >= 0xf0 then + adjusted_y = adjusted_y + 0x0f + adjusted_y = adjusted_y - 255 + end + + -- $10 is always #$00, except when moving cart is calling get_bg_collision + local nametable_number = (PPUCTRL_SETTINGS ~ 0x00) & 0x01 + if adjusted_x > 255 then + nametable_number = nametable_number ~ 1 + adjusted_x = adjusted_x - 255 + end + + adjusted_y = (adjusted_y >> 2) & 0x3c + adjusted_x = adjusted_x >> 4 + local bg_collision_offset = (adjusted_x >> 2) | adjusted_y + if nametable_number == 1 then + bg_collision_offset = bg_collision_offset | 0x40; + end + + local collisionCodeByte = emu.read(0x680 + bg_collision_offset, emu.memType.nesMemory) + adjusted_x = adjusted_x & 0x03; + local collisionCode = 0 + if adjusted_x == 0 then + collisionCode = collisionCodeByte >> 6 + elseif adjusted_x == 1 then + collisionCode = collisionCodeByte >> 4 + elseif adjusted_x == 2 then + collisionCode = collisionCodeByte >> 2 + else + collisionCode = collisionCodeByte + end + + collisionCode = collisionCode & 0x03; + + local floorColor = 0x508fbc8f + local waterColor = 0x500096ff + local solidColor = 0x50a9a9a9 + local tileColor = 0x0 + if collisionCode == 0x01 then + tileColor = floorColor + elseif collisionCode == 0x02 then + tileColor = waterColor + elseif collisionCode == 0x03 then + tileColor = solidColor + end + + if collisionCode ~= 0 then + emu.drawRectangle(x, y, 16, 16, tileColor, true) + end +end + +function Main() + local VERTICAL_SCROLL = emu.read(0xfc, emu.memType.nesMemory) + local HORIZONTAL_SCROLL = emu.read(0xfd, emu.memType.nesMemory) + for i = 0,300,16 do + for j = 0,300,16 do + get_bg_collision(i - math.fmod(HORIZONTAL_SCROLL, 16), j - math.fmod(VERTICAL_SCROLL, 16)) + end + end +end + +emu.addEventCallback(Main, emu.eventType.endFrame) \ No newline at end of file diff --git a/docs/lua_scripts/mesen2/Show Enemy Positions.lua b/docs/lua_scripts/mesen2/Show Enemy Positions.lua new file mode 100644 index 0000000..fc3276c --- /dev/null +++ b/docs/lua_scripts/mesen2/Show Enemy Positions.lua @@ -0,0 +1,106 @@ +-- shows the player - enemy collision boxes +-- does not show the player bullet - enemy collision boxes +-- doesn't currently handle collision code f (rising spiked walls and fire beams) + +function check_player_x_collision(player_index) + local PLAYER_STATE = emu.read(0x90 + player_index, emu.memType.nesMemory) + if PLAYER_STATE ~= 0x01 then + -- exit if player not in normal state + return nil + end + + -- @check_in_water_crouched + local collision_box = 0x00 + local LEVEL_LOCATION_TYPE = emu.read(0x40 + player_index, emu.memType.nesMemory) + local PLAYER_WATER_STATE = emu.read(0xb2 + player_index, emu.memType.nesMemory) + if LEVEL_LOCATION_TYPE & 0xfe == 0x00 then + -- outdoor + local PLAYER_SPRITE_SEQUENCE = emu.read(0xbc + player_index, emu.memType.nesMemory) + if PLAYER_WATER_STATE ~= 0x00 and PLAYER_SPRITE_SEQUENCE == 0x02 then + -- exit if player crouched in water, no collision can happen with player + return nil + end + end + + if PLAYER_WATER_STATE == 0x00 then + collision_box = collision_box + 1 + end + + -- not checking for crouched while on indoor level, because non-bullets can + -- collide when crouching player on indoor levels + + local PLAYER_JUMP_STATUS = emu.read(0xa0 + player_index, emu.memType.nesMemory) + local PLAYER_SPRITE_CODE = emu.read(0xd6 + player_index, emu.memType.nesMemory) + if PLAYER_JUMP_STATUS == 0x00 then + -- player not jumping + collision_box = collision_box + 1 + if PLAYER_SPRITE_CODE ~= 0x17 then + collision_box = collision_box + 1 + end + end + + -- draw player collision point + local SPRITE_Y_POS = emu.read(0x031a + player_index, emu.memType.nesMemory) + local SPRITE_X_POS = emu.read(0x0334 + player_index, emu.memType.nesMemory) + emu.drawRectangle(SPRITE_X_POS, SPRITE_Y_POS, 1, 1, 0x0000ff, false) + + for i = 0,0xf do + local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.nesMemory) + local ENEMY_ROUTINE = emu.read(0x04b8 + i, emu.memType.nesMemory) + local ENEMY_STATE_WIDTH = emu.read(0x0598 + i, emu.memType.nesMemory) + local should_test_collision = ENEMY_STATE_WIDTH & 0x01 == 0x00 + if ENEMY_ROUTINE ~= 0x00 and should_test_collision then + set_enemy_collision_box(player_index, i, collision_box) + end + end +end + +function set_enemy_collision_box(player_index, i, collision_box) + local ENEMY_SCORE_COLLISION = emu.read(0x0588 + i, emu.memType.nesMemory) & 0x0f + + if ENEMY_SCORE_COLLISION == 0x0f then + -- todo handle (fire beam and rising spiked wall) + return + end + + local collision_box_addr = emu.readWord(0xe4e8 + (collision_box * 2), emu.memType.nesMemory) -- collision_box_codes_tbl + local offset = ENEMY_SCORE_COLLISION * 4 + local y0 = emu.read(collision_box_addr + offset, emu.memType.nesMemory) + local x0 = emu.read(collision_box_addr + offset + 1, emu.memType.nesMemory) + local height = emu.read(collision_box_addr + offset + 2, emu.memType.nesMemory) + local width = emu.read(collision_box_addr + offset + 3, emu.memType.nesMemory) + local ENEMY_Y_POS = emu.read(0x0324 + i, emu.memType.nesMemory) + local ENEMY_X_POS = emu.read(0x033e + i, emu.memType.nesMemory) + + local topY = ENEMY_Y_POS + if y0 > 0x80 then + y0 = negateInt(y0) + topY = ENEMY_Y_POS - y0 + else + topY = ENEMY_Y_POS + y0 + end + + local topX = ENEMY_X_POS + if x0 > 0x80 then + x0 = negateInt(x0) + topX = ENEMY_X_POS - x0 + else + topX = ENEMY_X_POS + x0 + end + + emu.drawRectangle(topX, topY, width, height, 0x0000ff, false) +end + +function negateInt(num) + if(num > 0x80) then + num = (~num + 1) & 0xff + end + + return num +end + +function Main() + check_player_x_collision(0) +end + +emu.addEventCallback(Main, emu.eventType.endFrame) \ No newline at end of file diff --git a/docs/lua_scripts/mesen2/Show HUD Info.lua b/docs/lua_scripts/mesen2/Show HUD Info.lua new file mode 100644 index 0000000..a71e6a9 --- /dev/null +++ b/docs/lua_scripts/mesen2/Show HUD Info.lua @@ -0,0 +1,27 @@ +-- shows information about weapon on head-up-display area + +function Main() + local p1score1 = emu.read(0x07e2, emu.memType.nesMemory) + local p1score2 = emu.read(0x07e3, emu.memType.nesMemory) + p1score = ((p1score2 << 8) + p1score1) * 100 + + local weapon_hex = emu.read(0x00aa, emu.memType.nesMemory) + local rapid_fire = "" + if weapon_hex & 0x10 == 0x10 then + rapid_fire = " Rapid " + else + rapid_fire = " " + end + + weapon_hex = weapon_hex & 0x0f + local weapon_name = "" + + if weapon_hex == 0 then weapon_name = "Default Gun" + elseif weapon_hex == 1 then weapon_name = "Machine Gun" + elseif weapon_hex == 2 then weapon_name = "Flame Thrower" + elseif weapon_hex == 3 then weapon_name = "Spray" + elseif weapon_hex == 4 then weapon_name = "Laser" end + emu.drawString(5,40, "P1: " .. p1score .. rapid_fire .. weapon_name) +end + +emu.addEventCallback(Main, emu.eventType.endFrame) \ No newline at end of file diff --git a/src/bank7.asm b/src/bank7.asm index d39404f..dba378b 100644 --- a/src/bank7.asm +++ b/src/bank7.asm @@ -3996,7 +3996,12 @@ scroll_player: jsr find_scrolled_player ; find the player that isn't causing scroll and needs to be scrolled beq @exit ; exit if both players are causing scroll, don't cause any player to be scrolled dec SPRITE_X_POS,x ; move player that isn't causing scroll back - ; other player will remain at same relative position on screen + ; causing the player to remain at same relative position on screen + ; !(BUG?) assumes player causing scroll can only cause 1px of horizontal scroll per frame + ; when player causing scroll is on a moving cart and moving right, FRAME_SCROLL will be #$02 + ; due to this bug, the player on the moving cart will cause other player to be dragged forward + ; 1px for each frame the player is causing scroll while on moving cart + ; thanks @archycoffee @exit: rts