terraform/init.lua

883 lines
34 KiB
Lua

-- In-memory history/undo engine
local history = {
_lists = {},
-- get list of history entries
get_list = function(self, name)
self._lists[name] = self._lists[name] or {}
return self._lists[name]
end,
-- capture a cuboid in space using voxel manipulator
capture = function(self, player, data, va, minp, maxp)
local capture = {}
for i in va:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
table.insert(capture,data[i])
end
local op = {minp = minp, maxp = maxp, data = capture}
table.insert(self:get_list(player:get_player_name()), op)
end,
-- restore state of the world map from history
undo = function(self, player)
local op = table.remove(self:get_list(player:get_player_name()))
if not op then
return
end
local vm = minetest.get_voxel_manip()
local minv,maxv = vm:read_from_map(op.minp, op.maxp)
local va = VoxelArea:new({MinEdge = minv, MaxEdge = maxv})
local si = 1
local data = vm:get_data()
for i in va:iter(op.minp.x, op.minp.y, op.minp.z, op.maxp.x, op.maxp.y, op.maxp.z) do
data[i] = op.data[si]
si = si + 1
end
vm:set_data(data)
vm:write_to_map(false)
end
}
minetest.register_on_dignode(function(pos,oldnode,player)
history:capture(player, {minetest.get_content_id(oldnode.name)}, VoxelArea:new({MinEdge=pos,MaxEdge=pos}), pos, pos)
end)
minetest.register_on_placenode(function(pos,newnode,player)
history:capture(player, {minetest.CONTENT_AIR}, VoxelArea:new({MinEdge=pos,MaxEdge=pos}), pos, pos)
end)
-- public module API
terraform = {
_tools = {},
_history = history,
-- register a terraform tool
register_tool = function(self, name, spec)
spec.tool_name = name
self._tools[spec.tool_name] = spec
minetest.register_tool("terraform:"..spec.tool_name, {
description = spec.description,
short_description = spec.short_description,
inventory_image = spec.inventory_image,
wield_scale = {x=1,y=1,z=1},
stack_max = 1,
range = spec.range or 128.0,
liquids_pointable = true,
node_dig_prediction = "",
on_use = function(itemstack, player, target)
terraform:show_config(player, spec.tool_name, itemstack)
end,
on_secondary_use = function(itemstack, player, target)
terraform:show_config(player, spec.tool_name, itemstack)
end,
on_place = function(itemstack, player, target)
if player:get_player_control().aux1 then
history:undo(player)
else
spec:execute(player, target, itemstack:get_meta())
end
return itemstack
end,
})
end,
-- show configuration form for the specific tool
show_config = function(self, player, tool_name)
if not self._tools[tool_name].render_config then
return
end
local itemstack = player:get_wielded_item()
self._latest_form = { id = "terraform:props:"..tool_name, tool_name = tool_name}
local formspec = self._tools[tool_name]:render_config(player, itemstack:get_meta())
minetest.show_formspec(player:get_player_name(), terraform._latest_form.id, formspec)
end,
get_inventory = function(player)
return minetest.get_inventory({type = "detached", name = "terraform."..player:get_player_name()})
end,
-- Helpers for storing inventory into settings
string_to_list = function(s,size)
-- Accept: a comma-separated list of content names and desired list size
-- Return: a table with item names, compatible with inventory lists
local result = {}
for part in s:gmatch("[^,]+") do
table.insert(result, part)
end
while #result < size do table.insert(result, "") end
return result
end,
list_to_string = function(list)
-- Accept: result of InvRef:get_list
-- Retrun: a comma-separated list of items
local result = ""
for k,v in pairs(list) do
if v.get_name ~= nil then v = v:get_name() end -- ItemStack to string
if v ~= "" then
if string.len(result) > 0 then
result = result..","
end
result = result..v
end
end
return result
end
}
-- Handle input from forms
minetest.register_on_player_receive_fields(function(player, formname, fields)
if terraform._latest_form and formname == terraform._latest_form.id then
local tool_name = terraform._latest_form.tool_name
local tool = terraform._tools[tool_name]
if not tool.config_input then
return
end
local itemstack = player:get_wielded_item()
local reload = tool:config_input(player, fields, itemstack:get_meta())
-- update tool description in the inventory
if tool.get_description then
itemstack:get_meta():set_string("description", tool:get_description(itemstack:get_meta()))
end
player:set_wielded_item(itemstack)
if fields.quit then
terraform._latest_form = nil
return
end
if reload then
terraform:show_config(player, tool_name, itemstack)
end
end
end)
minetest.register_on_leaveplayer(function(player)
minetest.remove_detached_inventory("terraform."..player:get_player_name())
end)
-- Tools
-- Brush
--
terraform:register_tool("brush", {
description = "Brush\n\nPaints the world with broad strokes",
short_description = "Brush",
inventory_image = "terraform_tool_brush.png",
-- 16 logical tag colors
colors = { "red", "yellow", "lime", "aqua",
"darkred", "orange", "darkgreen", "mediumblue",
"violet", "wheat", "olive", "dodgerblue" },
max_size = 15,
render_config = function(self, player, settings)
local function selection(texture, selected)
if selected then return texture.."^terraform_selection.png" end
return texture
end
local inventory = minetest.create_detached_inventory("terraform."..player:get_player_name(), {
allow_move = function(inv,source,sindex,dest,dindex,count)
if source == "palette" and dest ~= "palette" then
inv:set_stack(dest,dindex, inv:get_stack(source, sindex))
return 0
elseif dest == "palette" and source ~= "palette" then
inv:set_stack(source, sindex, "")
return 0
end
return count
end
})
local palette = {}
local count = 0
local pattern = settings:get_string("search_text")
local skip = 40 * (settings:get_int("search_page") or 0)
for k,v in pairs(minetest.registered_nodes) do
if not pattern or string.find(k, pattern) ~= nil then
if skip > 0 then
skip = skip - 1
else
table.insert(palette, k)
if #palette >= 40 then
break
end
end
count = count + 1
end
end
while #palette < 40 do table.insert(palette, "") end
local paint = terraform.string_to_list(settings:get_string("paint"), 10)
local mask = terraform.string_to_list(settings:get_string("mask"), 10)
inventory:set_list("palette", palette)
inventory:set_list("paint", paint)
inventory:set_list("mask", mask)
spec =
"formspec_version[3]"..
"size[17,12]"..
"position[0.5,0.45]"..
"anchor[0.5,0.5]"..
"no_prepend[]"..
"button_exit[14.5,10.5;2,1;quit;Close]".. -- Close button !Remember to offset when form size changes
"container[0.5,0.5]".. -- shape
"label[0,0.5; Shape:]"
local pos = 0
for shape,_ in pairs(self.shapes) do
local x = pos % 3
local y = math.floor(pos / 3) + 1
spec = spec.."image_button["..x..","..y..";1,1;"..selection("terraform_shape_"..shape..".png",settings:get_string("shape") == shape)..";shape_"..shape..";]"
pos = pos + 1
end
spec = spec ..
"container_end[]"..
"container[0.5,4]".. -- size
"label[0,0.4; Size:]"..
"field[1,0;1,0.7;size;;"..(settings:get_int("size") or 3).."]"..
"field_close_on_enter[size;false]"..
"scrollbaroptions[min=0;max="..self.max_size..";smallstep=1;thumbsize=0;arrows=show]"..
"scrollbar[2,0;0.35,0.7;vertical;size_sb;"..(self.max_size - (settings:get_int("size") or 3)).."]"..
"container_end[]"..
"container[0.5, 5]".. -- flags
"checkbox[0,0;flags_surface;Surface;"..(settings:get_int("flags_surface") == 1 and "true" or "false").."]"..
"checkbox[0,0.5;flags_scatter;Scatter;"..(settings:get_int("flags_scatter") == 1 and "true" or "false").."]"..
"checkbox[0,1;flags_decor;Decoration;"..(settings:get_int("flags_decor") == 1 and "true" or "false").."]"..
"container_end[]"..
"container[4,0.5]".. -- creative
"label[0,0.5; Palette]"..
"label[4.75,0.5; Find nodes:]"..
"field[6.5,0.1;2,0.75;search_text;;"..(settings:get_string("search_text") or "").."]"..
"field_close_on_enter[search_text;false]"..
"button[8.5,0.1;2,0.75;search;Search]"..
"button[10.5,0.1;0.75,0.75;prev_page;<]"..
"button[11.25,0.1;0.75,0.75;next_page;>]"..
"list[detached:terraform."..player:get_player_name()..";palette;0,1;10,4]"..
"container_end[]"..
"container[4,6]".. -- paint
"label[0,0.5; Paint]"..
"list[detached:terraform."..player:get_player_name()..";paint;0,1;10,1]"..
"container_end[]"..
"container[4,8]".. -- Mask
"label[0,0.5; Mask]"..
"list[detached:terraform."..player:get_player_name()..";mask;0,1;10,1]"..
"container_end[]"
-- Color tags
spec = spec..
"container[0.5, 6]"..
"label[0,0.5; Color Tag]"
local count = 0
local size = 0.5
for _, color in ipairs(self.colors) do
local offset = size*(count % 4)
local line = 0.75 + size*math.floor(count / 4)
local texture = "terraform_tool_brush.png^[multiply:"..color..""
spec = spec..
"image_button["..offset..","..line..";"..size..","..size..";"..selection(texture,settings:get_string("color") == color)..";color_"..color..";]"
count = count + 1
end
spec = spec..
"container_end[]"..
""
return spec
end,
config_input = function(self, player, fields, settings)
local refresh = false
-- Shape
for shape,_ in pairs(self.shapes) do
if fields["shape_"..shape] ~= nil then
settings:set_string("shape", shape)
refresh = true
end
end
-- Size
if fields.size_sb ~= nil and string.find(fields.size_sb, "CHG") then
local e = minetest.explode_scrollbar_event(fields.size_sb)
if e.type == "CHG" then
settings:set_int("size", math.min(math.max(self.max_size - tonumber(e.value), 0), self.max_size))
refresh = true
end
elseif fields.size ~= nil then
settings:set_int("size", math.min(math.max(tonumber(fields.size), 0), self.max_size))
end
-- Flags
if fields.flags_surface ~= nil then
settings:set_int("flags_surface", fields.flags_surface == "true" and 1 or 0)
end
if fields.flags_scatter ~= nil then
settings:set_int("flags_scatter", fields.flags_scatter == "true" and 1 or 0)
end
if fields.flags_decor ~= nil then
settings:set_int("flags_decor", fields.flags_decor == "true" and 1 or 0)
end
-- Search
if fields.search_text ~= nil then
settings:set_string("search_text", fields.search_text or "")
refresh = true
end
if fields.search ~= nil or (fields.search_text ~= nil and fields.key_enter_field == "search_text") then
settings:set_int("search_page", 0)
refresh = true
end
if fields.prev_page ~= nil then
settings:set_int("search_page", math.max(0, (settings:get_int("search_page") or 0) - 1))
refresh = true
end
if fields.next_page ~= nil then
settings:set_int("search_page", (settings:get_int("search_page") or 0) + 1)
refresh = true
end
-- Color Tags
for _,color in ipairs(self.colors) do
if fields["color_"..color] then
settings:set_string("color", color)
refresh = true
end
end
local inv = terraform.get_inventory(player)
if inv ~= nil then
settings:set_string("paint", terraform.list_to_string(inv:get_list("paint")))
settings:set_string("mask", terraform.list_to_string(inv:get_list("mask")))
end
return refresh
end,
get_description = function(self, settings)
return "Terraform Brush ("..(settings:get_string("shape") or "shpere")..")\n"..
"size "..(settings:get_int("size") or 0).."\n"..
"paint "..(settings:get_string("paint")).."\n"..
"mask "..(settings:get_string("mask"))
end,
execute = function(self, player, target, settings)
-- Get position
local target_pos = minetest.get_pointed_thing_position(target)
if not target_pos then
return
end
-- Define size in 3d
local size = settings:get_int("size") or 3
local size_3d = vector.new(size, size, size)
-- Pick a shape
local shape_name = settings:get_string("shape") or "sphere"
if not self.shapes[shape_name] then shape_name = "sphere" end
local shape = self.shapes[shape_name]()
-- Define working area and load state
local minp,maxp = shape:get_bounds(player, target_pos, size_3d)
local v = minetest.get_voxel_manip()
local minv, maxv = v:read_from_map(minp, maxp)
local a = VoxelArea:new({MinEdge = minv, MaxEdge = maxv })
-- Get data and capture history
local data = v:get_data()
history:capture(player, data, a, minp, maxp)
-- Set up context
local ctx = {
size_3d = size_3d,
player = player
}
-- Prepare Paint
local paint = {}
for i,v in ipairs(terraform.string_to_list(settings:get_string("paint"), 10)) do
if v ~= "" then
table.insert(paint, minetest.get_content_id(v))
end
end
if #paint == 0 then
table.insert(paint, minetest.CONTENT_AIR)
end
ctx.get_paint = function()
return paint[math.random(1, #paint)]
end
-- Prepare Mask
local mask = {}
for i,v in ipairs(terraform.string_to_list(settings:get_string("mask"), 10)) do
if v ~= "" then
table.insert(mask, minetest.get_content_id(v))
end
end
ctx.in_mask = function(cid)
if #mask == 0 then return true end
for i,v in ipairs(mask) do if v == cid then return true end end
return false
end
-- Prepare flags
local flags = {}
if settings:get_int("flags_surface") == 1 then
table.insert(flags, function(i)
if data[i] == minetest.CONTENT_AIR then return nil end
if a:position(i).y < maxp.y and data[i+a.ystride] == minetest.CONTENT_AIR then return i end
return nil
end)
end
if settings:get_int("flags_decor") == 1 then
table.insert(flags, function(i)
if data[i] == minetest.CONTENT_AIR then return nil end
if a:position(i).y < maxp.y and data[i+a.ystride] == minetest.CONTENT_AIR then return i+a.ystride end
return nil
end)
end
if settings:get_int("flags_scatter") == 1 then
table.insert(flags, function(i)
return math.random(1,1000) <= 100 and i or nil
end)
end
ctx.draw = function(i, paint)
if not ctx.in_mask(data[i]) then return end -- if not in mask, skip painting
for _,f in ipairs(flags) do
i = f(i)
if not i then return end -- if i is nil, skip painting
end
data[i] = paint or ctx.get_paint()
end
-- Paint
shape:paint(data, a, target_pos, minp, maxp, ctx)
-- Save back to map, no light information
v:set_data(data)
v:write_to_map(false)
end,
-- Definition of shapes
shapes = {
cube = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
if player:get_pos().y > target_pos.y then
-- place on top if looking down
return vector.subtract(target_pos, vector.new(size_3d.x, 0, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, 2*size_3d.y, size_3d.z))
else
-- place on bottom if looking up
return vector.subtract(target_pos, vector.new(size_3d.x, 2*size_3d.y, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, 0, size_3d.z))
end
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
ctx.draw(i)
end
end,
}
end,
sphere = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
local ip = a:position(i)
local epsilon = 0.3
local delta = { x = ip.x - target_pos.x, y = ip.y - target_pos.y, z = ip.z - target_pos.z }
delta = { x = delta.x / (ctx.size_3d.x + epsilon), y = delta.y / (ctx.size_3d.y + epsilon), z = delta.z / (ctx.size_3d.z + epsilon) }
delta = { x = delta.x^2, y = delta.y^2, z = delta.z^2 }
if 1 > delta.x + delta.y + delta.z then
ctx.draw(i)
end
end
end,
}
end,
cylinder = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
if player:get_pos().y > target_pos.y then
-- place on top if looking down
return vector.subtract(target_pos, vector.new(size_3d.x, 0, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, size_3d.y, size_3d.z))
else
-- place on bottom if looking up
return vector.subtract(target_pos, vector.new(size_3d.x, size_3d.y, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, 0, size_3d.z))
end
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
for i in a:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
local ip = a:position(i)
local epsilon = 0.3
local delta = { x = ip.x - target_pos.x, z = ip.z - target_pos.z }
delta = { x = delta.x / (ctx.size_3d.x + epsilon), z = delta.z / (ctx.size_3d.z + epsilon) }
delta = { x = delta.x^2, z = delta.z^2 }
if 1 > delta.x + delta.z then
ctx.draw(i)
end
end
end,
}
end,
plateau = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
-- look up to 100 meters down
return vector.subtract(target_pos, vector.new(size_3d.x, 100, size_3d.z)), vector.add(target_pos, vector.new(size_3d.x, 0, size_3d.z))
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
-- find deepest level (as negative)
local depth = 0
for x = -ctx.size_3d.x,ctx.size_3d.x do
for z = -ctx.size_3d.z,ctx.size_3d.z do
-- look in the circle around origin
local r = (x/(ctx.size_3d.x+0.3))^2 + (z/(ctx.size_3d.z+0.3))^2
if r < 1 then
-- scan 100 levels down
for y = 0,-100,-1 do
-- stop if the bottom is hit
local p = origin + x + y * a.ystride + z * a.zstride
if not ctx.in_mask(data[p]) then
if y < depth then depth = y end
break
end
end
end
end
end
-- fill
for x = -ctx.size_3d.x,ctx.size_3d.x do
for z = -ctx.size_3d.z,ctx.size_3d.z do
-- look in the circle around origin
local r = (x/(ctx.size_3d.x+0.3))^2 + (z/(ctx.size_3d.z+0.3))^2
if r < 1 then
-- innermost 0.3 radius is fully filled, then sine descend
local cutoff = 0
if r > 0.6 then
cutoff = math.min(0, math.floor(depth * math.sin((r - 0.6) * math.pi / 4)))
end
-- fill with material down from cut off point to depth
for y = cutoff,depth,-1 do
i = origin + x + y * a.ystride + z * a.zstride
if ctx.in_mask(data[i]) then
ctx.draw(i)
else
break --stop at the first non-mask
end
end
end
end
end
end
}
end,
smooth = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
local b = {}
local function get_weight(i)
local weight = 0
for lx = -1,1 do
for ly = -1,1 do
for lz = -1,1 do
if data[i + lx + a.ystride*ly + a.zstride*lz] ~= minetest.CONTENT_AIR then
weight = weight + (1 / math.max(1, math.abs(lx) + math.abs(ly) + math.abs(lz)))
end
end
end
end
return weight
end
-- Spherical shape
-- Reduce all bounds by 1 to avoid edge glitches when looking for neighbours
for x = -ctx.size_3d.x+1,ctx.size_3d.x-1 do
for y = -ctx.size_3d.y+1,ctx.size_3d.y-1 do
for z = -ctx.size_3d.z+1,ctx.size_3d.z-1 do
local r = (x/ctx.size_3d.x)^2 + (y/ctx.size_3d.y)^2 + (z/ctx.size_3d.z)^2
if r <= 1 then
local i = origin + x + a.ystride*y + a.zstride*z
b[i] = get_weight(i) > 7.8 --max weight here is 15.6
end
end
end
end
for i,v in pairs(b) do
if ctx.in_mask(data[i]) then
if v then
data[i] = ctx.get_paint()
else
data[i] = minetest.CONTENT_AIR
end
end
end
end,
}
end,
cut = function()
return {
get_bounds = function(self, player, target_pos, size_3d)
return vector.subtract(target_pos, size_3d), vector.add(target_pos, size_3d)
end,
paint = function(self, data, a, target_pos, minp, maxp, ctx)
local origin = a:indexp(target_pos)
local normal = vector.direction(target_pos, ctx.player:get_pos())
local threshold = math.pi / 36 -- 5 degrees
-- Spherical shape
for x = -ctx.size_3d.x,ctx.size_3d.x do
for y = -ctx.size_3d.y,ctx.size_3d.y do
for z = -ctx.size_3d.z,ctx.size_3d.z do
local r = (x/ctx.size_3d.x)^2 + (y/ctx.size_3d.y)^2 + (z/ctx.size_3d.z)^2
if r <= 1 then
local i = origin + x + y*a.ystride + z*a.zstride
local dot = vector.dot(normal, vector.new(x, y, z))
if math.abs(dot) > threshold and ctx.in_mask(data[i]) then
if dot <= 0 then
data[i] = ctx.get_paint()
else
data[i] = minetest.CONTENT_AIR
end
end
end
end
end
end
end,
}
end,
}
})
minetest.register_alias("terraform:sculptor", "terraform:brush")
-- Colorize brush when putting to inventory
minetest.register_on_player_inventory_action(function(player,action,inventory,inventory_info)
if inventory_info.listname ~= "main" or inventory_info.stack:get_name() ~= "terraform:brush" then
return
end
local stack = inventory_info.stack
if (stack:get_meta():get_string("color") or "") == "" then
local colors = terraform._tools["brush"].colors
local color = colors[math.random(1,#colors)]
stack:get_meta():set_string("color", color)
inventory:set_stack(inventory_info.listname, inventory_info.index, stack)
end
end)
--
-- Undo changes to the world
--
terraform:register_tool("undo", {
description = "Terraform Undo\n\nUndoes changes to the world",
short_description = "Terraform Undo",
inventory_image = "terraform_tool_undo.png",
execute = function(itemstack, player, target)
history:undo(player)
end
})
--
-- A magic wand to fix light problems.
--
terraform:register_tool("fixlight", {
description = "Terraform Fix Light\n\nFix lighting problems",
short_description = "Terraform Fix Light",
inventory_image = "terraform_tool_fix_light.png",
execute = function(itemstack, player, target)
-- Get position
local target_pos = minetest.get_pointed_thing_position(target)
if not target_pos then
return
end
local s = 100
local origin = target_pos
local minp = vector.subtract(origin, vector.new(s,s,s))
local maxp = vector.add(origin, vector.new(s,s,s))
minetest.fix_light(minp, maxp)
end
})
--
-- Some helper functions needed for the light placement functionality
--
local function vector_floor(v)
return vector.new(math.floor(v.x), math.floor(v.y), math.floor(v.z))
end
local function vector_min(v1, v2)
return vector.new(math.min(v1.x,v2.x), math.min(v1.y,v2.y), math.min(v1.z,v2.z))
end
local function box_diff(a, b)
-- a - b for boxes a and b, both a { min = vector, max = vector }
-- return 3 boxes
-- * split along X axis, full Y and Z
-- * split along Y axis, half X, full Z
-- * half X, Y, Z
local function diff_split(ax1, ax2, bx1, bx2)
-- given boundaries of two boxes a and b along an axis (x)
-- return x coordinates of athe diff a - b as two boxes
-- first box is having full height
-- second box is having trimmed height (w/o the intersection part)
if ax1 < bx1 then
return ax1, bx1, bx1, ax2
else
return bx2, ax2, ax1, bx2
end
end
local fx1, fx2, hx1, hx2 = diff_split(a.min.x, a.max.x, b.min.x, b.max.x)
local fy1, fy2, hy1, hy2 = diff_split(a.min.y, a.max.y, b.min.y, b.max.y)
local fz1, fz2, hz1, hz2 = diff_split(a.min.z, a.max.z, b.min.z, b.max.z)
return { min = vector.new(fx1, a.min.y, a.min.z), max = vector.new(fx2, a.max.y, a.max.z)},
{ min = vector.new(hx1, fy1, a.min.z), max = vector.new(hx2, fy2, a.max.z)},
{ min = vector.new(hx1, hy1, fz1), max = vector.new(hx2, hy2, fz2)}
end
local light = {
size = 20,
level = minetest.LIGHT_MAX,
pitch_rate = 1/5,
queues = { light = {}, dark = {} },
players = {},
light_bounds = function(self, pos)
local s = self.size
return { min = vector.subtract(pos, vector.new(s,s,s)), max = vector.add(pos, vector.new(s,s,s)) }
end,
add_player = function(self, player)
self.players[player:get_player_name()] = { player = player }
end,
remove_player = function(self, player)
local light = self.players[player:get_player_name()]
if light ~= nil then
table.insert(self.queues.dark, self:light_bounds(vector.floor(light.player:get_pos())))
if light.last_pos ~= nil then
table.insert(self.queues.dark, self:light_bounds(light.last_pos))
end
end
self.players[player:get_player_name()] = nil
end,
tick = function(self)
for name,pl in pairs(self.players) do
local origin = vector.floor(pl.player:get_pos())
local box = self:light_bounds(origin)
local minp = box.min
local maxp = box.max
pl.c = (pl.c or 0) + 1
if pl.last_pos ~= nil then
if vector.distance(origin, pl.last_pos) < self.size * self.pitch_rate then
break -- skip the player, not enough movement
end
local old_box = self:light_bounds(pl.last_pos)
local b1, b2, b3 = box_diff(old_box, box)
table.insert(self.queues.dark, b1)
table.insert(self.queues.dark, b2)
table.insert(self.queues.dark, b3)
end
table.insert(self.queues.light, box)
pl.last_pos = origin
end
-- process queues
while #self.queues.dark > 0 do
local box = table.remove(self.queues.dark, 1)
local vm = minetest.get_voxel_manip(box.min, box.max)
vm:write_to_map(true) -- fix the light
end
while #self.queues.light > 0 do
local box = table.remove(self.queues.light, 1)
-- Load manipulator
local vm = minetest.get_voxel_manip()
local mine,maxe = vm:read_from_map(box.min, box.max)
local va = VoxelArea:new({MinEdge = mine, MaxEdge = maxe})
-- Set light information in the area
local light = vm:get_light_data()
local level = self.level
for i in va:iter(box.min.x, box.min.y, box.min.z, box.max.x, box.max.y, box.max.z) do
if light[i] == nil then
light[i] = level*17
else
light[i] = math.max(math.floor(light[i] / 16), level) * 16 + math.max(light[i] % 16, level)
end
end
vm:set_light_data(light)
vm:write_to_map(false)
end
end
}
terraform:register_tool("light", {
description = "Terraform Light\n\nTurn on the lights",
short_description = "Terraform Light",
inventory_image = "terraform_tool_light.png",
execute = function(itemstack, player, target)
if player:get_day_night_ratio() ~= nil then
player:override_day_night_ratio(nil)
else
player:override_day_night_ratio(1)
end
if light.players[player:get_player_name()] == nil then
light:add_player(player)
else
light:remove_player(player)
end
end
})
minetest.register_on_leaveplayer(function(player)
light:remove_player(player)
end)
local function place_lights()
light:tick()
minetest.after(0.5, place_lights)
end
minetest.after(0.5, place_lights)