User File #1552198160282053

Upload All User Files

#1552198160282053 - inputreplayer-v0.3.0 for lsnes

inputreplayer-v0.3.0.lua
System: Super Nintendo Entertainment System
1118 downloads
Uploaded 9/30/2012 9:14 PM by partyboy1a (see all 14)
You want to edit something in the middle of your movie without the hassle of manually editing some movie files? This tool is for you!
You want to create a multiplayer TAS with LSNES? Try this one out!
In a future version, this script will also take care about tracking important RAM values. It will make it a lot easier to compare your attempts.
If you want to use this one, you also need the config file.
If you don't understand the functions provided by this script, there is also a manual which is almost as big as the script itself.
---------------------------------
-- lsnes-input-replayer v0.3.0 --
-- by partyboy1a               --
---------------------------------
--
-- see the manual to get a description
-- of all the functions and tables.
--
-- You must change at least one
-- line in the config file before
-- this script starts, right at
-- the beginning.
--
-- have fun with this script!

dofile("inputreplayer-v0.3-config.lua")
-- dofile("inputreplayer-v0.3-memorywatch.lua") -- TODO: not implemented yet to be of any use.
-- dofile("tablehandling.lua") -- TODO: not implemented yet to be of any use.

if (inputreplayer.config.playercount ~= 1 and inputreplayer.config.playercount ~= 2) then
	print("nothing was tested for more than 2 players, good luck.")
end

--[[
	******************
	*                *
	* input handling *
	*                *
	******************
--]]

----- some global constants

-- required because lsnes uses rather comlicated
-- and inconsistent Lua functions for the input.
button_by_index = {
	[4] = "u", [5] = "d", [6] = "l", [7] = "r",
	[8] = "A", [0] = "B", [9] = "X", [1] = "Y",
	[3] = "S", [2] = "s",[10] = "L",[11] = "R"
}

button_by_name = {}
for index,name in pairs(button_by_index) do
	button_by_name[name] = index
end

-- Yes, reading and writing input don't work in the same way!
index_to_player_for_on_snoop = {
	[0] = {[0] = 1, [1] = 6, [2] = 7, [3] = 8},
	[1] = {[0] = 2, [1] = 3, [2] = 4, [3] = 5} --untested for more than 2 players!
}
player_to_index_for_setinput = {
	[1] = 0, [2] = 4, [3] = 5, [4] = 6,
	[5] = 7, [6] = 1, [7] = 2, [8] = 3 --untested for more than 2 players!
}

------ some global functions

-- required for saving ~80% filesize when saving to harddrive
-- TODO: not implemented right now
function compressinput(inputtable)
	local ret = ""
	for i = 0,11 do
		if inputtable[i] == 1 then
			ret = ret .. button_by_index[i]
		else
			ret = ret .. "."
		end
	end
	return ret
end

function decompressinput(inputstring)
	if inputstring == nil then
		return nil
	end
	local ret = {}
	for i = 1,12 do
		if string.sub(inputstring, i, i) ~= "." then
			ret[i-1] = 1
		else
			ret[i-1] = 0
		end
	end
	ret[12] = 0; ret[13] = 0; ret[14] = 0; ret[15] = 0
	return ret
end



--[[
	*********************
	*                   *
	* inputreplayer API *
	* v0.2.2            *
	*                   *
	*********************
--]]
-- here go all the functions which can be called from outside
-- Note: before v1.0.0 don't consider this to be stable or complete
-- Anything might change anytime.
-- Although a lot of these functions are quite trivial right now,
-- you should use these instead of directly manipulating
-- inputreplayer.state

inputreplayer.functions = {}


-- You can set which mode should be activated per player. There
-- are three modes right now: RECORD, REPLAY, INACTIVE
inputreplayer.functions.setmode = function(player, mode)
	assert(	mode == "RECORD"
		or	mode == "REPLAY"
		or	mode == "INACTIVE",
		"invalid mode! (you entered " .. mode ..")")
	inputreplayer.state.player[player].mode = mode
end

-- This will do the following:
-- If there is recorded data available for this frame,
-- the recorded input from player for frame
-- movie.currentframe() + inputreplayer.state.player[player].offset
-- will be offered to all players in the remap table. If the player in
-- the remap table is in REPLAY mode at that time, and there is no other
-- current joypad input for this player at the moment, this offer will
-- be accepted.
inputreplayer.functions.remaprecordedinput = function(player, remap)
	inputreplayer.state.player[player].remaprecordedinput = remap
