diff --git a/api.lua b/api.lua index f786030..b11f5f3 100644 --- a/api.lua +++ b/api.lua @@ -13,56 +13,53 @@ -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . ---Stores liquid_id and the corresponding liquid node names -liquid_physics.registered_liquids = {} +local modpath = core.get_modpath(core.get_current_modname()) +local internal = dofile(modpath .. "/internal.lua") ---Stores all liquid node names and their corresponding liquid_id -local liquid_ids = {} - --- Returns liquid_id when node is a liquid, 0 when node is air --- and nil when node is anything else --- Note: 0 is not a valid ID +-- Returns liquid_id of the node or +-- nil when the node is not a liquid +-- @param node_name string Name of the node, e.g. "default:water_source" +-- @return number ID of the liquid function liquid_physics.get_liquid_id(node_name) - if node_name == "air" then - return 0 - end - return liquid_ids[node_name] + return internal.get_liquid_id(node_name) end ---Returns name of liquid node. Note: if liquid_level == 0 then "air" will be returned -function liquid_physics.get_liquid_node_name(liquid_id, liquid_level) - if liquid_level == 0 then - return "air" +-- Returns the liquid of the position specified or +-- nil when the node is not a liquid +-- @param pos table Position of the Node +-- @return table Table with liquid_id and liquid_level +function liquid_physics.get_liquid_at(pos) + local lpn = internal.new_lpn(core.hash_node_position(pos), pos) + if not lpn.liquid_id then + return nil end - - return liquid_physics.registered_liquids[liquid_id][liquid_level + 1] + return { liquid_id = lpn.liquid_id, liquid_level = lpn.liquid_level } end ---Returns the liquid level. If the node is air, 0 will be returned. --- -1 will be returned for any other node -function liquid_physics.get_liquid_level(liquid_id, node_name) - if node_name == "air" then - return 0 +-- Sets the liquid at the position specified +-- Returns true if the operation was successful +-- @param pos table Position of the Node +-- @param liquid_id number ID of the liquid +-- @param liquid_level number [0..8] Level of the liquid +-- @return bool Success +function liquid_physics.set_liquid_at(pos, liquid_id, liquid_level) + if liquid_level > 8 then + return false end - if liquid_physics.registered_liquids[liquid_id] == nil then - error(liquid_id) - end - for i = 1, 9 do - if node_name == liquid_physics.registered_liquids[liquid_id][i] then - return i - 1 - end - end - - return -1 + local lpn = internal.new_lpn(core.hash_node_position(pos), pos) + internal.set_lpn(liquid_id, lpn, liquid_level) + internal.add_node_to_check(pos) + return true end ---@param namespace string Name of mod eg. default ---@param name string Name of former liquid source --- Executing this function will override the liquid source specified. +-- Executing this function will override the nodes specified. -- New liquid sources will be "liquid_physics:namespace_name_level_1" through 7 +-- @param namespace string Name of mod e.g. "default" +-- @param source_name string Name of the liquid source, e.g. "water_source" +-- @param flowing_name string Name of the flowing liquid, e.g. "water_flowing" function liquid_physics.register_liquid(namespace, source_name, flowing_name) --Get next id - local id = table.getn(liquid_physics.registered_liquids) + 1 + local id = table.getn(liquid_physics._registered_liquids) + 1 --Get source definition local source_liquid_name = namespace .. ":" .. source_name @@ -89,7 +86,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name) source_liquid_def.liquid_alternative_source = source_liquid_name core.register_node(":" .. source_liquid_name, source_liquid_def) - liquid_ids[source_liquid_name] = id + liquid_physics._liquid_ids[source_liquid_name] = id local liquids = {} table.insert(liquids, "air") @@ -134,7 +131,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name) } core.register_node(node_name, level_def) - liquid_ids[node_name] = id + liquid_physics._liquid_ids[node_name] = id table.insert(liquids, node_name) end @@ -149,7 +146,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name) flowing_liquid_def.liquid_alternative_source = source_liquid_name core.register_node(":" .. flowing_liquid_name, flowing_liquid_def) - liquid_ids[flowing_liquid_name] = id + liquid_physics._liquid_ids[flowing_liquid_name] = id - liquid_physics.registered_liquids[id] = liquids + liquid_physics._registered_liquids[id] = liquids end diff --git a/init.lua b/init.lua index 6759771..ccb32e9 100644 --- a/init.lua +++ b/init.lua @@ -17,6 +17,14 @@ local modpath = core.get_modpath(core.get_current_modname()) liquid_physics = {} +--INTERNAL USE ONLY - Stores all nodes which need to checked +liquid_physics._nodes_to_check = {} +--INTERNAL USE ONLY - Stores liquid_id and the corresponding liquid node names +liquid_physics._registered_liquids = {} +--INTERNAL USE ONLY - Stores all liquid node names and their corresponding liquid_id +liquid_physics._liquid_ids = {} + +local internal = dofile(modpath .. "/internal.lua") dofile(modpath .. "/api.lua") if core.get_modpath("default") then @@ -29,4 +37,37 @@ if core.get_modpath("mcl_core") then liquid_physics.register_liquid("mcl_core", "lava_source", "lava_flowing") end +-- Remove "air" from registered_liquids for lbm and abm +local source_names = {} +for key, value in pairs(liquid_physics._registered_liquids) do + for i = 2, table.getn(value) do + table.insert(source_names, value[i]) + end +end + +core.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing) + if internal.get_liquid_id(core.get_node(pos).name) ~= nil then + internal.add_node_to_check(pos) + end +end) + +core.register_lbm { + name = "liquid_physics:update", + nodenames = source_names, + run_at_every_load = true, + action = function(pos, node, dtime_s) + internal.add_node_to_check(pos) + end, +} + +core.register_abm({ + nodenames = source_names, + neighbors = { "air" }, + interval = 0.2, + chance = 0, + action = function(pos, node, active_object_count, active_object_count_wider) + internal.add_node_to_check(pos) + end +}) + dofile(modpath .. "/physics.lua") diff --git a/internal.lua b/internal.lua new file mode 100644 index 0000000..42b1f4a --- /dev/null +++ b/internal.lua @@ -0,0 +1,113 @@ +-- Copyright (C) 2025 snoutie +-- Authors: snoutie (copyright@achtarmig.org) +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. + +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. + +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +local internal = {} + +-- Returns liquid_id when node is a liquid +-- 0 when node is air and +-- nil when node is anything else +-- Note: 0 is not a valid ID +-- @param node_name string The name of the node +-- @return number The ID of the liquid +function internal.get_liquid_id(node_name) + if node_name == "air" then + return 0 + end + return liquid_physics._liquid_ids[node_name] +end + +-- Returns name of liquid node or +-- "air" for a liquid level of 0 +-- @param liquid_id number The ID of the liquid +-- @param liquid_level number The level of the liquid +-- @return string Name of the liquid node +function internal.get_liquid_node_name(liquid_id, liquid_level) + if liquid_level == 0 then + return "air" + end + + return liquid_physics._registered_liquids[liquid_id][liquid_level + 1] +end + +-- Returns the liquid level of the current node, where: +-- 0 will be returned for air and +-- -1 will be returned for non-liquids. +-- @param liquid_id number The ID of the liquid +-- @param node_name string The name of the node +-- @returns number Level of liquid +function internal.get_liquid_level(liquid_id, node_name) + if node_name == "air" then + return 0 + end + if liquid_physics._registered_liquids[liquid_id] == nil then + error("Invalid liquid_id: " .. liquid_id) + end + for i = 1, 9 do + if node_name == liquid_physics._registered_liquids[liquid_id][i] then + return i - 1 + end + end + + return -1 +end + +-- Returns new Liquid Physics Node (LPN) +-- A LPN contains all necessary information for processing +-- A liquid_level of -1 or liquid_id of nil means, this node is not a liquid +-- @param hash string Hashed position of the node +-- @param pos table Position of the node +-- @return table LPN +function internal.new_lpn(hash, pos) + local node = core.get_node(pos) + local liquid_id = internal.get_liquid_id(node.name) + local liquid_level = -1 + if liquid_id then + liquid_level = internal.get_liquid_level(liquid_id, node.name) + end + return { + hash = hash, + pos = pos, + node_name = node.name, + liquid_id = liquid_id, + init_liquid_level = liquid_level, + liquid_level = liquid_level, + } +end + +-- Sets liquid_level and liquid_id of LPN +-- Change liquid_id for transmution +-- @param liquid_id number The new ID of the liquid +-- @param lpn table Liquid Physics Node +-- @param liquid_level number The new level of the liquid +function internal.set_lpn(liquid_id, lpn, liquid_level) + lpn.liquid_level = liquid_level + lpn.liquid_id = liquid_id +end + +-- This node will be checked in the next cycle +-- @param pos table Position of the node +function internal.add_node_to_check(pos) + local h = core.hash_node_position(pos) + liquid_physics._nodes_to_check[h] = pos +end + +-- This node will no longer be checked +-- @param pos table Position of the node +function internal.remove_node_to_check(pos) + local h = core.hash_node_position(pos) + liquid_physics._nodes_to_check[h] = nil +end + +return internal diff --git a/physics.lua b/physics.lua index d84b39b..62d5aa3 100644 --- a/physics.lua +++ b/physics.lua @@ -13,32 +13,23 @@ -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . +local modpath = core.get_modpath(core.get_current_modname()) +local internal = dofile(modpath .. "/internal.lua") + +-- A table of offsets for ease of use local offsets = { { x = 0, z = 1, }, { x = 0, z = -1, }, { x = 1, z = 0, }, { x = -1, z = 0, } } +-- Store number of offsets for less calculations local number_of_offsets = table.getn(offsets) ---Nodes which will be checked -local nodes_to_check = {} - ---Adds node to check -local function add_node_to_check(pos) - local h = core.hash_node_position(pos) - nodes_to_check[h] = pos -end - ---This node will no longer be checked --- ---"Freezes" node -local function remove_node_to_check(pos) - local h = core.hash_node_position(pos) - nodes_to_check[h] = nil -end - ---Adds offset to pos +-- Adds offset to pos +-- @param pos table Position +-- @param offset table Offset +-- @return table Position with added offset local function add_offset_to_pos(pos, offset) return { x = pos.x + offset.x, @@ -47,49 +38,35 @@ local function add_offset_to_pos(pos, offset) } end ---Returns new Liquid Physics Node (lpn) ---A LPN contains all necessary information for processing ---A liquid_level of -1 means, this node is not a liquid -local function new_lpn(h, pos) - local node = core.get_node(pos) - local liquid_id = liquid_physics.get_liquid_id(node.name) - local liquid_level = -1 - if liquid_id then - liquid_level = liquid_physics.get_liquid_level(liquid_id, node.name) - end - return { - hash = h, - pos = pos, - node_name = node.name, - liquid_id = liquid_id, - init_liquid_level = liquid_level, - liquid_level = liquid_level, - } -end - ---Sets liquid_level and liquid_id of LPN ---Allows for transmution -local function set_lpn(liquid_id, lpn, liquid_level) - lpn.liquid_level = liquid_level - lpn.liquid_id = liquid_id -end - ---Reduce calls to Engine and reduce calculations of liquid_level +-- Get a LPN from either the buffer or create a new one +-- and add it to the buffer +-- @param b table Buffered LPN +-- @param pos table Positon of the node +-- @return table LPN local function get_lpn_buffered(b, pos) local h = core.hash_node_position(pos) if b[h] ~= nil then return b[h] end - b[h] = new_lpn(h, pos) + b[h] = internal.new_lpn(h, pos) return b[h] end ---Checks if liquid_id of curr and lpn are the same. Also if the liquid_level is above -1 +-- Checks if liquid_id of curr and lpn are the same. +-- Checks if the liquid_level is above -1 +-- @param curr table The current LPN +-- @param lpn table The LPN to compare against +-- @return bool Is this LPN relevant local function is_lpn_relevant(curr, lpn) return lpn.liquid_level >= 0 and (lpn.liquid_level == 0 or curr.liquid_id == lpn.liquid_id) end ---Gets all adjacent liquid nodes with the same liquid id, or air nodes +-- Gets all adjacent liquid nodes with the same liquid_id as curr +-- Gets all adjacent air nodes +-- @param b table Buffered LPN +-- @param curr table The current LPN +-- @return table Table of neighboring LPN +-- @return number Number of neighbors local function get_valid_neighbors(b, curr) local neighbors = {} local number_of_neighbors = 0 @@ -105,8 +82,11 @@ local function get_valid_neighbors(b, curr) return neighbors, number_of_neighbors end ---Gets neighbors of curr and calculates the pressure ---This function combines get_valid_neighbors and get_pressure_straight +-- Gets neighbors of curr and calculates the pressure +-- This function combines get_valid_neighbors and get_pressure_straight +-- @param b table Buffered LPN +-- @param curr table The current LPN +-- @return float Pressure of curr calculated from valid neighbors local function get_valid_neigbor_pressure(b, curr) local number_of_neighbors = 0 local total_liquid_level = curr.liquid_level @@ -123,6 +103,11 @@ local function get_valid_neigbor_pressure(b, curr) end -- Gets the pressure of curr from the neighbors given +-- @param b table Buffered LPN +-- @param neighbors table Table of neighboring LPN +-- @param number_of_neighbors number Number of neighbors +-- @param curr table The current LPN +-- @return float Pressure of curr calculated from neighbors local function get_pressure_straight(b, neighbors, number_of_neighbors, curr) local total_liquid_level = curr.liquid_level @@ -132,9 +117,12 @@ local function get_pressure_straight(b, neighbors, number_of_neighbors, curr) return total_liquid_level / (number_of_neighbors + 1) end ---Tries to move amount from "from" to "to" --- ---Returns amount moved +-- Tries to move amount from "from" to "to" +-- Actual amount moved may not be the same as "amount" +-- therefore the actual amount moved is returned +-- @param from table LPN from which amount is deducted +-- @param to table LPN to which amount is added +-- @return number Actual amount moved local function try_move(from, to, amount) if to.liquid_level >= 8 then return 0 @@ -142,8 +130,8 @@ local function try_move(from, to, amount) local max_allowed = amount + 8 - (to.liquid_level + amount) local max_allowed_clamped = math.min(max_allowed, amount, from.liquid_level) - set_lpn(from.liquid_id, from, from.liquid_level - max_allowed_clamped) - set_lpn(from.liquid_id, to, to.liquid_level + max_allowed_clamped) + internal.set_lpn(from.liquid_id, from, from.liquid_level - max_allowed_clamped) + internal.set_lpn(from.liquid_id, to, to.liquid_level + max_allowed_clamped) if max_allowed_clamped ~= amount then return max_allowed_clamped @@ -151,7 +139,9 @@ local function try_move(from, to, amount) return amount end ---Moves curr_pos +-- Calculates how to move curr_pos +-- @param b table Buffered LPN +-- @param curr_pos table Current position local function move(b, curr_pos) local curr_nbs local curr_nnbs @@ -162,7 +152,7 @@ local function move(b, curr_pos) local curr_lpn = get_lpn_buffered(b, curr_pos) if curr_lpn.liquid_level <= 0 then - remove_node_to_check(curr_lpn.pos) + internal.remove_node_to_check(curr_lpn.pos) return end @@ -173,101 +163,69 @@ local function move(b, curr_pos) if moved > 0 then up_lpn = get_lpn_buffered(b, { x = curr_pos.x, y = curr_pos.y + 1, z = curr_pos.z }) if up_lpn.liquid_level >= 0 then - add_node_to_check(up_lpn.pos) + internal.add_node_to_check(up_lpn.pos) end - add_node_to_check(down_lpn.pos) + internal.add_node_to_check(down_lpn.pos) end if moved == curr_lpn.liquid_level then - remove_node_to_check(curr_lpn.pos) + internal.remove_node_to_check(curr_lpn.pos) for i = 1, curr_nnbs do - add_node_to_check(curr_nbs[i].pos) + internal.add_node_to_check(curr_nbs[i].pos) end return end end - --Every neighbor will be higher (except for air) so processing is done + -- Every neighbor will be higher (except for air) so processing is done if curr_lpn.liquid_level <= 1 then - remove_node_to_check(curr_lpn.pos) + internal.remove_node_to_check(curr_lpn.pos) return end local curr_prs = get_pressure_straight(b, curr_nbs, curr_nnbs, curr_lpn) if curr_prs >= 4.8 then --4.8 == ((8*4/2)+8)/5 -> meaning maximum pressure, implies no way to move - remove_node_to_check(curr_lpn.pos) + internal.remove_node_to_check(curr_lpn.pos) return end - --TODO: Understand what is happening here and optimize + -- TODO: Understand what is happening here and optimize local number_of_swaps = 0 for i = 1, curr_nnbs do local next_lpn = curr_nbs[i] local next_prs = get_valid_neigbor_pressure(b, next_lpn) if curr_prs > next_prs and next_lpn.liquid_level < curr_lpn.liquid_level then if try_move(curr_lpn, next_lpn, 1) > 0 then - add_node_to_check(next_lpn.pos) - add_node_to_check(get_lpn_buffered(b, { x = curr_pos.x, y = curr_pos.y + 1, z = curr_pos.z }).pos) + internal.add_node_to_check(next_lpn.pos) + internal.add_node_to_check( + get_lpn_buffered(b, { x = curr_pos.x, y = curr_pos.y + 1, z = curr_pos.z }).pos + ) number_of_swaps = number_of_swaps + 1 end elseif curr_lpn.liquid_level < next_lpn.liquid_level then - add_node_to_check(next_lpn.pos) + internal.add_node_to_check(next_lpn.pos) end end if number_of_swaps == 0 then - remove_node_to_check(curr_lpn.pos) + internal.remove_node_to_check(curr_lpn.pos) end end ---Get placed liquids -core.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing) - if liquid_physics.get_liquid_id(core.get_node(pos).name) ~= nil then - add_node_to_check(pos) - end -end) - ---Remove "air" from registered_liquids for lbm and abm -local source_names = {} -for key, value in pairs(liquid_physics.registered_liquids) do - for i = 2, table.getn(value) do - table.insert(source_names, value[i]) - end -end - -core.register_lbm { - name = "liquid_physics:update", - nodenames = source_names, - run_at_every_load = true, - action = function(pos, node, dtime_s) - add_node_to_check(pos) - end, -} - -core.register_abm({ - nodenames = source_names, - neighbors = { "air" }, - interval = 0.2, - chance = 0, - action = function(pos, node, active_object_count, active_object_count_wider) - add_node_to_check(pos) - end -}) - core.register_on_mapblocks_changed(function(modified_blocks, modified_blocks_count) - --Buffer + -- Buffered LPN local b = {} - for hpos, pos in pairs(nodes_to_check) do + for hpos, pos in pairs(liquid_physics._nodes_to_check) do if core.compare_block_status(pos, "active") then move(b, pos) else - remove_node_to_check(pos) + internal.remove_node_to_check(pos) end end - --Update only changed nodes from buffer + -- Update only changed nodes from buffer for _, lpn in pairs(b) do if lpn.init_liquid_level ~= lpn.liquid_level then - core.set_node(lpn.pos, { name = liquid_physics.get_liquid_node_name(lpn.liquid_id, lpn.liquid_level) }) + core.set_node(lpn.pos, { name = internal.get_liquid_node_name(lpn.liquid_id, lpn.liquid_level) }) end end end)