mtsedit/mt-mod/mtsedit/init.lua

479 lines
20 KiB
Lua

--
-- mt-mod/minetest/init.lua
--
-- Copyright (C) 2019 bzt (bztsrc@gitlab)
--
-- Permission is hereby granted, free of charge, to any person
-- obtaining a copy of this software and associated documentation
-- files (the "Software"), to deal in the Software without
-- restriction, including without limitation the rights to use, copy,
-- modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be
-- included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.
--
-- @brief Minetest mod that adds support for exporting node spec to MTSEDit and import MTS files
-- https://gitlab.com/bztsrc/mtsedit
--
local S = minetest.get_translator("mtsedit")
mtsedit = { loadstat = {}, savestat = {} }
-- translations for some common Minetest nodes to Minecraft names as used by MTSEdit
-- both technical names and human readable names can be used as key, but spaces must
-- be converted to underscores '_' in the latter. Otherwise exact match expected.
mtsedit.translate = {
["Air"] = "",
["Cave_Ice"] = "Packed_Ice",
["Lily_Pad"] = "Waterlily",
["Papyrus"] = "Sugar_Canes",
["Bones"] = "Bone_Block",
-- needs "Block" to be added as an extra
["Double_Slab_of_Gold"] = "Double_Gold_Block_Slab",
["Double_Slab_of_Iron"] = "Double_Iron_Block_Slab",
["Slab_of_Gold"] = "Gold_Block_Slab",
["Slab_of_Iron"] = "Iron_Block_Slab"
}
-- these will be substituted in names
mtsedit.replacements = {
["_Wood"] = "",
["_Tree"] = "_Log",
["_Bars"] = "_Bar",
["_Bricks"] = "_Brick",
["_Stairs"] = "_Stair",
["_Planks"] = "_Plank",
["_Walls"] = "_Wall",
["Apple"] = "Birch",
["Steel"] = "Iron",
["Gray"] = "Grey",
["Wooden_"] = "Oak_",
["Moss_"] = "Mossy_",
["Mese_"] = "Redstone_"
}
mtsedit.replacements2 = {
["Block_of_"] = "_Block",
["Slab_of_"] = "_Slab",
["Stair_of_"] = "_Stair",
["Stairs_of_"] = "_Stair"
}
-- helper function to parse a CSV line into table
local function parsecsv(line)
local csv = {}
local pos = 1
if line == nil or line == "" then return nil end
while true do
local c = string.sub(line,pos,pos)
if (c == "" or c == "\r" or c == "\n") then break end
if (c == " ") then pos = pos + 1
elseif (c == '"') then
local txt = ""
repeat
local startp,endp = string.find(line,'^%b""',pos)
txt = txt..string.sub(line,startp+1,endp-1)
pos = endp + 1
c = string.sub(line,pos,pos)
if (c == '"') then txt = txt..'"' end
until (c ~= '"')
table.insert(csv,txt)
pos = pos + 1
else
local startp,endp = string.find(line,",",pos)
local txt = ""
if (startp) then
while (string.sub(line,startp,startp) == " ") do startp = startp - 1 end
table.insert(csv,string.sub(line,pos,startp-1))
pos = endp + 1
else
txt = string.gsub(string.gsub(string.sub(line,pos),"\r",""),"\n","")
table.insert(csv,txt)
break
end
end
end
return csv
end
-- helper to generate a block id name
local function getblockid(name, str)
-- if there's a known translation for the technical name
if (mtsedit.translate[name] ~= nil) then return mtsedit.translate[name] end
local blockid = string.gsub(string.gsub(str, "'", "_"), " ", "_")
-- cut off that I don't know what part
if string.sub(blockid,1,1) == "\27" then
local start2p,end2p = string.find(blockid,"\27",2)
local start1p,end1p = string.find(blockid,")",1)
if (start1p == nil) then start1p = 1 end
blockid = string.sub(blockid,start1p+1,start2p-1)
end
-- if there's a translation for the name
if (mtsedit.translate[blockid] ~= nil) then blockid = mtsedit.translate[blockid] end
for from,to in pairs(mtsedit.replacements2) do
if (string.sub(blockid,1,string.len(from)) == from) then
blockid = string.sub(blockid, string.len(from)+1)..to
end
end
for from,to in pairs(mtsedit.replacements) do blockid = string.gsub(blockid, from, to) end
return blockid
end
-- Export node data into CSV
function mtsedit.export(param)
if param == nil or param == "" then
return false, S("Specify <palette> (like \"Mineclone2\", \"Minetest Game\" etc.)")
end
local fn = minetest.get_worldpath() .. "/blocks.csv"
local defpalettes = { "_Name", "BlockID", "BlockName", "Biome Specific", "MapGen Alias", param }
local palettes, nodes, known, s = {}, {}, {}, {}
local line, node, i, cnt, idx = 0, "", 0, 1, 0
-- read in csv if exists
local file,err = io.open(fn, "r")
if file == nil or err then
palettes = defpalettes
file = nil
else
palettes = parsecsv(file:read())
if palettes == nil then
palettes = defpalettes
end
end
for i,t in ipairs(palettes) do if (cnt > 5 and t == param) then idx = i end cnt = cnt + 1 end
if (idx == 0) then
table.insert(palettes, param)
idx = cnt
end
idx = idx - 1
cnt = cnt - 1
if (file ~= nil) then
while true do
local node
line = parsecsv(file:read())
if (line == nil) then break end
node = string.gsub(string.gsub(table.remove(line, 1), "'", "_"), " ", "_")
nodes[node] = line
if (line[idx] ~= nil) then known[line[idx]] = node end
end
file:close()
end
-- add nodes which are defined in this game (nodes defined in enabled mods included)
for node,t in pairs(minetest.registered_nodes) do
local blockid
if (known[node] ~= nil) then
blockid = known[node]
else
blockid = getblockid(node, t["description"])
end
if (blockid ~= nil and blockid ~= "") then
-- if new node
if (nodes[blockid] == nil) then
i = 5
nodes[blockid] = { "", "", "", "" }
while (i < cnt) do table.insert(nodes[blockid], node) i = i + 1 end
end
-- if no biome specific name given already check if there's one
if (node ~= "air" and node ~= "ignore" and t["drawtype"] ~= "airlike" and
(nodes[blockid][3] == nil or nodes[blockid][3] == "")) then
for name,biome in pairs(minetest.registered_biomes) do
for propname,propval in pairs(biome) do
if (string.sub(propname,1,5) == "node_" and propval == node) then
nodes[blockid][3] = "biome:"..propname
break
end
end
end
end
-- mapgen specific name
if (nodes[blockid][4] == nil) then nodes[blockid][4] = "" end
if (nodes[blockid][4] == "") then
for mapgen,oname in pairs(minetest.registered_aliases) do
if (oname == node) then
nodes[blockid][4] = mapgen
break
end
end
end
-- replace block names
i = 5
while (i < cnt) do
if (i == idx or nodes[blockid][i] == nil or nodes[blockid][i] == "") then nodes[blockid][i] = node end
i = i + 1
end
end
end
-- write out updated blocks.csv
file,err = io.open(fn, "wb")
if (file == nil or err) then return false, S("Unable to write @1", fn) end
for i,t in ipairs(palettes) do
local start1p,end1p = string.find(t,",")
local start2p,end2p = string.find(t,'"')
if (i > 1) then file:write(",") end
if (start1p or start2p) then
file:write(string.format("%q", t))
else
file:write(t)
end
end
file:write("\r\n")
for node,t in pairs(nodes) do table.insert(s, node) end
table.sort(s)
for _,node in ipairs(s) do
local start1p,end1p = string.find(node,",")
local start2p,end2p = string.find(node,'"')
if (start1p or start2p) then
file:write(string.format("%q", node))
else
file:write(node)
end
for _,n in ipairs(nodes[node]) do
local startp,endp = string.find(n,",")
if (startp) then
file:write(","..string.format("%q", n))
else
file:write(","..n)
end
end
file:write("\r\n")
end
file:close()
return true, S("@1 saved.", fn)
end
-- Export node textures data
function mtsedit.imgs()
local fn = minetest.get_worldpath() .. "/blockimgs.csv"
local file,err = io.open(fn, "wb")
if err then return false, S("Unable to write @1", fn) end
-- write out blockimgs.csv
for node,t in pairs(minetest.registered_nodes) do
local blockid = getblockid(node, t["description"])
if (blockid ~= nil and blockid ~= "" and t["tiles"] ~= nil) then
file:write(string.format("%q", blockid)..","..string.format("%q", node))
if (t["drawtype"] == nil) then
file:write(",normal")
else
file:write(","..t["drawtype"])
end
if (t["paramtype2"] == nil) then
file:write(",regular")
else
file:write(","..t["paramtype2"])
end
if (t["node_box"] == nil or t["node_box"]["type"] == nil) then
file:write(",")
else
file:write(","..t["node_box"]["type"].."")
end
if (t["mod_origin"] == nil or t["mod_origin"] == "") then
file:write(",")
else
file:write(","..string.format("%q", minetest.get_modpath(t["mod_origin"])..""))
end
if (type(t["tiles"]) == "table") then
for _,tile in pairs(t["tiles"]) do
if (type(tile) == "table") then
file:write(","..string.format("%q", table.concat(tile,",")))
else
file:write(","..string.format("%q", tile..""))
end
end
else
file:write(","..string.format("%q", t["tiles"]..""))
end
file:write(",|")
if (type(t["overlay_tiles"]) == "table") then
for _,tile in pairs(t["overlay_tiles"]) do
if (type(tile) == "table") then
file:write(","..string.format("%q", table.concat(tile,",")))
else
file:write(","..string.format("%q", tile..""))
end
end
elseif (t["overlay_tiles"] ~= nil) then
file:write(","..string.format("%q", t["tiles"]..""))
end
file:write("\r\n")
end
end
file:close()
return true, S("@1 saved.", fn)
end
-- Load schematics into the World
function mtsedit.load(name, param)
if param == nil or param == "" then
return false, S("Specify <schematic> file")
end
-- do not let the user use the parent directory reference in the filename
local fn = minetest.get_worldpath() .. "/schems/" .. param .. ".mts"
local magic, x, z
if param:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") then
local file,err = io.open(fn, "rb")
if file ~= nil and err == nil then
magic = file:read(12)
file:close()
-- Fck Lua, string.unpack crashes Minetest
x = string.byte(string.sub(magic, 8, 8)) + string.byte(string.sub(magic, 7, 7)) * 256
z = string.byte(string.sub(magic, 12, 12)) + string.byte(string.sub(magic, 11, 11)) * 256
end
end
if magic == nil or string.sub(magic, 1, 4) ~= "MTSM" then return false, S("Failed to load @1", fn) end
mtsedit.loadstat[name] = { fn, x, z }
return true, S("Schematic loaded, use right click with the placement tool in your inventory")
end
-- List available schematic files
function mtsedit.list(name)
local path = minetest.get_worldpath() .. "/schems/"
local list = minetest.get_dir_list(path, false)
minetest.chat_send_player(name, "mtsedit: *.mts; PATH=" .. path)
for i,n in pairs(list) do
local l = string.len(n) - 4
if string.sub(n, l+1) == ".mts" then minetest.chat_send_player(name, " " .. string.sub(n, 1, l)) end
end
end
-- Save selected area into schematic file
function mtsedit.save(name, param)
if param == nil or param == "" then
return false, S("Specify <schematic> file")
end
if mtsedit.savestat[name] == nil then
return false, S("Select an area first by left clicking with the placement tool")
end
mtsedit.savestat[name][1]:remove()
mtsedit.savestat[name][1] = nil
local fn = minetest.get_worldpath() .. "/schems/" .. param .. ".mts"
if param:find("^[%w%s%^&'@{}%[%],%$=!%-#%(%)%%%.%+~_]+$") and minetest.create_schematic(mtsedit.savestat[name][2], mtsedit.savestat[name][3], nil, fn) ~= nil then
minetest.chat_send_player(name, "mtsedit: " .. S("Area") .. " " .. minetest.pos_to_string(mtsedit.savestat[name][2]).. " - " .. minetest.pos_to_string(mtsedit.savestat[name][3]))
return true, S("@1 saved.", fn)
end
return false, S("Unable to write @1", fn)
end
-- Grow or shrink the selected area
function mtsedit.grow(name, param)
if param == nil or param == "" then
return false, S("Specify <delta> value")
end
if mtsedit.savestat[name] ~= nil and mtsedit.savestat[name][1] ~= nil then
local props = mtsedit.savestat[name][1]:get_properties()
if props ~= nil and props["visual_size"] ~= nil then
local siz = props["visual_size"]
mtsedit.savestat[name][1]:set_properties({visual_size={x=siz.x + param, y=siz.y + param, z=siz.z + param}})
end
end
return true, ""
end
-- Register selected area marker
minetest.register_entity(":mtsedit:selection", {
initial_properties = {
visual = "cube",
visual_size = {x=1.3, y=1.3, z=1.3},
textures = {"mtsedit_select.png", "mtsedit_select.png", "mtsedit_select.png",
"mtsedit_select.png", "mtsedit_select.png", "mtsedit_select.png"},
physical = false,
use_texture_alpha = true,
backface_culling = false,
static_save = false,
glow = 0.5
},
})
-- Register placement tool
minetest.register_tool("mtsedit:place", {
description = S("Place the loaded structure"),
inventory_image = "mtsedit_place.png",
range = 8.0,
light_source = 14,
on_use = function(itemstack, user, pointed_thing)
local name = user:get_player_name()
local pos = user:get_pos()
pos.x, pos.y, pos.z = math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5)
if pointed_thing.type == "node" or mtsedit.savestat[name] == nil then
if pointed_thing.type == "node" then pos = pointed_thing.under end
if mtsedit.savestat[name] ~= nil then mtsedit.savestat[name][1]:remove() end
mtsedit.savestat[name] = { minetest.add_entity(pointed_thing.under, "mtsedit:selection"), pos }
elseif mtsedit.savestat[name] ~= nil then
local p1, p2, mp = {}, {}, mtsedit.savestat[name][2]
p1 = { x = math.min(mp.x, pos.x), y = math.min(mp.y, pos.y), z = math.min(mp.z, pos.z) }
p2 = { x = math.max(mp.x, pos.x), y = math.max(mp.y, pos.y), z = math.max(mp.z, pos.z) }
mp = { x = math.floor((1 + p2.x - p1.x) / 2) + p1.x, y = math.floor((1 + p2.y - p1.y) / 2) + p1.y, z = math.floor((1 + p2.z - p1.z) / 2) + p1.z}
mtsedit.savestat[name][1]:set_pos(mp)
mtsedit.savestat[name][1]:set_properties({visual_size={x=p2.x-p1.x, y=p2.y-p1.y, z=p2.z-p1.z}})
mtsedit.savestat[name][3] = pos
end
return nil
end,
on_place = function(itemstack, user, pointed_thing)
local name = user:get_player_name()
if pointed_thing.type == "node" then
if mtsedit.loadstat[name] ~= nil then
local pos, dir, st = pointed_thing.above, user:get_look_dir(), mtsedit.loadstat[name]
local d, r, x, z = "z", 0, math.abs(dir.x), math.abs(dir.z)
if x > z then
if dir.x > 0 then r = 90 else r = 270; pos.x = pos.x - st[2] end
else
if dir.z < 0 then r = 180; pos.z = pos.z - st[3]; end; d = "x"
end
if minetest.place_schematic(pos, st[1], r, {}, true, {["place_center_"..d] = true}) == nil then
minetest.chat_send_player(name, "mtsedit: " .. S("Failed to place the schematic @1", fn))
end
else
minetest.chat_send_player(name, "mtsedit: " .. S("First use the \"/mtsedit load\" chat command to load a schematic"))
end
end
end,
})
-- Register chat commands
minetest.register_chatcommand("mtsedit", {
description = S("Load and save MTS files easily or export MTSEdit block data"),
params = S("[load|list|save|export|imgs|help] [file]"),
func = function(name, param)
-- give placement tool
local structitem = ItemStack("mtsedit:place")
local inv = minetest.get_inventory({type="player",name=name})
if not inv:contains_item("main", "mtsedit:place") and not inv:contains_item("hand", "mtsedit:place") then
local left = inv:add_item("hand", structitem)
if not left:is_empty() then left = inv:add_item("main", left) end
end
-- parse arguments
if param == nil or param == "" or param == "help" then
minetest.chat_send_player(name, " /mtsedit load <mts> - " .. S("Load an MTS file from \"(world directory)/schems/<schematic>.mts\" into the world"))
minetest.chat_send_player(name, " /mtsedit list - " .. S("Show available schematic files"))
minetest.chat_send_player(name, " /mtsedit save <mts> - " .. S("Save the selected area into an MTS file"))
minetest.chat_send_player(name, " /mtsedit grow <delta> - " .. S("Grow or shrink the selected area"))
minetest.chat_send_player(name, " /mtsedit export <palette> - " .. S("Export node data to <palette> column of \"(world directory)/blocks.csv\""))
minetest.chat_send_player(name, " /mtsedit imgs - " .. S("Export node texture data to \"(world directory)/blockimgs.csv\""))
else
local startp,endp = string.find(param .. " ", " ", 3)
local arg = string.sub(param, startp+1)
if string.sub(param, 1, 4) == "load" then return mtsedit.load(name, arg)
elseif string.sub(param, 1, 4) == "list" then return mtsedit.list(name)
elseif string.sub(param, 1, 4) == "save" then return mtsedit.save(name, arg)
elseif string.sub(param, 1, 4) == "grow" then return mtsedit.grow(name, arg)
elseif string.sub(param, 1, 6) == "export" then return mtsedit.export(arg)
elseif string.sub(param, 1, 4) == "imgs" then return mtsedit.imgs()
else return false, S("Unknown mtsedit command") end
end
return false, ""
end,
})