end

-- This will do the following:
-- If you setup a joypad remap for player, it will overwrite the input
-- from all players listed in the remap table with the input from the
-- specified player. So if you're in RECORD mode, and you have used
-- remapjoypadinput(1,{2}), and remapjoypadinput(2,{1}), all input you
-- do for player 1 will be used AND recorded for player 2 and vice versa.
inputreplayer.functions.remapjoypadinput = function(player, remap)
	inputreplayer.state.player[player].remapjoypadinput = remap
end

-- If you want to apply the recorded input sooner or later than it
-- has been recorded, you can set an offset for this for each player.
-- Offset is applied before remapping is taken into account.
-- positive values will apply the recorded input sooner than it was recorded.
-- negative values will apply the recorded input later  than it was recorded.
inputreplayer.functions.addoffset = function(player,frames)
	inputreplayer.state.player[player].offset = inputreplayer.state.player[player].offset + frames
end

inputreplayer.functions.setoffset = function(player,frames)
	inputreplayer.state.player[player].offset = frames
end

-- if you had a good try, and now want to use this one as a new reference,
-- then you can use this function.
inputreplayer.functions.setnewreference = function()
	for player = 1, inputreplayer.config.playercount do
		if inputreplayer.state.player[player].newinput == nil then
			inputreplayer.state.player[player].newinput = {}
		end
		for frame, inputstring in pairs(inputreplayer.state.player[player].newinput) do
			inputreplayer.state.player[player].input[frame] = inputstring
		end
		inputreplayer.state.player[player].newinput = {}
	end
end

-- Somehow it isn't necessary to use on_snoop and on_input,
-- but this will make sure we definitely record the input
-- exactly in the way it is passed over to bsnes core.
inputreplayer.functions.on_snoop = function(nport, ncontroller, cindex, cvalue)
	local n = index_to_player_for_on_snoop[nport][ncontroller]
	
	if (inputreplayer.state.player[n].mode == "RECORD") then 
		
		local cf = movie.currentframe()
		guitext = "recording frame " .. cf
		
		if inputreplayer.state.player[n].input[cf] == nil
		or type(inputreplayer.state.player[n].input[cf]) == "string" then
			inputreplayer.state.player[n].input[cf] = {}
		end
		inputreplayer.state.player[n].input[cf][cindex] = cvalue
	
		-- assumption: on_snoop is called exactly 16 times per frame per controller
		-- first call is always cindex == 0, last call for the controller is always cindex == 15
		-- on_input doesn't get called in between cindex == 0 and cindex == 15.
		--
		-- compressing input saves a lot of space if the recorded input is stored on harddrive
		if cindex == 15 then 
			inputreplayer.state.player[n].input[cf] = compressinput(inputreplayer.state.player[n].input[cf])
		end
		
	else -- might be replaced by:
	     -- elseif (inputreplayer.state.player[n].mode == "REPLAY") then
	
		local cf = movie.currentframe()
		guitext = "recording frame " .. cf .. " (into last_attempt)"
		
		if inputreplayer.state.player[n].newinput == nil then
			inputreplayer.state.player[n].newinput = {}
		end
		if inputreplayer.state.player[n].newinput[cf] == nil
		or type(inputreplayer.state.player[n].newinput[cf]) == "string" then
			inputreplayer.state.player[n].newinput[cf] = {}
		end
		inputreplayer.state.player[n].newinput[cf][cindex] = cvalue
	
		if cindex == 15 then 
			inputreplayer.state.player[n].newinput[cf] = compressinput(inputreplayer.state.player[n].newinput[cf])
		end
	
	end
end

local emptyjoypad = {}; for i=0,11 do emptyjoypad[i] = 0 end

