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:
Buckaroo Banzai 2024-03-07 19:41:01 +01:00 committed by GitHub
parent 1b96953782
commit 6020a94ccf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 317 additions and 20 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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
})

View File

@ -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")

View File

@ -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
View 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
View 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
View 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

View File

@ -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

View File

@ -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()