Initial working version

A somewhat more optimized and cleaned up version of what I used
to rip the node appearances for Klots.

Added:
- Custom dumper with more compact format support
- Detecting reused tables and extracting into local vars
- Actually handling the media files instead of handing a list to
  the user to go find via shell script
- Documentation, more chatcommands
This commit is contained in:
Aaron Suen 2022-11-23 11:43:55 -05:00
commit 241e79647f
11 changed files with 424 additions and 0 deletions

8
.luacheckrc Normal file
View File

@ -0,0 +1,8 @@
globals = {
"minetest",
"vector",
"SecureRandom",
"VoxelArea",
}
color = false
quiet = 1

4
.lualocals Normal file
View File

@ -0,0 +1,4 @@
minetest
vector
VoxelArea
SecureRandom

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# Minetest Definition Ripper
You're making a puzzle/adventure game, and you need building materials for the scenery. It's a bother to make all original artwork, or you like the look and feel of something that already exists, but it comes with all this extra functionality that you don't want...
This mod will allow you to export the "superficial" definitions and media for selected items. Add the mod to a game that has all the things loaded that you want to export, instruct the mod which items to export (using chat commands), and it will dump the start of a mod that can be used to import item registrations into your game.
You will get a folder in your worldpath containing a skeletal mod:
- All media files referenced by the items, split into textures/sounds/models.
- An `exported.lua` file with each definition.
You will need to provide your own `init.lua`, `mod.conf` and other infrastructure, but the exported definitions are kept in a separate file so you can safely overwrite it later (e.g. if you add definitions) without destroying your custom logic.
`exported.lua` takes a register_item-like function as a file-level parameter. This function will receive a single parameter with the definition table; it does NOT get an item name. There is a `_raw_name` key inside the definition from which you will need to derive your own name in a manner you deem appropriate (you have an opportunity to customize before registration here).
Example:
```lua
local modname = minetest.get_current_modname()
loadfile(minetest.get_modpath(modname) .. "/exported.lua")(function(def)
local myname = modname .. ":" .. def._raw_name:gsub(":", "__")
def._raw_name = nil
minetest.register_item(myname, def)
end)
```
**NOTE** that you are still responsible for complying with licensing for all media files in the export. This includes:
- Tracing the origin of each file (the names should match, and be unique in any sane setup) and identifying the author and original license.
- Including attribution/credit in your resultant project.
- Complying with sublicensing requirements when selecting the license for your own projet containing these media.

44
commands.lua Normal file
View File

@ -0,0 +1,44 @@
-- LUALOCALS < ---------------------------------------------------------
local minetest, pairs
= minetest, pairs
-- LUALOCALS > ---------------------------------------------------------
local include = ...
local exportdb, savedb = include("exportdb")
local exportall = include("exportall")
local modname = minetest.get_current_modname()
minetest.register_chatcommand(modname, {
description = "Rip all items in already marked for export",
privs = {server = true},
func = function() return true, "exported " .. exportall() end
})
minetest.register_chatcommand(modname .. "_clear", {
description = "Clear all saved exports",
privs = {server = true},
func = function()
for k in pairs(exportdb) do exportdb[k] = nil end
savedb()
return true, "exported " .. exportall()
end
})
minetest.register_chatcommand(modname .. "_inv", {
description = "Rip all items in inventory",
privs = {server = true},
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then return false, "invalid player" end
for _, list in pairs(player:get_inventory():get_lists()) do
for _, stack in pairs(list) do
if not stack:is_empty() then
exportdb[stack:get_name()] = true
end
end
end
savedb()
return true, "exported " .. exportall()
end
})

90
dumptable.lua Normal file
View File