-- on_input:
-- This will modify the input in the desired way. The modifications are
-- applied in the following order:
-- 1: joypads get remapped.
-- 2: offset is applied to recorded input
-- 3: recorded input gets remapped.
-- 4: now per player:
--       either the joypad input or the recorded input is used.
inputreplayer.functions.on_input = function()
	local cf = movie.currentframe()

	-- first step: get all joypad input from all controllers
	local realjoypadinput = {}
	for player = 1, inputreplayer.config.playercount do
		local pnum = player_to_index_for_setinput[player]
		-- if player > 1 then extraguitext = ""..pnum  end
		realjoypadinput[player] = {}
		for i = 0, 11 do
			realjoypadinput[player][i] = input.get(pnum, i)
		end
	end

	-- second step: remap this input according to remapping rules for joypad
	local remappedjoypadinput = {}
	
	for player = 1, inputreplayer.config.playercount do
		local remap = inputreplayer.state.player[player].remapjoypadinput
		if remap ~= nil then
			for dontcare,newplayer in ipairs(remap) do
				remappedjoypadinput[newplayer] = realjoypadinput[player]
			end
		end
	end
	for player = 1, inputreplayer.config.playercount do
		remappedjoypadinput[player] = remappedjoypadinput[player] or realjoypadinput[player]
	end
	
	-- third step: get all recorded input from all controllers
	
	-- TODO: combining remapping + offset might work counter-intuitive.
	-- Test if this works in the way you think it works. If not:
	-- rewrite this and the next step.
	local realrecordedinput = {}
	for player = 1, inputreplayer.config.playercount do
		local offset = inputreplayer.state.player[player].offset
		realrecordedinput[player] = decompressinput(inputreplayer.state.player[player].input[cf + offset])
	end
	
	-- fourth step: remap this recorded input according to remapping rules for recorded input
	local remappedrecordedinput = {}
	
	for player = 1, inputreplayer.config.playercount do
		local remap = inputreplayer.state.player[player].remaprecordedinput
		if remap ~= nil then
			for dontcare,newplayer in ipairs(remap) do
				remappedrecordedinput[newplayer] = realrecordedinput[player]
			end
		end
	end
	for player = 1, inputreplayer.config.playercount do
		remappedrecordedinput[player] = remappedrecordedinput[player] or realrecordedinput[player]
	end
	
	-- fifth step: here goes the final input!
	local finalinput = {}
	
	extraguitext = ""
	-- NOW: react to this input in the right way
	for player = 1, inputreplayer.config.playercount do
		
		if inputreplayer.state.player[player].mode == "REPLAY" then
			-- REPLAY mode: writes recorded input if
			--   - there is no joypad input for the controller
			--        (otherwise: become inactive)
			--   - there is recorded input available
			--        (otherwise: show error)
			
			-- check if there was some input for the controller
			local custominput = 0
			for i = 0, 11 do 
				custominput =
					  custominput 
					+ remappedjoypadinput[player][i] -- TODO: check if that works for more than 2 players
			end
			custominput = (custominput > 0)
			
			-- Yes? Then become inactive!
			if custominput then
				--extraguitext = "custom input used, switching to inactive"
				inputreplayer.functions.setmode(player, "INACTIVE")
				finalinput[player] = remappedjoypadinput[player]
			
			-- No, but there is no recorded input available? Show a message!
			elseif remappedrecordedinput[player] == nil then
			
				print("ERROR: no input found for frame " .. cf)
				finalinput[player] = emptyjoypad
			
			-- No, and we have recorded input? Fine, use it!
			else
				finalinput[player] = remappedrecordedinput[player]
				--[[extraguitext = "input overwritten for frame " .. cf .. ", used " ..
					compressinput(finalinput[player]) .. " instead"--]]
			end
		
		else
			
			-- All our work is done already. Neither INACTIVE nor RECORD mode tries
			-- to alter the joypad input, and neither of them uses recorded data.
			finalinput[player] = remappedjoypadinput[player]
			
		end
	end -- for player = 1, inputreplayer.config.playercount
	
	-- finally: set all the input accordingly to the rules above
	inputtext = {}
	for player = 1, inputreplayer.config.playercount do
		local pnum = player_to_index_for_setinput[player]
		for i = 0, 11 do
			input.set(pnum, i, finalinput[player][i])
		end
		
		-- for displaying the input on the UI, as the built-in
		-- display doesn't take care of Lua modifications to
		-- the input, and that can be quite confusing.
		-- (there is for sure something better than this)
		inputtext[player] = "player "..player..": " .. compressinput(finalinput[player])
	end
end

