637 lines
18 KiB
Lua
637 lines
18 KiB
Lua
lzr_solutions = {}
|
|
|
|
local S = minetest.get_translator("lzr_solutions")
|
|
|
|
-- "idle", "recording" or "playing"
|
|
local state = "idle"
|
|
-- true if recording should automatically stop when reaching LEVEL_COMPLETE state
|
|
-- and trigger a file save
|
|
local autostop = false
|
|
|
|
-- true if running a full solution test of the core levels
|
|
local full_test = false
|
|
-- level number of currently tested core level
|
|
local full_test_level = 0
|
|
|
|
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 test_next_core_solution_callback = function()
|
|
local level_id = full_test_level
|
|
minetest.log("action", "[lzr_solutions] Testing solution for core level "..level_id)
|
|
|
|
local level_data = lzr_levels.get_core_level_data()
|
|
if not level_data.solutions_path then
|
|
-- No solutions path. Nothing to test!
|
|
minetest.log("error", "[lzr_solutions] No solutions path")
|
|
return false
|
|
end
|
|
|
|
local level = level_data[level_id]
|
|
if not level then
|
|
-- Level does not exist
|
|
minetest.log("error", "[lzr_solutions] Core level "..tostring(level_id).." does not exist")
|
|
return false
|
|
end
|
|
|
|
if not level.filename_solution then
|
|
-- No solution in level. Skip test.
|
|
minetest.log("error", "[lzr_solutions] Core level "..tostring(level_id).." doesn't have solution")
|
|
return false
|
|
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 = lzr_solutions.csv_to_solution(csv)
|
|
if solution then
|
|
lzr_solutions.replay_solution(solution)
|
|
minetest.log("action", "[lzr_solutions] Playing solution for core level "..level_id)
|
|
return true
|
|
else
|
|
minetest.log("error", "[lzr_solutions] Error in solution CSV file for core level "..level_id)
|
|
return false
|
|
end
|
|
else
|
|
minetest.log("error", "[lzr_solutions] Error while loading solution CSV file for core level "..level_id)
|
|
return false
|
|
end
|
|
end
|
|
|
|
local test_next_core_solution = function()
|
|
full_test_level = full_test_level + 1
|
|
if full_test_level > lzr_levels.LAST_LEVEL then
|
|
return false
|
|
end
|
|
minetest.log("info", "[lzr_solutions] Loading core level "..full_test_level.." ...")
|
|
lzr_gamestate.set_state(lzr_gamestate.LEVEL_TEST)
|
|
lzr_levels.start_level(full_test_level)
|
|
return true
|
|
end
|
|
lzr_levels.register_on_level_start(function()
|
|
minetest.log("verbose", "[lzr_solutions] on_level_start event")
|
|
if full_test then
|
|
test_next_core_solution_callback()
|
|
end
|
|
end)
|
|
|
|
lzr_solutions.test_core_solutions = function()
|
|
full_test = true
|
|
full_test_level = 0
|
|
local ok = test_next_core_solution()
|
|
if not ok then
|
|
full_test = false
|
|
end
|
|
end
|
|
|
|
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
|
|
-- Check if node is the expected node
|
|
local node = minetest.get_node(action.pos)
|
|
if node.name ~= action.node.name or node.param2 ~= action.node.param2 then
|
|
minetest.log("error", "[lzr_solutions] Expected to dig node '"..action.node.name.."' (param2="..action.node.param2..") but found '"..node.name.."' (param2="..node.param2..")")
|
|
return false
|
|
end
|
|
minetest.node_dig(action.pos, action.node, player)
|
|
local def = minetest.registered_nodes[action.node.name]
|
|
if def and def.sounds and def.sounds.dug then
|
|
minetest.sound_play(def.sounds.dug, {pos=action.pos}, true)
|
|
end
|
|
lzr_laser.full_laser_update_if_needed()
|
|
elseif action.type == "place" then
|
|
-- Check if action itemstack exists in player inventory
|
|
local inv = player:get_inventory()
|
|
local idx = nil
|
|
local astack = ItemStack(action.itemstack)
|
|
-- Set count of both itemstacks to 1 and clear metadata
|
|
astack:set_count(1)
|
|
astack:get_meta():from_table(nil) -- this clears metadata
|
|
for i=1, inv:get_size("main") do
|
|
local checkstack = inv:get_stack("main", i)
|
|
checkstack:set_count(1)
|
|
checkstack:get_meta():from_table(nil)
|
|
if astack:equals(checkstack) then
|
|
idx = i
|
|
break
|
|
end
|
|
end
|
|
if not idx then
|
|
minetest.log("error", "[lzr_solutions] Tried to place an item not found in the player inventory: "..action.itemstack:to_string())
|
|
return false
|
|
end
|
|
|
|
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
|
|
-- Update player inventory
|
|
inv:set_stack("main", idx, itemstack)
|
|
|
|
local def = minetest.registered_nodes[action.itemstack:get_name()]
|
|
if def and def.sounds and def.sounds.place then
|
|
minetest.sound_play(def.sounds.place, {pos=action.pointed_thing.under}, true)
|
|
end
|
|
lzr_laser.full_laser_update_if_needed()
|
|
elseif action.type == "rotate" then
|
|
local node = minetest.get_node(action.pos)
|
|
if node.name ~= action.node.name then
|
|
minetest.log("error", "[lzr_solutions] Tried to rotate node '"..action.node.name.."' but found '"..node.name.."'")
|
|
return false
|
|
end
|
|
minetest.swap_node(action.pos, action.node)
|
|
local def = minetest.registered_nodes[action.node.name]
|
|
if def and def.sounds and def.sounds._rotate then
|
|
minetest.sound_play(def.sounds._rotate, {pos=action.pos}, true)
|
|
end
|
|
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(activate_autostop)
|
|
if activate_autostop then
|
|
autostop = true
|
|
else
|
|
autostop = false
|
|
end
|
|
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"
|
|
autostop = false
|
|
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), 5)
|
|
s_pointed_thing_above = minetest.pos_to_string(w2l(action.pointed_thing.above), 5)
|
|
end
|
|
|
|
local row = {
|
|
action.type,
|
|
action.time,
|
|
minetest.pos_to_string(w2l(action.origin.pos), 5),
|
|
string.format("%.5g", action.origin.pitch),
|
|
string.format("%.5g", 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"
|
|
full_test = false
|
|
return
|
|
end
|
|
if current_action > #current_solution.actions then
|
|
-- Replay is finished
|
|
if full_test then
|
|
if not lzr_gamestate.is_loading() then
|
|
if lzr_laser.check_level_won() then
|
|
minetest.log("action", "[lzr_solutions] Solution for core level "..full_test_level.." completed!")
|
|
local ok = test_next_core_solution()
|
|
if not ok then
|
|
minetest.log("action", "[lzr_solutions] Core level solution test successfully completed!")
|
|
minetest.chat_send_all(S("Core level solution test successfully completed!"))
|
|
full_test = false
|
|
current_solution = nil
|
|
state = "idle"
|
|
lzr_levels.go_to_menu()
|
|
end
|
|
else
|
|
minetest.log("error", "[lzr_solutions] Replay for level solution is done but level wasn't solved")
|
|
full_test = false
|
|
current_solution = nil
|
|
state = "idle"
|
|
end
|
|
end
|
|
else
|
|
-- Go back to idle state
|
|
current_solution = nil
|
|
state = "idle"
|
|
end
|
|
return
|
|
end
|
|
local action = current_solution.actions[current_action]
|
|
if current_replay_time >= action.time*(0.001*lzr_globals.LEVEL_TEST_TIME_MULTIPLIER) 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"
|
|
full_test = false
|
|
end
|
|
current_action = current_action + 1
|
|
end
|
|
end)
|
|
|
|
lzr_solutions.save_solution_for_current_level = function(solution)
|
|
local csv = lzr_solutions.solution_to_csv(solution)
|
|
local world_solutions_path = minetest.get_worldpath().."/solutions"
|
|
local ok = minetest.mkdir(world_solutions_path)
|
|
if not ok then
|
|
return false, S("Could not create and/or access world solutions path.")
|
|
end
|
|
|
|
local level_data = lzr_levels.get_current_level_data()
|
|
local level_id = lzr_levels.get_current_level()
|
|
if not level_data or not level_id then
|
|
minetest.log("error", "[lzr_solutions] save_solution_for_current_level: Missing level_data or level ID!")
|
|
return false
|
|
end
|
|
local level = level_data[level_id]
|
|
local filename_solution = string.sub(level.filename, 1, -5) .. ".sol.csv"
|
|
|
|
local full_path = world_solutions_path.."/"..filename_solution
|
|
local solution_file = io.open(full_path, "w")
|
|
if solution_file then
|
|
solution_file:write(csv)
|
|
solution_file:close()
|
|
return true, S("Solution file written to: @1", full_path)
|
|
else
|
|
return false, S("Could not write solution file.")
|
|
end
|
|
end
|
|
|
|
-- Automatically stop recording when entering the LEVEL_COMPLETE state
|
|
-- and save solution to file.
|
|
lzr_gamestate.register_on_enter_state(function(new_state)
|
|
if state == "recording" and autostop then
|
|
if new_state == lzr_gamestate.LEVEL_COMPLETE then
|
|
local solution = lzr_solutions.stop_recording_solution()
|
|
local msg = S("Recording finished.")
|
|
local ok, save_msg = lzr_solutions.save_solution_for_current_level(solution)
|
|
msg = msg .. "\n"..save_msg
|
|
minetest.chat_send_player("singleplayer", msg)
|
|
elseif new_state ~= lzr_gamestate.LEVEL then
|
|
lzr_solutions.stop_recording_solution()
|
|
minetest.chat_send_player("singleplayer", S("Recording cancelled."))
|
|
end
|
|
elseif state == "playing" then
|
|
if new_state ~= lzr_gamestate.LEVEL_TEST then
|
|
full_test = false
|
|
state = "idle"
|
|
current_solution = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Show chat commands only in debug mode
|
|
if minetest.settings:get_bool("lzr_debug", false) == true then
|
|
|
|
minetest.register_chatcommand("replay_solution", {
|
|
privs = { debug = true, server = 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_gamestate.set_state(lzr_gamestate.LEVEL_TEST)
|
|
local player = minetest.get_player_by_name(name)
|
|
lzr_gui.set_play_gui(player, true)
|
|
lzr_player.set_play_inventory(player, nil, true)
|
|
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,
|
|
})
|
|
|
|
minetest.register_chatcommand("test_core_solutions", {
|
|
privs = { debug = true, server = true },
|
|
params = "",
|
|
description = S("Test the solution of all core levels"),
|
|
func = function(name, param)
|
|
if state == "playing" then
|
|
return false, S("Already replaying a solution!")
|
|
elseif state == "recording" then
|
|
return false, S("Already recording!")
|
|
end
|
|
lzr_solutions.test_core_solutions()
|
|
return true
|
|
end,
|
|
})
|
|
|
|
minetest.register_chatcommand("record_solution", {
|
|
privs = {},
|
|
params = "[ start | stop | cancel ]",
|
|
description = S("Start or stop recording solution for current level, writing to a solution file when stopping"),
|
|
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
|
|
|
|
-- Starts recording
|
|
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(true)
|
|
return true, S("Recording started.")
|
|
-- Stops recording and finalizes it by writing to a file
|
|
elseif param == "stop" then
|
|
if state ~= "recording" then
|
|
return false, S("Not recording!")
|
|
end
|
|
local solution = lzr_solutions.stop_recording_solution()
|
|
local ok, msg = lzr_solutions.save_solution_for_current_level(solution)
|
|
if ok then
|
|
return true, S("Recording stopped.").."\n"..msg
|
|
else
|
|
return false, S("Recording stopped.").."\n"..msg
|
|
end
|
|
-- Cancels recording without writing to file
|
|
elseif param == "cancel" then
|
|
if state ~= "recording" then
|
|
return false, S("Not recording!")
|
|
end
|
|
lzr_solutions.stop_recording_solution()
|
|
return true, S("Recording cancelled.")
|
|
else
|
|
return false
|
|
end
|
|
end,
|
|
})
|
|
|
|
end
|