Fantastic4 DTC7 script
-- FanTAStic 4 Donald Land Lua script.
-- Emulator Abstraction Layer for functions that don't work the same on
-- BizHawk and FCEUX.
-- HACKY: Emulator detection, global because a deeper namespace can't read
-- from a shallower namespace?
is_fceux = nil
if FCEU ~= nil then -- FCEU unique module, not available on BizHawk
is_fceux = true
else
is_fceux = false
end
-- EAL namespace
local EAL = {}
do
local mem = nil
local ROM = nil
local GUI = nil
local Core = nil
end
-- EAL Memory namespace functions
EAL.mem = {}
do
function EAL.mem.read_u8(addr)
if is_fceux then
return memory.readbyte(addr)
else
return memory.read_u8(addr)
end
end
function EAL.mem.read_s8(addr)
if is_fceux then
return memory.readbytesigned(addr)
else
return memory.read_s8(addr)
end
end
function EAL.mem.read_u16_be(addr)
if is_fceux then
return memory.readword(addr + 1, addr)
else
return memory.read_u16_be(addr)
end
end
function EAL.mem.read_s16_be(addr)
if is_fceux then
return memory.readwordsigned(addr + 1, addr)
else
return memory.read_s16_be(addr)
end
end
function EAL.mem.read_s16_be_scatter(hi, lo)
if is_fceux then
return memory.readwordsigned(lo, hi)
else
local val = memory.read_u8(hi)
val = bit.lshift(val, 8)
val = bit.bor(val, memory.read_u8(lo))
if val > 0x7FFF then
return val - 0x10000
else
return val
end
end
end
function EAL.mem.read_u16_le(addr)
if is_fceux then
return memory.readword(addr)
else
return memory.read_u16_le(addr)
end
end
end
-- EAL ROM namespace functions
EAL.ROM = {}
do
-- `addr` is the address as seen from the system bus
local function get_addr_in_bank(bank, addr)
local bank_addr = addr - 0x8000
if addr > 0xC000 then -- high region
bank_addr = bank_addr - 0x4000
end
return bank_addr
end
-- `addr` is the address as seen from the system bus
local function get_offset_in_prg(bank, addr)
local bank_addr = get_addr_in_bank(bank, addr)
local offset = (bank * 0x4000) + bank_addr
return offset
end
-- Usage note: `addr` is the address as seen from the system bus
function EAL.ROM.read_u8(bank, addr)
local offset = get_offset_in_prg(bank, addr)
if is_fceux then
offset = offset + 16 -- FCEUX includes the header
return rom.readbyte(offset)
else
return memory.read_u8(offset, "PRG ROM")
end
end
-- Usage note: `addr` is the address as seen from the system bus
function EAL.ROM.read_u16_be(bank, addr)
local offset = get_offset_in_prg(bank, addr)
if is_fceux then
offset = offset + 16 -- FCEUX includes the header
local val = rom.readbyteunsigned(offset)
val = bit.lshift(val, 8)
val = bit.bor(val, rom.readbyteunsigned(offset + 1))
return val
else
return memory.read_u16_be(offset, "PRG ROM")
end
end
function EAL.ROM.read_u16_le(bank, addr)
local offset = get_offset_in_prg(bank, addr)
if is_fceux then
offset = offset + 16 -- FCEUX includes the header
local val = rom.readbyteunsigned(offset + 1)
val = bit.lshift(val, 8)
val = bit.bor(val, rom.readbyteunsigned(offset))
return val
else
return memory.read_u16_le(offset, "PRG ROM")
end
end
end
-- EAL GUI namespace functions
EAL.GUI = {}
do
local offsetx = 0
local offsety = 0
function EAL.GUI.extend_draw_area(top, bottom, left, right)
if is_fceux then
return
else
client.SetGameExtraPadding(left, top, right, bottom)
offsetx = left
offsety = top
end
end
local textx
local texty
function EAL.GUI.set_text_area(x, y)
textx = x
texty = y
end
function EAL.GUI.print_data(token, value)
str = string.format("%s : %s", token, tostring(value))
if is_fceux then
gui.text(textx, texty, str)
texty = texty + 10
else
gui.drawText(textx + offsetx, texty + offsety,
str, nil, "blue")
texty = texty + 20
end
end
function EAL.GUI.print_data2(token, value, value2)
str = string.format("%s : %s %s", token, tostring(value), tostring(value2))
if is_fceux then
gui.text(textx, texty, str)
texty = texty + 10
else
gui.drawText(textx + offsetx, texty + offsety,
str, nil, "blue")
texty = texty + 20
end
end
function EAL.GUI.box(x1, x2, y1, y2)
if is_fceux then
gui.box(x1, y1, x2, y2)
else
gui.drawBox(x1 + offsetx, y1 + offsety,
x2 + offsetx, y2 + offsety)
end
end
end
-- Core namespace
EAL.Core = {}
do
function EAL.Core.set_lag(is_lag)
if is_fceux then
emu.setlagflag(is_lag)
else
emu.setislagged(is_lag)
if is_lag then
emu.setlagcount(emu.lagcount() + 1)
end
tastudio.setlag(emu.framecount(),is_lag)
end
end
end
---------------------------------------------------------------------------
---------------------------------------------------------------------------
--19:38 < micro500> if you throw an apple towards the left edge of the map the apple gets completely removed, and I see the code path for that
--20:38 < micro500> it seems like the apples are limited to a Y position of 0x08 or greater
--01:37 < micro500> I'm starting to think that counter 0-0x50 affects the vertical speed of the apple, and when it reaches 0x50 it is falling at full speed
--01:38 < micro500> I found a code path when an apple is thrown while hold down to set that counter to 0x50
--02:03 < micro500> I'm looking at the code path where it checks that "apple count" value
--02:04 < micro500> if your apple count is 0 and you already have an apple out it skips over running a function that I thought was important, but I NOP'd it out and it doesn't prevent apples from being thrown
--02:05 < micro500> so now I'm not sure how this works
--02:13 < micro500> for ViGadeomes: address 2 as far as I know is used as a temporary variable, so its pointless to put in the ram watch
--03:01 < micro500> so the loaded map in ram is larger than I thought
--03:01 < micro500> one block starts at 0x240, and another at 0x330
--03:02 < micro500> if you make the ram watch window large enough to see the area of 0x240-0x3F0 then move across the map you'll see what I mean
--03:19 < micro500> weirdly the powerup box gets picked up before it disappears on screen
--03:38 < micro500> the game assumes you are 2 blocks high, and when checking for point boxes it checks your Y position and your Y position + 16
--03:38 < micro500> so it ends up checking 2 blocks
--03:45 < micro500> looks like the first level data comes from bank 3 0x80d0, 0xC1C0 in the rom file
--03:47 < micro500> correction: 0xC0E0 in the rom file
--04:00 < micro500> looks like the data in 0x240... is for collision/deciding if you should get points/etc
--04:00 < micro500> I think when the game loads more data from the ROM it sends it to the PPU at that time
--04:01 < micro500> if you collect a box it updates the PPU memory then
--04:01 < micro500> when I collect a box (block ID 0x05) it changes in RAM to block id 0x32 (a background tile?), but if I change it back it doesn't put the block back on screen
--04:01 < micro500> so I think it only updates the PPU when it needs to
--04:02 < micro500> you can still collect the box and get points though :)
--04:02 < micro500> not sure if it is important, but I noticed there are some pixels below the timer that flicker
--04:07 < micro500> it seems that if you go forwards, spawn some powerup boxes, then go backwards, unspawn them, then go forwards to where they were they don't spawn again
--04:07 < micro500> so the game remembers not to spawn them again somehow
--04:08 < micro500> I'm guessing somewhere there is a max screen scrolled value to indicate the furthest right you've ever gone?
--04:34 < micro500> oh wtf
--04:34 < micro500> address 0x420 onwards
--04:34 < micro500> they store PPU memory writes there
--04:34 < micro500> addr high byte, addr low byte, value to write, etc
--05:13 < micro500> 0x605 seems to indicate how far into the level you are for use of the blocks
--05:13 < micro500> it seems to count how many blocks you've seen
--05:13 < micro500> and you can change it to a lower number and it will re-render blocks you have de-spawned
--05:25 < micro500> still trying to figure out how the map data is stored
--05:25 < micro500> its...odd
--05:26 < micro500> I found where they decide if the next powerup block should be rendered or not, but the math is a bit crazy
function get_number_apples()
-- Check if we're carrying 1 or 2 apples, non-0 == 2
local apples = EAL.mem.read_u8(0x062B) + 1
if apples ~= 1 then
apples = 2
end
return apples
end
function get_position()
local onscreen_x = EAL.mem.read_u8(0x0091)
local x = onscreen_x + get_screen_x_coordinate()
local subx = EAL.mem.read_u8(0x0092)
local y = EAL.mem.read_u8(0x0095)
return x, subx, y
end
function get_speed()
local x = EAL.mem.read_s16_be(0x0098)
local xa = EAL.mem.read_s8(0x009A)
local ya = EAL.mem.read_s8(0x009C)
local boost = EAL.mem.read_s8(0x00A9)
return x, xa, ya, boost
end
function get_screen_x_coordinate()
return EAL.mem.read_u16_le(0x00CC)
end
function get_pickup_boxes_level_addr(level)
local bank = 3
local level_addr = EAL.ROM.read_u16_le(bank, 0xB1AA + (level * 2))
return bank, level_addr
end
function get_level_info()
local level = EAL.mem.read_u8(0x0051)
local section = EAL.mem.read_u8(0x0052)
return level, section
end
function pickup_boxes_hud()
local level, section = get_level_info()
local screenx = get_screen_x_coordinate()
if level < 1 or level > 12 then -- don't try to index outside the array
return
end
local bank, pickup_box_addr = get_pickup_boxes_level_addr(level)
local data = {}
local count = 0
while true do
data[count] = {}
local x = EAL.ROM.read_u16_be(bank, pickup_box_addr)
if x == 0xFFFF then
break
end
data[count]["xpos"] = x
-- This unk0 seems to be related to height, but I am not sure how...
-- When y is subtracted with unk0 it makes some boxes appear
-- more "in place", but hides other boxes with strange IDs...
-- This value has only been seen to be either 0x90 or 0x70
-- and only in relation to objects that are "spawned".
data[count]["unk0"] = EAL.ROM.read_u8(bank, pickup_box_addr+2)
-- This unk1 is almost guaranteed an x-offset into the screen
-- while xpos is the screen offset into the level.
data[count]["unk1"] = EAL.ROM.read_u8(bank, pickup_box_addr+3)
data[count]["ypos"] = EAL.ROM.read_u8(bank, pickup_box_addr+4)
data[count]["type"] = EAL.ROM.read_u8(bank, pickup_box_addr+5)
EAL.GUI.set_text_area(data[count]["xpos"] - screenx + data[count]["unk1"],
data[count]["ypos"])
EAL.GUI.print_data("Type", data[count]["type"])
pickup_box_addr = pickup_box_addr + 6
count = count + 1
end
end
-- There are 8 enemy slots in the game
-- TODO: Refactor, make readable.
function get_enemy_and_apple_data()
local data = {}
-- The last 2 slots are reserved for apples
for i=0,7 do
data[i] = {}
data[i]["status"] = EAL.mem.read_u8(0x04F3 + i)
data[i]["xpos"] = EAL.mem.read_s16_be_scatter(0x0543 + i, 0x054B + i)
data[i]["ypos"] = EAL.mem.read_u8(0x0563 + i)
data[i]["hitbox_w"] = EAL.mem.read_u8(0x0573 + i)
data[i]["hitbox_h"] = EAL.mem.read_u8(0x057B + i)
data[i]["boost"] = EAL.mem.read_s8(0x0583 + i)
if i < 6 then
data[i]["type"] = EAL.mem.read_u8(0x04FB + i)
else -- apples
data[i]["type"] = nil
end
-- TODO, what do the timeouts mean?
-- The game increments the timeout by 1 each frame until it reaches
-- 80, or until the apple lands, in which case 80 is set directly
-- When the apple lands, the explosion countdown begins, it starts
-- at 100, then drops to 40, then counts down 1 each frame.
if i > 5 then -- apples
data[i]["timeout"] = EAL.mem.read_u8(0x04D5 + i - 6)
data[i]["explosion"] = EAL.mem.read_u8(0x04D7 + i - 6)
end
local s = data[i]["status"]
end
return data
end
function enemy_hud()
local screenx = get_screen_x_coordinate()
local enemy_data = get_enemy_and_apple_data()
for id,v in ipairs(enemy_data) do
repeat -- Because Lua 5.1 doesn't even have GOTO!
-- status enum
-- 0 = Slot empty
-- 1 = ??? (used?)
-- 2 = Enemy is on screen
-- 3 = Enemy is off screen
if v["status"] == 0 then -- slot empty, don't draw
break
end
EAL.GUI.box(v["xpos"] - v["hitbox_w"] - screenx + 1,
v["xpos"] + v["hitbox_w"] - screenx - 1,
v["ypos"] - v["hitbox_h"] + 1,
v["ypos"] + v["hitbox_h"] - 1)
EAL.GUI.set_text_area(v["xpos"] - screenx, v["ypos"] + 0x10)
EAL.GUI.print_data("Boost", v["boost"])
if v["type"] == nil then -- apples
EAL.GUI.print_data("Expl.timer", v["explosion"])
end
break
until true
end
end
function donald_hud()
local screenx = get_screen_x_coordinate()
local x, subx, y = get_position()
local speed, xaccel, yaccel, accel_boost = get_speed()
local apples = get_number_apples()
-- Donald's hitbox, hardcoded in the ROM as displacement checks
-- of the enemy hitbox against Donald's (x/y) coordinate.
EAL.GUI.box(x - 0x7 - screenx, x + 0x7 - screenx, y - 0xF, y + 0xF)
EAL.GUI.set_text_area(x - screenx, y + 0x10)
EAL.GUI.print_data2("X Pos", x, subx)
EAL.GUI.print_data("Speed", speed)
EAL.GUI.print_data2("Accel (x,y)", xaccel, yaccel)
EAL.GUI.print_data("Boost accel", accel_boost)
EAL.GUI.print_data("Apples", apples)
end
function draw_hidden_lag_warning()
EAL.GUI.print_data3("0 Speed!")
end
function erase_hidden_lag_warning()
EAL.GUI.print_data4("0 Speed!")
end
function main()
local previous_screen_x = 0
local current_screen_x = get_screen_x_coordinate()
EAL.GUI.extend_draw_area(0, 50, 200, 200)
while true do
enemy_hud()
donald_hud()
pickup_boxes_hud()
if previous_screen_x == current_screen_x then
EAL.Core.set_lag(true)
end
previous_screen_x = current_screen_x
emu.frameadvance()
current_screen_x = get_screen_x_coordinate()
end
end
main()