Revive szutilpack from updates from sztest and nodecore.

- Rename namespace to "top-level" szutil_*.
- Merge improvements from sztest.
- New utils: chatsock, consock, logtrace, usagesurvey.
- Lagometer/logtrace are chat-command-togglable.
This commit is contained in:
Aaron Suen 2019-03-27 06:23:19 -04:00
parent 4eb745efa7
commit 6c238b26a1
37 changed files with 1232 additions and 1764 deletions

4
.lualocals Normal file
View File

@ -0,0 +1,4 @@
~print
minetest
ItemStack
VoxelArea

21
TODO
View File

@ -1,21 +0,0 @@
------------------------------------------------------------------------
- Split sz_pos into more partials
- Global interval timer
- Node pos from hash constructor
- MapBlock position helpers
- To/from node pos (min/max)
- To/from db block id
- Get voxel manip, and recalc helpers
- MapBlock metadata?
- NodeMeta helper cleanup
- split into multiple return values
- merge with node_get, param to req metadata
- test with items with item metadata
------------------------------------------------------------------------

177
lualocals.pl Executable file
View File

@ -0,0 +1,177 @@
#!/usr/bin/perl -w
use strict;
use warnings;
use File::Find qw(find);
use Text::ParseWords qw(shellwords);
use Text::Wrap qw(wrap);
$Text::Wrap::columns = 72;
my $checkonly = grep { $_ eq '-c' } @ARGV;
# Find any ".lualocals" files that define the custom keywords
# localized in any lua files beneath that subdir.
my %custom;
find(
{ wanted => sub {
m#(|.*/)\.lualocals# or return;
open(my $fh, "<", $_) or die("$_: $!");
$custom{$1} = {
map { $_ => 1 } shellwords(
do { local $/; <$fh> }
)};
},
no_chdir => 1
},
".");
keys %custom or die("no .lualocals found");
# Support locals defined by the engine.
my $lua = `which lua51 lua5.1 2>/dev/null | head -n 1`;
$lua =~ m#\S# or die("failed to find lua");
chomp($lua);
open( my $fh, "-|", $lua, "-e", <<'EOD'
for k, v in pairs(_G) do
if k ~= "_G" then
print(k)
end
end
for pk, pv in pairs(package.loaded) do
if pk ~= "_G" and _G[pk] then
for fk, fv in pairs(pv) do
print(pk .. "." .. fk)
end
end
end
EOD
) or die($!);
my %support = map { chomp; $_ => 1 } <$fh>;
close($fh);
sub parsekeys {
m#^\s*--\s*\Q$_[0]\E:\s+(.*)#
and map { $_[1]->{$_} = 1 } shellwords($1);
1;
}
sub drawline {
my($w, $p, $n) = ($Text::Wrap::columns, @_);
$p ||= "";
my $l = "-" x ($w - length($p));
"$p$l" . ("\n" x ($n || 1));
}
sub mklocals {
@_ or return "";
my @x = map { my $y = $_; tr#.#_#; $y } @_;
local $" = ", ";
wrap("local ", " ", "@_\n") .
wrap(" = ", " ", "@x\n");
}
sub process {
my($path, $cust) = @_;
my %locals = (%support, %$cust);
# Read in code, parsing out SKIP and ADD values and stripping
# off the LUALOCALS block.
my $orig = "";
my $code = "";
my %skip;
my $inblock;
open(my $fh, "<", $path) or die($!);
while(<$fh>) {
$orig .= $_;
m#^\s*--\s*LUALOCALS\s*<# and $inblock = 1;
$inblock
and parsekeys("SKIP", \%skip)
and parsekeys("ADD", \%locals);
$inblock or $code .= $_;
m#\s*--\s*LUALOCALS\s*># and undef($inblock);
}
while($code =~ s#^\s*\n##) { }
# Substitution names for 2nd-tier locals.
my %subs = map { my $x = $_; $x =~ tr#.#_#; $_ => $x } keys %locals;
# Strip strings and comments out from code, so we don't
# accidentally match something inside a string literal.
my $mcode = "";
my($q, $b);
for my $c (split(m##, $code)) {
$b and(undef($b), next);
$c eq "\\" and(($b = 1), next);
$q ? ($c eq '"' and undef($q))
: ($c eq '"') ? ($q = 1)
: ($mcode .= $c);
}
$mcode =~ s#--\[\[.*?--\]\]##g;
$mcode =~ s#--.*$##gm;
# Process matched from code, and include dependencies, e.g. if
# math.floor is found, include math.
my %matched = map { $_ => 1 }
grep { $mcode =~ m#\b(\Q$_\E|\Q$subs{$_}\E)\b# }
grep { !m#^\~# } keys %locals;
for my $m (keys %matched) {
my $n = $m;
$n =~ s#\..*##;
$matched{$n} = 1;
}
# Remove skip entries.
for my $s ( (keys %skip, map { substr($_, 1) } grep { m#^\~# } keys %locals) ) {
delete($matched{$s});
$s =~ tr#.#_#;
delete($matched{$s});
}
# Flatten results.
my @found = sort keys %matched;
my @allskip = sort keys %skip;
1 while chomp($code);
$code .= "\n";
if(@found or @allskip) {
my $block = "";
$block .= drawline("-- LUALOCALS < ");
@allskip and $block .= wrap("-- SKIP: ", "-- SKIP: ", "@allskip\n");
local $" = ", ";
$block .= mklocals(grep { !m#\.# } @found);
$block .= mklocals(grep { m#\.# } @found);
my @unopt = grep { m#\.# and $code =~ m#\b\Q$_\E\b# } %locals;
@unopt and warn("UNOPTIMIZED($path) = @unopt\n");
$block .= drawline("-- LUALOCALS > ", 2);
$code = $block . $code;
}
$code eq $orig and return;
$checkonly and die("dirty: $path");
eval {
open(my $fh, ">", "$path.new") or die($!);
print $fh $code;
close($fh);
rename("$path.new", $path);
warn("-> $path\n");
};
unlink("$path.new");
$@ and die($@);
}
my %plan;
for my $root (keys %custom) {
find(
{ wanted => sub {
m#\.lua$# or return;
my $f = $_;
$plan{$f} = sub { process($f, $custom{$root}) }
},
no_chdir => 1
},
$root);
}
for my $k (sort keys %plan) {
$plan{$k}->();
}

2
modpack.conf Normal file
View File

@ -0,0 +1,2 @@
name = szutilpack
description = Sz Utility Pack

View File

@ -1,165 +0,0 @@
local modname = minetest.get_current_modname()
-- Proportion of time to spend each cycle on recalculations. For instance,
-- a value of 0.05 will mean that we attempt to use about 5% of each step
-- cycle trying to do recalculates.
local cycletime = tonumber(minetest.setting_get(modname .. "_cycletime")) or 0.02
-- How often statistics are written to the log, to track server CPU use.
local stattime = tonumber(minetest.setting_get(modname .. "_stattime")) or 3600
-- How often a mapblock can be recalculated, at the earliest.
local calctime = tonumber(minetest.setting_get(modname .. "_calctime")) or 60
-- Simple positional helper functions.
local function posadd(a, b) return {x = a.x + b.x, y = a.y + b.y, z = a.z + b.z} end
local function blockmin(v) return {x = v.x * 16, y = v.y * 16, z = v.z * 16} end
local function blockmax(v) return posadd(blockmin(v), {x = 15, y = 15, z = 15}) end
-- Generate exponentially-distributed random values, so low values (nearby
-- positions) are more likely to get prompt attention.
local function exporand()
local r = math.random()
if r == 0 then return exporand() end
r = math.log(r)
if math.random() < 0.5 then r = -r end
return r
end
local mapgenqueue = {}
-- Run voxelmanip lighting calc on chunks post-mapgen. It seems as though
-- the default mapgen lighting calc disagrees with this one (water does not
-- absorb light; bug?)
minetest.register_on_generated(function(minp, maxp)
for x = math.floor(minp.x / 16), math.floor(maxp.x / 16) do
for y = math.floor(minp.y / 16), math.floor(maxp.y / 16) do
for z = math.floor(minp.z / 16), math.floor(maxp.z / 16) do
local pos = {x = x, y = y, z = z}
mapgenqueue[minetest.hash_node_position(pos)] = pos
end
end
end
end)
-- Keep track of each block processed, and when its check expires
-- and reprocessing is possible.
local processed = {}
-- Statistics for reporting display.
local proctime = 0
local totaltime = 0
local totalqty = 0
-- Amount of time available for processing.
local availtime = 0
-- Helper method to automatically process blocks
-- (shared by mapgen and random queue).
local function procblock(pos, nextcalc)
-- Don't reprocess already-processed blocks too soon.
local h = minetest.hash_node_position(pos)
if processed[h] then return end
processed[h] = nextcalc
-- Don't process a block if it's not loaded, or if any of its
-- neighbors is not loaded, as that can cause lighting bugs (at least).
if not minetest.get_node_or_nil(blockmin(pos))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 1, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 1, y = 0, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = -1, y = 0, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 0, z = 1})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 0, z = -1})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = -1, z = 0})))
then return end
-- Recalc all fluids and lighting in that block.
local vm = minetest.get_voxel_manip(blockmin(pos), blockmax(pos))
vm:update_liquids()
vm:calc_lighting()
vm:write_to_map()
vm:update_map()
-- Keep track for periodic statistic summary.
totalqty = totalqty + 1
end
-- Run recalculates during each cycle.
minetest.register_globalstep(function(dtime)
-- Don't attempt to do anything if nobody is connected. There seems
-- to be some issue that may be crashing servers that run for a long
-- time with no players connected, which this may help avert.
local players = minetest.get_connected_players()
if #players < 1 then return end
-- Add our allotment to the amount of time available.
availtime = availtime + dtime * cycletime
-- Attenuate stored surplus/deficit time, so that we don't accumulate
-- a massive deficit (suspending recalcs for a long time) or a massive
-- surplus (effectively freezing the game for a ton of redundant
-- recalcs).
availtime = availtime * 0.95
-- Calculate when the recalculation is supposed to stop, based on
-- real-time clock.
local starttime = os.clock()
local endtime = starttime + availtime
-- Get the current timestamp, to be used in expiration timestamps.
local now = os.time()
-- Prune already-expired blocks from the processed list.
local del = {}
for k, v in pairs(processed) do
if v < now then del[k] = true end
end
for k, v in pairs(del) do
processed[k] = nil
end
local nextcalc = now + calctime
-- Process generated chunks first.
for k, v in pairs(mapgenqueue) do
procblock(v, nextcalc)
mapgenqueue = {}
end
-- Skip random recalcs if we don't actually have any time to do them.
if endtime > starttime then
-- Keep searching for blocks to recalc until we run out of allotted time.
while os.clock() < endtime do
-- Pick a random player, and then pick a random exponentially-
-- distributed random block around that player.
local pos = players[math.random(1, #players)]:getpos()
pos.x = math.floor(pos.x / 16 + exporand() + 0.5)
pos.y = math.floor(pos.y / 16 + exporand() + 0.5)
pos.z = math.floor(pos.z / 16 + exporand() + 0.5)
procblock(pos, nextcalc)
end
end
-- Update our actual end time (in case we ran long with voxel operations),
-- and keep track for periodic statistic summary.
endtime = os.clock()
totaltime = totaltime + dtime
proctime = proctime + endtime - starttime
-- Update available time allotment.
availtime = availtime + starttime - endtime
end)
-- Periodically display statistics, so we can track actual performance.
local function reportstats()
if totaltime == 0 then return end
local function ms(i) return math.floor(i * 1000000) / 1000 end
print(modname .. ": processed " .. totalqty .. " mapblocks using "
.. ms(proctime) .. "ms out of " .. ms(totaltime) .. "ms ("
.. (math.floor(proctime / totaltime * 10000) / 100)
.. "%), " .. ms(availtime) .. "ms saved")
totalqty = 0
totaltime = 0
proctime = 0
minetest.after(stattime, reportstats)
end
minetest.after(stattime, reportstats)

View File

@ -1 +0,0 @@
default

View File

@ -1,16 +0,0 @@
This mod provides:
- A framework for dealing with some of the most frequently-used
types and operations (e.g. 3d position vectors) in an object-
oriented manner (e.g. a:add(b) instead of helper.add(a, b))
- Convenience wrappers for more complex operations, many with
optional parameters with sane defaults (e.g. pos:smoke())
- A few language extensions (e.g. string:startswith(prefix))
- A couple of new chat commands for debugging by mod authors
(server privileges required).
This mod is meant to be used as a dependency by other mods, and should
have no direct impact on gameplay by itself.

View File

@ -1,49 +0,0 @@
minetest.register_chatcommand("regstats", {
privs = {server = true},
description = "Statistics about total registered things.",
func = function(name)
local regpref = "registered_"
local regkeys = sz_table:new()
for k, v in pairs(core) do
if k:startswith(regpref) and type(v) == "table" then
regkeys:insert(k:sub(regpref:len() + 1))
end
end
regkeys:sort()
local regrpt = sz_table:new()
for i, k in ipairs(regkeys) do
local qty = 0
for ik, iv in pairs(core[regpref .. k]) do
qty = qty + 1
end
if qty > 0 then regrpt:insert(k .. " " .. qty) end
end
minetest.chat_send_player(name, "registration count: " .. regrpt:concat(", "))
end
})
minetest.register_chatcommand("regnodes", {
privs = {server = true},
description = "Statistics about total registered nodes, by mod.",
func = function(name)
local rn = sz_table:new({ TOTAL = 0 })
for k, v in pairs(minetest.registered_nodes) do
local mod = k
local idx = k:find(":", 1, true)
if idx then mod = mod:sub(1, idx) .. "*" end
rn[mod] = (rn[mod] or 0) + 1
rn.TOTAL = rn.TOTAL + 1
end
local keys = rn:keys()
keys:sort(function(a, b)
if rn[a] == rn[b] then return a < b end
return rn[a] > rn[b]
end)
local noderpt = sz_table:new()
for i, v in ipairs(keys) do
noderpt:insert(rn[v] .. " " .. v)
end
minetest.chat_send_player(name, "registered node count: " .. noderpt:concat(", "))
end
})

View File

View File

@ -1,22 +0,0 @@
-- Load the master utility base class.
dofile(minetest.get_modpath(minetest.get_current_modname())
.. "/sz_class.lua")
-- Load subclasses defined in this mod.
sz_class:loadsubclasses(
"sz_table"
)
sz_table:loadsubclasses(
"sz_util",
"sz_pos",
"sz_facedir",
"sz_nodetrans"
)
sz_class:loadlibs(
"string",
"sz_pos_environ",
"sz_pos_limitfx",
"sz_pos_nodeshatter",
"chatcommands"
)

View File

@ -1,20 +0,0 @@
-- Quick helper to tell if a string starts with a prefix
-- string, without all the sub/len mess-around-ery.
function string:startswith(pref)
return self:sub(1, pref:len()) == pref
end
-- Quick helper to tell if a string ends with a suffix
-- string, without all the sub/len mess-around-ery.
function string:endswith(suff)
return suff == "" or self:sub(-suff:len()) == suff
end
-- Quick helper to tell if a string contains another string
-- anywhere inside.
function string:contains(substr, ignorecase)
if ignorecase then
return self:lower():find(substr:lower(), 1, true)
end
return self:find(substr, 1, true)
end

View File

@ -1,29 +0,0 @@
sz_class = { }
sz_class.__index = sz_class
function sz_class:new(init)
init = init or { }
setmetatable(init, self)
return init
end
function sz_class:loadlibs(...)
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname) .. "/"
for i, class in ipairs({...}) do
dofile(modpath .. class .. ".lua")
end
end
function sz_class:loadsubclasses(...)
for i, class in ipairs({...}) do
local t = self:new(rawget(_G, class))
t.__index = t
if class:sub(1, 3) == "sz_" then
rawset(_G, class, t)
else
_G[class] = t
end
end
return self:loadlibs(...)
end

View File

@ -1,84 +0,0 @@
-- This is a library for working with facedirs, which are orthogonal
-- 3d rotation states with 24 possible values.
------------------------------------------------------------------------
-- CONSTRUCTORS AND STATIC PROPERTIES
-- Axis ("top" vector) information for each group of 4 facedir values.
local facedir_axis = { [0] = "u", "n", "s", "e", "w", "d" }
for k, v in pairs(facedir_axis) do
facedir_axis[k] = sz_pos.dirs[v]
end
-- Create a new sz_facedir from a param2 value.
function sz_facedir:from_param(param)
return sz_facedir:new({ param = param })
end
-- Create a new sz_facedir from the "back" (required) and "top"
-- (optional) direction vectors.
function sz_facedir:from_vectors(back, top)
local min = 0
local max = 23
if top then
for k, v in pairs(facedir_axis) do
if v:eq(top) then
min = k * 4
max = k * 4 + 3
end
end
end
back = sz_pos:new(back)
for i = min, max do
if back:eq(minetest.facedir_to_dir(i)) then
return sz_facedir:from_param(i)
end
end
end
------------------------------------------------------------------------
-- DIRECTIONS
function sz_facedir:back()
return sz_pos:new(minetest.facedir_to_dir(self.param))
end
function sz_facedir:front()
return self:back():neg()
end
function sz_facedir:top()
return facedir_axis[math.floor(self.param / 4)]
end
function sz_facedir:bottom()
return self:top():neg()
end
function sz_facedir:right()
return self:top():cross(self:back())
end
function sz_facedir:left()
return self:right():neg()
end
------------------------------------------------------------------------
-- ROTATION
-- Rotate 90 degrees around the given rotational axis (right-hand rule).
function sz_facedir:rotate(axis)
axis = sz_pos:new(axis)
local top = self:top()
if top:dot(axis) == 0 then
top = axis:cross(top)
end
local back = self:back()
if back:dot(axis) == 0 then
back = axis:cross(back)
end
return sz_facedir:from_vectors(back, top)
end
------------------------------------------------------------------------
return sz_facedir

View File

@ -1,142 +0,0 @@
-- This is a library for working with atomic transactions of multiple
-- nodes. Transactions can be rolled back until they are committed, and
-- problematic conditions such as "ignore" nodes can abort the
-- transaction.
------------------------------------------------------------------------
local ignore = "ignore"
local trans_abort = "abort sz_nodetrans: "
local tn_helpers = { }
function tn_helpers:pre()
local n = self:node_get()
if n and n.name and n.name == ignore and self.trans
and self.trans.abort_on_ignore then
sz_nodetrans.abort("unloaded mapblock encountered")
end
if not n then
sz_nodetrans.abort("sz_pos:node_get() returned nil")
end
n = sz_table:new(n)
rawset(self, "pre", n)
return n
end
function tn_helpers:now()
local n = self.pre
if not n then return end
n = sz_table.copy(n)
rawset(self, "now", n)
return n
end
function tn_helpers:def()
local n = self.now
if not n then return { } end
n = n.name
if not n then return { } end
return minetest.registered_items[n]
end
function tn_helpers:facedir()
if now and now.param2 then
return sz_facedir.from_param(now.param2)
end
end
function tn_helpers:metapre()
local m = self:meta()
if not m then return end
m = sz_table:new(m:to_table())
self.metapre = m
return m
end
function tn_helpers:metanow()
local t = self.metapre
if not t then return end
t = sz_table.copy(t)
self.metanow = t
return t
end
local transnode = { }
function transnode:__index(self, key)
local f = tn_helpers[key]
if f then return f(self) end
f = self.now[key]
if f then return f end
return sz_pos[key]
end
function transnode:__newindex(self, key, value)
if not tn_helpers[key] then
self.now[key] = value
end
rawset(self, key, value)
end
function sz_nodetrans:get(pos)
local idx = self.idx
if not idx then
idx = sz_table:new()
self.idx = idx
end
pos = sz_pos:new(pos)
local hash = pos:hash()
local state = idx[hash]
if state then return state end
state = pos:copy()
state.trans = self
setmetatable(state, transnode)
idx[hash] = state
return state
end
function sz_nodetrans:post(act)
local dopost = self.dopost
if not dopost then
dopost = sz_table:new()
self.dopost = dopost
end
dopost:insert(act)
end
function sz_nodetrans.abort(msg)
error(trans_abort .. msg)
end
local function commit(aoi, ok, msg, ...)
self.abort_on_ignore = aoi
if not ok then
if msg:sub(1, trans_abort:len()) == trans_abort then
print(msg)
return
end
error(msg, 0)
end
for k, v in pairs(self.idx or { }) do
if minetest.serialize(v.pre) ~= minetest.serialize(v.now) then
v:node_set(v.now)
end
if minetest.serialize(v.metapre) ~= minetest.serialize(v.metanow) then
v:meta():from_table(v.metanow)
end
if v.now.post then v.now.post() end
end
self.idx = nil
for k, v in pairs(self.dopost or { }) do
v()
end
self.dopost = nil
return msg, ...
end
function sz_nodetrans.begin(act)
local aoi = self.abort_on_ignore
self.abort_on_ignore = true
commit(aoi, pcall(act))
end
------------------------------------------------------------------------
return sz_nodetrans

View File

@ -1,426 +0,0 @@
-- This is a general-purpose 3d vector helper library, which represents
-- tuples of (x, y, z) coordinates, in both relative and absolute
-- contexts.
------------------------------------------------------------------------
-- CONSTRUCTORS AND STATIC PROPERTIES
-- Create a new sz_pos from loose coordinates declared in order.
function sz_pos:xyz(x, y, z)
return sz_pos:new({ x = x, y = y, z = z })
end
-- Trivial zero vector.
sz_pos.zero = sz_pos:xyz(0, 0, 0)
-- All 6 cardinal directions in 3 dimensions.
sz_pos.dirs = sz_table:new({
u = sz_pos:xyz(0, 1, 0),
d = sz_pos:xyz(0, -1, 0),
n = sz_pos:xyz(0, 0, 1),
s = sz_pos:xyz(0, 0, -1),
e = sz_pos:xyz(1, 0, 0),
w = sz_pos:xyz(-1, 0, 0),
})
-- Create a new sz_pos from a wallmounted param2 value.
local wm_lookup = { }
function sz_pos:from_wallmounted(w)
return wm_lookup[w]
end
for k, v in pairs(sz_pos.dirs) do
wm_lookup[minetest.dir_to_wallmounted(v)] = v
end
-- Get an array of all directions in random order. Useful for things
-- that operate in a random direction, or more than one direction in
-- random order.
function sz_pos.shuffledirs()
return sz_pos.dirs:values():shuffle()
end
------------------------------------------------------------------------
-- ARITHMETIC
-- Return true if two positions are equal.
function sz_pos:eq(pos)
if self == pos then return true end
return self.x == pos.x and self.y == pos.y and self.z == pos.z
end
-- Round to nearest integer coordinates.
function sz_pos:round()
return sz_pos:new({
x = math.floor(self.x + 0.5),
y = math.floor(self.y + 0.5),
z = math.floor(self.z + 0.5)
})
end
-- Vector addition.
function sz_pos:add(pos)
return sz_pos:new({
x = self.x + pos.x,
y = self.y + pos.y,
z = self.z + pos.z
})
end
-- Locate a random position within the given node space. Note that we
-- actually scatter a little less than the full node size, so that items
-- don't get hung up on ledges.
function sz_pos:scatter(scale)
scale = scale or 0.5
return self:round():add({
x = (math.random() - 0.5) * scale,
y = (math.random() - 0.5) * scale,
z = (math.random() - 0.5) * scale
})
end
-- Vector subtraction. A shortcut (both syntactically and computationally)
-- for sz_pos:add(pos:neg())
function sz_pos:sub(pos)
return sz_pos:new({
x = self.x - pos.x,
y = self.y - pos.y,
z = self.z - pos.z
})
end
-- Inverse vector, i.e. negate each coordinate.
function sz_pos:neg(pos)
return sz_pos:new({ x = -self.x, y = -self.y, z = -self.z })
end
-- Vector scalar multiplication.
function sz_pos:scale(k)
return sz_pos:new({
x = self.x * k,
y = self.y * k,
z = self.z * k
})
end
-- Vector dot multiplication.
function sz_pos:dot(pos)
return self.x * pos.x
+ self.y * pos.y
+ self.z * pos.z
end
-- Vector cross multiplication.
function sz_pos:cross(pos)
return sz_pos:new({
x = self.y * pos.z - self.z * pos.y,
y = self.z * pos.x - self.x * pos.z,
z = self.x * pos.y - self.y * pos.x
})
end
-- Get the euclidian length of the vector.
function sz_pos:len()
return math.sqrt(self:dot(self))
end
-- Return a vector in the same direction as the original, but whose
-- length is either zero (for zero-length vectors) or one (for any other).
function sz_pos:norm()
local l = self:len()
if l == 0 then return self end
return self:scale(1 / l)
end
-- Find the cardinal unit vector (one of 6 directions) that most closely
-- matches the general direction of this vector.
function sz_pos:dir()
local function bigz()
if self.z >= 0 then
return sz_pos.dirs.n
else
return sz_pos.dirs.s
end
end
local xsq = self.x * self.x
local ysq = self.y * self.y
local zsq = self.z * self.z
if xsq > ysq then
if zsq > xsq then
return bigz()
else
if self.x >= 0 then
return sz_pos.dirs.e
else
return sz_pos.dirs.w
end
end
else
if zsq > ysq then
return bigz()
else
if self.y >= 0 then
return sz_pos.dirs.u
else
return sz_pos.dirs.d
end
end
end
end
-- Get the absolute value of each coordinate. This can be used to
-- convert a vector into a 3D "size" for e.g. a bounding box.
function sz_pos:abs()
return sz_pos:new({
x = (self.x >= 0) and self.x or -self.x,
y = (self.y >= 0) and self.y or -self.y,
z = (self.z >= 0) and self.z or -self.z
})
end
------------------------------------------------------------------------
-- VOLUMETRICS
-- Scan all neighboring positions within a given range (including this
-- one). Return the first true return value and short circuit
-- execution.
function sz_pos:scan_around(range, func, ...)
for x = -range, -range do
for y = -range, -range do
for z = -range, -range do
local res = func(self:add({
x = x,
y = y,
z = z
}), ...)
if res then return res end
end
end
end
end
-- Scan a range around this position using a depth-last flood-fill
-- algorithm. Run a function for each position and return the first
-- true return value. If the function returns false (not nil), then
-- its neighbors are not scanned (unless included by another position).
-- Each position is visited once, in random order for each depth level.
function sz_pos:scan_flood(range, func)
local q = sz_table:new({ self })
local seen = { }
for d = 0, range do
local next = sz_table:new()
for i, p in ipairs(q) do
local res = func(p)
if res then return res end
if res == nil then
for k, v in pairs(sz_pos.dirs) do
local np = p:add(v)
local nk = np:hash()
if not seen[nk] then
seen[nk] = true
next:insert(np)
end
end
end
end
q = next:shuffle()
if #q < 1 then break end
end
end
-- A convenient wrapper for minetest.find_nodes_in_area that
-- takes a center and radius instead of two corners.
function sz_pos:nodes_in_area(size, ...)
size = sz_pos:new(size):abs()
local p0 = self:sub(size)
local p1 = self:add(size)
return minetest.find_nodes_in_area(p0, p1, ...)
end
------------------------------------------------------------------------
-- CONVERSION HELPERS
-- Convert to a string. Also the default formatting for display.
sz_pos.to_string = minetest.pos_to_string
sz_pos.__tostring = minetest.pos_to_string
-- Lookup the "simple" facedir (not factoring in rotation) for this pos.
function sz_pos:to_facedir(...)
return sz_facedir:from_vectors(self, ...)
end
-- Convert to a "wallmounted" direction, which is like a facedir but
-- without rotation.
function sz_pos:to_wallmounted()
return minetest.dir_to_wallmounted(self)
end
-- Compute a hash value, for hashtable lookup use.
sz_pos.hash = minetest.hash_node_position
------------------------------------------------------------------------
-- NODE ACCESS
-- Get the node at this position.
function sz_pos:node_get()
return minetest.get_node(self)
end
-- Change the node at this position.
function sz_pos:node_set(n)
return minetest.set_node(self, n or { name = "air" })
end
-- Get the definition of the node at this position, or nil if
-- there is no node here, or the node is not defined.
function sz_pos:nodedef()
local n = self:node_get()
if n == nil or n.name == nil then return end
return minetest.registered_nodes[n.name]
end
-- Get the light level at this node.
function sz_pos:light(...)
return minetest.get_node_light(self, ...) or 0
end
-- Shortcuts for some minetest utility functions.
sz_pos.node_swap = minetest.swap_node
sz_pos.light = minetest.get_node_light
sz_pos.timer = minetest.get_node_timer
sz_pos.drops = minetest.get_node_drops
------------------------------------------------------------------------
-- NODE METADATA ACCESS
-- Get the metadata reference for this node position.
function sz_pos:meta()
return minetest.get_meta(self)
end
-- Get the inventory for this node position.
function sz_pos:inv()
return self:meta():get_inventory()
end
-- Get both the node and metadata at this position,
-- as pure lua serializable data.
function sz_pos:nodemeta_get()
local t = self:node_get()
t.meta = sz_util.meta_to_lua(self:meta())
return t
end
-- Set both the node and metadata at this position,
-- using a value from nodemeta_get().
function sz_pos:nodemeta_set(nm)
self:node_set(nm)
if nm and nm.meta then
sz_util.lua_to_meta(nm.meta, self:meta())
end
end
-- Copy a node, including metadata.
function sz_pos:node_copyto(dest)
return sz_pos:new(dest):nodemeta_set(self:nodemeta_get())
end
-- Move a node, including metadata, and leaving
-- the specified content, or air, in its place.
function sz_pos:node_moveto(dest, nodemeta)
self:node_copyto(dest)
return self:nodemeta_set(nodemeta)
end
-- Trade 2 node positions, including metadata.
function sz_pos:node_trade(dest)
dest = sz_pos:new(dest)
return self:node_moveto(dest, dest:nodemeta_get())
end
------------------------------------------------------------------------
-- NODE DEFINITION ANALYSIS
-- If the definition of the node at this location has a registered hook
-- with the given name, trigger it with the given arguments.
function sz_pos:node_signal(hook, ...)
local def = self:nodedef()
if not def then return end
hook = def[hook]
if hook then return hook(...) end
end
sz_pos.nodedef_trigger = sz_pos.node_signal -- Legacy name
-- A safe accessor to get the groups for the node definition
-- at this location that will always return a table.
function sz_pos:groups()
return sz_table:new((self:nodedef() or { }).groups or { })
end
-- Return true if this location contains only air.
function sz_pos:is_empty()
local node = self:node_get()
return node and node.name == "air"
end
------------------------------------------------------------------------
-- OBJECT HANDLING
-- Eject items from this location, optionally flying in random
-- directions.
function sz_pos:item_eject(stack, speed, qty)
for i = 1, (qty or 1) do
local obj = minetest.add_item(self:scatter(), stack)
if obj then
obj:setvelocity(sz_pos.zero:scatter():scale(speed or 0))
end
end
end
-- An alternative to objects_in_radius that automatically excludes
-- players who don't have the "interact" privilege, i.e. are effectively
-- just spectators, and should not be "detected" by some code.
function sz_pos:tangible_in_radius(...)
local t = sz_table:new()
for k, v in pairs(self:objects_in_radius(...)) do
if not v:is_player() or minetest.get_player_privs(
v:get_player_name()).interact then
t[k] = v
end
end
return t
end
-- Hurt all entities within a radius of this location, with linear
-- fall-off, and an optional elliptoid shape.
function sz_pos:hitradius(r, hp, shape)
-- Default shape if not specified to a sphere of the
-- same radius as our search area.
if shape then
shape = sz_pos:new(shape):abs()
else
shape = sz_pos:xyz(r, r, r)
end
-- Degenerate elliptoid, no volume. Skip the rest, since
-- there's no actual damage volume, and we'd divide by 0.
if shape.x == 0 or shape.y == 0 or shape.z == 0 then return end
-- Scan for nearby objects.
for k, v in pairs(self:tangible_in_radius(r)) do
local p = self:sub(v:getpos())
local d = sz_pos:xyz(
p.x / shape.x,
p.y / shape.y,
p.z / shape.z):len()
if d < 1 then
v:set_hp(v:get_hp() - hp * (1 - d))
end
end
end
-- Shortcuts for some minetest utility functions.
sz_pos.objects_in_radius = minetest.get_objects_inside_radius
sz_pos.entity_add = minetest.add_entity
------------------------------------------------------------------------
return sz_pos

View File

@ -1,132 +0,0 @@
-- Extension methods for sz_pos that deal with general environmental
-- features, like the presence of nearby fluids or heat sources.
------------------------------------------------------------------------
-- FLUID DYNAMICS
-- Determine if the node at this position is a fluid, and measure
-- its "depth," up to a certain number of nodes above.
function sz_pos:fluid_depth(recurse)
local def = self:nodedef()
-- Solid nodes have an undefined depth.
if not def or def.walkable then return nil end
-- Non-walkable nodes are considered "air" and have
-- zero depth by default.
local depth = 0
-- Source blocks are 9 ticks deep,
-- since flowing are between 1 and 8.
if def.liquidtype == "source" then
depth = 9
-- Flowing block depth is stored in param2.
elseif def.liquidtype == "flowing" then
local node = self:node_get()
if not node then return 0 end
depth = node.param2 % 8 + 1
end
-- If this node has a full depth, then add the depth of the
-- node above, up to the recursion limit.
if depth >= 8 and recurse and recurse > 0 then
local above = self:add(sz_pos.dirs.u):fluid_depth(recurse - 1)
if above then depth = depth + above end
end
return depth, def
end
-- Determine if there is "pressure" from a nearby fluid to "wash out" this
-- node if it's washable. If washout is true, returns true, the position from
-- which washout is happening, and the definition of the node trying to do the
-- washout. If washout is false, returns nil.
function sz_pos:fluid_washout(mindepth)
-- Check for fluids from above. Any fluid level above will try
-- to descend into this node, washing out its contents.
local above = self:add(sz_pos.dirs.u)
local depth, def = above:fluid_depth()
if depth and depth > 0 then
return true, above, def
end
-- On each side, there must be fluid, that fluid must not have
-- a node below it into which the fluid would flow instead of
-- this one, and the fluid must have sufficient depth.
mindepth = mindepth or 2
for k, v in pairs({ sz_pos.dirs.n, sz_pos.dirs.s, sz_pos.dirs.e, sz_pos.dirs.w }) do
local p = self:add(v)
depth, def = p:fluid_depth()
if depth and depth > mindepth then
local b = p:add(sz_pos.dirs.d):node_get().name
if b ~= "air" and b ~= def.liquid_alternative_flowing then
return true, p, def
end
end
end
end
------------------------------------------------------------------------
-- HEAT AND FLAME
-- Determine if fire is allowed at a certain location.
function sz_pos:fire_allowed()
-- Check for whether fire should extinguish at this location.
if fire and fire.flame_should_extinguish
and fire.flame_should_extinguish(self) then
return
end
-- Flames can replace air, anything flammable, and other flames.
-- Multiple flames are supported using the "flame" group.
if self:is_empty() then return true end
local grp = self:groups()
if grp.flammable or grp.flame then return true end
end
-- Helper for heat_level() that calculates the distance-adjusted
-- contribution from a single node.
local function heat_contrib(pos, v, group, mult)
if pos:eq(v) then return 0 end
v = sz_pos:new(v)
if v:node_get().name == "ignore" then return end
local contrib = v:groups()[group]
if not contrib then return 0 end
if type(contrib) ~= "number" then contrib = 1 end
contrib = contrib * (mult or 1)
v = v:sub(pos)
v.y = v.y / 2
return contrib / (v:dot(v) * 3)
end
-- Calculate a "heat" level for a given node, based on contribuions from
-- other nearby nodes, for things like environmental cooking.
function sz_pos:heat_level()
local temp = 0
-- Search the vicinity around and below the node, and calculate
-- the temperature of its immediate surroundings. Nodes in the "hot"
-- group contribute to this based on their "hot" value, and contributions
-- are inversely proportional to distance squared, though y distance is
-- "squashed" to simulate convection.
local min = self:add(sz_pos:xyz(-1, -2, -1))
local max = self:add(sz_pos:xyz(1, 0, 1))
for k, v in pairs(minetest.find_nodes_in_area(min, max, { "group:hot" })) do
local c = heat_contrib(self, v, "hot")
if not c then return end
temp = temp + c
end
-- Similar to the "hot" check, we search for "puts_out_fire" (cold) nodes
-- above and sum up their contributions, in the negative.
local min = self:add(sz_pos:xyz(-1, 0, -1))
local max = self:add(sz_pos:xyz(1, 2, 1))
for k, v in pairs(minetest.find_nodes_in_area(min, max, { "group:puts_out_fire" })) do
local c = heat_contrib(self, v, "puts_out_fire", -5)
if not c then return end
temp = temp + c
end
return temp
end

View File

@ -1,100 +0,0 @@
-- Some special extensions for sz_pos to play special effects, with
-- hopefully-intelligent rate limiting to prevent very large complex
-- events from hammering networks and clients.
------------------------------------------------------------------------
-- SPECIAL EFFECTS RATE LIMITING
-- Cache to keep track of rate limit data.
local limitfx_cache = { }
-- Possibly add some special effect at the given location, subject to
-- rate limits. Up to "burst" effects are tolerated before limiting
-- to about 1 effect per "period" seconds, with some random scattering.
-- Rate limits are individual by "name", representing a distinct
-- audio or visual signature such as "smoke" or "boom." "func" is
-- only called if the rate limit check passes. "cell" is the size of
-- effect volume cells over which effects are spatially combined.
function sz_pos:limitfx(name, burst, period, func, cell)
local now = minetest.get_gametime()
-- Sanitize inputs.
if burst < 1 then burst = 1 end
if period <= 0 then period = 1 end
-- Look up the existing data for the given cell/name.
local cellkey = self:scale(1 / (cell or 4)):round():hash()
.. ":" .. name
local data = limitfx_cache[cellkey] or { q = 0, t = now }
-- Calculate what the "quantity" value would be now, based on
-- the existing data at a previous time.
local nowq = data.q - (now - data.t) / period
-- Do a random check for whether or not to play the FX, based
-- on the current quantity and burst tolerance, skip the rest
-- if we're not going to play.
if (1 + math.random() * (burst - 1)) <= nowq then return end
-- Update the limitfx cache.
limitfx_cache[cellkey] = { q = nowq + 1, t = now, p = period }
-- Play the effect, if passed as a closure, otherwise return
-- the fact that we would have played one.
if func then return func(self) end
return true
end
-- Garbage-collect the limitfx cache every so often, so it doesn't
-- gradually expand to fill RAM on a long-running server.
local function limitfx_gc()
minetest.after(60 + math.random() * 15, limitfx_gc)
local now = minetest.get_gametime()
local rm = { }
for k, v in pairs(limitfx_cache) do
if (v.q - (now - v.t) / v.p) <= 0 then
rm[k] = true
end
end
for k, v in pairs(rm) do
limitfx_cache[k] = nil
end
end
limitfx_gc()
------------------------------------------------------------------------
-- SPECIAL EFFECTS HELPERS
-- Play a sound at the location, with some sane defaults.
function sz_pos:sound(name, spec, burst, period, cell)
spec = spec or { }
spec.pos = spec.pos or self
return self:limitfx("sound:" .. name, burst or 3, period or 0.5,
function() return minetest.sound_play(name, spec) end,
cell or 4)
end
-- Add smoke particles with some sane defaults.
function sz_pos:smoke(qty, vel, spec, burst, period, name, cell)
vel = sz_pos:new(vel or sz_pos:xyz(2, 2, 2))
spec = spec or { }
spec.amount = qty or spec.amount or 5
spec.time = spec.time or 0.25
spec.minpos = spec.minpos or self:sub(sz_pos:xyz(0.5, 0.5, 0.5))
spec.maxpos = spec.maxpos or self:add(sz_pos:xyz(0.5, 0.5, 0.5))
spec.maxvel = spec.maxvel or vel:abs()
spec.minvel = spec.minvel or sz_pos:new(spec.maxvel):neg()
spec.maxacc = spec.maxacc or sz_pos.dirs.u:scale(3)
spec.minacc = spec.minacc or sz_pos:new(spec.maxacc):scale(1 / 3)
spec.maxexptime = spec.maxexptime or 2
spec.minexptime = spec.minexptime or spec.maxexptime / 2
spec.maxsize = spec.maxsize or 8
spec.minsize = spec.minsize or spec.maxsize / 2
spec.texture = spec.texture or "tnt_smoke.png"
if spec.collisiondetection == nil then spec.collisiondetection = true end
return self:limitfx(name or "particle:smoke", burst or 5, period or 1,
function() return minetest.add_particlespawner(spec) end,
cell or 4)
end

View File

@ -1,67 +0,0 @@
-- There are a number of ways in which machinery can be made to fail
-- catastrophically. When this happens, we dig the node, deconstruct it
-- into its craft components, suffer some random loss of some of those
-- components, scatter the pieces, and play some explosion effects.
------------------------------------------------------------------------
-- The main helper method to actually shatter a node; it figures out
-- the standard behavior mostly automatically. Accepts a reason string
-- to include in logs.
function sz_pos:shatter(reason, item, lossratio, speed, sound, smoke)
-- If we're not provided an item to shatter at this location,
-- then we're breaking a node; get the node that's being torn
-- apart and make sure it's valid.
local node
if not item then
node = self:node_get()
if not node or not node.name or node.name == "air"
or node.name == "ignore" or node.name == "" then
return
end
item = node.name
local def = minetest.registered_items[node.name]
if not def or not def.groups or not def.groups.can_shatter then return end
if def and def.drop and def.drop ~= "" then item = def.drop end
end
-- Admin log notification.
msg = item
if node and node.name ~= item then
msg = "node " .. node.name .. " -> " .. item
end
local msg = "shattered " .. msg .. " at " .. self:to_string()
if reason then msg = msg .. " because: " .. reason end
print(msg)
-- "Un-craft" the node into minute pieces.
local inv = sz_util.shatter_item(item)
-- Remove any shattered node.
if node then self:node_set() end
-- Any nearby entities get hurt from this. Amount of damage is
-- related to the amount of actual shrapnel produced.
local dmg = 0
for k, v in pairs(inv) do dmg = dmg + v end
dmg = math.sqrt(dmg)
self:hitradius(dmg, dmg)
-- For each item, there is a chance it's destroyed.
-- For everything that's not destroyed, eject it violently.
for k, v in pairs(inv) do
local q = 0
for i = 1, v do
if math.random() <= (lossratio or 0.8) then q = q + 1 end
end
if q > 1 then
self:item_eject(k, speed, q)
end
end
-- Play special effects.
if sound or sound ~= nil then self:sound(sound or "tnt_explode") end
self:smoke(5, sz_pos:xyz(speed, speed, speed):scale(0.25),
{texture = smoke})
end

View File

@ -1,94 +0,0 @@
-- This is a helper class for tables of arbitrary data. It provides
-- access to some of the Lua built-in table helpers, as well as some of
-- its own functionality.
------------------------------------------------------------------------
-- GENERAL HELPER METHODS
-- Randomize the order of an array. WARNING: modifies original!
function sz_table:shuffle()
local l = #self
for i, v in ipairs(self) do
local j = math.random(1, l)
self[i], self[j] = self[j], v
end
return self
end
-- Create an independent copy of this table. This is NOT a deep copy,
-- and all referenced objects are aliases of the original table.
function sz_table:copy()
local t = sz_table:new()
for k, v in pairs(self) do
t[k] = v
end
return t
end
-- Merge a list of tables together into one table. Each key in the
-- output table will hold the value of the first input table to define
-- a value for that key.
function sz_table.merge(...)
local t = sz_table:new()
for i, p in ipairs({...}) do
for k, v in pairs(p) do
if t[k] == nil then
t[k] = v
end
end
end
return t
end
-- Like sz_table.merge, merge a list of tables together, keeping the
-- value for each key from the first table to define it. This also
-- recursively deep-merges any values which are also tables.
function sz_table.mergedeep(...)
local t = sz_table:new()
for i, p in ipairs({...}) do
for k, v in pairs(p) do
local o = t[k]
if o == nil then
t[k] = v
elseif type(o) == "table" and type(v) == "table" then
t[k] = sz_table.mergedeep(o, v)
end
end
end
return t
end
-- Create an array of all keys in this table. This is useful for
-- creating duplicate-free lists by using creating a t[valure] = true
-- index, then using keys to convert it back to a {value, value...}
-- array.
function sz_table:keys()
local t = sz_table:new()
for k, v in pairs(self) do
t:insert(k)
end
return t
end
-- Create an array of all values in the table.
function sz_table:values()
local t = sz_table:new()
for k, v in pairs(self) do
t:insert(v)
end
return t
end
-- Copy minetest's serialize method.
sz_table.serialize = minetest.serialize
------------------------------------------------------------------------
-- LUA BUILT-IN LIBRARY METHODS
-- Copy all helper methods from the standard table library that aren't
-- already defined in sz_table, e.g. concat, insert, sort...
for k, v in pairs(table) do
if not sz_table[k] then
sz_table[k] = v
end
end

View File

@ -1,271 +0,0 @@
-- Some very basic common methods, and/or miscellany.
------------------------------------------------------------------------
-- MODIFY NODE DEFINITIONS
-- Merge modifications into a node definition. This works for current
-- and future registrations, by way of intercepting the
-- minetest.register_node method.
local nodemods = {}
function sz_util.modify_node(name, mod)
-- Mods can be a table, to be merged over the original;
-- convert to a function.
if type(mod) == "table" then
local modtbl = mod
mod = function(old) return sz_table.mergedeep(modtbl, old) end
end
-- Mod functions should only apply to each node type once.
local oldmod = mod
local modsdone = {}
mod = function(def, name, ...)
if modsdone[name] then return def end
modsdone[name] = true
return oldmod(def, name, ...)
end
-- Add the mod to the mods table, for future matching
-- registrations.
local mods = nodemods[name]
if not mods then
mods = sz_table:new()
nodemods[name] = mods
end
mods:insert(mod)
-- Apply the mod to any existing registrations.
local function modold(name, old)
local mn = minetest.get_current_modname()
if name:sub(1, mn:len() + 1) ~= (mn .. ":")
and name:sub(1, 1) ~= ":" then
name = ":" .. name
end
minetest.register_node(name, mod(old, name))
end
if name == "*" then
for k, v in pairs(minetest.registered_nodes) do
modold(k, v)
end
else
modold(name, minetest.registered_nodes[name])
end
end
local oldreg = minetest.register_node
minetest.register_node = function(name, def, ...)
local function applymods(mods)
if not mods then return end
for i, v in ipairs(mods) do
def = v(def, name)
end
end
applymods(nodemods[name])
applymods(nodemods["*"])
return oldreg(name, def, ...)
end
------------------------------------------------------------------------
-- SHATTER ITEM
-- Break apart an item into its constituent parts by effectively
-- reversing crafting recipes, favoring those that will produce more
-- total items.
function sz_util.shatter_item(item, iterations)
item = ItemStack(item)
-- Figure out the initial quantities of items we're working
-- with here, and put the item in the "working pile."
local inv = sz_table:new()
inv[item:get_name()] = item:get_count()
* (65535 - item:get_wear()) / 65535
-- Run the specified number of iterations of recipe reversal,
-- choosing recipes to try at random.
iterations = iterations or 10
for pass = 1, iterations do
-- Make sure we have at least 1 thing to break down.
local ik = inv:keys()
if #ik < 1 then break end
-- Pick a random item to break from the pile we've
-- accumulated.
ik = ik[math.random(1, #ik)]
-- Pick a random recipe for the item.
local recs = minetest.get_all_craft_recipes(ik)
local rec
if recs and #recs > 0 then
rec = recs[math.random(1, #recs)]
end
-- Require a valid crafting recipe. Cooking recipes, etc.
-- won't work because we're going to "uncraft" the item,
-- not "uncook" it.
if rec and rec.output and ((rec.type == "normal")
or (rec.type == "shapeless")) then
-- If we have more than 1, break apart a random
-- number of them.
local u = inv[ik]
if u > 1 then u = math.random(1, inv[ik]) end
-- Figure out how many items the recipe is supposed
-- to make; that will be a divisor for the quantities
-- produced by each item we break.
local q = ItemStack(rec.output):to_table().count
-- Determine if the item being broken up is made via
-- "precision" crafting; if it is, then we can break it
-- into similar "precision" items. Precision-crafted
-- items such as finely cut nodeboxes may be crafted
-- together into non-precision items like full nodes,
-- but "shattering" doesn't have the precision to reverse
-- that.
local luik = minetest.registered_items[ik]
local precisionok = luik and luik.groups
and luik.groups.precision_craft
-- Start copying the "uncrafting recipe outputs" into
-- a new list, and keep track of whether or not we
-- run into a situation that indicates that the recipe
-- is actually "irreversible" or that reversing it
-- could cause balance issues (i.e. breaking apart a
-- common item into rare and valuable components).
local newinv = sz_table:new()
local irrev = false
for rk, rv in pairs(rec.items) do
if rv and rv ~= "" then
-- We can't break apart into a group, it has
-- to be a specific thing.
if rv:sub(0, 6) == "group:" then
irrev = true
break
end
-- Look up the item definition. If the input item
-- is part of the special "precision_craft" group, then
-- the recipe is only reversible if the object being
-- broken up is also precision.
if not precisionok then
local lun = minetest.registered_items[rv]
if lun and lun.groups
and lun.groups.precision_craft then
irrev = true
break
end
end
-- Add the input item from the crafting recipe
-- into the uncrafting recipe output.
newinv[rv] = (newinv[rv] or 0) + (u / q)
end
end
-- Skip the rest if the recipe was deemed "irreversible."
if not irrev then
-- Round down the number of items produced
-- by the recipe, and count the total.
local t = 0
for rk, rv in pairs(newinv) do
rv = math.floor(rv)
if rv > 0 then
newinv[rk] = rv
else
newinv[rk] = nil
end
t = t + rv
end
-- Only apply the recipe if it produced more than 1
-- item, or 10% of the time if it's 1:1 with input.
if t > u or t == u and math.random(1, 10) == 1 then
-- Remove the original quantity of items that
-- were un-crafted.
local q = inv[ik] - u
if q < 1 then q = nil end
-- Add the new quantities.
inv[ik] = q
for nk, nv in pairs(newinv) do
inv[nk] = (inv[nk] or 0) + nv
end
end
end
end
end
-- Return the resulting table, which is keyed on item name,
-- and with item quantities in values. It is left as an exercise
-- to the caller to determine if any loss should be incurred (beyond
-- any partial quantity truncation already done) and how to deliver
-- the shattered items.
return inv
end
------------------------------------------------------------------------
-- METADATA / PURE TABLE CONVERSION
-- NodeMetaRef:to_table() apparently returns a "mixed" lua table with
-- some userdata refs mixed in with pure lua structures. These methods
-- attempt to convert metadata to/from pure lua data, which can be
-- serialized and copied around freely.
-- Convert metadata to a pure lua table.
function sz_util.meta_to_lua(meta)
if not meta then return end
local t = meta:to_table()
local o = {}
-- Copy fields, if there are any.
if t.fields then
for k, v in pairs(t.fields) do
o.f = t.fields
break
end
end
-- Copy inventory, if there are any.
local i = meta:get_inventory()
for k, v in pairs(i:get_lists()) do
o.i = o.i or {}
local j = {}
o.i[k] = j
local s = i:get_size(k)
j.s = s
j.i = {}
for n = 0, s do
local x = i:get_stack(k, n)
if x then j.i[n] = x:to_table() end
end
end
-- Try to return nil, if possible, for an empty
-- metadata table, otherwise return the data.
for k, v in pairs(o) do return o end
end
-- Write a pure lua metadata table back into a NodeMetaRef.
function sz_util.lua_to_meta(lua, meta)
-- Always clear the meta, and load the fields if any.
local t = {fields = {}, inventory = {}}
if lua and lua.f then t.fields = lua.f end
meta:from_table(t)
-- Load inventory, if any.
if lua and lua.i then
local i = m:get_inventory()
for k, v in pairs(lua.i) do
i:set_size(k, v.s)
for sk, sv in pairs(v.i) do
i:set_stack(ik, sk, ItemStack(sv))
end
end
end
end
------------------------------------------------------------------------
return sz_util

20
szutil_chatsocket/README Normal file
View File

@ -0,0 +1,20 @@
------------------------------------------------------------------------
This mod exposes a UNIX-domain socket for integrating arbitrary external
chat relay systems. You can use OpenBSD's netcat (nc -U) to connect to
the console from a regular command line, though the preferred way is to
connect a bridge program to act as a client for both this socket and the
external chat system.
The communication protocol is simple: all in-game chat (including emote,
player join/leave, etc) messages will be broadcast to all connected
socket clients. Each line of input text received (newline-terminated)
from any socket client will be displayed in-game and broadcast to all
other connected socket clients.
* * * * * * * * * * * * * * * * * * * * * * * *
WARNING: For this mod to work, LuaSockets must be installed, and this
mod must be listed in the "secure.trusted_mods" setting.
------------------------------------------------------------------------

104
szutil_chatsocket/init.lua Normal file
View File

@ -0,0 +1,104 @@
-- LUALOCALS < ---------------------------------------------------------
local assert, minetest, os, pairs, require, string, tostring
= assert, minetest, os, pairs, require, string, tostring
local os_remove, string_gsub, string_match
= os.remove, string.gsub, string.match
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
-- Keep track of multiple connected clients.
local clients = {}
-- Lua pattern string to strip color codes from chat text.
local stripcolor = minetest.get_color_escape_sequence('#ffffff')
stripcolor = string_gsub(stripcolor, "%W", "%%%1")
stripcolor = string_gsub(stripcolor, "ffffff", "%%x+")
-- Intercept broadcast messages and send them all clients.
do
local old_sendall = minetest.chat_send_all
function minetest.chat_send_all(text, ...)
local t = string_gsub(text, stripcolor, "")
for k, v in pairs(clients) do
if v.sent ~= t then
v.sock:send(t .. "\n")
else
v.sent = nil
end
end
return old_sendall(text, ...)
end
end
-- Intercept non-command chat messages and send them to all clients.
minetest.register_on_chat_message(function(name, text)
if text:sub(1,1) ~= "/" then
local t = string_gsub(text, stripcolor, "")
for k, v in pairs(clients) do
v.sock:send("<" .. name .. "> " .. t .. "\n")
end
end
end)
-- Create a listening unix-domain socket inside the world dir.
-- All sockets and connections will be non-blocking, by setting
-- timeout to zero, so we don't block the game engine.
local master = assert(require("socket.unix")())
assert(master:settimeout(0))
local sockpath = minetest.get_worldpath() .. "/" .. modname .. ".sock"
os_remove(sockpath)
assert(master:bind(sockpath))
assert(master:listen())
-- Helper function to log console debugging information.
local function clientlog(client, str)
minetest.log(modname .. "[" .. client.id .. "]: " .. str)
end
-- Attempt to accept a new client connection.
local function accept()
local sock, err = master:accept()
if sock then
-- Make the new client non-blocking too.
assert(sock:settimeout(0))
-- Try to determine an identifier for the connection.
local id = string_match(tostring(sock), "0x%x+")
or tostring(sock)
-- Register new connection.
local c = {id = id, sock = sock}
clients[id] = c
clientlog(c, "connected")
elseif err ~= "timeout" then
minetest.log(modname .. " accept(): " .. err)
end
end
-- Receive chat messages.
local function conchat(client, line)
minetest.chat_send_all(line);
end
-- Attempt to receive an input line from the console client, if
-- one is ready (buffered non-blocking IO)
local function receive(client)
local line, err = client.sock:receive("*l")
if line ~= nil then
clientlog(client, "message: " .. line)
client.sent = line
minetest.chat_send_all(line)
elseif err ~= "timeout" then
clientlog(client, err)
clients[client.id] = nil
end
end
-- On every server cycle, check for new connections, and
-- process commands from existing ones.
minetest.register_globalstep(function()
accept()
for id, client in pairs(clients) do receive(client) end
end)

21
szutil_consocket/README Normal file
View File

@ -0,0 +1,21 @@
------------------------------------------------------------------------
This mod exposes a console for running administrative commands on a
UNIX-domain socket. Multiple clients can connect and issue commands
independently. You can use OpenBSD's netcat (nc -U) to connect to
the console from a regular command line.
Commands issued will be run as the CONSOLE "virtual player," who is
granted ALL permissions. Ordinary clients cannot connect as the
CONSOLE player name.
Only immediate responses to command input are output to the socket.
Delayed responses, other gameplay action, and chat are not sent to
the client, and can only be seen in server logs or in-game.
* * * * * * * * * * * * * * * * * * * * * * * *
WARNING: For this mod to work, LuaSockets must be installed, and this
mod must be listed in the "secure.trusted_mods" setting.
------------------------------------------------------------------------

176
szutil_consocket/init.lua Normal file
View File

@ -0,0 +1,176 @@
-- LUALOCALS < ---------------------------------------------------------
local assert, error, ipairs, minetest, os, pairs, pcall, require,
string, tostring
= assert, error, ipairs, minetest, os, pairs, pcall, require,
string, tostring
local os_remove, string_gsub, string_lower, string_match
= os.remove, string.gsub, string.lower, string.match
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
-- VIRTUAL PLAYER SETUP
-- Name for the virtual "player" used for the console.
local CONSOLE = "CONSOLE"
-- Override privileges for the "console player", granting
-- them every privilege, including "cheats". We assume that
-- there are no "anti-privileges" registered that would
-- actually limit access.
do
local old_checkprivs = minetest.get_player_privs
function minetest.get_player_privs(who, ...)
if who == CONSOLE then
local p = {}
for k, v in pairs(minetest.registered_privileges) do
if k ~= "shout" then p[k] = true end
end
return p
else
return old_checkprivs(who, ...)
end
end
end
-- Disallow any player from actually connecting with the
-- "console" player name, which would grant them the corresponding
-- special privileges.
minetest.register_on_prejoinplayer(function(name)
if string_lower(name) == string_lower(CONSOLE) then
return "Player name " .. CONSOLE .. " is reserved."
end
end)
-- Hook to send messages to a client socket for immediate
-- command responses.
local conmsg
-- Lua pattern string to strip color codes from chat text.
local stripcolor = minetest.get_color_escape_sequence('#ffffff')
stripcolor = string_gsub(stripcolor, "%W", "%%%1")
stripcolor = string_gsub(stripcolor, "ffffff", "%%x+")
-- Intercept messages sent to the "console" player and send them
-- to the actual console instead.
do
local old_chatsend = minetest.chat_send_player
function minetest.chat_send_player(who, text, ...)
if who == CONSOLE then
text = string_gsub(text, stripcolor, "")
if conmsg then conmsg(text) end
return print("to " .. CONSOLE .. ": " .. text)
else
return old_chatsend(who, text, ...)
end
end
end
-- Intercept broadcast messages and send them to the console
-- user, if in response to a command.
do
local old_sendall = minetest.chat_send_all
function minetest.chat_send_all(text, ...)
if conmsg then conmsg(string_gsub(text, stripcolor, "")) end
return old_sendall(text, ...)
end
end
------------------------------------------------------------------------
-- CONSOLE CLIENT SOCKETS
-- Keep track of multiple connected clients.
local clients = {}
-- Create a listening unix-domain socket inside the world dir.
-- All sockets and connections will be non-blocking, by setting
-- timeout to zero, so we don't block the game engine.
local master = assert(require("socket.unix")())
assert(master:settimeout(0))
local sockpath = minetest.get_worldpath() .. "/" .. modname .. ".sock"
os_remove(sockpath)
assert(master:bind(sockpath))
assert(master:listen())
-- Helper function to log console debugging information.
local function clientlog(client, str)
print(modname .. "[" .. client.id .. "]: " .. str)
end
-- Attempt to accept a new client connection.
local function accept()
local sock, err = master:accept()
if sock then
-- Make the new client non-blocking too.
assert(sock:settimeout(0))
-- Try to determine an identifier for the connection.
local id = string_match(tostring(sock), "0x%x+")
or tostring(sock)
-- Register new connection.
local c = {id = id, sock = sock}
clients[id] = c
clientlog(c, "connected")
c.sock:send("connected as " .. id .. "\n> ")
elseif err ~= "timeout" then
print(CONSOLE .. " accept(): " .. err)
end
end
-- Execute actual console commands.
local function concmd(client, line)
-- Special "exit" command to disconnect, e.g. when
-- unable to send an EOF or interrupt.
if line == "/exit" then
clients[client.id] = nil
return client.sock:close()
end
-- Try to run registered chat commands, and return a
-- failure if not found.
for k, v in ipairs(minetest.registered_on_chat_messages) do
local ok, err = pcall(function() return v(CONSOLE, line) end)
if ok and err then return end
if not ok then
return minetest.chat_send_player(CONSOLE, err)
end
end
minetest.chat_send_player(CONSOLE, "unrecognized command")
end
-- Attempt to receive an input line from the console client, if
-- one is ready (buffered non-blocking IO)
local function receive(client)
local line, err = client.sock:receive("*l")
if line ~= nil then
-- Prepend the slash. We assume that all input is to
-- be commands rather than accidentally leaking chat.
while line:sub(1, 1) == "/" do
line = line:sub(2)
end
line = "/" .. line
clientlog(client, "command: " .. line)
-- Hook console messages and send to client, too.
conmsg = function(x)
client.sock:send(x .. "\n")
end
local ok, err = pcall(function() concmd(client, line) end)
conmsg = nil
if not ok then return error(err) end
client.sock:send("> ")
elseif err ~= "timeout" then
clientlog(client, err)
clients[client.id] = nil
end
end
-- On every server cycle, check for new connections, and
-- process commands from existing ones.
minetest.register_globalstep(function()
accept()
for id, client in pairs(clients) do receive(client) end
end)

View File

@ -1,3 +1,5 @@
------------------------------------------------------------------------
This is a hack to fix lighting and fluid transform issues automatically,
using a carefully metered allotment of CPU time to do background
recalculations.
@ -8,7 +10,7 @@ near the loaded/ignore frontier, and in map generation.
Some example issues:
- Mapgen lighting seems to look only at the heightmap, and
ignore other changes, such as adding water or tree, so e.g.
ignore other changes, such as adding water or trees, so e.g.
the ocean floor will be incorrectly fully-lit. Custom mapgen
cannot seem to modify the heightmap, so large carve-outs may
end up unlit.
@ -25,3 +27,5 @@ This mod addresses these by:
See init.lua for configuration options. Settings are set to sane
defaults for a server that is NOT heavily loaded; some tuning may be
necessary otherwise.
------------------------------------------------------------------------

172
szutil_fixhack/init.lua Normal file
View File

@ -0,0 +1,172 @@
-- LUALOCALS < ---------------------------------------------------------
local math, minetest, pairs, tonumber
= math, minetest, pairs, tonumber
local math_floor, math_log, math_random
= math.floor, math.log, math.random
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
-- Proportion of time to spend each cycle on recalculations. For instance,
-- a value of 0.05 will mean that we attempt to use about 5% of each step
-- cycle trying to do recalculates.
local cycletime = tonumber(minetest.setting_get(modname .. "_cycletime")) or 0.02
-- How often statistics are written to the log, to track server CPU use.
local stattime = tonumber(minetest.setting_get(modname .. "_stattime")) or 3600
-- How often a mapblock can be recalculated, at the earliest.
local calctime = tonumber(minetest.setting_get(modname .. "_calctime")) or 60
-- Simple positional helper functions.
local function posadd(a, b) return {x = a.x + b.x, y = a.y + b.y, z = a.z + b.z} end
local function blockmin(v) return {x = v.x * 16, y = v.y * 16, z = v.z * 16} end
local function blockmax(v) return posadd(blockmin(v), {x = 15, y = 15, z = 15}) end
-- Generate exponentially-distributed random values, so low values (nearby
-- positions) are more likely to get prompt attention.
local function exporand()
local r = math_random()
if r == 0 then return exporand() end
r = math_log(r)
if math_random() < 0.5 then r = -r end
return r
end
local mapgenqueue = {}
-- Run voxelmanip lighting calc on chunks post-mapgen. It seems as though
-- the default mapgen lighting calc disagrees with this one (water does not
-- absorb light; bug?)
minetest.register_on_generated(function(minp, maxp)
for x = math_floor(minp.x / 16), math_floor(maxp.x / 16) do
for y = math_floor(minp.y / 16), math_floor(maxp.y / 16) do
for z = math_floor(minp.z / 16), math_floor(maxp.z / 16) do
local pos = {x = x, y = y, z = z}
mapgenqueue[minetest.hash_node_position(pos)] = pos
end
end
end
end)
-- Keep track of each block processed, and when its check expires
-- and reprocessing is possible.
local processed = {}
-- Statistics for reporting display.
local proctime = 0
local totaltime = 0
local totalqty = 0
-- Amount of time available for processing.
local availtime = 0
-- Helper method to automatically process blocks
-- (shared by mapgen and random queue).
local function procblock(pos, nextcalc)
-- Don't reprocess already-processed blocks too soon.
local h = minetest.hash_node_position(pos)
if processed[h] then return end
processed[h] = nextcalc
-- Don't process a block if it's not loaded, or if any of its
-- neighbors is not loaded, as that can cause lighting bugs (at least).
if not minetest.get_node_or_nil(blockmin(pos))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 1, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 1, y = 0, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = -1, y = 0, z = 0})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 0, z = 1})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = 0, z = -1})))
or not minetest.get_node_or_nil(blockmin(posadd(pos, {x = 0, y = -1, z = 0})))
then return end
-- Recalc all fluids and lighting in that block.
local vm = minetest.get_voxel_manip(blockmin(pos), blockmax(pos))
vm:update_liquids()
vm:calc_lighting()
vm:write_to_map()
vm:update_map()
-- Keep track for periodic statistic summary.
totalqty = totalqty + 1
end
-- Run recalculates during each cycle.
minetest.register_globalstep(function(dtime)
-- Don't attempt to do anything if nobody is connected. There seems
-- to be some issue that may be crashing servers that run for a long
-- time with no players connected, which this may help avert.
local players = minetest.get_connected_players()
if #players < 1 then return end
-- Add our allotment to the amount of time available.
availtime = availtime + dtime * cycletime
-- Attenuate stored surplus/deficit time, so that we don't accumulate
-- a massive deficit (suspending recalcs for a long time) or a massive
-- surplus (effectively freezing the game for a ton of redundant
-- recalcs).
availtime = availtime * 0.95
-- Calculate when the recalculation is supposed to stop, based on
-- real-time clock.
local starttime = minetest.get_us_time() / 1000000
local endtime = starttime + availtime
-- Get the current timestamp, to be used in expiration timestamps.
local now = starttime
-- Prune already-expired blocks from the processed list.
local del = {}
for k, v in pairs(processed) do
if v < now then del[k] = true end
end
for k, v in pairs(del) do
processed[k] = nil
end
local nextcalc = now + calctime
-- Process generated chunks first.
for k, v in pairs(mapgenqueue) do
procblock(v, nextcalc)
mapgenqueue = {}
end
-- Skip random recalcs if we don't actually have any time to do them.
if endtime > starttime then
-- Keep searching for blocks to recalc until we run out of allotted time.
while (minetest.get_us_time() / 1000000) < endtime do
-- Pick a random player, and then pick a random exponentially-
-- distributed random block around that player.
local pos = players[math_random(1, #players)]:getpos()
pos.x = math_floor(pos.x / 16 + exporand() + 0.5)
pos.y = math_floor(pos.y / 16 + exporand() + 0.5)
pos.z = math_floor(pos.z / 16 + exporand() + 0.5)
procblock(pos, nextcalc)
end
end
-- Update our actual end time (in case we ran long with voxel operations),
-- and keep track for periodic statistic summary.
endtime = minetest.get_us_time() / 1000000
totaltime = totaltime + dtime
proctime = proctime + endtime - starttime
-- Update available time allotment.
availtime = availtime + starttime - endtime
end)
-- Periodically display statistics, so we can track actual performance.
local function reportstats()
if totaltime == 0 then return end
local function ms(i) return math_floor(i * 1000000) / 1000 end
minetest.log(modname .. ": processed " .. totalqty .. " mapblocks using "
.. ms(proctime) .. "ms out of " .. ms(totaltime) .. "ms ("
.. (math_floor(proctime / totaltime * 10000) / 100)
.. "%), " .. ms(availtime) .. "ms saved")
totalqty = 0
totaltime = 0
proctime = 0
minetest.after(stattime, reportstats)
end
minetest.after(stattime, reportstats)

View File

@ -1,3 +1,5 @@
------------------------------------------------------------------------
Display a simple HUD meter showing an estimate of current server "lag,"
i.e. the size of "steps" the server is running. This allows admins to
monitor system performance, or mod authors to monitor code performance
@ -8,3 +10,5 @@ some parameters can be tuned. See init.lua for configuration details.
Players will see the lagometer iff they have the "lagometer" priv.
The meter displays just above the health bar.
------------------------------------------------------------------------

View File

@ -1,3 +1,10 @@
-- LUALOCALS < ---------------------------------------------------------
local minetest, pairs, string, tonumber
= minetest, pairs, string, tonumber
local string_format
= string.format
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
-- How often to publish updates to players. Too infrequent and the meter
@ -19,32 +26,67 @@ local meters = {}
-- Create a separate privilege for players to see the lagometer. This
-- feature is too "internal" to show to all players unconditionally,
-- but not so "internal" that it should depend on the "server" priv.
core.register_privilege("lagometer", "Can see the lagometer")
minetest.register_privilege("lagometer", "Can see the lagometer")
minetest.register_chatcommand("lagometer", {
description = "Toggle the lagometer",
privs = {lagometer = true},
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then return end
local old = player:get_attribute("lagometer") or ""
local v = (old == "") and "1" or ""
player:set_attribute("lagometer", v)
minetest.chat_send_player(name, "Lagometer: "
.. (v ~= "" and "ON" or "OFF"))
end,
})
-- Function to publish current lag values to all receiving parties.
local function publish()
-- Format the lag string with the raw numerical value, and
-- a cheapo ASCII "bar graph" to provide a better visual cue
-- for its general magnitude.
local t = string.format("Server Lag: %2.2f ", lag)
local t = string_format("Server Lag: %2.2f ", lag)
local q = lag * 10 + 0.5
if q > 40 then q = 40 end
for i = 1, q, 1 do t = t .. "|" end
-- Apply the appropriate text to each meter.
for k, v in pairs(meters) do
for _, p in ipairs(minetest.get_connected_players()) do
local n = p:get_player_name()
local v = meters[n]
-- Players with privilege will see the meter, players without
-- will get an empty string. The meters are always left in place
-- rather than added/removed for simplicity, and to make it easier
-- to handle when the priv is granted/revoked while the player
-- is connected.
local s = ""
if minetest.get_player_privs(k).lagometer then s = t end
if minetest.get_player_privs(n).lagometer
and (p:get_attribute("lagometer") or "") ~= ""
then s = t end
-- Only apply the text if it's changed, to minimize the risk of
-- generating useless unnecessary packets.
if v.text ~= s then
v.player:hud_change(v.hud, "text", s)
if s ~= "" and not v then
meters[n] = {
text = s,
hud = p:hud_add({
hud_elem_type = "text",
position = { x = 0.5, y = 1 },
text = s,
alignment = { x = 1, y = -1 },
number = 0xC0C0C0,
scale = { x = 1280, y = 20 },
offset = { x = -262, y = -88 }
})
}
elseif v and s == "" then
p:hud_remove(v.hud)
meters[n] = nil
elseif v and v.text ~= s then
p:hud_change(v.hud, "text", s)
v.text = s
end
end
@ -62,33 +104,14 @@ update()
-- up, publish immediately; if not, allow the timer to publish as
-- it falls off.
minetest.register_globalstep(function(dtime)
lag = lag * falloff
if dtime > lag then
lag = dtime
publish()
end
end)
-- When players join, create and register their HUD. These
-- are created unconditionally, regardless of player privilege,
-- to simplify granting/removal without having to re-login.
minetest.register_on_joinplayer(function(player)
meters[player:get_player_name()] = {
player = player,
text = "",
hud = player:hud_add({
hud_elem_type = "text",
position = { x = 0.5, y = 1 },
text = "",
alignment = { x = 1, y = -1 },
number = 0xC0C0C0,
scale = { x = 1280, y = 20 },
offset = { x = -262, y = -88 }
})
}
end)
lag = lag * falloff
if dtime > lag then
lag = dtime
publish()
end
end)
-- Remove meter registrations when players leave.
minetest.register_on_leaveplayer(function(player)
meters[player:get_player_name()] = nil
end)
meters[player:get_player_name()] = nil
end)

View File

@ -1,3 +1,5 @@
------------------------------------------------------------------------
Limit the size of the generated world (i.e. to limit its HDD space
usage). Terrain outside the generated bounds will be filled with air
(leaving the allowed continent as a floating island), discouraging
@ -11,3 +13,5 @@ do not free-fall.
See init.lua for configuration details. The only required setting
is sz_limitworld_scale; if this is not set, the mod is effectively
disabled.
------------------------------------------------------------------------

View File

@ -1,3 +1,10 @@
-- LUALOCALS < ---------------------------------------------------------
local VoxelArea, math, minetest, pairs, tonumber
= VoxelArea, math, minetest, pairs, tonumber
local math_floor
= math.floor
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
@ -17,7 +24,7 @@ local margin = tonumber(minetest.setting_get(modname .. "_margin")) or 2
if scale.x <= margin or scale.y <= margin or scale.z <= margin then return end
local iscale = { x = scale.x - margin, y = scale.y - margin, z = scale.z - margin }
print(modname .. ": scale " .. minetest.pos_to_string(scale) .. " center "
minetest.log(modname .. ": scale " .. minetest.pos_to_string(scale) .. " center "
.. minetest.pos_to_string(center) .. " margin " .. margin)
-- Critical speed at which falling off the world damages players.
@ -26,83 +33,78 @@ local fallspeed = tonumber(minetest.setting_get(modname .. "_fallspeed")) or 20
-- Relative rate of damage (linear with airspeed) for falling-off-world damage.
local falldamage = tonumber(minetest.setting_get(modname .. "_falldamage")) or 0.25
print(modname .. ": falling critical speed " .. fallspeed
minetest.log(modname .. ": falling critical speed " .. fallspeed
.. " damage rate " .. falldamage)
------------------------------------------------------------------------
-- NODE CONTENT ID'S
-- ID of air to replace everything outside of world.
local c_air = minetest.get_content_id("air")
-- ID of solid node to replace liquids near the edge, to keep
-- them from spilling down to infinity.
local c_solid = minetest.get_content_id("default:stone")
-- ID's of all liquid nodes that need to be solidified near the edge.
local c_air, s_solid;
local c_liquid = {}
minetest.after(0, function()
for k, v in pairs(minetest.registered_nodes) do
if v.liquidtype ~= "none" then
local i = minetest.get_content_id(k)
if i then c_liquid[i] = true end
c_air = minetest.get_content_id("air")
c_solid = minetest.get_content_id("mapgen_stone")
for k, v in pairs(minetest.registered_nodes) do
if v.liquidtype ~= "none" then
local i = minetest.get_content_id(k)
if i then c_liquid[i] = true end
end
end
end
end)
end)
------------------------------------------------------------------------
-- MAP GENERATION LOGIC
-- Map generation hook that does actual terrain replacement.
minetest.register_on_generated(function(minp, maxp)
local vox, emin, emax = minetest.get_mapgen_object("voxelmanip")
local data = vox:get_data()
local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
local x, y, z, dx, dy, dz, ix, iy, iz, rs, irs, i
for z = emin.z, emax.z do
dz = (z - center.z) / scale.z
dz = dz * dz
iz = (z - center.z) / iscale.z
iz = iz * iz
for x = emin.x, emax.x do
dx = (x - center.x) / scale.x
dx = dx * dx
ix = (x - center.x) / iscale.x
ix = ix * ix
for y = emin.y, emax.y do
repeat
-- Flatten y coordinate above center to zero, effectively
-- treating an infinite cylinder above the bottom hemispherical
-- shell of the world as "inside," to reduce lighting bugs caused
-- by upper hemispherical carve-outs creating heightmap
-- disagreements with mapgen.
dy = y - center.y
if dy > 0 then dy = 0 end
local vox, emin, emax = minetest.get_mapgen_object("voxelmanip")
local data = vox:get_data()
local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
local x, y, z, dx, dy, dz, ix, iy, iz, rs, irs, i
for z = emin.z, emax.z do
dz = (z - center.z) / scale.z
dz = dz * dz
iz = (z - center.z) / iscale.z
iz = iz * iz
for x = emin.x, emax.x do
dx = (x - center.x) / scale.x
dx = dx * dx
ix = (x - center.x) / iscale.x
ix = ix * ix
for y = emin.y, emax.y do
repeat
-- Flatten y coordinate above center to zero, effectively
-- treating an infinite cylinder above the bottom hemispherical
-- shell of the world as "inside," to reduce lighting bugs caused
-- by upper hemispherical carve-outs creating heightmap
-- disagreements with mapgen.
dy = y - center.y
if dy > 0 then dy = 0 end
-- Inside the inner allowed area: no changes.
iy = dy / iscale.y
iy = iy * iy
irs = ix + iy + iz
if irs < 1 then break end
-- Inside the inner allowed area: no changes.
iy = dy / iscale.y
iy = iy * iy
irs = ix + iy + iz
if irs < 1 then break end
i = area:index(x, y, z)
i = area:index(x, y, z)
-- Outside the outer area: only air allowed.
dy = dy / scale.y
dy = dy * dy
rs = dx + dy + dz
if rs >= 1 then data[i] = c_air break end
-- Outside the outer area: only air allowed.
dy = dy / scale.y
dy = dy * dy
rs = dx + dy + dz
if rs >= 1 then data[i] = c_air break end
-- In the "shell" zone: solidify liquids.
if c_liquid[data[i]] then data[i] = c_solid end
until true
-- In the "shell" zone: solidify liquids.
if c_liquid[data[i]] then data[i] = c_solid end
until true
end
end
end
end
vox:set_data(data)
vox:calc_lighting()
vox:write_to_map()
end)
vox:set_data(data)
vox:calc_lighting()
vox:write_to_map()
end)
------------------------------------------------------------------------
-- DAMAGE FROM FALLING OFF THE WORLD
@ -136,10 +138,10 @@ local function dofalldmg(dtime, player)
-- HP saved from before.
local n = player:get_player_name()
local d = (falldmg[n] or 0) + dtime
* (-vy - fallspeed) * falldamage
* (-vy - fallspeed) * falldamage
-- Apply whole HP damage to the player, if any.
local f = math.floor(d)
local f = math_floor(d)
if f > 0 then player:set_hp(player:get_hp() - f) end
-- Save remaining fractional HP for next cycle.
@ -148,7 +150,7 @@ end
-- Register hook to apply falling damage to all players.
minetest.register_globalstep(function(dtime)
for _, player in pairs(minetest.get_connected_players()) do
dofalldmg(dtime, player)
end
end)
for _, player in pairs(minetest.get_connected_players()) do
dofalldmg(dtime, player)
end
end)

50
szutil_logtrace/init.lua Normal file
View File

@ -0,0 +1,50 @@
-- LUALOCALS < ---------------------------------------------------------
local ipairs, minetest, pairs, table, type
= ipairs, minetest, pairs, table, type
local table_concat
= table.concat
-- LUALOCALS > ---------------------------------------------------------
minetest.register_privilege("logtrace", "Receive server log messages")
minetest.register_chatcommand("logtrace", {
description = "Toggle debug trace messages",
privs = {logtrace = true},
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then return end
local old = player:get_attribute("logtrace") or ""
local v = (old == "") and "1" or ""
player:set_attribute("logtrace", v)
minetest.chat_send_player(name, "Log Trace: "
.. (v ~= "" and "ON" or "OFF"))
end,
})
local function logtrace(...)
local t = {"#", ...}
for i, v in ipairs(t) do
if type(v) == "table" then
t[i] = minetest.serialize(v):sub(("return "):length())
end
end
local msg = table_concat(t, " ")
for _, p in pairs(minetest.get_connected_players()) do
local n = p:get_player_name()
if minetest.get_player_privs(n).logtrace then
local a = p:get_attribute("logtrace")
if a and a ~= "" then
minetest.chat_send_player(n, msg)
end
end
end
end
local function tracify(func)
return function(...)
logtrace(...)
return func(...)
end
end
print = tracify(print)
minetest.log = tracify(minetest.log)

14
szutil_motd/README Normal file
View File

@ -0,0 +1,14 @@
------------------------------------------------------------------------
This mod displays a pop-up message to players automatically upon login
using a nice formspec interface. Messages are only displayed
automatically if changed since the last viewing, or if the player has
never seen the message before. Players can summon the message manually
with a chat command.
This can be used to display server rules, hints, and/or news on player
login. The form is designed for 72-column hand-wrapped text, and
displays using minetest's proportional font (indentations do not
necessarily line up).
------------------------------------------------------------------------

