User File #638757645500544857

Upload All User Files

#638757645500544857 - TAStudio Output Logger Framework

TAStudioLogger.lua
1 download
Uploaded 16 hours ago by TASeditor (see all 191)
A framework to streamline logging memory values and outputting them in TAStudio and saving and loading them with branches as well as script starting and closing.
Version 0.9, not fully done yet. Runs in Bizhawk 2.10

To get started:
This script is not meant to be run in the Lua Console!
Save this lua script in your lua folder and make a new empty lua script.
In the new script copy the following:
require("TAStudioLogger")

-- Add your loggers. e.g.: AddLogger("XSpeed", function() return memory.read_s8(0x1234) end)
-- Or AddLogger("Grounded", function() return memory.read_u8(0xABCD) & 2 end)

-- Add your printers, e.g.: AddPrinter("XSpd" "XSpeed", function(index, value) return ColorGradient(index, math.abs(value), 100 0xFFFF0000, 0xFF00FF00) end)
-- Or AddPrinter("P1 A", "Grounded", 0xFFE0A3014)

InitializeLogger()

while true do
	
	UpdateLogger() 
	
	-- Add your own usual lua functionality

	emu.frameadvance();
end
Load your scipt into the Lua Console and run it with TAStudio open.

-- TAStudio Logger
-- written by TASeditor
-- version 0.9, BizHawk 2.10
-- Not to be run in the Lua Console

--------------
--	Logger  --
--------------

local Logs = { }
local Printers = { }
local branches = { }

--[[Adds a logger to the Logs table
	Expressionfunction needs to be a function which takes no argument and returns a number.
	The value from that function will be stored in the Logs[name].values for each frame.]]
function AddLogger(name, expressionfunction)

	if Logs[name] ~= nil
	then console.log("WARNING: "..name.." is already logged")
		 return
	end
	
	Logs[name] = {expression = expressionfunction,
				  values = {}}
				  
end

--[[Adds a printer to the Printers table for outputing text and color into the TAStudio list.
	Sets predefined function depending on the parameters passed.
	Or the printer uses functions passed as parameters.
	
	Columnname is a string for TAStudio button list header.
	
	valueexpression needs to be a string or a function. 
	As a string it needs to be the name for a Logs table. 
	As a function it needs to be a function that takes a number (index) as an argument and returns a number.
	
	colorexpression will define which colors will be drawn into the TAStudio list.
	It can be a a string with "#RRGGBB" or "#AARRGGBB" (A)RGB values.
	A number in the form of 0xAARRGGBB ARGB hexadecimal color value.
	A table with value/color pairs in which each value can be assigned a color.
	A function which takes two numbers (index, value) as an argument and returns a color.
	As nil or no parameter passed as a parameter for colorexpression, no color will be drawn.
	
	textexpression will define how text will be displayed in the TAStudio list.
	No text can be printed into button columns.
	When nil or no parameter passed the value for each frame will be displayed.
	It can be a string which will be displayed if the value for the corresponding frame is greater than zero. "" to display no text.
	A table with value/text pairs in which each value can be a assigned a text.
	False for which no text will be displayed.
	A function which takes a number (value) and a number (index) as and optional paramenter as arguments and returns a string.
	
	Digits is the number of digits to be displayed as text in TAStudio list, default is 3.]]