-- Here goes the internal state of inputreplayer.
-- Not to be modified from outside.
inputreplayer.state = {}
inputreplayer.state.player = {}
for player = 1, inputreplayer.config.playercount do
	inputreplayer.state.player[player] = {
		input = {},
		offset = 0,
		remaprecordedinput = nil, -- not required, but it's here
		remapjoypadinput = nil,   -- to give you the right names
		newinput = nil            -- for the variables.
	}
	inputreplayer.functions.setmode(player, "RECORD")
end

--[[
	*******************
	*                 *
	* menu navigation *
	*                 *
	*******************
--]]

-- built-in menu. All functions called from here should be treated like external functions,
-- they shouldn't manipulate anything directly, they should use inputreplayer.functions
-- instead.
-- Everything here should  be pretty self-explanatory.
inputreplayer_ui.menu = {
	[1] = {
		text = "set mode",
		submenu = {
		
			[1] = {
				text = "player 1: switch to REPLAY",
				action = function()
					inputreplayer.functions.setmode(1, "REPLAY")
				end
			},
			[2] = {
				text = "player 1: switch to INACTIVE",
				action = function()
					inputreplayer.functions.setmode(1, "INACTIVE")
				end
			}, 
			[3] = {
				text = "player 1: switch to RECORD",
				action = function()
					inputreplayer.functions.setmode(1, "RECORD")
				end
			}, 
			[4] = {
				text = "player 2: switch to REPLAY",
				action = function()
					inputreplayer.functions.setmode(2, "REPLAY")
				end
			}, 
			[5] = {
				text = "player 2: switch to INACTIVE",
				action = function()
					inputreplayer.functions.setmode(2, "INACTIVE")
				end
			}, 
			[6] = {
				text = "player 2: switch to RECORD",
				action = function()
					inputreplayer.functions.setmode(2, "RECORD")
				end
			}, 
			[7] = {
				text = "ALL: switch to REPLAY",
				action = function()
					inputreplayer.functions.setmode(1, "REPLAY")
					inputreplayer.functions.setmode(2, "REPLAY")
				end
			}, 
			[8] = {
				text = "ALL: switch to INACTIVE",
				action = function()
					inputreplayer.functions.setmode(1, "INACTIVE")
					inputreplayer.functions.setmode(2, "INACTIVE")
				end
			},					
			[9] = {
				text = "ALL: switch to RECORD",
				action = function()
					inputreplayer.functions.setmode(1, "RECORD")
					inputreplayer.functions.setmode(2, "RECORD")
				end
			}
			
		} -- submenu main.setmode
	}, -- main.1
	
	[2] = {
		text = "set offset (player 1)",
		-- setting "goback" to false will let the menu stay where it is,
		-- allowing us to execute these actions multiple times in a  row.
		-- (for example, to set offset to 23, you will use "+1" three times,
		-- and "+10" two times). Use the menu key to leave this menu.
		submenu = {
			[1] = {text = "+1",    action = function() inputreplayer.functions.addoffset(1, 1)     end, goback=false},
			[2] = {text = "+10",   action = function() inputreplayer.functions.addoffset(1, 10)    end, goback=false},
			[3] = {text = "+100",  action = function() inputreplayer.functions.addoffset(1, 100)   end, goback=false},
			[4] = {text = "+1000", action = function() inputreplayer.functions.addoffset(1, 1000)  end, goback=false},
			[5] = {text = "-1",    action = function() inputreplayer.functions.addoffset(1, -1)    end, goback=false},
			[6] = {text = "-10",   action = function() inputreplayer.functions.addoffset(1, -10)   end, goback=false},
			[7] = {text = "-100",  action = function() inputreplayer.functions.addoffset(1, -100)  end, goback=false},
			[8] = {text = "-1000", action = function() inputreplayer.functions.addoffset(1, -1000) end, goback=false},
			[9] = {text = "reset", action = function() inputreplayer.functions.setoffset(1, 0)     end, goback=false}
		}
	},
	[3] = {
		text = "set offset (player 2)",
		submenu = {
			[1] = {text = "+1",    action = function() inputreplayer.functions.addoffset(2, 1)     end, goback=false},
			[2] = {text = "+10",   action = function() inputreplayer.functions.addoffset(2, 10)    end, goback=false},
			[3] = {text = "+100",  action = function() inputreplayer.functions.addoffset(2, 100)   end, goback=false},
			[4] = {text = "+1000", action = function() inputreplayer.functions.addoffset(2, 1000)  end, goback=false},
			[5] = {text = "-1",    action = function() inputreplayer.functions.addoffset(2, -1)    end, goback=false},
			[6] = {text = "-10",   action = function() inputreplayer.functions.addoffset(2, -10)   end, goback=false},
			[7] = {text = "-100",  action = function() inputreplayer.functions.addoffset(2, -100)  end, goback=false},
			[8] = {text = "-1000", action = function() inputreplayer.functions.addoffset(2, -1000) end, goback=false},
			[9] = {text = "reset", action = function() inputreplayer.functions.setoffset(2, 0)     end, goback=false}
		}
	},
	
	[4] = {
		text = "remap recorded input",
		submenu = {
			[1] = {
				text = "delete all remappings",
				action = function()
					inputreplayer.functions.remaprecordedinput(1, nil)
					inputreplayer.functions.remaprecordedinput(2, nil)
				end
			},
			[2] = {
				text = "1 -> 2 and 2 -> 1",
				action = function()
					inputreplayer.functions.remaprecordedinput(1, {2})
					inputreplayer.functions.remaprecordedinput(2, {1})
				end
			},
			[3] = {
				text = "1 -> 1+2",
				action = function()
					inputreplayer.functions.remaprecordedinput(1, {1, 2})
				end
			},
			[4] = {
				text = "2 -> 1+2",
				action = function()
					inputreplayer.functions.remaprecordedinput(2, {1, 2})
				end
			}
		} --menu.remaprecordedinput
	}, -- menu.4
	[5] = {
		text = "remap joypad input",
		submenu = {
			[1] = {
				text = "delete all remappings",
				action = function()
					inputreplayer.functions.remapjoypadinput(1, nil)
					inputreplayer.functions.remapjoypadinput(2, nil)
				end
			},
			[2] = {
				text = "1 -> 2 and 2 -> 1",
				action = function()
					inputreplayer.functions.remapjoypadinput(1, {2})
					inputreplayer.functions.remapjoypadinput(2, {1})
				end
			},
			[3] = {
				text = "1 -> 1+2",
				action = function()
					inputreplayer.functions.remapjoypadinput(1, {1, 2})
				end
			},
			[4] = {
				text = "2 -> 1+2",
				action = function()
					inputreplayer.functions.remapjoypadinput(2, {1, 2})
				end
			}
		} --menu.remapjoypadinput
	}, -- menu.5
	[6] = {
		text = "set new reference",
		action = function()
			inputreplayer.functions.setnewreference()
		end
	}
} -- menu


