diff --git a/output/luaScripts/Multitrack.lua b/output/luaScripts/Multitrack.lua new file mode 100644 index 00000000..325c86b1 --- /dev/null +++ b/output/luaScripts/Multitrack.lua @@ -0,0 +1,567 @@ +-- Multi-track rerecording for FCEUX +-- To make up for the lack of an internal multi-track rerecording in FCEUX. +-- It may get obsolete when TASEdit comes around. +--FatRatKnight +-- Rewind added by DarkKobold + + +-- Stuff that you are encouraged to change to fit what you need. +--***************************************************************************** +local players= 2 -- You may tweak the number of players here. + +local saveMax= 1000; -- Max Rewind Power + +local opaque= 0 -- Default opacity. 0 is solid, 4 is invisible. + +local dispX, dispY= 10, 99 -- For the display stuffs + +-- Control scheme. You may want to change these. +local selectplayer = "S" -- For selecting which player +local recordingtype= "space" -- For selecting how to record +local show, hide = "pageup", "pagedown" -- Opacity adjuster +local scrlup, scrldown, scrlleft, scrlright= "numpad8", "numpad2", "numpad4", "numpad6" -- Input display mover +local add, remove = "insert", "delete" -- Movin' frames around... +local rewind = "R" +local key = {"right", "left", "down", "up", "C", "V", "X", "Z"} + +local btn = {"right", "left", "down", "up", "start", "select", "B", "A"} +-- Don't change btn, unless it's to adjust the spacing to keep it lined up neatly. +--***************************************************************************** + +-- Stuff that really shouldn't be messed with, unless you plan to change the code significantly. +local optType= {"Both", "Keys", "List", "Off"} -- Names for option types +local keys, lastkeys= {}, {} -- To hold keyboard input +local Pin, thisInput= {}, {} -- Input manipulation array +local BufInput, BufLen= {},{}-- In case you want to insert or remove frames. +local option= {} -- Options for individual button input by script +local fc, lastfc= nil, nil -- Frame counters +local FrameAdvance= false -- Frame advance detection +local framed= nil -- "frameadvance", "stateload", or nil +local pl= 1 -- Player selected +local plmin, plmax= 1, 1 -- For the all option +local repeater= 0; -- for holding a button +local rewinding= false -- For unpaused rewinding + + + +--The stuff below is for more advanced users, enter at your own peril! + + + +local saveArray = {};--the Array in which the save states are stored +local saveCount = 0;--used for finding which array position to cycle through +local saver; -- the variable used for storing the save state +local rewindCount = 0;--this stops you looping back around the array if theres nothing at the end +local savePreventBuffer = 1;--Used for more control over when save states will be saved, not really used in this version much. + +-- Initialize tables for each player. +for i= 1, players do + Pin[i] = {} + thisInput[i] = {} + option[i] = {} + BufInput[i] = {} + BufLen[i] = 0 + for j= 1, 8 do + option[i][btn[j]]= 1 + end +end + +-- Intention of options: +-- "Both" Script buttons and input list used +-- "Keys" Script buttons used +-- "List" Input list used +-- "Off" Script will not interfere with button + + +--***************************************************************************** +function press(button) +--***************************************************************************** +--FatRatKnight +-- Checks if a button is pressed. +-- The tables it accesses should be obvious in the next line. + + if keys[button] and not lastkeys[button] then + return true + + end + return false +end + + +--***************************************************************************** +function pressrepeater(button) +--***************************************************************************** +--DarkKobold; Changes by FatRatKnight. Description by FatRatKnight +-- Checks if a button is pressed. Will keep returning true if held long enough. +-- Accesses: keys[], lastkeys[], repeater + + if keys[button] then + if not lastkeys[button] or repeater >= 3 then + return true + else + repeater = repeater + 1; + end + + elseif lastkeys[button] then -- To allow more calls for other buttons + repeater= 0 + end; + return false +end + + +--***************************************************************************** +function NewFrame() +--***************************************************************************** +--FatRatKnight +-- Psuedo-detection of frame-advance or state load. +-- The lack of a function to otherwise detect state loads calls for this function +-- Accesses: fc, lastfc, FrameAdvance + + if FrameAdvance then -- Main loop sets this flag + FrameAdvance= false -- Don't re-detect FA. + + if fc-1 == lastfc then -- Sanity versus an unpaused + return "frameadvance" -- game during a stateload. + + else -- fc-1 instead of lastfc+1 + return "stateload" -- is used as lastfc isn't + end -- necessarily defined. + end + + if fc ~= lastfc then -- Main loop didn't set FrameAdvance, + return "stateload" -- so here we are, checking if the + end -- frame did change. + + return nil -- fc == lastfc. No advance or load +end -- Yes, a stateload on the same frame + -- is counted as a nil. + +--***************************************************************************** +function RewindThis() +--***************************************************************************** +--Added by DarkKobold; Made into function that returns T/F by FatRatKnight +-- Loads a state that was saved just the frame prior +-- Lets you know whether it was successful by returning true or false. +-- Accesses: rewindCount, saveArray, saveCount, saveMax + + if rewindCount==0 then + return false -- Failed to rewind + else + savestate.load(saveArray[saveCount]); + saveCount = saveCount-1; + rewindCount = rewindCount-1; + if saveCount==0 then + saveCount = saveMax; + end; + end; + return true -- Yay, rewind! +end + + +--***************************************************************************** +function GetNextInput(P) +--***************************************************************************** +--FatRatKnight +-- Gets the input from the input table. +-- Accesses: BufLen[], BufInput[], fc, Pin[] + + if BufLen[P] > 0 then + return BufInput[P][BufLen[P]] + end + return Pin[P][fc-BufLen[P]] +end + + +--***************************************************************************** +function LoadStoredInput(P) +--***************************************************************************** +--FatRatKnight +-- Loads up input into thisInput. +-- Found a good reason why it should be called in several spots. +-- Accesses: option[], thisInput[] + + local next= GetNextInput(P) + if next then + for i= 1, 8 do + local op= option[P][btn[i]] + if op == 1 or op == 3 then -- "Both" or "List" + thisInput[P][btn[i]]= next[btn[i]] + else + thisInput[P][btn[i]]= nil + end + end + else -- No input to load, so erase stuff. + for i= 1, 8 do + thisInput[P][btn[i]]= nil + end + end +end + + +--***************************************************************************** +function GetList(P,Offset) +--***************************************************************************** +--FatRatKnight +-- Fetches the input from the list relative to current framecount. +-- It takes into account insertions and deletions. +-- Accesses: thisInput[], Pin[], BufLen[], BufInput[] + + if Offset == 0 then + return thisInput[P] + elseif Offset < 0 then + return Pin[P][fc+Offset] + end + + if BufLen[P] > 0 and BufLen[P] > Offset then + return BufInput[P][BufLen[P]-Offset] + end + return Pin[P][fc+Offset-BufLen[P]] +end + + +--***************************************************************************** +function InputSnap(P) +--***************************************************************************** +--FatRatKnight +-- Will shove the input list over. +-- Might end up freezing for a moment if there's a few thousand frames to shift +-- Accesses: framed, BufLen, BufInput[], Pin[], players + + + if BufLen[P] < 0 then -- Squish! + local pointer= lastfc + while Pin[P][pointer] do + Pin[P][pointer]= Pin[P][pointer-BufLen[P]] + pointer= pointer+1 + end + end + + + if BufLen[P] > 0 then -- Foom! + local pointer= lastfc + while Pin[P][pointer] do + pointer= pointer+1 + end + -- pointer is now looking at a null frame. + -- Assume later frames are also null. + + while pointer > lastfc do + pointer= pointer-1 + Pin[P][pointer+BufLen[P]]= Pin[P][pointer] + end + -- pointer should now match lastfc. + -- Everything at lastfc and beyond should be moved over by BufLen. + + for i=0, BufLen[P]-1 do + Pin[P][lastfc +i]= BufInput[P][BufLen[P]-i] + end + end + + BufLen[P]= 0 -- If it ain't zero before, we did stuff to make us want it zero. +end + + +--***************************************************************************** +function DisplayInput() +--***************************************************************************** +--FatRatKnight; Changes by DarkKobold. +-- Shows the input stream that is stored in this script. +-- Converted into function form. There might be other ways of handling it. +-- Accesses: dispX, dispY, Pin[], thisInput[], players + +dispX= math.min(math.max(dispX,-2),219) -- Limits +dispY= math.min(math.max(dispY,51),185) + +if pl > players then + dispX= math.min(dispX,243-16*players) +end + + for i= -10, 10 do + + local Y= dispY + if i < 0 then + Y= Y-3 + elseif i > 0 then + Y= Y+3 + end + + for P= plmin, plmax do + local temp= {} -- I may want to pick thisInput instead of the frame list. + temp= GetList(P,i) + + local color + for j= 1, 8 do + if not temp then + color= "white" + elseif temp[btn[j]] then + color= "green" + else + color= "red" + end + + if pl <= players then + gui.drawbox(dispX +4*j,Y +4*i,dispX+2 +4*j,Y+2 +4*i,color) + else -- all players + local bx= dispX+1 +j +2*players*(j-1) +2*P + gui.drawbox(bx , Y+4*i , bx+1 , Y+4*i +2 ,color) + end + end + end + end + if pl <= players then + gui.drawbox(dispX+2,dispY-2,dispX+36,dispY+4,"blue") + else + gui.drawbox(dispX+2,dispY-2,dispX+12 +16*players,dispY+4,"blue") + end +end + + +-- Primary function. ... Why did I pick itisyourturn, anyway? +--***************************************************************************** +function itisyourturn() +--***************************************************************************** + keys= input.get() -- We need the keyboard and mouse! + fc= movie.framecount() -- We also need the Frame Count. + framed= NewFrame() -- Are we looking at a new frame? + +-- State: Rewind + if saveMax > 0 then + if pressrepeater(rewind) or rewinding then + rewinding= false + if RewindThis() then + fc= movie.framecount() -- Definite change here! + framed= "stateload" -- Don't even bother with NewFrame()... + movie.rerecordcounting(true) + else + keys[rewind]= nil + FCEU.pause() + end + end + end + +-- Selection: Players + if press(selectplayer) then -- Hopefully, this block is + pl= pl + 1 -- self-explanatory. + if (pl > players+1) or (players == 1) then + pl= 1 -- If not, it... Selects a player... + end -- Joy. + end + +-- For standard player loops for allplayer option + if pl > players then + plmin= 1 + plmax= players + else + plmin= pl + plmax= pl + end + + +-- Input: Insert or delete frames + if press(add) then -- Part of the reason for + for P= plmin, plmax do -- speedy insertions and + BufLen[P]= BufLen[P]+1 -- deletions is due to the + BufInput[P][BufLen[P]]= {} -- fact that I don't shift + LoadStoredInput(P) -- frames upon add/remove. + end + end -- I only shift once you + if press(remove) then -- begin rewind or load a + for P= plmin, plmax do -- state. At that point, + BufLen[P]= BufLen[P]-1 -- it may take a while if + LoadStoredInput(P) -- there's enough frames + end -- to shift around. + end + +-- Option: Opacity + if press(hide) and opaque < 4 then + opaque= opaque+1 + end + if press(show) and opaque > 0 then + opaque= opaque-1 + end + gui.transparency(opaque) + +-- Option: Move input display by keyboard + if pressrepeater(scrlup) then + dispY= dispY - 1 + end + if pressrepeater(scrldown) then + dispY= dispY + 1 + end + if pressrepeater(scrlleft) then + dispX= dispX - 1 + end + if pressrepeater(scrlright) then + dispX= dispX + 1 + end + +-- Option: Move input display by mouse + if keys["leftclick"] and lastkeys["leftclick"] then + dispX= dispX + keys.xmouse - lastkeys.xmouse + dispY= dispY + keys.ymouse - lastkeys.ymouse + end + + +-- Begin start of frame change + if framed then + for P= 1, players do + if not Pin[P][lastfc] or framed == "stateload" then + InputSnap(P) -- If last frame was empty, kill the shift + end -- Don't track more than one buffer for loads + + + if framed == "frameadvance" then + if not Pin[P][lastfc] then + Pin[P][lastfc]= {} + end + +-- Scroll the BufInput if there's anything in there. +-- This can potentially slow things if you insert enough frames. + if BufLen[P] > 0 then + for i= BufLen[P], 1, -1 do + BufInput[P][i]= BufInput[P][i-1] + end + BufInput[P][1]= Pin[P][lastfc] + end + +-- Store input + Pin[P][lastfc]= joypad.get(P) + end + + LoadStoredInput(P) + end -- Loop for each player + +-- Resets key input, so it counts as pressed again if held through frame advance. + for i= 1, 8 do + lastkeys[key[i]]= nil + end + + end +-- Done checking for a new frame + + +-- Begin checking key input +-- Are you setting button options? + if keys[recordingtype] then + for i= 1, 8 do + +-- Set current options + for P= plmin, plmax do + if press(key[i]) then + option[P][btn[i]]= option[plmax][btn[i]]+1 + if option[P][btn[i]] > 4 then + option[P][btn[i]]= 1 + end + end + end + +-- Display current options + gui.transparency(0) + gui.text(20,20+12*i,btn[i]) + + if pl >= players + 1 then + local z + local q= optType[option[1][btn[i]]] + for z= 2, players do + if q ~= optType[option[z][btn[i]]] then + q= "???" + break + end + end + gui.text(50,20+12*i,q) + else + gui.text(50,20+12*i,optType[option[pl][btn[i]]]) + + end + end -- key loop + +-- Are you setting actual input? + else + for i= 1, 8 do + +-- Toggle keyed input depending on options + for P= plmin, plmax do + op= option[P][btn[i]] + if op == 1 or op == 2 then -- "Both" or "Keys" + if press(key[i]) then -- Spread the AND to the next line + if thisInput[P][btn[i]] then + thisInput[P][btn[i]]= nil + else + thisInput[P][btn[i]]= true + end + end + end + end + + end -- key loop + +-- Done with input. +-- The next stuff is just displaying input table. +-- It's here to hide it when setting options. + if opaque < 4 then + DisplayInput() + end + end -- Done with "options" if + +-- Display selected player. Or other odds and ends. + if pl <= players then + gui.text(30,10,pl) + gui.text(45,10,BufLen[pl]) + else + gui.text(30,10,"A") + end; + +-- Feed the input to the emulation. + if movie.mode() ~= "playback" then + for P= 1, players do + joypad.set(P, thisInput[P]) + end + end + + lastfc= fc -- Standard "this happened last time" stuff + lastkeys= keys -- Don't want to keep registering key hits. + +end -- Yes, finally! The end of itisyourturn! +--***************************************************************************** + + +gui.register(itisyourturn) -- Need to call while in between frames + +FCEU.pause(); -- Immediate pause is nice + +--***************************************************************************** +while true do -- Main loop +--***************************************************************************** +-- Most of the stuff here by DarkKobold. Minor stuff by FatRatKnight +-- Keep in mind stuff here only happens on a frame advance or when unpaused. + FCEU.frameadvance() + + if saveMax > 0 then -- Don't process if no Rewind + if keys[rewind] then + rewinding= true + else + saveCount=saveCount+1; + rewindCount = math.min(rewindCount + 1,saveMax); + if saveCount==saveMax+1 then + saveCount = 1; + end + + if saveArray[saveCount] == nil then + saver = savestate.create(); + else + saver = saveArray[saveCount]; + end; + savestate.save(saver); + saveArray[saveCount] = saver; -- I'm pretty sure this line is unnecessary. + movie.rerecordcounting(false) + end + end + + FrameAdvance= true -- For the NewFrame function +end + + +-- Possible inconveniences include: +-- * You should reload the script every time you switch through movies. +-- Failure to reload will result in old, "junk" input showing up. It hasn't been erased yet. +-- * Won't care about what you set up for your normal player input. Though it works with it fine. \ No newline at end of file