add oil, dynamic fluid, and waterworks

master
Elkien3 2021-04-26 15:29:09 -05:00
parent 93980cf192
commit 950a006663
70 changed files with 3926 additions and 5 deletions

View File

@ -512,6 +512,12 @@ end)
local function car_step(self, dtime)
if dtime > .2 then dtime = .2 end
local def = cars_registered_cars[self.object:get_entity_name()]
if self.ignition and oil and def.gas_usage then
if self.gas <= 0 then
self.ignition = false
self.gas = 0
end
end
local velocity = self.object:getvelocity()
local yaw = self.object:getyaw()
if not yaw then return end
@ -602,8 +608,12 @@ local function car_step(self, dtime)
end
end
local driver = self.passengers[1].player
if driver and self.ignition then
driver:hud_change(self.hud, "text", tostring(math.abs(rnd(self.v*2.23694, 10)).." MPH"))
if driver then
local text = tostring(math.abs(rnd(self.v*2.23694, 10)).." MPH")
if oil and def.gas_usage then
text = text.." "..tostring(math.floor(self.gas*10)/10).."L Gas"
end
driver:hud_change(self.hud, "text", text)
local ctrl = driver:get_player_control()
local sign
local lights
@ -636,6 +646,7 @@ local function car_step(self, dtime)
end
end
--VELOCITY MOVEMENT
if self.ignition then
local newv = self.v
if ctrl.up then
if sign >= 0 then
@ -676,6 +687,16 @@ local function car_step(self, dtime)
cars.setlight(lights, "brakelights", false)
end
else
local sign
if self.v == 0 then sign = 0 else sign = get_sign(self.v) end
if sign ~= 0 then
self.v = self.v - def.coasting*dtime*sign
if get_sign(self.v) ~= sign then
self.v = 0
end
end
end
--ACCELERATION MOVEMENT
--[[
if ctrl.up then
@ -742,7 +763,7 @@ local function car_step(self, dtime)
if attachTimer >= 5 then
if self.wheel.backright then self.wheel.backright:set_attach(self.object, "", {z=-11.75,y=2.5,x=-8.875}, {x=0,y=0,z=0}) end
if self.wheel.backleft then self.wheel.backleft:set_attach(self.object, "", {z=-11.75,y=2.5,x=8.875}, {x=0,y=0,z=0}) end
if self.lights then self.lights:set_attach(self.object, "", {x=0,y=0,x=0}, {x=0,y=0,z=0}) end
if self.lights then self.lights:set_attach(self.object, "", {x=0,y=0,z=0}, {x=0,y=0,z=0}) end
end
self.lastctrl = ctrl
else
@ -833,6 +854,13 @@ local function car_step(self, dtime)
local abs_v = math.abs(self.v)
--if abs_v > 0 and driver ~= nil then
if self.ignition then
if oil and def.gas_usage and not slowing then
if self.v == 0 or self.cruise then--idle gas usage
self.gas = self.gas - (def.gas_usage*dtime*.25)/60
else--you are accelerating
self.gas = self.gas - (def.gas_usage*dtime)/60
end
end
self.timer1 = self.timer1 + dtime
local rpm = 1
for i, tab in pairs(def.rpmvalues) do
@ -995,6 +1023,7 @@ function cars_register_car(def)
if not self.timer1 then self.timer1 = 0 end
if not self.timer2 then self.timer2 = 0 end
if not self.hp then self.hp = 20 end
if not self.gas then self.gas = 0 end
if not self.platenumber then
self.platenumber = {}
end
@ -1008,6 +1037,7 @@ function cars_register_car(def)
self.trunkinv = deserializeContents(deserialized.trunk)
self.key = deserialized.key
self.hp = deserialized.hp or 20
self.gas = deserialized.gas or 0
if deserialized.plate then
self.platenumber.text = deserialized.plate.text
end
@ -1057,7 +1087,7 @@ function cars_register_car(def)
self.object:set_hp(self.hp)
end,
get_staticdata = function(self)
return minetest.serialize({owner = self.owner, trunk = serializeContents(self.trunkinv), secret = self.secret, locked = self.locked, key = self.key, plate = self.platenumber, hp = self.hp})
return minetest.serialize({owner = self.owner, trunk = serializeContents(self.trunkinv), secret = self.secret, locked = self.locked, key = self.key, plate = self.platenumber, hp = self.hp, gas = self.gas})
end,
on_step = function(self, dtime)
car_step(self, dtime)
@ -1074,7 +1104,21 @@ function cars_register_car(def)
return true
end
local punchitem = puncher:get_wielded_item():get_name()
if (punchitem == "") and (time_from_last_punch >= tool_capabilities.full_punch_interval) and math.random(1,2) == 1 then
if oil and name and oil.fueling[name] then
local data = oil.fueling[name]
local obj = oil.fueling[name].obj
local pos = oil.fueling[name].pos
minetest.get_node_timer(pos):start(1)
local meta = minetest.get_meta(pos)
meta:set_string("name", name)
if obj then
local ent = obj:get_luaentity()
ent.finishobj = self.object
if def.gas_offset then
ent.finishoffset = def.gas_offset
end
end
elseif (punchitem == "") and (time_from_last_punch >= tool_capabilities.full_punch_interval) and math.random(1,2) == 1 then
local closeid = getClosest(puncher, self)
if DEBUG_TEXT then
minetest.chat_send_all(tostring(closeid))

View File

@ -21,6 +21,8 @@ local sedandef = {
acceleration = 3,
braking = 10,
coasting = 2,
gas_usage = 1,
gas_offset = {x=-1,y=.8,z=-1.1},
max_speed = 24.5872,
trunksize = {x=6,y=2},
trunkloc = {x = 0, y = 4, z = -8},

17
mods/dynamic_liquid/.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

47
mods/dynamic_liquid/.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@ -0,0 +1,21 @@
dynamic_liquid mod for Minetest
Copyright (C) 2017 FaceDeer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,39 @@
This mod implements a simple approach to making the various liquid node types (water, lava) behave in a more realistic manner. In a nutshell, it sets an Active Block Modifier running for liquid nodes that implements the following behavior:
* If there is a "flowing" or air node below a "source" liquid block, swap the liquid block with the node below it (ie, the liquid drops)
* Else swap with a random flowing or air node beside it.
This causes "source" blocks for liquids to randomly shuffle around when they don't completely fill a horizontal layer, and causes them to drain rapidly down holes or flow rapidly down hillsides. Normal "flowing" behaviour is unchanged so even though this mod is just moving cubes around the surface of the water looks reasonably smooth. The ABM only runs for blocks that are adjacent to flowing nodes and does very few calculations so it's also reasonably lightweight - I'm essentially borrowing the engine's built-in liquid flow detection to determine when my own code needs to run.
Each type of liquid can have this behaviour enabled independently of each other. By default lava is configured to flow more slowly than water, but this can be changed in the mod's settings.
## Damp Clay Counters Ocean Loss
If basic water is set as dynamic, it loses its "renewability" - water nodes don't replicate like they normally do. If you are concerned about the global water level dropping over time as water pours down into undersea caverns, or just generally don't like the idea of water being a finite resource (even with an ocean's worth to start with), this mod includes an optional feature that turns natural clay deposits into "springs". Clay deposits get an Active Block Modifier that checks if there's air or shallow water above them and refills them with newly spawned water source blocks.
If this clay is dug out by players and reconstituted into clay blocks elsewhere, the "spring" behaviour won't occur for these - it only happens for clay spawned by mapgen while this feature is enabled. (In technical terms, when clay springs are enabled the map generator replaces generated clay with an otherwise identical "damp clay" node type that drops regular clay when dug. The ABM only affects this "damp clay" node type.)
There's also a "spring" block that's available only via the creative menu or /give command. This block has an ABM that ensures there's always a single block of water generated directly above it, regardless of where the block is placed.
Note that other than this clay spring feature and the override of water's "renewability" this mod doesn't affect any default node definitions, so any other mods dealing with liquids should still function as they did before. If you disable any of the liquid types they will just "freeze" in whatever position they were in at the time.
## "Flow-through" Blocks
There are many blocks in the game that one would think should not serve as an impenetrable barrier to water flow, but which do. For example: metal bars, grass, leaves. The "flow-through" feature of this mod makes them permeable, allowing water_source and lava_source blocks to "teleport" from one side of a flow-through block to the other. Liquid source blocks will only teleport through a single layer of flow-through node and only in the cardinal directions.
Other mods can mark blocks as flow-through by adding them to the "flow_through" group.
## Lava/Water Interaction
Due to the way default lava/water interaction works a single block of lava_source would be able to wander endlessly around on the surface of the ocean turning the uppermost layer of water into stone. Similarly, a single block of water_source could wander around on the surface of a lava pool turning the uppermost layer into obsidian. This is an undesirable outcome so the dynamic_liquid mod includes a more sophisticated replacement to the default lava-cooling ABM.
lava_flowing nodes are now eliminated entirely by nodes marked cools_lava, they do not leave behind default:stone. This means that infinite stone generation is no longer possible using arrangements of lava and water.
lava_source nodes are turned into obsidian only by water_source nodes; water_flowing nodes are eliminated by lava_source nodes without affecting them. The water_source node is destroyed in the process. This means that if you want to cool a large amount of lava you're going to need a correspondingly large amount of water.
For modders, you can now fine-tune the effects of blocks on lava (and vice versa) using the following groups:
* "group:dynamic_cools_lava_flowing" - causes lava_flowing nodes to vanish. The old "group:cools_lava" has this effect.
* "group:dynamic_lava_flowing_destroys" - causes lava_flowing nodes to destroy nodes belonging to this group.
* "group:dynamic_cools_lava_source" - causes lava_source nodes to turn into obsidian. The old "group:cools_lava" does *not* have this effect.
* "group:dynamic_lava_source_destroys" - causes lava_source nodes to destroy nodes belonging to this group.

View File

@ -0,0 +1,224 @@
if not minetest.get_modpath("default") then return end
local new_lava_cooling = minetest.settings:get_bool("dynamic_liquid_new_lava_cooling", true)
if not new_lava_cooling then return end
local falling_obsidian = minetest.settings:get_bool("dynamic_liquid_falling_obsidian", false)
-- The existing cool_lava ABM is hard-coded to respond to water nodes
-- and overriding node groups doesn't appear to work:
-- https://github.com/minetest/minetest/issues/5518
-- So modifying the lava-cooling system requires some unfortunate
-- workarounds. Most notable is the "dynamic_cools_lava" group;
-- the "cools_lava" group can't be removed from flowing water for ABM
-- purposes so this redundancy is required.
-- Mods can hook into this system by adding the group
-- "dynamic_cools_lava_flowing" and/or "dynamic_cools_lava_source"
-- to nodes that should cool lava and
-- "dynamic_lava_flowing_destroys" and/or "dynamic_lava_source_destroys"
-- to nodes that should be destroyed by proximity to lava.
local particles = minetest.settings:get_bool("enable_particles", true)
local steam = function(pos)
if particles then
minetest.add_particlespawner({
amount = 6,
time = 1,
minpos = pos,
maxpos = pos,
minvel = {x=-2, y=0, z=-2},
maxvel = {x=2, y=1, z=2},
minacc = {x=0, y=2, z=0},
maxacc = {x=0, y=2, z=0},
minexptime = 1,
maxexptime = 4,
minsize = 16,
maxsize = 16,
collisiondetection = false,
vertical = false,
texture = "smoke_puff.png",
})
end
end
default.cool_lava = function(pos, node)
-- no-op disables default cooling ABM
end
-------------------------------------------------------------------------------------------------
local dynamic_cools_lava_flowing = {"group:dynamic_cools_lava_flowing", "group:cools_lava"}
-- Flowing lava will turn these blocks into steam.
local dynamic_lava_flowing_destroys = {
"group:dynamic_lava_flowing_destroys",
"default:water_flowing",
"default:river_water_flowing",
"default:snow",
"default:snowblock"
}
local all_flowing_nodes = {unpack(dynamic_cools_lava_flowing)}
for i = 1, #dynamic_lava_flowing_destroys do
all_flowing_nodes[#dynamic_cools_lava_flowing + i] = dynamic_lava_flowing_destroys[i]
end
local cool_lava_flowing = function(pos, node)
local cooler_adjacent = minetest.find_node_near(pos, 1, dynamic_cools_lava_flowing)
if cooler_adjacent ~= nil then
-- pulling nearby sources into position is necessary to break certain classes of
-- flow "deadlock". Weird, but what're you gonna do.
local nearby_source = minetest.find_node_near(pos, 1, "default:lava_source")
if nearby_source then
minetest.set_node(pos, {name="default:lava_source"})
minetest.set_node(nearby_source, {name="air"})
steam(nearby_source)
else
minetest.set_node(pos, {name = "air"})
steam(pos)
end
end
local evaporate_list = minetest.find_nodes_in_area(
vector.add(pos,{x=-1, y=-1, z=-1}),
vector.add(pos,{x=1, y=1, z=1}),
dynamic_lava_flowing_destroys
)
for _, loc in pairs(evaporate_list) do
minetest.set_node(loc, {name="air"})
steam(loc)
end
minetest.sound_play("default_cool_lava",
{pos = pos, max_hear_distance = 16, gain = 0.25})
end
minetest.register_abm({
label = "Lava flowing cooling",
nodenames = {"default:lava_flowing"},
neighbors = all_flowing_nodes,
interval = 1,
chance = 1,
catch_up = false,
action = function(...)
cool_lava_flowing(...)
end,
})
-------------------------------------------------------------------------------------------------
local dynamic_cools_lava_source = {"group:dynamic_cools_lava_source"}
for name, node_def in pairs(minetest.registered_nodes) do
-- We don't want "flowing" nodes to cool lava source blocks, otherwise when water falls onto a large pool of lava there's
-- way too many blocks turned to obsidian.
if minetest.get_item_group(name, "cools_lava") > 0 and name ~= "default:water_flowing" and name ~= "default:river_water_flowing" then
table.insert(dynamic_cools_lava_source, name)
end
end
-- lava source blocks will turn these blocks into steam.
local dynamic_lava_source_destroys = {
"group:dynamic_lava_source_destroys",
"default:water_source",
"default:river_water_source",
"default:water_flowing",
"default:river_water_flowing",
"default:ice",
"default:snow",
"default:snowblock"
}
local all_source_nodes = {unpack(dynamic_cools_lava_source)}
for i = 1, #dynamic_lava_source_destroys do
all_source_nodes[#dynamic_cools_lava_source + i] = dynamic_lava_source_destroys[i]
end
local is_pos_in_list = function(pos, list)
for _, loc in pairs(list) do
if vector.equals(pos, loc) then return true end
end
return false
end
local function shuffle_array(a)
local rand = math.random
local iterations = #a
local j
for i = iterations, 2, -1 do
j = rand(i)
a[i], a[j] = a[j], a[i]
end
end
local cool_lava_source = function(pos, node)
local cooler_list = minetest.find_nodes_in_area(
vector.add(pos,{x=-1, y=-1, z=-1}),
vector.add(pos,{x=1, y=1, z=1}),
dynamic_cools_lava_source)
local evaporate_list = minetest.find_nodes_in_area(
vector.add(pos,{x=-1, y=-1, z=-1}),
vector.add(pos,{x=1, y=1, z=1}),
dynamic_lava_source_destroys
)
shuffle_array(cooler_list)
local obsidian_location = nil
for _, loc in pairs(cooler_list) do
if is_pos_in_list(loc, evaporate_list) then
if loc.y < pos.y then
if loc.z == pos.z and loc.x == pos.x then
obsidian_location = loc -- best outcome, directly below us. End loop immediately.
break
else
obsidian_location = loc -- next-best outcome, in the tier below us.
end
elseif loc.y == pos.y and obsidian_location == nil then
obsidian_location = loc -- On the same level as us, acceptable if nothing else comes along.
end
end
end
if obsidian_location == nil and #cooler_list > 0 then
obsidian_location = pos -- there's an adjacent cooler node, but it's above us. Turn into obsidian in place.
end
for _, loc in pairs(evaporate_list) do
minetest.set_node(loc, {name="air"})
steam(loc)
end
if obsidian_location ~= nil then
minetest.set_node(pos, {name = "air"})
minetest.set_node(obsidian_location, {name = "default:obsidian"})
if minetest.spawn_falling_node and falling_obsidian then -- TODO cutting-edge dev function, so check if it exists for the time being. Remove check when 0.4.16 is released.
minetest.spawn_falling_node(obsidian_location)
end
elseif #evaporate_list > 0 then
-- Again, this weird bit is necessary for breaking certain types of flow deadlock
local loc = evaporate_list[math.random(1,#evaporate_list)]
if loc.y <= pos.y then
minetest.set_node(pos, {name = "air"})
minetest.set_node(loc, {name = "default:lava_source"})
end
end
minetest.sound_play("default_cool_lava",
{pos = pos, max_hear_distance = 16, gain = 0.25})
end
minetest.register_abm({
label = "Lava source cooling",
nodenames = {"default:lava_source"},
neighbors = all_source_nodes,
interval = 1,
chance = 1,
catch_up = false,
action = function(...)
cool_lava_source(...)
end,
})

View File

@ -0,0 +1,5 @@
default?
intllib?
doc?
xpanes?
carts?

View File

@ -0,0 +1,4 @@
Flowing dynamic liquids and ocean-maintenance springs. v0.5
Github: https://github.com/FaceDeer/dynamic_liquid/
Forum: https://forum.minetest.net/viewtopic.php?t=16485

421
mods/dynamic_liquid/i18n.py Normal file
View File

@ -0,0 +1,421 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
#
# See https://github.com/minetest-tools/update_translations for
# potential future updates to this script.
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
# Running params
params = {"recursive": False,
"help": False,
"mods": False,
"verbose": False,
"folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"mods": ['--installed-mods'],
"verbose": ['--verbose', '-v']
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["mods"])}
run on locally installed modules
{', '.join(options["verbose"])}
add output information
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
elif params["recursive"] and params["mods"]:
print("Option --installed-mods is incompatible with --recursive")
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if params["mods"]:
print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
run_all_subfolders("~/.minetest/mods")
elif len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None
#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = modname + "." + language_code + ".tr"
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
if params["verbose"]:
print(f"{tr_name} already exists, ignoring {name}")
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
lOut = [f"# textdomain: {mod_name}\n"]
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
sourceList.sort()
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
localizedStrings.sort()
lOut.append("")
lOut.append(source)
lOut.append("")
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n##### not used anymore #####\n")
if len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
#TODO remove comments here
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed.findall(text):
strings.append(s)
for s in strings:
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
dOut = {}
text = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line[:3] == "###":
# Reset comment block if we hit a header
latest_comment_block = None
continue
if line[:1] == "#":
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text)
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
if params["verbose"]:
print(f"{fname}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(f"### {os.path.basename(fname)} ###")
dOut[s] = sources
if len(dOut) == 0:
return None
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
return dOut
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name)
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print(f"Updating translations for {modname}")
data = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
else:
print("Unable to find modname in folder " + folder)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
for subfolder in subfolders:
update_mod(subfolder + "/")
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
update_folder(modfolder + "/")
main()

View File

@ -0,0 +1,567 @@
dynamic_liquid = {} -- global table to expose liquid_abm for other mods' usage
dynamic_liquid.registered_liquids = {} -- used by the flow-through node abm
dynamic_liquid.registered_liquid_neighbors = {}
local water_level = tonumber(minetest.get_mapgen_setting("water_level"))
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
dofile(modpath.."/cooling_lava.lua")
-- By making this giant table of all possible permutations of horizontal direction we can avoid
-- lots of redundant calculations.
local all_direction_permutations = {
{{x=0,z=1},{x=0,z=-1},{x=1,z=0},{x=-1,z=0}},
{{x=0,z=1},{x=0,z=-1},{x=-1,z=0},{x=1,z=0}},
{{x=0,z=1},{x=1,z=0},{x=0,z=-1},{x=-1,z=0}},
{{x=0,z=1},{x=1,z=0},{x=-1,z=0},{x=0,z=-1}},
{{x=0,z=1},{x=-1,z=0},{x=0,z=-1},{x=1,z=0}},
{{x=0,z=1},{x=-1,z=0},{x=1,z=0},{x=0,z=-1}},
{{x=0,z=-1},{x=0,z=1},{x=-1,z=0},{x=1,z=0}},
{{x=0,z=-1},{x=0,z=1},{x=1,z=0},{x=-1,z=0}},
{{x=0,z=-1},{x=1,z=0},{x=-1,z=0},{x=0,z=1}},
{{x=0,z=-1},{x=1,z=0},{x=0,z=1},{x=-1,z=0}},
{{x=0,z=-1},{x=-1,z=0},{x=1,z=0},{x=0,z=1}},
{{x=0,z=-1},{x=-1,z=0},{x=0,z=1},{x=1,z=0}},
{{x=1,z=0},{x=0,z=1},{x=0,z=-1},{x=-1,z=0}},
{{x=1,z=0},{x=0,z=1},{x=-1,z=0},{x=0,z=-1}},
{{x=1,z=0},{x=0,z=-1},{x=0,z=1},{x=-1,z=0}},
{{x=1,z=0},{x=0,z=-1},{x=-1,z=0},{x=0,z=1}},
{{x=1,z=0},{x=-1,z=0},{x=0,z=1},{x=0,z=-1}},
{{x=1,z=0},{x=-1,z=0},{x=0,z=-1},{x=0,z=1}},
{{x=-1,z=0},{x=0,z=1},{x=1,z=0},{x=0,z=-1}},
{{x=-1,z=0},{x=0,z=1},{x=0,z=-1},{x=1,z=0}},
{{x=-1,z=0},{x=0,z=-1},{x=1,z=0},{x=0,z=1}},
{{x=-1,z=0},{x=0,z=-1},{x=0,z=1},{x=1,z=0}},
{{x=-1,z=0},{x=1,z=0},{x=0,z=-1},{x=0,z=1}},
{{x=-1,z=0},{x=1,z=0},{x=0,z=1},{x=0,z=-1}},
}
local get_node = minetest.get_node
local set_node = minetest.swap_node
-- Dynamic liquids
-----------------------------------------------------------------------------------------------------------------------
local disable_flow_above = tonumber(minetest.settings:get("dynamic_liquid_disable_flow_above"))
if disable_flow_above == nil or disable_flow_above >= 31000 then
-- version without altitude check
dynamic_liquid.liquid_abm = function(liquid, flowing_liquid, chance)
minetest.register_abm({
label = "dynamic_liquid " .. liquid .. " and " .. flowing_liquid,
nodenames = {liquid},
neighbors = {flowing_liquid},
interval = 1,
chance = chance or 1,
catch_up = false,
action = function(pos,node) -- Do everything possible to optimize this method
local check_pos = {x=pos.x, y=pos.y-1, z=pos.z}
local check_node = get_node(check_pos)
local check_node_name = check_node.name
if check_node_name == flowing_liquid or check_node_name == "air" then
set_node(pos, check_node)
set_node(check_pos, node)
return
end
local perm = all_direction_permutations[math.random(24)]
local dirs -- declare outside of loop so it won't keep entering/exiting scope
for i=1,4 do
dirs = perm[i]
-- reuse check_pos to avoid allocating a new table
check_pos.x = pos.x + dirs.x
check_pos.y = pos.y
check_pos.z = pos.z + dirs.z
check_node = get_node(check_pos)
check_node_name = check_node.name
if check_node_name == flowing_liquid or check_node_name == "air" then
set_node(pos, check_node)
set_node(check_pos, node)
return
end
end
end
})
dynamic_liquid.registered_liquids[liquid] = flowing_liquid
table.insert(dynamic_liquid.registered_liquid_neighbors, liquid)
end
else
-- version with altitude check
dynamic_liquid.liquid_abm = function(liquid, flowing_liquid, chance)
minetest.register_abm({
label = "dynamic_liquid " .. liquid .. " and " .. flowing_liquid .. " with altitude check",
nodenames = {liquid},
neighbors = {flowing_liquid},
interval = 1,
chance = chance or 1,
catch_up = false,
action = function(pos,node) -- Do everything possible to optimize this method
-- This altitude check is the only difference from the version above.
-- If the altitude check is disabled we don't ever need to make the comparison,
-- hence the two different versions.
if pos.y > disable_flow_above then
return
end
local check_pos = {x=pos.x, y=pos.y-1, z=pos.z}
local check_node = get_node(check_pos)
local check_node_name = check_node.name
if check_node_name == flowing_liquid or check_node_name == "air" then
set_node(pos, check_node)
set_node(check_pos, node)
return
end
local perm = all_direction_permutations[math.random(24)]
local dirs -- declare outside of loop so it won't keep entering/exiting scope
for i=1,4 do
dirs = perm[i]
-- reuse check_pos to avoid allocating a new table
check_pos.x = pos.x + dirs.x
check_pos.y = pos.y
check_pos.z = pos.z + dirs.z
check_node = get_node(check_pos)
check_node_name = check_node.name
if check_node_name == flowing_liquid or check_node_name == "air" then
set_node(pos, check_node)
set_node(check_pos, node)
return
end
end
end
})
dynamic_liquid.registered_liquids[liquid] = flowing_liquid
table.insert(dynamic_liquid.registered_liquid_neighbors, liquid)
end
end
if not minetest.get_modpath("default") then
return
end
local water = minetest.settings:get_bool("dynamic_liquid_water", true)
local river_water = minetest.settings:get_bool("dynamic_liquid_river_water", false)
local lava = minetest.settings:get_bool("dynamic_liquid_lava", true)
local water_probability = tonumber(minetest.settings:get("dynamic_liquid_water_flow_propability"))
if water_probability == nil then
water_probability = 1
end
local river_water_probability = tonumber(minetest.settings:get("dynamic_liquid_river_water_flow_propability"))
if river_water_probability == nil then
river_water_probability = 1
end
local lava_probability = tonumber(minetest.settings:get("dynamic_liquid_lava_flow_propability"))
if lava_probability == nil then
lava_probability = 5
end
local springs = minetest.settings:get_bool("dynamic_liquid_springs", true)
if water then
-- override water_source and water_flowing with liquid_renewable set to false
local override_def = {liquid_renewable = false}
minetest.override_item("default:water_source", override_def)
minetest.override_item("default:water_flowing", override_def)
end
if lava then
dynamic_liquid.liquid_abm("default:lava_source", "default:lava_flowing", lava_probability)
end
if water then
dynamic_liquid.liquid_abm("default:water_source", "default:water_flowing", water_probability)
end
if river_water then
dynamic_liquid.liquid_abm("default:river_water_source", "default:river_water_flowing", river_water_probability)
end
-- Flow-through nodes
-----------------------------------------------------------------------------------------------------------------------
local flow_through = minetest.settings:get_bool("dynamic_liquid_flow_through", true)
if flow_through then
local flow_through_directions = {
{{x=1,z=0},{x=0,z=1}},
{{x=0,z=1},{x=1,z=0}},
}
minetest.register_abm({
label = "dynamic_liquid flow-through",
nodenames = {"group:flow_through", "group:leaves", "group:sapling", "group:grass", "group:dry_grass", "group:flora", "groups:rail", "groups:flower"},
neighbors = dynamic_liquid.registered_liquid_neighbors,
interval = 1,
chance = 2, -- since liquid is teleported two nodes by this abm, halve the chance
catch_up = false,
action = function(pos)
local source_pos = {x=pos.x, y=pos.y+1, z=pos.z}
local dest_pos = {x=pos.x, y=pos.y-1, z=pos.z}
local source_node = get_node(source_pos)
local dest_node
local source_flowing_node = dynamic_liquid.registered_liquids[source_node.name]
local dest_flowing_node
if source_flowing_node ~= nil then
dest_node = minetest.get_node(dest_pos)
if dest_node.name == source_flowing_node or dest_node.name == "air" then
set_node(dest_pos, source_node)
set_node(source_pos, dest_node)
return
end
end
local perm = flow_through_directions[math.random(2)]
local dirs -- declare outside of loop so it won't keep entering/exiting scope
for i=1,2 do
dirs = perm[i]
-- reuse to avoid allocating a new table
source_pos.x = pos.x + dirs.x
source_pos.y = pos.y
source_pos.z = pos.z + dirs.z
dest_pos.x = pos.x - dirs.x
dest_pos.y = pos.y
dest_pos.z = pos.z - dirs.z
source_node = get_node(source_pos)
dest_node = get_node(dest_pos)
source_flowing_node = dynamic_liquid.registered_liquids[source_node.name]
dest_flowing_node = dynamic_liquid.registered_liquids[dest_node.name]
if (source_flowing_node ~= nil and (dest_node.name == source_flowing_node or dest_node.name == "air")) or
(dest_flowing_node ~= nil and (source_node.name == dest_flowing_node or source_node.name == "air"))
then
set_node(source_pos, dest_node)
set_node(dest_pos, source_node)
return
end
end
end,
})
local add_flow_through = function(node_name)
local node_def = minetest.registered_nodes[node_name]
if node_def == nil then
minetest.log("warning", "dynamic_liquid attempted to call add_flow_through on the node name "
.. node_name .. ", which was not found in minetest.registered_nodes.")
return
end
local new_groups = node_def.groups
new_groups.flow_through = 1
minetest.override_item(node_name,{groups = new_groups})
end
if minetest.get_modpath("default") then
for _, name in pairs({
"default:apple",
"default:papyrus",
"default:dry_shrub",
"default:bush_stem",
"default:acacia_bush_stem",
"default:sign_wall_wood",
"default:sign_wall_steel",
"default:ladder_wood",
"default:ladder_steel",
"default:fence_wood",
"default:fence_acacia_wood",
"default:fence_junglewood",
"default:fence_pine_wood",
"default:fence_aspen_wood",
}) do
add_flow_through(name)
end
end
if minetest.get_modpath("xpanes") then
add_flow_through("xpanes:bar")
add_flow_through("xpanes:bar_flat")
end
if minetest.get_modpath("carts") then
add_flow_through("carts:rail")
add_flow_through("carts:powerrail")
add_flow_through("carts:brakerail")
end
end
-- Springs
-----------------------------------------------------------------------------------------------------------------------
local function deep_copy(table_in)
local table_out = {}
for index, value in pairs(table_in) do
if type(value) == "table" then
table_out[index] = deep_copy(value)
else
table_out[index] = value
end
end
return table_out
end
local duplicate_def = function (name)
local old_def = minetest.registered_nodes[name]
return deep_copy(old_def)
end
-- register damp clay whether we're going to set the ABM or not, if the user disables this feature we don't want existing
-- spring clay to turn into unknown nodes.
local clay_def = duplicate_def("default:clay")
clay_def.description = S("Damp Clay")
if not springs then
clay_def.groups.not_in_creative_inventory = 1 -- take it out of creative inventory though
end
minetest.register_node("dynamic_liquid:clay", clay_def)
local data = {}
if springs then
local c_clay = minetest.get_content_id("default:clay")
local c_spring_clay = minetest.get_content_id("dynamic_liquid:clay")
-- Turn mapgen clay into spring clay
minetest.register_on_generated(function(minp, maxp, seed)
if minp.y >= water_level or maxp.y <= -15 then
return
end
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
vm:get_data(data)
for voxelpos, voxeldata in pairs(data) do
if voxeldata == c_clay then
data[voxelpos] = c_spring_clay
end
end
vm:set_data(data)
vm:write_to_map()
end)
minetest.register_abm({
label = "dynamic_liquid damp clay spring",
nodenames = {"dynamic_liquid:clay"},
neighbors = {"air", "default:water_source", "default:water_flowing"},
interval = 1,
chance = 1,
catch_up = false,
action = function(pos,node)
local check_node
local check_node_name
while pos.y < water_level do
pos.y = pos.y + 1
check_node = get_node(pos)
check_node_name = check_node.name
if check_node_name == "air" or check_node_name == "default:water_flowing" then
set_node(pos, {name="default:water_source"})
elseif check_node_name ~= "default:water_source" then
--Something's been put on top of this clay, don't send water through it
break
end
end
end
})
local spring_sounds = nil
if default.node_sound_gravel_defaults ~= nil then
spring_sounds = default.node_sound_gravel_defaults()
elseif default.node_sound_sand_defaults ~= nil then
spring_sounds = default.node_sound_dirt_defaults()
end
-- This is a creative-mode only node that produces a modest amount of water continuously no matter where it is.
-- Allow this one to turn into "unknown node" when this feature is disabled, since players had to explicitly place it.
minetest.register_node("dynamic_liquid:spring", {
description = S("Spring"),
_doc_items_longdesc = S("A natural spring that generates an endless stream of water source blocks"),
_doc_items_usagehelp = S("Generates one source block of water directly on top of itself once per second, provided the space is clear. If this natural spring is dug out the flow stops and it is turned into ordinary cobble."),
drops = "default:gravel",
tiles = {"default_cobble.png^[combine:16x80:0,-48=crack_anylength.png",
"default_cobble.png","default_cobble.png","default_cobble.png","default_cobble.png","default_cobble.png",
},
is_ground_content = false,
groups = {cracky = 3, stone = 2},
sounds = spring_sounds,
})
minetest.register_abm({
label = "dynamic_liquid creative spring",
nodenames = {"dynamic_liquid:spring"},
neighbors = {"air", "default:water_flowing"},
interval = 1,
chance = 1,
catch_up = false,
action = function(pos,node)
pos.y = pos.y + 1
local check_node = get_node(pos)
local check_node_name = check_node.name
if check_node_name == "air" or check_node_name == "default:water_flowing" then
set_node(pos, {name="default:water_source"})
end
end
})
end
local mapgen_prefill = minetest.settings:get_bool("dynamic_liquid_mapgen_prefill", true)
local waternodes
if mapgen_prefill then
local c_water = minetest.get_content_id("default:water_source")
local c_air = minetest.get_content_id("air")
waternodes = {}
local fill_to = function (vi, data, area)
if area:containsi(vi) and area:position(vi).y <= water_level then
if data[vi] == c_air then
data[vi] = c_water
table.insert(waternodes, vi)
end
end
end
-- local count = 0
local drop_liquid = function(vi, data, area, min_y)
if data[vi] ~= c_water then
-- we only care about water.
return
end
local start = vi -- remember the water node we started from
local ystride = area.ystride
vi = vi - ystride
if data[vi] ~= c_air then
-- if there's no air below this water node, give up immediately.
return
end
vi = vi - ystride -- There's air below the water, so move down one.
while data[vi] == c_air and area:position(vi).y > min_y do
-- the min_y check is here to ensure that we don't put water into the mapgen
-- border zone below our current map chunk where it might get erased by future mapgen activity.
-- if there's more air, keep going.
vi = vi - ystride
end
vi = vi + ystride -- Move back up one. vi is now pointing at the last air node above the first non-air node.
data[vi] = c_water
data[start] = c_air
-- count = count + 1
-- if count % 100 == 0 then
-- minetest.chat_send_all("dropped water " .. (start-vi)/ystride .. " at " .. minetest.pos_to_string(area:position(vi)))
-- end
end
minetest.register_on_generated(function(minp, maxp, seed)
if minp.y > water_level then
-- we're in the sky.
return
end
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
local area = VoxelArea:new{MinEdge=emin, MaxEdge=emax}
vm:get_data(data)
local maxp_y = maxp.y
local minp_y = minp.y
if maxp_y > -70 then
local top = vector.new(maxp.x, math.min(maxp_y, water_level), maxp.z) -- prevents flood fill from affecting any water above sea level
for vi in area:iterp(minp, top) do
if data[vi] == c_water then
table.insert(waternodes, vi)
end
end
while table.getn(waternodes) > 0 do
local vi = table.remove(waternodes)
local below = vi - area.ystride
local left = vi - area.zstride
local right = vi + area.zstride
local front = vi - 1
local back = vi + 1
fill_to(below, data, area)
fill_to(left, data, area)
fill_to(right, data, area)
fill_to(front, data, area)
fill_to(back, data, area)
end
else
-- Caves sometimes generate with liquid nodes hovering in mid air.
-- This immediately drops them straight down as far as they can go, reducing the ABM thrashing.
-- We only iterate down to minp.y+1 because anything at minp.y will never be dropped farther anyway.
for vi in area:iter(minp.x, minp_y+1, minp.z, maxp.x, maxp_y, maxp.z) do
-- fortunately, area:iter iterates through y columns going upward. Just what we need!
-- We could possibly be a bit more efficient by remembering how far we dropped then
-- last liquid node in a column and moving stuff down that far,
-- but for now let's keep it simple.
drop_liquid(vi, data, area, minp_y)
end
end
vm:set_data(data)
vm:write_to_map()
vm:update_liquids()
end)
end
local displace_liquid = minetest.settings:get_bool("dynamic_liquid_displace_liquid", true)
if displace_liquid then
local cardinal_dirs = {
{x= 0, y=0, z= 1},
{x= 1, y=0, z= 0},
{x= 0, y=0, z=-1},
{x=-1, y=0, z= 0},
{x= 0, y=-1, z= 0},
{x= 0, y=1, z= 0},
}
-- breadth-first search passing through liquid searching for air or flowing liquid.
local flood_search_outlet = function(start_pos, source, flowing)
local start_node = minetest.get_node(start_pos)
local start_node_name = start_node.name
if start_node_name == "air" or start_node_name == flowing then
return start_pos
end
local visited = {}
visited[minetest.hash_node_position(start_pos)] = true
local queue = {start_pos}
local queue_pointer = 1
while #queue >= queue_pointer do
local current_pos = queue[queue_pointer]
queue_pointer = queue_pointer + 1
for _, cardinal_dir in ipairs(cardinal_dirs) do
local new_pos = vector.add(current_pos, cardinal_dir)
local new_hash = minetest.hash_node_position(new_pos)
if visited[new_hash] == nil then
local new_node = minetest.get_node(new_pos)
local new_node_name = new_node.name
if new_node_name == "air" or new_node_name == flowing then
return new_pos
end
visited[new_hash] = true
if new_node_name == source then
table.insert(queue, new_pos)
end
end
end
end
return nil
end
-- Conserve liquids, when placing nodes in liquids try to find a place to displace the liquid to.
minetest.register_on_placenode(function(pos, newnode, placer, oldnode, itemstack, pointed_thing)
local flowing = dynamic_liquid.registered_liquids[oldnode.name]
if flowing ~= nil then
local dest = flood_search_outlet(pos, oldnode.name, flowing)
if dest ~= nil then
minetest.swap_node(dest, oldnode)
end
end
end)
end

View File

@ -0,0 +1,12 @@
# textdomain: dynamic_liquid
### init.lua ###
A natural spring that generates an endless stream of water source blocks=
Damp Clay=Feuchten Lehm
Generates one source block of water directly on top of itself once per second, provided the space is clear. If this natural spring is dug out the flow stops and it is turned into ordinary cobble.=
Spring=Quelle

View File

@ -0,0 +1,12 @@
# textdomain: dynamic_liquid
### init.lua ###
A natural spring that generates an endless stream of water source blocks=Un manantial natural que genera un flujo sin fín de bloques fuente de agua
Damp Clay=Arcilla húmeda
Generates one source block of water directly on top of itself once per second, provided the space is clear. If this natural spring is dug out the flow stops and it is turned into ordinary cobble.=Genera un bloque fuente de agua directamente encima de sí mismo una vez por segundo, en tanto el espacio esté libre. Si éste manantial natural es excavado el flujo se detiene y se convierte en adoquines comúnes.
Spring=Manantial

View File

@ -0,0 +1,12 @@
# textdomain: dynamic_liquid
### init.lua ###
A natural spring that generates an endless stream of water source blocks=
Damp Clay=Argile humide
Generates one source block of water directly on top of itself once per second, provided the space is clear. If this natural spring is dug out the flow stops and it is turned into ordinary cobble.=
Spring=Source

View File

@ -0,0 +1,12 @@
# textdomain: dynamic_liquid
### init.lua ###
A natural spring that generates an endless stream of water source blocks=
Damp Clay=
Generates one source block of water directly on top of itself once per second, provided the space is clear. If this natural spring is dug out the flow stops and it is turned into ordinary cobble.=
Spring=

View File

@ -0,0 +1,3 @@
name = dynamic_liquid
optional_depends = default, doc, xpanes, carts
description = Flowing dynamic liquids and ocean-maintenance springs.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,76 @@
#Makes water limited (it doesn't spawn new source blocks any more) and causes
#water blocks to move dynamically
dynamic_liquid_water (Dynamic water) bool true
#Causes river water blocks to move dynamically. Use caution when using this
#with the "valleys" mapgen, the streams generated by that mapgen have elevation
#changes and can make a mess if their water moves downhill uncontrolled.
dynamic_liquid_river_water (Dynamic river water) bool false
#Causes lava blocks to move dynamically
dynamic_liquid_lava (Dynamic lava) bool true
#Floatlands are "leaky", so if you wish to prevent their lakes from draining
#set this value to somewhere below the elevation the floatlands are generated
#at.
dynamic_liquid_disable_flow_above (Disable flow above this altitude) int 31000
#Causes natural clay deposits to act as water sources, seeping new water blocks
#into the space above them.
#Also adds a "spring" block to the creative inventory to serve as an infinite water
#source in creative mode.
dynamic_liquid_springs (Springs) bool true
#Allows dynamic liquids to "teleport" through nodes that should allow liquids to
#pass through. This includes the groups:
#leaves, sapling, grass, dry_grass, flora, rail, flower
#As well as select other nodes from the default game's definitions. Other mods
#can set nodes as flow-through by adding them to these groups or to the group
#"flow_through"
dynamic_liquid_flow_through (Flow-through) bool true
#During map generation, this mod can attempt to "pre-fill" underwater cave
#entrances by replacing air directly below water with additional water blocks.
#This process is only attempted below "sea level" and is not perfect - it will
#only fill down to the bottom of the map block being generated and it won't
#fill out openings to the sides. But it should hopefully reduce the magnitude
#of the whirlpools that form over cave entrances and the load on the active
#block modifier in these situations.
dynamic_liquid_mapgen_prefill (Mapgen water prefill) bool true
#Dynamic liquid movement causes trouble with the default lava cooling system.
#When set to true, this replaces the default lava cooling system with one
#that will not result in oceans being paved over by a single wandering
#lava block or lava seas being paved by a single wandering water block.
dynamic_liquid_new_lava_cooling (Revised lava cooling) bool true
#When this is enabled, obsidian blocks spawned by lava cooling will fall from
#their origin point like sand or gravel does. Otherwise, obsidian will still
#behave the same - this occurs only when it's first created.
dynamic_liquid_falling_obsidian (Falling obsidian) bool false
#When true, placing a block where a registered liquid source currently is
#will make a simple attempt to displace the liquid source somewhere else
#rather than destroying it.
dynamic_liquid_displace_liquid (Displace liquid) bool true
[Flow Rates]
#Sets the probability of water flow per block per second.
#That is, if this is set to 5 then there's a 1/5 chance per second that a water block
#will check if it should move. Increase this value to make water flow slower.
#This value only has an effect if dynamic water is set to true
dynamic_liquid_water_flow_propability (Water flow probability) int 1 1 100
#Sets the probability of river water flow per block per second.
#That is, if this is set to 5 then there's a 1/5 chance per second that a river water
#block will check if it should move. Increase this value to make river water flow
#slower.
#This value only has an effect if dynamic river water is set to true
dynamic_liquid_river_water_flow_propability (River water flow probability) int 1 1 100
#Sets the probability of lava flow per block per second.
#That is, if this is set to 5 then there's a 1/5 chance per second that a lava block
#will check if it should move. Increase this value to make lava flow slower.
#This value only has an effect if dynamic lava is set to true
dynamic_liquid_lava_flow_propability (Lava flow probability) int 5 1 100

2
mods/gas_lib/depends.txt Normal file
View File

@ -0,0 +1,2 @@
default
fire

71
mods/gas_lib/gasses.lua Normal file
View File

@ -0,0 +1,71 @@
local function add_group(name, group, val)
local defgroup=table.copy(minetest.registered_nodes[name].groups)
defgroup[group] = val
minetest.override_item(name, { groups=defgroup })
end
gas_lib.register_gas("gas_lib:smoke", {
description = 'Smoke',
tiles = {{
name = "smoke.png^gui_hb_bg.png",
--backface_culling=false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 6,
},
}},
inventory_image = "smoke.png^[verticalframe:16:1^gui_hb_bg.png",
wield_image = "smoke.png^[verticalframe:16:1^gui_hb_bg.png",
post_effect_color = {a = 60, r = 100, g = 100, b = 100},
damage_per_second = 1,
drowning = 1,
interval = 3,
weight = -8,
deathchance = 5,
})
gas_lib.register_gas("gas_lib:steam", {
description = 'Steam',
tiles = {{
name = "steam.png^gui_hb_bg.png",
--backface_culling=false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 6,
},
}},
inventory_image = "steam.png^[verticalframe:16:1^gui_hb_bg.png",
wield_image = "steam.png^[verticalframe:16:1^gui_hb_bg.png",
post_effect_color = {a = 60, r = 100, g = 100, b = 100},
damage_per_second = 3,
interval = 3,
weight = -7,
liquid = "default:water_source",
deathchance = 5,
})
minetest.register_abm{
label="Smoke Gen",
nodenames= {"group:smokey"},
neighbors={"air"},
interval=4,
chance=1,
action=function(pos)
local newpos = minetest.find_node_near(pos, 1, "air")
if newpos and math.random(1, minetest.get_item_group(minetest.get_node(pos).name, "smokey"))==1 then
minetest.add_node(newpos, {name = "gas_lib:smoke"})
end
end
}
local water_def = minetest.registered_nodes["default:water_source"]
water_def.groups.vaporizable = 1
minetest.override_item("default:water_source", { gas="gas_lib:steam", groups = water_def.groups, gas_byproduct = "default:dirt"})
add_group("default:furnace_active", "smokey", 1)
add_group("fire:basic_flame", "smokey", 2)
add_group("default:lava_source", "smokey", 4)

137
mods/gas_lib/init.lua Normal file
View File

@ -0,0 +1,137 @@
gas_lib = {}
local function shuffle(tbl)
for i = #tbl, 2, -1 do
local j = math.random(i)
tbl[i], tbl[j] = tbl[j], tbl[i]
end
end
function gas_lib.find_air_neighbor(pos)
local gn=minetest.get_node
if math.random(3) == 1 and gn(pos).name == "air" then return pos end
local pos_table = {{"z", 0}, {"z", 1}, {"z", -1}, {"x", 1}, {"x", -1}}
shuffle(pos_table)
for k, v in pairs(pos_table) do
local newpos = vector.new(pos)
newpos[v[1]] = newpos[v[1]] + v[2]
if gn(newpos).name == "air" then return newpos end
end
return nil
end
function gas_lib.tick(itemstring, pos, elapsed)
local def = minetest.registered_nodes[itemstring]
local node = minetest.get_node(pos)
if node.param2 == 0 then node.param2 = def.lifetime+1 end
--node deletion
if math.random(100) <= def.deathchance then
minetest.remove_node(pos)
return
end
local pos1 = vector.new({x = pos.x + 1, y = pos.y + 1, z = pos.z + 1})
local pos2 = vector.new({x = pos.x - 1, y = pos.y - 1, z = pos.z - 1})
local nodes = minetest.find_nodes_in_area(pos1, pos2, "air")
if #nodes == 0 then minetest.get_node_timer(pos):start(def.interval+math.random(10)/10) return end -- gas has no space to move, may as well not do anything
if node.param2 ~= 0 and #nodes > 13 then
node.param2 = node.param2 - 1
if node.param2 <=1 then
minetest.remove_node(pos)
return
else
minetest.swap_node(pos, node)
end
end
local rand = math.random(10)
local weight = def.weight
if weight == 0 then --if has no weight, have a small chance to either go up or down
weight = math.random(2)
if weight == 2 then weight = -1 end
end
if rand <= math.abs(weight) then -- move up/down
local sign = weight/math.abs(weight)
local newpos = vector.new(pos)
newpos.y = newpos.y - sign
newpos = gas_lib.find_air_neighbor(newpos)
if newpos then
minetest.remove_node(pos)
minetest.add_node(newpos, node)
return
end
end
--hasn't moved up or down, so move horizontally.
local newpos = gas_lib.find_air_neighbor(vector.new(pos))
if newpos then
minetest.remove_node(pos)
minetest.add_node(newpos, node)
return
end
minetest.get_node_timer(pos):start(def.interval+math.random(10)/10)
end
local function table_combine(table1, table2)
local newtable = table.copy(table1) -- table 1 gets written over
for index,data in pairs(table2) do newtable[index] = data end
return newtable
end
local defaultdef = {
drawtype = "glasslike",
paramtype = "light",
paramtype2 = "none",
drop="",
use_texture_alpha=true,
sunlight_propagates = true,
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
floodable= true,
interval = 2.5,
lifetime = 10, --how many intervals it can last in open air
weight = 0, --chance out of 10 to go up, use negative to go down
deathchance = 0, --chance out of 100 to randomly die
on_flood = function(pos, oldnode, newnode)
local newpos = gas_lib.find_air_neighbor(pos)
if newpos then
minetest.add_node(newpos, oldnode)
end
end
}
function gas_lib.register_gas(itemstring, def)
def = table_combine(defaultdef, def)
def.on_construct = function(pos) minetest.get_node_timer(pos):start(def.interval+math.random(10)/10) end
def.on_timer = function(pos, elapsed) gas_lib.tick(itemstring, pos, elapsed) end
if not def.groups then def.groups = {} end
if not def.groups.gas then def.groups.gas = 1 end
minetest.register_node(itemstring, def)
end
minetest.register_abm{
label="Remove gas",
nodenames= {"group:gas"},
interval=60,
chance=20,
action=function(pos)
minetest.remove_node(pos)
end
}
minetest.register_lbm{
name="gas_lib:ensuretimer",
nodenames= {"group:gas"},
run_at_every_load = true,
action=function(pos)
local timer = minetest.get_node_timer(pos)
if not timer:is_started() then
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
minetest.get_node_timer(pos):start(def.interval+math.random(10)/10)
end
end
}
local path = minetest.get_modpath(minetest.get_current_modname())
dofile(path .. "/gasses.lua")
dofile(path .. "/tools.lua")

View File

@ -0,0 +1,2 @@
-- Animated smoke texture courtesy of Texmex.
-- License for textures: CC--BY-SA 4.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

333
mods/gas_lib/tools.lua Normal file
View File

@ -0,0 +1,333 @@
local S = default.get_translator
local function furnace_active(fuel_percent)
return "size[8,8.5]"..
"list[context;fuel;2.75,2.5;1,1;]"..
"image[2.75,1.5;1,1;default_furnace_fire_bg.png^[lowpart:"..
(fuel_percent)..":default_furnace_fire_fg.png]"..
"list[current_player;main;0,4.25;8,1;]"..
"list[current_player;main;0,5.5;8,3;8]"..
"listring[context;fuel]"..
"listring[current_player;main]"..
default.get_hotbar_bg(0, 4.25)
end
local function furnace_inactive()
return "size[8,8.5]"..
"list[context;fuel;2.75,2.5;1,1;]"..
"image[2.75,1.5;1,1;default_furnace_fire_bg.png]"..
"list[current_player;main;0,4.25;8,1;]"..
"list[current_player;main;0,5.5;8,3;8]"..
"listring[context;fuel]"..
"listring[current_player;main]"..
default.get_hotbar_bg(0, 4.25)
end
--
-- Node callback functions that are the same for active and inactive furnace
--
local function can_dig(pos, player)
local meta = minetest.get_meta(pos);
local inv = meta:get_inventory()
return inv:is_empty("fuel")
end
local function allow_metadata_inventory_put(pos, listname, index, stack, player)
if minetest.is_protected(pos, player:get_player_name()) then
return 0
end
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
if listname == "fuel" then
if minetest.get_craft_result({method="fuel", width=1, items={stack}}).time ~= 0 then
if inv:is_empty("src") then
meta:set_string("infotext", S("Furnace is empty"))
end
return stack:get_count()
else
return 0
end
end
end
local function allow_metadata_inventory_move(pos, from_list, from_index, to_list, to_index, count, player)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
local stack = inv:get_stack(from_list, from_index)
return allow_metadata_inventory_put(pos, to_list, to_index, stack, player)
end
local function allow_metadata_inventory_take(pos, listname, index, stack, player)
if minetest.is_protected(pos, player:get_player_name()) then
return 0
end
return stack:get_count()
end
local function swap_node(pos, name)
local node = minetest.get_node(pos)
if node.name == name then
return
end
node.name = name
minetest.swap_node(pos, node)
end
local function vaporize(p, airabove)
if not p then return end
local def = minetest.registered_nodes[minetest.get_node(p).name]
if def.gas then
if airabove then
minetest.add_node({x=p.x,y=p.y+1,z=p.z}, {name = def.gas})
else
minetest.add_node(p, {name = def.gas})
end
end
if def.gas_byproduct and math.random(def.gas_byproduct_chance or 4) == 1 then
minetest.swap_node(p, {name=def.gas_byproduct})
elseif airabove then
minetest.remove_node(p)
end
end
local function furnace_node_timer(pos, elapsed)
--
-- Initialize metadata
--
local meta = minetest.get_meta(pos)
local fuel_time = meta:get_float("fuel_time") or 0
local fuel_totaltime = meta:get_float("fuel_totaltime") or 0
local inv = meta:get_inventory()
local fuellist
local fuel
local update = true
while elapsed > 0 and update do
update = false
fuellist = inv:get_list("fuel")
local el = math.min(elapsed, fuel_totaltime - fuel_time)
if fuel_time < fuel_totaltime then
-- The furnace is currently active and has enough fuel
fuel_time = fuel_time + el
if elapsed >= 1 and math.random(10) == 1 then
local airabove = true
local p = minetest.find_nodes_in_area_under_air({x=pos.x+1,y=pos.y+1,z=pos.z+1}, {x=pos.x-1,y=pos.y-1,z=pos.z-1}, "group:vaporizable")
p = p[math.random(#p)]
if not p then
airabove = false
p = minetest.find_nodes_in_area({x=pos.x+1,y=pos.y+1,z=pos.z+1}, {x=pos.x-1,y=pos.y-1,z=pos.z-1}, "group:vaporizable")
p = p[math.random(#p)]
end
vaporize(p,airabove)
end
else
local afterfuel
fuel, afterfuel = minetest.get_craft_result({method = "fuel", width = 1, items = fuellist})
fuel.time = fuel.time*2
if fuel.time == 0 then
-- No valid fuel in fuel list
fuel_totaltime = 0
fuel_time = 0
else
-- Take fuel from fuel list
inv:set_stack("fuel", 1, afterfuel.items[1])
-- Put replacements in dst list or drop them on the furnace.
local replacements = fuel.replacements
if replacements[1] then
local above = vector.new(pos.x, pos.y + 1, pos.z)
local drop_pos = minetest.find_node_near(above, 1, {"air"}) or above
minetest.item_drop(replacements[1], nil, drop_pos)
end
update = true
fuel_totaltime = fuel.time + (fuel_totaltime - fuel_time)
fuel_time = 0
end
end
elapsed = elapsed - el
end
if fuel and fuel_totaltime > fuel.time then
fuel_totaltime = fuel.time
end
--
-- Update formspec, infotext and node
--
local formspec
local item_state
local fuel_state = S("Empty")
local active = false
local result = false
if fuel_totaltime ~= 0 then
active = true
local fuel_percent = 100 - math.floor(fuel_time / fuel_totaltime * 100)
fuel_state = S("@1%", fuel_percent)
formspec = furnace_active(fuel_percent, 0)
swap_node(pos, "gas_lib:furnace_active")
-- make sure timer restarts automatically
result = true
else
if fuellist and not fuellist[1]:is_empty() then
fuel_state = S("@1%", 0)
end
formspec = furnace_inactive()
swap_node(pos, "gas_lib:furnace")
-- stop timer on the inactive furnace
minetest.get_node_timer(pos):stop()
end
local infotext
if active then
infotext = S("Furnace active")
else
infotext = S("Furnace inactive")
end
infotext = infotext .. "\n" .. S("(Fuel: @1)", fuel_state)
--
-- Set meta values
--
meta:set_float("fuel_totaltime", fuel_totaltime)
meta:set_float("fuel_time", fuel_time)
meta:set_string("formspec", formspec)
meta:set_string("infotext", infotext)
return result
end
--
-- Node definitions
--
minetest.register_node("gas_lib:furnace", {
description = S("Furnace"),
tiles = {
"default_furnace_top.png", "default_furnace_bottom.png",
"default_furnace_side.png", "default_furnace_side.png",
"default_furnace_side.png", "default_furnace_front.png"
},
paramtype2 = "facedir",
groups = {cracky=2},
legacy_facedir_simple = true,
is_ground_content = false,
sounds = default.node_sound_stone_defaults(),
can_dig = can_dig,
on_timer = furnace_node_timer,
on_construct = function(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size('fuel', 1)
furnace_node_timer(pos, 0)
end,
on_metadata_inventory_move = function(pos)
minetest.get_node_timer(pos):start(1.0)
end,
on_metadata_inventory_put = function(pos)
-- start timer function, it will sort out whether furnace can burn or not.
minetest.get_node_timer(pos):start(1.0)
end,
on_blast = function(pos)
local drops = {}
default.get_inventory_drops(pos, "src", drops)
default.get_inventory_drops(pos, "fuel", drops)
default.get_inventory_drops(pos, "dst", drops)
drops[#drops+1] = "gas_lib:furnace"
minetest.remove_node(pos)
return drops
end,
allow_metadata_inventory_put = allow_metadata_inventory_put,
allow_metadata_inventory_move = allow_metadata_inventory_move,
allow_metadata_inventory_take = allow_metadata_inventory_take,
})
minetest.register_node("gas_lib:furnace_active", {
description = S("Furnace"),
tiles = {
"default_furnace_top.png", "default_furnace_bottom.png",
"default_furnace_side.png", "default_furnace_side.png",
"default_furnace_side.png",
{
image = "default_furnace_front_active.png",
backface_culling = false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 1.5
},
}
},
paramtype2 = "facedir",
light_source = 8,
drop = "gas_lib:furnace",
groups = {cracky=2, not_in_creative_inventory=1, smokey = 2},
legacy_facedir_simple = true,
is_ground_content = false,
sounds = default.node_sound_stone_defaults(),
on_timer = furnace_node_timer,
can_dig = can_dig,
allow_metadata_inventory_put = allow_metadata_inventory_put,
allow_metadata_inventory_move = allow_metadata_inventory_move,
allow_metadata_inventory_take = allow_metadata_inventory_take,
})
minetest.register_craft({
output = "gas_lib:furnace",
recipe = {
{"group:stone", "group:stone", "group:stone"},
{"group:stone", "default:wood", "group:stone"},
{"group:stone", "group:stone", "group:stone"},
}
})
local exchangertick = 10
minetest.register_node("gas_lib:heat_exchanger", {
description = "Heat Exchanger",
tiles = {"default_stone.png"},
groups = {cracky = 1},
on_construct = function(pos)
minetest.get_node_timer(pos):start(exchangertick)
end,
on_timer = function(pos, elapsed)
local rn = minetest.registered_nodes
local above = rn[minetest.get_node({x=pos.x,y=pos.y+1,z=pos.z}).name]
local below = rn[minetest.get_node({x=pos.x,y=pos.y-1,z=pos.z}).name]
if not above or not below then return true end
if not above.gas then return true end
if not below.liquid then return true end
if math.random(3) == 3 then--heat liquid
vaporize({x=pos.x,y=pos.y+1,z=pos.z}, minetest.get_node({x=pos.x,y=pos.y+2,z=pos.z}).name == "air")
else --cool gas
minetest.swap_node({x=pos.x,y=pos.y-1,z=pos.z}, {name=below.liquid})
end
return true
end
})
minetest.register_lbm{
name="gas_lib:heattimer",
nodenames= {"gas_lib:heat_exchanger", "gas_lib:furnace_active"},
run_at_every_load = true,
action=function(pos)
local timer = minetest.get_node_timer(pos)
if not timer:is_started() then
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
minetest.get_node_timer(pos):start(exchangertick)
end
end
}

6
mods/oil/depends.txt Normal file
View File

@ -0,0 +1,6 @@
default
gas_lib
bucket
dynamic_liquid
currency
waterworks?

507
mods/oil/init.lua Normal file
View File

@ -0,0 +1,507 @@
-- textures LGLv2.1" = "ShadMOrdre. Tenplus1, Gail de Sailly, VannessaE, runs, and numerous others."
oil = {}
oil.fueling = {}
minetest.register_node("oil:gasoline_source", {
description = "Gasoline Source",
drawtype = "liquid",
waving = 3,
tiles = {
{
name = "cars_gasoline_source_animated.png",
backface_culling = false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2.0,
},
},
{
name = "cars_gasoline_source_animated.png",
backface_culling = true,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2.0,
},
},
},
alpha = 240,
paramtype = "light",
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
is_ground_content = false,
drop = "",
drowning = 1,
liquid_renewable = false,
liquidtype = "source",
liquid_alternative_flowing = "oil:gasoline_flowing",
liquid_alternative_source = "oil:gasoline_source",
liquid_viscosity = 4,
post_effect_color = {a = 103, r = 30, g = 60, b = 90},
groups = {water = 3, liquid = 3, vaporizable = 1, flammable = 1},
sounds = default.node_sound_water_defaults(),
gas = "oil:gasoline_vapor"
})
minetest.register_node("oil:gasoline_flowing", {
description = "Gasoline Flowing",
drawtype = "flowingliquid",
waving = 3,
tiles = {"cars_gasoline_source.png"},
special_tiles = {
{
name = "cars_gasoline_flowing_animated.png",
backface_culling = false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2,
},
},
{
name = "cars_gasoline_flowing_animated.png",
backface_culling = true,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2,
},
},
},
alpha = 240,
paramtype = "light",
paramtype2 = "flowingliquid",
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
is_ground_content = false,
drop = "",
drowning = 1,
liquid_renewable = false,
liquidtype = "flowing",
liquid_alternative_flowing = "oil:gasoline_flowing",
liquid_alternative_source = "oil:gasoline_source",
liquid_viscosity = 4,
post_effect_color = {a = 103, r = 30, g = 60, b = 90},
groups = {water = 3, liquid = 3, not_in_creative_inventory = 1, flammable = 1},
sounds = default.node_sound_water_defaults(),
})
minetest.register_node("oil:oil_source", {
description = "Crude Oil Source",
drawtype = "liquid",
waving = 3,
tiles = {
{
name = "cars_oil_source_animated.png",
backface_culling = false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2.0,
},
},
{
name = "cars_oil_source_animated.png",
backface_culling = true,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2.0,
},
},
},
alpha = 240,
paramtype = "light",
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
is_ground_content = false,
drop = "",
drowning = 1,
liquid_renewable = false,
liquidtype = "source",
liquid_alternative_flowing = "oil:oil_flowing",
liquid_alternative_source = "oil:oil_source",
liquid_viscosity = 4,
post_effect_color = {a = 103, r = 30, g = 60, b = 90},
groups = {water = 3, liquid = 3, vaporizable = 1, flammable = 1},
sounds = default.node_sound_water_defaults(),
gas = "oil:gasoline_vapor",
gas_byproduct = "oil:tar",
gas_byproduct_chance = 6
})
minetest.register_node("oil:oil_flowing", {
description = "Crude Oil Flowing",
drawtype = "flowingliquid",
waving = 3,
tiles = {"cars_oil_source.png"},
special_tiles = {
{
name = "cars_oil_flowing_animated.png",
backface_culling = false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2,
},
},
{
name = "cars_oil_flowing_animated.png",
backface_culling = true,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2,
},
},
},
alpha = 240,
paramtype = "light",
paramtype2 = "flowingliquid",
walkable = false,
pointable = false,
diggable = false,
buildable_to = true,
is_ground_content = false,
drop = "",
drowning = 1,
liquid_renewable = false,
liquidtype = "flowing",
liquid_alternative_flowing = "oil:oil_flowing",
liquid_alternative_source = "oil:oil_source",
liquid_viscosity = 4,
post_effect_color = {a = 103, r = 30, g = 60, b = 90},
groups = {water = 3, liquid = 3, not_in_creative_inventory = 1, flammable = 1},
sounds = default.node_sound_water_defaults(),
})
dynamic_liquid.liquid_abm("oil:oil_source", "oil:oil_flowing", 1)
dynamic_liquid.liquid_abm("oil:gasoline_source", "oil:gasoline_flowing", 1)
if waterworks then
waterworks.register_liquid("oil:oil_source", {flowing = "oil:oil_flowing"})
waterworks.register_liquid("oil:gasoline_source", {flowing = "oil:gasoline_flowing"})
end
minetest.register_node("oil:tar", {
description = "Tar",
tiles = {"cars_oil_source.png"},
})
minetest.register_ore({
ore_type = "blob",
ore = "oil:oil_source",
wherein = "default:stone",
clust_scarcity = 32 * 32 * 32,
clust_num_ores = 16,
clust_size = 6,
y_min = -31000,
y_max = -64,
})
bucket.register_liquid(
"oil:gasoline_source",
"oil:gasoline_flowing",
"oil:bucket_gasoline",
"bucket_water.png",
"Gasoline Bucket",
{tool = 1, gasoline_bucket = 1}
)
bucket.register_liquid(
"oil:oil_source",
"oil:oil_flowing",
"oil:bucket_oil",
"bucket_water.png",
"Oil Bucket",
{tool = 1, oil_bucket = 1}
)
gas_lib.register_gas("oil:gasoline_vapor", {
description = 'Gasoline Vapor',
tiles = {{
name = "smoke.png^gui_hb_bg.png",
--backface_culling=false,
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 6,
},
}},
inventory_image = "smoke.png^[verticalframe:16:1^gui_hb_bg.png",
wield_image = "smoke.png^[verticalframe:16:1^gui_hb_bg.png",
post_effect_color = {a = 60, r = 100, g = 100, b = 100},
damage_per_second = 1,
drowning = 1,
interval = 3,
weight = -8,
deathchance = 3,
liquid = "oil:gasoline_source"
})
local bucket_liters = 1000
local pump_capacity = 2*bucket_liters
local reclick_stopper = {}
local function form_pump(pos, owner)
local meta = minetest.get_meta(pos)
local spos = pos.x .. "," .. pos.y .. "," .. pos.z
local price = meta:get_string("price")
if price == "" then price = "0" end
local form = "size[8,7]" ..
"list[current_player;main;0,3;8,4;0]" ..
"list[nodemeta:"..spos..";input;6,0.9;1,1.1;0]" ..
"label[5.7,0.1;Gas: "..meta:get_int("gas").."L]" ..
"label[5.7,0.4;Bought: "..meta:get_int("gasbought").."L]" ..
"button_exit[6,1.8;1,1;buy;Buy]"..
"label[5.7,-.2;Price: "..price.." minegeld/L]"
if owner then
form = form..
"label[0.5,0;Customers gave:]" ..
"list[nodemeta:" .. spos .. ";output;0.5,0.5;2.5,2;0]"..
"field[3,0.6;2.5,1;price;Price (minegeld/L);"..price.."]"
else
end
return form
end
local form_table = {}
local function rotateVector(x, y, a)
local c = math.cos(a)
local s = math.sin(a)
return c*x - s*y, s*x + c*y
end
oil.stopfuel = function(name)
local data = oil.fueling[name]
if not data then return end
if data.obj then
data.obj:remove()
end
local meta = minetest.get_meta(data.pos)
meta:set_string("name", "")
minetest.get_node_timer(data.pos):stop()
oil.fueling[name] = nil
end
minetest.register_entity("oil:line", {
hp_max = 1,
physical = false,
pointable = false,
weight = 5,
collisionbox = {-0.1,-0.1,-0.1, 0.1,0.1,0.1},
visual = "cube",
visual_size = {x=.05, y=.1},
textures = {"blackline.png", "blackline.png", "blackline.png", "blackline.png", "blackline.png", "blackline.png"}, -- number of required textures depends on visual
colors = {}, -- number of required colors depends on visual
spritediv = {x=1, y=1},
initial_sprite_basepos = {x=0, y=0},
is_visible = true,
makes_footstep_sound = false,
automatic_rotate = 0,
on_step = function(self, dtime)
if self.startobj then
self.start = self.startobj:get_pos()
end
if self.finishobj then
self.finish = self.finishobj:get_pos()
if self.finishoffset then
local offset = table.copy(self.finishoffset)
local yaw
if self.finishobj:is_player() then
yaw = self.finishobj:get_look_horizontal() or 0
else
yaw = self.finishobj:get_yaw() or 0
end
offset.x, offset.z = rotateVector(offset.x, offset.z, yaw)
self.finish = vector.add(self.finish,offset)
end
end
local sp = self.start
local fp = self.finish
if not sp or not fp then self.object:remove() return end
if self.laststart and self.lastfinish and vector.equals(self.laststart, sp) and vector.equals(self.lastfinish, fp) then return end
local dist = vector.distance(sp, fp)
if dist > 4 then
for name, data in pairs(oil.fueling) do
if data.obj == self.object then
oil.stopfuel(name)
end
end
return
end
local delta = vector.subtract(sp, fp)
local yaw = math.atan2(delta.z, delta.x) - math.pi / 2
local pitch = math.atan2(delta.y, math.sqrt(delta.z*delta.z + delta.x*delta.x))
pitch = pitch + math.pi/2
self.object:move_to({x=(sp.x+fp.x)/2, y=(sp.y+fp.y)/2, z=(sp.z+fp.z)/2, })
self.object:set_rotation({x=pitch, y=yaw, z=0})
self.object:set_properties({visual_size = {x=.05, y=dist}})
self.laststart = sp
self.lastfinish = fp
end,
on_activate = function(self, staticdata, dtime_s)
if not staticdata or staticdata == "" then self.object:remove() return end
end
})
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "oil:gas_pump" then return end
if not player then return end
local name = player:get_player_name()
local pos = form_table[name]
if not pos then return end
local meta = minetest.get_meta(pos)
if fields.buy then
local inv = meta:get_inventory()
local price = tonumber(meta:get_string("price"))
local input = inv:get_stack("input", 1)
local moneys = {minegeld = 1, minegeld_5 = 5, minegeld_10 = 10}
local amount = (moneys[string.gsub(input:get_name(), "currency:", "")] or 0) * input:get_count()
if price > 0 and (amount >= price or meta:get_int("gasbought") > 0) then
local gasbought = math.floor(amount/price)
local gas = meta:get_int("gas")
if gasbought > gas then gasbought = gas end
local change = math.floor(amount-gasbought*price)
local output = ItemStack({name = "currency:minegeld", count = amount-change})
local changestack = ItemStack({name = "currency:minegeld", count = change})
if inv:room_for_item("output", output) then
if change > 0 then
inv:set_stack("input", 1, changestack)
else
inv:set_stack("input", 1, ItemStack())
end
meta:set_int("gas", gas-gasbought)
inv:add_item("output", output)
gasbought = gasbought+meta:get_int("gasbought")
meta:set_int("gasbought", gasbought)
if gasbought > 0 then
minetest.chat_send_player(name, "Punch the car you wish to gas up.")
local obj = minetest.add_entity(pos, "oil:line", "sup")
local ent = obj:get_luaentity()
local offset = {x=.4,y=.1,z=-.4}
local yaw = minetest.dir_to_yaw(minetest.facedir_to_dir(minetest.get_node(pos).param2))
offset.x, offset.z = rotateVector(offset.x, offset.z, yaw)
ent.start = vector.add(pos, offset)
ent.finishobj = player
ent.finishoffset = {x=0,y=1.2,z=.3}
oil.stopfuel(name)
oil.fueling[name] = {pos = pos, obj = obj}
end
else
minetest.chat_send_player(name, "Pump is full and cannot fit payment, contact pump owner.")
end
end
end
if fields.key_enter_field == "price" then
local owner = meta:get_string("owner")
if owner == "" or owner == name then
if tonumber(fields.price) then
meta:set_string("price", fields.price)
end
end
end
if fields.quit then
form_table[name] = nil
return true
end
end)
minetest.register_node("oil:pump", {
description = "Gas Pump",
drawtype = "mesh",
mesh = "gaspump.b3d",
paramtype2 = "facedir",
tiles = {"gaspumpUV.png"},
groups = {cracky = 3, stone = 1},
sounds = default.node_sound_stone_defaults(),
on_rightclick = function(pos, node, clicker)
if not clicker then return end
local name = clicker:get_player_name()
if not name then return end
local meta = minetest.get_meta(pos)
local owner = meta:get_string("owner")
minetest.show_formspec(name,"oil:gas_pump", form_pump(pos, owner == "" or owner == name))
form_table[name] = pos
end,
after_place_node = function(pos, placer)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
inv:set_size("input", 1)
inv:set_size("output", 4)
meta:set_string("owner", placer:get_player_name() or "")
meta:set_string("infotext", "Gas Pump (owned by "..meta:get_string("owner")..")")
end,
on_punch = function(pos, node, puncher, pointed_thing)
if not puncher then return end
local meta = minetest.get_meta(pos)
local name = puncher:get_player_name()
if not name then return end
if meta:get_string("owner") ~= "" and meta:get_string("owner") ~= name then return end
if reclick_stopper[name] then reclick_stopper[name] = nil return end
local wield = puncher:get_wielded_item():get_name()
local gas = meta:get_int("gas") or 0
if wield == "bucket:bucket_empty" then
if gas >= bucket_liters then
meta:set_string("gas", gas-bucket_liters)
puncher:set_wielded_item("oil:bucket_gasoline")
reclick_stopper[name] = true
minetest.after(.5, function() reclick_stopper[name] = nil end)
end
elseif wield == "oil:bucket_gasoline" then
if gas + bucket_liters <= pump_capacity then
puncher:set_wielded_item("bucket:bucket_empty")
meta:set_string("gas", gas+bucket_liters)
reclick_stopper[name] = true
minetest.after(.5, function() reclick_stopper[name] = nil end)
end
end
end,
on_timer = function(pos, elapsed)
local meta = minetest.get_meta(pos)
local name = meta:get_string("name")
if name == "" then return end
local data = oil.fueling[name]
if not data then return end
if not data.obj or data.obj:is_player() then return end
local ent = data.obj:get_luaentity()
if not ent or not ent.finishobj then return end
local def = cars_registered_cars[ent.finishobj:get_entity_name()]
if not def then return end
local carent = ent.finishobj:get_luaentity()
local maxgas = def.gas_cap or 50
if not carent.gas then carent.gas = 0 end
local gas = meta:get_int("gasbought") or 0
if gas == 0 then oil.stopfuel(name) return end
meta:set_int("gasbought", gas - 1)
carent.gas = carent.gas + 1
--minetest.chat_send_all(carent.gas)
if carent.gas >= maxgas then carent.gas = maxgas oil.stopfuel(name) return end
return true
end
})

