229 lines
9.2 KiB
Lua
Executable File
229 lines
9.2 KiB
Lua
Executable File
-- Construction helpers; dependencies are supplied by the content registrar.
|
|
return function(hearth, api)
|
|
local MAX_REACH = 6
|
|
local directions = {
|
|
{x = 1, y = 0, z = 0}, {x = -1, y = 0, z = 0},
|
|
{x = 0, y = 0, z = 1}, {x = 0, y = 0, z = -1},
|
|
}
|
|
|
|
local function near(player, pos)
|
|
if not player or not player:is_player() then return false end
|
|
local p = player:get_pos()
|
|
if not p then return false end
|
|
local dx, dy, dz = p.x - pos.x, p.y - pos.y, p.z - pos.z
|
|
return dx * dx + dy * dy + dz * dz <= MAX_REACH * MAX_REACH
|
|
end
|
|
|
|
local function allowed(pos, player, action)
|
|
if not near(player, pos) then return false end
|
|
local name = player:get_player_name()
|
|
local protected = hearth.external_protected or api.is_protected
|
|
if (protected and protected(pos, name)) or not hearth.claim_allowed(pos, name, action or "build") then
|
|
api.record_protection_violation(pos, name)
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local removal_safe
|
|
local function can_dig(pos, player)
|
|
if not allowed(pos, player, "dig") then return false end
|
|
if removal_safe(pos, player) then return true end
|
|
if player and hearth.feedback then
|
|
hearth.feedback(player, "Keep the connected structure supported and within one homestead before removing this block.", "warning")
|
|
elseif player then hearth.notify(player, "Keep connected construction supported before removing this block.") end
|
|
return false
|
|
end
|
|
|
|
local function node_def(pos)
|
|
local node = api.get_node_or_nil(pos)
|
|
return node and api.registered_nodes[node.name], node
|
|
end
|
|
|
|
local function placement_pos(pointed)
|
|
if not pointed or pointed.type ~= "node" then return nil end
|
|
local under = node_def(pointed.under)
|
|
local above = node_def(pointed.above)
|
|
if not under or not above then return nil end
|
|
if under.buildable_to then return pointed.under end
|
|
if above.buildable_to then return pointed.above end
|
|
end
|
|
|
|
local function place(stack, player, pointed)
|
|
if not pointed or pointed.type ~= "node" then return stack end
|
|
local def = node_def(pointed.under)
|
|
-- item_place dispatches right-click callbacks before attempting placement.
|
|
if def and def.on_rightclick and player and not player:get_player_control().sneak then
|
|
local action = def.groups and (def.groups.hearth_storage or 0) > 0 and "storage" or "use"
|
|
if not allowed(pointed.under, player, action) then return stack end
|
|
if action == "storage" then
|
|
return def.on_rightclick(pointed.under, api.get_node(pointed.under), player, stack, pointed) or stack
|
|
end
|
|
return api.item_place(stack, player, pointed)
|
|
end
|
|
local pos = placement_pos(pointed)
|
|
if not pos or not allowed(pos, player) then return stack end
|
|
return api.item_place(stack, player, pointed)
|
|
end
|
|
|
|
local function is_building(name)
|
|
return name == "hearth:wall" or name == "hearth:roof" or name == "hearth:foundation"
|
|
or name == "hearth:frame" or name == "hearth:stairs" or name == "hearth:door"
|
|
or name == "hearth:door_open" or name == "hearth:window" or name == "hearth:vent"
|
|
end
|
|
|
|
local function solid(def)
|
|
return def and def.walkable ~= false and not def.buildable_to
|
|
and (not def.liquidtype or def.liquidtype == "none")
|
|
and not (def.groups and (def.groups.leaves or def.groups.hearth_non_support))
|
|
end
|
|
|
|
local function same_pos(a, b)
|
|
return a.x == b.x and a.y == b.y and a.z == b.z
|
|
end
|
|
local function household_owner(pos)
|
|
return hearth.claim_owner_at and hearth.claim_owner_at(pos) or nil
|
|
end
|
|
local function same_household(target, support)
|
|
local target_owner = household_owner(target)
|
|
local support_owner = household_owner(support)
|
|
return support_owner == nil or support_owner == target_owner
|
|
end
|
|
|
|
local function supported(pos, ignore)
|
|
-- Search existing construction only: at most two horizontal links and
|
|
-- sixteen blocks down. The horizontal budget never resets at a new block,
|
|
-- so sequential placement cannot extend a cantilever indefinitely.
|
|
local queue = {{pos = pos, span = 0, depth = 0}}
|
|
local seen, index, unloaded = {}, 1, false
|
|
while index <= #queue do
|
|
local entry = queue[index]
|
|
index = index + 1
|
|
local p = entry.pos
|
|
local key = p.x .. ":" .. p.y .. ":" .. p.z .. ":" .. entry.span
|
|
if not seen[key] then
|
|
seen[key] = true
|
|
local below = {x = p.x, y = p.y - 1, z = p.z}
|
|
local def, node
|
|
if ignore and same_pos(below, ignore) then
|
|
def, node = api.registered_nodes.air or {walkable = false, buildable_to = true}, {name = "air"}
|
|
else def, node = node_def(below) end
|
|
if not def or node.name == "ignore" then unloaded = true end
|
|
if solid(def) and node.name ~= "ignore" and same_household(pos, below) then
|
|
if not is_building(node.name) then return true end
|
|
if entry.depth < 15 then
|
|
queue[#queue + 1] = {pos = below, span = entry.span,
|
|
depth = entry.depth + 1}
|
|
end
|
|
end
|
|
if entry.span < 2 then
|
|
for _, d in ipairs(directions) do
|
|
local adjacent = {x = p.x + d.x, y = p.y, z = p.z + d.z}
|
|
local _, n
|
|
if ignore and same_pos(adjacent, ignore) then n = {name = "air"}
|
|
else _, n = node_def(adjacent) end
|
|
if n and is_building(n.name) and same_household(pos, adjacent) then
|
|
queue[#queue + 1] = {pos = adjacent, span = entry.span + 1,
|
|
depth = entry.depth}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false, unloaded and "unloaded" or "support"
|
|
end
|
|
|
|
-- Refuse support removal rather than cascading destruction. Check the maximum
|
|
-- span/height a placement may legally depend on, and fail closed across claims.
|
|
removal_safe = function(pos, player)
|
|
local target = api.get_node_or_nil(pos)
|
|
if not target or target.name == "ignore" then return false end
|
|
local target_def = api.registered_nodes[target.name]
|
|
if not solid(target_def) then return true end
|
|
local name = player:get_player_name()
|
|
local removed_owner = household_owner(pos)
|
|
for y = 0, 16 do
|
|
for dx = -2, 2 do for dz = -2, 2 do
|
|
local candidate = {x = pos.x + dx, y = pos.y + y, z = pos.z + dz}
|
|
if not same_pos(candidate, pos) then
|
|
local node = api.get_node_or_nil(candidate)
|
|
if not node or node.name == "ignore" then return false end
|
|
if is_building(node.name) then
|
|
local dependent_owner = household_owner(candidate)
|
|
if dependent_owner and dependent_owner ~= removed_owner then return false end
|
|
if not hearth.claim_allowed(candidate, name, "dig") then return false end
|
|
local ok = supported(candidate, pos)
|
|
if not ok then return false end
|
|
end
|
|
end
|
|
end end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- pos is the resolved destination, not a pointed_thing. This read-only
|
|
-- query deliberately does not call allowed(): previews must never record a
|
|
-- violation, consume an ItemStack, play sound, or create node metadata.
|
|
local function preview(pos, player, item)
|
|
if type(pos) ~= "table" then return false, "invalid" end
|
|
for _, axis in ipairs({"x", "y", "z"}) do
|
|
local n = pos[axis]
|
|
if type(n) ~= "number" or n ~= n or math.abs(n) > 31000 or n % 1 ~= 0 then
|
|
return false, "invalid"
|
|
end
|
|
end
|
|
if not near(player, pos) then return false, "distance" end
|
|
local name = player:get_player_name()
|
|
if api.is_protected(pos, name) or not hearth.claim_allowed(pos, name) then
|
|
return false, "protected"
|
|
end
|
|
local def, node = node_def(pos)
|
|
if not def or node.name == "ignore" then return false, "unloaded" end
|
|
if not def.buildable_to then return false, "occupied" end
|
|
local itemname = type(item) == "string" and item
|
|
if not itemname and item and (type(item) == "table" or type(item) == "userdata")
|
|
and type(item.get_name) == "function" then itemname = item:get_name() end
|
|
if type(itemname) ~= "string" or not api.registered_nodes[itemname] then
|
|
return false, "item"
|
|
end
|
|
if is_building(itemname) then
|
|
local ok, reason = supported(pos)
|
|
if not ok then return false, reason end
|
|
end
|
|
return true, "ok"
|
|
end
|
|
|
|
local function hint(player, message)
|
|
-- Retain chat compatibility for existing callers/tests without feedback.
|
|
if hearth.feedback then hearth.feedback(player, message, "warning")
|
|
else hearth.notify(player, message) end
|
|
if hearth.feedback_sound then hearth.feedback_sound("hearth_denial", player, 0.2) end
|
|
end
|
|
|
|
local function place_building(stack, player, pointed)
|
|
if not pointed or pointed.type ~= "node" then return stack end
|
|
local under = node_def(pointed.under)
|
|
if under and under.on_rightclick and player and not player:get_player_control().sneak then
|
|
if not allowed(pointed.under, player) then return stack end
|
|
return api.item_place(stack, player, pointed)
|
|
end
|
|
local pos = placement_pos(pointed)
|
|
if not pos or not allowed(pos, player) then return stack end
|
|
if not supported(pos) then
|
|
hint(player, "Needs solid support within two horizontal blocks (maximum height 16).")
|
|
return stack
|
|
end
|
|
return api.item_place(stack, player, pointed)
|
|
end
|
|
|
|
local function owner(pos, player)
|
|
if player and player:is_player() then
|
|
api.get_meta(pos):set_string("owner", player:get_player_name())
|
|
end
|
|
end
|
|
|
|
return {near = near, allowed = allowed, can_dig = can_dig, place = place,
|
|
place_building = place_building, owner = owner, preview = preview,
|
|
removal_safe = removal_safe}
|
|
end
|