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:
parent
4eb745efa7
commit
6c238b26a1
4
.lualocals
Normal file
4
.lualocals
Normal file
@ -0,0 +1,4 @@
|
||||
~print
|
||||
minetest
|
||||
ItemStack
|
||||
VoxelArea
|
21
TODO
21
TODO
@ -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
177
lualocals.pl
Executable 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
2
modpack.conf
Normal file
@ -0,0 +1,2 @@
|
||||
name = szutilpack
|
||||
description = Sz Utility Pack
|
@ -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)
|
@ -1 +0,0 @@
|
||||
default
|
@ -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.
|
@ -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
|
||||
})
|
@ -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"
|
||||
)
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
20
szutil_chatsocket/README
Normal 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
104
szutil_chatsocket/init.lua
Normal 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
21
szutil_consocket/README
Normal 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
176
szutil_consocket/init.lua
Normal 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)
|
@ -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
172
szutil_fixhack/init.lua
Normal 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)
|
@ -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.
|
||||
|
||||
------------------------------------------------------------------------
|
@ -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)
|
@ -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.
|
||||
|
||||
------------------------------------------------------------------------
|
@ -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
50
szutil_logtrace/init.lua
Normal 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
14
szutil_motd/README
Normal 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).
|
||||
|
||||
------------------------------------------------------------------------
|
@ -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
|
@ -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.
|
||||
|
||||
------------------------------------------------------------------------
|
@ -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
16
szutil_usagesurvey/README
Normal 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
278
szutil_usagesurvey/init.lua
Normal 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
|
||||
})
|
||||
|
||||
------------------------------------------------------------------------
|
Loading…
x
Reference in New Issue
Block a user