From 6020a94ccf684326db0600bbc8e5fecf12869429 Mon Sep 17 00:00:00 2001 From: Buckaroo Banzai <39065740+BuckarooBanzay@users.noreply.github.com> Date: Thu, 7 Mar 2024 19:41:01 +0100 Subject: [PATCH] recording and playback support (#3) * recording and playback support * wip * wip --------- Co-authored-by: BuckarooBanzay --- configure.lua | 1 + create_tool.lua | 3 +- handle_node.lua | 34 +++++++-- init.lua | 3 + place_tool.lua | 14 +++- playback.lua | 66 +++++++++++++++++ record.lua | 189 ++++++++++++++++++++++++++++++++++++++++++++++++ registry.lua | 12 +++ rotate_tool.lua | 4 +- serialize.lua | 11 +-- 10 files changed, 317 insertions(+), 20 deletions(-) create mode 100644 playback.lua create mode 100644 record.lua create mode 100644 registry.lua diff --git a/configure.lua b/configure.lua index 8264aa7..f02459b 100644 --- a/configure.lua +++ b/configure.lua @@ -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) diff --git a/create_tool.lua b/create_tool.lua index ca3cc1b..439d91c 100644 --- a/create_tool.lua +++ b/create_tool.lua @@ -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) diff --git a/handle_node.lua b/handle_node.lua index 336546a..d50e9a1 100644 --- a/handle_node.lua +++ b/handle_node.lua @@ -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 +}) \ No newline at end of file diff --git a/init.lua b/init.lua index bb9356c..06b0d5b 100644 --- a/init.lua +++ b/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") diff --git a/place_tool.lua b/place_tool.lua index d5149d6..295b08f 100644 --- a/place_tool.lua +++ b/place_tool.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 diff --git a/playback.lua b/playback.lua new file mode 100644 index 0000000..f49e61a --- /dev/null +++ b/playback.lua @@ -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 diff --git a/record.lua b/record.lua new file mode 100644 index 0000000..9579d98 --- /dev/null +++ b/record.lua @@ -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 .. "" + 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 \ No newline at end of file diff --git a/registry.lua b/registry.lua new file mode 100644 index 0000000..52a679b --- /dev/null +++ b/registry.lua @@ -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 \ No newline at end of file diff --git a/rotate_tool.lua b/rotate_tool.lua index 2b551cc..c8dce41 100644 --- a/rotate_tool.lua +++ b/rotate_tool.lua @@ -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 diff --git a/serialize.lua b/serialize.lua index fb9c682..6f27dd6 100644 --- a/serialize.lua +++ b/serialize.lua @@ -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()