After converting the functions from ASM to lua and debugging, it turns out that the predictor is not good at estimating the exact time when an animal leaves the screen ("exit prediction") except for the initial batch of animals or for when the animal is on the move. One reason I can for this is object placement: animals loaded in slots after the capsule will load one frame earlier than those loaded before the capsule; this 1-frame difference can change the direction that the animal will travel, and this will affect exit time. Another factor (which I account for) is that an animal loaded after the capsule will take one extra frame to register as missing. I have no idea what other factors are at play; but the fact is that the exit prediction is off by up to plus/minus 3 frames in my tests. The wait until the animals are spawned/start moving works perfectly, only the endpoint prediction that fails -- unless the animal is already on the move.
The only ways out of this are simulating the object placement or emulating all frames until the animals leave. I be looking into the latter; in any case, here is the current version of the script:
Download predict-animals-v2.luaLanguage: lua
local v_objspace = 0xFFFFD000
local c_objsize = 0x40
local obRender = 0x1
local obX = 0x8
local obY = 0xC
local obVelX = 0x10
local obVelY = 0x12
local obFrame = 0x1A
local obHeight = 0x16
local obWidth = 0x17
local obTimeFrame = 0x1E
local obRoutine = 0x24
local v_random = 0xFFFFF636
local v_zone = 0xFFFFFE10
local v_vbla_byte = 0xFFFFFE0F
local v_collindex = 0xFFFFF796
local v_lvllayout = 0xFFFFA400
local AngleMap = 0x00062900
local CollArray1 = 0x00062A00
local function find_capsule()
for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do
if memory.readbyte(addr) == 0x3e and memory.readbyte(addr+obRoutine) > 2 then
return addr
end
end
return nil
end
local function enum_animals()
local animals = {}
for addr=v_objspace + c_objsize,v_objspace + 0x40 * c_objsize,c_objsize do
if memory.readbyte(addr)==0x28 and memory.readbyte(addr+obRoutine) > 0 then
table.insert(animals, addr)
end
end
return animals
end
local function speed2color(speed, min, max)
local green = math.floor(((speed - min) * 255) / (max - min))
local red = math.floor(((max - speed) * 255) / (max - min))
local blue, alpha = 0, 255
return {red, green, blue, alpha}
end
local minspd, maxspd = 0x140, 0x300
local animals = {[0]={"Rabbit" , speed2color(0x200, minspd, maxspd), -0x200, -0x400},
[1]={"Chicken" , speed2color(0x200, minspd, maxspd), -0x200, -0x300},
[2]={"Eagle" , speed2color(0x180, minspd, maxspd), -0x180, -0x300},
[3]={"Seal" , speed2color(0x140, minspd, maxspd), -0x140, -0x180},
[4]={"Pig" , speed2color(0x1C0, minspd, maxspd), -0x1C0, -0x300},
[5]={"Flicky" , speed2color(0x300, minspd, maxspd), -0x300, -0x400},
[6]={"Squirrel", speed2color(0x280, minspd, maxspd), -0x280, -0x380}}
local typelist = {[0]={[0]=0, [1]=5},
[1]={[0]=2, [1]=3},
[2]={[0]=6, [1]=3},
[3]={[0]=4, [1]=5},
[4]={[0]=4, [1]=1},
[5]={[0]=0, [1]=1}}
local capsule_switch = nil
local seed = 0
local last_seed = -1
local last_vbla = -1
local function GetRandom()
local d1 = seed or 0x2A6D365A
local d0 = d1
d1 = SHIFT(d1, -2)
d1 = d1 + d0
d1 = SHIFT(d1, -3)
d1 = d1 + d0
d0 = OR(AND(d1, 0x0000FFFF), AND(d0, 0xFFFF0000))
d1 = OR(SHIFT(AND(d1, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16))
d0 = OR(AND(d1 + d0, 0x0000FFFF), AND(d0, 0xFFFF0000))
d1 = OR(SHIFT(AND(d0, 0x0000FFFF), -16), SHIFT(AND(d1, 0xFFFF0000), 16))
seed = d1
return d0
end
local function FindNearestTile(objRender, objX, objY)
local index = AND(SHIFT(objY, 1), 0x380) + AND(SHIFT(objX, 8), 0x7F)
local tilenum = memory.readbyte(v_lvllayout + index)
if AND(tilenum, 0x80) ~= 0 then
tilenum = AND(tilenum, 0x7F)
if AND(objRender, 0x40) ~= 0 then
tilenum = tilenum + 1
if tilenum == 0x29 then
tilenum = 0x51
end
end
tilenum = AND(tilenum - 1, 0xFF)
if AND(tilenum, 0x80) ~= 0 then
tilenum = OR(tilenum, 0xFF00)
end
tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7))
return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000)
end
if tilenum > 0 then
tilenum = AND(tilenum - 1, 0xFF)
if AND(tilenum, 0x80) ~= 0 then
tilenum = OR(tilenum, 0xFF00)
end
tilenum = OR(SHIFT(AND(tilenum, 0x7F), -9), SHIFT(AND(tilenum, 0xFF80), 7))
return OR(AND(tilenum + AND(2 * objY, 0x1E0) + AND(SHIFT(objX, 3), 0x1E), 0xFFFF), 0xFFFF0000)
end
return 0xFFFFFF00
end
local function FindFloor2(objRender, objX, objY, solidbit, delta, initangle, flipmask)
local angle = initangle
local tileaddr = FindNearestTile(objRender, objX, objY)
local floordist = 0
local tile = memory.readword(tileaddr)
local tileid = AND(tile, 0x7FF)
if tileid ~= 0 and AND(tile, solidbit) ~= 0 then
local colptr = memory.readlong(v_collindex)
local block = memory.readbyte(colptr + tileid)
if block == 0 then
return 0xF - AND(objY, 0xF), tileaddr, angle
end
angle = memory.readbytesigned(AngleMap + block)
block = SHIFT(block, -4)
local xcopy = objX
if AND(tile, BIT(0xB)) ~= 0 then
xcopy = -objX-1
angle = -angle
end
if AND(tile, BIT(0xC)) ~= 0 then
angle = -0x80 - angle
end
xcopy = AND(objX, 0xF) + block
local colhgt = memory.readbytesigned(CollArray1 + xcopy)
tile = XOR(tile, flipmask)
if AND(tile, BIT(0xC)) ~= 0 then
colhgt = -colhgt
end
if colhgt == 0 then
return 0xF - AND(objY, 0xF), tileaddr, angle
elseif colhgt < 0 then
local dist = AND(objY, 0xF)
if dist + colhgt < 0 then
return -dist-1, tileaddr, angle
end
else
local dist = AND(objY, 0xF)
return 0xF - (dist + colhgt), tileaddr, angle
end
end
return 0xF - AND(objY, 0xF), tileaddr, angle
end
local function FindFloor(objRender, objX, objY, solidbit, delta, initangle, flipmask)
local angle = initangle
local tileaddr = FindNearestTile(objRender, objX, objY)
local floordist = 0
local tile = memory.readword(tileaddr)
local tileid = AND(tile, 0x7FF)
if tileid ~= 0 and AND(tile, solidbit) ~= 0 then
local colptr = memory.readlong(v_collindex)
local block = memory.readbyte(colptr + tileid)
if block == 0 then
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask)
return floordist + 0x10, tileaddr, angle
end
angle = memory.readbytesigned(AngleMap + block)
block = SHIFT(block, -4)
local xcopy = objX
if AND(tile, BIT(0xB)) ~= 0 then
xcopy = -objX-1
angle = -angle
end
if AND(tile, BIT(0xC)) ~= 0 then
angle = -0x80 - angle
end
xcopy = AND(objX, 0xF) + block
local colhgt = memory.readbytesigned(CollArray1 + xcopy)
tile = XOR(tile, flipmask)
if AND(tile, BIT(0xC)) ~= 0 then
colhgt = -colhgt
end
if colhgt == 0 then
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask)
return floordist + 0x10, tileaddr, angle
elseif colhgt < 0 then
local dist = AND(objY, 0xF)
if dist + colhgt < 0 then
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask)
return floordist - 0x10, tileaddr, angle
end
elseif colhgt == 0x10 then
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY - delta, solidbit, delta, initangle, flipmask)
return floordist - 0x10, tileaddr, angle
else
return 0xF - (AND(objY, 0xF) + colhgt), tileaddr, angle
end
end
floordist, tileaddr, angle = FindFloor2(objRender, objX, objY + delta, solidbit, delta, initangle, flipmask)
return floordist + 0x10, tileaddr, angle
end
local function ObjFloorDist(objX, objY, objH, objR)
local floordist, tileaddr, angle = FindFloor(objR, objX, objY + objH, BIT(0xD), 0x10, 0, 0)
if AND(angle, 1) ~= 0 then
angle = 0
end
return floordist, tileaddr, angle
end
local function ObjectFall(objX, objY, objVX, objVY)
objX = AND(objX + SHIFT(objVX, -8), 0xFFFFFFFF)
objY = AND(objY + SHIFT(objVY, -8), 0xFFFFFFFF)
objVY = objVY + 0x38
return objX, objY, objVX, objVY
end
local function animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla)
objX, objY, objVX, objVY = ObjectFall(objX, objY, objVX, objVY)
if objVY >= 0 then
local floordist = ObjFloorDist(SHIFT(objX, 16), SHIFT(objY, 16), objH, objR)
if floordist < 0 then
objY = objY + floordist
objVX = objNVX
objVY = objNVY
if AND(vbla, BIT(4)) ~= 0 then
objVX = -objVX
objR = XOR(objR, BIT(0))
end
end
end
return objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla
end
local function simulate_animal_delay_fall_escape(obj, objX, objY, objVX, objVY, objNVX, objNVY, objWait, vbla, capX)
local objH = 0xC
local objR = 0x85
local objW = 8
local initvbla = vbla
if obj ~= nil then
objX = memory.readlong(obj+obX)
objY = memory.readlong(obj+obY)
objVX = memory.readwordsigned(obj+obVelX)
objVY = memory.readwordsigned(obj+obVelY)
objNVX = memory.readwordsigned(obj+0x32)
objNVY = memory.readwordsigned(obj+0x34)
objWait = memory.readword(obj+0x36) - 1
end
if objWait > 0 then
vbla = vbla + objWait + 1
end
while objVX == 0 do
vbla = vbla + 1
objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla =
animal_fall(objX, objY, objVX, objVY, objH, objR, objNVX, objNVY, vbla)
end
local le = SHIFT(capX - 160 - objW, -8)
local re = SHIFT(capX + 160 + objW, -8)
objX = SHIFT(objX, 8)
local lastvbla = vbla
if objVX > 0 then
vbla = vbla + math.abs(math.floor((re - objX + objVX - 1) / objVX)) + 1
else
vbla = vbla + math.abs(math.floor((objX - le - objVX - 1) / -objVX)) + 1
end
return objVX, vbla
end
function print_animal_objects(vbla)
local animal_list = enum_animals()
if #animal_list > 0 then
row = 4
gui.drawtext(4, row, "Spawned animals:")
row = row + 8
gui.drawtext(12, row, "Animal Wait D Exit at")
row = row + 8
local capX = memory.readword(capsule_switch+obX)
local list1 = {}
local list2 = {}
for i,m in pairs(animal_list) do
local ty = memory.readbyte(m+0x30)
local delay = memory.readword(m+0x36)
local vx, endvbla = simulate_animal_delay_fall_escape(m, nil, nil, nil, nil, nil,
nil, nil, vbla, capX)
local dir = (vx > 0 and ">") or "<"
local endpt = movie.framecount() + endvbla - vbla + 1
table.insert((delay == 0 and list2) or list1, {endpt, function(rw)
gui.drawtext(12, rw,
string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir,
endpt), animals[ty][2])
end})
end
table.sort(list1, function(item1, item2)
return item1[1] < item2[1]
end)
table.sort(list2, function(item1, item2)
return item1[1] < item2[1]
end)
for i,m in pairs(list1) do
m[2](row)
row = row + 8
end
gui.drawtext(12, row, "========= Free =========")
row = row + 8
for i,m in pairs(list2) do
m[2](row)
row = row + 8
end
end
end
function predict_animals()
capsule_switch = find_capsule()
if capsule_switch == nil then
last_seed = memory.readlong(v_random)
last_vbla = memory.readbyte(v_vbla_byte)
return
end
seed = memory.readlong(v_random)
local rout = memory.readbyte(capsule_switch+obRoutine)
local vbla = memory.readbyte(v_vbla_byte)
if last_vbla == vbla then
vbla = vbla + 1
end
local savevbla = vbla
if rout == 4 then
gui.drawtext(4, 4, "Capsule not broken yet.")
last_seed = seed
last_vbla = savevbla
return
end
local row = 0
gui.drawbox(0, 0, 223, 28*8-1, { 0, 0, 0, 128}, {255, 255, 255, 255})
if rout == 0xE then
gui.drawtext(128, 12, "Waiting for animals\nto leave screen.", {255, 255, 0, 255})
print_animal_objects(vbla)
last_seed = seed
last_vbla = savevbla
return
end
local zone = memory.readbyte(v_zone)
local types = typelist[zone]
if rout <= 0xA then
local explosions_left = 0
local countdown = memory.readword(capsule_switch + obTimeFrame)
local dt = 7 - AND(vbla, 7)
for i = 1, countdown + 1 do
if AND(vbla + i - 1, 7) == 0 then
explosions_left = explosions_left + 1
local rand = GetRandom()
end
end
if explosions_left > 0 then
gui.drawtext(112, 180,
string.format("Next oportunity to change\nanimal pattern: %d frame%s",
dt, (dt == 1 and "") or "s"), {255, 255, 0, 255})
end
row = 4
gui.drawtext(4, row, string.format("Explosions left: %d", explosions_left))
row = row + 16
gui.drawtext(4, row, "Initial animals:")
row = row + 8
gui.drawtext(12, row, "Animal Wait D Exit at")
row = row + 8
local randseq = {}
for i = 1, 8 do
local rand = GetRandom()
table.insert(randseq, 1, rand)
end
local capX = memory.readword(capsule_switch+obX)
local capY = memory.readword(capsule_switch+obY) + 0x20
for i = 1, 8 do
local rand = randseq[1]
table.remove(randseq, 1)
local ty = types[AND(rand, 1)]
local objX = SHIFT(capX + 0x1C - 7 * i, -16)
local objY = SHIFT(capY, -16)
local delay = countdown + 90 + 8 * i
local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400,
animals[ty][3], animals[ty][4],
delay, vbla, capX)
local dir = (vx > 0 and ">") or "<"
gui.drawtext(12, row,
string.format("%-8s %4d %s %7d", animals[ty][1], delay, dir,
movie.framecount() + endvbla - vbla),
animals[ty][2])
row = row + 8
end
end
print_animal_objects(vbla)
if rout <= 0xC then
row = 4
local saverow = row
row = row + 8
gui.drawtext(124, row, "Animal Spawn D Exit at")
row = row + 8
local animals_left = 0
local countdown = (rout < 0xC and memory.readword(capsule_switch+obTimeFrame)) or 0
local dt = 7 - AND(vbla + countdown, 7)
local timer = (rout == 0xC and memory.readword(capsule_switch+obTimeFrame)) or 150
local capX = memory.readword(capsule_switch+obX)
local capY = memory.readword(capsule_switch+obY) + ((rout < 0xC and 0x20) or 0)
for i = 1, timer + 1 do
if AND(vbla + countdown + i - 1, 7) == 0 then
local rand = AND(GetRandom(), 0x1F) - 6
local pos = (seed > 0 and rand) or -rand
rand = GetRandom()
local ty = types[AND(rand, 1)]
if countdown + i - 2 > 0 then
local objX = SHIFT(capX + pos, -16)
local objY = SHIFT(capY, -16)
local delay = countdown + i + 12
local vx, endvbla = simulate_animal_delay_fall_escape(nil, objX, objY, 0, -0x400,
animals[ty][3], animals[ty][4],
delay, vbla, capX)
local dir = (vx > 0 and ">") or "<"
gui.drawtext(124, row,
string.format("%-8s %4d %s %7d", animals[ty][1], delay-14, dir,
movie.framecount() + endvbla - vbla + (rout < 0xC and 1 or 0)),
animals[ty][2])
row = row + 8
animals_left = animals_left + 1
end
end
end
if rout == 0xC and animals_left > 0 then
gui.drawtext(112, 180,
string.format("Next oportunity to prevent\nanimal spawn: %d frame%s",
dt, (dt == 1 and "") or "s"), {255, 255, 0, 255})
end
if animals_left > 0 then
gui.drawtext(116, saverow, string.format("Animal spawns: %d left", animals_left))
end
end
last_seed = memory.readlong(v_random)
last_vbla = savevbla
return
end
gens.registerafter(predict_animals)
savestate.registerload(predict_animals)