2024-06-16 09:41:29 -05:00

753 lines
25 KiB
Lua

local S = minetest.get_translator("modding_commands")
minetest.register_on_joinplayer(function(player)
local player_name = player:get_player_name()
local message = minetest.colorize("#00FFFF", "<Modding Commands> Please make backups of any files you plan to modify with any of the commands from the modding_commands mod. This mod is still experimental, but fully functional (for the most part. Besides the commands that aren't included ingame). Please keep all backups until you verify the modified files work properly.")
if not ie then
local disclamer = minetest.colorize("#FF4000", "This mod requires an insecure environment, if you haven't done so already, please add it to secure.trusted_mods. If you don't trust it please examine the code, before you show it as trusted.")
minetest.chat_send_player(player_name, disclamer)
end
end)
local DIR_DELIM = "/"
local ie = minetest.request_insecure_environment()
-- The following script checks for invalid whitespace areas.
minetest.register_chatcommand("check_whitespace", {
params = "<modname> <filename>",
description = "Checks for improper indentation (leading/trailing spaces) in the specified mod and file, ignoring comments",
privs = {server = true},
func = function(name, param)
local modname, filename = param:match("(%S+)%s+(%S+)")
if not modname or not filename then
return false, "Invalid parameters. Usage: /check_whitespace <modname> <filename>"
end
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod not found: " .. modname
end
local filepath = modpath .. "/" .. filename
local file = ie.io.open(filepath, "r")
if not file then
return false, "File not found: " .. filename
end
local lines = {}
local line_num = 1
local in_multiline_comment = false
for line in file:lines() do
local check_line = line
if in_multiline_comment then
if line:match(".*%]%]") then
in_multiline_comment = false
end
line_num = line_num + 1
elseif line:match(".*%[%[.*") then
in_multiline_comment = true
line_num = line_num + 1
else
check_line = check_line:gsub("%-%-.*", "")
local indent = check_line:match("^ *")
if indent:match(" $") then
table.insert(lines, "Line " .. line_num .. ": Trailing space in indentation")
end
if line:match(".* +$") then
table.insert(lines, "Line " .. line_num .. ": Trailing space at end of line")
end
line_num = line_num + 1
end
end
file:close()
if #lines == 0 then
return true, "No improper indentation or trailing spaces found in " .. filename
else
local result = "Issues found in " .. filename .. ":\n"
result = result .. table.concat(lines, "\n")
return true, result
end
end,
})
-- The following script can replace a specified word in all lua files of a specified mod.
minetest.register_chatcommand("bulk_replace", {
params = "<modname> <oldword> <newword>",
description = "Replace a word in all Lua files and mod.conf of a mod",
privs = {server = true},
func = function(name, param)
local modname, oldword, newword = string.match(param, "^(%S+)%s+(%S+)%s+(%S+)$")
if not modname or not oldword or not newword then
return false, "Invalid usage. Correct usage: /bulk_replace <modname> <oldword> <newword>"
end
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod " .. modname .. " not found."
end
local total_replacements = 0
local function replace_in_file(filepath)
local file = ie.io.open(filepath, "r")
if not file then
return false, "File " .. filepath .. " not found."
end
local content = file:read("*all")
file:close()
local new_content, replacements = content:gsub(oldword, newword)
if replacements > 0 then
file = ie.io.open(filepath, "w")
if not file then
return false, "Failed to open file " .. filepath .. " for writing."
end
file:write(new_content)
file:close()
minetest.chat_send_player(name, "Replaced " .. replacements .. " occurrences in file: " .. filepath)
total_replacements = total_replacements + replacements
else
minetest.chat_send_player(name, "No occurrences of '" .. oldword .. "' found in file: " .. filepath)
end
end
local confpath = modpath .. "/mod.conf"
replace_in_file(confpath)
local function search_and_replace_in_files(path)
local file_list = minetest.get_dir_list(path, false)
local dir_list = minetest.get_dir_list(path, true)
for _, filename in ipairs(file_list) do
local filepath = path .. "/" .. filename
if filename:sub(-4) == ".lua" then
replace_in_file(filepath)
end
end
for _, dirname in ipairs(dir_list) do
local dirpath = path .. "/" .. dirname
search_and_replace_in_files(dirpath)
end
end
search_and_replace_in_files(modpath)
minetest.chat_send_player(name, "Word replacement completed in mod: " .. modname .. ". Total replacements: " .. total_replacements)
return true, "Word replacement completed in mod: " .. modname .. ". Total replacements: " .. total_replacements
end,
})
-- The following script searches for all mods that have files containing a specified key word.
function searchMods(keyword)
local mods = minetest.get_modnames()
local matchingMods = {}
for _, modname in ipairs(mods) do
local modpath = minetest.get_modpath(modname)
if modpath then
local files = minetest.get_dir_list(modpath, false)
if files then
local modFiles = {}
for _, file in ipairs(files) do
if containsKeyword(modpath .. "/" .. file, keyword) then
table.insert(modFiles, file)
end
end
if next(modFiles) then
matchingMods[modname] = modFiles
end
else
minetest.log("warning", "Failed to retrieve files in mod: " .. modname)
end
else
minetest.log("warning", "Failed to retrieve modpath for mod: " .. modname)
end
end
return matchingMods
end
function containsKeyword(file, keyword)
local f = ie.io.open(file, "r")
if f then
local content = f:read("*all")
f:close()
return content:find(keyword, 1, true) ~= nil
else
minetest.log("warning", "Failed to open file: " .. file)
return false
end
end
minetest.register_chatcommand("findmods", {
params = "<keyword>",
description = "Find mods containing a specified keyword",
privs = {server = true},
func = function(name, param)
local matchingMods = searchMods(param)
if next(matchingMods) then
local message = "Mods containing the word '" .. param .. "':\n"
for modname, files in pairs(matchingMods) do
message = message .. modname .. ":\n" .. table.concat(files, ", ") .. "\n"
end
minetest.chat_send_player(name, message)
else
minetest.chat_send_player(name, "No mods found containing the word '" .. param .. "'.")
end
end,
})
-- The following script shows the description and dependencies of all enabled mods.
minetest.register_chatcommand("depmap", {
description = "Maps the dependency tree of all enabled mods",
privs = {server = true},
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then
return false, "Player not found."
end
local mods = minetest.get_modnames()
local function get_mod_info(modname)
local path = minetest.get_modpath(modname)
if not path then return nil end
local mod_conf = path .. "/mod.conf"
local depends_conf = path .. "/depends.txt"
local mod_info = {
name = modname,
description = "No description available",
depends = {},
optional_depends = {}
}
local file = ie.io.open(mod_conf, "r")
if file then
for line in file:lines() do
if line:match("^description%s*=") then
mod_info.description = line:match("^description%s*=%s*(.*)")
elseif line:match("^depends%s*=") then
for dep in line:match("^depends%s*=%s*(.*)"):gmatch("([^,]+)") do
table.insert(mod_info.depends, dep:match("^%s*(.-)%s*$"))
end
elseif line:match("^optional_depends%s*=") then
for dep in line:match("^optional_depends%s*=%s*(.*)"):gmatch("([^,]+)") do
table.insert(mod_info.optional_depends, dep:match("^%s*(.-)%s*$"))
end
end
end
file:close()
end
file = ie.io.open(depends_conf, "r")
if file then
for line in file:lines() do
local dep = line:match("^([^%s#]+)")
if dep and dep ~= "" then
if line:find("?") then
table.insert(mod_info.optional_depends, dep)
else
table.insert(mod_info.depends, dep)
end
end
end
file:close()
end
return mod_info
end
local mod_dependencies = {}
for _, modname in ipairs(mods) do
mod_dependencies[modname] = get_mod_info(modname)
end
local function format_mod_info(mod_info)
if not mod_info then return "Unknown mod" end
local str = "Mod: " .. mod_info.name .. "\n"
str = str .. "Description: " .. mod_info.description .. "\n"
str = str .. "Dependencies: " .. (next(mod_info.depends) and table.concat(mod_info.depends, ", ") or "None") .. "\n"
str = str .. "Optional Dependencies: " .. (next(mod_info.optional_depends) and table.concat(mod_info.optional_depends, ", ") or "None") .. "\n"
return str
end
local msg = ""
for modname, info in pairs(mod_dependencies) do
msg = msg .. format_mod_info(info) .. "\n"
end
minetest.chat_send_player(name, msg)
return true
end
})
-- The following script checks for errors in the code of a certain lua file. ie. nil values
minetest.register_chatcommand("checkcode", {
params = "<mod_name> <file_name>",
privs = {server = true},
description = "Check code for errors in a specific mod file",
func = function(name, param)
local mod_name, file_name = param:match("(%S+)%s+(%S+)")
if not mod_name or not file_name then
return false, "Usage: /checkcode <mod_name> <file_name>"
end
local player = minetest.get_player_by_name(name)
if not player then
return false, "Player not found"
end
if not minetest.get_modpath(mod_name) then
return false, "Mod '" .. mod_name .. "' not found"
end
local file_path = minetest.get_modpath(mod_name) .. "/" .. file_name
local file, err_msg = ie.io.open(file_path, "r")
if not file then
return false, "Error opening file: " .. err_msg
end
local code = file:read("*all")
file:close()
local success, error_msg = pcall(loadstring(code))
if not success then
minetest.chat_send_player(name, "Error in code: " .. error_msg)
else
minetest.chat_send_player(name, "Code has no errors")
end
end,
})
-- The following script looks through all mts files in a specified directory and reads them to see if they contain unknown nodes.
local function get_files_in_dir(path, extension)
local files = {}
local p = ie.io.popen('find "' .. path .. '" -type f -name "*' .. extension .. '"')
for file in p:lines() do
table.insert(files, file)
end
p:close()
return files
end
local function read_mts_file(filename)
local success, schem = pcall(minetest.read_schematic, filename, {})
if not success then
return nil, "Failed to read schematic: " .. filename
end
return schem
end
local function save_report(filename, report)
local file, err = ie.io.open(filename, "w")
if not file then
return false, err
end
file:write(report)
file:close()
return true
end
local OUTPUT_CHAT = 1
local OUTPUT_FORMSPEC = 2
local OUTPUT_FILE = 3
local function parse_output_option(output)
if output == "chat" then
return OUTPUT_CHAT
elseif output == "formspec" then
return OUTPUT_FORMSPEC
elseif output == "file" then
return OUTPUT_FILE
else
return nil
end
end
minetest.register_chatcommand("check_nodes", {
params = "<modname> [output: chat, formspec, file]",
description = "Check for unregistered nodes in .mts files for the specified mod",
privs = {server=true},
func = function(name, param)
local modname, output = param:match("(%S+)%s*(.*)")
if not modname then
return false, "Usage: /check_nodes <modname> [output: chat, formspec, file]"
end
if output == "" then
return false, "Invalid output option. Choose from: chat, formspec, file"
end
local output_option = parse_output_option(output)
if not output_option then
return false, "Invalid output option. Choose from: chat, formspec, file"
end
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod not found: " .. modname
end
local files = get_files_in_dir(modpath, ".mts")
if #files == 0 then
return false, "No .mts files found in the mod: " .. modname
end
local unregistered_nodes = {}
local invalid_files = {}
for _, file in ipairs(files) do
local schem, err = read_mts_file(file)
if not schem then
minetest.log("error", err)
else
local has_invalid_nodes = false
for _, node in ipairs(schem.data) do
local nodename = node.name
if not minetest.registered_nodes[nodename] then
unregistered_nodes[nodename] = true
has_invalid_nodes = true
end
end
if has_invalid_nodes then
table.insert(invalid_files, file)
end
end
end
table.sort(invalid_files)
local sorted_unregistered_nodes = {}
for nodename, _ in pairs(unregistered_nodes) do
table.insert(sorted_unregistered_nodes, nodename)
end
table.sort(sorted_unregistered_nodes)
local node_report = ""
for _, nodename in ipairs(sorted_unregistered_nodes) do
node_report = node_report .. "- " .. nodename .. "\n"
end
local file_report = ""
for _, file_name in ipairs(invalid_files) do
file_report = file_report .. "- " .. file_name .. "\n"
end
local result_msg
if node_report == "" then
result_msg = "All nodes in .mts files are registered."
else
result_msg = "Unregistered nodes found:\n" .. node_report
end
if file_report ~= "" then
result_msg = result_msg .. "\n\nFiles with invalid nodes:\n" .. file_report
end
if output_option == OUTPUT_FORMSPEC then
local formspec = "size[8,5]"
formspec = formspec .. "textarea[0.5,0.5;7.5,4;;;" .. result_msg .. "]"
minetest.show_formspec(name, "check_nodes_result", formspec)
elseif output_option == OUTPUT_FILE then
local filename = modpath .. "/report.txt"
local save_result, save_error = save_report(filename, result_msg)
if not save_result then
return false, "Failed to save report to file: " .. save_error
end
minetest.chat_send_player(name, "Report saved to file: " .. filename)
elseif output_option == OUTPUT_CHAT then
minetest.chat_send_player(name, result_msg)
end
return
end,
})
minetest.register_chatcommand("mts2lua_all", {
description = "Convert all .mts schematic files in a mod directory to .lua files",
privs = {server = true},
params = "<modname> [comments]",
func = function(name, param)
local modname, comments_str = string.match(param, "^([^ ]+) *(.*)$")
if not modname then
return false, "No mod name specified."
end
local comments = comments_str == "comments"
local modpath = minetest.get_modpath(modname) .. "/schematics"
if not modpath then
return false, "Mod not found: " .. modname
end
local schem_files = minetest.get_dir_list(modpath, false)
local export_path = modpath-- .. DIR_DELIM .. "schematics"
--minetest.mkdir(export_path) -- Ensure directory exists
for _, file in ipairs(schem_files) do
if file:sub(-4) == ".mts" then
local schem_path = modpath .. DIR_DELIM .. file
local schematic = minetest.read_schematic(schem_path, {})
if schematic then
local str = minetest.serialize_schematic(schematic, "lua", {lua_use_comments=comments})
local lua_file = file:sub(1, -5) .. ".lua"
local lua_path = export_path .. DIR_DELIM .. lua_file
local output_file = ie.io.open(lua_path, "w")
if output_file and str then
output_file:write(str)
output_file:flush()
output_file:close()
minetest.chat_send_player(name, "Converted " .. file .. " to " .. lua_file)
else
minetest.chat_send_player(name, "Failed to convert " .. file)
minetest.log("error", "Failed to write Lua file: " .. lua_path)
end
else
minetest.chat_send_player(name, "Failed to read schematic " .. file)
minetest.log("error", "Failed to read schematic: " .. schem_path)
end
end
end
return true, "Conversion completed."
end,
})
-- This command changes all lua schematic files in a specified directory into mts files and if specified replaces certain words in the process.
minetest.register_chatcommand("lua2mts_all", {
description = "Convert all .lua schematic files in a mod directory to .mts files with optional word replacements",
privs = {server = true},
params = "<modname> [replacements]",
func = function(name, param)
local modname, replacements_str = string.match(param, "^([^ ]+) *(.*)$")
if not modname then
return false, "No mod name specified."
end
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod not found: " .. modname
end
modpath = modpath .. "/schematics"
local replacements = {}
if replacements_str ~= "" then
for replacement in replacements_str:gmatch("[^,]+") do
local old, new = replacement:match("([^:]+):([^:]+)")
if old and new then
replacements[old] = new
end
end
end
local lua_files = minetest.get_dir_list(modpath, false)
for _, file in ipairs(lua_files) do
if file:sub(-4) == ".lua" then
local lua_path = modpath .. DIR_DELIM .. file
local input_file = ie.io.open(lua_path, "r")
if input_file then
local content = input_file:read("*all")
input_file:close()
-- Apply replacements
for old, new in pairs(replacements) do
content = content:gsub(old, new)
end
local schematic_func, err = loadstring(content)
if not schematic_func then
minetest.chat_send_player(name, "Failed to load Lua file " .. file .. ": " .. err)
goto continue
end
local schematic_env = {}
setfenv(schematic_func, schematic_env)
local success, result = pcall(schematic_func)
if not success then
minetest.chat_send_player(name, "Error executing Lua file " .. file .. ": " .. result)
goto continue
end
local schematic = schematic_env.schematic
if schematic then
local mts_file = file:sub(1, -5) .. ".mts"
local mts_path = modpath .. DIR_DELIM .. ".." .. DIR_DELIM .. "schematics" .. DIR_DELIM .. mts_file
-- Serialize schematic
local schem_string = minetest.serialize_schematic(schematic, "mts", {})
if schem_string then
local output_file = ie.io.open(mts_path, "wb")
if output_file then
output_file:write(schem_string)
output_file:close()
minetest.chat_send_player(name, "Converted " .. file .. " to " .. mts_file)
else
minetest.chat_send_player(name, "Failed to write MTS file for " .. file)
end
else
minetest.chat_send_player(name, "Failed to serialize schematic for " .. file)
end
else
minetest.chat_send_player(name, "No schematic found in Lua file " .. file)
end
else
minetest.chat_send_player(name, "Failed to read Lua file " .. file)
end
end
::continue::
end
return true, "Conversion completed."
end,
})
-- The following script registers all schematics in a certain directory
minetest.register_chatcommand("list_schematics", {
params = "<modname>",
privs = {server = true},
description = "List all schematics in a certain mod's schematic folder",
func = function(name, param)
if param == "" then
return false, "Please provide a mod name."
end
local modname = param
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod not found: " .. modname
end
local schematic_folder = modpath .. "/schematics"
local file_list = minetest.get_dir_list(schematic_folder, false) or {}
if #file_list == 0 then
return true, "No schematics found in the mod: " .. modname .. ". Either the folder doesn't exist or it is empty. Try /list_schems instead"
end
local result = "Schematics in " .. modname .. ":\n"
for _, file in ipairs(file_list) do
result = result .. file .. "\n"
end
return true, result
end,
})
-- The following script is an alternative to the one above incase a mod doesn't have a schematics folder, but rather a schems folder
minetest.register_chatcommand("list_schems", {
params = "<modname>",
privs = {server = true},
description = "List all schematics in a certain mod's schem folder",
func = function(name, param)
if param == "" then
return false, "Please provide a mod name."
end
local modname = param
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod not found: " .. modname
end
local schematic_folder = modpath .. "/schems"
local file_list = minetest.get_dir_list(schematic_folder, false) or {}
if #file_list == 0 then
return true, "No schematics found in the mod: " .. modname .. ". Either the folder doesn't exist or it is empty. Try /list_schematics instead"
end
local result = "Schematics in " .. modname .. ":\n"
for _, file in ipairs(file_list) do
result = result .. file .. "\n"
end
return true, result
end,
})
minetest.register_chatcommand("unused_textures", {
params = "<modname>",
description = "Find unused textures in a specified mod",
privs = {server=true},
func = function(name, param)
if param == "" then
return false, "You must specify a mod name."
end
local modname = param
local modpath = minetest.get_modpath(modname)
if not modpath then
return false, "Mod '" .. modname .. "' not found."
end
local texture_path = modpath .. "/textures"
local textures = {}
local function list_files_in_directory(directory)
local files = {}
local p = ie.io.popen('ls "' .. directory .. '"')
for filename in p:lines() do
table.insert(files, filename)
end
p:close()
return files
end
local texture_files = list_files_in_directory(texture_path)
for _, filename in ipairs(texture_files) do
if filename:match("%.png$") then
textures[filename] = true
end
end
local function mark_texture_used(texture)
for sub_texture in texture:gmatch("[^%^]+") do
sub_texture = sub_texture:gsub("%s+", "")-- Remove any whitespace
if textures[sub_texture] then
textures[sub_texture] = nil
end
end
end
local function parse_lua_file(file_path)
local file = ie.io.open(file_path, "r")
if not file then
return
end
local lua_code = file:read("*all")
file:close()
for str in lua_code:gmatch('"([^"]+%.png)"') do
mark_texture_used(str)
end
end
local function scan_lua_files(directory)
for _, filename in ipairs(list_files_in_directory(directory)) do
if filename:match("%.lua$") then
parse_lua_file(directory .. "/" .. filename)
end
end
end
scan_lua_files(modpath)
for name, def in pairs(minetest.registered_nodes) do
if type(def.tiles) == "table" then
for _, texture in ipairs(def.tiles) do
if type(texture) == "string" then
mark_texture_used(texture)
end
end
elseif type(def.tiles) == "string" then
mark_texture_used(def.tiles)
end
if type(def.overlay_tiles) == "table" then
for _, texture in ipairs(def.overlay_tiles) do
if type(texture) == "string" then
mark_texture_used(texture)
end
end
elseif type(def.overlay_tiles) == "string" then
mark_texture_used(def.overlay_tiles)
end
end
for name, def in pairs(minetest.registered_items) do
if type(def.inventory_image) == "string" then
mark_texture_used(def.inventory_image)
end
if type(def.wield_image) == "string" then
mark_texture_used(def.wield_image)
end
if type(def.tiles) == "table" then
for _, texture in ipairs(def.tiles) do
if type(texture) == "string" then
mark_texture_used(texture)
end
end
elseif type(def.tiles) == "string" then
mark_texture_used(def.tiles)
end
end
for name, def in pairs(minetest.registered_entities) do
if type(def.textures) == "table" then
for _, texture in ipairs(def.textures) do
if type(texture) == "string" then
mark_texture_used(texture)
end
end
elseif type(def.textures) == "string" then
mark_texture_used(def.textures)
end
end
for _, def in pairs(minetest.registered_particles or {}) do
if type(def.texture) == "string" then
mark_texture_used(def.texture)
end
end
for _, def in pairs(minetest.registered_particlespawners or {}) do
if type(def.texture) == "string" then
mark_texture_used(def.texture)
end
end
local unused_textures = {}
for texture, _ in pairs(textures) do
table.insert(unused_textures, texture)
end
if #unused_textures == 0 then
return true, "No unused textures found."
else
return true, "Unused textures:\n" .. table.concat(unused_textures, "\n")
end
end
})