Here is an improvement to drawbox, to prevent it from crashing when drawing boxes that extend outside the screen:
int x1m = min(max(0,x1),255);
int y1m = min(max(0,y1),238);
int x2m = min(max(0,x2),255);
int y2m = min(max(0,y2),238);
// top surface
if(y1 == y1m) for (i=x1m; i <= x2m; i++)
gui_data[y1*256 + i] = colour;
// bottom surface
if(y2 == y2m) for (i=x1m; i <= x2m; i++)
gui_data[y2*256 + i] = colour;
// left surface
if(x1 == x1m) for (i=y1m; i <= y2m; i++)
gui_data[i*256+x1] = colour;
// right surface
if(x2 == x2m) for (i=y1m; i <= y2m; i++)
gui_data[i*256+x2] = colour;
This replaces the end of drawbox. I have also found this function handy:
// movie.open()
//
// Opens movie playback/recording
static int movie_open(lua_State *L) {
const char *filename = lua_tostring(L,1);
int readonly = TRUE;
int sync = 0;
Settings.WrongMovieStateProtection = false;
S9xMovieOpen(filename, readonly, sync);
return 0;
}
Using these changes, I have made a ghosting script for Super Metroid. I suspect that it might sometimes be off by 1 frame, and it has not been completely tested how it reacts to savestates, but perhaps it will come in handy. It consists of two parts:
recorder.lua:
-- the movie file to use
moviefile = "herooftheday2-smetroid.smv"
-- output file. contains ingame frame, scroll, position and hitbox information
dumpfile = moviefile..".dump"
io.output(dumpfile)
-- index file. contains room transition information
indexfile = io.open(moviefile..".index", "w")
snes9x.speedmode("turbo")
movie.open(moviefile)
prev = {}
prev.roomid1 = memory.readword(0x7ED552)
prev.roomid2 = memory.readword(0x7ED554)
prev.roomid3 = memory.readword(0x7ED556)
while true do
if movie.mode() then
igframe = 3600*memory.readbyte(0x7E09DE)+60*memory.readbyte(0x7E09DC)+memory.readbyte(0x7E09DA)
roomid1 = memory.readword(0x7ED552)
roomid2 = memory.readword(0x7ED554)
roomid3 = memory.readword(0x7ED556)
scrollx, scrolly = memory.readword(0x7E0911), memory.readword(0x7E0915)
posx, posy = memory.readword(0x7E0AF6), memory.readword(0x7E0AFA)
hitx, hity = memory.readword(0x7E0AFE), memory.readword(0x7E0B00)
io.write(igframe, " ", roomid1, " ", roomid2, " ", roomid3, " ", scrollx, " ",
scrolly, " ", posx, " ", posy, " ", hitx, " ", hity, "\n")
-- keep track of door transitions. this is used in room ghost mode
if roomid1 ~= prev.roomid1 or roomid2 ~= prev.roomid2 or roomid3 ~= prev.roomid3 then
indexfile:write(roomid1, " ", roomid2, " ", roomid3, " ", prev.roomid1, " ", prev.roomid2, " ", prev.roomid3, " ", movie.framecount(), " ", igframe, "\n")
end
prev.roomid1 = roomid1
prev.roomid2 = roomid2
prev.roomid3 = roomid3
end
snes9x.frameadvance()
end
Specify which smv to use at the top of this file, and it will generate a .dump and .index file for that movie. Do this for all the movies you want a ghost for. The main part of the script is in player.lua:
moviefile = "cpadolf2-smetroid.smv"
others = { "herooftheday-smetroid.smv", "herooftheday2-smetroid.smv", "cpadolf-smetroid.smv", "cpadolf2-smetroid.smv" }
colors = { "red", "green", "blue", "white" }
-- ingame mode:
-- 0: Show all real-time frames
-- 1: Skip non-ingame frames for main character
-- 2: Skip non-ingame frames for everybody
ingame = 2
-- room comparison mode:
-- 0: Normal
-- 1: Synchroniced room entry
room = 1
-- set up movie data
data = {}
pattern = "(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)"
for i,name in ipairs(others) do
print("Loading "..name.." position data")
local filedata = {}
io.input(name..".dump")
for line in io.lines() do
local entry = {}
_,_,entry.igframe,entry.roomid1, entry.roomid2, entry.roomid3,
entry.scrollx, entry.scrolly, entry.posx, entry.posy,
entry.hitx, entry.hity = string.find(line,pattern)
table.insert(filedata, entry)
end
filedata.progress = 1
filedata.sync = 0
filedata.offset = 0
filedata.init = false
table.insert(data,filedata)
snes9x.frameadvance() -- hack to avoid snes9x thinking we are hanging
end
-- set up index data
index = {}
pattern = "(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)"
for i,name in ipairs(others) do
print("Loading "..name.." room transition data")
local filedata = {}
io.input(name..".index")
for line in io.lines() do
local entry = {}
_,_,entry.roomid1, entry.roomid2, entry.roomid3,
entry.roomid1p, entry.roomid2p, entry.roomid3p,
entry.frame, entry.igframe = string.find(line,pattern)
local key = entry.roomid1.." "..entry.roomid2.." "..entry.roomid3.." "..
entry.roomid1p.." "..entry.roomid2p.." "..entry.roomid3p
if not filedata[key] then filedata[key] = {} end
local val = {}
val.frame = entry.frame
val.igframe = entry.igframe
table.insert(filedata[key],val)
end
table.insert(index,filedata)
snes9x.frameadvance() -- hack to avoid snes9x thinking we are hanging
end
-- find the frame corresponding to a given in-game frame.
-- Does a halving search, but is optimized for the case where
-- we are quite close already
seek = function(t, igf)
local tmp = 4
local a = t.progress
local b = t.progress
-- setup search window
while igf - t[a].igframe < 0 do
b = a
a = a - tmp
tmp = tmp * 2
if a <1> 0 do
a = b
b = b + tmp
tmp = tmp * 2
if b > table.getn(t) then b = table.getn(t) end
end
if t[b].igframe == igf then return b end
-- now do a halving search.
-- We know that the value we want is between a and b
-- and that a < b
while true do
local mid = (a+b)/2
if t[mid].igframe - igf == 0 then return mid
elseif t[mid].igframe - igf <0> 0 then igframe = 0
else beginning = false end
end
-- igframe = memory.readword(0x7E1842)
-- unless we are in real time mode, we will skip frames where the
-- incame frame counter does not change
if ingame > 0 and igframe ~= igframe_old then
skippedframes = count-lastcount-1
snes9x.speedmode("normal")
-- build the full room id. not sure if all these are needed
roomid1 = memory.readword(0x7ED552)
roomid2 = memory.readword(0x7ED554)
roomid3 = memory.readword(0x7ED556)
roomid = ""..roomid1.." "..roomid2.." "..roomid3
-- information for drawing the hitboxes
scrollx, scrolly = memory.readword(0x7E0911), memory.readword(0x7E0915)
posx, posy = memory.readword(0x7E0AF6), memory.readword(0x7E0AFA)
hitx, hity = memory.readword(0x7E0AFE), memory.readword(0x7E0B00)
-- have we had any room transitions?
if roomid ~= roomid_old then transition = true else transition = false end
for i = 1, table.getn(data) do
-- do we need to fix our position in the file?
if stloaded then
if ingame > 0 then data[i].progress = seek(data[i],igframe)
else data[i].progress = count+1 end
end
-- however, in room ghost mode, we will find the closest
-- matching room transition.
if room == 1 and transition then
local frame
if ingame < 2 then frame = count else frame = igframe end
local trans = index[i][roomid.." "..roomid_old]
if trans then
local diff = 1e55, bj
for j = 1, table.getn(trans) do
local tframe
if ingame < 2 then tframe = trans[j].frame
else tframe = trans[j].igframe end
local cdiff = math.abs(frame-tframe)
if cdiff <diff> 0 do
data[i].progress = data[i].progress + 1
end
data[i].init = true
end
-- eat frames where igframe does not change
while data[i][data[i].progress].igframe -
data[i][data[i].progress+1].igframe == 0 do
data[i].progress = data[i].progress+1
end
data[i].progress = data[i].progress+1
else data[i].progress = count+1 end
c = data[i].progress
if c < table.getn(data[i]) then
local entry = data[i][c]
if roomid1 - entry.roomid1 == 0 and
roomid2 - entry.roomid2 == 0 and
roomid3 - entry.roomid3 == 0 then
x = entry.posx-scrollx
y = entry.posy-scrolly
gui.drawbox(x-entry.hitx,y-entry.hity,x+entry.hitx,y+entry.hity,colors[i])
end
end
end
roomid_old = roomid
lastcount = count
else snes9x.speedmode("maximum") end
igframe_old = igframe
frameinc()
snes9x.frameadvance()
end
The options of interest are moviefile, others, ingame and room, and are documented in the file.