function AddPrinter(columnname, valueexpression, colorexpression, textexpression, digits)

	if Printers[columnname] ~= nil
	then console.log(columnname.." is already printed")
		 return
	end
	
	-- valueexpression parameter check
	if type(valueexpression) == "string" 
	then if Logs[valueexpression] == nil 
		 then console.log("WARNING: Log with the name "..valueexpression.." does not exist.")
			  return
		 else local s = valueexpression
			  valueexpression = function(index) return Logs[s].values[index] end --  Value will be the value in the Log as is with the name of the string.
		 end
		 
	elseif type(valueexpression) ~= "function" -- Value will be the number returned from the function
		then console.log("WARNING: Second parameter valueexpression must be a string or a function.")
			 return
	end
	
	-- colorexpression parameter check
	if type(colorexpression) == "table"
	then local c = colorexpression
		 colorexpression = function(index, value) return MultiColor(index, value, c) end
		 
	elseif type(colorexpression) == "number" -- 0xAARRGGBB ARGB value
	then local c = colorexpression
		 colorexpression = function(index, value) return Color(index, value > 0, c) end
		 
	elseif type(colorexpression) == "string" 
	then if string.len(colorexpression) == 7 and string.len(string.match(colorexpression, "#%x%x%x%x%x%x")) == 7 -- "#RRGGBB" RGB value
		 or string.len(colorexpression) == 9 and string.len(string.match(colorexpression, "#%x%x%x%x%x%x%x%x")) == 9 -- "#AARRGGBB" ARGB color value
		 then local c = tonumber("0x"..colorexpression)
			  colorexpression = function(index, value) return Color(index, value > 0, c) end
		  
		 else console.log("WARNING: Third parameter colorexpression as a string must be \"#RRGGBB\" or \"#AARRGGBB\".")
			  return
		 end
		 
	elseif type(colorexpression) ~= "nil" and type(colorexpression) ~= "function"
		then console.log("WARNING: Third parameter colorexpression must be a number, a string, a table, a function or nil.")	
			 return
	end
		
	-- textexpression parameter check
	if type(textexpression) == "nil"
	then textexpression = function(value) return tostring(value) end -- Display the value from the valueexpression as text
	elseif type(textexpression) == "string"
	then if string.len(textexpression) > 0
		 then local t = textexpression
			  textexpression = function(value) return value > 0 and t or "" end
		 else textexpression = nil
		 end
		 
	elseif type(textexpression) == "table" -- Value, Text pairs for displaying text with the corresponding value
	then local t = textexpression
		 if next(t) ~= nil
		 then textexpression = function(value) return t[value] or "" end
		 else textexpression = nil
		 end
		 
	elseif type(textexpression) == "boolean"
	then if textexpression == true
		 then textexpression = function(value) return tostring(value) end -- Display the value from the valueexpression as text
		 else textexpression = nil -- Display no text 
		 end
		
	elseif type(textexpression) ~= "function"
	then console.log("WARNING: Fourth parameter textexpression must be a string, a table, a function, boolean or nil")
	end
	------------------------------
	
	if IsButtonColumn(columnname)
	then if textexpression ~= nil
		 then console.log("WARNING: Can't print values into column "..columnname..".Only color will be displayed.")
		 end
		 
		 textexpression = nil -- Don't print text in button columns
		 
	else digits = digits or 3
		 tastudio.addcolumn(columnname, columnname, (digits * 6) + 14)
	end	
	
	Printers[columnname] = {value = valueexpression,
							color = colorexpression,
							text = textexpression}

end


------------
--	File  --
------------

-- Saves the log file to disk
function SaveFile(index)

	if string.match(movie.filename(), "default.tasproj") == "default.tasproj"
	then console.log("WARNING: The movie has yet not been saved. The log file wasn't saved.")
		 return
	end
	
	local file = io.open(movie.filename()..tostring(index)..".txt", "w+")
	
	file:write("ungreenframe:\n"..tostring(ungreenframe).."\n")
	
	for k in pairs(Logs) do -- Save header with log order
		file:write(tostring(k)..";")
	end
	
	file:write("\n")
	
	for i = 0, movie.length(), 1 do -- Save log values to file
		for k, v in pairs(Logs) do
			file:write(tostring(Logs[k].values[i])..";")
		end
		
		file:write("\n")
	end
	
	file:close()
	
	gui.addmessage("Saved log file #"..tostring(index))

end

-- Loads the log file from disk
function LoadFile(index, target)

	local file = io.open(movie.filename()..tostring(index)..".txt", "r")
	
	if file ~= nil
	then file:read("l")
		 ungreenframe = tonumber(file:read("l"));
		 
		 local names = bizstring.split(file:read("l"), ";")
		 for k,v in pairs(names) do
			if Logs[v] == nil
			then file:close() -- Logs in the file don't match the defined logs in the script
				 console.log("Error reading file.\n"..v.." not defined as log.")
				 return
			end
			
			if target[v] == nil
			then target[v] = {values = {}}
			end
		 end
		 
		 local i = 0
		 for line in file:lines("l") do -- Load log values from the file
			local s = bizstring.split(line, ";")
			
			for k,v in pairs(names) do	
				target[v].values[i] = tonumber(s[k])
			end
			
			i = i + 1
		end
		
		file:close()	
	end
	
	gui.addmessage("Loaded log file #"..tostring(index))

end


----------------
--	TAStudio  --
----------------

-- Responsible for printing text into the TAStudio list
function TAStudioText(index, column)

	if Printers[column] ~= nil
	then if Printers[column].text ~= nil and Printers[column].value(index) ~= nil
		 then return Printers[column].text(Printers[column].value(index), index)
		 elseif IsButtonColumn(column) == false -- Don't make input invisible for button columns
			 then return ""
		 end
	end
	
end

