Compare commits
5 Commits
213d3562bd
...
7ffc0268df
Author | SHA1 | Date |
---|---|---|
paradust7 | 7ffc0268df | |
Lars Müller | e7d4ec6834 | |
Lars Müller | f4a53f7ee6 | |
Lars Müller | 3eafcab64e | |
Jude Melton-Houghton | ba65e0ace7 |
|
@ -19,34 +19,43 @@ jobs:
|
|||
name: "Compile and run multiplayer tests"
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install deps
|
||||
run: |
|
||||
source ./util/ci/common.sh
|
||||
install_linux_deps clang-10 gdb libluajit-5.1-dev
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install deps
|
||||
run: |
|
||||
source ./util/ci/common.sh
|
||||
install_linux_deps clang-10 gdb libluajit-5.1-dev
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
./util/ci/build.sh
|
||||
env:
|
||||
CC: clang-10
|
||||
CXX: clang++-10
|
||||
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0"
|
||||
- name: Build
|
||||
run: |
|
||||
./util/ci/build.sh
|
||||
env:
|
||||
CC: clang-10
|
||||
CXX: clang++-10
|
||||
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0"
|
||||
|
||||
- name: Integration test + devtest
|
||||
run: |
|
||||
./util/test_multiplayer.sh
|
||||
- name: Integration test + devtest
|
||||
run: |
|
||||
./util/test_multiplayer.sh
|
||||
|
||||
luacheck:
|
||||
name: "Builtin Luacheck and Unit Tests"
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: leafo/gh-actions-lua@v9
|
||||
with:
|
||||
luaVersion: "5.1.5"
|
||||
- uses: leafo/gh-actions-luarocks@v4
|
||||
|
||||
- name: Install LuaJIT
|
||||
run: |
|
||||
cd $HOME
|
||||
git clone https://github.com/LuaJIT/LuaJIT/
|
||||
cd LuaJIT
|
||||
make -j$(nproc)
|
||||
|
||||
- name: Install luarocks tools
|
||||
run: |
|
||||
luarocks install --local luacheck
|
||||
|
@ -56,6 +65,7 @@ jobs:
|
|||
run: |
|
||||
$HOME/.luarocks/bin/luacheck builtin
|
||||
$HOME/.luarocks/bin/busted builtin
|
||||
$HOME/.luarocks/bin/busted builtin --lua=$HOME/LuaJIT/src/luajit
|
||||
|
||||
- name: Run checks (devtest)
|
||||
run: |
|
||||
|
|
|
@ -1,205 +1,214 @@
|
|||
--- Lua module to serialize values as Lua code.
|
||||
-- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
|
||||
-- From: https://github.com/appgurueu/modlib/blob/master/luon.lua
|
||||
-- License: MIT
|
||||
-- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
|
||||
-- @author Fabien Fleutot <metalua@gmail.com>
|
||||
-- @author ShadowNinja <shadowninja@minetest.net>
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- Serialize an object into a source code string. This string, when passed as
|
||||
-- an argument to deserialize(), returns an object structurally identical to
|
||||
-- the original one. The following are currently supported:
|
||||
-- * Booleans, numbers, strings, and nil.
|
||||
-- * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
|
||||
-- * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
|
||||
-- This works in two phases:
|
||||
-- 1. Recursively find and record multiple references and recursion.
|
||||
-- 2. Recursively dump the value into a string.
|
||||
-- @param x Value to serialize (nil is allowed).
|
||||
-- @return load()able string containing the value.
|
||||
function core.serialize(x)
|
||||
local local_index = 1 -- Top index of the "_" local table in the dump
|
||||
-- table->nil/1/2 set of tables seen.
|
||||
-- nil = not seen, 1 = seen once, 2 = seen multiple times.
|
||||
local seen = {}
|
||||
local next, rawget, pairs, pcall, error, type, setfenv, loadstring
|
||||
= next, rawget, pairs, pcall, error, type, setfenv, loadstring
|
||||
|
||||
-- nest_points are places where a table appears within itself, directly
|
||||
-- or not. For instance, all of these chunks create nest points in
|
||||
-- table x: "x = {}; x[x] = 1", "x = {}; x[1] = x",
|
||||
-- "x = {}; x[1] = {y = {x}}".
|
||||
-- To handle those, two tables are used by mark_nest_point:
|
||||
-- * nested - Transient set of tables being currently traversed.
|
||||
-- Used for detecting nested tables.
|
||||
-- * nest_points - parent->{key=value, ...} table cantaining the nested
|
||||
-- keys and values in the parent. They're all dumped after all the
|
||||
-- other table operations have been performed.
|
||||
--
|
||||
-- mark_nest_point(p, k, v) fills nest_points with information required
|
||||
-- to remember that key/value (k, v) creates a nest point in table
|
||||
-- parent. It also marks "parent" and the nested item(s) as occuring
|
||||
-- multiple times, since several references to it will be required in
|
||||
-- order to patch the nest points.
|
||||
local nest_points = {}
|
||||
local nested = {}
|
||||
local function mark_nest_point(parent, k, v)
|
||||
local nk, nv = nested[k], nested[v]
|
||||
local np = nest_points[parent]
|
||||
if not np then
|
||||
np = {}
|
||||
nest_points[parent] = np
|
||||
end
|
||||
np[k] = v
|
||||
seen[parent] = 2
|
||||
if nk then seen[k] = 2 end
|
||||
if nv then seen[v] = 2 end
|
||||
local table_concat, string_dump, string_format, string_match, math_huge
|
||||
= table.concat, string.dump, string.format, string.match, math.huge
|
||||
|
||||
-- Recursively counts occurences of objects (non-primitives including strings) in a table.
|
||||
local function count_objects(value)
|
||||
local counts = {}
|
||||
if value == nil then
|
||||
-- Early return for nil; tables can't contain nil
|
||||
return counts
|
||||
end
|
||||
|
||||
-- First phase, list the tables and functions which appear more than
|
||||
-- once in x.
|
||||
local function mark_multiple_occurences(x)
|
||||
local tp = type(x)
|
||||
if tp ~= "table" and tp ~= "function" then
|
||||
-- No identity (comparison is done by value, not by instance)
|
||||
local function count_values(val)
|
||||
local type_ = type(val)
|
||||
if type_ == "boolean" or type_ == "number" then
|
||||
return
|
||||
end
|
||||
if seen[x] == 1 then
|
||||
seen[x] = 2
|
||||
elseif seen[x] ~= 2 then
|
||||
seen[x] = 1
|
||||
end
|
||||
|
||||
if tp == "table" then
|
||||
nested[x] = true
|
||||
for k, v in pairs(x) do
|
||||
if nested[k] or nested[v] then
|
||||
mark_nest_point(x, k, v)
|
||||
else
|
||||
mark_multiple_occurences(k)
|
||||
mark_multiple_occurences(v)
|
||||
local count = counts[val]
|
||||
counts[val] = (count or 0) + 1
|
||||
if type_ == "table" then
|
||||
if not count then
|
||||
for k, v in pairs(val) do
|
||||
count_values(k)
|
||||
count_values(v)
|
||||
end
|
||||
end
|
||||
nested[x] = nil
|
||||
elseif type_ ~= "string" and type_ ~= "function" then
|
||||
error("unsupported type: " .. type_)
|
||||
end
|
||||
end
|
||||
|
||||
local dumped = {} -- object->varname set
|
||||
local local_defs = {} -- Dumped local definitions as source code lines
|
||||
|
||||
-- Mutually recursive local functions:
|
||||
local dump_val, dump_or_ref_val
|
||||
|
||||
-- If x occurs multiple times, dump the local variable rather than
|
||||
-- the value. If it's the first time it's dumped, also dump the
|
||||
-- content in local_defs.
|
||||
function dump_or_ref_val(x)
|
||||
if seen[x] ~= 2 then
|
||||
return dump_val(x)
|
||||
end
|
||||
local var = dumped[x]
|
||||
if var then -- Already referenced
|
||||
return var
|
||||
end
|
||||
-- First occurence, create and register reference
|
||||
local val = dump_val(x)
|
||||
local i = local_index
|
||||
local_index = local_index + 1
|
||||
var = "_["..i.."]"
|
||||
local_defs[#local_defs + 1] = var.." = "..val
|
||||
dumped[x] = var
|
||||
return var
|
||||
end
|
||||
|
||||
-- Second phase. Dump the object; subparts occuring multiple times
|
||||
-- are dumped in local variables which can be referenced multiple
|
||||
-- times. Care is taken to dump local vars in a sensible order.
|
||||
function dump_val(x)
|
||||
local tp = type(x)
|
||||
if x == nil then return "nil"
|
||||
elseif tp == "string" then return string.format("%q", x)
|
||||
elseif tp == "boolean" then return x and "true" or "false"
|
||||
elseif tp == "function" then
|
||||
return string.format("loadstring(%q)", string.dump(x))
|
||||
elseif tp == "number" then
|
||||
-- Serialize numbers reversibly with string.format
|
||||
return string.format("%.17g", x)
|
||||
elseif tp == "table" then
|
||||
local vals = {}
|
||||
local idx_dumped = {}
|
||||
local np = nest_points[x]
|
||||
for i, v in ipairs(x) do
|
||||
if not np or not np[i] then
|
||||
vals[#vals + 1] = dump_or_ref_val(v)
|
||||
end
|
||||
idx_dumped[i] = true
|
||||
end
|
||||
for k, v in pairs(x) do
|
||||
if (not np or not np[k]) and
|
||||
not idx_dumped[k] then
|
||||
vals[#vals + 1] = "["..dump_or_ref_val(k).."] = "
|
||||
..dump_or_ref_val(v)
|
||||
end
|
||||
end
|
||||
return "{"..table.concat(vals, ", ").."}"
|
||||
else
|
||||
error("Can't serialize data of type "..tp)
|
||||
end
|
||||
end
|
||||
|
||||
local function dump_nest_points()
|
||||
for parent, vals in pairs(nest_points) do
|
||||
for k, v in pairs(vals) do
|
||||
local_defs[#local_defs + 1] = dump_or_ref_val(parent)
|
||||
.."["..dump_or_ref_val(k).."] = "
|
||||
..dump_or_ref_val(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mark_multiple_occurences(x)
|
||||
local top_level = dump_or_ref_val(x)
|
||||
dump_nest_points()
|
||||
|
||||
if next(local_defs) then
|
||||
return "local _ = {}\n"
|
||||
..table.concat(local_defs, "\n")
|
||||
.."\nreturn "..top_level
|
||||
else
|
||||
return "return "..top_level
|
||||
end
|
||||
count_values(value)
|
||||
return counts
|
||||
end
|
||||
|
||||
-- Deserialization
|
||||
-- Build a "set" of Lua keywords. These can't be used as short key names.
|
||||
-- See https://www.lua.org/manual/5.1/manual.html#2.1
|
||||
local keywords = {}
|
||||
for _, keyword in pairs({
|
||||
"and", "break", "do", "else", "elseif",
|
||||
"end", "false", "for", "function", "if",
|
||||
"in", "local", "nil", "not", "or",
|
||||
"repeat", "return", "then", "true", "until", "while",
|
||||
"goto" -- LuaJIT, Lua 5.2+
|
||||
}) do
|
||||
keywords[keyword] = true
|
||||
end
|
||||
|
||||
local function safe_loadstring(...)
|
||||
local func, err = loadstring(...)
|
||||
if func then
|
||||
setfenv(func, {})
|
||||
return func
|
||||
local function quote(string)
|
||||
return string_format("%q", string)
|
||||
end
|
||||
|
||||
local function dump_func(func)
|
||||
return string_format("loadstring(%q)", string_dump(func))
|
||||
end
|
||||
|
||||
-- Serializes Lua nil, booleans, numbers, strings, tables and even functions
|
||||
-- Tables are referenced by reference, strings are referenced by value. Supports circular tables.
|
||||
local function serialize(value, write)
|
||||
local reference, refnum = "r1", 1
|
||||
-- [object] = reference string
|
||||
local references = {}
|
||||
-- Circular tables that must be filled using `table[key] = value` statements
|
||||
local to_fill = {}
|
||||
for object, count in pairs(count_objects(value)) do
|
||||
local type_ = type(object)
|
||||
-- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
|
||||
if count >= 2 and (type_ ~= "string" or #reference + 2 < #object) then
|
||||
write(reference)
|
||||
write("=")
|
||||
if type_ == "table" then
|
||||
write("{}")
|
||||
elseif type_ == "function" then
|
||||
write(dump_func(object))
|
||||
elseif type_ == "string" then
|
||||
write(quote(object))
|
||||
end
|
||||
write(";")
|
||||
references[object] = reference
|
||||
if type_ == "table" then
|
||||
to_fill[object] = reference
|
||||
end
|
||||
refnum = refnum + 1
|
||||
reference = ("r%X"):format(refnum)
|
||||
end
|
||||
end
|
||||
return nil, err
|
||||
-- Used to decide whether we should do "key=..."
|
||||
local function use_short_key(key)
|
||||
return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$")
|
||||
end
|
||||
local function dump(value)
|
||||
-- Primitive types
|
||||
if value == nil then
|
||||
return write("nil")
|
||||
end
|
||||
if value == true then
|
||||
return write("true")
|
||||
end
|
||||
if value == false then
|
||||
return write("false")
|
||||
end
|
||||
local type_ = type(value)
|
||||
if type_ == "number" then
|
||||
return write(string_format("%.17g", value))
|
||||
end
|
||||
-- Reference types: table, function and string
|
||||
local ref = references[value]
|
||||
if ref then
|
||||
return write(ref)
|
||||
end
|
||||
if type_ == "string" then
|
||||
return write(quote(value))
|
||||
end
|
||||
if type_ == "function" then
|
||||
return write(dump_func(value))
|
||||
end
|
||||
if type_ == "table" then
|
||||
write("{")
|
||||
-- First write list keys:
|
||||
-- Don't use the table length #value here as it may horribly fail
|
||||
-- for tables which use large integers as keys in the hash part;
|
||||
-- stop at the first "hole" (nil value) instead
|
||||
local len = 0
|
||||
local first = true -- whether this is the first entry, which may not have a leading comma
|
||||
while true do
|
||||
local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
|
||||
if v == nil then break end
|
||||
if first then first = false else write(",") end
|
||||
dump(v)
|
||||
len = len + 1
|
||||
end
|
||||
-- Now write map keys ([key] = value)
|
||||
for k, v in next, value do
|
||||
-- We have written all non-float keys in [1, len] already
|
||||
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
|
||||
if first then first = false else write(",") end
|
||||
if use_short_key(k) then
|
||||
write(k)
|
||||
else
|
||||
write("[")
|
||||
dump(k)
|
||||
write("]")
|
||||
end
|
||||
write("=")
|
||||
dump(v)
|
||||
end
|
||||
end
|
||||
write("}")
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Write the statements to fill circular tables
|
||||
for table, ref in pairs(to_fill) do
|
||||
for k, v in pairs(table) do
|
||||
write(ref)
|
||||
if use_short_key(k) then
|
||||
write(".")
|
||||
write(k)
|
||||
else
|
||||
write("[")
|
||||
dump(k)
|
||||
write("]")
|
||||
end
|
||||
write("=")
|
||||
dump(v)
|
||||
write(";")
|
||||
end
|
||||
end
|
||||
write("return ")
|
||||
dump(value)
|
||||
end
|
||||
|
||||
function core.serialize(value)
|
||||
local rope = {}
|
||||
serialize(value, function(text)
|
||||
-- Faster than table.insert(rope, text) on PUC Lua 5.1
|
||||
rope[#rope + 1] = text
|
||||
end)
|
||||
return table_concat(rope)
|
||||
end
|
||||
|
||||
local function dummy_func() end
|
||||
|
||||
local nan = (0/0)^1 -- +nan
|
||||
|
||||
function core.deserialize(str, safe)
|
||||
if type(str) ~= "string" then
|
||||
return nil, "Cannot deserialize type '"..type(str)
|
||||
.."'. Argument must be a string."
|
||||
end
|
||||
if str:byte(1) == 0x1B then
|
||||
return nil, "Bytecode prohibited"
|
||||
end
|
||||
local f, err = loadstring(str)
|
||||
if not f then return nil, err end
|
||||
local func, err = loadstring(str)
|
||||
if not func then return nil, err end
|
||||
|
||||
-- The environment is recreated every time so deseralized code cannot
|
||||
-- pollute it with permanent references.
|
||||
setfenv(f, {loadstring = safe and dummy_func or safe_loadstring})
|
||||
|
||||
local good, data = pcall(f)
|
||||
if good then
|
||||
return data
|
||||
-- math.huge is serialized to inf, NaNs are serialized to nan by Lua
|
||||
local env = {inf = math_huge, nan = nan}
|
||||
if safe then
|
||||
env.loadstring = dummy_func
|
||||
else
|
||||
return nil, data
|
||||
env.loadstring = function(str, ...)
|
||||
local func, err = loadstring(str, ...)
|
||||
if func then
|
||||
setfenv(func, env)
|
||||
return func
|
||||
end
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
setfenv(func, env)
|
||||
local success, value_or_err = pcall(func)
|
||||
if success then
|
||||
return value_or_err
|
||||
end
|
||||
return nil, value_or_err
|
||||
end
|
||||
|
|
|
@ -6,38 +6,92 @@ _G.setfenv = require 'busted.compatibility'.setfenv
|
|||
dofile("builtin/common/serialize.lua")
|
||||
dofile("builtin/common/vector.lua")
|
||||
|
||||
describe("serialize", function()
|
||||
it("works", function()
|
||||
local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
-- Supports circular tables; does not support table keys
|
||||
-- Correctly checks whether a mapping of references ("same") exists
|
||||
-- Is significantly more efficient than assert.same
|
||||
local function assert_same(a, b, same)
|
||||
same = same or {}
|
||||
if same[a] or same[b] then
|
||||
assert(same[a] == b and same[b] == a)
|
||||
return
|
||||
end
|
||||
if a == b then
|
||||
return
|
||||
end
|
||||
if type(a) ~= "table" or type(b) ~= "table" then
|
||||
assert(a == b)
|
||||
return
|
||||
end
|
||||
same[a] = b
|
||||
same[b] = a
|
||||
local count = 0
|
||||
for k, v in pairs(a) do
|
||||
count = count + 1
|
||||
assert(type(k) ~= "table")
|
||||
assert_same(v, b[k], same)
|
||||
end
|
||||
for _ in pairs(b) do
|
||||
count = count - 1
|
||||
end
|
||||
assert(count == 0)
|
||||
end
|
||||
|
||||
assert.same(test_in, test_out)
|
||||
local x, y = {}, {}
|
||||
local t1, t2 = {x, x, y, y}, {x, y, x, y}
|
||||
assert.same(t1, t2) -- will succeed because it only checks whether the depths match
|
||||
assert(not pcall(assert_same, t1, t2)) -- will correctly fail because it checks whether the refs match
|
||||
|
||||
describe("serialize", function()
|
||||
local function assert_preserves(value)
|
||||
local preserved_value = core.deserialize(core.serialize(value))
|
||||
assert_same(value, preserved_value)
|
||||
end
|
||||
it("works", function()
|
||||
assert_preserves({cat={sound="nyan", speed=400}, dog={sound="woof"}})
|
||||
end)
|
||||
|
||||
it("handles characters", function()
|
||||
local test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
assert_preserves({escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"})
|
||||
end)
|
||||
|
||||
it("handles NaN & infinities", function()
|
||||
local nan = core.deserialize(core.serialize(0/0))
|
||||
assert(nan ~= nan)
|
||||
assert_preserves(math.huge)
|
||||
assert_preserves(-math.huge)
|
||||
end)
|
||||
|
||||
it("handles precise numbers", function()
|
||||
local test_in = 0.2695949158945771
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
assert_preserves(0.2695949158945771)
|
||||
end)
|
||||
|
||||
it("handles big integers", function()
|
||||
local test_in = 269594915894577
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
assert_preserves(269594915894577)
|
||||
end)
|
||||
|
||||
it("handles recursive structures", function()
|
||||
local test_in = { hello = "world" }
|
||||
test_in.foo = test_in
|
||||
assert_preserves(test_in)
|
||||
end)
|
||||
|
||||
local test_out = core.deserialize(core.serialize(test_in))
|
||||
assert.same(test_in, test_out)
|
||||
it("handles cross-referencing structures", function()
|
||||
local test_in = {
|
||||
foo = {
|
||||
baz = {
|
||||
{}
|
||||
},
|
||||
},
|
||||
bar = {
|
||||
baz = {},
|
||||
},
|
||||
}
|
||||
|
||||
test_in.foo.baz[1].foo = test_in.foo
|
||||
test_in.foo.baz[1].bar = test_in.bar
|
||||
test_in.bar.baz[1] = test_in.foo.baz[1]
|
||||
|
||||
assert_preserves(test_in)
|
||||
end)
|
||||
|
||||
it("strips functions in safe mode", function()
|
||||
|
@ -47,6 +101,7 @@ describe("serialize", function()
|
|||
end,
|
||||
foo = "bar"
|
||||
}
|
||||
setfenv(test_in.func, _G)
|
||||
|
||||
local str = core.serialize(test_in)
|
||||
assert.not_nil(str:find("loadstring"))
|
||||
|
@ -58,13 +113,75 @@ describe("serialize", function()
|
|||
|
||||
it("vectors work", function()
|
||||
local v = vector.new(1, 2, 3)
|
||||
assert.same({{x = 1, y = 2, z = 3}}, core.deserialize(core.serialize({v})))
|
||||
assert.same({x = 1, y = 2, z = 3}, core.deserialize(core.serialize(v)))
|
||||
assert_preserves({v})
|
||||
assert_preserves(v)
|
||||
|
||||
-- abuse
|
||||
v = vector.new(1, 2, 3)
|
||||
v.a = "bla"
|
||||
assert.same({x = 1, y = 2, z = 3, a = "bla"},
|
||||
core.deserialize(core.serialize(v)))
|
||||
assert_preserves(v)
|
||||
end)
|
||||
|
||||
it("handles keywords as keys", function()
|
||||
assert_preserves({["and"] = "keyword", ["for"] = "keyword"})
|
||||
end)
|
||||
|
||||
describe("fuzzing", function()
|
||||
local atomics = {true, false, math.huge, -math.huge} -- no NaN or nil
|
||||
local function atomic()
|
||||
return atomics[math.random(1, #atomics)]
|
||||
end
|
||||
local function num()
|
||||
local sign = math.random() < 0.5 and -1 or 1
|
||||
local val = math.random(0, 2^52)
|
||||
local exp = math.random() < 0.5 and 1 or 2^(math.random(-120, 120))
|
||||
return sign * val * exp
|
||||
end
|
||||
local function charcodes(count)
|
||||
if count == 0 then return end
|
||||
return math.random(0, 0xFF), charcodes(count - 1)
|
||||
end
|
||||
local function str()
|
||||
return string.char(charcodes(math.random(0, 100)))
|
||||
end
|
||||
local primitives = {atomic, num, str}
|
||||
local function primitive()
|
||||
return primitives[math.random(1, #primitives)]()
|
||||
end
|
||||
local function tab(max_actions)
|
||||
local root = {}
|
||||
local tables = {root}
|
||||
local function random_table()
|
||||
return tables[#tables == 1 and 1 or math.random(1, #tables)] -- luacheck: ignore
|
||||
end
|
||||
for _ = 1, math.random(1, max_actions) do
|
||||
local tab = random_table()
|
||||
local value
|
||||
if math.random() < 0.5 then
|
||||
if math.random() < 0.5 then
|
||||
value = random_table()
|
||||
else
|
||||
value = {}
|
||||
table.insert(tables, value)
|
||||
end
|
||||
else
|
||||
value = primitive()
|
||||
end
|
||||
tab[math.random() < 0.5 and (#tab + 1) or primitive()] = value
|
||||
end
|
||||
return root
|
||||
end
|
||||
it("primitives work", function()
|
||||
for _ = 1, 1e3 do
|
||||
assert_preserves(primitive())
|
||||
end
|
||||
end)
|
||||
it("tables work", function()
|
||||
for _ = 1, 100 do
|
||||
local fuzzed_table = tab(1e3)
|
||||
assert_same(fuzzed_table, table.copy(fuzzed_table))
|
||||
assert_preserves(fuzzed_table)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
@ -4551,8 +4551,14 @@ Callbacks:
|
|||
* Called when the object is instantiated.
|
||||
* `dtime_s` is the time passed since the object was unloaded, which can be
|
||||
used for updating the entity state.
|
||||
* `on_deactivate(self)
|
||||
* `on_deactivate(self, removal)
|
||||
* Called when the object is about to get removed or unloaded.
|
||||
* `removal`: boolean indicating whether the object is about to get removed.
|
||||
Calling `object:remove()` on an active object will call this with `removal=true`.
|
||||
The mapblock the entity resides in being unloaded will call this with `removal=false`.
|
||||
* Note that this won't be called if the object hasn't been activated in the first place.
|
||||
In particular, `minetest.clear_objects({mode = "full"})` won't call this,
|
||||
whereas `minetest.clear_objects({mode = "quick"})` might call this.
|
||||
* `on_step(self, dtime)`
|
||||
* Called on every server tick, after movement and collision processing.
|
||||
`dtime` is usually 0.1 seconds, as per the `dedicated_server_step` setting
|
||||
|
@ -7632,7 +7638,7 @@ Used by `minetest.register_lbm`.
|
|||
|
||||
A loading block modifier (LBM) is used to define a function that is called for
|
||||
specific nodes (defined by `nodenames`) when a mapblock which contains such nodes
|
||||
gets loaded.
|
||||
gets activated (not loaded!)
|
||||
|
||||
{
|
||||
label = "Upgrade legacy doors",
|
||||
|
@ -7647,8 +7653,8 @@ gets loaded.
|
|||
-- Groups (as of group:groupname) will work as well.
|
||||
|
||||
run_at_every_load = false,
|
||||
-- Whether to run the LBM's action every time a block gets loaded,
|
||||
-- and not only the first time the block gets loaded after the LBM
|
||||
-- Whether to run the LBM's action every time a block gets activated,
|
||||
-- and not only the first time the block gets activated after the LBM
|
||||
-- was introduced.
|
||||
|
||||
action = function(pos, node),
|
||||
|
|
|
@ -31,8 +31,8 @@ minetest.register_entity("testentities:callback", {
|
|||
on_activate = function(self, staticdata, dtime_s)
|
||||
message("Callback entity: on_activate! pos="..spos(self).."; dtime_s="..dtime_s)
|
||||
end,
|
||||
on_deactivate = function(self)
|
||||
message("Callback entity: on_deactivate! pos="..spos(self))
|
||||
on_deactivate = function(self, removal)
|
||||
message("Callback entity: on_deactivate! pos="..spos(self) .. "; removal=" .. tostring(removal))
|
||||
end,
|
||||
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
|
||||
local name = get_object_name(puncher)
|
||||
|
|
|
@ -493,6 +493,7 @@ void Client::step(float dtime)
|
|||
ClientEvent *event = new ClientEvent();
|
||||
event->type = CE_PLAYER_DAMAGE;
|
||||
event->player_damage.amount = damage;
|
||||
event->player_damage.effect = true;
|
||||
m_client_event_queue.push(event);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ struct ClientEvent
|
|||
struct
|
||||
{
|
||||
u16 amount;
|
||||
bool effect;
|
||||
} player_damage;
|
||||
struct
|
||||
{
|
||||
|
|
|
@ -2605,6 +2605,9 @@ void Game::handleClientEvent_PlayerDamage(ClientEvent *event, CameraOrientation
|
|||
if (client->modsLoaded())
|
||||
client->getScript()->on_damage_taken(event->player_damage.amount);
|
||||
|
||||
if (!event->player_damage.effect)
|
||||
return;
|
||||
|
||||
// Damage flash and hurt tilt are not used at death
|
||||
if (client->getHP() > 0) {
|
||||
LocalPlayer *player = client->getEnv().getLocalPlayer();
|
||||
|
|
|
@ -570,6 +570,10 @@ void Client::handleCommand_HP(NetworkPacket *pkt)
|
|||
|
||||
u16 hp;
|
||||
*pkt >> hp;
|
||||
bool damage_effect = true;
|
||||
try {
|
||||
*pkt >> damage_effect;
|
||||
} catch (PacketError &e) {};
|
||||
|
||||
player->hp = hp;
|
||||
|
||||
|
@ -581,6 +585,7 @@ void Client::handleCommand_HP(NetworkPacket *pkt)
|
|||
ClientEvent *event = new ClientEvent();
|
||||
event->type = CE_PLAYER_DAMAGE;
|
||||
event->player_damage.amount = oldhp - hp;
|
||||
event->player_damage.effect = damage_effect;
|
||||
m_client_event_queue.push(event);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,15 +38,6 @@
|
|||
// Unsigned magic seed prevents undefined behavior.
|
||||
#define NOISE_MAGIC_SEED 1013U
|
||||
|
||||
typedef float (*Interp2dFxn)(
|
||||
float v00, float v10, float v01, float v11,
|
||||
float x, float y);
|
||||
|
||||
typedef float (*Interp3dFxn)(
|
||||
float v000, float v100, float v010, float v110,
|
||||
float v001, float v101, float v011, float v111,
|
||||
float x, float y, float z);
|
||||
|
||||
FlagDesc flagdesc_noiseparams[] = {
|
||||
{"defaults", NOISE_FLAG_DEFAULTS},
|
||||
{"eased", NOISE_FLAG_EASED},
|
||||
|
@ -198,47 +189,34 @@ inline float linearInterpolation(float v0, float v1, float t)
|
|||
inline float biLinearInterpolation(
|
||||
float v00, float v10,
|
||||
float v01, float v11,
|
||||
float x, float y)
|
||||
{
|
||||
float tx = easeCurve(x);
|
||||
float ty = easeCurve(y);
|
||||
float u = linearInterpolation(v00, v10, tx);
|
||||
float v = linearInterpolation(v01, v11, tx);
|
||||
return linearInterpolation(u, v, ty);
|
||||
}
|
||||
|
||||
|
||||
inline float biLinearInterpolationNoEase(
|
||||
float v00, float v10,
|
||||
float v01, float v11,
|
||||
float x, float y)
|
||||
float x, float y,
|
||||
bool eased)
|
||||
{
|
||||
// Inlining will optimize this branch out when possible
|
||||
if (eased) {
|
||||
x = easeCurve(x);
|
||||
y = easeCurve(y);
|
||||
}
|
||||
float u = linearInterpolation(v00, v10, x);
|
||||
float v = linearInterpolation(v01, v11, x);
|
||||
return linearInterpolation(u, v, y);
|
||||
}
|
||||
|
||||
|
||||
float triLinearInterpolation(
|
||||
inline float triLinearInterpolation(
|
||||
float v000, float v100, float v010, float v110,
|
||||
float v001, float v101, float v011, float v111,
|
||||
float x, float y, float z)
|
||||
float x, float y, float z,
|
||||
bool eased)
|
||||
{
|
||||
float tx = easeCurve(x);
|
||||
float ty = easeCurve(y);
|
||||
float tz = easeCurve(z);
|
||||
float u = biLinearInterpolationNoEase(v000, v100, v010, v110, tx, ty);
|
||||
float v = biLinearInterpolationNoEase(v001, v101, v011, v111, tx, ty);
|
||||
return linearInterpolation(u, v, tz);
|
||||
}
|
||||
|
||||
float triLinearInterpolationNoEase(
|
||||
float v000, float v100, float v010, float v110,
|
||||
float v001, float v101, float v011, float v111,
|
||||
float x, float y, float z)
|
||||
{
|
||||
float u = biLinearInterpolationNoEase(v000, v100, v010, v110, x, y);
|
||||
float v = biLinearInterpolationNoEase(v001, v101, v011, v111, x, y);
|
||||
// Inlining will optimize this branch out when possible
|
||||
if (eased) {
|
||||
x = easeCurve(x);
|
||||
y = easeCurve(y);
|
||||
z = easeCurve(z);
|
||||
}
|
||||
float u = biLinearInterpolation(v000, v100, v010, v110, x, y, false);
|
||||
float v = biLinearInterpolation(v001, v101, v011, v111, x, y, false);
|
||||
return linearInterpolation(u, v, z);
|
||||
}
|
||||
|
||||
|
@ -256,10 +234,7 @@ float noise2d_gradient(float x, float y, s32 seed, bool eased)
|
|||
float v01 = noise2d(x0, y0+1, seed);
|
||||
float v11 = noise2d(x0+1, y0+1, seed);
|
||||
// Interpolate
|
||||
if (eased)
|
||||
return biLinearInterpolation(v00, v10, v01, v11, xl, yl);
|
||||
|
||||
return biLinearInterpolationNoEase(v00, v10, v01, v11, xl, yl);
|
||||
return biLinearInterpolation(v00, v10, v01, v11, xl, yl, eased);
|
||||
}
|
||||
|
||||
|
||||
|
@ -283,17 +258,11 @@ float noise3d_gradient(float x, float y, float z, s32 seed, bool eased)
|
|||
float v011 = noise3d(x0, y0 + 1, z0 + 1, seed);
|
||||
float v111 = noise3d(x0 + 1, y0 + 1, z0 + 1, seed);
|
||||
// Interpolate
|
||||
if (eased) {
|
||||
return triLinearInterpolation(
|
||||
v000, v100, v010, v110,
|
||||
v001, v101, v011, v111,
|
||||
xl, yl, zl);
|
||||
}
|
||||
|
||||
return triLinearInterpolationNoEase(
|
||||
return triLinearInterpolation(
|
||||
v000, v100, v010, v110,
|
||||
v001, v101, v011, v111,
|
||||
xl, yl, zl);
|
||||
xl, yl, zl,
|
||||
eased);
|
||||
}
|
||||
|
||||
|
||||
|
@ -518,9 +487,6 @@ void Noise::gradientMap2D(
|
|||
s32 x0, y0;
|
||||
|
||||
bool eased = np.flags & (NOISE_FLAG_DEFAULTS | NOISE_FLAG_EASED);
|
||||
Interp2dFxn interpolate = eased ?
|
||||
biLinearInterpolation : biLinearInterpolationNoEase;
|
||||
|
||||
x0 = std::floor(x);
|
||||
y0 = std::floor(y);
|
||||
u = x - (float)x0;
|
||||
|
@ -547,7 +513,8 @@ void Noise::gradientMap2D(
|
|||
u = orig_u;
|
||||
noisex = 0;
|
||||
for (i = 0; i != sx; i++) {
|
||||
gradient_buf[index++] = interpolate(v00, v10, v01, v11, u, v);
|
||||
gradient_buf[index++] =
|
||||
biLinearInterpolation(v00, v10, v01, v11, u, v, eased);
|
||||
|
||||
u += step_x;
|
||||
if (u >= 1.0) {
|
||||
|
@ -583,8 +550,7 @@ void Noise::gradientMap3D(
|
|||
u32 nlx, nly, nlz;
|
||||
s32 x0, y0, z0;
|
||||
|
||||
Interp3dFxn interpolate = (np.flags & NOISE_FLAG_EASED) ?
|
||||
triLinearInterpolation : triLinearInterpolationNoEase;
|
||||
bool eased = np.flags & NOISE_FLAG_EASED;
|
||||
|
||||
x0 = std::floor(x);
|
||||
y0 = std::floor(y);
|
||||
|
@ -625,10 +591,11 @@ void Noise::gradientMap3D(
|
|||
u = orig_u;
|
||||
noisex = 0;
|
||||
for (i = 0; i != sx; i++) {
|
||||
gradient_buf[index++] = interpolate(
|
||||
gradient_buf[index++] = triLinearInterpolation(
|
||||
v000, v100, v010, v110,
|
||||
v001, v101, v011, v111,
|
||||
u, v, w);
|
||||
u, v, w,
|
||||
eased);
|
||||
|
||||
u += step_x;
|
||||
if (u >= 1.0) {
|
||||
|
|
|
@ -198,7 +198,7 @@ void read_object_properties(lua_State *L, int index,
|
|||
prop->hp_max = (u16)rangelim(hp_max, 0, U16_MAX);
|
||||
|
||||
if (prop->hp_max < sao->getHP()) {
|
||||
PlayerHPChangeReason reason(PlayerHPChangeReason::SET_HP);
|
||||
PlayerHPChangeReason reason(PlayerHPChangeReason::SET_HP_MAX);
|
||||
sao->setHP(prop->hp_max, reason);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ void ScriptApiEntity::luaentity_Activate(u16 id,
|
|||
lua_pop(L, 2); // Pop object and error handler
|
||||
}
|
||||
|
||||
void ScriptApiEntity::luaentity_Deactivate(u16 id)
|
||||
void ScriptApiEntity::luaentity_Deactivate(u16 id, bool removal)
|
||||
{
|
||||
SCRIPTAPI_PRECHECKHEADER
|
||||
|
||||
|
@ -120,9 +120,9 @@ void ScriptApiEntity::luaentity_Deactivate(u16 id)
|
|||
if (!lua_isnil(L, -1)) {
|
||||
luaL_checktype(L, -1, LUA_TFUNCTION);
|
||||
lua_pushvalue(L, object);
|
||||
|
||||
lua_pushboolean(L, removal);
|
||||
setOriginFromTable(object);
|
||||
PCALL_RES(lua_pcall(L, 1, 0, error_handler));
|
||||
PCALL_RES(lua_pcall(L, 2, 0, error_handler));
|
||||
} else {
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ public:
|
|||
bool luaentity_Add(u16 id, const char *name);
|
||||
void luaentity_Activate(u16 id,
|
||||
const std::string &staticdata, u32 dtime_s);
|
||||
void luaentity_Deactivate(u16 id);
|
||||
void luaentity_Deactivate(u16 id, bool removal);
|
||||
void luaentity_Remove(u16 id);
|
||||
std::string luaentity_GetStaticdata(u16 id);
|
||||
void luaentity_GetProperties(u16 id,
|
||||
|
|
|
@ -1107,7 +1107,7 @@ PlayerSAO* Server::StageTwoClientInit(session_t peer_id)
|
|||
SendInventory(playersao, false);
|
||||
|
||||
// Send HP
|
||||
SendPlayerHP(playersao);
|
||||
SendPlayerHP(playersao, false);
|
||||
|
||||
// Send death screen
|
||||
if (playersao->isDead())
|
||||
|
@ -1374,7 +1374,7 @@ void Server::SendMovement(session_t peer_id)
|
|||
void Server::HandlePlayerHPChange(PlayerSAO *playersao, const PlayerHPChangeReason &reason)
|
||||
{
|
||||
m_script->player_event(playersao, "health_changed");
|
||||
SendPlayerHP(playersao);
|
||||
SendPlayerHP(playersao, reason.type != PlayerHPChangeReason::SET_HP_MAX);
|
||||
|
||||
// Send to other clients
|
||||
playersao->sendPunchCommand();
|
||||
|
@ -1383,15 +1383,15 @@ void Server::HandlePlayerHPChange(PlayerSAO *playersao, const PlayerHPChangeReas
|
|||
HandlePlayerDeath(playersao, reason);
|
||||
}
|
||||
|
||||
void Server::SendPlayerHP(PlayerSAO *playersao)
|
||||
void Server::SendPlayerHP(PlayerSAO *playersao, bool effect)
|
||||
{
|
||||
SendHP(playersao->getPeerID(), playersao->getHP());
|
||||
SendHP(playersao->getPeerID(), playersao->getHP(), effect);
|
||||
}
|
||||
|
||||
void Server::SendHP(session_t peer_id, u16 hp)
|
||||
void Server::SendHP(session_t peer_id, u16 hp, bool effect)
|
||||
{
|
||||
NetworkPacket pkt(TOCLIENT_HP, 1, peer_id);
|
||||
pkt << hp;
|
||||
NetworkPacket pkt(TOCLIENT_HP, 3, peer_id);
|
||||
pkt << hp << effect;
|
||||
Send(&pkt);
|
||||
}
|
||||
|
||||
|
|
|
@ -353,7 +353,7 @@ public:
|
|||
void printToConsoleOnly(const std::string &text);
|
||||
|
||||
void HandlePlayerHPChange(PlayerSAO *sao, const PlayerHPChangeReason &reason);
|
||||
void SendPlayerHP(PlayerSAO *sao);
|
||||
void SendPlayerHP(PlayerSAO *sao, bool effect);
|
||||
void SendPlayerBreath(PlayerSAO *sao);
|
||||
void SendInventory(PlayerSAO *playerSAO, bool incremental);
|
||||
void SendMovePlayer(session_t peer_id);
|
||||
|
@ -439,7 +439,7 @@ private:
|
|||
void init();
|
||||
|
||||
void SendMovement(session_t peer_id);
|
||||
void SendHP(session_t peer_id, u16 hp);
|
||||
void SendHP(session_t peer_id, u16 hp, bool effect);
|
||||
void SendBreath(session_t peer_id, u16 breath);
|
||||
void SendAccessDenied(session_t peer_id, AccessDeniedCode reason,
|
||||
const std::string &custom_reason, bool reconnect = false);
|
||||
|
|
|
@ -117,13 +117,13 @@ void LuaEntitySAO::addedToEnvironment(u32 dtime_s)
|
|||
}
|
||||
}
|
||||
|
||||
void LuaEntitySAO::dispatchScriptDeactivate()
|
||||
void LuaEntitySAO::dispatchScriptDeactivate(bool removal)
|
||||
{
|
||||
// Ensure that this is in fact a registered entity,
|
||||
// and that it isn't already gone.
|
||||
// The latter also prevents this from ever being called twice.
|
||||
if (m_registered && !isGone())
|
||||
m_env->getScriptIface()->luaentity_Deactivate(m_id);
|
||||
m_env->getScriptIface()->luaentity_Deactivate(m_id, removal);
|
||||
}
|
||||
|
||||
void LuaEntitySAO::step(float dtime, bool send_recommended)
|
||||
|
|
|
@ -80,9 +80,9 @@ public:
|
|||
bool collideWithObjects() const;
|
||||
|
||||
protected:
|
||||
void dispatchScriptDeactivate();
|
||||
virtual void onMarkedForDeactivation() { dispatchScriptDeactivate(); }
|
||||
virtual void onMarkedForRemoval() { dispatchScriptDeactivate(); }
|
||||
void dispatchScriptDeactivate(bool removal);
|
||||
virtual void onMarkedForDeactivation() { dispatchScriptDeactivate(false); }
|
||||
virtual void onMarkedForRemoval() { dispatchScriptDeactivate(true); }
|
||||
|
||||
private:
|
||||
std::string getPropertyPacket();
|
||||
|
|
|
@ -495,7 +495,7 @@ void PlayerSAO::setHP(s32 target_hp, const PlayerHPChangeReason &reason, bool fr
|
|||
m_hp = hp;
|
||||
m_env->getGameDef()->HandlePlayerHPChange(this, reason);
|
||||
} else if (from_client)
|
||||
m_env->getGameDef()->SendPlayerHP(this);
|
||||
m_env->getGameDef()->SendPlayerHP(this, true);
|
||||
}
|
||||
|
||||
void PlayerSAO::setBreath(const u16 breath, bool send)
|
||||
|
|
|
@ -235,6 +235,7 @@ struct PlayerHPChangeReason
|
|||
enum Type : u8
|
||||
{
|
||||
SET_HP,
|
||||
SET_HP_MAX, // internal type to allow distinguishing hp reset and damage (for effects)
|
||||
PLAYER_PUNCH,
|
||||
FALL,
|
||||
NODE_DAMAGE,
|
||||
|
@ -277,6 +278,7 @@ struct PlayerHPChangeReason
|
|||
{
|
||||
switch (type) {
|
||||
case PlayerHPChangeReason::SET_HP:
|
||||
case PlayerHPChangeReason::SET_HP_MAX:
|
||||
return "set_hp";
|
||||
case PlayerHPChangeReason::PLAYER_PUNCH:
|
||||
return "punch";
|
||||
|
|
Loading…
Reference in New Issue