View File

@ -1,12 +1,19 @@
-- LUALOCALS < ---------------------------------------------------------
local io, minetest, tonumber
= io, minetest, tonumber
local io_close, io_open
= io.close, io.open
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
-- Generic function to read an entire text file, used to load
-- the "seen" database, and to read the motd.
local function readfile(path, trans)
local f = io.open(path, "rb")
local f = io_open(path, "rb")
if f then
local d = f:read("*all")
f:close()
io_close(f)
if trans then return trans(d) end
return d
end
@ -30,13 +37,13 @@ do
local tbw = fsw - 0.25
local tbh = fsh - 0.75
fspref = "size[" .. fsw .. "," .. fsh .. ",true]"
.. "textlist[0,0;" .. tbw .. "," .. tbh .. ";motd;"
.. "textlist[0,0;" .. tbw .. "," .. tbh .. ";motd;"
fssuff = ";0;true]button_exit[0," .. tbh .. ";" .. fsw
.. ",1;ok;Continue]"
.. ",1;ok;Continue]"
end
-- Function to send the actual MOTD content to the player, in either
-- automatic mod (on login) or "forced" mode (on player request).
-- automatic mode (on login) or "forced" mode (on player request).
local function sendmotd(name, force)
-- Load the MOTD fresh on each request, so changes can be
-- made while the server is running, and take effect immediately.
@ -72,10 +79,10 @@ local function sendmotd(name, force)
-- so we don't send another copy of the same content to the
-- same player automatically.
seendb[name] = hash
local f = io.open(seenpath, "wb")
local f = io_open(seenpath, "wb")
if f then
f:write(minetest.serialize(seendb))
f:close()
io_close(f)
end
-- Remind the player where they can get the MOTD if they

