Compare commits

...

7 Commits
0.2.1 ... main

Author SHA1 Message Date
c60873e80f Workaround Multiple Sources for VoxeLibre 2025-01-13 21:13:54 +01:00
1e606df0b4 Settings Default to True 2025-01-13 21:12:51 +01:00
6a021676b9 Enable Wear for Buckets 2025-01-13 20:39:57 +01:00
606a215f77 Update README to Inlcude New Features 2025-01-11 23:02:29 +01:00
b6b2ba4672 Update License 2025-01-11 22:59:24 +01:00
ed7e26fb8a Add Bucket Support 2025-01-11 22:57:44 +01:00
2d6c43e69f Update API and Add Settings 2025-01-08 16:51:57 +01:00
10 changed files with 483 additions and 29 deletions

View File

@ -6,16 +6,17 @@ This mod is in early alpha.
## What NOT to expect
- Proper water equalization and water pressure
- Removing weeds and other wall mounted items
- Interaction of different liquid types, e.g. lava and water
- Ability to scoop up liquids with a level below 8
- Proper water equalization and water pressure *
- Removing weeds and other wall mounted items *
- Interaction of different liquid types, e.g. lava and water *
- Interaction with pistons or pipes
- Perfect performance
- Integration with any other mods
- Integration with any other mods *
- A smooth liquid surface
- Viscosity for different liquid types
* to an extend
## What to expect
- Water leveling for flat surfaces
@ -30,6 +31,9 @@ This mod is in early alpha.
--Example: Register default:water_source
liquid_physics.register_liquid("default", "water_source", "water_flowing")
--Example: Register bucket:bucket_water to scoop up liquids
liquid_physics.register_bucket("bucket:bucket_water")
--Example: Check if block underneath is liquid and then proceed to reduce its level
local id_water = liquid_physics.get_liquid_id("default:water_source")
local liquid = liquid_physics.get_liquid_at(pos)

32
api.lua
View File

@ -15,6 +15,7 @@
local modpath = core.get_modpath(core.get_current_modname())
local internal = dofile(modpath .. "/internal.lua")
local internal_bucket = dofile(modpath .. "/bucket.lua")
-- Returns liquid_id of the node or
-- nil when the node is not a liquid
@ -40,6 +41,18 @@ function liquid_physics.get_liquid_at(pos)
return { liquid_id = lpn.liquid_id, liquid_level = lpn.liquid_level }
end
-- Returns the node names for a liquid ordered by the corresponding liquid level
-- Useful if you want to register an ABM or LBM
-- @param liquid_id number ID of the liquid
-- @return table Table of liquid node names for that id
function liquid_physics.get_liquid_node_names(liquid_id)
local nodes = {}
for i = 2, 9 do
table.insert(nodes, liquid_physics._registered_liquids[liquid_id][i])
end
return nodes
end
-- Sets the liquid at the position specified
-- Returns true if the operation was successful
-- @param pos table Position of the Node
@ -52,10 +65,18 @@ function liquid_physics.set_liquid_at(pos, liquid_id, liquid_level)
end
local lpn = internal.new_lpn(core.hash_node_position(pos), pos)
internal.set_lpn(liquid_id, lpn, liquid_level)
internal.set_node(lpn)
internal.add_node_to_check(pos)
return true
end
-- This function will override the bucket provided to add the capapability
-- of transporting liquids of factional size
-- @param name string Name of the bucket, e.g. "bucket:bucket:water"
function liquid_physics.register_bucket(name)
internal_bucket.register_filled_bucket(name)
end
-- 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"
@ -85,6 +106,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name)
--Overwrite source
source_liquid_def.liquid_range = 0
source_liquid_def.groups.liquid_physics = 1
core.register_node(":" .. source_liquid_name, source_liquid_def)
liquid_physics._liquid_ids[source_liquid_name] = id
@ -96,6 +118,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name)
local node_name = "liquid_physics:" .. namespace .. "_" .. source_name .. "_level_" .. i
local level_def = {
name = node_name,
description = source_liquid_def.description .. " Level " .. i,
tiles = source_liquid_def.tiles,
use_texture_alpha = source_liquid_def.use_texture_alpha,
@ -131,7 +154,7 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name)
sounds = source_liquid_def.sounds,
}
core.register_node(node_name, level_def)
core.register_node(":" .. node_name, level_def)
liquid_physics._liquid_ids[node_name] = id
table.insert(liquids, node_name)
@ -140,7 +163,12 @@ function liquid_physics.register_liquid(namespace, source_name, flowing_name)
table.insert(liquids, source_liquid_name)
-- Finally, stop flowing
core.override_item(flowing_liquid_name, { liquid_range = 0, liquid_renewable = false }, nil)
flowing_liquid_def.groups.liquid_physics = 1
core.override_item(flowing_liquid_name, {
liquid_range = 0,
liquid_renewable = false,
groups = flowing_liquid_def.groups
}, nil)
liquid_physics._liquid_ids[flowing_liquid_name] = id
liquid_physics._registered_liquids[id] = liquids

