If you somehow missed this fact from the title I have up there, I have it in nice big words here, just in case.
MtEdit is a TASing lua script designed to hold on to the user's input separate from the emulator and play it back, allowing the user to make modifications as desired, and with a neat display to go with it, showing exactly what you did.
The script has since gone through a complete rewrite in v2. It will auto-detect which controllers are plugged into lsnes when it starts up, and will also respond to lsnes hotkeys for the controllers.
By the way, I'm holding on to all earlier versions I've made. Below is v1, but v2 is linked to above. I prefer that you try the latest stuff.
Download MtEdit_lsnes_v01.luaLanguage: lua
--Leeland Kirwan (FatRatKnight)
-- The legendary MtEdit I've always wanted to implement.
-- Alas, bare-bones features only.
-- No inserts/deletes
-- No macros
-- No individualized button control
-- SNES controller only
-- No watermarks
-- No subframe control
-- Useful stuff that is in place:
-- List of input displayed
-- Allow user input mixed in with recorded stream
-- Control for multiple players
-- Lag display
--#############################################################################
--#############################################################################
-- Setup
local Players= bit.value( -- Uncomment players you need.
0, -- Port 0. Almost certainly player 1.
-- 1, -- Multitap for Port 0.
-- 2, -- Multitap for Port 0.
-- 3, -- Multitap for Port 0.
4, -- Port 1. Usually player 2.
-- 5, -- Multitap for Port 1.
-- 6, -- Multitap for Port 1.
-- 7, -- Multitap for Port 1.
nil -- To ensure proper syntax. Otherwise serves no purpose.
)
-- The script controls override user's setup in lsnes, for controller 1.
local Controls= { -- Adjust these as necessary.
[0]="g", -- B Oh, leave the [0]= you see there alone.
"t", -- Y But feel free to change stuff in the "quotes".
"r", -- Select
"u", -- Start The script overrides whatever controls you
"w", -- Up set up in lsnes. So pick keys you want as
"s", -- Down your controls here.
"a", -- Left
"d", -- Right
"h", -- A
"y", -- X
"f", -- L
"j" -- R
}
for i= 0, 11 do Controls[Controls[i]]= i end -- Don't change this.
local cmd_NextPlayer= "l" -- Switch to next player.
local cmd_MtEditRead= "z" -- Allow the script to mix recorded input with user?
local cmd_InputMode= "x" -- Switch through input modes. (Only 2)
--local cmd_CustomKey= "c" -- Custom
--#############################################################################
--#############################################################################
-- Useful global junk. Please don't change.
local PlSel= 0 -- Player selection
local function NullFN() end -- A "do nothing" function, just in case.
local HeldBtns= {} -- 16 bits; Number for button storage.
local LastBtns= {} -- 32 bits; Various status bits of last frame.
local ReadMovie= {} -- Read from movie?
local ReadMacro= {} -- Read from macro?
local BtnCtrlA= {} -- Control masks for individual buttons.
local BtnCtrlB= {} -- 00=Ignore 01=Normal 10=Sticky 11=Hold
local InputList= {} -- Will contain inputs. As well as two subframe inputs.
--local OverSnoop= {} -- Just in case input is polled more than three times.
local ResetList= {} -- Will store all the resets that take place.
for Pl= 0, 7 do
HeldBtns[Pl]= 0x0000
LastBtns[Pl]= 0x00000000
ReadMovie[Pl]= 0x0FFF
ReadMacro[Pl]= 0x0000
BtnCtrlA[Pl]= 0x0FFF
BtnCtrlB[Pl]= 0x0000
InputList[Pl]= {}
end
local inp_Top= 0 -- Where to locate my input list...
local inp_Lef= -96
local inp_Off= -27 -- The top line's relative frame.
local ExpectedSize= 56 -- Without access to screen height, I have to guess.
local inp_Display= gui.bitmap_new(95,ExpectedSize*8-1,true) -- My display.
local Tiles= {} -- Holds all relevant tile stuff. Tiles[Shape][Color]
local BlankFrame= gui.bitmap_new(95,7,true) -- Blank frames
local This_Subframe= 0 -- I'll track my own subframes...
--*****************************************************************************
local function PlayerLoop(Fn)
--*****************************************************************************
-- It appears I need this loop frequently.
-- Good with lambda expressions, too!
for p= 0, 7 do
if bit.extract(Players,p) ~= 0 then
Fn(p)
end
end
end
--#############################################################################
--#############################################################################
-- Icons
--*****************************************************************************
local function Dbmp7x7(n,clr)
--*****************************************************************************
-- Converts a number into a monochrome 7x7 BITMAP.
-- Enough space in a double for this to happen. So I use it.
-- However, readability suffers greatly due to this.
local bitmap= gui.bitmap_new(7,7,true)
for i= 0, 48 do
local c= -1
if bit.extract(n,i) == 1 then c= clr end
gui.bitmap_pset(bitmap, -- DBITMAP object.
i%7, -- x
math.floor(i/7), -- y
c -- color
)
end
return bitmap -- Be sure to hand out what I just made.
end
-------------------------------------------------------------------------------
local Icons7x7= {
[0]= 0x0FF1E37F8F1BF, -- B Incidentally, I just care my icons work.
0x060C1830F3343, -- Y
0x198CB4A484486, -- Select I've put no effort in the
0x0994A848894F6, -- Start readability of these numbers.
0x18DB3E3870408, -- Up
0x02041C38F9B63, -- Down Left to right, top to bottom,
0x10383C3EF3840, -- Left 7 bits per line,
0x00439EF878381, -- Right bit-packed, made by hand.
0x18FFFFC78DF1C, -- A
0x10F33C30F3343, -- X I could use bit.value and give a more
0x1FFF83060C183, -- L visual representation in this code.
0x18F1BF7F8F1BF -- R But I already have these numbers...
} -- If I find I need to make edits, I may switch to bit.value.
-------------------------------------------------------------------------------
-- Make white tiles.
local BlankTiles= {}
for i= 0, 11 do
BlankTiles[i]= Dbmp7x7(Icons7x7[i],0xFFFFFF)
end
local Tc= { -- Tile color
-- Red Green
[0]= 0xFF2000, 0x00FF00, -- Saturated, normal
0xFF8080, 0xA0FFA0, -- Light, lag indicator
0xA00000, 0x008000, -- Dark, Watermark
0xC04040, 0x40C040, -- Desaturated, lag+watermark
}
for x= 0, 11 do
Tiles[x]= {}
for c= 0, #Tc do
Tiles[x][c]= gui.bitmap_new(7,7,true,Tc[c])
gui.bitmap_blit(Tiles[x][c],0,0,
BlankTiles[x],0,0,7,7 , 0xFFFFFF)
end
end
-- Produce a blank frame.
-------------------------------------------------------------------------------
for x= 0, 11 do
gui.bitmap_blit(BlankFrame,x*8,0,
BlankTiles[x],0,0,7,7) -- Produce my tile on the spot
end
--#############################################################################
--#############################################################################
-- Display
-- B Y s S ^ v < > A X L R
local x_off= {[0]= 6,5,3,4,1,1,0,2,7,6,0,7} -- Offsets for use with displaying
local y_off= {[0]= 2,1,1,1,0,2,1,1,1,0,0,0} -- the immediate joypad stuff.
local ModeNames= {[0]= "Ignore","Normal","Sticky","Hold"}
local ModeClrs= {[0]= 0xFF2000,0xFFFFFF,0x00FF00,0x00FFFF}
--*****************************************************************************
local function InputDisplay(Pl)
--*****************************************************************************
-- For the immediate user input.
local Right, Bottom= gui.resolution()
gui.bottom_gap(26)
local BCa, BCb= BtnCtrlA[Pl], BtnCtrlB[Pl]
gui.rectangle(Pl*64,Bottom,64,24,1,0x000080,0x000080)
PlayerLoop(function(p)
local HB= HeldBtns[p]
for i= 0, 11 do
local c= Tiles[i][bit.extract(HB,i)]
gui.bitmap_draw(p*64 + x_off[i]*8,Bottom+1+y_off[i]*8,c)
end
end)
local mode= bit.bor( -- Make an assumption about modes, for today...
bit.extract(BCa,0),
bit.extract(BCb,false,0)
)
gui.text(inp_Lef,Bottom+10,ModeNames[mode],ModeClrs[mode])
gui.text(-10,Bottom+10,PlSel,0x00FFFF)
end
--*****************************************************************************
local function ShowList()
--*****************************************************************************
-- Its only purpose is to splat the image on screen.
gui.left_gap(-inp_Lef+1)
gui.rectangle(inp_Lef-1, inp_Top + inp_Off*(-8) - 1, 97, 9, 1,
0x00FFFF, 0x404040) -- Cyan, dark grey.
gui.bitmap_draw(inp_Lef,inp_Top,inp_Display)
-- Oh, and show whether we are picking up movie inputs.
local RM= ReadMovie[PlSel]
for i= 0, 11 do
local clr= 0xFF0000
if bit.extract(RM,i) == 1 then clr= 0x00FF00 end
gui.circle(inp_Lef+3+8*i,451,4,1,clr,clr)
end
end
--*****************************************************************************
local function BlitFrame(Pl, y_pos, frame)
--*****************************************************************************
-- Modifies a single line in main image.
-- It should request the frame from lsnes first, then poll myself for it.
-- Who should take priority? lsnes, as it knows the past, and if in readonly,
-- it knows the future, too. I just hold on to the input just in case...
-- For this revision, however, I handle it all myself.
local GetFrame= InputList[Pl][frame]
if GetFrame then
for x= 0, 11 do
local tile= Tiles[x][bit.extract(GetFrame,x,x+16)]
gui.bitmap_blit(inp_Display,x*8,y_pos*8,
tile,0,0,7,7)
end
else -- Wait, we didn't get a frame? Blit a blank, instead!
gui.bitmap_blit(inp_Display,0,y_pos*8,
BlankFrame,0,0,95,7)
end
end
--*****************************************************************************
local function UpdateBox(Pl, frame)
--*****************************************************************************
-- Adjusts the current image. Only two sets of blits rather than 56!
-- This assumes the only things to read are the frame we just did and the next
-- frame after the end of the display. I hope that's an appropriate assumption.
-- Elsewhere in code, I have already moved the display, in case of multi-core
-- processors. This simply exists to clean up that last bit of detail.
BlitFrame(Pl, ExpectedSize-1, -- Position: Last line in image
frame+inp_Off+ExpectedSize-1) -- How far past the mid is it?
BlitFrame(Pl, -inp_Off - 1, -- Position: Just before current line
frame-1) -- We're almost looking at it anyway.
end
--*****************************************************************************
local function RefreshBox(Pl, frame)
--*****************************************************************************
-- Repaints the current image from scratch. Use for long frame jumps.
local StartFrame= frame + inp_Off
for y= 0, ExpectedSize-1 do
BlitFrame(Pl, y,StartFrame+y)
end
end
RefreshBox(PlSel, movie.currentframe()) -- Undefined frame to known frame is a jump.
local lsnesBGui= InputDisplay -- the display by pointing them to NullFN.
--#############################################################################
--#############################################################################
-- Joypad control
--*****************************************************************************
local function GetKeyboardHolds()
--*****************************************************************************
-- Extracts direct from keyboard to figure out which keys are held.
-- Returns a value similar to input.geta(). Well, for controllers, anyway.
local keys= input.raw()
local c= 0
for i= 0, 11 do
local key= keys[Controls[i]]
c= bit.bor(c, bit.lshift(key.last_rawval,i))
end
return c
end
--*****************************************************************************
local function UpdateControl(Pl, frame)
--*****************************************************************************
-- Get Movie data, may need to take into account subframes and multiple players
PlayerLoop(function(p)
local MovieData= InputList[p][frame] or 0x0000
HeldBtns[p]= bit.band(MovieData,ReadMovie[p]) -- Apply mask
end)
-- Get Macro data. In a later version, anyway.
-- Get HoldMode data (Copy last frame data)
-- local HoldData= bit.band(
-- BtnCtrlA, BtnCtrlB, -- Construct mask
-- LastBtns -- ... Something besides last frame's input...
-- )
-- Get NormalMode data
local KeyData= bit.band(
BtnCtrlA[Pl], bit.bnot(BtnCtrlB[Pl]), -- Construct mask
GetKeyboardHolds() -- Get controls direct from keyboard.
)
HeldBtns[Pl]= bit.bxor(HeldBtns[Pl],KeyData)
end
--*****************************************************************************
local function NormalMode(index, KeyState)
--*****************************************************************************
-- Toggle relevant button. No questions asked.
HeldBtns[PlSel]= bit.bxor(HeldBtns[PlSel], bit.value(index))
end
--*****************************************************************************
local function HoldMode(index, KeyState)
--*****************************************************************************
-- Toggle the relevant button if key is pressed, do nothing if released.
HeldBtns[PlSel]= bit.bxor(HeldBtns[PlSel], bit.lshift(KeyState,index))
end
local lsnesHandleBtn= {
[0]=NullFN, -- Ignore mode. Block user input.
NormalMode, -- Normal mode. Read directly from keyboard.
HoldMode, -- Sticky mode. Toggle on keypress, but clear after advancing
HoldMode -- Hold mode. Toggle on keypress, maintain state on advance.
}
--#############################################################################
--#############################################################################
-- Keyboard commands
--*****************************************************************************
KeyPress= {} -- On KeyDown, run keyed function
--*****************************************************************************
-- A table used for a generic routine to call arbitrary functions.
-- I simply list functions in KeyPress, and the generic routine does the rest.
-------------------------------------------------------------------------------
KeyPress[cmd_MtEditRead]= function() -- Toggle input mix
-------------------------------------------------------------------------------
if ReadMovie[PlSel] == 0 then
ReadMovie[PlSel]= 0x0FFF
else
ReadMovie[PlSel]= 0
end
end
-------------------------------------------------------------------------------
KeyPress[cmd_InputMode]= function() -- Switch input modes
-------------------------------------------------------------------------------
if BtnCtrlA[PlSel] ~= 0 then
BtnCtrlA[PlSel]= 0
BtnCtrlB[PlSel]= 0x0FFF -- To Sticky mode!
else
BtnCtrlA[PlSel]= 0x0FFF -- To Normal mode!
BtnCtrlB[PlSel]= 0
end
end
-------------------------------------------------------------------------------
KeyPress[cmd_NextPlayer]= function()
-------------------------------------------------------------------------------
local Sanity= PlSel
repeat
PlSel= (PlSel+1)%8
until (bit.extract(Players,PlSel) ~= 0) or (Sanity == PlSel)
RefreshBox(PlSel, movie.currentframe()-1)
end
-------------------------------------------------------------------------------
--KeyPress[cmd_CustomKey]= function() -- Hold this key, press a control key.
-------------------------------------------------------------------------------
--end
--*****************************************************************************
KeyRelease= {} -- On KeyUp, run keyed function
--*****************************************************************************
-------------------------------------------------------------------------------
--KeyRelease[cmd_CustomKey]= function()
-------------------------------------------------------------------------------
--end
--#############################################################################
--#############################################################################
-- lsnes tie-in.
--*****************************************************************************
function on_paint()
--*****************************************************************************
-- Handle some frame-advancing flag I made up myself.
-- Besides that flag, just call the display functions.
ShowList() -- Paint the main display
InputDisplay(PlSel) -- Paint the current user input
-- Possibly include subframe display (right side gap?)
-- Possibly include macro display (right side gap?)
end
gui.repaint() -- May as well request a paint.
--*****************************************************************************
function on_keyhook(s,t)
--*****************************************************************************
-- A generic "Handle Commands" routine, but with support for special keys for
-- the controller. Just get rid of the else and the lines it does, and you have
-- a generic KeyPress routine.
if KeyPress[s] then
if t.last_rawval == 1 then
KeyPress[s]()
gui.repaint()
end
else -- Must be the keyed controller input, then.
local c= Controls[s]
local mode = bit.bor(
bit.extract(BtnCtrlA[PlSel],c),
bit.extract(BtnCtrlB[PlSel],false,c)
)
lsnesHandleBtn[mode](c, t.last_rawval)
gui.repaint()
end
-- if it's just a key release for a command key, don't request a repaint.
end
-- Tell lsnes to pay attention to my keys:
for k,v in pairs(KeyPress) do input.keyhook(k,true) end --generic
for k,v in pairs(KeyRelease) do input.keyhook(k,true) end --generic
for i= 0, 11 do input.keyhook(Controls[i],true) end --cnt
--*****************************************************************************
local function SubframeWatch() -- on_idle(25ms).
--*****************************************************************************
-- There exists no callback where emulation halts, waiting further user input.
-- on_input(true) is called immediately after emulation resumes. I need
-- something that is called immediately prior to waiting on user.
-- Hence, this function abusing on_idle for this purpose.
-- Update based on some subframe values.
-- Handle subframe controls.
end
--*****************************************************************************
function on_frame_emulated()
--*****************************************************************************
-- A full frame advance finishes now. Use standard update.
local frame= movie.currentframe()-1
PlayerLoop(function(p)
InputList[p][frame]= LastBtns[p]
end)
on_idle= nil -- Turn off the subframe watch -- Not in a subframe!
UpdateControl(PlSel, movie.currentframe()) -- New frame. Update controls.
This_Subframe= 0 -- Reset subframe count.
UpdateBox(PlSel,movie.currentframe())
end
local FrameJumped= false
--*****************************************************************************
local function FrameJump() -- on_post_load, on_rewind
--*****************************************************************************
-- Ensure our display and controls still match up.
FrameJumped= true -- We jumped. on_input isn't 100% with this...
UpdateControl(PlSel, movie.currentframe()) -- Reset controls
This_Subframe= 0 -- Reset subframe count
RefreshBox(PlSel, movie.currentframe()-1) -- Reset display
gui.repaint() -- Poor timing of on_paint requires a wasteful second call.
end
on_post_load= FrameJump; on_rewind= FrameJump
--*****************************************************************************
function on_input(b)
--*****************************************************************************
-- Make sure what the user sees is being applied to the actual controls.
if (not b) or FrameJumped then -- A new frame.
-- Move the display NOW! The multicore compliant way to do so.
gui.bitmap_blit(inp_Display,0,0, -- Dangerous use of blit.(src=dst)
inp_Display,0,8,95,ExpectedSize*8-9) -- Shift display up one frame.
-- Attempt reset (if any)
if (not b) then -- If this fails, then you've glitched my reset control
if ResetList[movie.currentframe()] then
input.reset(ResetList[movie.currentframe()])
end
end
This_Subframe= 0 -- Keep an internal counter. No lsnes provided call.
end
FrameJumped= false
-- Input trickery
PlayerLoop(function(p)
input.seta(p, HeldBtns[p])
local lsnesInput= input.geta(p) -- I want only one return value!
LastBtns[p]= bit.bor(0x0FFF0000, lsnesInput)
end)
-- Set up on_idle, shall we?
This_Subframe= This_Subframe+1
on_idle = SubframeWatch -- Ensure on_idle is happy.
set_idle_timeout(25000) -- 25 ms. My manual gui.subframe_update()
-- ... I sure hope no TASer finds need to mash subframe advance at 40Hz.
end
--*****************************************************************************
function on_snoop(port,pad,btn,v)
--*****************************************************************************
-- Handle player 1. Ignore the others (this version, anyway).
-- Read up values to properly display last frame status.
local Pl= port*4 + pad
if bit.extract(Players, Pl) == 0 then return end -- Don't handle unexpected
if btn >= 12 then return end -- Don't mess with out-of-range buttons.
LastBtns[Pl]= bit.bor(bit.band(LastBtns[Pl],bit.bnot(bit.value(btn))),
bit.lshift(v, btn)
)
-- Clear lagged status.
local LagBit= bit.lshift(0x10000,btn)
LastBtns[Pl]= bit.band(LastBtns[Pl],bit.bnot(LagBit))
end
print("MtEdit loaded. Please enjoy the script.")
--#############################################################################
--#############################################################################
-- Safeguards. To ensure a previous script has no effect.
gui.subframe_update(false) -- If true, chokes up my fps. Ouch.
on_video= nil -- Not an encoding script
on_frame= nil -- Bad timing for on_paint
on_startup= nil -- May wish to use, if someone manages to load script early?
on_quit= nil -- If I need persistent data, I might want this.
on_reset= nil -- For detecting resets. Unused this version, however.
on_readwrite= nil -- Might be handy. Unused for now.
on_pre_load= nil -- Nothing to handle before it loads.
on_err_load= nil -- Nothing to handle the previous not-handling.
on_pre_save= nil -- If I need to save stuff to the state, might be handy.
on_err_save= nil -- Unused. Not sure of any potential need.
on_post_save= nil -- Unused. Again, not sure of any potential need.