diff --git a/api/api.lua b/api/api.lua
index 214f094..d272b1c 100644
--- a/api/api.lua
+++ b/api/api.lua
@@ -17,3 +17,4 @@ dofile(path.."/crafter_auto.lua")
dofile(path.."/crafting_supplier.lua")
dofile(path.."/cobble_supplier.lua")
dofile(path.."/synchronizer.lua")
+dofile(path.."/reservoir.lua")
diff --git a/api/reservoir.lua b/api/reservoir.lua
new file mode 100644
index 0000000..d090518
--- /dev/null
+++ b/api/reservoir.lua
@@ -0,0 +1,176 @@
+local S = logistica.TRANSLATOR
+local function L(str) return "logistica:"..str end
+
+local META_LIQUID_LEVEL = "liquidLevel"
+local META_LIQUID_NAME = "liquidName"
+
+local EMPTY_SUFFIX = "_empty"
+
+local LIQUID_NONE = ""
+
+local VAR_SMALL = "silverin"
+local VAR_LARGE = "obsidian"
+
+local SMALL_MAX = 32
+local LARGE_MAX = 128
+
+local variants = {VAR_SMALL}
+if logistica.settings.large_liquid_tank_enabled then table.insert(variants, VAR_LARGE) end
+
+local variantSpecificDefs = {
+ [VAR_SMALL] = {
+ description = logistica.reservoir_get_description(0, SMALL_MAX, ""),
+ tiles = {"logistica_reservoir_silverin.png"},
+ sounds = logistica.node_sound_metallic(),
+ logistica = {
+ maxBuckets = SMALL_MAX,
+ },
+ },
+ [VAR_LARGE] = {
+ description = logistica.reservoir_get_description(0, LARGE_MAX, ""),
+ tiles = {"logistica_reservoir_obsidian.png"},
+ sounds = default.node_sound_stone_defaults(),
+ logistica = {
+ maxBuckets = LARGE_MAX,
+ },
+ }
+}
+
+----------------------------------------------------------------
+-- general helper functions
+----------------------------------------------------------------
+
+-- local function drop_item(pos, stack)
+-- minetest.add_item(pos, stack)
+-- end
+
+local function give_item_to_player(pos, player, stack)
+ local inv = player:get_inventory()
+ local leftover = inv:add_item("main", stack)
+ if leftover and not leftover:is_empty() then
+ -- print("leftover not empty, size = " .. leftover:get_count())
+ minetest.item_drop(leftover, player, player:get_pos())
+ end
+end
+
+----------------------------------------------------------------
+-- callbacks
+----------------------------------------------------------------
+
+
+local function after_place_node(pos, placer, itemstack, pointed_thing)
+ local nodeMeta = minetest.get_meta(pos)
+ local node = minetest.get_node(pos)
+ local stackMeta = itemstack:get_meta()
+ local nodeDef = minetest.registered_nodes[node.name]
+ if not nodeDef or not nodeDef.logistica then return end
+
+ local liquidLevel = stackMeta:get_int(META_LIQUID_LEVEL)
+ local liquidDesc = nodeDef.logistica.liquidDesc
+ local maxBuckets = nodeDef.logistica.maxBuckets
+
+ nodeMeta:set_int(META_LIQUID_LEVEL, liquidLevel)
+ node.param2 = logistica.reservoir_make_param2(liquidLevel, maxBuckets)
+ minetest.swap_node(pos, node)
+ nodeMeta:set_string("infotext", logistica.reservoir_get_description(liquidLevel, maxBuckets, liquidDesc))
+end
+
+
+local function preserve_metadata(pos, oldnode, oldmeta, drops)
+ if not drops or not drops[1] then return end
+ local nodeDef = minetest.registered_nodes[oldnode.name]
+ if not nodeDef or not nodeDef.logistica then return end
+
+ local meta = minetest.get_meta(pos)
+ local drop = drops[1]
+ local dropMeta = drop:get_meta()
+ local liquidDesc = nodeDef.logistica.liquidDesc
+ local maxBuckets = nodeDef.logistica.maxBuckets
+ local liquidLevel = meta:get_int(META_LIQUID_LEVEL)
+
+ dropMeta:set_int(META_LIQUID_LEVEL, liquidLevel)
+ dropMeta:set_string("description", logistica.reservoir_get_description(liquidLevel, maxBuckets, liquidDesc))
+end
+
+local function on_rightclick(pos, node, player, itemstack, pointed_thing, max)
+ if not player or not player:is_player() or minetest.is_protected(pos, player:get_player_name()) then return end
+
+ local usedItem = logistica.reservoir_use_item_on(pos, itemstack, node)
+
+ if not usedItem then return end
+
+ if itemstack:get_count() == 1 then
+ return usedItem
+ else
+ give_item_to_player(pos, player, usedItem)
+ itemstack:take_item(1)
+ return itemstack
+ end
+end
+
+--------------------------------
+-- registration helpers
+--------------------------------
+
+local commonDef = {
+ drawtype = "glasslike_framed_optional",
+ paramtype = "light",
+ paramtype2 = "glasslikeliquidlevel",
+ is_ground_content = false,
+ sunlight_propagates = false,
+ groups = {cracky = 3, level = 1},
+ preserve_metadata = preserve_metadata,
+ after_place_node = after_place_node,
+ on_rightclick = on_rightclick,
+ stack_max = 1,
+ backface_culling = false,
+}
+
+local function get_variant_def(variantName)
+ if not variantSpecificDefs[variantName] then return nil end
+ local vDef = variantSpecificDefs[variantName]
+ local def = table.copy(commonDef)
+ for k,v in pairs(vDef) do def[k] = v end
+ return def
+end
+
+--------------------------------
+-- minetest registration
+--------------------------------
+
+-- register empty tanks, always
+for _, variantName in ipairs(variants) do
+ local def = table.copy(get_variant_def(variantName))
+ local nodeName = L("reservoir_"..variantName..EMPTY_SUFFIX)
+ def.drops = nodeName
+ def.logistica.liquidName = LIQUID_NONE
+ def.logistica.liquidDesc = LIQUID_NONE
+ minetest.register_node(nodeName, def)
+end
+
+--[[
+ `liquidName`: the name used to register the reservoir, should have no spaces and all lowercase
+ `liquidDesc`: a human readable liquid description, e.g. "Water"
+ `bucketItemName` : the name of the bucket that holds the liquid
+ `liquidTexture` : a single texture to use for the liquid
+ `optLight` : optional, if nil assumed 0. How much a non-empty reservoir will glow
+ `emptyBucketName` : optional, if nil, bucket:bucket_empty will be used - the "empty" container to use
+]]
+function logistica.register_reservoir(liquidName, liquidDesc, bucketItemName, liquidTexture, optLight, optEmptyBucketName)
+ local lname = string.lower(liquidName:gsub(" ", "_"))
+
+ for _, variantName in ipairs(variants) do
+ local nodeName = L("reservoir_"..variantName.."_"..lname)
+ local def = table.copy(get_variant_def(variantName))
+ def.drops = nodeName
+ def.special_tiles = {liquidTexture}
+ def.logistica.liquidName = lname
+ def.logistica.liquidDesc = liquidDesc
+ def.groups.not_in_creative_inventory = 1
+ def.light_source = optLight
+
+ minetest.register_node(nodeName, def)
+
+ logistica.reservoir_register_names(lname, bucketItemName, optEmptyBucketName)
+ end
+end
diff --git a/logic/logic.lua b/logic/logic.lua
index 7a1d4f2..312c231 100644
--- a/logic/logic.lua
+++ b/logic/logic.lua
@@ -18,3 +18,4 @@ dofile(path.."/vaccuum_chest.lua")
dofile(path.."/autocrafting_logic.lua")
dofile(path.."/crafting_supplier.lua")
dofile(path.."/synchronizer.lua")
+dofile(path.."/reservoir.lua")
diff --git a/logic/reservoir.lua b/logic/reservoir.lua
new file mode 100644
index 0000000..15e65c7
--- /dev/null
+++ b/logic/reservoir.lua
@@ -0,0 +1,140 @@
+local S = logistica.TRANSLATOR
+
+local BUCKET_TO_NAME = {}
+local NAME_TO_BUCKET = {}
+local NAME_TO_EMPTY_BUCKET = {}
+local EMPTY_BUCKET = "bucket:bucket_empty"
+local EMPTY_SUFFIX = "_empty"
+
+local META_LIQUID_LEVEL = "liquidLevel"
+local LIQUID_NONE = ""
+local BUCKET_ANY = "BUCKET_ANY"
+
+local strDescription = S("Reservoir")
+local strEmpty = S("Empty")
+local getStrContains = function(number, max, ofWhat)
+ if number == 0 then
+ return S("@1 / @2 buckets", number, max)
+ else
+ return S("@1 / @2 buckets of @3", number, max, ofWhat)
+ end
+end
+
+local function ends_with(str, ending)
+ return ending == "" or string.sub(str, -#ending) == ending
+end
+
+local function get_empty_bucket_needed_for(liquidName)
+ local savedBucket = NAME_TO_EMPTY_BUCKET[liquidName]
+ if savedBucket then return savedBucket
+ else return EMPTY_BUCKET end
+end
+
+local function get_full_bucket_needed_for(liquidName)
+ if liquidName == LIQUID_NONE then return BUCKET_ANY end
+ return NAME_TO_BUCKET[liquidName]
+end
+
+local function get_empty_reservoir_name(nodeName, liquidName)
+ if not ends_with(nodeName, liquidName) then return nodeName end
+ if ends_with(nodeName, EMPTY_SUFFIX) then return nodeName end
+ local nodeBase = string.sub(nodeName, 1, (#nodeName) - (#liquidName) - 1)
+ return nodeBase..EMPTY_SUFFIX
+end
+
+local function get_liquid_reservoir_name_for(nodeName, liquidName)
+ if not ends_with(nodeName, EMPTY_SUFFIX) then return nodeName end
+ local nodeBase = string.sub(nodeName, 1, (#nodeName) - (#EMPTY_SUFFIX))
+ local newName = nodeBase.."_"..liquidName
+ if not minetest.registered_nodes[newName] then return nodeName
+ else return newName end
+end
+
+--------------------------------
+-- public functions
+--------------------------------
+
+function logistica.reservoir_make_param2(val, max)
+ local ret = math.floor(63*(val/max))
+ if val > 0 and ret == 0 then
+ ret = 1 -- this ensures we always have at least 1 visible liquid level
+ end
+ return ret
+end
+
+function logistica.reservoir_get_description(currBuckets, maxBuckets, liquidName)
+ return strDescription.."\n"..getStrContains(currBuckets, maxBuckets, liquidName)
+end
+
+function logistica.reservoir_register_names(liquidName, bucketName, emptyBucketName)
+ BUCKET_TO_NAME[bucketName] = liquidName
+ NAME_TO_BUCKET[liquidName] = bucketName
+ if emptyBucketName then
+ NAME_TO_EMPTY_BUCKET[liquidName] = emptyBucketName
+ end
+end
+
+-- returns nil if item had no effect
+-- returns an ItemStack to replace the item, if it had effect (e.g. took or stored liquid)
+function logistica.reservoir_use_item_on(pos, itemstack, optNode)
+ local node = optNode or minetest.get_node(pos)
+ local nodeDef = minetest.registered_nodes[node.name]
+ if not nodeDef or not nodeDef.logistica or not nodeDef.logistica.liquidName or not nodeDef.logistica.maxBuckets then return end
+
+ local itemStackName = itemstack:get_name()
+ local meta = minetest.get_meta(pos)
+ local nodeLiquidLevel = meta:get_int(META_LIQUID_LEVEL)
+ local liquidName = nodeDef.logistica.liquidName
+ local maxBuckets = nodeDef.logistica.maxBuckets
+ local liquidDesc = nodeDef.logistica.liquidDesc
+
+ local emptyBucket = get_empty_bucket_needed_for(liquidName)
+ local fullBucket = get_full_bucket_needed_for(liquidName)
+
+ if itemStackName == emptyBucket then
+ if nodeLiquidLevel == 0 then
+ -- make sure we swap this for the empty reservoir
+ logistica.swap_node(pos, get_empty_reservoir_name(node.name, liquidName))
+ return nil
+ end
+ if not fullBucket then return nil end
+
+ nodeLiquidLevel = nodeLiquidLevel - 1
+ if nodeLiquidLevel == 0 then
+ node.param2 = 0
+ else
+ node.param2 = logistica.reservoir_make_param2(nodeLiquidLevel, maxBuckets)
+ end
+ minetest.swap_node(pos, node)
+ meta:set_int(META_LIQUID_LEVEL, nodeLiquidLevel)
+ if nodeLiquidLevel == 0 then
+ local newNodeName = get_empty_reservoir_name(node.name, liquidName)
+ if not minetest.registered_nodes[newNodeName] then return nil end
+ logistica.swap_node(pos, newNodeName)
+ end
+ meta:set_string("infotext", logistica.reservoir_get_description(nodeLiquidLevel, maxBuckets, liquidDesc))
+
+ return ItemStack(fullBucket)
+ elseif fullBucket == BUCKET_ANY or itemStackName == fullBucket then
+ local newLiquidName = BUCKET_TO_NAME[itemStackName]
+ if not newLiquidName then return nil end -- wasn't a bucket we can use
+ local newEmptyBucket = get_empty_bucket_needed_for(newLiquidName)
+ if not newEmptyBucket then return nil end
+
+ nodeLiquidLevel = nodeLiquidLevel + 1
+ if nodeLiquidLevel > maxBuckets then return nil end
+ node.param2 = logistica.reservoir_make_param2(nodeLiquidLevel, maxBuckets)
+ minetest.swap_node(pos, node)
+ local newNodeName = get_liquid_reservoir_name_for(node.name, newLiquidName)
+
+ if not minetest.registered_nodes[newNodeName] then return nil end
+ if nodeLiquidLevel == 1 then -- first bucket we added, swap to that reservoir type
+ logistica.swap_node(pos, newNodeName)
+ end
+ local newLiquidDesc = minetest.registered_nodes[newNodeName].logistica.liquidDesc
+ meta:set_string("infotext", logistica.reservoir_get_description(nodeLiquidLevel, maxBuckets, newLiquidDesc))
+ meta:set_int(META_LIQUID_LEVEL, nodeLiquidLevel)
+ return ItemStack(newEmptyBucket)
+ end
+ return nil
+end
diff --git a/registration/machines_api_reg.lua b/registration/machines_api_reg.lua
index 86ad176..54d21b8 100644
--- a/registration/machines_api_reg.lua
+++ b/registration/machines_api_reg.lua
@@ -185,6 +185,14 @@ local function ins_tiles(lname) return {
logistica.register_requester("Item Request Inserter\nInserts 1 item at a time", "requester_item", 1, ins_tiles("item"))
logistica.register_requester("Bulk Request Inserter\nInserts up to 64 items at a time", "requester_stack", 64, ins_tiles("stack"))
+--------------------------------
+-- Reservoirs
+--------------------------------
+
+logistica.register_reservoir("lava", "Lava", "bucket:bucket_lava", "default_lava.png", 8)
+logistica.register_reservoir("water", "Water", "bucket:bucket_water", "default_water.png")
+logistica.register_reservoir("river_water", "River Water", "bucket:bucket_river_water", "default_river_water.png")
+
--------------------------------
-- Passive Supply Chest
--------------------------------
diff --git a/registration/node_recipes.lua b/registration/node_recipes.lua
index 89de123..836ea99 100644
--- a/registration/node_recipes.lua
+++ b/registration/node_recipes.lua
@@ -147,3 +147,21 @@ minetest.register_craft({
{L("silverin_plate"), L("wireless_crystal"), L("silverin_plate")},
}
})
+
+minetest.register_craft({
+ output = L("reservoir_silverin_empty"),
+ recipe = {
+ {L("silverin_plate"), "", L("silverin_plate")},
+ {L("optic_cable"), "bucket:bucket_empty", L("photonizer")},
+ {L("silverin_plate"), "", L("silverin_plate")},
+ }
+})
+
+minetest.register_craft({
+ output = L("reservoir_obsidian_empty"),
+ recipe = {
+ {"default:obsidianbrick", L("silverin_plate"), "default:obsidianbrick"},
+ {L("optic_cable"), "bucket:bucket_empty", L("photonizer")},
+ {"default:obsidianbrick", L("silverin_plate"), "default:obsidianbrick"},
+ }
+})
diff --git a/settingtypes.txt b/settingtypes.txt
index 6be1963..408144a 100644
--- a/settingtypes.txt
+++ b/settingtypes.txt
@@ -3,3 +3,4 @@ logistica_wap_max_range (Max range for the Wirless Access Pad, in nodes.) int 64
logistica_wap_upgrade_step (How much distance each upgrade to the WAP adds) int 250 10 5000
logistica_wifi_upgrader_hard_mode (Make Wireless Upgrader's minigame harder) bool false
logistica_cable_size (Changes the visual/hitbox size of cables) enum Medium Small,Medium,Large,XLarge
+logistica_enable_large_liquid_tank (Enable the Large Liquid Tank) bool true
diff --git a/textures/logistica_reservoir_obsidian.png b/textures/logistica_reservoir_obsidian.png
new file mode 100644
index 0000000..acf7520
Binary files /dev/null and b/textures/logistica_reservoir_obsidian.png differ
diff --git a/textures/logistica_reservoir_silverin.png b/textures/logistica_reservoir_silverin.png
new file mode 100644
index 0000000..c6b5b37
Binary files /dev/null and b/textures/logistica_reservoir_silverin.png differ
diff --git a/util/settings.lua b/util/settings.lua
index 43a2098..6d5aaeb 100644
--- a/util/settings.lua
+++ b/util/settings.lua
@@ -32,3 +32,5 @@ logistica.settings.wap_upgrade_step = get_int("wap_upgrade_step", 250)
logistica.settings.wifi_upgrader_hard_mode = get_bool("wifi_upgrader_hard_mode", false)
logistica.settings.cable_size = get_cable_size_from_settings()
+
+logistica.settings.large_liquid_tank_enabled = get_bool("enable_large_liquid_tank", true)