@ -0,0 +1,90 @@
-- LUALOCALS < ---------------------------------------------------------
local error, ipairs, math, minetest, pairs, string, table, tostring,
type
= error, ipairs, math, minetest, pairs, string, table, tostring,
type
local math_floor, string_format, string_gsub, string_match,
table_concat, table_sort
= math.floor, string.format, string.gsub, string.match,
table.concat, table.sort
-- LUALOCALS > ---------------------------------------------------------
local function sortedpairs(tbl)
local keys = {}
for k in pairs(tbl) do keys[#keys + 1] = k end
table_sort(keys)
local i = 0
return function()
i = i + 1
local k = keys[i]
if k == nil then return end
return k, tbl[k]
end
end
local bytype = {}
local function dumptable(obj, ctx)
local bt = bytype[type(obj)]
if bt then return bt(obj, ctx) end
return error(string_format("unsupported type %q", type(obj)))
end
function bytype.string(obj)
return string_format("%q", minetest.get_translated_string("en", obj))
end
bytype.boolean = tostring
bytype.number = tostring
local function dumptableraw(obj, ctx)
local ents = {}
local oldindent = ctx.indent
ctx.indent = ctx.indent .. "\t"
for i, v in ipairs(obj) do
local oldpath = ctx.path
ctx.path = oldpath and (oldpath .. "_" .. i) or i
ents[#ents + 1] = dumptable(v, ctx)
ctx.path = oldpath
end
for k, v in sortedpairs(obj) do
if type(k) ~= "number" or k < 1 or k > #obj or k ~= math_floor(k) then
local oldpath = ctx.path
local nk = string_gsub(k, "%W", "_")
ctx.path = oldpath and (oldpath .. "_" .. nk) or nk
if type(k) == "string" and string_match(k, "^[A-Za-z_][A-Za-z0-9_]*$") then
ents[#ents + 1] = k .. " = " .. dumptable(v, ctx)
else
ents[#ents + 1] = "[" .. dumptable(k, ctx)
.. "] = " .. dumptable(v, ctx)
end
ctx.path = oldpath
end
end
ctx.indent = oldindent
local short = "{" .. table_concat(ents, ", ") .. "}"
if #short <= 60 then return short end
return "{" .. table_concat(ents, ",\n" .. ctx.indent) .. "}"
end
function bytype.table(obj, ctx)
if ctx.locals then
local oldindent = ctx.indent
ctx.indent = "\t"
local oldlocals = ctx.locals
ctx.locals = nil
local raw = dumptableraw(obj, ctx)
ctx.locals = oldlocals
ctx.indent = oldindent
local found = ctx.locals(raw, ctx, obj)
if found ~= nil then return found end
end
ctx.indent = ctx.indent or "\t"
return dumptableraw(obj, ctx)
end
bytype["nil"] = function() return "nil" end
bytype["function"] = bytype["nil"]
bytype["userdata"] = bytype["nil"]
return dumptable, sortedpairs

135
exportall.lua Normal file
View File

@ -0,0 +1,135 @@
-- LUALOCALS < ---------------------------------------------------------
local io, minetest, pairs, string, type
= io, minetest, pairs, string, type
local io_open, string_gsub, string_sub
= io.open, string.gsub, string.sub
-- LUALOCALS > ---------------------------------------------------------
local include = ...
local dumptable, sortedpairs = include("dumptable")
local getdir, stealmedia = include("fileops")
local exportdb = include("exportdb")
local modname = minetest.get_current_modname()
local nodekeys = {
climbable = true,
description = true,
drawtype = true,
inventory_image = true,
legacy_facedir_simple = true,
light_source = true,
liquid_move_physics = function(def)
return def.liquid_move_physics
or def.liquidtype ~= nil and def.liquidtype ~= "none"
or nil
end,
mesh = true,
node_box = true,
paramtype = true,
paramtype2 = true,
use_texture_alpha = true,
sounds = true,
sunlight_propagates = true,
tiles = true,
type = true,
visual_scale = true,
walkable = true,
wield_image = true,
}
local function exportall()
local count = 0
local outdir = getdir(minetest.get_worldpath() .. "/" .. modname)
local outf = io_open(outdir .. "/exported.lua", "w")
outf:write("local reg = ...\n\n")
local filtered = {}
for k, v in pairs(minetest.registered_items) do
if exportdb[k] and v.liquidtype ~= "flowing" then
local t = {}
for k2, v2 in pairs(v) do
local keydef = nodekeys[k2]
if type(keydef) == "function" then
t[k2] = keydef(v)
elseif keydef then
t[k2] = v2
end
end
t._raw_name = k
filtered[k] = t
end
end
local localized = {}
do
local candidates = {}
for k, v in pairs(filtered) do
dumptable(v, {
path = string_gsub(k, "%W", "_"),
locals = function(item, ctx, obj)
local cand = candidates[item]
if not cand then
cand = {
qty = 0,
name = ctx.path,
obj = obj
}
candidates[item] = cand
end
cand.qty = cand.qty + 1
if #ctx.path < #cand.name
or (#ctx.path == #cand.name
and ctx.path < cand.name) then
cand.name = ctx.path
end
end
})
end
for k, v in pairs(candidates) do
dumptable(v.obj, {
path = v.name,
locals = function(item)
if item ~= k then
local cand = candidates[item]
cand.rawqty = cand.rawqty or cand.qty
cand.useby = cand.useby or {}
cand.useby[v.name] = true
cand.qty = cand.qty - v.qty + 1
end
end
})
end
for k, v in sortedpairs(candidates) do
if #k + #v.name * (v.qty + 1) < #k * v.qty then
outf:write("local " .. v.name .. " = " .. k .. "\n\n")
localized[k] = v.name
end
end
end
for _, v in sortedpairs(filtered) do
stealmedia(v.sounds, outdir, "sounds", function(spec, fn)
return string_sub(fn, 1, #spec + 1) == spec .. "."
and string_sub(fn, -4) == ".ogg"
end)
stealmedia(v.tiles, outdir, "textures")
stealmedia(v.special_tiles, outdir, "textures")
stealmedia(v.inventory_image, outdir, "textures")
stealmedia(v.wield_image, outdir, "textures")
stealmedia(v.mesh, outdir, "models")
outf:write("reg(" .. dumptable(v, {
locals = function(item)
return localized[item]
end
}) .. ")\n\n")
count = count + 1
end
outf:close()
return count
end
return exportall

16
exportdb.lua Normal file
View File

@ -0,0 +1,16 @@
-- LUALOCALS < ---------------------------------------------------------
local minetest
= minetest
-- LUALOCALS > ---------------------------------------------------------
local modstore = minetest.get_mod_storage()
local exportdb
local s = modstore:get_string("export")
exportdb = s and s ~= "" and minetest.deserialize(s) or {}
local function savedb()
modstore:set_string("export", minetest.serialize(exportdb))
end
return exportdb, savedb

56
fileops.lua Normal file
View File

@ -0,0 +1,56 @@
-- LUALOCALS < ---------------------------------------------------------
local error, io, ipairs, minetest, pairs, print, string, type
= error, io, ipairs, minetest, pairs, print, string, type
local io_open, string_format, string_gsub
= io.open, string.format, string.gsub
-- LUALOCALS > ---------------------------------------------------------
local include = ...
local mediacache = include("mediacache")
local function getdir(s)
if not minetest.mkdir(s) then return error(string_format("failed to mkdir %q", s)) end
return s
end
local function cpfile(src, dst)
local done = io_open(dst, "rb")
if done then
done:close()
return true
end
local sf = io_open(src, "rb")
if not sf then return error(string_format("failed to read %q", src)) end
local df = io_open(dst, "wb")
if not sf then return error(string_format("failed to write %q", dst)) end
while true do
local chunk = sf:read(65536)
if not chunk then
sf:close()
df:close()
return true
end
df:write(chunk)
end
end
local function stealmedia(thing, dest, rel, test)
test = test or function(a, b) return a == b end
if type(thing) == "string" then
for _, s in ipairs(string_gsub(thing, "[\\[:,=&{]", "^"):split("^")) do
print(thing .. " -> " .. s)
for k, v in pairs(mediacache) do
if test(s, k) then
cpfile(v, getdir(dest .. "/" .. rel) .. "/" .. k)
end
end
end
elseif type(thing) == "table" then
if thing.name then return stealmedia(thing.name, dest, rel, test) end
for _, v in pairs(thing) do
stealmedia(v, dest, rel, test)
end
end
end
return getdir, stealmedia

21
init.lua Normal file
View File

@ -0,0 +1,21 @@
-- LUALOCALS < ---------------------------------------------------------
local loadfile, minetest, unpack
= loadfile, minetest, unpack
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
local include
do
local modpath = minetest.get_modpath(modname)
local included = {}
include = function(n)
local found = included[n]
if found ~= nil then return unpack(found) end
found = {loadfile(modpath .. "/" .. n .. ".lua")(include)}
included[n] = found
return unpack(found)
end
end
include("commands")

21
mediacache.lua Normal file
View File

@ -0,0 +1,21 @@
-- LUALOCALS < ---------------------------------------------------------
local ipairs, minetest
= ipairs, minetest
-- LUALOCALS > ---------------------------------------------------------
local mediacache = {}
do
local function scan(path)
for _, v in ipairs(minetest.get_dir_list(path, false)) do
mediacache[v] = path .. "/" .. v
end
for _, v in ipairs(minetest.get_dir_list(path, true)) do
scan(path .. "/" .. v)
end
end
for _, n in ipairs(minetest.get_modnames()) do
scan(minetest.get_modpath(n))
end
end
return mediacache

1
mod.conf Normal file
View File

@ -0,0 +1 @@
name = defripper