BIN
mods/oil/models/gaspump.b3d Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,4 @@
default
dynamic_liquid
bucket
waterworks?

View File

@ -0,0 +1,42 @@
local def = table.copy(minetest.registered_nodes["default:water_source"])
def.liquid_alternative_source = "static_ocean:water_source"
def.liquid_alternative_flowing = "static_ocean:water_flowing"
def.liquid_renewable = true
minetest.register_node("static_ocean:water_source", def)
local def = table.copy(minetest.registered_nodes["default:water_flowing"])
def.liquid_alternative_source = "static_ocean:water_source"
def.liquid_alternative_flowing = "static_ocean:water_flowing"
def.liquid_renewable = true
minetest.register_node("static_ocean:water_flowing", def)
local source = "static_ocean:water_source"
local flowing = "static_ocean:water_flowing"
bucket.liquids[source] = {
source = source,
flowing = flowing,
itemname = "bucket:bucket_water",
force_renew = false,
}
bucket.liquids[flowing] = bucket.liquids[source]
minetest.register_alias_force("mapgen_water_source", "static_ocean:water_source")--replace mapgen water with static water (not effected by dynamic_liquid)
if waterworks then
waterworks.register_liquid("static_ocean:water_source", {flowing = "static_ocean:water_flowing", replace = "default:water_source"})
end
minetest.register_abm({
label = "delete normal water near ocean",
nodenames = {"static_ocean:water_source"},
neighbors = {"default:water_source"},
interval = 4,
chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider)
local pos1 = vector.subtract(pos, 1)
local pos2 = vector.add(pos, 1)
local nodes = minetest.find_nodes_in_area(pos1, pos2, "default:water_source")
for index, nodepos in pairs(nodes) do
minetest.remove_node(nodepos)
end
end,
})

