-- 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