2024-10-20 02:49:40 +02:00

397 lines
9.8 KiB
Lua

lzr_solutions = {}
local S = minetest.get_translator("lzr_solutions")
-- "idle", "recording" or "playing"
local state = "idle"
local current_replay_time = 0
local current_action
local current_solution
local current_solution_start_time
-- shortcuts
local w2l = lzr_world.world_pos_to_level_pos
local l2w = lzr_world.level_pos_to_world_pos
--[[
solution: {
actions = {
action,
action,
action,
...
},
}
action: {
type = "dig" / "place" / "rotate" / "find_treasure",
time = <number>,
origin = {
pos,
pitch,
yaw,
},
-- for action "dig":
pos = <pos of node to dig>,
-- for action "place":
pos = <pos of placed node>,
node = <node to place>,
itemstack = <itemstack to place>,
param2 = <param2 of created node>,
pointed_thing = <pointed_thing of placement (of type "node")>
-- for action "rotate":
pos = <pos of rotated node>,
node = <node after rotation>,
}
]]
local record_action = function(solution, action)
local new_action = table.copy(action)
local time = math.floor(minetest.get_us_time()/1000)
new_action.time = time - current_solution_start_time
table.insert(solution.actions, new_action)
end
local replay_action = function(player, action)
player:set_pos(action.origin.pos)
player:set_look_horizontal(action.origin.yaw)
player:set_look_vertical(action.origin.pitch)
if action.type == "dig" then
minetest.node_dig(action.pos, action.node, player)
lzr_laser.full_laser_update_if_needed()
elseif action.type == "place" then
local itemstack, position = minetest.item_place_node(action.itemstack, player, action.pointed_thing, action.param2, false)
if not position then
minetest.log("error", "[lzr_solutions] Tried to place item as node but failed")
return false
end
lzr_laser.full_laser_update_if_needed()
elseif action.type == "rotate" then
minetest.swap_node(action.pos, action.node)
lzr_laser.full_laser_update_if_needed()
elseif action.type == "find_treasure" then
local node = minetest.get_node(action.pos)
local def = minetest.registered_nodes[node.name]
if def.on_punch then
def.on_punch(action.pos, node, player)
else
minetest.log("error", "[lzr_solutions] Expected punchable open treasure chest at "..minetest.pos_to_string(pos).." while replaying solution but didn't find one")
return false
end
end
return true
end
lzr_solutions.record_solution = function()
state = "recording"
current_solution = {
actions = {},
}
current_solution_start_time = math.floor(minetest.get_us_time()/1000)
end
lzr_solutions.stop_recording_solution = function()
state = "idle"
if current_solution then
local solution = table.copy(current_solution)
current_solution = nil
current_solution_start_time = nil
return solution
else
return
end
end
lzr_solutions.replay_solution = function(solution)
current_replay_time = 0
current_action = 1
current_solution = table.copy(solution)
state = "playing"
end
lzr_solutions.solution_to_csv = function(solution)
local rows = {}
for a=1, #solution.actions do
local action = solution.actions[a]
local s_pos, s_node_name, s_node_param2 = "", "", ""
local s_itemstack = ""
local s_pointed_thing_under = ""
local s_pointed_thing_above = ""
if action.pos then
s_pos = minetest.pos_to_string(w2l(action.pos))
end
if action.node then
s_node_name = action.node.name
s_node_param2 = action.node.param2
end
if action.itemstack then
s_itemstack = action.itemstack:to_string()
end
if action.pointed_thing and action.pointed_thing.type == "node" then
s_pointed_thing_under = minetest.pos_to_string(w2l(action.pointed_thing.under))
s_pointed_thing_above = minetest.pos_to_string(w2l(action.pointed_thing.above))
end
local row = {
action.type,
action.time,
minetest.pos_to_string(w2l(action.origin.pos)),
action.origin.pitch,
action.origin.yaw,
-- action-specific fields:
-- pos
s_pos,
-- node (name and param2)
s_node_name,
s_node_param2,
-- itemstack
s_itemstack,
-- pointed_thing (type node)
s_pointed_thing_under,
s_pointed_thing_above,
}
table.insert(rows, row)
end
return lzr_csv.write_csv(rows)
end
lzr_solutions.csv_to_solution = function(csv)
local rows, csv_error = lzr_csv.parse_csv(csv)
if not rows then
return nil, csv_error
end
local solution = {
actions = {},
}
for r=1, #rows do
local row = rows[r]
local function row_exists(i)
return row[i] and row[i] ~= ""
end
local action = {}
action.type = row[1]
action.time = tonumber(row[2])
action.origin = {
pos = l2w(minetest.string_to_pos(row[3])),
pitch = tonumber(row[4]),
yaw = tonumber(row[5]),
}
if row_exists(6) then
action.pos = l2w(minetest.string_to_pos(row[6]))
end
if row_exists(7) and row_exists(8) then
action.node = {
name = row[7],
param2 = tonumber(row[8]),
}
end
if row_exists(9) then
action.itemstack = ItemStack(row[9])
end
if row_exists(10) and row_exists(11) then
action.pointed_thing = {
type = "node",
under = l2w(minetest.string_to_pos(row[10])),
above = l2w(minetest.string_to_pos(row[11])),
}
end
table.insert(solution.actions, action)
end
return solution
end
minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing)
if state ~= "recording" then
return
end
if not placer then
return
end
local action = {
type = "place",
origin = {
pos = placer:get_pos(),
yaw = placer:get_look_horizontal(),
pitch = placer:get_look_vertical(),
},
pos = table.copy(pos),
param2 = newnode.param2,
itemstack = ItemStack(itemstack),
pointed_thing = table.copy(pointed_thing),
}
record_action(current_solution, action)
end)
minetest.register_on_dignode(function(pos, oldnode, digger)
if state ~= "recording" then
return
end
if not digger then
return
end
local action = {
type = "dig",
origin = {
pos = digger:get_pos(),
yaw = digger:get_look_horizontal(),
pitch = digger:get_look_vertical(),
},
pos = pos,
node = oldnode,
}
record_action(current_solution, action)
end)
lzr_hook.register_after_rotate(function(pos, new_node, player)
if state ~= "recording" then
return
end
if not player then
return
end
local action = {
type = "rotate",
origin = {
pos = player:get_pos(),
yaw = player:get_look_horizontal(),
pitch = player:get_look_vertical(),
},
pos = pos,
node = new_node,
}
record_action(current_solution, action)
end)
lzr_treasure.register_after_found_treasure(function(pos, player)
if state ~= "recording" then
return
end
if not player then
return
end
local action = {
type = "find_treasure",
origin = {
pos = player:get_pos(),
yaw = player:get_look_horizontal(),
pitch = player:get_look_vertical(),
},
pos = pos,
}
record_action(current_solution, action)
end)
minetest.register_globalstep(function(dtime)
if state ~= "playing" then
return
end
current_replay_time = current_replay_time + dtime
if not current_solution then
minetest.log("error", "[lzr_solutions] In 'playing' state but current_solution is nil!")
state = "idle"
return
end
if current_action > #current_solution.actions then
-- Replay is finished, go back to idle state
current_solution = nil
state = "idle"
return
end
local action = current_solution.actions[current_action]
if current_replay_time >= action.time/1000 then
local player = minetest.get_player_by_name("singleplayer")
if not player then
return
end
local ok = replay_action(player, action)
if not ok then
-- Abort replay if replay_action returns false (in case of error)
current_solution = nil
state = "idle"
end
current_action = current_action + 1
end
end)
minetest.register_chatcommand("record_solution", {
privs = {},
params = "[ start | stop ]",
description = S("Start recording solution for current level"),
func = function(name, param)
local gstate = lzr_gamestate.get_state()
if gstate ~= lzr_gamestate.LEVEL then
return false, S("Not playing in a level!")
end
if param == "start" or param == "" then
if state == "playing" then
return false, S("Already replaying a solution!")
elseif state == "recording" then
return false, S("Already recording!")
end
lzr_solutions.record_solution()
return true, S("Recording started.")
elseif param == "stop" then
if state ~= "recording" then
return false, S("Not recording!")
end
local solution = lzr_solutions.stop_recording_solution()
local csv = lzr_solutions.solution_to_csv(solution)
-- Print solution CSV in console
-- TODO: Automatically save solution into file instead
print(csv)
return true, S("Recording stopped.")
else
return false
end
end,
})
minetest.register_chatcommand("replay_solution", {
privs = { debug = true },
params = "",
description = S("Replay saved solution for current level, if one exists"),
func = function(name, param)
local gstate = lzr_gamestate.get_state()
if gstate ~= lzr_gamestate.LEVEL then
return false, S("Not playing in a level!")
end
if state == "playing" then
return false, S("Already replaying a solution!")
elseif state == "recording" then
return false, S("Already recording!")
end
local level_data = lzr_levels.get_current_level_data()
local level_id = lzr_levels.get_current_level()
local level = level_data[level_id]
if not level_data.solutions_path or not level.filename_solution then
return false, S("No solution available.")
end
local full_path = level_data.solutions_path.."/"..level.filename_solution
local solution_file = io.open(full_path, "r")
if solution_file then
local csv = solution_file:read("*a")
local solution, csv_error = lzr_solutions.csv_to_solution(csv)
if solution then
lzr_solutions.replay_solution(solution)
return true, S("Replay started.")
else
return false, S("CSV error in solution: @1.", csv_error)
end
else
return false, S("No solution file available.")
end
end,
})