2
mods/waterworks/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

41
mods/waterworks/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 FaceDeer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

56
mods/waterworks/README.md Normal file
View File

@ -0,0 +1,56 @@
This mod provides a set of nodes that allow large quantities of water to be pumped from one place to another, potentially over very long distances.
**Important Note:** The default behaviour of water in Minetest does not actually lend itself well to this kind of activity. In particular, ``default:water_source`` has ``liquid_renewable = true`` set in its node definition, which often causes new water nodes to be created where old ones are removed.
This mod includes an optional setting that disables ``liquid_renewable`` for default water, but for best effect it is highly recommended that this mod be used in conjunction with the [dynamic_liquid](https://github.com/minetest-mods/dynamic_liquid) mod.
The [airtanks](https://github.com/minetest-mods/airtanks) mod can also be helpful for players who are interested in doing large-scale underwater construction projects.
A pipe network is only active when a player is near one of the terminal nodes attached to it. The map blocks containing all terminal nodes for the network will be forceloaded as long as the pipe network is active, allowing water to be added or removed from remote locations, but note that water will be unable to flow into or out from those remote map blocks into adjoining blocks so it may behoove a player to visit these places from time to time to ensure continued flow.
## Pipes
The core node type introduced by this mod is the pipe. When pipes are laid adjacent to each other they connect up to form a pipe network, to which inlets, outlets, and pumps can be connected. All contiguous pipes are part of the same network, and all terminal nodes connected to that network will be able to interact with each other through the pipes.
Pipes automatically connect to other pipes through any of their six faces.
## Terminals
Terminals can only be connected to a pipe network via one face, the side that by default is facing away from the player when they place the node in world. They interact with water only on the opposite face - the one facing toward the player when they place the node in world. Terminals require at least one pipe segment to connect to, they don't interact directly with each other.
A screwdriver can be used to reorient terminals if you want one facing upward or downward.
The types of terminals in this mod are:
* Inlets let water enter the pipe but not leave
* Outlets let water out but not in
* Grates let water flow either way depending on pressure
* Pumps are inlets that force water into the network at a higher pressure than their elevation would normally give it.
## Valves
A valve can be used to connect or disconnect sections of a pipe network with a simple right-click. When a valve is "open" it acts like a pipe section, and when it's "closed" it does not act like a pipe.
## Elevation and pressure
The flow of water through the network is determined by two main factors; the directionality of each type of terminal, and the pressure of the water at that terminal.
Water flows from high pressure terminals to low pressure terminals. The rise and fall of the pipe in between those two terminals doesn't really matter, just the pressure at the terminals themselves.
The following figure illustrates the basics of how this works with a very simple three-terminal pipe network:
![Figure 1](/screenshots/waterworks_figure_1.png)
The two terminals on the left side are "inlets", only permitting water to enter the network, and the terminal on the right is a grate that allows water in or out.
If terminal 1 were to be immersed in water, water nodes would be transferred from terminal 1 to terminal 2 because terminal 1's higher elevation gives it higher pressure than terminal 2. Water would *not* be transferred to terminal 3 as terminal 3 is an inlet only.
If terminal 2 were to be immersed, likewise no water would be transferred because although terminal 2 can allow water to enter the pipe (it's a grate) there are no valid outlet terminals it could go to.
If terminal 3 were immersed in water, no water would be transferred because terminal 2 is higher elevation and therefore there isn't enough pressure at terminal 3 to reach it.
![Figure 2](/screenshots/waterworks_figure_2.png)
In this example terminal 3 is a pump, which acts as if it were an inlet located at an elevation 100 meters higher than it actually is. There are two potential outlets for water entering the system. Water is preferentially emitted from the *lowest* pressure outlet, so if terminal 3 was immersed in water it would be sent to terminal 2. However, if terminal 2 was contained in an enclosed space that had run out of room for additional water, the water would then be sent to the next-lowest outlet and come out of terminal 1.
If terminal 1 was immersed, then water would transfer from it to terminal 2. Terminal 3 is an inlet, so water wouldn't come out of it.

View File

@ -0,0 +1,92 @@
--"waterworks:pipe"
if minetest.get_modpath("pipeworks") then
minetest.register_craft( {
output = "waterworks:pipe 12",
recipe = {
{ "default:steel_ingot", "", "default:steel_ingot" },
{ "default:steel_ingot", "", "default:steel_ingot" },
{ "default:steel_ingot", "", "default:steel_ingot" }
},
})
else
minetest.register_craft( {
output = "waterworks:pipe 12",
recipe = {
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" },
{ "", "", "" },
{ "default:steel_ingot", "default:steel_ingot", "default:steel_ingot" }
},
})
end
--"waterworks:valve_on"
minetest.register_craft( {
output = "waterworks:valve_on",
recipe = {
{ "default:steel_ingot", "", "default:steel_ingot" },
{ "", "waterworks:pipe", "" },
{ "default:steel_ingot", "", "default:steel_ingot" }
},
})
--"waterworks:inlet"
minetest.register_craft( {
output = "waterworks:inlet",
recipe = {
{ "default:steel_ingot", "", "" },
{ "", "waterworks:pipe", "" },
{ "default:steel_ingot", "", "" }
},
})
--"waterworks:pumped_inlet"
minetest.register_craft( {
output = "waterworks:pumped_inlet",
recipe = {
{ "default:steel_ingot", "", "" },
{ "default:mese_crystal_fragment", "waterworks:pipe", "" },
{ "default:steel_ingot", "", "" }
},
})
minetest.register_craft( {
output = "waterworks:pumped_inlet",
recipe = {
{ "default:mese_crystal_fragment", "waterworks:inlet"},
},
})
--"waterworks:outlet"
minetest.register_craft( {
output = "waterworks:outlet",
recipe = {
{ "", "", "default:steel_ingot" },
{ "", "waterworks:pipe", "" },
{ "", "", "default:steel_ingot" }
},
})
--"waterworks:grate"
minetest.register_craft( {
output = "waterworks:grate",
recipe = {
{ "", "default:steel_ingot", "" },
{ "", "waterworks:pipe", "" },
{ "", "default:steel_ingot", "" }
},
})
-- Allow the basic connectors to be cycled through
minetest.register_craft( {
output = "waterworks:inlet",
type = "shapeless",
recipe = { "waterworks:outlet"},
})
minetest.register_craft( {
output = "waterworks:outlet",
type = "shapeless",
recipe = {"waterworks:grate"},
})
minetest.register_craft( {
output = "waterworks:grate",
type = "shapeless",
recipe = {"waterworks:inlet"},
})

View File

@ -0,0 +1 @@
default

209
mods/waterworks/execute.lua Normal file
View File

@ -0,0 +1,209 @@
local pressure_margin = 20
local pipe_cache = {}
local cardinal_dirs = {
{x= 0, y=0, z= 1},
{x= 1, y=0, z= 0},
{x= 0, y=0, z=-1},
{x=-1, y=0, z= 0},
{x= 0, y=-1, z= 0},
{x= 0, y=1, z= 0},
}
local sort_by_pressure = function(first, second)
local first_pressure = first.pressure
local second_pressure = second.pressure
if first_pressure == nil or second_pressure == nil then
minetest.log("error", "[waterworks] attempted to sort something by pressure that had no pressure value: " .. dump(first) .. "\n" .. dump(second))
return
end
return first_pressure > second_pressure
end
local valid_sink = function(node_name)
return node_name == "air" or node_name == "default:water_flowing" or node_name == "static_ocean:water_flowing"
end
local valid_source = function(node_name)
return waterworks.registered_liquids[node_name] ~= nil
end
-- breadth-first search passing through water searching for air or flowing water, limited to y <= pressure.
-- I could try to be fancy about water flowing downward preferentially, let's leave that as a TODO for now.
local flood_search_outlet = function(start_pos, pressure)
local start_node = minetest.get_node(start_pos)
local start_node_name = start_node.name
if valid_sink(start_node_name) then
return start_pos
end
local visited = {}
visited[minetest.hash_node_position(start_pos)] = true
local queue = {start_pos}
local queue_pointer = 1
while #queue >= queue_pointer do
local current_pos = queue[queue_pointer]
queue_pointer = queue_pointer + 1
for _, cardinal_dir in ipairs(cardinal_dirs) do
local new_pos = vector.add(current_pos, cardinal_dir)
local new_hash = minetest.hash_node_position(new_pos)
if visited[new_hash] == nil and new_pos.y <= pressure then
local new_node = minetest.get_node(new_pos)
local new_node_name = new_node.name
if valid_sink(new_node_name) then
return new_pos
end
visited[new_hash] = true
if valid_source(new_node_name) then
table.insert(queue, new_pos)
end
end
end
end
return nil
end
local upward_dirs = {
{x= 0, y=0, z= 1},
{x= 1, y=0, z= 0},
{x= 0, y=0, z=-1},
{x=-1, y=0, z= 0},
{x= 0, y=1, z= 0},
}
local shuffle = function(tbl)
for i = #tbl, 2, -1 do
local rand = math.random(i)
tbl[i], tbl[rand] = tbl[rand], tbl[i]
end
return tbl
end
-- depth-first random-walk search trending in an upward direction, returns when it gets cornered
local find_source = function(start_pos)
local current_node = minetest.get_node(start_pos)
local current_node_name = current_node.name
if not valid_source(current_node_name) then
return nil
end
local visited = {[minetest.hash_node_position(start_pos)] = true}
local current_pos = start_pos
local continue = true
while continue do
continue = false
shuffle(upward_dirs)
for _, dir in ipairs(upward_dirs) do
local next_pos = vector.add(current_pos, dir)
local next_hash = minetest.hash_node_position(next_pos)
if visited[next_hash] == nil then
visited[next_hash] = true
local next_node = minetest.get_node(next_pos)
local next_node_name = next_node.name
if valid_source(next_node_name) then
current_pos = next_pos
continue = true
break
end
end
end
end
return current_pos
end
waterworks.execute_pipes = function(net_index, net_capacity)
local net = waterworks.pipe_networks[net_index]
if net == nil then
minetest.log("error", "[waterworks] Invalid net index given to execute: " .. tostring(net_index))
return
end
local inlets
local outlets
if net.cache_valid then
-- We don't need to recalculate, nothing about the pipe network has changed since last time
inlets = pipe_cache[net_index].inlets
outlets = pipe_cache[net_index].outlets
else
-- Find all the inlets and outlets and sort them by pressure
inlets = {}
if net.connected.inlet ~= nil then
for _, inlet_set in pairs(net.connected.inlet) do
for _, inlet in pairs(inlet_set) do
table.insert(inlets, inlet)
end
end
end
table.sort(inlets, sort_by_pressure)
outlets = {}
if net.connected.outlet ~= nil then
for _, outlet_set in pairs(net.connected.outlet) do
for _, outlet in pairs(outlet_set) do
table.insert(outlets, outlet)
end
end
end
table.sort(outlets, sort_by_pressure)
-- Cache the results
pipe_cache[net_index] = {}
pipe_cache[net_index].inlets = inlets
pipe_cache[net_index].outlets = outlets
net.cache_valid = true
end
local inlet_index = 1
local outlet_index = #outlets
local inlet_count = #inlets
local count = 0
-- Starting with the highest-pressure inlet and the lowest-pressure outlet, attempt to move water.
-- We then proceed to steadily lower-pressure inlets and higher-pressure outlets until we meet in the middle, at which point
-- the system is in equilibrium.
while inlet_index <= inlet_count and outlet_index > 0 and count < net_capacity do
local source = inlets[inlet_index]
local sink = outlets[outlet_index]
--minetest.debug("source: " .. dump(source))
--minetest.debug("sink: " .. dump(sink))
-- pressure_margin allows us to check sources that are a little bit below sinks,
-- in case the extra pressure from their water depth is sufficient to force water through
if source.pressure + pressure_margin >= sink.pressure then
local source_pos = find_source(source.target)
local sink_pos
if source_pos ~= nil then
sink_pos = flood_search_outlet(sink.target, math.max(source.pressure, source_pos.y))
if sink_pos ~= nil then
local source_node = minetest.get_node(source_pos).name
local source_def = waterworks.registered_liquids[source_node]
if source_def and source_def.replace then
source_node = source_def.replace
end
minetest.swap_node(sink_pos, {name=source_node})
minetest.swap_node(source_pos, {name="air"})
count = count + 1
end
end
if source_pos == nil then
-- the outlet had available space but the inlet didn't provide
inlet_index = inlet_index + 1
elseif sink_pos == nil then
-- the inlet provided but the outlet didn't have space
outlet_index = outlet_index - 1
end
else
break
end
end
end

View File

@ -0,0 +1,112 @@
local worldpath = minetest.get_worldpath()
local network_filename = worldpath.."/waterworks_network.json"
-- Json storage
local save_data = function()
if waterworks.dirty_data ~= true then
return
end
local file = io.open(network_filename, "w")
if file then
file:write(minetest.serialize(waterworks.pipe_networks))
file:close()
waterworks.dirty_data = false
end
end
local read_data = function()
local file = io.open(network_filename, "r")
if file then
waterworks.pipe_networks = minetest.deserialize(file:read("*all")) -- note: any cached references to pipe_networks is invalidated here, so do this once at the beginning of the run and never again thereafter.
file:close()
else
waterworks.pipe_networks = {}
end
waterworks.dirty_data = false
for _, net in ipairs(waterworks.pipe_networks) do
net.cache_valid = false
end
end
read_data()
----------------------------------------------
local nets_near_players = {}
minetest.register_abm ({
label = "Active connected node tracking",
nodenames = {"group:waterworks_connected"},
interval = 1.0,
chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider)
local player_close = false
for _, player in ipairs(minetest.get_connected_players()) do
local player_pos = player:get_pos()
if math.abs(player_pos.x - pos.x) < 161 and math.abs(player_pos.z - pos.z) < 161 and math.abs(player_pos.y - pos.y) < 161 then
player_close = true
break
end
end
if not player_close then return end
local hash = minetest.hash_node_position(pos) + waterworks.facedir_to_hash(node.param2)
local net_index = waterworks.find_network_for_pipe_hash(hash)
if net_index < 0 then return end
--minetest.chat_send_all("net near player " .. tostring(net_index))
nets_near_players[net_index] = 5.0
end,
})
local forceloads = {}
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer > 1.0 then
if waterworks.dirty_data then
-- it's possible that a pipe network was split or merged, invalidating the nets_near_players values here.
-- Best to clear them and do nothing for one globalstep, they'll be repopulated shortly.
nets_near_players = {}
end
-- find connected node positions for all networks with connected nodes near players
local ensure_forceload = {}
for index, live_time in pairs(nets_near_players) do
local new_time = live_time - timer
--minetest.chat_send_all("new time " .. tostring(new_time))
if new_time < 0 then
nets_near_players[index] = nil
else
nets_near_players[index] = new_time
for connection_type, connections in pairs(waterworks.pipe_networks[index].connected) do
for hash, _ in pairs(connections) do
ensure_forceload[hash] = true
end
end
end
end
-- clear forceloads that are no longer needed
for hash, _ in pairs(forceloads) do
if not ensure_forceload[hash] then
minetest.forceload_free_block(minetest.get_position_from_hash(hash), true)
end
end
forceloads = ensure_forceload
-- enable forceloads that are needed
for hash, _ in pairs(forceloads) do
minetest.forceload_block(minetest.get_position_from_hash(hash), true)
end
timer = timer - 1.0
save_data()
for index, _ in pairs(nets_near_players) do
--minetest.chat_send_all("executing index " .. tostring(index))
waterworks.execute_pipes(index, 8)
end
end
end)

78
mods/waterworks/init.lua Normal file
View File

@ -0,0 +1,78 @@
waterworks = {}
local modpath = minetest.get_modpath(minetest.get_current_modname())
waterworks.registered_liquids = {}
waterworks.register_liquid = function(name, def)
if not def then def = {} else
if not def.replace then
minetest.override_item(name, {liquid_renewable = false})
if def.flowing then
minetest.override_item(def.flowing, {liquid_renewable = false})
end
end
end
waterworks.registered_liquids[name] = def
end
waterworks.register_liquid("default:water_source", {flowing = "default:water_flowing"})
dofile(modpath .. "/globalstep.lua")
dofile(modpath .. "/network.lua")
dofile(modpath .. "/execute.lua")
dofile(modpath .. "/nodes.lua")
dofile(modpath .. "/crafting.lua")
-- This rebuilds pipe networks whenever the map block is loaded.
-- May be useful for fixing broken stuff.
-- Note that this doesn't *remove* pipes that are inappropriately listed in the network,
-- that may be tricky.
--minetest.register_lbm({
-- label = "Validate waterworks pipe networks",
-- name = "waterworks:validate_pipe_networks",
-- nodenames = {"group:waterworks_pipe"},
-- run_at_every_load = true,
-- action = function(pos, node)
-- local hash_pos = minetest.hash_node_position(pos)
-- local found = false
-- for i, net in ipairs(waterworks.pipe_networks) do
-- if net.pipes[hash_pos] then
-- found = true
-- break
-- end
-- end
-- if not found then
-- local new_net_index = waterworks.place_pipe(pos)
-- minetest.log("warning", "[waterworks] detected an unregistered pipe at " ..
-- minetest.pos_to_string(pos) .. ", added it to pipe network " ..
-- tostring(new_net_index))
-- end
-- end,
--})
--minetest.register_chatcommand("dump_pipes", {
---- params = "pos", -- Short parameter description
-- description = "dump pipe network to debug log",
-- func = function(name, param)
-- --minetest.debug(dump(pipe_networks))
--
-- for i, net in ipairs(waterworks.pipe_networks) do
-- minetest.debug("net #" .. tostring(i) .. " cache valid: " .. dump(net.cache_valid))
-- minetest.debug("\tpipes:")
-- local count = 0
-- for hash_pos, _ in pairs(net.pipes) do
-- minetest.debug("\t\t"..minetest.pos_to_string(minetest.get_position_from_hash(hash_pos)))
-- count = count + 1
-- end
-- minetest.debug("\tpipe count: " .. tostring(count))
-- minetest.debug("\tconnections:")
-- for connection_type, connection_list in pairs(net.connected) do
-- minetest.debug("\t\t"..connection_type..":")
-- for connection_hash, data in pairs(connection_list) do
-- minetest.debug("\t\t\t"..minetest.pos_to_string(minetest.get_position_from_hash(connection_hash))..": "..string.gsub(string.gsub(dump(data), "\t", ""), "\n", " "))
-- end
-- end
-- end
--
-- end,
--})

3
mods/waterworks/mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = waterworks
description = Provides pipes, pumps, and valves for moving water over long distances
depends = default

386
mods/waterworks/network.lua Normal file
View File

@ -0,0 +1,386 @@
local pipe_networks = waterworks.pipe_networks
local invalidate_cache = function(pipe_network)
pipe_network.cache_valid = false
waterworks.dirty_data = true
end
local cardinal_dirs = {
{x= 0, y=0, z= 1},
{x= 1, y=0, z= 0},
{x= 0, y=0, z=-1},
{x=-1, y=0, z= 0},
{x= 0, y=-1, z= 0},
{x= 0, y=1, z= 0},
}
-- Mapping from facedir value to index in cardinal_dirs.
local facedir_to_dir_map = {
[0]=1, 2, 3, 4,
5, 2, 6, 4,
6, 2, 5, 4,
1, 5, 3, 6,
1, 6, 3, 5,
1, 4, 3, 2,
}
-- Turn the cardinal directions into a set of integers you can add to a hash to step in that direction.
local cardinal_dirs_hash = {}
for i, dir in ipairs(cardinal_dirs) do
cardinal_dirs_hash[i] = minetest.hash_node_position(dir) - minetest.hash_node_position({x=0, y=0, z=0})
end
local facedir_to_dir_index = function(param2)
return facedir_to_dir_map[param2 % 32]
end
local facedir_to_cardinal_hash = function(dir_index)
return cardinal_dirs_hash[dir_index]
end
waterworks.facedir_to_hash = function(param2)
return facedir_to_cardinal_hash(facedir_to_dir_index(param2))
end
local init_new_network = function(hash_pos)
waterworks.dirty_data = true
return {pipes = {[hash_pos] = true}, connected = {}, cache_valid = false}
end
local get_neighbor_pipes = function(pos)
local neighbor_pipes = {}
local neighbor_connected = {}
for _, dir in ipairs(cardinal_dirs) do
local potential_pipe_pos = vector.add(pos, dir)
local neighbor = minetest.get_node(potential_pipe_pos)
if minetest.get_item_group(neighbor.name, "waterworks_pipe") > 0 then
table.insert(neighbor_pipes, potential_pipe_pos)
elseif minetest.get_item_group(neighbor.name, "waterworks_connected") > 0 then
table.insert(neighbor_connected, potential_pipe_pos)
end
end
return neighbor_pipes, neighbor_connected
end
local merge_networks = function(index_list)
table.sort(index_list)
local first_index = table.remove(index_list, 1)
local merged_network = pipe_networks[first_index]
-- remove in reverse order so that indices of earlier tables to remove don't get disrupted
for i = #index_list, 1, -1 do
local index = index_list[i]
local net_to_merge = pipe_networks[index]
for pipe_hash, _ in pairs(net_to_merge.pipes) do
merged_network.pipes[pipe_hash] = true
end
for item_type, item_list in pairs(net_to_merge.connected) do
merged_network.connected[item_type] = merged_network.connected[item_type] or {}
for connection_hash, connection_data in pairs(item_list) do
merged_network.connected[item_type][connection_hash] = connection_data
end
end
table.remove(pipe_networks, index)
end
invalidate_cache(merged_network)
return first_index
end
local handle_connected = function(connected_positions)
for _, pos in ipairs(connected_positions) do
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if node_def._waterworks_update_connected then
node_def._waterworks_update_connected(pos)
else
minetest.log("error", "[waterworks] Node def for " .. node.name .. " had no _waterworks_update_connected defined")
end
end
end
-- When placing a pipe at pos, identifies what pipe network to add it to and updates the network map.
-- Note that this can result in fusing multiple networks together into one network.
waterworks.place_pipe = function(pos)
local hash_pos = minetest.hash_node_position(pos)
local neighbor_pipes, neighbor_connected = get_neighbor_pipes(pos)
local neighbor_count = #neighbor_pipes
if neighbor_count == 0 then
-- this newly-placed pipe has no other pipes next to it, so make a new network for it.
local new_net = init_new_network(hash_pos)
table.insert(pipe_networks, new_net)
handle_connected(neighbor_connected)
return #pipe_networks
elseif neighbor_count == 1 then
-- there's only one pipe neighbor. Look up what network it belongs to and add this pipe to it too.
local neighbor_pos_hash = minetest.hash_node_position(neighbor_pipes[1])
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[neighbor_pos_hash] then
pipes[hash_pos] = true
invalidate_cache(net)
handle_connected(neighbor_connected)
return i
end
end
else
local neighbor_index_set = {} -- set of indices for networks that neighbors belong to
local neighbor_index_list = {} -- list version of above
for _, neighbor_pos in ipairs(neighbor_pipes) do
local neighbor_hash = minetest.hash_node_position(neighbor_pos)
for i, net in ipairs(pipe_networks) do
if net.pipes[neighbor_hash] then
if not neighbor_index_set[i] then
table.insert(neighbor_index_list, i)
neighbor_index_set[i] = true
end
end
end
end
if #neighbor_index_list == 1 then -- all neighbors belong to one network. Add this node to that network.
local target_network_index = neighbor_index_list[1]
pipe_networks[target_network_index]["pipes"][hash_pos] = true
invalidate_cache(pipe_networks[target_network_index])
handle_connected(neighbor_connected)
return target_network_index
end
-- The most complicated case, this new pipe segment bridges multiple networks.
if #neighbor_index_list > 1 then
local new_index = merge_networks(neighbor_index_list)
pipe_networks[new_index]["pipes"][hash_pos] = true
handle_connected(neighbor_connected)
return new_index
end
end
-- if we get here we're in a strange state - there are neighbor pipe nodes but none are registered in a network.
-- We could be trying to recover from corruption, so pretend the neighbors don't exist and start a new network.
-- The unregistered neighbors may join it soon.
local new_net = init_new_network(hash_pos)
table.insert(pipe_networks, new_net)
handle_connected(neighbor_connected)
return #pipe_networks
end
waterworks.remove_pipe = function(pos)
local hash_pos = minetest.hash_node_position(pos)
local neighbor_pipes = get_neighbor_pipes(pos)
local neighbor_count = #neighbor_pipes
if neighbor_count == 0 then
-- no neighbors, so this is the last of its network.
for i, net in ipairs(pipe_networks) do
if net.pipes[hash_pos] then
table.remove(pipe_networks, i)
waterworks.dirty_data = true
return i
end
end
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks. Something went wrong to get to this state.")
return -1
elseif neighbor_count == 1 then
-- there's only one pipe neighbor. This pipe is at the end of a line, so just remove it.
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[hash_pos] then
pipes[hash_pos] = nil
invalidate_cache(net)
-- If there's anything connected to the pipe here, remove it from the network too
for _, connected_items in pairs(net.connected) do
connected_items[hash_pos] = nil
end
return i
end
end
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks, despite being neighbor to one at " ..
minetest.pos_to_string(neighbor_pipes[1]) ..
". Something went wrong to get to this state.")
return -1
else
-- we may be splitting networks. This is complicated.
-- find the network we currently belong to. Remove ourselves from it.
local old_net
local old_pipes
local old_connected
local old_index
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[hash_pos] then
old_connected = net.connected
old_net = net
old_pipes = pipes
old_index = i
old_pipes[hash_pos] = nil
-- if there's anything connected to the pipe here, remove it
for _, connected_items in pairs(old_connected) do
connected_items[hash_pos] = nil
end
end
end
if old_index == nil then
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks, despite being neighbor to several. Something went wrong to get to this state.")
return -1
end
-- get the hashes of the neighbor positions.
-- We're maintaining a set as well as a list because they're
-- efficient for different purposes. The list is easy to count,
-- the set is easy to test membership of.
local neighbor_hashes_list = {}
local neighbor_hashes_set = {}
for i, neighbor_pos in ipairs(neighbor_pipes) do
local neighbor_hash = minetest.hash_node_position(neighbor_pos)
neighbor_hashes_list[i] = neighbor_hash
neighbor_hashes_set[neighbor_hash] = true
end
-- We're going to need to traverse through the old network, starting from each of our neighbors,
-- to establish what's still connected.
local to_visit = {}
local visited = {[hash_pos] = true} -- set of hashes we've visited already. We know the starting point is not valid.
local new_nets = {} -- this will be where we put new sets of connected nodes.
while #neighbor_hashes_list > 0 do
local current_neighbor = table.remove(neighbor_hashes_list) -- pop neighbor hash and push it into the to_visit list.
neighbor_hashes_set[current_neighbor] = nil
table.insert(to_visit, current_neighbor) -- file that neighbor hash as our starting point.
local new_net = init_new_network(current_neighbor) -- we know that hash is in old_net, so initialize the new_net with it.
local new_pipes = new_net.pipes
while #to_visit > 0 do
local current_hash = table.remove(to_visit)
for _, cardinal_hash in ipairs(cardinal_dirs_hash) do
local test_hash = cardinal_hash + current_hash
if not visited[test_hash] then
if old_pipes[test_hash] then
-- we've traversed to a node that was in the old network
old_pipes[test_hash] = nil -- remove from old network
new_pipes[test_hash] = true -- add to one we're building
table.insert(to_visit, test_hash) -- flag it as next one to traverse from
if neighbor_hashes_set[test_hash] then
--we've encountered another neighbor while traversing
--eliminate it from future consideration as a starting point.
neighbor_hashes_set[test_hash] = nil
for i, neighbor_hash_in_list in ipairs(neighbor_hashes_list) do
if neighbor_hash_in_list == test_hash then
table.remove(neighbor_hashes_list, i)
break
end
end
if #neighbor_hashes_list == 0 then
--Huzzah! We encountered all neighbors. The rest of the nodes in old_net should belong to new_net.
--We can skip all remaining pathfinding flood-fill and connected testing
for remaining_hash, _ in pairs(old_pipes) do
new_pipes[remaining_hash] = true
to_visit = {}
end
break
end
end
end
end
end
visited[current_hash] = true
end
table.insert(new_nets, new_net)
end
-- distribute connected items to the new nets
if #new_nets == 1 then
-- net didn't split, just keep the old stuff
new_nets[1].connected = old_connected
else
for _, new_net in ipairs(new_nets) do
local new_pipes = new_net.pipes
for item_type, item_list in pairs(old_connected) do
new_net.connected[item_type] = new_net.connected[item_type] or {}
for connection_hash, connection_data in pairs(item_list) do
if new_pipes[connection_hash] then
new_net.connected[item_type][connection_hash] = connection_data
end
end
end
end
end
-- replace the old net with one of the new nets
pipe_networks[old_index] = table.remove(new_nets)
-- if there are any additional nets left, add those as brand new ones.
for _, new_net in ipairs(new_nets) do
table.insert(pipe_networks, new_net)
end
return old_index
end
end
waterworks.place_connected = function(pos, item_type, data)
local node = minetest.get_node(pos)
local dir_index = facedir_to_dir_index(node.param2)
local dir_hash = facedir_to_cardinal_hash(dir_index)
local pos_hash = minetest.hash_node_position(pos)
local connection_hash = pos_hash + dir_hash
for i, net in ipairs(pipe_networks) do
if net.pipes[connection_hash] then
net.connected[item_type] = net.connected[item_type] or {}
net.connected[item_type][connection_hash] = net.connected[item_type][connection_hash] or {}
net.connected[item_type][connection_hash][dir_index] = data
invalidate_cache(net)
return i
end
end
return -1
end
waterworks.remove_connected = function(pos, item_type)
local node = minetest.get_node(pos)
local dir_index = facedir_to_dir_index(node.param2)
local dir_hash = facedir_to_cardinal_hash(dir_index)
local pos_hash = minetest.hash_node_position(pos)
local connection_hash = pos_hash + dir_hash
for i, net in ipairs(pipe_networks) do
if net.pipes[connection_hash] then
local item_list = net.connected[item_type]
if item_list then
if item_list[connection_hash] ~= nil then
local connected_items = item_list[connection_hash]
connected_items[dir_index] = nil
local count = 0
for _, data in pairs(connected_items) do
count = count + 1
end
if count == 0 then
item_list[connection_hash] = nil
end
count = 0
for _, item in pairs(item_list) do
count = count + 1
end
if count == 0 then
net.connected[item_type] = nil
end
invalidate_cache(net)
return i
end
end
break -- If we get here, we didn't find the connected node even though we should have.
end
end
return -1
end
waterworks.find_network_for_pipe_hash = function(hash)
for i, net in ipairs(pipe_networks) do
if net.pipes[hash] then
return i
end
end
return -1
end

