--[[ Tube Library ============ Copyright (C) 2017-2020 Joachim Stolberg AGPL v3 See LICENSE.txt for more information command.lua: See [api.md] for the interface documentation ]]-- --- for lazy programmers local S = function(pos) if pos then return minetest.pos_to_string(pos) end end local P = minetest.string_to_pos local M = minetest.get_meta local function deserialize(s) local tbl = {} for line in s:gmatch("[^;]+") do local num, spos = unpack(string.split(line, "=")) tbl[num] = {pos = minetest.string_to_pos(spos)} end return tbl end local function serialize(data) local tbl = {} for k,v in pairs(data) do tbl[#tbl+1] = k.."="..minetest.pos_to_string(v.pos) end return table.concat(tbl, ";") end -- to convert the data base from v2 to v3 local function extract_data(data) local tbl = {} for num,item in pairs(data) do local new_item = {} local data_present = false for k,v in pairs(item) do if k ~= "name" and k ~= "pos" then new_item[k] = v data_present = true end end if data_present then tbl[num] = new_item end end return tbl end ------------------------------------------------------------------ -- Data base storage ------------------------------------------------------------------- local storage = minetest.get_mod_storage() local NextNumber = minetest.deserialize(storage:get_string("NextNumber")) or 1 local Version = minetest.deserialize(storage:get_string("Version")) or 1 local Number2Pos local Number2Data local TemporaryRemovedNodes if Version == 2 then -- legacy data base Number2Pos = minetest.deserialize(storage:get_string("Number2Pos")) or {} Number2Data = extract_data(Number2Pos) TemporaryRemovedNodes = {} Version = 3 else Number2Pos = deserialize(storage:get_string("Number2Pos")) Number2Data = minetest.deserialize(storage:get_string("Number2Data")) or {} TemporaryRemovedNodes = minetest.deserialize(storage:get_string("TemporaryRemovedNodes")) or {} end local function update_mod_storage() local t = minetest.get_us_time() minetest.log("action", "[Tubelib] Store data...") storage:set_string("NextNumber", minetest.serialize(NextNumber)) storage:set_string("Version", minetest.serialize(Version)) storage:set_string("Number2Pos", serialize(Number2Pos)) storage:set_string("Number2Data", minetest.serialize(Number2Data)) storage:set_string("TemporaryRemovedNodes", minetest.serialize(TemporaryRemovedNodes)) -- store data each hour minetest.after(60*59, update_mod_storage) t = minetest.get_us_time() - t minetest.log("action", "[Tubelib] Data stored. t="..t.."us") end minetest.register_on_shutdown(function() update_mod_storage() end) -- store data after one hour minetest.after(60*59, update_mod_storage) -- Key2Number will be generated at runtine local Key2Number = {} local Name2Name = {} -- translation table ------------------------------------------------------------------- -- Local helper functions ------------------------------------------------------------------- -- Localize functions to avoid table lookups (better performance). local string_find = string.find local string_split = string.split local tubelib_NodeDef = tubelib.NodeDef local Tube = tubelib.Tube -- Generate a key string based on the given pos table, -- Used internaly as table key, local function get_key_str(pos) pos = minetest.pos_to_string(pos) return string.sub(pos, 2, -2) end -- Determine position related node number for addressing purposes local function get_number(pos) local key = get_key_str(pos) if not Key2Number[key] then Key2Number[key] = NextNumber NextNumber = NextNumber + 1 end return string.format("%.04u", Key2Number[key]) end local function 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:index(pos.x, pos.y, pos.z) node = { name = minetest.get_name_from_content_id(data[idx]), param2 = param2_data[idx] } return node end local function generate_Key2Number() local key for num,item in pairs(Number2Pos) do key = get_key_str(item.pos) Key2Number[key] = num end end local function not_protected(pos, placer_name, clicker_name) local meta = minetest.get_meta(pos) if meta then local cached_name = meta:get_string("tubelib_cached_name") if placer_name and (placer_name == cached_name or not minetest.is_protected(pos, placer_name)) then meta:set_string("tubelib_cached_name", placer_name) if clicker_name == nil or not minetest.is_protected(pos, clicker_name) then return true end end end return false end local function register_lbm(name, nodenames) minetest.register_lbm({ label = "[Tubelib] Node update", name = name.."update", nodenames = nodenames, run_at_every_load = true, action = function(pos, node) local name = Name2Name[node.name] if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_node_load then tubelib_NodeDef[name].on_node_load(pos) end end }) end local DirToSide = {"B", "R", "F", "L", "D", "U"} local function dir_to_side(dir, param2) if dir < 5 then dir = (((dir - 1) - (param2 % 4)) % 4) + 1 end return DirToSide[dir] end local SideToDir = {B=1, R=2, F=3, L=4, D=5, U=6} local function side_to_dir(side, param2) local dir = SideToDir[side] if dir < 5 then dir = (((dir - 1) + (param2 % 4)) % 4) + 1 end return dir end local function get_dest_node(pos, side) local _,node = Tube:get_node(pos) local dir = side_to_dir(side, node.param2) local spos, sdir = Tube:get_connected_node_pos(pos, dir) if not (spos and sdir) then return end _,node = Tube:get_node(spos) local out_side = dir_to_side(tubelib2.Turn180Deg[sdir], node.param2) return spos, out_side, Name2Name[node.name] or node.name end local function item_handling_node(name) local node_def = name and tubelib_NodeDef[name] if node_def then return node_def.on_pull_item or node_def.on_push_item or node_def.is_pusher end end ------------------------------------------------------------------- -- API helper functions ------------------------------------------------------------------- -- Check the given list of numbers. -- Returns true if number(s) is/are valid and point to real nodes. function tubelib.check_numbers(numbers) if numbers then for _,num in ipairs(string_split(numbers, " ")) do if Number2Pos[num] == nil then return false end end return true end return false end -- Function returns { pos, name } for the node on the given position number. function tubelib.get_node_info(dest_num) if Number2Pos[dest_num] then return Number2Pos[dest_num] end return nil end -- Function returns the node number from the given position or -- nil, if no node number for this position is assigned. function tubelib.get_node_number(pos) local key = get_key_str(pos) local num = Key2Number[key] if num then num = string.format("%.04u", num) if Number2Pos[num] and Number2Pos[num].name then return num end end return nil end -- Function is used for available nodes with lost numbers, only. function tubelib.get_new_number(pos, name) -- store position local number = get_number(pos) Number2Pos[number] = { pos = pos, name = name, } return number end -- Store any node number related, additional data -- param number: node number, returned by tubelib.add_node -- param name: name of the data (string) -- param data: any data (number, string, table) function tubelib.set_data(number, name, data) Number2Data[number] = Number2Data[number] or {} if type(name) == "string" then Number2Data[number]["u_"..name] = data end end -- Read node number related data -- param number: node number, returned by tubelib.add_node -- param name: name of the data (string) function tubelib.get_data(number, name) if Number2Data[number] and type(name) == "string" then return Number2Data[number]["u_"..name] end return nil end ------------------------------------------------------------------- -- Node construction/destruction functions ------------------------------------------------------------------- -- Add node to the tubelib lists. -- Function determines and returns the node position number, -- needed for message communication. function tubelib.add_node(pos, name) if item_handling_node(name) then Tube:after_place_node(pos) end -- store position local number = get_number(pos) Number2Pos[number] = { pos = pos, name = name, } return number end -- Function removes the node from the tubelib lists. function tubelib.remove_node(pos) local number = get_number(pos) local name if Number2Pos[number] then name = Number2Pos[number].name Number2Pos[number] = { pos = pos, name = nil, time = minetest.get_day_count() -- used for reservation timeout } end if item_handling_node(name) then Tube:after_dig_node(pos) end end ------------------------------------------------------------------- -- Node register function ------------------------------------------------------------------- -- Register node for tubelib communication -- Call this function only at load time! -- Param name: The node name like "tubelib:pusher" -- Param add_names: Alternativ node names if needded, e.g.: "tubelib:pusher_active" -- Param node_definition: A table according to: -- { -- on_pull_item = func(pos, side, player_name), -- on_push_item = func(pos, side, item, player_name), -- on_unpull_item = func(pos, side, item, player_name), -- on_recv_message = func(pos, topic, payload), -- on_node_load = func(pos), -- LBM function -- on_node_repair = func(pos), -- repair defect (feature!) nodes -- } function tubelib.register_node(name, add_names, node_definition) tubelib_NodeDef[name] = node_definition -- store facedir table for all known node names Name2Name[name] = name for _,n in ipairs(add_names) do Name2Name[n] = name end if node_definition.on_pull_item or node_definition.on_push_item or node_definition.is_pusher then Tube:add_secondary_node_names({name}) Tube:add_secondary_node_names(add_names) tubelib.KnownNodes[name] = true for _,n in ipairs(add_names) do tubelib.KnownNodes[n] = true end end -- register LBM if node_definition.on_node_load then local nodenames = {name} for _,n in ipairs(add_names) do nodenames[#nodenames + 1] = n end register_lbm(name, nodenames) end end ------------------------------------------------------------------- -- Send message functions ------------------------------------------------------------------- function tubelib.send_message(numbers, placer_name, clicker_name, topic, payload) for _,num in ipairs(string_split(numbers, " ")) do if Number2Pos[num] and Number2Pos[num].name then local data = Number2Pos[num] if not_protected(data.pos, placer_name, clicker_name) then if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload) end end end end end function tubelib.send_request(number, topic, payload) if Number2Pos[number] and Number2Pos[number].name then local data = Number2Pos[number] if tubelib_NodeDef[data.name] and tubelib_NodeDef[data.name].on_recv_message then return tubelib_NodeDef[data.name].on_recv_message(data.pos, topic, payload) end end return false end -- for defect nodes function tubelib.repair_node(pos) local node = minetest.get_node(pos) local name = Name2Name[node.name] if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_node_repair then return tubelib_NodeDef[name].on_node_repair(pos) end return false end ------------------------------------------------------------------- -- Client side Push/Pull item functions ------------------------------------------------------------------- function tubelib.pull_items(pos, side, player_name) local npos, nside, name = get_dest_node(pos, side) if npos == nil then return end if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_pull_item then return tubelib_NodeDef[name].on_pull_item(npos, nside, player_name) end return nil end function tubelib.push_items(pos, side, items, player_name) local npos, nside, name = get_dest_node(pos, side) if npos == nil then return end if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_push_item then return tubelib_NodeDef[name].on_push_item(npos, nside, items, player_name) elseif name == "air" then minetest.add_item(npos, items) return true end return false end function tubelib.unpull_items(pos, side, items, player_name) local npos, nside, name = get_dest_node(pos, side) if npos == nil then return end if tubelib_NodeDef[name] and tubelib_NodeDef[name].on_unpull_item then return tubelib_NodeDef[name].on_unpull_item(npos, nside, items, player_name) end return false end function tubelib.pull_stack(pos, side, player_name) local npos, nside, name = get_dest_node(pos, side) if npos == nil then return end if tubelib_NodeDef[name] then if tubelib_NodeDef[name].on_pull_stack then return tubelib_NodeDef[name].on_pull_stack(npos, nside, player_name) elseif tubelib_NodeDef[name].on_pull_item then return tubelib_NodeDef[name].on_pull_item(npos, nside, player_name) end end return nil end ------------------------------------------------------------------- -- Server side helper functions ------------------------------------------------------------------- -- Get one item from the given ItemList. The position within the list -- is incremented each time so that different item stacks will be considered. -- Returns nil if ItemList is empty. function tubelib.get_item(meta, listname) if meta == nil or meta.get_inventory == nil then return nil end local inv = meta:get_inventory() if inv:is_empty(listname) then return nil end local size = inv:get_size(listname) local startpos = meta:get_int("tubelib_startpos") or 0 for idx = startpos, startpos+size do idx = (idx % size) + 1 local items = inv:get_stack(listname, idx) if items:get_count() > 0 then local taken = items:take_item(1) inv:set_stack(listname, idx, items) meta:set_int("tubelib_startpos", idx) return taken end end meta:set_int("tubelib_startpos", 0) return nil end -- Get one (or more) item(s) from the given ItemList, specified by stack number (1..n). -- Returns nil if ItemList is empty. function tubelib.get_this_item(meta, listname, list_number, num_items) if meta == nil or meta.get_inventory == nil then return nil end local inv = meta:get_inventory() if inv:is_empty(listname) then return nil end if num_items == nil then num_items = 1 end local items = inv:get_stack(listname, list_number) if items:get_count() > 0 then local taken = items:take_item(num_items) inv:set_stack(listname, list_number, items) return taken end return nil end -- Put the given item into the given ItemList. -- Function returns false if ItemList is full. function tubelib.put_item(meta, listname, item) if meta == nil or meta.get_inventory == nil then return false end local inv = meta:get_inventory() if inv:room_for_item(listname, item) then inv:add_item(listname, item) return true end return false end -- Take the number of items from the given ItemList. -- Returns nil if the requested number is not available. function tubelib.get_num_items(meta, listname, num) if meta == nil or meta.get_inventory == nil then return nil end local inv = meta:get_inventory() if inv:is_empty(listname) then return nil end local size = inv:get_size(listname) for idx = 1, size do local items = inv:get_stack(listname, idx) if items:get_count() >= num then local taken = items:take_item(num) inv:set_stack(listname, idx, items) return taken end end return nil end function tubelib.get_stack(meta, listname) local inv = meta:get_inventory() local item = tubelib.get_item(meta, listname) if item and item:get_stack_max() > 1 and inv:contains_item(listname, item) then -- try to remove a complete stack item:set_count(math.min(98, item:get_stack_max() - 1)) local taken = inv:remove_item(listname, item) -- add the already removed taken:set_count(taken:get_count() + 1) return taken end return item end -- Return "full", "loaded", or "empty" depending -- on the number of fuel stack items. -- Function only works on fuel inventories with one stacks/99 items function tubelib.fuelstate(meta, listname, item) if meta == nil or meta.get_inventory == nil then return nil end local inv = meta:get_inventory() if inv:is_empty(listname) then return "empty" end local list = inv:get_list(listname) if #list == 1 and list[1]:get_count() == 99 then return "full" else return "loaded" end end -- Return "full", "loaded", or "empty" depending -- on the inventory load. -- Full is returned, when no empty stack is available. function tubelib.get_inv_state(meta, listname) if meta == nil or meta.get_inventory == nil then return nil end local inv = meta:get_inventory() local state if inv:is_empty(listname) then state = "empty" else local list = inv:get_list(listname) state = "full" local num = 0 for i, item in ipairs(list) do if item:is_empty() then return "loaded" end end end return state end -- Mainly used for door/gate nodes -- To delete an entry, provide nil as number. The stored data will be returned. function tubelib.temporary_remove_node(pos, number, name, add_data) local key = get_key_str(pos) if number then add_data = add_data or {} add_data.pos = pos add_data.number = number add_data.name = name TemporaryRemovedNodes[key] = add_data else if TemporaryRemovedNodes[key] then local data = table.copy(TemporaryRemovedNodes[key]) TemporaryRemovedNodes[key] = nil return data end end end ------------------------------------------------------------------------------- -- Data Maintenance ------------------------------------------------------------------------------- local function parse_number(s) for _,word in ipairs(s:split(" ")) do local n = tonumber(word) if n and n > 0 then return word end end end local function get_node_number(pos) local meta = M(pos) local num = meta:get_string("tubelib_number") if num and num ~= "" then return num end num = meta:get_string("number") if num and num ~= "" then return num end num = meta:get_string("own_number") if num and num ~= "" then return num end num = parse_number(meta:get_string("infotext")) if num and num ~= "" then return num end return 0 end local function data_maintenance() minetest.log("info", "[Tubelib] Data maintenance started") -- Remove unused positions local tbl = table.copy(Number2Pos) Number2Pos = {} local cnt1 = 0 local cnt2 = 0 for num,item in pairs(tbl) do local name = Name2Name[get_node_lvm(item.pos).name] cnt1 = cnt1 + 1 -- Is there a tubelib node? if tubelib_NodeDef[name] then -- Does the number match? local nnum = get_node_number(item.pos) if nnum == num then cnt2 = cnt2 + 1 -- Store again Number2Pos[num] = item -- Add node names which are not stored as file Number2Pos[num].name = name --print("added", num, name) else --print("wrong number", num, name) end else local key = get_key_str(item.pos) local data = TemporaryRemovedNodes[key] if data then cnt2 = cnt2 + 1 -- Store again Number2Pos[data.number] = data --print("restored", data.number, data.name) else --print("no data", num) end end end minetest.log("info", "[Tubelib] Data base shrank from "..cnt1.." to "..cnt2.." nodes") minetest.log("info", "[Tubelib] Data maintenance finished") end generate_Key2Number() -- maintain data after 2 seconds minetest.after(2, data_maintenance)