-- Responsible for coloring cells in the TAStudio list
function TAStudioColor(index, column)

	if Printers[column] ~= nil and Printers[column].color ~= nil and Printers[column].value(index) ~= nil
	then return Printers[column].color(index, Printers[column].value(index)) or nil
	end
	
	return nil

end

-- Called when the greenzone in invalidated by editing the movie
function Ungreen(index)

	if ungreenframe > index
	then ungreenframe = index - 1
	end
	
end


----------------
--	Branches  --
----------------

-- Called when a branch is loaded
function BranchLoad(index)

	if index >= 0
	then local logs_ = deepcopy(Logs) -- backup for undo branch load
		 branches[-1] = logs_
	end
	
	for k,v in pairs(Logs) do
		local vals = deepcopy(branches[index+1][k].values)
		Logs[k].values = vals
	end
	

end


-- Called when a branch is saved
function BranchSave(index)

	if index >= 0
	then local logs_= deepcopy(Logs)
		 branches[index+1] = logs_
	end
	
	SaveFile(index+1)
	
end

-- Called when a branch is removed
function BranchRemove(index)

	local logs_ = deepcopy(branches[index+1]) -- make backup
	branches[-1] = logs_
	table.remove(branches, index+1)
	
	os.remove(movie.filename()..tostring(index+1).."_deleted.txt") -- delete the backup
	os.rename(movie.filename()..tostring(index+1)..".Txt", movie.filename()..tostring(index).."_deleted.txt") -- rename for backup
	
	for i = index+1, #tastudio.getbranches(), 1 do
		os.rename(movie.filename()..tostring(i+1)..".txt", movie.filename()..tostring(i)..".txt")
	end

end


-------------
--	Color  --
-------------

--TODO: Add more
Colors = {}
Colors.blue = 	0XFF00C8FF	
Colors.yellow = 0XFFFFFF00	
Colors.orange = 0XFFFF8000	
Colors.purple = 0XFFBE00FF	

function CalcPaleColor(alpha, red, green, blue)

	red = math.floor((255-red)/120*(200-240)+255)
	green = math.floor((255-green)/120*(200-240)+255)
	blue = math.floor((255-blue)/120*(200-240)+255)
	
	return (alpha*0x1000000)+(red*0x10000)+(green*0x100)+blue

end

function CalcPaleColor2(color)

	local alpha = (color & 0xFF000000)>>24
	local red = (color & 0x00FF0000)>>16
    local green = (color & 0x0000FF00)>>8
	local blue = (color & 0x000000FF)
	
	red = math.floor((255-red)/120*(200-240)+255)
	green = math.floor((255-green)/120*(200-240)+255)
	blue = math.floor((255-blue)/120*(200-240)+255)
	
	return (alpha*0x1000000)+(red*0x10000)+(green*0x100)+blue

end

-- Draws color into the cell if the condition is true
function Color(index, condition, color)
	
	if condition == true --or value > 0 --not condition and value > 0 or value == condition 
	then if index < ungreenframe
		 then return color
		 else return CalcPaleColor2(color)
		 end
	end
	
end

-- Each value in the table can be assigned a color
function MultiColor(index, value, colortable)

	if colortable[value] ~= nil
	then if index < ungreenframe
		 then return colortable[value]
		 else return CalcPaleColor2(colortable[value])
		 end
	end
	
end

-- The gradient starts at startcolor for values below minvalue and ends at endcolor for value greater than maxvalue.
-- When no paremeters for startcolor and endcolor are passed into the function, then the gradient will be from red to green.
-- The default value for minvalue is 0.
-- An optional parameter called exponent controls the distribution of the gradient.
function ColorGradient(index, value, maxvalue, startcolor, endcolor, exponent, minvalue)

	minvalue = minvalue or 0
	
	-- fraction is a value from 0 to 1, with minvalue being eqaul to 0 and maxvalue equal to 1
	local fraction = ( Clamp(value-minvalue, 0, maxvalue-minvalue)/(maxvalue - minvalue) )^(exponent or 1)
	
	startcolor = startcolor or 0xFFFF0000 -- default red
	local as = (startcolor & 0xFF000000)>>24
	local rs = (startcolor & 0x00FF0000)>>16
    local gs = (startcolor & 0x0000FF00)>>8
	local bs = (startcolor & 0x000000FF)
	
	endcolor = endcolor or 0xFF00FF00 -- default green
	local ae = (endcolor & 0xFF000000)>>24
	local re = (endcolor & 0x00FF0000)>>16
	local ge = (endcolor & 0x0000FF00)>>8
	local be = (endcolor & 0x000000FF)
	
	-- Clamps the color value to a slope function y = 2*(_e - _s)*x +_s for ascending slopes, 
	-- or y = 2*(_e - _s)*x + 2*_s-_e for descending slopes between _s and _e, 
	-- where _s is the startcolor a,r,g,b part and _e is the endcolor part.
	local alpha = math.floor(Clamp((ae-as)*2*fraction + ((ae-as)<0 and 2*as-ae or as), math.min(as,ae), math.max(as, ae)))
	local red =   math.floor(Clamp((re-rs)*2*fraction + ((re-rs)<0 and 2*rs-re or rs), math.min(rs,re), math.max(rs, re)))
	local green = math.floor(Clamp((ge-gs)*2*fraction + ((ge-gs)<0 and 2*gs-ge or gs), math.min(gs,ge), math.max(gs, ge)))
	local blue =  math.floor(Clamp((be-bs)*2*fraction + ((be-bs)<0 and 2*gs-be or gs), math.min(bs,be), math.max(bs, be)))
	
	if index >= ungreenframe
	then return CalcPaleColor(alpha, red, green, blue)
	end
	
	return (alpha*0x1000000)+(red*0x10000)+(green*0x100)+blue