362
bucket.lua Normal file
View File

@ -0,0 +1,362 @@
-- 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 <https://www.gnu.org/licenses/>.
local internal_bucket = {}
local wear_levels = {}
wear_levels[1] = 65535
wear_levels[2] = 57337
wear_levels[3] = 49146
wear_levels[4] = 40955
wear_levels[5] = 32764
wear_levels[6] = 24573
wear_levels[7] = 16382
wear_levels[8] = 8191
wear_levels[9] = 0
local base_mcl_buckets = core.get_modpath("mcl_buckets")
local base_default = core.get_modpath("default")
local function check_protection(pos, name, text)
if core.is_protected(pos, name) then
core.log("action", (name ~= "" and name or "A mod")
.. " tried to " .. text
.. " at protected position "
.. core.pos_to_string(pos)
.. " with a bucket")
core.record_protection_violation(pos, name)
return true
end
return false
end
function internal_bucket.get_wear(liquid_level)
return wear_levels[liquid_level + 1]
end
function internal_bucket.get_liquid_level(wear)
for i = 1, 9 do
if wear_levels[i] == wear then
return i - 1
end
end
end
local function set_bucket_item_liquid_level(item, liquid_level)
local meta = item:get_meta()
meta:set_string("description",
ItemStack(item:get_name()):get_description() .. " " .. liquid_level .. "/8")
item:set_wear(internal_bucket.get_wear(liquid_level))
end
local function mcl_get_pointed_thing(usr)
local start = usr:get_pos()
start.y = start.y + usr:get_properties().eye_height
local look_dir = usr:get_look_dir()
local _end = vector.add(start, vector.multiply(look_dir, 5))
local ray = core.raycast(start, _end, false, true)
for pointed_thing in ray do
local name = core.get_node(pointed_thing.under).name
local def = core.registered_nodes[name]
if not def or def.drawtype ~= "flowingliquid" then
return pointed_thing
end
end
end
local function get_bucket_name_empty()
if base_default then
return "bucket:bucket_empty"
elseif base_mcl_buckets then
return "mcl_buckets:bucket_empty"
end
return nil
end
local function get_bucket_name_filled(source_name)
if base_default then
return bucket.liquids[source_name].itemname
elseif base_mcl_buckets then
return mcl_buckets.liquids[source_name].bucketname
end
return nil
end
local function get_liquid_name(bucket_name)
if base_default then
for source, b in pairs(bucket.liquids) do
if b.itemname and b.itemname == bucket_name then
return b.source
end
end
elseif base_mcl_buckets then
-- TODO: How Should I handle this then...
return mcl_buckets.buckets[bucket_name].source_take[1]
end
return nil
end
local function bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
if pointed_thing.type == "object" then
pointed_thing.ref:punch(user, 1.0, { full_punch_interval = 1.0 }, nil)
return user:get_wielded_item()
elseif pointed_thing.type ~= "node" then
return
end
local held_bucket_name = itemstack:get_name()
local is_bucket_filled = held_bucket_name ~= get_bucket_name_empty()
local bucket_contents = is_bucket_filled and get_liquid_name(held_bucket_name)
local bucket_fill_level = internal_bucket.get_liquid_level(itemstack:get_wear())
local liquid = liquid_physics.get_liquid_at(pointed_thing.under)
-- Bucket is filled and pointed at liquid is not registered
-- -> Don't scoop
if bucket_contents and liquid == nil then
return itemstack
end
-- Pointed at liquid is not registered
-- -> Scoop via fallback
if liquid == nil then
return on_use_fallback(itemstack, user, pointed_thing)
end
-- Bucket is filled and pointed at liquid is of different kind
-- -> Don't scoop
local liquid_source_name = liquid_physics.get_liquid_node_names(liquid.liquid_id)[8]
if bucket_contents and bucket_contents ~= liquid_source_name then
return itemstack
end
local node = core.get_node(pointed_thing.under)
if check_protection(pointed_thing.under,
user:get_player_name(),
"take " .. node.name) then
return
end
local bucket_name_filled = get_bucket_name_filled(liquid_source_name)
-- Don't know how to handle
-- -> Fallback
if bucket_name_filled == nil then
return on_use_fallback(itemstack, user, pointed_thing)
end
local item_bucket = ItemStack(bucket_name_filled)
if not is_bucket_filled then
set_bucket_item_liquid_level(item_bucket, liquid.liquid_level)
local item_count = user:get_wielded_item():get_count()
if item_count > 1 then
local inv = user:get_inventory()
if inv:room_for_item("main", { name = bucket_name_filled }) then
inv:add_item("main", item_bucket)
else
local pos = user:getpos()
pos.y = math.floor(pos.y + 0.5)
core.add_item(pos, item_bucket)
end
-- set to return empty buckets minus 1
item_bucket = ItemStack(get_bucket_name_empty() .. tostring(item_count - 1))
end
liquid_physics.set_liquid_at(pointed_thing.under, 0, 0)
return item_bucket
else
local give_amount = math.min(8 - bucket_fill_level, liquid.liquid_level)
set_bucket_item_liquid_level(item_bucket, bucket_fill_level + give_amount)
liquid_physics.set_liquid_at(pointed_thing.under, liquid.liquid_id, liquid.liquid_level - give_amount)
return item_bucket
end
end
local function bucket_on_place(on_place_fallback, bucket_liquid_id, source_name, itemstack, user, pointed_thing)
-- Must be pointing to node
if pointed_thing.type ~= "node" then
return
end
local node = core.get_node(pointed_thing.under)
local node_def = core.registered_nodes[node.name]
if not node_def then
return itemstack
end
-- Call on_rightclick if the pointed node defines it
if node_def.on_rightclick and
not (user and user:is_player() and
user:get_player_control().sneak) then
return node_def.on_rightclick(
pointed_thing.under,
node, user,
itemstack)
end
-- Where to place the liquid at
local place_at_pos
if node_def.buildable_to then
place_at_pos = pointed_thing.under
else
place_at_pos = pointed_thing.above
node = core.get_node(place_at_pos)
local node_above_def = core.registered_nodes[node.name]
if not node_above_def or not node_above_def.buildable_to then
return itemstack
end
end
if check_protection(place_at_pos, user
and user:get_player_name()
or "", "place " .. source_name) then
return
end
local liquid_level = internal_bucket.get_liquid_level(itemstack:get_wear())
local liquid = liquid_physics.get_liquid_at(place_at_pos)
if liquid == nil then
if liquid_physics.set_liquid_at(place_at_pos, bucket_liquid_id, liquid_level) then
return ItemStack(get_bucket_name_empty())
end
return itemstack
end
if liquid.liquid_id == bucket_liquid_id then
local give_amount = math.min(8 - liquid.liquid_level, liquid_level)
if liquid_physics.set_liquid_at(place_at_pos, bucket_liquid_id, liquid.liquid_level + give_amount) then
if give_amount == liquid_level then
return ItemStack(get_bucket_name_empty())
end
set_bucket_item_liquid_level(itemstack, liquid_level - give_amount)
return itemstack
end
return itemstack
end
end
function internal_bucket.register_empty_bucket(bucket_name)
local bucket_tool = core.registered_items[bucket_name]
if base_default then
local on_use_fallback = bucket_tool.on_use
core.override_item(bucket_name, {
on_use = function(itemstack, user, pointed_thing)
return bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
end
}, nil)
elseif base_mcl_buckets then
local on_use_fallback = bucket_tool.on_place
if core.settings:get_bool("liquid_physics_voxelibre_enable_scooping_via_use", true) then
core.override_item(bucket_name, {
on_use = function(itemstack, user, pointed_thing)
return bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
end
}, nil)
end
core.override_item(bucket_name, {
on_place = function(itemstack, user, pointed_thing)
local use_select_box = core.settings:get_bool("mcl_buckets_use_select_box", false)
if use_select_box == false then
-- TODO: Understand why this is nil
if user.get_pos == nil then
return itemstack
end
pointed_thing = mcl_get_pointed_thing(user)
end
return bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
end
}, nil)
end
end
function internal_bucket.register_filled_bucket(name)
local bucket_tool = core.registered_items[name]
local source_name = get_liquid_name(name)
if source_name == nil then
error("Liquid Physics: Could not register bucket. Liquid for bucket " .. name .. " was not found")
end
local bucket_liquid_id = liquid_physics.get_liquid_id(source_name)
if bucket_liquid_id == nil then
error("Liquid Physics: Could not register bucket. Liquid " ..
source_name .. " was not registered with liquid physics.")
end
if base_default then
local on_place_fallback = bucket_tool.on_place
local on_use_fallback = bucket_tool.on_place
core.override_item(name, {
on_use = function(itemstack, user, pointed_thing)
return bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
end,
on_place = function(itemstack, user, pointed_thing)
return bucket_on_place(on_place_fallback, bucket_liquid_id, source_name, itemstack, user, pointed_thing)
end,
wear_color = {
blend = "linear",
color_stops = {
[0.0] = "#ff0000",
[0.5] = "slateblue",
[1.0] = { r = 0, g = 255, b = 0, a = 150 },
}
},
}, nil)
core.register_tool(":" .. name, core.registered_items[name])
elseif base_mcl_buckets then
local on_place_fallback = bucket_tool.on_place
if core.settings:get_bool("liquid_physics_voxelibre_enable_scooping_via_use", true) then
local on_use_fallback = bucket_tool.on_use
core.override_item(name, {
on_use = function(itemstack, user, pointed_thing)
return bucket_on_use(on_use_fallback, itemstack, user, pointed_thing)
end
}, nil)
end
core.override_item(name, {
on_place = function(itemstack, user, pointed_thing)
return bucket_on_place(on_place_fallback, bucket_liquid_id, source_name, itemstack, user, pointed_thing)
end,
wear_color = {
blend = "linear",
color_stops = {
[0.0] = "#ff0000",
[0.5] = "slateblue",
[1.0] = { r = 0, g = 255, b = 0, a = 150 },
}
},
}, nil)
core.register_tool(":" .. name, core.registered_items[name])
end
end
return internal_bucket