View File

@ -1,3 +1,5 @@
------------------------------------------------------------------------
This mod allows administering a server using a single, shared password.
Any user who knows the password can authenticate with it and gain the
"privs" privilege, which allows them to gain any other privilege. This
@ -6,6 +8,11 @@ one specific account always to have all permissions.
Set the sz_suadmin_password setting to the desired shared password.
Plain-text passwords are automatically upgraded to salted hashes as
appropriate; this may rewrite the config file at startup time.
appropriate; this may rewrite the config file at startup time. Enable
the sz_suadmin_strict setting to require a /su password to access
the "privs" privilege; users are strippd of it on login/logout
automatically.
See the "/su" and "/unsu" commands for in-game admin access.
------------------------------------------------------------------------

View File

@ -1,3 +1,10 @@
-- LUALOCALS < ---------------------------------------------------------
local error, math, minetest, os, pcall
= error, math, minetest, os, pcall
local math_random, os_time
= math.random, os.time
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
@ -15,7 +22,7 @@ local function gensalt()
local alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
local salt = ""
while salt:len() < hashlen do
local n = math.random(1, alpha:len())
local n = math_random(1, alpha:len())
salt = salt .. alpha:sub(n, n)
end
return salt
@ -55,7 +62,7 @@ upgradepass(minetest.setting_save)
-- - Users with "server" privs (who can use /set) but without "privs"
-- privs cannot exploit certain known settings to gain "privs" access.
if minetest.chatcommands and minetest.chatcommands.set
and minetest.chatcommands.set.func then
and minetest.chatcommands.set.func then
local prefix = modname .. "_"
local oldfunc = minetest.chatcommands.set.func
minetest.chatcommands.set.func = function(name, ...)
@ -109,37 +116,52 @@ local function changeprivs(name, priv)
privs.privs = priv
minetest.set_player_privs(name, privs)
return true, "Privileges of " .. name .. ": "
.. minetest.privs_to_string(minetest.get_player_privs(name))
.. minetest.privs_to_string(minetest.get_player_privs(name))
end
-- Keep track of last attempt, and apply a short delay to rate-limit
-- players trying to brute-force passwords.
local retry = {}
-- Register /su command to escalate privs by password. The argument is the
-- password, which must match the one configured. If no password is configured,
-- then the command will always return failure.
minetest.register_chatcommand("su", {
description = "Escalate privileges by password.",
func = function(name, pass)
if minetest.check_player_privs(name, {privs = true}) then
return false, "You are already a superuser."
end
local hash = minetest.setting_get(modname .. "_password_hash")
local salt = minetest.setting_get(modname .. "_password_salt")
if not pass or pass == ""
description = "Escalate privileges by password.",
func = function(name, pass)
-- Check for already admin.
if minetest.check_player_privs(name, {privs = true}) then
return false, "You are already a superuser."
end
-- Check rate limit.
local now = os_time()
if retry[name] and now < (retry[name] + 5) then
return false, "Wait a few seconds before trying again."
end
retry[name] = now
-- Check password.
local hash = minetest.setting_get(modname .. "_password_hash")
local salt = minetest.setting_get(modname .. "_password_salt")
if not pass or pass == ""
or not hash or hash == ""
or not salt or salt == ""
or minetest.get_password_hash(salt, pass) ~= hash then
return false, "Authentication failure."
return false, "Authentication failure."
end
return changeprivs(name, true)
end
return changeprivs(name, true)
end
})
})
-- A shortcut to exit "su mode"; this is really just a shortcut for
-- "/revoke <me> privs", which escalated users will be able to do.
minetest.register_chatcommand("unsu", {
description = "Shortcut to de-escalate privileges from su.",
privs = {privs = true},
func = function(name) return changeprivs(name) end
})
description = "Shortcut to de-escalate privileges from su.",
privs = {privs = true},
func = function(name) return changeprivs(name) end
})
------------------------------------------------------------------------
-- STRICT MODE ENFORCEMENT

