document 2 player cart scroll bug. Thanks @archycoffee

Additionally, added mesen2-compatible lua scripts.
This commit is contained in:
Michael Miceli 2025-05-13 21:32:51 -04:00
parent c6acdb8299
commit b477373b5b
9 changed files with 289 additions and 9 deletions

View File

@ -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.
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
```

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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