-- The "stack" solution works very good for using the menu
-- TODO: There should be no single line of code inside the menu code
-- which directly writes to inputreplayer.state. They should be completely
-- separated.
inputreplayer_ui.state = {}

inputreplayer_ui.state.menustack = {}
inputreplayer_ui.state.menustack[1] = inputreplayer_ui.menu
inputreplayer_ui.state.menustacktop = 1

-- This function is used to navigate through the menu while the menu
-- is visible. If it is hidden at the moment, nothing will happen until
-- it is explicitly opened again.
inputreplayer_ui.functions = {}

inputreplayer_ui.functions.menu_navigate = function(num)
	if num == 0 then -- go back one step, or let the menu appear
		if inputreplayer_ui.state.menustacktop > 0 then
			inputreplayer_ui.state.menustack[inputreplayer_ui.state.menustacktop] = nil
			inputreplayer_ui.state.menustacktop = inputreplayer_ui.state.menustacktop - 1
		else
			inputreplayer_ui.state.menustack[1] = inputreplayer_ui.menu
			inputreplayer_ui.state.menustacktop = 1
		end
	end

	local currentmenu = inputreplayer_ui.state.menustack[inputreplayer_ui.state.menustacktop]
	-- extraguitext = extraguitext .. '\n' .. "menunavigate "..num -- for debugging purposes
	
	-- currentmenu == nil:
	--    menu is hidden at the moment -> ignore input
	-- currentmenu[num] == nil:
	--    item number num doesn't exist in the current menu -> ignore input
	if currentmenu and currentmenu[num] then
		if currentmenu[num].submenu then
			inputreplayer_ui.state.menustacktop = inputreplayer_ui.state.menustacktop + 1
			inputreplayer_ui.state.menustack[inputreplayer_ui.state.menustacktop] = currentmenu[num].submenu
		elseif currentmenu[num].action then
			currentmenu[num].action()
			
			-- note:
			--      currentmenu[num].goback == true
			-- is NOT the same value as
			--      currentmenu[num].goback ~= false
			-- because if currentmenu[num].goback == nil,
			-- the first statement will return false,
			-- the second statement will return true.
			if currentmenu[num].goback ~= false then
				inputreplayer_ui.state.menustack[inputreplayer_ui.state.menustacktop] = nil
				inputreplayer_ui.state.menustacktop = inputreplayer_ui.state.menustacktop - 1
			end
		end
	end
	
	gui.repaint()
