Darktide-Mod-Framework/scripts/mods/dmf/modules/debug/table_dump.lua
Lucas Schwiderski 78000cde56
Merge remote-tracking branch 'upstream/master' into feat/mod-bundles
* upstream/master:
  Add parameter defaults and better error messages for dump and dump_to_file (#37)
2023-07-22 13:48:48 +02:00

378 lines
10 KiB
Lua

local dmf = get_mod("DMF")
-- Local backup of the io library
local _io = dmf:persistent_table("_io")
_io.initialized = _io.initialized or false
if not _io.initialized then
_io = dmf.deepcopy(Mods.lua.io)
end
-- Local backup of the os library
local _os = dmf:persistent_table("_os")
_os.initialized = _os.initialized or false
if not _os.initialized then
_os = dmf.deepcopy(Mods.lua.os)
end
-- Global backup of original print() method
local print = __print
local function log_and_console_print(message, ...)
print(message, ...)
CommandWindow.print(message, ...)
end
local function table_dump(key, value, depth, max_depth)
if max_depth < depth then
return
end
local prefix = string.rep(" ", depth) .. ((key == nil and "") or "[" .. tostring(key) .. "]")
local value_type = type(value)
if value_type == "table" then
prefix = prefix .. ((key == nil and "") or " = ")
log_and_console_print(prefix .. "table")
for key_, value_ in pairs(value) do
table_dump(key_, value_, depth + 1, max_depth)
end
local meta = getmetatable(value)
if meta then
if type(meta) == "boolean" then
log_and_console_print(prefix .. "protected metatable")
else
log_and_console_print(prefix .. "metatable")
for key_, value_ in pairs(meta) do
if key_ ~= "__index" and key_ ~= "super" then
table_dump(key_, value_, depth + 1, max_depth)
end
end
end
end
elseif value_type == "function" or value_type == "thread" or value_type == "userdata" or value == nil then
log_and_console_print(prefix .. " = " .. tostring(value))
else
log_and_console_print(prefix .. " = " .. tostring(value) .. " (" .. value_type .. ")")
end
end
DMFMod.dump = function (self, dumped_object, object_name, max_depth)
if dmf.check_wrong_argument_type(self, "dump", "dumped_object_name", object_name, "string", "nil") or
dmf.check_wrong_argument_type(self, "dump", "max_depth", max_depth, "number", "nil")
then
return
end
local object_type = type(dumped_object)
max_depth = max_depth or 1
if object_type ~= "table" then
local error_message = string.format(
'(dump): "%s" is not a table but of type "%s"',
object_name or "Dump object",
object_type
)
if object_type ~= "nil" then
error_message = error_message .. " (" .. tostring(dumped_object) .. ")"
end
self:error(error_message)
return
end
if object_name then
log_and_console_print(string.format("<%s>", object_name))
end
local success, error_message = pcall(function()
for key, value in pairs(dumped_object) do
table_dump(key, value, 0, max_depth)
end
end)
if not success then
self:error("(dump): %s", tostring(error_message))
end
if object_name then
log_and_console_print(string.format("</%s>", object_name))
end
end
local function table_dump_to_file(dumped_table, dumped_table_name, max_depth)
-- #####################
-- ## Parsing ##
-- #####################
-- All tables which needs to be parsed will be put in here (their references).
local parsing_queue = {}
-- Special entry is created for every table added to 'parsing_queue'. It will contain parsed contents
-- of the table in the text form and other 'reached_tables' entries (plus some extra info)
local reached_tables = {}
-- This variable will contain the reference to the top 'reached_tables' entry
local parsed_tree
parsing_queue[1] = {}
table.insert(parsing_queue[1], dumped_table)
local system_table_name = tostring(dumped_table)
local new_table_entry = {
false, -- 'true' if parser will come across this table again
system_table_name:sub(8), -- table identifier, will be shown in json if previous value is 'true'
{}, -- all things which are stored inside the parsed table will be put in here
nil -- if table has metatable, {} will be created and it will be parsed as well
}
reached_tables[system_table_name] = new_table_entry
parsed_tree = new_table_entry
-- some temporary variables for speeding things up
local current_entry
local value_type
local reached_table
local parsed_metatable
-- parsing
for i = 1, max_depth do
-- the parser is not reached the max_level but there's nothing more to parse
if #parsing_queue[i] == 0 then
break
end
local allow_pushing_new_entries = i < max_depth
if allow_pushing_new_entries then
parsing_queue[i + 1] = {}
end
local function parse_table(table_entry, parsed_table)
for key, value in pairs(parsed_table) do
if key ~= "__index" then
-- key can be the table and the userdata. Unfortunately JSON does not support table keys
key = tostring(key):gsub('\\','\\\\'):gsub('\"','\\\"'):gsub('\t','\\t'):gsub('\n','\\n')
value_type = type(value)
if value_type == "table" then
if allow_pushing_new_entries then
system_table_name = tostring(value)
reached_table = reached_tables[system_table_name]
if reached_table then
reached_table[1] = true
table_entry[key] = "(table)(" .. system_table_name:sub(8) .. ")"
else
new_table_entry = {
false,
system_table_name:sub(8),
{},
nil
}
reached_tables[system_table_name] = new_table_entry
table_entry[key] = new_table_entry
table.insert(parsing_queue[i + 1], value)
end
else
table_entry[key] = "(table)"
end
elseif value_type == "function" or value_type == "thread" then
table_entry[key] = "[" .. value_type .. "]"
else
value = tostring(value):gsub('\\','\\\\'):gsub('\"','\\\"'):gsub('\t','\\t'):gsub('\n','\\n')
table_entry[key] = value .. " (" .. value_type .. ")"
end
end
end
end
-- parsing all the tables in 'parsing_queue' for the current depth level
for _, parsed_table in pairs(parsing_queue[i]) do
current_entry = reached_tables[tostring(parsed_table)]
-- table
parse_table(current_entry[3], parsed_table)
-- metatable
parsed_metatable = getmetatable(parsed_table)
if parsed_metatable and type(parsed_metatable) == "table" then
current_entry[4] = {}
parse_table(current_entry[4], parsed_metatable)
end
end
end
-- ####################
-- ## Saving to file ##
-- ####################
_os.execute("mkdir dump 2>nul")
local file = assert(_io.open("./dump/" .. dumped_table_name .. ".json", "w+"))
local function dump_to_file(table_entry, table_name, depth)
local print_string = nil
local prefix = string.rep(' ', depth)
-- if table was reached more than once, add its system identifier to its name
if table_entry[1] then
file:write(prefix .. '"' .. table_name .. ' (' .. table_entry[2] .. ')": {\n')
else
file:write(prefix .. '"' .. table_name .. '": {\n')
end
-- if table has metatable
if table_entry[4] then
local prefix2 = prefix .. ' '
local prefix3 = prefix2 .. ' "'
-- TABLE
file:write(prefix2 .. '"table": {\n')
for key, value in pairs(table_entry[3]) do
if print_string then
file:write(print_string .. ',\n')
end
if type(value) == "table" then
dump_to_file(value, key, depth + 2)
print_string = prefix2 .. ' }'
else
print_string = prefix3 .. key .. '": "' .. value .. '"'
end
end
if print_string then
file:write(print_string .. '\n')
print_string = nil
end
file:write(prefix2 .. '},\n')
-- METATABLE
file:write(prefix2 .. '"metatable": {\n')
for key, value in pairs(table_entry[4]) do
if print_string then
file:write(print_string .. ',\n')
end
if type(value) == "table" then
dump_to_file(value, key, depth + 2)
print_string = prefix2 .. ' }'
else
print_string = prefix3 .. key .. '": "' .. value .. '"'
end
end
if print_string then
file:write(print_string .. '\n')
end
file:write(prefix2 .. '}\n')
else
local prefix2 = prefix .. ' "'
for key, value in pairs(table_entry[3]) do
if print_string then
file:write(print_string .. ',\n')
end
if type(value) == "table" then
dump_to_file(value, key, depth + 1)
print_string = prefix .. ' }'
else
print_string = prefix2 .. key .. '": "' .. value .. '"'
end
end
if print_string then
file:write(print_string .. '\n')
end
end
end
-- dumping parsed info to the file
file:write('{\n')
dump_to_file (parsed_tree, dumped_table_name, 1)
file:write(' }\n')
file:write('}\n')
file:close()
end
DMFMod.dump_to_file = function (self, dumped_object, object_name, max_depth)
if dmf.check_wrong_argument_type(self, "dump_to_file", "object_name", object_name, "string", "nil") or
dmf.check_wrong_argument_type(self, "dump_to_file", "max_depth", max_depth, "number", "nil")
then
return
end
local object_type = type(dumped_object)
object_name = object_name or "mod_dump_to_file"
max_depth = max_depth or 1
if object_type ~= "table" then
local error_message = string.format(
'(dump_to_file): "%s" is not a table but of type "%s"',
object_name or "Dump object",
object_type
)
if object_type ~= "nil" then
error_message = error_message .. " (" .. tostring(dumped_object) .. ")"
end
self:error(error_message)
return
end
local success, error_message = pcall(function() table_dump_to_file(dumped_object, object_name, max_depth) end)
if not success then
self:error("(dump_to_file): %s", tostring(error_message))
end
end
DMFMod.dtf = DMFMod.dump_to_file
-- Managers.curl._requests crashes the game