397 lines
9.8 KiB
Lua
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,
|
|
})
|