--[[ TechAge ======= Copyright (C) 2019-2022 Joachim Stolberg AGPL v3 See LICENSE.txt for more information Helper functions ]]-- -- for lazy programmers local P = minetest.string_to_pos local M = minetest.get_meta local S = techage.S -- Input data to generate the Param2ToDir table local Input = { 8,9,10,11, -- 1 16,17,18,19, -- 2 4,5,6,7, -- 3 12,13,14,15, -- 4 0,1,2,3, -- 5 20,21,22,23, -- 6 } -- Input data to turn a "facedir" block to the right/left local ROTATION = { {5,14,11,16}, -- x+ {7,12,9,18}, -- x- {0,1,2,3}, -- y+ {22,21,20,23}, -- y- {6,15,8,17}, -- z+ {4,13,10,19}, -- z- } local FACEDIR_TO_ROT = {[0] = {x=0.000000, y=0.000000, z=0.000000}, {x=0.000000, y=4.712389, z=0.000000}, {x=0.000000, y=3.141593, z=0.000000}, {x=0.000000, y=1.570796, z=0.000000}, {x=4.712389, y=0.000000, z=0.000000}, {x=3.141593, y=1.570796, z=1.570796}, {x=1.570796, y=4.712389, z=4.712389}, {x=3.141593, y=4.712389, z=4.712389}, {x=1.570796, y=0.000000, z=0.000000}, {x=0.000000, y=4.712389, z=1.570796}, {x=4.712389, y=1.570796, z=4.712389}, {x=0.000000, y=1.570796, z=4.712389}, {x=0.000000, y=0.000000, z=1.570796}, {x=4.712389, y=0.000000, z=1.570796}, {x=0.000000, y=3.141593, z=4.712389}, {x=1.570796, y=3.141593, z=4.712389}, {x=0.000000, y=0.000000, z=4.712389}, {x=1.570796, y=0.000000, z=4.712389}, {x=0.000000, y=3.141593, z=1.570796}, {x=4.712389, y=0.000000, z=4.712389}, {x=0.000000, y=0.000000, z=3.141593}, {x=0.000000, y=1.570796, z=3.141593}, {x=0.000000, y=3.141593, z=3.141593}, {x=0.000000, y=4.712389, z=3.141593}, } local RotationViaYAxis = {} for _,row in ipairs(ROTATION) do for i = 1,4 do local val = row[i] local left = row[i == 1 and 4 or i - 1] local right = row[i == 4 and 1 or i + 1] RotationViaYAxis[val] = {left, right} end end function techage.facedir_to_rotation(facedir) return FACEDIR_TO_ROT[facedir] end function techage.param2_turn_left(param2) return (RotationViaYAxis[param2] or RotationViaYAxis[0])[2] end function techage.param2_turn_right(param2) return (RotationViaYAxis[param2] or RotationViaYAxis[0])[1] end -- Roll a block in north direction (south is vice versa) local RollNorth = { {0,4,22,8}, {1,5,23,9}, {2,6,20,10}, {3,7,21,11}, {12,13,14,15}, {16,19,18,17}, } -- Roll a block in east direction (west is vice versa) local RollEast = { {0,12,20,16}, {1,13,21,17}, {2,14,22,18}, {3,15,23,19}, {4,7,6,5}, {8,9,10,11}, } -- Generate a table for all facedir and param2 values: -- TurnUp[facedir][param2] = new_param2 local TurnUp = {[0] = {}, {}, {}, {}} for i = 1,6 do for j = 1,4 do local idx = RollNorth[i][j] TurnUp[0][idx] = RollNorth[i][j == 4 and 1 or j + 1] -- north TurnUp[2][idx] = RollNorth[i][j == 1 and 4 or j - 1] -- south idx = RollEast[i][j] TurnUp[1][idx] = RollEast[i][j == 4 and 1 or j + 1] -- east TurnUp[3][idx] = RollEast[i][j == 1 and 4 or j - 1] -- west end end -- facedir is from the players (0..3) -- param2 is from the node (0..23) function techage.param2_turn_up(facedir, param2) return TurnUp[facedir % 4][param2 % 24] end ------------------------------------------------------------------------------- -- Rotate nodes around the center ------------------------------------------------------------------------------- function techage.positions_center(lpos) local c = {x=0, y=0, z=0} for _,v in ipairs(lpos) do c = vector.add(c, v) end c = vector.divide(c, #lpos) c = vector.round(c) c.y = 0 return c end function techage.rotate_around_axis(v, c, turn) local dx, dz = v.x - c.x, v.z - c.z if turn == "l" then return { x = c.x - dz, y = v.y, z = c.z + dx, } elseif turn == "r" then return { x = c.x + dz, y = v.y, z = c.z - dx, } elseif turn == "" then return v else -- turn 180 degree return { x = c.x - dx, y = v.y, z = c.z - dz, } end end -- Function returns a list αΊƒith the new node positions -- turn is one of "l", "r", "2l", "2r" -- cpos is the center pos (optional) function techage.rotate_around_center(nodes1, turn, cpos) cpos = cpos or techage.positions_center(nodes1) local nodes2 = {} for _,pos in ipairs(nodes1) do nodes2[#nodes2 + 1] = techage.rotate_around_axis(pos, cpos, turn) end return nodes2 end -- allowed for digging local RegisteredNodesToBeDug = {} function techage.register_node_to_be_dug(name) RegisteredNodesToBeDug[name] = true end -- translation from param2 to dir (out of the node upwards) local Param2Dir = {} for idx,val in ipairs(Input) do Param2Dir[val] = math.floor((idx - 1) / 4) + 1 end -- used by lamps and power switches function techage.determine_node_bottom_as_dir(node) return tubelib2.Turn180Deg[Param2Dir[node.param2] or 1] end function techage.determine_node_top_as_dir(node) return Param2Dir[node.param2] or 1 end -- rotation rules (screwdriver) for wallmounted "facedir" nodes function techage.rotate_wallmounted(param2) local offs = math.floor(param2 / 4) * 4 local rot = ((param2 % 4) + 1) % 4 return offs + rot end function techage.in_range(val, min, max) val = tonumber(val) if val < min then return min end if val > max then return max end return val end function techage.one_of(val, selection) for _,v in ipairs(selection) do if val == v then return val end end return selection[1] end function techage.index(list, x) for idx, v in pairs(list) do if v == x then return idx end end return nil end function techage.in_list(list, x) for idx, v in pairs(list) do if v == x then return true end end return false end function techage.add_to_set(set, x) if not techage.index(set, x) then table.insert(set, x) end end -- techage.tbl_filter({"a", "b", "c", "d"}, function(v, k, t) return v >= "c" end) --> {"c","d"} techage.tbl_filter = function(t, filterIter) local out = {} for k, v in pairs(t) do if filterIter(v, k, t) then out[k] = v end end return out end function techage.get_node_lvm(pos) local node = minetest.get_node_or_nil(pos) if node then return node end local vm = minetest.get_voxel_manip() local MinEdge, MaxEdge = vm:read_from_map(pos, pos) local data = vm:get_data() local param2_data = vm:get_param2_data() local area = VoxelArea:new({MinEdge = MinEdge, MaxEdge = MaxEdge}) local idx = area:indexp(pos) if data[idx] and param2_data[idx] then return { name = minetest.get_name_from_content_id(data[idx]), param2 = param2_data[idx] } end return {name="ignore", param2=0} end function techage.is_air_like(name) local ndef = minetest.registered_nodes[name] if ndef and ndef.buildable_to then return true end return false end -- returns true, if node can be dug, otherwise false function techage.can_node_dig(node, ndef) if RegisteredNodesToBeDug[node.name] then return true end if not ndef then return false end if node.name == "ignore" then return false end if node.name == "air" then return true end if ndef.buildable_to == true then return true end if ndef.diggable == false then return false end if ndef.after_dig_node then return false end -- add it to the white list RegisteredNodesToBeDug[node.name] = true return true end techage.dig_states = { NOT_DIGGABLE = 1, INV_FULL = 2, DUG = 3 } -- Digs a node like a player would by utilizing a fake player object. -- add_to_inv(itemstacks) is a method that should try to add the dropped stacks to an appropriate inventory. -- The node will only be dug, if add_to_inv(itemstacks) returns true. function techage.dig_like_player(pos, fake_player, add_to_inv) local node = techage.get_node_lvm(pos) local ndef = minetest.registered_nodes[node.name] if not ndef or ndef.diggable == false or (ndef.can_dig and not ndef.can_dig(pos, fake_player)) then return techage.dig_states.NOT_DIGGABLE end local drop_as_strings = minetest.get_node_drops(node) local drop_as_stacks = {} for _,itemstring in ipairs(drop_as_strings) do drop_as_stacks[#drop_as_stacks+1] = ItemStack(itemstring) end local meta = M(pos) if ndef.preserve_metadata then ndef.preserve_metadata(pos, node, meta, drop_as_stacks) end if add_to_inv(drop_as_stacks) then local oldmeta = meta:to_table() minetest.remove_node(pos) if ndef.after_dig_node then ndef.after_dig_node(pos, node, oldmeta, fake_player) end return techage.dig_states.DUG end return techage.dig_states.INV_FULL end local function handle_drop(drop) -- To keep it simple, return only the item with the lowest rarity if drop.items then local rarity = 9999 local name for idx,item in ipairs(drop.items) do if item.rarity and item.rarity < rarity then rarity = item.rarity name = item.items[1] -- take always the first item else return item.items[1] -- take always the first item end end return name end return false end -- returns the node name, if node can be dropped, otherwise nil function techage.dropped_node(node, ndef) if node.name == "air" then return end --if ndef.buildable_to == true then return end if not ndef.diggable then return end if ndef.drop == "" then return end if type(ndef.drop) == "table" then return handle_drop(ndef.drop) end return ndef.drop or node.name end -- needed for windmill plants local function determine_ocean_ids() techage.OceanIdTbl = {} for name, _ in pairs(minetest.registered_biomes) do if string.find(name, "ocean") then local id = minetest.get_biome_id(name) --print(id, name) techage.OceanIdTbl[id] = true end end end determine_ocean_ids() -- check if natural water is on given position (water placed by player has param2 = 1) function techage.is_ocean(pos) if pos.y > 1 then return false end local node = techage.get_node_lvm(pos) if node.name ~= "default:water_source" then return false end if node.param2 == 1 then return false end return true end function techage.item_image(x, y, itemname, count) local name, size = unpack(string.split(itemname, " ")) size = count and count or size size = tonumber(size) or 1 local label = "" local text = minetest.formspec_escape(ItemStack(itemname):get_description()) local tooltip = "tooltip["..x..","..y..";1,1;"..text..";#0C3D32;#FFFFFF]" if minetest.registered_tools[name] and size > 1 then local offs = 0 if size < 10 then offs = 0.65 elseif size < 100 then offs = 0.5 elseif size < 1000 then offs = 0.35 else offs = 0.2 end label = "label["..(x + offs)..","..(y + 0.45)..";"..tostring(size).."]" end return "box["..x..","..y..";0.85,0.9;#808080]".. "item_image["..x..","..y..";1,1;"..itemname.."]".. tooltip.. label end function techage.item_image_small(x, y, itemname, tooltip_prefix) local name = unpack(string.split(itemname, " ")) local tooltip = "" local ndef = minetest.registered_nodes[name] or minetest.registered_items[name] or minetest.registered_craftitems[name] if ndef and ndef.description then local text = minetest.formspec_escape(ndef.description) tooltip = "tooltip["..x..","..y..";0.8,0.8;"..tooltip_prefix..": "..text..";#0C3D32;#FFFFFF]" end return "box["..x..","..y..";0.65,0.7;#808080]".. "item_image["..x..","..y..";0.8,0.8;"..name.."]".. tooltip end function techage.mydump(o, indent, nested, level) local t = type(o) if not level and t == "userdata" then -- when userdata (e.g. player) is passed directly, print its metatable: return "userdata metatable: " .. techage.mydump(getmetatable(o)) end if t ~= "table" then return basic_dump(o) end -- Contains table -> true/nil of currently nested tables nested = nested or {} if nested[o] then return "" end nested[o] = true indent = " " level = level or 1 local t = {} local dumped_indexes = {} for i, v in ipairs(o) do t[#t + 1] = techage.mydump(v, indent, nested, level + 1) dumped_indexes[i] = true end for k, v in pairs(o) do if not dumped_indexes[k] then if type(k) ~= "string" or not is_valid_identifier(k) then k = "["..techage.mydump(k, indent, nested, level + 1).."]" end v = techage.mydump(v, indent, nested, level + 1) t[#t + 1] = k.." = "..v end end nested[o] = nil if indent ~= "" then local indent_str = string.rep(indent, level) local end_indent_str = string.rep(indent, level - 1) return string.format("{%s%s%s}", indent_str, table.concat(t, ","..indent_str), end_indent_str) end return "{"..table.concat(t, ", ").."}" end function techage.vector_dump(posses) local t = {} for _,pos in ipairs(posses) do t[#t + 1] = minetest.pos_to_string(pos) end return table.concat(t, " ") end -- title bar help (width is the fornmspec width) function techage.question_mark_help(width, tooltip) local x = width- 0.6 return "label["..x..",-0.1;"..minetest.colorize("#000000", minetest.formspec_escape("[?]")).."]".. "tooltip["..x..",-0.1;0.5,0.5;"..tooltip..";#0C3D32;#FFFFFF]" end function techage.wrench_tooltip(x, y) local tooltip = S("Block has an\nadditional wrench menu") return "label["..x..","..y..";"..minetest.colorize("#000000", minetest.formspec_escape("[?]")).."]".. "tooltip["..x..","..y..";0.5,0.5;"..tooltip..";#0C3D32;#FFFFFF]" end techage.RegisteredMobsMods = {} -- Register mobs mods for the move/fly controllers function techage.register_mobs_mods(mod) techage.RegisteredMobsMods[mod] = true end ------------------------------------------------------------------------------- -- Terminal history buffer ------------------------------------------------------------------------------- local BUFFER_DEPTH = 10 function techage.historybuffer_add(pos, s) local mem = techage.get_mem(pos) mem.hisbuf = mem.hisbuf or {} if #s > 2 then table.insert(mem.hisbuf, s) if #mem.hisbuf > BUFFER_DEPTH then table.remove(mem.hisbuf, 1) end mem.hisbuf_idx = #mem.hisbuf + 1 end end function techage.historybuffer_priv(pos) local mem = techage.get_mem(pos) mem.hisbuf = mem.hisbuf or {} mem.hisbuf_idx = mem.hisbuf_idx or 1 mem.hisbuf_idx = math.max(1, mem.hisbuf_idx - 1) return mem.hisbuf[mem.hisbuf_idx] end function techage.historybuffer_next(pos) local mem = techage.get_mem(pos) mem.hisbuf = mem.hisbuf or {} mem.hisbuf_idx = mem.hisbuf_idx or 1 mem.hisbuf_idx = math.min(#mem.hisbuf, mem.hisbuf_idx + 1) return mem.hisbuf[mem.hisbuf_idx] end ------------------------------------------------------------------------------- -- Player TA5 Experience Points ------------------------------------------------------------------------------- function techage.get_expoints(player) if player and player.get_meta then local meta = player:get_meta() if meta then return meta:get_int("techage_ex_points") end end end -- Can only be used from one collider function techage.add_expoint(player, number) if player and player.get_meta then local meta = player:get_meta() if meta then if not meta:contains("techage_collider_number") then meta:set_string("techage_collider_number", number) end if meta:get_string("techage_collider_number") == number then meta:set_int("techage_ex_points", meta:get_int("techage_ex_points") + 1) return true else minetest.chat_send_player(player:get_player_name(), "[techage] More than one collider is not allowed!") return false end end end end function techage.on_remove_collider(player) if player and player.get_meta then local meta = player:get_meta() if meta then meta:set_string("techage_collider_number", "") end end end function techage.set_expoints(player, ex_points) if player and player.get_meta then local meta = player:get_meta() if meta then meta:set_int("techage_ex_points", ex_points) return true end end end ------------------------------------------------------------------------------- -- Scheduler for a table-based, cyclic call of functions ------------------------------------------------------------------------------- local TABLE_SIZE = 256 techage.scheduler = {} local function add_to_table(tbl, i, func) while i < TABLE_SIZE do if not tbl[i] then tbl[i] = func return i + 1 end i = i + 1 end return i end function techage.scheduler.init(pos) local mem = techage.get_mem(pos) mem.sched_idx = 0 end -- tFunc : (empty) table of functions -- call_rate : (2,4,8,16,32,64 or 128) -- offset : 0-128 -- func : function to be called function techage.scheduler.register(tFunc, call_rate, offset, func) local i= 0 while i < TABLE_SIZE do if (i % call_rate) == offset then i = add_to_table(tFunc, i, func) else i = i + 1 end end return tFunc end -- tFunc : table of functions -- default : default function (optional) -- Returns a function to be called be the callee function techage.scheduler.get(pos, tFunc, default) local mem = techage.get_mem(pos) mem.sched_idx = ((mem.sched_idx or 0) + 1) % TABLE_SIZE return tFunc[mem.sched_idx] or default or function() end end