/* Minetest Copyright (C) 2024 sfan5 This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "test.h" #include "mock_server.h" #include "server/luaentity_sao.h" #include "emerge.h" /* * Tests how SAOs behave in the server environment. * See also test_serveractiveobjectmgr.cpp and test_activeobject.cpp for other tests. */ class TestSAO : public TestBase { public: TestSAO() { TestManager::registerTestModule(this); } const char *getName() { return "TestSAO"; } void runTests(IGameDef *gamedef); void testStaticSave(ServerEnvironment *env); void testNotSaved(ServerEnvironment *env); void testActivate(ServerEnvironment *env); void testStaticToFalse(ServerEnvironment *env); void testStaticToTrue(ServerEnvironment *env); private: // enough for both removeRemovedObjects and deactivateFarObjects to be called // use as param to ServerEnvironment::step() float m_step_interval; }; static TestSAO g_test_instance; const static char *helper_lua_src = R"( core.register_entity(":test:static", {}) core.register_entity(":test:non_static", { initial_properties = { static_save = false, } }) )"; void TestSAO::runTests(IGameDef *gamedef) { MockServer server(getTestTempDirectory()); const auto helper_lua = getTestTempFile(); { std::ofstream ofs(helper_lua, std::ios::out | std::ios::binary); ofs << helper_lua_src; std::ofstream ofs2(server.getWorldPath() + DIR_DELIM "world.mt", std::ios::out | std::ios::binary); ofs2 << "backend = dummy\n"; } server.createScripting(); try { auto script = server.getScriptIface(); script->loadBuiltin(); script->loadMod(helper_lua, BUILTIN_MOD_NAME); } catch (ModError &e) { rawstream << e.what() << std::endl; num_tests_failed = 1; return; } // unfortunately we have to create a lot since a DummyMap doesn't cut it. // TODO: best to factor entity mgmt out of ServerEnvironment, also // EmergeManager should become mockable MetricsBackend mb; EmergeManager emerge(&server, &mb); auto map = std::make_unique(server.getWorldPath(), gamedef, &emerge, &mb); ServerEnvironment env(std::move(map), &server, &mb); env.loadMeta(); m_step_interval = std::max( g_settings->getFloat("active_block_mgmt_interval"), 0.5f) + 0.1f; TEST(testStaticSave, &env); TEST(testNotSaved, &env); TEST(testActivate, &env); TEST(testStaticToFalse, &env); TEST(testStaticToTrue, &env); env.deactivateBlocksAndObjects(); } static LuaEntitySAO *add_entity(ServerEnvironment *env, v3f pos, const char *name) { auto obj_u = std::make_unique(env, pos, name, ""); auto obj = obj_u.get(); u16 id = env->addActiveObject(std::move(obj_u)); if (!id || obj->isGone()) return nullptr; return obj; } static u16 assert_active_in_block(MapBlock *block, u16 obj_id = 0) { const auto &so = block->m_static_objects; UASSERTEQ(size_t, so.getStoredSize(), 0); UASSERTEQ(size_t, so.getActiveSize(), 1); if (obj_id) { UASSERT(so.getAllActives().count(obj_id) == 1); } else { obj_id = so.getAllActives().begin()->first; UASSERT(obj_id != 0); } return obj_id; } void TestSAO::testStaticSave(ServerEnvironment *env) { Map &map = env->getMap(); const v3f testpos(0, -66 * BS, 0); const v3s16 testblockpos = getNodeBlockPos(floatToInt(testpos, BS)); auto obj = add_entity(env, testpos, "test:static"); UASSERT(obj); const u16 obj_id = obj->getId(); // static data should have been created UASSERT(obj->accessObjectProperties()->static_save); UASSERT(obj->m_static_exists); UASSERTEQ(auto, obj->m_static_block, testblockpos); // check that it's really in there auto *block = map.getBlockNoCreateNoEx(testblockpos); UASSERT(block); assert_active_in_block(block, obj_id); // Since we have no active blocks, stepping will deactivate it. env->step(m_step_interval); obj = nullptr; // dangling reference UASSERT(!env->getActiveObject(obj_id)); UASSERTEQ(size_t, block->m_static_objects.getStoredSize(), 1); UASSERTEQ(size_t, block->m_static_objects.getActiveSize(), 0); } void TestSAO::testNotSaved(ServerEnvironment *env) { Map &map = env->getMap(); const v3f testpos(0, 4 * BS, 0); const v3s16 testblockpos = getNodeBlockPos(floatToInt(testpos, BS)); // make sure matching block exists (see below) auto *block = map.emergeBlock(testblockpos, true); UASSERT(block); auto obj = add_entity(env, testpos, "test:non_static"); UASSERT(obj); const u16 obj_id = obj->getId(); UASSERT(!obj->accessObjectProperties()->static_save); UASSERT(!obj->m_static_exists); UASSERTEQ(size_t, block->m_static_objects.size(), 0); // Non-static objects are only unloaded together with their block, so doing // this will *not* delete it. env->step(m_step_interval); UASSERT(env->getActiveObject(obj_id) == obj); // Here we force the block to be unloaded, this has the expected effect. map.timerUpdate(10.0f, 5.0f, -1); env->step(m_step_interval); block = nullptr; obj = nullptr; UASSERT(!env->getActiveObject(obj_id)); block = map.emergeBlock(testblockpos, false); if (block) UASSERTEQ(size_t, block->m_static_objects.size(), 0); } void TestSAO::testActivate(ServerEnvironment *env) { Map &map = env->getMap(); const v3f testpos(0, 0, 100 * BS); const v3s16 testblockpos = getNodeBlockPos(floatToInt(testpos, BS)); // this time insert the static object manually StaticObject s_obj; { auto obj = std::make_unique(env, testpos, "test:static", ""); s_obj = StaticObject(obj.get(), obj->getBasePosition()); } auto *block = map.emergeBlock(testblockpos, true); UASSERT(block); block->m_static_objects.insert(0, s_obj); // Activating the block will convert it to active. env->activateBlock(block); const u16 obj_id = assert_active_in_block(block); auto *obj = env->getActiveObject(obj_id); UASSERT(obj); // same conditions as testStaticSave UASSERT(obj->m_static_exists); UASSERTEQ(auto, obj->m_static_block, testblockpos); // let the object be deactivated, it causes a warning when we unload // MapBlocks in further tests otherwise // FIXME: figure out if this is a bug that needs to be fixed env->step(m_step_interval); } void TestSAO::testStaticToFalse(ServerEnvironment *env) { Map &map = env->getMap(); const v3f testpos(0, 0, -22 * BS); const v3s16 testblockpos = getNodeBlockPos(floatToInt(testpos, BS)); /* * This test represents an edge case where static_save changed from true * to false while the server was off. */ StaticObject s_obj; { auto obj = std::make_unique(env, testpos, "test:non_static", ""); // since the object is not added, it doesn't read info from Lua and retains // the default value. This is what allows us to do this. UASSERT(obj->isStaticAllowed()); s_obj = StaticObject(obj.get(), obj->getBasePosition()); } auto *block = map.emergeBlock(testblockpos, true); UASSERT(block); block->m_static_objects.insert(0, s_obj); env->activateBlock(block); const u16 obj_id = assert_active_in_block(block); auto *obj = env->getActiveObject(obj_id); UASSERT(obj); // environment must remember place of static data UASSERT(obj->m_static_exists); UASSERTEQ(auto, obj->m_static_block, testblockpos); #if 0 /* * FIXME: needs investigation * When using this code path the test fails and there's also a warning. * The problem is that the block is unloaded while the object is still in the * active list, removeFarObjects() has no chance at doing something. * This seems to be a general issue that in practice only applies with static_save=false. */ map.timerUpdate(10.0f, 5.0f, -1); env->step(m_step_interval); #else obj->markForRemoval(); env->step(m_step_interval); #endif block = nullptr; obj = nullptr; UASSERT(!env->getActiveObject(obj_id)); // static data must have been deleted block = map.emergeBlock(testblockpos, false); if (block) UASSERTEQ(size_t, block->m_static_objects.size(), 0); } void TestSAO::testStaticToTrue(ServerEnvironment *env) { Map &map = env->getMap(); const v3f testpos(123 * BS, 5 * BS, 0); const v3s16 testblockpos = getNodeBlockPos(floatToInt(testpos, BS)); auto obj = add_entity(env, testpos, "test:non_static"); UASSERT(obj); const u16 obj_id = obj->getId(); UASSERT(!obj->m_static_exists); obj->accessObjectProperties()->static_save = true; env->step(m_step_interval); obj = nullptr; UASSERT(!env->getActiveObject(obj_id)); // data must have been newly saved auto *block = map.getBlockNoCreateNoEx(testblockpos); UASSERT(block); UASSERTEQ(size_t, block->m_static_objects.getStoredSize(), 1); UASSERTEQ(size_t, block->m_static_objects.getActiveSize(), 0); }