Not perfect, not entirely sure how to fix the remaining bugs.
-- addresses
local addr_base = 0xB000
local addr_mode = 0xF600
local addr_inlev = 0xB004
local addr_cam1 = 0xEE78
local addr_cam3 = 0xFDB8
local addr_cam4 = 0xF616
local addr_time = 0xFE22
local addr_lockX = 0xEE16
local addr_lockY = 0xEE1A
local addr_camlock = 0xEE0B
local addr_camdelay = 0xEE24
local romaddr_renderrings = 0xEB86
local romaddr_renderobjects = 0x1B7F2
local romaddr_camlock = 0x1C0B0
local lastframe = 0
-- offsets
local offs_flags = 4
local offs_posX = 0x10
local offs_posY = 0x14
-- constants
local SCREEN_W = 320
local SCREEN_H = 224
local LEVEL_H = 2047
local SCROLLRATEX = 32
local SCROLLRATEY = 16
local FREEZE_SIZE = 18
local MAX_FRAMES = 100
local OST_ENTRY_LENGTH = 0x4A
-- aliases
local rb = mainmemory.readbyte
local rs16 = mainmemory.read_s16_be
local ru16 = mainmemory.read_u16_be
local rs32 = mainmemory.read_s32_be
local ru32 = mainmemory.read_u32_be
local wb = mainmemory.writebyte
local ws16 = mainmemory.write_s16_be
local wu16 = mainmemory.write_u16_be
local ws32 = mainmemory.write_s32_be
local wu32 = mainmemory.write_u32_be
-- buffers
local freezebuffer = {}
local cambuf = {}
local memorystate
local renderingcamhack
local pre_ring_code = memory.read_s16_be(romaddr_renderrings, "MD CART")
local pre_obje_code = memory.read_s16_be(romaddr_renderobjects, "MD CART")
local pre_camlock = memory.read_s16_be(romaddr_camlock, "MD CART")
gui.defaultPixelFont("gens")
local function CamHack()
client.invisibleemulation(false)
renderingcamhack = false
local levelheight = ru32( 0xEEAA )
local x = rs16(addr_base + offs_posX) -- character x position
local y = rs16(addr_base + offs_posY) -- character y position
local origx = rs16(addr_cam1) -- initial camera x position
local origy = rs16(addr_cam1 + 4) -- initial camera y position
local lockx = rs16(addr_lockX) -- right screen lock
local locky = rs16(addr_lockY) -- Bottom screen lock
local xx = math.max(0, math.min(x - SCREEN_W / 2, lockx)) -- desired camera x position
local yy = math.min(y - SCREEN_H / 2, locky) -- desired camera y position
local flags = rb(addr_base + offs_flags)
local timer = rs32(addr_time)
local deltaX = SCROLLRATEX
local levelwrap = x > origx + (SCREEN_W * 2)
local lastobject = 20
local lagframes = 0
local camdelay = rs16( addr_camdelay )
local lastx = 0
local lasty = 0
-- first we need to adjust for latency
memorystate = memorysavestate.savecorestate()
-- memory.write_s16_be(romaddr_camlock, 0x4241, "MD CART")
client.invisibleemulation(true)
client.seekframe(emu.framecount() + 1)
--[[
gui.pixelText(10, 100, string.format(
" flags: %s\n"..
"0xF7CD: %4d\n"..
" cam: %4d | %s\n"..
" y: %s | inl: %s\n",
flags & 0x80 ~= 0, rb(0xF7CD),
camdelay, x - origx <= SCREEN_W,
(y - origy <= 240 or (y + LEVEL_H - origy <= SCREEN_H and origy >= LEVEL_H - SCREEN_H) or (origy < 0 and (y >= LEVEL_H + origy or y <= origy + SCREEN_H))),
rb(addr_inlev)
)
)
--]]--
-- if flags && 0x80 ~= 0
if ( flags & 0x80 ~= 0 and camdelay == 0 )
or rb(0xF7CD) ~= 0
or (
camdelay == 0 and x - origx <= SCREEN_W
and (
y - origy <= 240
-- going downward through level-wraps
or (y + LEVEL_H - origy <= SCREEN_H and origy >= LEVEL_H - SCREEN_H)
-- going upward through level-wraps
or (origy < 0 and (y >= LEVEL_H + origy or y <= origy + SCREEN_H))
)
)
then
-- if we have nothing to do then just output the frame and be on our way
client.invisibleemulation(false)
client.seekframe(emu.framecount() + 1)
-- memory.write_s16_be(romaddr_camlock, pre_camlock, "MD CART")
lastx = rs16(addr_cam1)
lasty = rs16(addr_cam1 + 4)
memorysavestate.loadcorestate(memorystate)
memorysavestate.removestate(memorystate)
return
end
-- set the camera no more than 640 pixels distant
if math.abs(xx - origx) > SCREEN_W * 2 then
origx = math.max(0, xx - SCREEN_W * 2)
end
local delayforcam = 0
if camdelay then
-- if we're coming from camdelay then don't try to adjust the camera vertically
yy = origy
delayforcam = 3
ws16( addr_camdelay, 0)
end
-- and always above target, because Sonic doesn't like externally forced upward scrolling
if origy > yy then
origy = yy - SCREEN_H * 2
else
origy = math.max(yy - SCREEN_H * 2, origy)
end
if xx < origx then
deltaX = -deltaX
end
if lastx > origx and lastx < xx + SCROLLRATEX then
xx = lastx
end
local numframes = 0 + math.floor(math.max(
math.abs(xx - origx) / SCROLLRATEX,
math.abs(yy - origy) / SCROLLRATEY,
delayforcam
)
)
--[[
gui.pixelText(10, 100, string.format(
" pos: %4d x %4d\n"..
"orig: %4d | %4d x %4d | %4d\n"..
" cam: %4d %4d %4d %4d\n"..
" xx: %4d | %4d yy: %4d | %4d\n"..
" nf: %4d ",
x, y,
origx, rs16(addr_cam1),
origy, rs16(addr_cam1+4),
lastx, lasty, delayforcam, camdelay,
xx, math.max(0, x - SCREEN_W / 2),
yy, y - SCREEN_H / 2,
numframes))
--]]--
if numframes > MAX_FRAMES then
numframes = MAX_FRAMES
end
for i = 1, FREEZE_SIZE do
freezebuffer[i] = ru32(addr_base + i*4)
end
if levelwrap then
memory.write_s16_be(romaddr_renderrings, 0x4E75, "MD CART")
memory.write_s16_be(romaddr_renderobjects, 0x4E75, "MD CART")
end
lagframes = 0
for v = 0, 3 do
cambuf[v] = ru32(0xEE0C + v*4)
end
local orignumframes = numframes
for frame = 1, MAX_FRAMES do
if frame >= numframes then break end
if rb(addr_mode) ~= 0x0c then break end
if origy < -SCROLLRATEY then origy = -SCROLLRATEY end
if delayforcam == numframes and xx == rs16(addr_cam1) then break end
renderingcamhack = true
wb(addr_camlock, 1)
ws16(addr_cam1, origx)
ws16(addr_cam1 + 4, origy)
ws32(addr_time, timer)
for v = 1, FREEZE_SIZE do
wu32(addr_base + v*4, freezebuffer[v])
end
for v = 0, 3 do
wu32(0xEE0C + v*4, cambuf[v])
end
client.seekframe(emu.framecount() + 1)
if numframes < MAX_FRAMES and emu.islagged() then
numframes = numframes + 1
lagframes = lagframes + 1
if numframes >= 15 and lagframes >= 10 then
break
end
else
-- the game doesn't like being forced to scroll up
if yy > origy then
origy = origy + math.min(SCROLLRATEY, yy - origy)
end
if xx ~= origx then
if math.abs(xx - origx) <= SCROLLRATEX then
origx = xx
else
origx = origx + deltaX
end
end
end
end
--[[
gui.pixelText(10, 100, string.format(
" pos: %4d x %4d\n"..
"orig: %4d | %4d x %4d | %4d\n"..
" cam: %4d %4d %4d\n"..
" xx: %4d | %4d yy: %4d | %4d\n"..
" nf: %4d | orignumframes: %4d",
x, y,
origx, rs16(addr_cam1),
origy, rs16(addr_cam1+4),
rs32(addr_cam3), rs32(addr_cam4), rs32(addr_cam4+4),
xx, math.max(0, x - SCREEN_W / 2),
yy, y - SCREEN_H / 2,
numframes, orignumframes))
]]--
client.invisibleemulation(false)
client.seekframe(emu.framecount() + 1)
client.invisibleemulation(true)
lastx = rs16(addr_cam1)
lasty = rs16(addr_cam1 + 4)
memory.write_s16_be(romaddr_renderrings, pre_ring_code, "MD CART")
memory.write_s16_be(romaddr_renderobjects, pre_obje_code, "MD CART")
-- memory.write_s16_be(romaddr_camlock, pre_camlock, "MD CART")
memorysavestate.loadcorestate(memorystate)
memorysavestate.removestate(memorystate)
--client.invisibleemulation(false)
end
while true do
if emu.framecount() > lastframe then
CamHack()
end
lastframe = emu.framecount()
emu.frameadvance()
gui.clearGraphics()
end