30
game/default.lua Normal file
View File

@ -0,0 +1,30 @@
-- 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 <https://www.gnu.org/licenses/>.
local modpath = core.get_modpath(core.get_current_modname())
local internal_bucket = dofile(modpath .. "/bucket.lua")
internal_bucket.register_empty_bucket("bucket:bucket_empty")
-- Settings
if core.settings:get_bool("liquid_physics_enable_water_physics", true) then
liquid_physics.register_liquid("default", "water_source", "water_flowing")
internal_bucket.register_filled_bucket("bucket:bucket_water")
end
if core.settings:get_bool("liquid_physics_enable_lava_physics", true) then
liquid_physics.register_liquid("default", "lava_source", "lava_flowing")
internal_bucket.register_filled_bucket("bucket:bucket_lava")
end

30
game/voxelibre.lua Normal file
View File

@ -0,0 +1,30 @@
-- 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 <https://www.gnu.org/licenses/>.
local modpath = core.get_modpath(core.get_current_modname())
local internal_bucket = dofile(modpath .. "/bucket.lua")
internal_bucket.register_empty_bucket("mcl_buckets:bucket_empty")
-- Settings
if core.settings:get_bool("liquid_physics_enable_water_physics", true) then
liquid_physics.register_liquid("mcl_core", "water_source", "water_flowing")
internal_bucket.register_filled_bucket("mcl_buckets:bucket_water")
end
if core.settings:get_bool("liquid_physics_enable_lava_physics", true) then
liquid_physics.register_liquid("mcl_core", "lava_source", "lava_flowing")
internal_bucket.register_filled_bucket("mcl_buckets:bucket_lava")
end