16
szutil_usagesurvey/README Normal file
View File

@ -0,0 +1,16 @@
------------------------------------------------------------------------
This mod maintains a summary of player interactions with the map on a
per-mapblock, per-player basis, and records totals to a file-based
database in a subdir of the world.
Interactions include time spent standing/moving, and counts of actions
such as nodes dug/placed, deaths/respawns, joins/leaves, hp hurt/healed,
etc.
The purpose of this data collection is to allow a server admin to
identify valuable areas of a world map, i.e. areas that players have the
most effort invested in, e.g. for pruning databases, prioritizing
backups, showcasing builds, etc.
------------------------------------------------------------------------

278
szutil_usagesurvey/init.lua Normal file
View File

@ -0,0 +1,278 @@
-- LUALOCALS < ---------------------------------------------------------
local io, ipairs, math, minetest, pairs, table, type
= io, ipairs, math, minetest, pairs, table, type
local io_open, math_floor, table_concat, table_sort
= io.open, math.floor, table.concat, table.sort
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
------------------------------------------------------------------------
-- IN-MEMORY DATABASE AND UTILITY
local db = { }
local function getsub(tbl, id)
local x = tbl[id]
if x then return x end
x = { }
tbl[id] = x
return x
end
local function statadd(blockid, playername, stat, value)
local t = getsub(db, blockid)
t = getsub(t, playername)
t[stat] = (t[stat] or 0) + value
end
local function getpn(whom)
if not whom then return end
local pn = whom.get_player_name
if not pn then return end
pn = pn(whom)
if not pn or not pn:find("%S") then return end
return pn
end
local function blockid(pos)
return math_floor((pos.x + 0.5) / 16)
+ 4096 * math_floor((pos.y + 0.5) / 16)
+ 16777216 * math_floor((pos.z + 0.5) / 16)
end
------------------------------------------------------------------------
-- PLAYER ACTIVITY EVENT HOOKS
local function reghook(func, stat, pwhom, ppos)
return func(function(...)
local t = {...}
local whom = t[pwhom]
local pn = getpn(whom)
if not pn then return end
local pos = ppos and t[ppos] or whom:getpos()
local id = blockid(pos)
return statadd(id, pn, stat, 1)
end)
end
reghook(minetest.register_on_dignode, "dig", 3, 1)
reghook(minetest.register_on_placenode, "place", 3, 1)
reghook(minetest.register_on_dieplayer, "die", 1)
reghook(minetest.register_on_respawnplayer, "spawn", 1)
reghook(minetest.register_on_joinplayer, "join", 1)
reghook(minetest.register_on_leaveplayer, "leave", 1)
reghook(minetest.register_on_craft, "craft", 2)
minetest.register_on_player_hpchange(function(whom, change)
local pn = getpn(whom)
if not pn then return end
local id = blockid(whom:getpos())
if change < 0 then
return statadd(id, pn, "hurt", -change)
else
return statadd(id, pn, "heal", change)
end
end)
------------------------------------------------------------------------
-- PLAYER MOVEMENT/IDLE HOOKS
local playdb = { }
local idlemin = 5
local function procstep(dt, player)
local pn = getpn(player)
if not pn then return end
local pd = getsub(playdb, pn)
local pos = player:getpos()
local dir = player:get_look_dir()
local cur = { pos.x, pos.y, pos.z, dir.x, dir.y, dir.z }
local moved
if pd.last then
for i = 1, 6 do
moved = moved or pd.last[i] ~= cur[i]
end
end
pd.last = cur
local id = blockid(pos)
local t = pd.t or 0
if moved then
pd.t = 0
if t >= idlemin then
statadd(id, pn, "idle", t)
return statadd(id, pn, "move", dt)
else
return statadd(id, pn, "move", t + dt)
end
else
if t >= idlemin then
return statadd(id, pn, "idle", dt)
else
pd.t = t + dt
if (t + dt) >= idlemin then
return statadd(id, pn, "idle", t + dt)
end
end
end
end
minetest.register_globalstep(function(dt)
for _, player in pairs(minetest.get_connected_players()) do
procstep(dt, player)
end
end)
------------------------------------------------------------------------
-- DATABASE FLUSH CYCLE
local function deepadd(t, u)
for k, v in pairs(u) do
if type(v) == "table" then
t[k] = deepadd(t[k] or { }, v)
else
t[k] = (t[k] or 0) + v
end
end
return t
end
local function dbpath(id)
local p = minetest.get_worldpath() .. "/" .. modname
if id then
id = "" .. id
if id:sub(1, 3) ~= "blk" then
id = "blk" .. id .. ".txt"
end
p = p .. "/" .. id
end
return p
end
local function dbload(id)
local f = io_open(dbpath(id))
if not f then return { } end
local u = minetest.deserialize(f:read("*all"))
f:close()
return u
end
local lasttime = minetest.get_us_time() / 1000000
local savedqty = 0
local alltime = 0
local runtime = 0
local function dbflush(forcerpt)
local now = minetest.get_us_time() / 1000000
alltime = alltime + now - lasttime
lasttime = now
minetest.mkdir(dbpath())
for id, t in pairs(db) do
t = deepadd(dbload(id), t)
minetest.safe_file_write(dbpath(id), minetest.serialize(t))
savedqty = savedqty + 1
end
db = { }
now = minetest.get_us_time() / 1000000
runtime = runtime + now - lasttime
if not forcerpt and ((runtime < 1 and alltime < 3600 and savedqty < 100)
or savedqty < 1) then return end
local function ms(i) return math_floor(i *1000000) / 1000 end
minetest.log(modname .. ": recorded " .. savedqty .. " block(s) using "
.. ms(runtime) .. "ms out of " .. ms(alltime) .. "ms ("
.. (math_floor(runtime / alltime * 10000) / 100)
.. "%)")
runtime = 0
alltime = 0
savedqty = 0
end
local function flushcycle()
dbflush()
return minetest.after(60, flushcycle)
end
flushcycle()
minetest.register_on_shutdown(function()
for _, player in pairs(minetest.get_connected_players()) do
local pn = getpn(player)
if pn then
local id = blockid(player:getpos())
statadd(id, pn, "shutdown", 1)
end
end
dbflush(true)
end)
------------------------------------------------------------------------
-- CHAT COMMAND
local function fmtrpt(t, id)
local p = { }
for k, v in pairs(t) do
local n = 0
for k2, v2 in pairs(v) do
n = n + v2
end
p[#p + 1] = k
p[k] = n
end
table_sort(p, function(a, b)
if p[a] == p[b] then return a < b end
return p[a] > p[b]
end)
local r = id and { "block", id } or { "world" }
for _, k in ipairs(p) do
r[#r + 1] = "[" .. k .. "]"
local v = t[k]
local s = { }
for k2, v2 in pairs(v) do
s[#s + 1] = k2
end
table_sort(s, function(a, b)
if v[a] == v[b] then return a < b end
return v[a] > v[b]
end)
for _, k2 in ipairs(s) do
r[#r + 1] = k2
r[#r + 1] = math_floor(v[k2])
end
end
return table_concat(r, " ")
end
minetest.register_chatcommand("blockuse", {
privs = {server = true},
description = "Statistics about usage within the current mapblock.",
func = function(name)
local player = minetest.get_player_by_name(name)
if not player then return end
local id = blockid(player:getpos())
local t = deepadd(deepadd({ }, getsub(db, id)), dbload(id))
minetest.chat_send_player(name, fmtrpt(t, id))
end
})
minetest.register_chatcommand("worlduse", {
privs = {server = true},
description = "Statistics about usage across the entire world.",
func = function(name)
local t = { }
for k, v in pairs(db) do
t = deepadd(t, v)
end
for i, v in ipairs(minetest.get_dir_list(dbpath(), false)) do
t = deepadd(t, dbload(v))
end
minetest.chat_send_player(name, fmtrpt(t))
end
})
------------------------------------------------------------------------