280
mods/waterworks/nodes.lua Normal file
View File

@ -0,0 +1,280 @@
minetest.register_node("waterworks:pipe", {
description = "Waterworks Pipe",
tiles = {
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.25,-0.25,-0.25,0.25,0.25,0.25},
connect_top = {-0.375, 0, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, 0, 0.375},
connect_back = {-0.375, -0.375, 0, 0.375, 0.375, 0.5},
connect_right = {0, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, 0},
connect_left = {-0.5, -0.375, -0.375, 0, 0.375, 0.375},
disconnected = {-0.375,-0.375,-0.375,0.375,0.375,0.375},
},
paramtype = "light",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_pipe = 1},
sounds = default.node_sound_metal_defaults(),
on_construct = function(pos)
waterworks.place_pipe(pos)
end,
on_destruct = function(pos)
waterworks.remove_pipe(pos)
end,
})
-----------------------------------------------------------------
minetest.register_node("waterworks:valve_on", {
description = "Waterworks Valve (open)",
tiles = {"waterworks_metal.png^waterworks_valve_seam.png^waterworks_valve_on.png",},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.4375,-0.4375,-0.4375,0.4375,0.4375,0.4375},
connect_top = {-0.375, 0.4375, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, -0.4375, 0.375},
connect_back = {-0.375, -0.375, 0.4375, 0.375, 0.375, 0.5},
connect_right = {0.4375, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, -0.4375},
connect_left = {-0.5, -0.375, -0.375, -0.4375, 0.375, 0.375},
},
paramtype = "light",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_pipe = 1},
sounds = default.node_sound_metal_defaults(),
on_construct = function(pos)
waterworks.place_pipe(pos)
end,
on_destruct = function(pos)
waterworks.remove_pipe(pos)
end,
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
node.name = "waterworks:valve_off"
minetest.set_node(pos, node)
end,
})
minetest.register_node("waterworks:valve_off", {
description = "Waterworks Valve (closed)",
tiles = {"waterworks_metal.png^waterworks_valve_seam.png^waterworks_valve_off.png",},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.4375,-0.4375,-0.4375,0.4375,0.4375,0.4375},
connect_top = {-0.375, 0.4375, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, -0.4375, 0.375},
connect_back = {-0.375, -0.375, 0.4375, 0.375, 0.375, 0.5},
connect_right = {0.4375, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, -0.4375},
connect_left = {-0.5, -0.375, -0.375, -0.4375, 0.375, 0.375},
},
paramtype = "light",
drops = "waterworks:valve_on",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_inert = 1, not_in_creative_inventory = 1},
sounds = default.node_sound_metal_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
node.name = "waterworks:valve_on"
minetest.set_node(pos, node)
end,
})
-----------------------------------------------------------------
local place_inlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "inlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Inlet elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:inlet", {
description = "Waterworks Inlet",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_inlet.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
sounds = default.node_sound_metal_defaults(),
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
_waterworks_update_connected = place_inlet,
on_construct = function(pos)
place_inlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_inlet(pos)
return true
end,
})
local place_pumped_inlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "inlet", {pos = pos, target = target, pressure = target.y + 100})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Pump effective elevation " .. tostring(target.y + 100))
end
minetest.register_node("waterworks:pumped_inlet", {
description = "Waterworks Pumped Inlet",
tiles = {
"waterworks_turbine_base.png",
"waterworks_turbine_base.png",
"waterworks_turbine_side.png^[transformFX",
"waterworks_turbine_side.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_turbine_base.png^waterworks_turbine.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
sounds = default.node_sound_metal_defaults(),
paramtype = "light",
drawtype = "normal",
_waterworks_update_connected = place_pumped_inlet,
on_construct = function(pos)
place_pumped_inlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_pumped_inlet(pos)
return true
end,
})
local place_outlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "outlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Outlet elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:outlet", {
description = "Waterworks Outlet",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_outlet.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
sounds = default.node_sound_metal_defaults(),
_waterworks_update_connected = place_outlet,
on_construct = function(pos)
place_outlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "outlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "outlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_outlet(pos)
return true
end,
})
local place_grate = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "outlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Grate elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:grate", {
description = "Waterworks Grate",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_grate.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
sounds = default.node_sound_metal_defaults(),
_waterworks_update_connected = place_outlet,
on_construct = function(pos)
place_outlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "outlet")
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "outlet")
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_outlet(pos)
return true
end,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1 @@
waterworks_make_default_water_non_renewable (Disable self-replication of default water) bool false

View File

@ -0,0 +1,17 @@
The following textures are licensed under the MIT and CC0 licenses by FaceDeer:
waterworks_connected_back
waterworks_grate
waterworks_inlet
waterworks_metal
waterworks_outlet
waterworks_pipe
waterworks_pipe_rivets
waterworks_pipe_rivets_offset
waterworks_pipe_rivets_offset_2
waterworks_turbine
waterworks_turbine_base
waterworks_turbine_side
waterworks_valve_off
waterworks_valve_on
waterworks_valve_seam

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B