Alpha version 0.5 is out! The latest version of the script can be found here.
Purpose:
To minimize the input in an existing movie file. Minimal input is a secondary goal (at best), but I personally find it aesthetically pleasing. I also know it can be very frustrating to minimize a movie's input while making it. It is much easier to hold a direction or set a button to auto-fire than it is to find the exact frame in which to press a button. This script removes all button presses that don't affect a movie's sync.
The script:
Download InputMin.luaLanguage: lua
local function getinput()
local buttons={}
local state1=savestate.create()
savestate.save(state1)
for i=emu.framecount(),movie.length() do
emu.frameadvance()
local frame=emu.framecount()-1
buttons[frame]=joypad.get(1)
end
savestate.load(state1)
return buttons
end
local function inputlist(buttons)
local k=1
local buttonlist={'right','left','down','up','start','select','B','A'}
local list={}
for i=0,#buttons do
for j=1,#buttonlist do
if buttons[i][buttonlist[j]] then
list[k]={i,buttonlist[j]}
k=k+1
end
end
end
return list
end
local function readgoldenRAM(goldenRAM)
local vals={}
for k=1,#goldenRAM do
vals[k]=memory.readbyte(goldenRAM[k])
end
return vals
end
local function getgoldenvals(goldenRAM,checkpoint,buttons)
local state1=savestate.create()
savestate.save(state1)
while emu.framecount()<checkpoint do
joypad.set(1,buttons[emu.framecount()])
emu.frameadvance()
end
local goldenvals=readgoldenRAM(goldenRAM)
print(goldenvals)
savestate.load(state1)
return goldenvals
end
local function comparevals(test,golden)
match=true
for i=1,#test do
match = (match and (test[i]==golden[i]))
end
return match
end
local checkpoint=movie.length()
local goldenRAM={0x85, 0x86, 0x90, 0xa4}
local buttons=getinput(inputRAM)
local list=inputlist(buttons)
movie.close()
print(list)
print(#list)
local removed=0
local kept=0
local testvals={}
local match=false
local goldenvals=getgoldenvals(goldenRAM,checkpoint,buttons)
print(goldenvals)
local gamestart=savestate.create()
savestate.save(gamestart)
for i=1,#list do
while emu.framecount()<checkpoint do
joypad.set(1,buttons[emu.framecount()])
if emu.framecount()==list[i][1] then
joypad.set(1,{[list[i][2]]=false})
end
emu.frameadvance()
end
testvals=readgoldenRAM(goldenRAM)
syncs=comparevals(testvals,goldenvals)
if syncs then
buttons[list[i][1]][list[i][2]]=false
removed=removed+1
else
kept=kept+1
end
print(removed)
print(kept)
savestate.load(gamestart)
end
How to use the script:
1) Create a movie. It should be short (<1 minute) and it should be a game that you know quite well. You will need to input some RAM addresses. I chose Dr. Mario because it was available.
2) Pick a frame that you want to test for synchronization. By default, this is the end of the movie, but you can change it to any value (before or after the end of the movie). To change it, change the value of "checkpoint" on line 66.
3) Pick RAM addresses to be tested for synchronization. These are addresses whose values you can be certain are important to the movie's synchronization. The more you include, the better... up to a certain point. If you include most/all of the game's RAM values, it will take much longer to compare them and evaluate synchronization. Also, if you include too many addresses, you may include one that is not actually important to synchronization (increasing the number of false positives). Ideally, you want just enough addresses to have zero false negatives (essential button presses that are deemed unessential) and not so many that you introduce false positives (non-essential button presses that are deemed essential). Set these addresses on line 74 in the array "goldenRAM". For my test movie, I included just four RAM addresses corresponding to the following values: pill counter, pill x, pill y, virus counter. Using just these four addresses, my script worked fine.
4) Run the game, pause FCEUX. Load the movie. Run the script.
5) Wait. A long time. The movie will play, removing exactly one input each time. If the movie desyncs, the input is returned in subsequent playbacks. If the movie syncs, the input is permanently removed.
6) As you wait, a few pieces of information will be printed to the Lua console. It will first print a list of every button you pressed in the movie along with the frame you pressed it on, it will then print the total number of button presses, it will then print the values of the RAM addresses that it is seeking to synchronize, and finally, at the end of every iteration of the number of removed inputs followed by the number of kept inputs. After the script is finished running, no data is saved to file (this will be changed in future versions).
Explanation of script's algorithm:
In its current version, the script is just about as naive as it can be. It runs the movie up until the frame specified by "checkpoint". It then reads all the values at addresses specified by "goldenRAM". Next, it plays the movie, removing the player's very first input. When it reaches the "checkpoint" frame, it again checks the values at addresses in "goldenRAM". If any of them fail to match up, the movie is deemed to have desynced and the input is kept (assumed to be essential). If they all agree, the movie is deemed to have synced and the input is discarded.
Results:
I ran this script on a 52 second movie with the emulator on turbo speed. It picked up 839 total inputs. Running the script took about 40 minutes. 713 inputs were removed and just 126 were kept. You can download and test the movie file
here. It may actually be faster to just create and test your own movie file...
Things to do...:
1) Make sure the script runs when process is started in the middle of a movie (I think it does, but I haven't checked).
2) Improve compatibility for use in other emulators. It may work in emulators other than FCEUX, but I'm not sure.
3) Save final input to movie file. Well, what good is input minimization if the script doesn't actually produce anything?
4) Technically, the script may need to be re-run several times to truly optimize input. To see why, suppose the input is such that A is held for five consecutive frames but only needs to be pressed on the first frame to produce the desired result. The script will check for synchronization something like this:
AAAAA <-- Original input. No desync.
.AAAA <-- Desyncs. A is pressed a frame later.
A.AAA <-- Desyncs! A is pressed twice!
AA.AA <-- Desyncs. A is pressed twice.
AAA.A <-- Desyncs. A is pressed twice.
AAAA. <-- Syncs. A is pressed once.
In this example, the script would need to be run five times before it could be verified that input can no longer be optimized. To make checking faster, I might break the algorithm into checking "chunks" of button presses so that the whole script doesn't need to be re-run several times.
5) I'm sure there's a way that I can save a state right before input is changed so that it isn't necessary to replay the entire movie. This is just the quick-and-dirty version of the script.
6) The algorithm needs to be improved. I plan on changing it to a method that completes the process in four shorter steps. First, the script will remove random individual inputs from the movie file and check both whether desync occurs and exactly how long it takes. This will be done approximately 100 times and will be used to assemble statistics. These statistics will be used to determine optimal values for tau (expected time to desync) and B (bin size). Second, all addresses are checked individually for desync, up through tau frames. This saves time because desync is checked only up to tau frames, not to the end of the movie. However, there will be some false negatives. These will be caught in the second pass. Third, the presses that seemed to sync in the first pass are assembled
randomly into bins (of size B). Each bin is tested for synchronization through to the end of the movie. If the movie syncs, all inputs in the bin are deemed unessential and are removed from the movie file. Fourth, any bins that desynced have their component inputs individually checked through the end of the movie. This is a final, exhaustive search and all inputs are now definitively classified as either essential or non-essential.
I expect programming this will take a while...
7) Other miscellaneous things that I can't think of right now. Make a suggestion!
Try it out and tell me how you like it!