end

-- on_paint: used to display the current state of inputreplayer
--   and to display the menus
inputreplayer_ui.functions.on_paint = function()
	-- print the menu on the left side of the screen, at the bottom.
	local lgap = inputreplayer_ui.config.lgap
	gui.left_gap(lgap)
	local currentmenu = inputreplayer_ui.state.menustack[inputreplayer_ui.state.menustacktop]
	if currentmenu ~= nil then
		for i,item in ipairs(currentmenu) do
			gui.text(-lgap,200+i*20,i .. ": " .. item.text)
		end
		gui.text(-lgap + 120, 180,extraguitext) -- for debugging purposes
	end
	
	-- display current internal state at the left side of the screen, at the top.
	gui.text(-lgap,  0, "mode player 1: " .. inputreplayer.state.player[1].mode)
    gui.text(-lgap, 20, "offset player 1: " .. inputreplayer.state.player[1].offset)
	gui.text(-lgap, 40, "mode player 2: " .. inputreplayer.state.player[2].mode)
	gui.text(-lgap, 60, "offset player 2: " .. inputreplayer.state.player[2].offset)
	
	function temp(t) local s = ""; for dontcare,n in ipairs(t) do s = s .. n .. "; " end; return s; end
	for player = 1, inputreplayer.config.playercount do
		gui.text(-lgap, 80 + 20 * (player - 1), "  joypad ".. player .." -> " .. temp(inputreplayer.state.player[player].remapjoypadinput or {"default"}))
		gui.text(-lgap, 120+ 20 * (player - 1), "recorded ".. player .." -> " .. temp(inputreplayer.state.player[player].remaprecordedinput or {"default"}))
	end
	temp = nil
	
	-- display which input was applied to the last frame.
	gui.text(-lgap, 400, inputtext[1] or "-/-")
	gui.text(-lgap, 420, inputtext[2] or "-/-")
end
	
--[[ 
	*********************
	*                   *
	* keyhook functions *
	*                   *
	*********************
--]]
-- Here we create one function per key to react accordingly to the key which was pressed.

-- the only place with a global variable, to let you add your custom
-- keyhook functions without having to change anything inside inputreplayer or inputreplayer_ui
keyhook = {}
assignkey = function(key,func)
	keyhook[key] = func
	input.keyhook(key, func ~= nil)
end

assignkey(
	inputreplayer_ui.config.keys.menu,
	function()
		inputreplayer_ui.functions.menu_navigate(0)
	end
)

for i = 1, 9 do
	assignkey(
		inputreplayer_ui.config.keys.item[i],
		function()
			inputreplayer_ui.functions.menu_navigate(i)
		end
	)
end

function on_keyhook(key, state)
	-- check if there is a matching keyhook function
	-- and if the button was pressed. (If state.last_rawval == 0 it was released)
	--
	-- assumption: this function is called exactly once when the key is hold down,
	-- and once the key is released again.
	if keyhook[key] and state.last_rawval == 1 then
		-- extraguitext = "on_keyhook "..key --.. "/" .. state.last_rawval -- for debugging purposes
		keyhook[key]()
	end
end

extraguitext = "" -- for debugging purposes
inputtext = {} -- to display the input the script caused.

function on_paint()
	inputreplayer_ui.functions.on_paint()
end

function on_snoop(nport, ncontroller, cindex, cvalue)
	inputreplayer.functions.on_snoop(nport, ncontroller, cindex, cvalue)
end

function on_input()
	inputreplayer.functions.on_input()
end

print("inputreplayer v0.3.0 ready")