recording and playback support (#3)
* recording and playback support * wip * wip --------- Co-authored-by: BuckarooBanzay <BuckarooBanzay@users.noreply.github.com>
This commit is contained in:
parent
1b96953782
commit
6020a94ccf
@ -66,6 +66,7 @@ end
|
||||
-- sets handle nodes where possible
|
||||
function pick_and_place.configure(pos1, pos2, name)
|
||||
pos1, pos2 = pick_and_place.sort_pos(pos1, pos2)
|
||||
pick_and_place.register_template(name, pos1, pos2)
|
||||
|
||||
for _, cpos in ipairs(get_outer_corners(pos1, pos2)) do
|
||||
local node = minetest.get_node(cpos)
|
||||
|
@ -7,7 +7,8 @@ function pick_and_place.create_tool(pos1, pos2, name)
|
||||
|
||||
-- serialize schematic
|
||||
local schematic = pick_and_place.serialize(pos1, pos2)
|
||||
tool_meta:set_string("schematic", schematic)
|
||||
local encoded_schematic = pick_and_place.encode_schematic(schematic)
|
||||
tool_meta:set_string("schematic", encoded_schematic)
|
||||
|
||||
-- set name
|
||||
tool_meta:set_string("name", name)
|
||||
|
@ -1,9 +1,6 @@
|
||||
local function on_rightclick(pos, _, _, itemstack)
|
||||
if not itemstack:is_empty() then
|
||||
-- not an empty hand
|
||||
return
|
||||
end
|
||||
|
||||
-- returns the absolute positions and name for the handle
|
||||
-- TODO: a better name perhaps?
|
||||
function pick_and_place.get_template_data_from_handle(pos)
|
||||
local meta = minetest.get_meta(pos)
|
||||
|
||||
-- relative positions
|
||||
@ -20,6 +17,20 @@ local function on_rightclick(pos, _, _, itemstack)
|
||||
local pos1 = vector.add(pos, rel_pos1)
|
||||
local pos2 = vector.add(pos, rel_pos2)
|
||||
|
||||
return pos1, pos2, name
|
||||
end
|
||||
|
||||
local function on_rightclick(pos, _, _, itemstack)
|
||||
if not itemstack:is_empty() then
|
||||
-- not an empty hand
|
||||
return
|
||||
end
|
||||
|
||||
local pos1, pos2, name = pick_and_place.get_template_data_from_handle(pos)
|
||||
if not pos1 or not pos2 then
|
||||
return
|
||||
end
|
||||
|
||||
return pick_and_place.create_tool(pos1, pos2, name)
|
||||
end
|
||||
|
||||
@ -39,3 +50,14 @@ minetest.register_node("pick_and_place:handle", {
|
||||
not_in_creative_inventory = 1
|
||||
}
|
||||
})
|
||||
|
||||
minetest.register_lbm({
|
||||
label = "register pick-and-place handles",
|
||||
name = "pick_and_place:handle_register",
|
||||
nodenames = {"pick_and_place:handle"},
|
||||
run_at_every_load = true,
|
||||
action = function(pos)
|
||||
local pos1, pos2, name = pick_and_place.get_template_data_from_handle(pos)
|
||||
pick_and_place.register_template(name, pos1, pos2)
|
||||
end
|
||||
})
|
3
init.lua
3
init.lua
@ -23,6 +23,9 @@ dofile(MP .. "/pick_tool.lua")
|
||||
dofile(MP .. "/place_tool.lua")
|
||||
dofile(MP .. "/preview.lua")
|
||||
dofile(MP .. "/craft.lua")
|
||||
dofile(MP .. "/record.lua")
|
||||
dofile(MP .. "/playback.lua")
|
||||
dofile(MP .. "/registry.lua")
|
||||
|
||||
if minetest.get_modpath("mtt") and mtt.enabled then
|
||||
dofile(MP .. "/configure.spec.lua")
|
||||
|
@ -39,21 +39,31 @@ minetest.register_tool("pick_and_place:place", {
|
||||
local controls = player:get_player_control()
|
||||
|
||||
local meta = itemstack:get_meta()
|
||||
|
||||
local pos1, pos2 = get_pos(meta, player)
|
||||
|
||||
if controls.aux1 then
|
||||
-- removal
|
||||
pick_and_place.remove_area(pos1, pos2)
|
||||
pick_and_place.record_removal(pos1, pos2)
|
||||
notify_change(pos1, pos2)
|
||||
else
|
||||
-- placement
|
||||
local disable_replacements = controls.zoom
|
||||
local schematic = meta:get_string("schematic")
|
||||
local name = meta:get_string("name")
|
||||
local rotation = meta:get_int("rotation")
|
||||
local encoded_schematic = meta:get_string("schematic")
|
||||
local schematic, err = pick_and_place.decode_schematic(encoded_schematic)
|
||||
if err then
|
||||
minetest.chat_send_player(playername, "Decode error: " .. err)
|
||||
end
|
||||
|
||||
local success, msg = pick_and_place.deserialize(pos1, schematic, disable_replacements)
|
||||
if not success then
|
||||
minetest.chat_send_player(playername, "Placement error: " .. msg)
|
||||
else
|
||||
if name ~= "" then
|
||||
pick_and_place.record_placement(pos1, pos2, rotation, name)
|
||||
end
|
||||
notify_change(pos1, pos2)
|
||||
end
|
||||
end
|
||||
|
66
playback.lua
Normal file
66
playback.lua
Normal file
@ -0,0 +1,66 @@
|
||||
local playback_active = false
|
||||
|
||||
local function get_cache_key(name, rotation)
|
||||
return name .. "/" .. rotation
|
||||
end
|
||||
|
||||
local function playback(ctx)
|
||||
-- shift
|
||||
ctx.i = ctx.i + 1
|
||||
|
||||
-- pick next entry
|
||||
local entry = ctx.recording.entries[ctx.i]
|
||||
if not entry then
|
||||
minetest.chat_send_player(ctx.playername, "pnp 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)
|
||||
end
|
||||
|
||||
if entry.type == "place" then
|
||||
local tmpl = pick_and_place.get_template(entry.name)
|
||||
if tmpl then
|
||||
local key = get_cache_key(entry.name, entry.rotation)
|
||||
local schematic = ctx.cache[key]
|
||||
|
||||
if not schematic then
|
||||
-- cache schematic with rotation
|
||||
schematic = pick_and_place.serialize(tmpl.pos1, tmpl.pos2)
|
||||
pick_and_place.schematic_rotate(schematic, entry.rotation)
|
||||
ctx.cache[key] = schematic
|
||||
end
|
||||
|
||||
-- resolve absolute position
|
||||
local abs_pos1 = vector.add(ctx.origin, entry.pos1)
|
||||
pick_and_place.deserialize(abs_pos1, schematic)
|
||||
end
|
||||
elseif entry.type == "remove" then
|
||||
local abs_pos1 = vector.add(ctx.origin, entry.pos1)
|
||||
local abs_pos2 = vector.add(ctx.origin, entry.pos2)
|
||||
|
||||
pick_and_place.remove_area(abs_pos1, abs_pos2)
|
||||
end
|
||||
|
||||
-- re-schedule
|
||||
minetest.after(0, playback, ctx)
|
||||
end
|
||||
|
||||
function pick_and_place.start_playback(playername, origin, recording)
|
||||
if playback_active then
|
||||
return false, "playback already running"
|
||||
end
|
||||
|
||||
playback({
|
||||
playername = playername,
|
||||
origin = origin,
|
||||
recording = recording,
|
||||
i = 0,
|
||||
cache = {}
|
||||
})
|
||||
|
||||
return true, "playback started"
|
||||
end
|
189
record.lua
Normal file
189
record.lua
Normal file
@ -0,0 +1,189 @@
|
||||
|
||||
-- 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|stop|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)
|
||||
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
|
||||
})
|
||||
end
|
12
registry.lua
Normal file
12
registry.lua
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
-- registry of templates
|
||||
-- name => { pos1 = {}, pos2 = {} }
|
||||
local registry = {}
|
||||
|
||||
function pick_and_place.register_template(name, pos1, pos2)
|
||||
registry[name] = { pos1=pos1, pos2=pos2}
|
||||
end
|
||||
|
||||
function pick_and_place.get_template(name)
|
||||
return registry[name]
|
||||
end
|
@ -5,8 +5,8 @@ function pick_and_place.rotate_tool(itemstack, rotation)
|
||||
end
|
||||
|
||||
local meta = itemstack:get_meta()
|
||||
local schematic_data = meta:get_string("schematic")
|
||||
local schematic, err = pick_and_place.decode_schematic(schematic_data)
|
||||
local encoded_schematic = meta:get_string("schematic")
|
||||
local schematic, err = pick_and_place.decode_schematic(encoded_schematic)
|
||||
if err then
|
||||
return false, "Schematic decode error: " .. err
|
||||
end
|
||||
|
@ -43,23 +43,16 @@ function pick_and_place.serialize(pos1, pos2)
|
||||
metadata[minetest.pos_to_string(rel_pos)] = meta_table
|
||||
end
|
||||
|
||||
local schematic = {
|
||||
return {
|
||||
node_id_data = node_id_data,
|
||||
param2_data = param2_data,
|
||||
metadata = metadata,
|
||||
size = vector.add(vector.subtract(pos2, pos1), 1)
|
||||
}
|
||||
|
||||
return pick_and_place.encode_schematic(schematic)
|
||||
end
|
||||
|
||||
|
||||
function pick_and_place.deserialize(pos1, encoded_data, disable_replacements)
|
||||
local schematic, err = pick_and_place.decode_schematic(encoded_data)
|
||||
if err then
|
||||
return false, "Decode error: " .. err
|
||||
end
|
||||
|
||||
function pick_and_place.deserialize(pos1, schematic, disable_replacements)
|
||||
local pos2 = vector.add(pos1, vector.subtract(schematic.size, 1))
|
||||
|
||||
local manip = minetest.get_voxel_manip()
|
||||
|
Loading…
x
Reference in New Issue
Block a user