end


---------------
--	Utility  --
---------------

-- Checks wheter the value is contained in the values table
function AnyValueMatching(value, values)

	for k,v in pairs(values) do
		if value == v
		then return true
		end
	end
	
	return false
end

-- Checks wheter the column belongs to the input button columns
function IsButtonColumn(column)

	for button in pairs(joypad.getimmediate()) do
		if button == column
		then return true
		end
	end
	
	return false
	
end

-- Return the sign of the value x
function sign(x)
	return x < 0 and -1 or x >= 0 and 1
end

function signOrZero(x)
	return x < 0 and -1 or x > 0 and 1 or 0
end

-- Returns the value or minimum if value < minimum or maximum if value > maximum
function Clamp(value, minimum, maximum)
	return math.max(minimum, math.min(value, maximum))
end

function deepcopy(orig)

    local orig_type = type(orig)
    local copy
	
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else copy = orig -- number, string, boolean, etc
    end
	
    return copy
	
end

-- Calculates the difference between current log value with the corresponding name and the value in the branch log for the selected branch
function BranchCompare(name, index)

	if Logs[name].values[index] ~= nil and branches[selectedbranch] ~= nil and branches[selectedbranch][name].values[index] ~= nil
	then return Logs[name].values[index] - branches[selectedbranch][name].values[index]
	end
	
end

-- Similiar to BranchCompare, but multiplies the result by the sign of the value in the norm log
function BranchCompareNormalized(name, norm, index)

	-- Assume when Logs[name].value[index] not nil then Logs[norm].value[index] must also not be nil
	if Logs[name].values[index] ~= nil and branches[selectedbranch] ~= nil and branches[selectedbranch][name].values[index] ~= nil 
	then return (Logs[name].values[index] - branches[selectedbranch][name].values[index])*sign(Logs[norm].values[index])
	end

end

-- Difference from previous frame
function Difference(name, index)

	if Logs[name].values[index] ~= nil and Logs[name].values[index-1] ~= nil
	then return Logs[name].values[index] - Logs[name].values[index-1]
	end

end

function GetLogValue(name, index)

	if Logs[name].values[index] ~= nil
	then return Logs[name].values[index]
	end 
	
end

-- Called after each frame
function LogValues()

	for k, v in  pairs(Logs) do
		Logs[k].values[emu.framecount()] = Logs[k].expression() -- Log values
	end

end

-- Called when the script is closed
local function Exit()

	SaveFile(-2)
	
	-- Empty tables for script reloading
	Logs = {}
	Printers = {}
	branches = {}

end

function InitializeLogger()
	if tastudio.engaged() == false
	then console.log("WARNING: This script only works with TAStudio open.")
	else selectedbranch = 1
		 ungreenframe = 0

		 LoadFile(-2, Logs) -- Load file saved when script is stopped
	
		 for i = 1, #tastudio.getbranches(), 1 do
			local logs_ = {}
			LoadFile(i, logs_)
			branches[i] = logs_
		 end
	
		 tastudio.onqueryitemtext(TAStudioText)
		 tastudio.onqueryitembg(TAStudioColor)
		 tastudio.ongreenzoneinvalidated(Ungreen)
		 
		 tastudio.onbranchsave(BranchSave)
		 tastudio.onbranchload(BranchLoad)
		 tastudio.onbranchremove(BranchRemove)
		 
		 event.onexit(Exit)
	end
end

function UpdateLogger()

	if emu.framecount() > ungreenframe
	then ungreenframe = emu.framecount()
	end
	
	LogValues()
		
end