Skip to content

Instantly share code, notes, and snippets.

@Zinfidel
Created August 29, 2021 19:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zinfidel/047a31b5fb0e3d86854e0cf8e7b00c80 to your computer and use it in GitHub Desktop.
Save Zinfidel/047a31b5fb0e3d86854e0cf8e7b00c80 to your computer and use it in GitHub Desktop.
Scripts for force-syncing movies in BizHawk (particularly N64 movies with different plug-in settings). See http://tasvideos.org/Zinfidel/SyncScripts.html for usage.
package.path = package.path .. ';luasocket/lua/?.lua;luasocket/lua/socket/?.lua'
package.cpath = package.cpath .. ';luasocket/?.dll;luasocket/mime/?.dll;luasocket/socket/?.dll'
local socket = require 'socket'
local socClient
local server
local onExitEvent = event.onexit(
function()
socClient:close()
server:close()
console.log("Client closed.")
end
, "Exit Event" );
local function endsWith(str, ending)
return ending == "" or str:sub(-#ending) == ending
end
local function strChop(str, ending)
return str:sub(1, -(#ending + 1))
end
-- Pause the client and establish a socket listener.
client.pause()
server = assert(socket.bind("127.0.0.1", 45678), "Socket server failed to bind to ip/port. Script aborted.")
console.write("Waiting on connection...")
emu.yield()
server:settimeout(5)
server:setoption('keepalive', true)
-- Wait for the other instance to connect.
while true do
local err = nil
socClient, err = server:accept()
if not err then
console.log(" Connected!")
emu.yield()
break
else
console.write('.')
end
emu.yield()
end
socClient:settimeout(10)
console.write("Awaiting a message ...")
while true do
emu.yield()
local line, err = socClient:receive()
if not err then
if line == "quit" then
console.log("\nReceived exit message. Exiting now.")
break;
elseif endsWith(line, ".State") then
local frameNum = tonumber(strChop(line, ".State"))
console.write("\nReceived request for a savestate at frame " .. tostring(frameNum) .. ". Seeking...")
emu.yield()
client.seekframe(frameNum)
savestate.save(line)
console.log("\nCreated " .. line .. ". Sending to the client.")
emu.yield()
socClient:send(line .. "\n")
console.write("\nAwaiting a message ...")
else
console.log("\nUnknown command " .. line .. " received.")
end
elseif err == "timeout" then
console.write('.')
elseif err == "closed" then
console.log("\nConnection with client closed. Exiting now.")
break;
else
console.log("\nNo message received. Error: " .. err)
end
emu.yield()
end
-- This script is for collecting "sync data" for a game that consists of some sort of canary data
-- that is very likely to indicate a desync happening. Good choices for this kind of data are transformation
-- matrices for models, or manually collected position data like x,y,z,pitch,yaw,roll,etc.
-- Change this to some amount of frames on which data will be flushed. 200 means flush to file every 200 frames.
FLUSH_FRAMES = 200
-- Check if a file exists on the filesystem
local function file_exists(name)
local f=io.open(name,"r")
if f~=nil then io.close(f) return true else return false end
end
-- Create a "log.txt" file for the data, but add a number so we don't accidentally overwrite previous results.
local name = "log.txt"
local i = 1
while (file_exists(name)) do
name = "log" .. tostring(i) .. ".txt"
i = i + 1
end
LogFile = io.open(name, "w")
-- Flush and close the log file when the script closes.
local onExitEvent = event.onexit(
function()
LogFile:close()
console.log("")
console.log("Script stopping. Closing log file.")
end
, "Exit Event" );
-- Collect sync data
console.write("Flushing to file ")
while true do
-- Custom data collection for the game goes in here. The example code below is for Star Wars Episode 1: Racer,
-- it finds the player's position matrix and records each matrix entry as a float to 5 decimals. It writes these
-- data to a comma-separated line.
---------------------------------------------------------------------------------------------------------------
Racedata = 0x118F90
Statedata = mainmemory.read_u24_be(Racedata+0x84+0x1)
MatrixStart = Statedata + 0x1C
for j=0, 15, 1 do
LogFile:write(string.format("%.5f",mainmemory.readfloat(MatrixStart + 4*j, true)))
if (j ~= 15) then LogFile:write(',') end
end
---------------------------------------------------------------------------------------------------------------
-- End custom collection code
-- New line, and if we are at a flush boundary, flush the file.
LogFile:write('\n')
if (emu.framecount() % FLUSH_FRAMES) == 0 then
LogFile:flush()
console.write('.')
end
emu.frameadvance()
end
-- feos, 2017
-- DK64 resync workflow
-- props to thecoreyburton
t = {} -- array of state names found
count = 0 -- total states found, just for print()
av_paused = false -- pretend we have that info from the emu
offset = 10 -- av delay after stated frame
-- scan the script dir for states and store their names to array
function scandir()
local popen = io.popen -- use cmd
local pfile = popen('dir . /b') -- cmd arguments
local i = 0
local index = 0
local name = ""
for filename in pfile:lines() do
i = i + 1
-- skip all but state files
if filename:find(".State") then
-- strip frame numbers
name = string.gsub(filename, ".State", "")
index = tonumber(name)
t[index] = filename
count = count + 1
end
end
pfile:close()
end
scandir()
print("States found: " .. count) -- #t doesn't work for some reason (gaps in the table?)
last_frame = 0
while true do
if movie.isloaded() then
-- check if there's a state of "offset" frames back
local name = t[emu.framecount() - offset]
-- suspend av and load the state, with a delay
if name and not av_paused then
client.pause_av()
av_paused = true
savestate.load(name)
print("Loaded " .. name)
end
-- basic report you'd barely see
if av_paused then
gui.text(0, 40, "AV paused")
end
-- resume av when that frame occurs again
if t[emu.framecount() - offset] and av_paused then
client.unpause_av()
av_paused = false
end
end
emu.frameadvance()
end
package.path = package.path .. ';luasocket/lua/?.lua;luasocket/lua/socket/?.lua'
package.cpath = package.cpath .. ';luasocket/?.dll;luasocket/mime/?.dll;luasocket/socket/?.dll'
local socket = require 'socket'
local tab = {} -- entire parsed file broken into lines
local gap_minimum = 10 -- Set the minimum number of frames between savestates. This can help prevent thrashing in bad sections.
function ParseFile()
print("Parsing file...")
local logfile = io.open("log.txt", "r")
local char
repeat
char = logfile:read("*l")
table.insert(tab, char)
until char == nil
logfile:close()
print("File parsed! Line count: "..#tab.."\n")
end
-- Customize this function to retrieve your sync data and format it the same way as it appears in log.txt. An example of how
-- to do this for N64 Star Wars Episode 1: Racer, matching the example in sync-collect.lua, is the default implementation. this
-- value will get compared, as a string, to the corresponding line in log.txt to verify sync.
function GetData()
Racedata = 0x118F90
Statedata = mainmemory.read_u24_be(Racedata+0x84+0x1)
SyncData = {}
MatrixStart = Statedata + 0x1C
for i=0, 15, 1 do
table.insert(SyncData, string.format("%.5f",mainmemory.readfloat(MatrixStart + 4*i, true)))
end
return table.concat(SyncData, ",")
end
-- Pause the client and establish a tcp connection to the server.
client.pause()
local tcp = assert(socket.connect("127.0.0.1", 45678), "Socket client failed to establish tcp connection with server. Script aborted.")
console.write("Connection to server established!\n")
tcp:settimeout(2)
emu.yield()
ParseFile()
local onExitEvent = event.onexit(
function()
console.log("\nScript exiting. Sending quit command to server." );
tcp:send("quit\n")
end
, "OnExit" );
local last_ss = 0
while true do
local frame = emu.framecount()
local original = tab[frame+1]
local yours = GetData()
gui.pixelText(0, 40, string.format( "%s - original\n%s - yours", original, yours))
-- mismatch
if (yours ~= original) and ((frame - last_ss) > gap_minimum) then
last_ss = frame
client.pause()
-- screen and console alarm
gui.pixelText(0, 40, string.format( "%s - original\n%s - yours\ndesync at frame %d", original, yours, frame), "red")
print(string.format( "desync at frame %d\n%s\n%s\n", frame, yours, original))
emu.yield()
local response = ""
tcp:send(frame .. ".State\n")
console.write("Sent packet asking for frame " .. tostring(frame) .. " ...")
emu.yield()
while true do
local line, err, partial = tcp:receive()
if not err then
response = line
console.log("\nReceived message that savestate " .. tostring(frame) .. " is available. Loading ...")
emu.yield()
savestate.load(response)
client.unpause()
break
elseif err == "timeout" then
response = response .. partial
console.write('.')
elseif err == "closed" then
console.log("\nConnection with server closed. Exiting now.")
break
else
console.log("\nUnknown communication error.")
end
emu.yield()
end
end
-- so far so good
emu.frameadvance()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment