Adding a multitrack input recorder lua script made by FatRatKnight
This commit is contained in:
parent
ae4cb5bd94
commit
b7dbc544ef
|
@ -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.
|
Loading…
Reference in New Issue