Skip to content

Instantly share code, notes, and snippets.

@taotao54321
Created March 6, 2023 07:50
Show Gist options
  • Save taotao54321/1bf4cd801f8de4cddaa4d1e94b99d7d2 to your computer and use it in GitHub Desktop.
Save taotao54321/1bf4cd801f8de4cddaa4d1e94b99d7d2 to your computer and use it in GitHub Desktop.
Plays a NesHawk movie on SubNesHawk
--[[
Plays a NesHawk movie on SubNesHawk.
Set INPUT_PATH, and launch this script (at frame 0).
Power/Reset is not supported.
--]]
-- extract "Input Log.txt" from your NesHawk movie, and put it to the script directory.
-- (or specify an absolute path)
local INPUT_PATH = "Input Log.txt"
local function str_at(s, idx)
return string.sub(s, idx, idx)
end
local function str_starts_with(s, prefix)
return string.find(s, prefix, 1, true) == 1
end
local function panic(msg)
error(msg)
end
-- parses a player input. (e.g. "UDLRSsBA")
local function parse_input_player(s)
local CHARS = { "U", "D", "L", "R", "S", "s", "B", "A" }
local NAMES = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A" }
if string.len(s) ~= 8 then
return nil
end
local input = {}
for i = 1, 8 do
local ch = str_at(s, i)
if ch == "." then
-- do nothing
elseif ch == CHARS[i] then
input[NAMES[i]] = true
else
return nil
end
end
return input
end
-- parses a frame line. (e.g. "|..|UDLRSsBA|")
local function parse_input_frame(line)
local invalid = function ()
panic(string.format("invalid input frame: %s", line))
end
if not str_starts_with(line, "|") then
invalid()
end
-- current position in the line.
local pos = 2
-- skip power/reset field.
do
local pos_sep = string.find(line, "|", pos, true)
if pos_sep == nil then
invalid()
end
pos = pos_sep + 1
end
-- array for each player's input.
local inputs = {}
-- parse inputs, up to 4 players.
for _i = 1, 4 do
local pos_sep = string.find(line, "|", pos, true)
if pos_sep == nil then
break
end
local s = string.sub(line, pos, pos_sep - 1)
local input = parse_input_player(s)
if input == nil then
invalid()
end
table.insert(inputs, input)
pos = pos_sep + 1
end
-- at least, player 1's input must exist.
if #inputs == 0 then
invalid()
end
return inputs
end
-- parses "Input Log.txt".
local function parse_input(path)
local rdr = io.open(path, "r")
if rdr == nil then
panic(string.format("can't open file '%s'", path))
end
local lines = rdr:lines()
-- check for header.
do
local line_first = lines()
if line_first ~= "[Input]" then
panic("invalid input: '[Input]' not found")
end
local line_second = lines()
if not str_starts_with(line_second, "LogKey:") then
panic("invalid input: 'LogKey:' not found")
end
end
-- array for each frame.
local frames = {}
-- parse each frames.
for line in rdr:lines() do
if line == "[/Input]" then
break
end
local inputs = parse_input_frame(line)
table.insert(frames, inputs)
end
rdr:close()
return frames
end
local function play(frames)
-- NOTE: you can't use emu.islagged() to detect VSYNC.
-- use emu.lagcount().
local frame = emu.lagcount()
for _i, inputs in ipairs(frames) do
repeat
for j, input in ipairs(inputs) do
joypad.set(input, j)
end
emu.frameadvance()
until frame ~= emu.lagcount()
frame = emu.lagcount()
end
end
local function main()
local frames = parse_input(INPUT_PATH)
play(frames)
end
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment