Editor, Skilled player (1158)
Joined: 9/27/2008
Posts: 1084
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.lua
Language: 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.
Player (83)
Joined: 7/25/2011
Posts: 58
Hey FatRatKnight, could you please help me understand this script? I am very interested in being able to make modifications to the input, as well as, identifying lag frames. I've read through the script a few times and just couldn't understand how to do those things. Also, is there any way to disable the re-mapping of the keys? I understand that's a good feature to help people get started with lsnes, but I already have them mapped. Not a big deal, just curious.
Editor, Skilled player (1158)
Joined: 9/27/2008
Posts: 1084
I have been away for such a long time, that I can't even remember most of the details of this script. I recall having trouble getting the emulator's configuration through the lua side, so that's why the override from the script. Until I take a second look, I will say I haven't figured out how to read the lsnes configuration from lua. I also recall an annoying bug in the current script, but I can't recall the particular details. Something about the script failing to get the right input on state load or something. Until I can put the time into figuring out how my own stuff works again, you're more or less on your own. Which is too bad, since I'm really the only "expert" on this script, and I've lost any memories necessary to give a decent answer. As there does not appear to be anyone else who would know the details, this means there's no one alive who can help. At least, not at this moment. My response is very late, as I've only now saw your message. And I apologize for the lack of help, but there isn't much I can say. The moment I can devote the time to figuring out how my own script works, I will get on explaining the details.
AnS
Emulator Coder, Experienced player (723)
Joined: 2/23/2006
Posts: 682
FatRatKnight wrote:
Until I can put the time into figuring out how my own stuff works again, you're more or less on your own. Which is too bad, since I'm really the only "expert" on this script, and I've lost any memories necessary to give a decent answer.
Hey I know that feeling... That's basically why I've spent more time on documenting TAS Editor than on coding it. Knowledge is too ephemeral when not formalized properly.
Player (83)
Joined: 7/25/2011
Posts: 58
Haha no worries. I should apologize for waiting to try it out. It is still a very useful script for proofing my inputs, and I can run a separate instance in lsnes and follow that input if I made changes earlier in the movie. So thanks for that!
Player (136)
Joined: 9/18/2007
Posts: 389
That's a nice little script, and it's even quite intuitive in this version. Well, it would have been, if there were instructions on how to use it... But finally I got it So here is how this one works if you want to try it for a two-player game ("the lights" are those circles directly below the input display) - savestate, and play around a bit with player one. press "z" (-> those red dots on the bottom turn green), then "l" (-> lights at the bottom turn red, and the second controller on the bottom gets activated) - loadstate, and now use the same buttons to play around with player 2. when you're satisfied with the results for player two, press "z" (-> lights turn green), then "l" (-> lights stay green, first controller gets activated), then "z" (-> lights turn red). savestate. - play around as player one. press "z" (lights turn green), then "l" (-> 2nd player is activated, lights stay green), then "z" (lights turn red). loadstate - play around as player two. press "z" (lights turn green), then "l" (-> 1st player is activated, lights stay green), then "z" (lights turn red). Now you should know how it works... which controller is active, and which buttons are to be held can be seen clearly at the bottom. there is also a "sticky" input mode which can be activated and deactivated by pressing "x", which is essentially the same as "typing input". for example, pressing x,d,frameadvance,frameadvance one after another will hold down "right" for the first frame, and release it for the second one. pressing x,d,d,frameadvance will result in not having pressed "right" at all. when the lights are red for a specific controller, previous input for these buttons is ignored. when the lights are green, then either the previous input is used, or -- if this player is active at that time -- the new input is XORed with the previous input. I think this script could also support "mixing" red and green lights. This would turn out to be useful if you want to try different frames for a specific button, but want to keep the rest of your input untouched.
Editor, Skilled player (1158)
Joined: 9/27/2008
Posts: 1084
Well, well... http://tasvideos.org/userfiles/info/6680317080319470 I have a completely rewritten MtEdit now. The changes to the internals are vast, but hopefully the script is a bit more intuitive. Or, at the least, just as intuitive. v2 should be able to do most of the things v1 did, but if there's anything I messed up on, I really would like to know. I'm a little burnt out to be writing full instructions now, but just run the script. Stuff should show up along the bottom and to the left. All the controls for player 1 is used for the current controller. If you have something in port 2, the first controller of that port should (theoretically) be used as well, but I haven't tested that. While running it, it can pick up input from either your TASing or from a movie playing on read-only. When you load state, or otherwise go back to some earlier point, take note that the script will automatically attempt to inject what it already has seen, on the appropriate frames. Here's the "Edit" part of MtEdit: Your controls will still affect the input stream! Still lacks a few pretty critical functions, but hopefully I can start pulling things together better now. Inserts and deletes would be wonderful things to include in there.