towercrane/init.lua
2018-07-21 19:30:43 +02:00

765 lines
24 KiB
Lua

--[[
Tower Crane Mod
===============
v1.0 by JoSt
Copyright (C) 2017-2018 Joachim Stolberg
LGPLv2.1+
See LICENSE.txt for more information
History:
2017-06-04 v0.01 first version
2017-06-06 v0.02 Hook bugfix
2017-06-07 v0.03 fixed 2 bugs, added config.lua and sound
2017-06-08 v0.04 recipe and rope length now configurable
2017-06-10 v0.05 resizing bugfix, area protection added
2017-07-11 v0.06 fixed the space check bug, settingtypes added
2017-07-16 v0.07 crane remove bug fix
2017-07-16 v0.08 player times out bugfix
2017-08-19 v0.09 crane protection area to prevent crane clusters
2017-08-27 v0.10 hook instance and sound switch off bug fixes
2017-09-09 v0.11 further player bugfixes
2017-09-24 v0.12 Switched from entity hook model to real fly privs
2017-10-17 v0.13 Area protection bugfix
2017-11-01 v0.14 Crane handing over bugfix
2017-11-07 v0.15 Working zone is now restricted to areas with necessary rights
2018-02-27 v0.16 "fly privs" bug fixed (issue #2)
2018-04-12 v0.17 "area owner changed" bug fixed (issue #3)
2018-05-28 v1.0 Mod released
]]--
-- crane minimum size
MIN_SIZE = 8
towercrane = {}
dofile(minetest.get_modpath("towercrane") .. "/config.lua")
local function chat(owner, text)
if owner ~= nil then
minetest.chat_send_player(owner, "[Tower Crane] "..text)
end
end
-- To prevent race condition crashes
local Currently_left_the_game = nil
--##################################################################################################
--## Construction Area
--##################################################################################################
-- Areas = {
-- pos_key = {owner="...", pos1=pos, pos2=pos},
-- }
local storage = minetest.get_mod_storage()
local Areas = minetest.deserialize(storage:get_string("Areas")) or {}
local function update_mod_storage()
storage:set_string("Areas", minetest.serialize(Areas))
end
minetest.register_on_shutdown(function()
update_mod_storage()
end)
----------------------------------------------------------------------------------------------------
-- The same player can't place a crane within the same protection area
----------------------------------------------------------------------------------------------------
local function no_area_violation(owner, pos)
local res = true
local px, py, pz = pos.x, pos.y, pos.z
for key, area in pairs(Areas) do
if owner == area.owner then
local pos1, pos2 = area.pos1, area.pos2
if (px >= pos1.x and px <= pos2.x) and (py >= pos1.y and py <= pos2.y) and
(pz >= pos1.z and pz <= pos2.z) then
res = false
break
end
end
end
return res
end
local function store_crane_data(owner, pos, pos1, pos2)
-- normalize x/z so that pos2 > pos1
if pos2.x < pos1.x then
pos2.x, pos1.x = pos1.x, pos2.x
end
if pos2.z < pos1.z then
pos2.z, pos1.z = pos1.z, pos2.z
end
-- store data
local key = minetest.pos_to_string(pos)
Areas[key] = {owner=owner, pos1=pos1, pos2=pos2}
update_mod_storage()
end
local function remove_crane_data(pos)
local key = minetest.pos_to_string(pos)
Areas[key] = nil
update_mod_storage()
end
--##################################################################################################
--## Tower Crane Hook (player)
--##################################################################################################
-- give/take player the necessary privs/physics
local function fly_privs(pos, player, enable)
local privs = minetest.get_player_privs(player:get_player_name())
local physics = player:get_physics_override()
if privs and physics then
if enable == true then
local spos = minetest.pos_to_string(pos)
player:set_attribute("tower_crane_active", spos)
privs["fly"] = true
privs["fast"] = nil
physics.speed = 0.7
else
privs["fast"] = minetest.deserialize(player:get_attribute("tower_crane_store_fast"))
privs["fly"] = minetest.deserialize(player:get_attribute("tower_crane_store_fly"))
physics.speed = minetest.deserialize(player:get_attribute("tower_crane_store_speed"))
player:set_attribute("tower_crane_active", nil)
end
player:set_physics_override(physics)
minetest.set_player_privs(player:get_player_name(), privs)
end
end
-- Normalize the player privs
local function remove_hook(pos, player)
if player then
if pos then
local meta = minetest.get_meta(pos)
meta:set_int("running", 0)
end
fly_privs(pos, player, nil)
end
end
local function control_player(pos, pos1, pos2, player_name)
if player_name == Currently_left_the_game then
Currently_left_the_game = nil
return
end
local player = minetest.get_player_by_name(player_name)
if player then
local meta = minetest.get_meta(pos)
local running = meta:get_int("running")
if running == 1 then
-- check if outside of the construction area
local correction = false
local pl_pos = player:getpos()
if pl_pos then
if pl_pos.x < pos1.x then pl_pos.x = pos1.x; correction = true end
if pl_pos.x > pos2.x then pl_pos.x = pos2.x; correction = true end
if pl_pos.y < pos1.y then pl_pos.y = pos1.y; correction = true end
if pl_pos.y > pos2.y then pl_pos.y = pos2.y; correction = true end
if pl_pos.z < pos1.z then pl_pos.z = pos1.z; correction = true end
if pl_pos.z > pos2.z then pl_pos.z = pos2.z; correction = true end
-- check if a protected area is violated
if correction == false and minetest.is_protected(pl_pos, player_name) then
chat(player_name, "Area is protected.")
correction = true
end
if correction == true then
local last_pos = minetest.string_to_pos(meta:get_string("last_known_pos"))
if last_pos then
player:set_pos(last_pos)
end
else -- store last known correct position
meta:set_string("last_known_pos", minetest.pos_to_string(pl_pos))
end
minetest.after(1, control_player, pos, pos1, pos2, player_name)
else
remove_hook(pos, player)
end
else
remove_hook(pos, player)
end
else
local meta = minetest.get_meta(pos)
meta:set_int("running", 0)
end
end
-- Place the player in front of the base and give fly privs
local function place_hook(pos, dir, player, pos1, pos2)
if player then
local switch_pos = {x=pos.x, y=pos.y, z=pos.z}
local meta = minetest.get_meta(switch_pos)
meta:set_int("running", 1)
-- set privs
fly_privs(pos, player, true)
-- place the player
pos.y = pos.y - 1
pos.x = pos.x + dir.x
pos.z = pos.z + dir.z
player:set_pos(pos)
meta:set_string("last_known_pos", minetest.pos_to_string(pos))
-- control player every second
minetest.after(1, control_player, switch_pos, pos1, pos2, player:get_player_name())
end
end
--##################################################################################################
--## Tower Crane
--##################################################################################################
local function turnright(dir)
local facedir = minetest.dir_to_facedir(dir)
return minetest.facedir_to_dir((facedir + 1) % 4)
end
local function turnleft(dir)
local facedir = minetest.dir_to_facedir(dir)
return minetest.facedir_to_dir((facedir + 3) % 4)
end
----------------------------------------------------------------------------------------------------
-- generic function for contruction and removement
----------------------------------------------------------------------------------------------------
local function crane_body_plan(pos, dir, height, width, clbk, tArg)
pos.y = pos.y + 1
clbk(pos, "towercrane:mast_ctrl_off", tArg)
for i = 1,height+1 do
pos.y = pos.y + 1
clbk(pos, "towercrane:mast", tArg)
end
pos.y = pos.y - 2
pos.x = pos.x - dir.x
pos.z = pos.z - dir.z
clbk(pos, "towercrane:arm2", tArg)
pos.x = pos.x - dir.x
pos.z = pos.z - dir.z
clbk(pos, "towercrane:arm", tArg)
pos.x = pos.x - dir.x
pos.z = pos.z - dir.z
clbk(pos, "towercrane:balance", tArg)
pos.x = pos.x + 3 * dir.x
pos.z = pos.z + 3 * dir.z
for i = 1,width do
pos.x = pos.x + dir.x
pos.z = pos.z + dir.z
if i % 2 == 0 then
clbk(pos, "towercrane:arm2", tArg)
else
clbk(pos, "towercrane:arm", tArg)
end
end
end
----------------------------------------------------------------------------------------------------
-- Check space are protection for mast and arm
----------------------------------------------------------------------------------------------------
local function check_space(pos, dir, height, width, owner)
local remove = function(pos, node_name, tArg)
if minetest.get_node(pos).name ~= "air" then
tArg.res = false
elseif minetest.is_protected(pos, tArg.owner) then
tArg.res = false
end
end
local tArg = {res = true, owner = owner}
crane_body_plan(pos, dir, height, width, remove, tArg)
return tArg.res
end
----------------------------------------------------------------------------------------------------
-- Constuct mast and arm
----------------------------------------------------------------------------------------------------
local function construct_crane(pos, dir, height, width, owner)
local add = function(pos, node_name, tArg)
minetest.add_node(pos, {name=node_name, param2=minetest.dir_to_facedir(tArg.dir)})
end
local tArg = {dir = dir}
crane_body_plan(table.copy(pos), dir, height, width, add, tArg)
pos.y = pos.y + 1
local meta = minetest.get_meta(pos)
meta:set_string("dir", minetest.pos_to_string(dir))
meta:set_string("owner", owner)
meta:set_int("height", height or 0)
meta:set_int("width", width or 0)
end
----------------------------------------------------------------------------------------------------
-- Remove the crane
----------------------------------------------------------------------------------------------------
local function remove_crane(pos, dir, height, width)
local remove = function(pos, node_name, tArg)
if minetest.get_node(pos).name == node_name or
minetest.get_node(pos).name == "towercrane:mast_ctrl_on" then
minetest.remove_node(pos)
end
end
crane_body_plan(table.copy(pos), dir, height, width, remove, {})
end
----------------------------------------------------------------------------------------------------
-- Calculate and set the protection area (pos1, pos2)
----------------------------------------------------------------------------------------------------
local function protect_area(pos, dir, height, width, owner)
if not areas then return 0 end
-- pos1 = close/right/below
dir = turnright(dir)
dir = turnright(dir)
local pos1 = vector.add(pos, vector.multiply(dir, 2))
dir = turnleft(dir)
pos1 = vector.add(pos1, vector.multiply(dir, width/2))
dir = turnleft(dir)
pos1.y = pos.y - 2
-- pos2 = far/left/above
local pos2 = vector.add(pos1, vector.multiply(dir, width+2))
dir = turnleft(dir)
pos2 = vector.add(pos2, vector.multiply(dir, width))
pos2.y = pos.y + 2 + height
store_crane_data(owner, pos, pos1, pos2)
-- add area
local canAdd, errMsg = areas:canPlayerAddArea(pos1, pos2, owner)
if canAdd then
local id = areas:add(owner, "Construction site", pos1, pos2, nil)
areas:save()
return id
end
return nil
end
----------------------------------------------------------------------------------------------------
-- Remove the protection area
----------------------------------------------------------------------------------------------------
local function remove_area(id, owner)
if not areas then return end
if areas:isAreaOwner(id, owner) then
areas:remove(id)
areas:save()
end
end
----------------------------------------------------------------------------------------------------
-- Check user input (height, width)
----------------------------------------------------------------------------------------------------
local function check_input(fields)
local size = string.split(fields.size, ",")
if #size == 2 then
local height = tonumber(size[1])
local width = tonumber(size[2])
if height ~= nil and width ~= nil then
height = math.max(height, MIN_SIZE)
height = math.min(height, towercrane.max_height)
width = math.max(width, MIN_SIZE)
width = math.min(width, towercrane.max_width)
return height, width
end
end
return 0, 0
end
----------------------------------------------------------------------------------------------------
-- Register Crane base
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:base", {
description = "Tower Crane Base",
inventory_image = "towercrane_invent.png",
tiles = {
"towercrane_base_top.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
},
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
sounds = default.node_sound_stone_defaults(),
is_ground_content = false,
groups = {cracky=2},
-- set meta data (form for crane height and width, dir of the arm)
after_place_node = function(pos, placer)
local meta = minetest.get_meta(pos)
local owner = placer:get_player_name()
meta:set_string("owner", owner)
local formspec = "size[5,4]"..
"label[0,0;Construction area size]" ..
"field[1,1.5;3,1;size;height,width;]" ..
"button_exit[1,2;2,1;exit;Save]"
meta:set_string("formspec", formspec)
local fdir = minetest.dir_to_facedir(placer:get_look_dir(), false)
local dir = minetest.facedir_to_dir(fdir)
meta:set_string("dir", minetest.pos_to_string(dir))
end,
-- evaluate user input (height, width), destroyed old crane and build a new one with
-- the given size
on_receive_fields = function(pos, formname, fields, player)
local switch_pos = {x=pos.x, y=pos.y+1, z=pos.z}
if fields.size == nil then
return
end
local meta = minetest.get_meta(switch_pos)
local running = meta:get_int("running")
if running == 1 then
return
end
meta = minetest.get_meta(pos)
local owner = meta:get_string("owner")
local dir = minetest.string_to_pos(meta:get_string("dir"))
local height = meta:get_int("height")
local width = meta:get_int("width")
local id = meta:get_int("id")
if not player or not player:is_player() then
return
end
if player:get_player_name() ~= owner then
return
end
-- destroy area and crane
if dir ~= nil and height ~= nil and width ~= nil then
remove_crane_data(pos)
remove_area(id, owner)
remove_crane(table.copy(pos), dir, height, width)
end
-- evaluate user input
height, width = check_input(fields)
if height ~= 0 then
meta:set_int("height", height)
meta:set_int("width", width)
meta:set_string("infotext", "Owner: ".. owner ..", Crane size: " .. height .. "," .. width)
if no_area_violation(owner, pos) then
if dir ~= nil then
if check_space(table.copy(pos), dir, height, width, owner) then
-- add protection area
meta:set_int("id", protect_area(table.copy(pos), table.copy(dir), height, width, owner) or 0)
construct_crane(table.copy(pos), table.copy(dir), height, width, owner)
else
chat(owner, "area is protected or too less space to raise up the crane!")
end
end
else
chat(owner, "Too less distance to your other crane(s)!")
end
else
chat(owner, "Invalid input!")
end
end,
can_dig = function(pos, player)
local switch_pos = {x=pos.x, y=pos.y+1, z=pos.z}
local meta = minetest.get_meta(pos)
local owner = meta:get_string("owner")
if player:get_player_name() ~= owner and
not minetest.check_player_privs(player:get_player_name(), "creative") then
return false
end
meta = minetest.get_meta(switch_pos)
local running = meta:get_int("running")
if running == 1 then
return false
end
return true
end,
-- remove mast and arm if base gets destroyed
on_destruct = function(pos)
local meta = minetest.get_meta(pos)
local dir = minetest.string_to_pos(meta:get_string("dir"))
local height = meta:get_int("height")
local width = meta:get_int("width")
local id = meta:get_int("id")
local owner = meta:get_string("owner")
-- remove protection area
if id ~= nil then
remove_area(id, owner)
end
-- remove crane
if dir ~= nil and height ~= nil and width ~= nil then
remove_crane_data(pos)
remove_crane(pos, dir, height, width)
end
-- remove hook
local player = minetest.get_player_by_name(owner)
end,
})
----------------------------------------------------------------------------------------------------
-- Register Crane balance
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:balance", {
description = "Tower Crane Balance",
tiles = {
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
"towercrane_base.png",
},
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Crane mast
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast", {
description = "Tower Crane Mast",
drawtype = "glasslike_framed",
tiles = {
"towercrane_mast.png",
"towercrane_mast.png",
"towercrane_mast.png",
"towercrane_mast.png",
"towercrane_mast.png",
"towercrane_mast.png",
},
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Crane Switch (on)
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast_ctrl_on", {
description = "Tower Crane Mast Ctrl On",
drawtype = "node",
tiles = {
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl_on.png",
"towercrane_mast_ctrl_on.png",
},
-- switch the crane OFF
on_rightclick = function (pos, node, clicker)
local meta = minetest.get_meta(pos)
if not clicker or not clicker:is_player() then
return
end
if clicker:get_player_name() ~= meta:get_string("owner") then
return
end
-- Check if this is the wrong crane
local spos = minetest.pos_to_string(pos)
if clicker:get_attribute("tower_crane_active") ~= nil
and clicker:get_attribute("tower_crane_active") ~= spos then
return
end
node.name = "towercrane:mast_ctrl_off"
minetest.swap_node(pos, node)
remove_hook(pos, clicker)
end,
on_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Switch crane on/off")
end,
after_place_node = function(pos, placer, itemstack, pointed_thing)
local meta = minetest.get_meta(pos)
local owner = placer:get_player_name()
meta:set_string("owner", owner)
end,
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Crane Switch (off)
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:mast_ctrl_off", {
description = "Tower Crane Mast Ctrl Off",
drawtype = "node",
tiles = {
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl.png",
"towercrane_mast_ctrl_off.png",
"towercrane_mast_ctrl_off.png",
},
-- switch the crane ON
on_rightclick = function (pos, node, clicker)
-- calculate the construction area, and place the hook
local meta = minetest.get_meta(pos)
-- only the owner is allowed to switch
if not clicker or not clicker:is_player() then
return
end
if clicker:get_player_name() ~= meta:get_string("owner") then
return
end
-- prevent handing over to the next crane
if clicker:get_attribute("tower_crane_active") ~= nil then
return
end
-- swap to the other node
node.name = "towercrane:mast_ctrl_on"
minetest.swap_node(pos, node)
local dir = minetest.string_to_pos(meta:get_string("dir"))
if pos ~= nil and dir ~= nil then
--
-- calculate the construction area dimension (pos1, pos2)
--
local height = meta:get_int("height")
local width = meta:get_int("width")
-- pos1 = close/right/below
dir = turnright(dir)
local pos1 = vector.add(pos, vector.multiply(dir, width/2))
dir = turnleft(dir)
pos1 = vector.add(pos1, vector.multiply(dir, 1))
pos1.y = pos.y - 2 + height - towercrane.rope_length
-- pos2 = far/left/above
local pos2 = vector.add(pos1, vector.multiply(dir, width-1))
dir = turnleft(dir)
pos2 = vector.add(pos2, vector.multiply(dir, width))
pos2.y = pos.y - 3 + height
-- normalize x/z so that pos2 > pos1
if pos2.x < pos1.x then
pos2.x, pos1.x = pos1.x, pos2.x
end
if pos2.z < pos1.z then
pos2.z, pos1.z = pos1.z, pos2.z
end
dir = minetest.string_to_pos(meta:get_string("dir"))
place_hook(pos, dir, clicker, pos1, pos2)
end
end,
on_construct = function(pos)
-- add infotext
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Switch crane on/off")
end,
after_place_node = function(pos, placer, itemstack, pointed_thing)
-- store owner for dig protection
local meta = minetest.get_meta(pos)
local owner = placer:get_player_name()
meta:set_string("owner", owner)
end,
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Crane arm 1
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:arm", {
description = "Tower Crane Arm",
drawtype = "glasslike_framed",
tiles = {
"towercrane_arm.png",
"towercrane_arm.png",
"towercrane_arm.png",
"towercrane_arm.png",
"towercrane_arm.png",
"towercrane_arm.png",
},
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Crane arm 2
----------------------------------------------------------------------------------------------------
minetest.register_node("towercrane:arm2", {
description = "Tower Crane Arm2",
drawtype = "glasslike_framed",
tiles = {
"towercrane_arm2.png",
"towercrane_arm2.png",
"towercrane_arm2.png",
"towercrane_arm2.png",
"towercrane_arm2.png",
"towercrane_arm2.png",
},
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
is_ground_content = false,
groups = {crumbly=0, not_in_creative_inventory=1},
})
----------------------------------------------------------------------------------------------------
-- Register Recipe
----------------------------------------------------------------------------------------------------
if towercrane.recipe then
minetest.register_craft({
output = "towercrane:base",
recipe = {
{"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
{"default:steel_ingot", "", ""},
{"default:steel_ingot", "dye:yellow", ""}
}
})
end
-- store standard player privs
minetest.register_on_joinplayer(function(player)
local privs = minetest.get_player_privs(player:get_player_name())
local physics = player:get_physics_override()
-- player still hanging on the crane?
if player:get_attribute("tower_crane_active") ~= nil then
fly_privs(nil, player, nil)
else
-- store the player privs default values
player:set_attribute("tower_crane_store_fast", minetest.serialize(privs["fast"]))
player:set_attribute("tower_crane_store_fly", minetest.serialize(privs["fly"]))
player:set_attribute("tower_crane_store_speed", minetest.serialize(physics.speed))
end
end)
-- switch back to normal player privs
minetest.register_on_leaveplayer(function(player, timed_out)
remove_hook(nil, player)
Currently_left_the_game = player:get_player_name()
end)