View File

@ -17,32 +17,23 @@ local modpath = core.get_modpath(core.get_current_modname())
liquid_physics = {}
--INTERNAL USE ONLY - Stores all nodes which need to checked
-- 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
-- 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
-- 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
liquid_physics.register_liquid("default", "water_source", "water_flowing")
liquid_physics.register_liquid("default", "lava_source", "lava_flowing")
end
if core.get_modpath("mcl_core") then
liquid_physics.register_liquid("mcl_core", "water_source", "water_flowing")
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
dofile(modpath .. "/game/default.lua")
elseif core.get_modpath("mcl_core") then
dofile(modpath .. "/game/voxelibre.lua")
else
error("Liquid Physics only supports VoxeLibre or Minetest")
end
core.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing)
@ -53,7 +44,7 @@ end)
core.register_lbm {
name = "liquid_physics:update",
nodenames = source_names,
nodenames = "group:liquid_physics",
run_at_every_load = true,
action = function(pos, node, dtime_s)
internal.add_node_to_check(pos)
@ -61,7 +52,7 @@ core.register_lbm {
}
core.register_abm({
nodenames = source_names,
nodenames = "group:liquid_physics",
neighbors = { "air" },
interval = 0.2,
chance = 0,

View File

@ -96,6 +96,12 @@ function internal.set_lpn(liquid_id, lpn, liquid_level)
lpn.liquid_id = liquid_id
end
-- Sets liquid node from LPN
-- @param lpn table Liquid Physics Node
function internal.set_node(lpn)
core.set_node(lpn.pos, { name = internal.get_liquid_node_name(lpn.liquid_id, lpn.liquid_level) })
end
-- This node will be checked in the next cycle
-- @param pos table Position of the node
function internal.add_node_to_check(pos)

View File

@ -1,6 +1,6 @@
name = liquid_physics
depends =
optional_depends = default, mcl_core
optional_depends = default, mcl_core, bucket, mcl_buckets
author = snoutie
description = Adds physics to liquids
title = Liquid Physics

View File

@ -225,7 +225,7 @@ core.register_on_mapblocks_changed(function(modified_blocks, modified_blocks_cou
-- 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 = internal.get_liquid_node_name(lpn.liquid_id, lpn.liquid_level) })
internal.set_node(lpn)
end
end
end)

3
settingtypes.txt Normal file
View File

@ -0,0 +1,3 @@
liquid_physics_enable_water_physics (Enable Physics For Water) bool true
liquid_physics_enable_lava_physics (Enable Physics For Lava) bool true
liquid_physics_voxelibre_enable_scooping_via_use (VoxeLibre: Enable Scooping Via Use/Punch) bool true