Composition tool (#6)
* persist template metadata * composition tool * composition utils * wip * wip * use swap_node for handles * formspec * wip * set composition origin to first min-pos * composition duplication and we area * wallmounted replacement --------- Co-authored-by: BuckarooBanzay <BuckarooBanzay@users.noreply.github.com>
This commit is contained in:
parent
4b7be7442a
commit
3466f84540
@ -1,7 +1,8 @@
|
||||
std = "minetest+min"
|
||||
|
||||
globals = {
|
||||
"pick_and_place"
|
||||
"pick_and_place",
|
||||
"worldedit"
|
||||
}
|
||||
|
||||
read_globals = {
|
||||
|
42
common.lua
42
common.lua
@ -48,4 +48,46 @@ function pick_and_place.get_outer_corners(pos1, pos2)
|
||||
{ x=pos2.x, y=pos2.y, z=pos1.z },
|
||||
{ x=pos2.x, y=pos2.y, z=pos2.z }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
function pick_and_place.get_replacement_nodeid(ctx, metadata)
|
||||
local group = metadata.fields.group
|
||||
local selected_name
|
||||
if group and group ~= "" and ctx[group] then
|
||||
-- group placement
|
||||
selected_name = metadata.inventory.main[ctx[group]]
|
||||
else
|
||||
-- random placement
|
||||
local replacement_names = {}
|
||||
for _, name in ipairs(metadata.inventory.main) do
|
||||
if name ~= "" then
|
||||
table.insert(replacement_names, name)
|
||||
end
|
||||
end
|
||||
|
||||
if #replacement_names == 0 then
|
||||
-- no replacement
|
||||
return
|
||||
end
|
||||
|
||||
local i = math.random(#replacement_names)
|
||||
selected_name = replacement_names[i]
|
||||
|
||||
-- set group context
|
||||
if group and group ~= "" then
|
||||
ctx[group] = i
|
||||
end
|
||||
end
|
||||
|
||||
local stack = ItemStack(selected_name)
|
||||
local nodename = stack:get_name()
|
||||
|
||||
if not minetest.registered_nodes[nodename] then
|
||||
-- node not found
|
||||
return
|
||||
end
|
||||
|
||||
local nodeid = minetest.get_content_id(nodename)
|
||||
return nodeid
|
||||
end
|
330
composition.lua
Normal file
330
composition.lua
Normal file
@ -0,0 +1,330 @@
|
||||
local function serialize_composition(composition)
|
||||
local serialized_data = minetest.serialize(composition)
|
||||
local compressed_data = minetest.compress(serialized_data, "deflate")
|
||||
return minetest.encode_base64(compressed_data)
|
||||
end
|
||||
|
||||
local function deserialize_composition(str)
|
||||
local compressed_data = minetest.decode_base64(str)
|
||||
local serialized_data = minetest.decompress(compressed_data, "deflate")
|
||||
return minetest.deserialize(serialized_data)
|
||||
end
|
||||
|
||||
-- playername -> tool-id
|
||||
local active_tools = {}
|
||||
|
||||
-- tool-id -> composition
|
||||
local compositions = {}
|
||||
|
||||
-- TODO: sync inventory with global state
|
||||
|
||||
local function get_current_composition_tool(playername)
|
||||
local id = active_tools[playername]
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local player = minetest.get_player_by_name(playername)
|
||||
local inv = player:get_inventory()
|
||||
|
||||
local list = inv:get_list("main")
|
||||
for _, itemstack in ipairs(list) do
|
||||
if itemstack:get_name() == "pick_and_place:composition" then
|
||||
local meta = itemstack:get_meta()
|
||||
local stack_id = meta:get_string("id")
|
||||
if stack_id == id then
|
||||
-- match
|
||||
return itemstack
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_current_composition_tool(playername, new_itemstack)
|
||||
local id = active_tools[playername]
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local player = minetest.get_player_by_name(playername)
|
||||
local inv = player:get_inventory()
|
||||
|
||||
local list = inv:get_list("main")
|
||||
for i, itemstack in ipairs(list) do
|
||||
if itemstack:get_name() == "pick_and_place:composition" then
|
||||
local meta = itemstack:get_meta()
|
||||
local stack_id = meta:get_string("id")
|
||||
if stack_id == id then
|
||||
-- match
|
||||
inv:set_stack("main", i, new_itemstack)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function update(itemstack, playername, state)
|
||||
local meta = itemstack:get_meta()
|
||||
pick_and_place.update_composition_tool(meta)
|
||||
local id = meta:get_string("id")
|
||||
|
||||
if state then
|
||||
meta:set_string("state", state)
|
||||
|
||||
if state == "record" then
|
||||
meta:set_string("color", "#ff0000") -- red
|
||||
active_tools[playername] = id
|
||||
local data = meta:get_string("data")
|
||||
if data ~= "" then
|
||||
-- existing composition
|
||||
compositions[id] = deserialize_composition(data)
|
||||
else
|
||||
-- new composition
|
||||
compositions[id] = { entries = {} }
|
||||
end
|
||||
else
|
||||
meta:set_string("color", "#0000ff") -- blue
|
||||
active_tools[playername] = nil
|
||||
compositions[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: check state
|
||||
pick_and_place.update_composition_tool(meta)
|
||||
end
|
||||
|
||||
function pick_and_place.update_composition_tool(meta)
|
||||
local id = meta:get_string("id")
|
||||
if id == "" then
|
||||
-- initialize
|
||||
id = pick_and_place.create_id()
|
||||
meta:set_string("id", id)
|
||||
end
|
||||
|
||||
local name = meta:get_string("name")
|
||||
local data = meta:get_string("data")
|
||||
local bytes = #data
|
||||
local entries = meta:get_int("entries")
|
||||
|
||||
local desc = string.format("Composition tool '%s' (id: %s, %d entries, %d bytes)", name, id, entries, bytes)
|
||||
meta:set_string("description", desc)
|
||||
end
|
||||
|
||||
function pick_and_place.update_composition_fields(itemstack, playername, fields)
|
||||
local meta = itemstack:get_meta()
|
||||
meta:set_string("name", fields.name)
|
||||
update(itemstack, playername)
|
||||
end
|
||||
|
||||
function pick_and_place.record_composition(itemstack, playername)
|
||||
update(itemstack, playername, "record")
|
||||
end
|
||||
|
||||
function pick_and_place.pause_composition(itemstack, playername)
|
||||
update(itemstack, playername, "pause")
|
||||
end
|
||||
|
||||
function pick_and_place.play_composition(itemstack, playername)
|
||||
local meta = itemstack:get_meta()
|
||||
|
||||
local origin = meta:get_string("origin")
|
||||
local pos = minetest.string_to_pos(origin)
|
||||
if not pos then
|
||||
return
|
||||
end
|
||||
|
||||
local data = meta:get_string("data")
|
||||
if data ~= "" then
|
||||
local composition = deserialize_composition(data)
|
||||
|
||||
local success, msg = pick_and_place.start_playback(playername, pos, composition)
|
||||
if not success then
|
||||
minetest.chat_send_player(playername, msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function pick_and_place.mark_composition_area(itemstack, playername)
|
||||
local meta = itemstack:get_meta()
|
||||
local origin = minetest.string_to_pos(meta:get_string("origin"))
|
||||
if not origin then
|
||||
return
|
||||
end
|
||||
|
||||
local data = meta:get_string("data")
|
||||
if not data then
|
||||
return
|
||||
end
|
||||
|
||||
local composition = deserialize_composition(data)
|
||||
if not composition then
|
||||
return
|
||||
end
|
||||
|
||||
if not minetest.get_modpath("worldedit") then
|
||||
return
|
||||
end
|
||||
|
||||
if composition.min_pos then
|
||||
local pos1 = vector.add(origin, composition.min_pos)
|
||||
worldedit.pos1[playername] = pos1
|
||||
worldedit.mark_pos1(playername);
|
||||
end
|
||||
|
||||
if composition.max_pos then
|
||||
local pos2 = vector.add(origin, composition.max_pos)
|
||||
worldedit.pos2[playername] = pos2
|
||||
worldedit.mark_pos2(playername);
|
||||
end
|
||||
end
|
||||
|
||||
function pick_and_place.duplicate_composition_tool(itemstack, playername)
|
||||
itemstack = ItemStack(itemstack)
|
||||
local meta = itemstack:get_meta()
|
||||
meta:set_string("id", pick_and_place.create_id())
|
||||
pick_and_place.update_composition_tool(meta)
|
||||
|
||||
local player = minetest.get_player_by_name(playername)
|
||||
local inv = player:get_inventory()
|
||||
inv:add_item("main", itemstack)
|
||||
end
|
||||
|
||||
function pick_and_place.set_composition_origin(itemstack, playername)
|
||||
local player = minetest.get_player_by_name(playername)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
local pos = vector.round(player:get_pos())
|
||||
local meta = itemstack:get_meta()
|
||||
meta:set_string("origin", minetest.pos_to_string(pos))
|
||||
end
|
||||
|
||||
function pick_and_place.tp_composition_origin(itemstack, playername)
|
||||
local player = minetest.get_player_by_name(playername)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local meta = itemstack:get_meta()
|
||||
local origin = meta:get_string("origin")
|
||||
local pos = minetest.string_to_pos(origin)
|
||||
if not pos then
|
||||
return
|
||||
end
|
||||
|
||||
player:set_pos(pos)
|
||||
end
|
||||
|
||||
local function track_min_max_pos(composition, pos)
|
||||
if not composition.min_pos then
|
||||
composition.min_pos = vector.copy(pos)
|
||||
elseif pos.x < composition.min_pos.x then
|
||||
composition.min_pos.x = pos.x
|
||||
elseif pos.y < composition.min_pos.y then
|
||||
composition.min_pos.y = pos.y
|
||||
elseif pos.z < composition.min_pos.z then
|
||||
composition.min_pos.z = pos.z
|
||||
end
|
||||
|
||||
if not composition.max_pos then
|
||||
composition.max_pos = vector.copy(pos)
|
||||
elseif pos.x > composition.max_pos.x then
|
||||
composition.max_pos.x = pos.x
|
||||
elseif pos.y > composition.max_pos.y then
|
||||
composition.max_pos.y = pos.y
|
||||
elseif pos.z > composition.max_pos.z then
|
||||
composition.max_pos.z = pos.z
|
||||
end
|
||||
end
|
||||
|
||||
function pick_and_place.record_removal(playername, pos1, pos2)
|
||||
local itemstack = get_current_composition_tool(playername)
|
||||
if not itemstack then
|
||||
return
|
||||
end
|
||||
|
||||
local meta = itemstack:get_meta()
|
||||
local id = meta:get_string("id")
|
||||
local origin = minetest.string_to_pos(meta:get_string("origin"))
|
||||
if not origin then
|
||||
return
|
||||
end
|
||||
|
||||
local composition = compositions[id]
|
||||
if not composition or #composition.entries == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local rel_pos1 = vector.subtract(pos1, origin)
|
||||
local rel_pos2 = vector.subtract(pos2, origin)
|
||||
track_min_max_pos(composition, rel_pos1)
|
||||
track_min_max_pos(composition, rel_pos2)
|
||||
|
||||
-- search and remove exact pos1/2 matches
|
||||
local entry_removed = false
|
||||
for i, entry in ipairs(composition.entries) do
|
||||
if vector.equals(entry.pos1, rel_pos1) and vector.equals(entry.pos2, rel_pos2) then
|
||||
-- remove matching entry
|
||||
table.remove(composition.entries, i)
|
||||
entry_removed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not entry_removed then
|
||||
-- non-aligned removal, just record
|
||||
table.insert(composition.entries, {
|
||||
type = "remove",
|
||||
pos1 = rel_pos1,
|
||||
pos2 = rel_pos2
|
||||
})
|
||||
end
|
||||
|
||||
meta:set_string("entries", #composition.entries)
|
||||
meta:set_string("data", serialize_composition(composition))
|
||||
set_current_composition_tool(playername, itemstack)
|
||||
end
|
||||
|
||||
function pick_and_place.record_placement(playername, pos1, pos2, rotation, name, id)
|
||||
local itemstack = get_current_composition_tool(playername)
|
||||
if not itemstack then
|
||||
return
|
||||
end
|
||||
|
||||
local meta = itemstack:get_meta()
|
||||
local tool_id = meta:get_string("id")
|
||||
local origin = minetest.string_to_pos(meta:get_string("origin"))
|
||||
if not origin then
|
||||
-- set origin to pos1
|
||||
origin = vector.copy(pos1)
|
||||
meta:set_string("origin", minetest.pos_to_string(origin))
|
||||
end
|
||||
|
||||
local composition = compositions[tool_id]
|
||||
local rel_pos1 = vector.subtract(pos1, origin)
|
||||
local rel_pos2 = vector.subtract(pos2, origin)
|
||||
track_min_max_pos(composition, rel_pos1)
|
||||
track_min_max_pos(composition, rel_pos2)
|
||||
|
||||
-- search and remove exact pos1/2 matches
|
||||
for i, entry in ipairs(composition.entries) do
|
||||
if vector.equals(entry.pos1, rel_pos1) and vector.equals(entry.pos2, rel_pos2) then
|
||||
-- remove matching entry
|
||||
table.remove(composition.entries, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(composition.entries, {
|
||||
type = "place",
|
||||
pos1 = rel_pos1,
|
||||
pos2 = rel_pos2,
|
||||
rotation = rotation,
|
||||
name = name,
|
||||
id = id
|
||||
})
|
||||
|
||||
meta:set_string("entries", #composition.entries)
|
||||
meta:set_string("data", serialize_composition(composition))
|
||||
set_current_composition_tool(playername, itemstack)
|
||||
end
|
88
composition_tool.lua
Normal file
88
composition_tool.lua
Normal file
@ -0,0 +1,88 @@
|
||||
local FORMSPEC_NAME = "pick_and_place:composition"
|
||||
|
||||
local function get_formspec(_, meta)
|
||||
local name = minetest.formspec_escape(meta:get_string("name"))
|
||||
local id = meta:get_string("id")
|
||||
|
||||
local origin = meta:get_string("origin")
|
||||
local data = meta:get_string("data")
|
||||
local bytes = #data
|
||||
local entries = meta:get_int("entries")
|
||||
|
||||
local state = meta:get_string("state")
|
||||
|
||||
return [[
|
||||
size[10,5]
|
||||
real_coordinates[true]
|
||||
|
||||
label[0.1,0.5;Name]
|
||||
field[2,0;6,1;name;;]] .. name .. [[]
|
||||
button_exit[8,0;2,1;save;Save]
|
||||
|
||||
label[0.1,1.5;Origin]
|
||||
label[2.1,1.5;]] .. (origin ~= "" and origin or "<not set>") .. [[]
|
||||
button_exit[6,1;2,1;set_origin;Set origin]
|
||||
button_exit[8,1;2,1;tp_origin;Teleport]
|
||||
|
||||
label[0.1,2.5;Stats]
|
||||
label[2,2.5;]] .. "ID: " .. id .. " Entries: " .. entries .. " / " .. bytes .. " bytes" .. [[]
|
||||
|
||||
label[0.1,3.5;Status]
|
||||
label[2,3.5;]] .. "Not active" .. [[]
|
||||
|
||||
label[0.1,4.5;Actions]
|
||||
button_exit[2,4;2,1;]] .. (state == "record" and "pause;Pause" or "record;Record") .. [[]
|
||||
button_exit[4,4;2,1;playback;Playback]
|
||||
button_exit[6,4;2,1;mark_area;Mark Area]
|
||||
button_exit[8,4;2,1;duplicate;Duplicate]
|
||||
]]
|
||||
end
|
||||
|
||||
minetest.register_tool("pick_and_place:composition", {
|
||||
description = "Composition tool (new)",
|
||||
inventory_image = "pick_and_place_composition.png",
|
||||
stack_max = 1,
|
||||
range = 0,
|
||||
color = "#0000ff",
|
||||
on_use = function(itemstack, player)
|
||||
local meta = itemstack:get_meta()
|
||||
pick_and_place.update_composition_tool(meta)
|
||||
local playername = player:get_player_name()
|
||||
minetest.show_formspec(playername, FORMSPEC_NAME, get_formspec(player, meta))
|
||||
return itemstack
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= FORMSPEC_NAME then
|
||||
return false
|
||||
end
|
||||
|
||||
local itemstack = player:get_wielded_item()
|
||||
if itemstack:get_name() ~= "pick_and_place:composition" then
|
||||
return true
|
||||
end
|
||||
|
||||
local playername = player:get_player_name()
|
||||
if fields.save then
|
||||
pick_and_place.update_composition_fields(itemstack, playername, fields)
|
||||
elseif fields.record then
|
||||
pick_and_place.record_composition(itemstack, playername)
|
||||
elseif fields.pause then
|
||||
pick_and_place.pause_composition(itemstack, playername)
|
||||
elseif fields.playback then
|
||||
pick_and_place.play_composition(itemstack, playername)
|
||||
elseif fields.set_origin then
|
||||
pick_and_place.set_composition_origin(itemstack, playername)
|
||||
elseif fields.tp_origin then
|
||||
pick_and_place.tp_composition_origin(itemstack, playername)
|
||||
elseif fields.mark_area then
|
||||
pick_and_place.mark_composition_area(itemstack, playername)
|
||||
elseif fields.duplicate then
|
||||
pick_and_place.duplicate_composition_tool(itemstack, playername)
|
||||
end
|
||||
|
||||
player:set_wielded_item(itemstack)
|
||||
|
||||
return true
|
||||
end)
|
@ -54,7 +54,7 @@ function pick_and_place.configure(pos1, pos2, name, id)
|
||||
for _, cpos in ipairs(pick_and_place.get_outer_corners(pos1, pos2)) do
|
||||
local node = minetest.get_node(cpos)
|
||||
if node.name == "air" or node.name == "pick_and_place:handle" then
|
||||
minetest.set_node(cpos, { name = "pick_and_place:handle" })
|
||||
minetest.swap_node(cpos, { name = "pick_and_place:handle" })
|
||||
local meta = minetest.get_meta(cpos)
|
||||
|
||||
-- relative positions
|
||||
|
4
init.lua
4
init.lua
@ -12,6 +12,7 @@ dofile(MP .. "/schematic_flip.lua")
|
||||
dofile(MP .. "/schematic_orient.lua")
|
||||
dofile(MP .. "/schematic_transpose.lua")
|
||||
dofile(MP .. "/replacement.lua")
|
||||
dofile(MP .. "/replacement_wallmounted.lua")
|
||||
dofile(MP .. "/pointed.lua")
|
||||
dofile(MP .. "/configure.lua")
|
||||
dofile(MP .. "/remove.lua")
|
||||
@ -23,9 +24,10 @@ dofile(MP .. "/create_tool.lua")
|
||||
dofile(MP .. "/configure_tool.lua")
|
||||
dofile(MP .. "/pick_tool.lua")
|
||||
dofile(MP .. "/place_tool.lua")
|
||||
dofile(MP .. "/composition_tool.lua")
|
||||
dofile(MP .. "/composition.lua")
|
||||
dofile(MP .. "/preview.lua")
|
||||
dofile(MP .. "/craft.lua")
|
||||
dofile(MP .. "/record.lua")
|
||||
dofile(MP .. "/playback.lua")
|
||||
dofile(MP .. "/registry.lua")
|
||||
dofile(MP .. "/snap.lua")
|
||||
|
@ -32,7 +32,7 @@ minetest.register_tool("pick_and_place:place", {
|
||||
if controls.aux1 then
|
||||
-- removal
|
||||
pick_and_place.remove_area(pos1, pos2)
|
||||
pick_and_place.record_removal(pos1, pos2)
|
||||
pick_and_place.record_removal(playername, pos1, pos2)
|
||||
notify_change(pos1, pos2)
|
||||
else
|
||||
-- placement
|
||||
@ -51,7 +51,7 @@ minetest.register_tool("pick_and_place:place", {
|
||||
minetest.chat_send_player(playername, "Placement error: " .. msg)
|
||||
else
|
||||
if name ~= "" then
|
||||
pick_and_place.record_placement(pos1, pos2, rotation, name, id)
|
||||
pick_and_place.record_placement(playername, pos1, pos2, rotation, name, id)
|
||||
end
|
||||
notify_change(pos1, pos2)
|
||||
end
|
||||
|
16
playback.lua
16
playback.lua
@ -9,16 +9,20 @@ local function playback(ctx)
|
||||
ctx.i = ctx.i + 1
|
||||
|
||||
-- pick next entry
|
||||
local entry = ctx.recording.entries[ctx.i]
|
||||
local entry = ctx.composition.entries[ctx.i]
|
||||
if not entry then
|
||||
minetest.chat_send_player(ctx.playername, "pnp playback done with " .. (ctx.i-1) .. " entries")
|
||||
minetest.chat_send_player(
|
||||
ctx.playername, "composition playback done with " .. (ctx.i-1) .. " entries"
|
||||
)
|
||||
playback_active = false
|
||||
return
|
||||
end
|
||||
|
||||
if ctx.i % 10 == 0 then
|
||||
-- status update
|
||||
minetest.chat_send_player(ctx.playername, "pnp playback: entry " .. ctx.i .. "/" .. #ctx.recording.entries)
|
||||
minetest.chat_send_player(
|
||||
ctx.playername, "composition playback: entry " .. ctx.i .. "/" .. #ctx.composition.entries
|
||||
)
|
||||
end
|
||||
|
||||
if entry.type == "place" then
|
||||
@ -38,7 +42,7 @@ local function playback(ctx)
|
||||
local abs_pos1 = vector.add(ctx.origin, entry.pos1)
|
||||
pick_and_place.deserialize(abs_pos1, schematic)
|
||||
else
|
||||
minetest.chat_send_player(ctx.playername, "pnp playback: template not found: '" .. entry.id .. "'")
|
||||
minetest.chat_send_player(ctx.playername, "composition playback: template not found: '" .. entry.id .. "'")
|
||||
end
|
||||
elseif entry.type == "remove" then
|
||||
local abs_pos1 = vector.add(ctx.origin, entry.pos1)
|
||||
@ -51,7 +55,7 @@ local function playback(ctx)
|
||||
minetest.after(0, playback, ctx)
|
||||
end
|
||||
|
||||
function pick_and_place.start_playback(playername, origin, recording)
|
||||
function pick_and_place.start_playback(playername, origin, composition)
|
||||
if playback_active then
|
||||
return false, "playback already running"
|
||||
end
|
||||
@ -59,7 +63,7 @@ function pick_and_place.start_playback(playername, origin, recording)
|
||||
playback({
|
||||
playername = playername,
|
||||
origin = origin,
|
||||
recording = recording,
|
||||
composition = composition,
|
||||
i = 0,
|
||||
cache = {}
|
||||
})
|
||||
|
190
record.lua
190
record.lua
@ -1,190 +0,0 @@
|
||||
|
||||
-- current state of recording
|
||||
local state = false
|
||||
|
||||
-- current recording entry
|
||||
local recording
|
||||
|
||||
-- current recording origin
|
||||
local origin
|
||||
|
||||
local function reset_recording()
|
||||
recording = {
|
||||
entries = {}
|
||||
}
|
||||
end
|
||||
reset_recording()
|
||||
|
||||
minetest.register_chatcommand("pnp_record_save", {
|
||||
params = "[filename]",
|
||||
description = "save the current recording to a file in the world-directory",
|
||||
func = function(_, param)
|
||||
local json = minetest.write_json(recording)
|
||||
local filename = minetest.get_worldpath() .. "/" .. param .. ".json"
|
||||
local f = io.open(filename, "w")
|
||||
f:write(json)
|
||||
f:close()
|
||||
return true, "saved " .. #recording.entries .. " entries to '" .. filename .. "'"
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_chatcommand("pnp_record_load", {
|
||||
params = "[filename]",
|
||||
description = "loads a recording from a file in the world-directory",
|
||||
func = function(_, param)
|
||||
local filename = minetest.get_worldpath() .. "/" .. param .. ".json"
|
||||
local f = io.open(filename, "r")
|
||||
if not f then
|
||||
return false, "file not found: '" .. filename .. "'"
|
||||
end
|
||||
recording = minetest.parse_json(f:read("*all"))
|
||||
if not recording then
|
||||
reset_recording()
|
||||
return false, "could not parse file '" .. filename .. "'"
|
||||
end
|
||||
return true, "read " .. #recording.entries .. " entries from '" .. filename .. "'"
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
|
||||
minetest.register_chatcommand("pnp_record", {
|
||||
params = "[origin|start|info|pause|reset|play]",
|
||||
description = "manages the recording state or plays the current recording",
|
||||
func = function(name, param)
|
||||
if param == "origin" then
|
||||
if state then
|
||||
return false, "origin can't be set while the recording is active"
|
||||
end
|
||||
-- set origin to current player pos
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if not player then
|
||||
return false, "player not found"
|
||||
end
|
||||
origin = vector.round(player:get_pos())
|
||||
return true, "origin set to: " .. minetest.pos_to_string(origin)
|
||||
end
|
||||
|
||||
if not origin then
|
||||
return false, "origin not set, please use '/pnp_record origin' first"
|
||||
end
|
||||
|
||||
if param == "start" then
|
||||
state = true
|
||||
return true, "recording started"
|
||||
|
||||
elseif param == "pause" then
|
||||
state = false
|
||||
return true, "recording paused"
|
||||
|
||||
elseif param == "reset" then
|
||||
reset_recording()
|
||||
return true, "recording reset"
|
||||
|
||||
elseif param == "play" then
|
||||
return pick_and_place.start_playback(name, origin, recording)
|
||||
|
||||
else
|
||||
local msg = "recording state: "
|
||||
|
||||
if state then
|
||||
msg = msg .. "running"
|
||||
else
|
||||
msg = msg .. "stopped/paused"
|
||||
end
|
||||
|
||||
msg = msg .. ", entries: " .. #recording.entries
|
||||
|
||||
msg = msg .. ", origin: "
|
||||
if origin then
|
||||
msg = msg .. minetest.pos_to_string(origin)
|
||||
else
|
||||
msg = msg .. "<not set>"
|
||||
end
|
||||
|
||||
if recording.min_pos then
|
||||
msg = msg .. ", min_pos: " .. minetest.pos_to_string(recording.min_pos)
|
||||
end
|
||||
if recording.max_pos then
|
||||
msg = msg .. ", max_pos: " .. minetest.pos_to_string(recording.max_pos)
|
||||
end
|
||||
return true, msg
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
local function track_min_max_pos(pos)
|
||||
if not recording.min_pos then
|
||||
recording.min_pos = vector.copy(pos)
|
||||
elseif pos.x < recording.min_pos.x then
|
||||
recording.min_pos.x = pos.x
|
||||
elseif pos.y < recording.min_pos.y then
|
||||
recording.min_pos.y = pos.y
|
||||
elseif pos.z < recording.min_pos.z then
|
||||
recording.min_pos.z = pos.z
|
||||
end
|
||||
|
||||
if not recording.max_pos then
|
||||
recording.max_pos = vector.copy(pos)
|
||||
elseif pos.x > recording.max_pos.x then
|
||||
recording.max_pos.x = pos.x
|
||||
elseif pos.y > recording.max_pos.y then
|
||||
recording.max_pos.y = pos.y
|
||||
elseif pos.z > recording.max_pos.z then
|
||||
recording.max_pos.z = pos.z
|
||||
end
|
||||
end
|
||||
|
||||
function pick_and_place.record_removal(pos1, pos2)
|
||||
if not state or not origin then
|
||||
return
|
||||
end
|
||||
if #recording.entries == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local rel_pos1 = vector.subtract(pos1, origin)
|
||||
local rel_pos2 = vector.subtract(pos2, origin)
|
||||
track_min_max_pos(rel_pos1)
|
||||
track_min_max_pos(rel_pos2)
|
||||
|
||||
-- search and remove exact pos1/2 matches
|
||||
local entry_removed = false
|
||||
for i, entry in ipairs(recording.entries) do
|
||||
if vector.equals(entry.pos1, rel_pos1) and vector.equals(entry.pos2, rel_pos2) then
|
||||
-- remove matching entry
|
||||
table.remove(recording.entries, i)
|
||||
entry_removed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not entry_removed then
|
||||
-- non-aligned removal, just record
|
||||
table.insert(recording.entries, {
|
||||
type = "remove",
|
||||
pos1 = rel_pos1,
|
||||
pos2 = rel_pos2
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function pick_and_place.record_placement(pos1, pos2, rotation, name, id)
|
||||
if not state or not origin then
|
||||
return
|
||||
end
|
||||
|
||||
local rel_pos1 = vector.subtract(pos1, origin)
|
||||
local rel_pos2 = vector.subtract(pos2, origin)
|
||||
track_min_max_pos(rel_pos1)
|
||||
track_min_max_pos(rel_pos2)
|
||||
|
||||
table.insert(recording.entries, {
|
||||
type = "place",
|
||||
pos1 = rel_pos1,
|
||||
pos2 = rel_pos2,
|
||||
rotation = rotation,
|
||||
name = name,
|
||||
id = id
|
||||
})
|
||||
end
|
@ -47,44 +47,3 @@ minetest.register_node("pick_and_place:replacement", {
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
function pick_and_place.get_replacement_nodeid(ctx, metadata)
|
||||
local group = metadata.fields.group
|
||||
local selected_name
|
||||
if group and group ~= "" and ctx[group] then
|
||||
-- group placement
|
||||
selected_name = metadata.inventory.main[ctx[group]]
|
||||
else
|
||||
-- random placement
|
||||
local replacement_names = {}
|
||||
for _, name in ipairs(metadata.inventory.main) do
|
||||
if name ~= "" then
|
||||
table.insert(replacement_names, name)
|
||||
end
|
||||
end
|
||||
|
||||
if #replacement_names == 0 then
|
||||
-- no replacement
|
||||
return
|
||||
end
|
||||
|
||||
local i = math.random(#replacement_names)
|
||||
selected_name = replacement_names[i]
|
||||
|
||||
-- set group context
|
||||
if group and group ~= "" then
|
||||
ctx[group] = i
|
||||
end
|
||||
end
|
||||
|
||||
local stack = ItemStack(selected_name)
|
||||
local nodename = stack:get_name()
|
||||
|
||||
if not minetest.registered_nodes[nodename] then
|
||||
-- node not found
|
||||
return
|
||||
end
|
||||
|
||||
local nodeid = minetest.get_content_id(nodename)
|
||||
return nodeid
|
||||
end
|
54
replacement_wallmounted.lua
Normal file
54
replacement_wallmounted.lua
Normal file
@ -0,0 +1,54 @@
|
||||
|
||||
local function update_formspec(meta)
|
||||
local group = meta:get_string("group")
|
||||
|
||||
meta:set_string("formspec", [[
|
||||
size[10,8.3]
|
||||
real_coordinates[true]
|
||||
field[0.1,0.4;8.8,0.8;group;Group;]] .. group .. [[]
|
||||
button_exit[9,0.4;0.9,0.8;set;Set]
|
||||
list[context;main;0.1,1.4;8,1;]
|
||||
list[current_player;main;0.1,3;8,4;]
|
||||
listring[]
|
||||
]])
|
||||
|
||||
local txt = "Replacement node wallmounted"
|
||||
if group and group ~= "" then
|
||||
txt = txt .. " (group: '" .. group .. "')"
|
||||
end
|
||||
meta:set_string("infotext", txt)
|
||||
end
|
||||
|
||||
minetest.register_node("pick_and_place:replacement_wallmounted", {
|
||||
description = "Replacement node wallmounted",
|
||||
tiles = {"pick_and_place.png^[colorize:#ff0000"},
|
||||
drawtype = "signlike",
|
||||
use_texture_alpha = "blend",
|
||||
paramtype = "light",
|
||||
paramtype2 = "wallmounted",
|
||||
walkable = false,
|
||||
climbable = true,
|
||||
sunlight_propagates = true,
|
||||
selection_box = {
|
||||
type = "wallmounted"
|
||||
},
|
||||
groups = {
|
||||
oddly_breakable_by_hand = 3
|
||||
},
|
||||
|
||||
on_construct = function(pos)
|
||||
local meta = minetest.get_meta(pos)
|
||||
local inv = meta:get_inventory()
|
||||
inv:set_size("main", 8)
|
||||
|
||||
update_formspec(meta)
|
||||
end,
|
||||
|
||||
on_receive_fields = function(pos, _, fields)
|
||||
if fields.set then
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("group", fields.group)
|
||||
update_formspec(meta)
|
||||
end
|
||||
end
|
||||
})
|
@ -1,5 +1,8 @@
|
||||
local air_cid = minetest.get_content_id("air")
|
||||
local replacement_cid = minetest.get_content_id("pick_and_place:replacement")
|
||||
local replacement_cids = {
|
||||
[minetest.get_content_id("pick_and_place:replacement")] = true,
|
||||
[minetest.get_content_id("pick_and_place:replacement_wallmounted")] = true
|
||||
}
|
||||
|
||||
function pick_and_place.serialize(pos1, pos2)
|
||||
local manip = minetest.get_voxel_manip()
|
||||
@ -77,7 +80,7 @@ function pick_and_place.deserialize(pos1, schematic, disable_replacements)
|
||||
local nodeid = schematic.node_id_data[j]
|
||||
node_ids[nodeid] = true
|
||||
|
||||
if nodeid == replacement_cid and not disable_replacements then
|
||||
if replacement_cids[nodeid] and not disable_replacements then
|
||||
-- replacement placement
|
||||
local abs_pos = {x=x, y=y, z=z}
|
||||
local rel_pos = vector.subtract(abs_pos, pos1)
|
||||
|
BIN
textures/pick_and_place_composition.png
Normal file
BIN
textures/pick_and_place_composition.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
Loading…
x
Reference in New Issue
Block a user