From 1df282c635cc33a2d94aa1d9bd776811488043fb Mon Sep 17 00:00:00 2001 From: SFENCE Date: Thu, 6 Feb 2025 20:44:27 +0100 Subject: [PATCH] Add GUIDs and unit tests. --- builtin/game/features.lua | 1 + doc/lua_api.md | 16 ++++++++- doc/world_format.md | 4 ++- games/devtest/mods/unittests/entity.lua | 18 ++++++++++ games/devtest/mods/unittests/player.lua | 8 +++++ src/CMakeLists.txt | 1 + src/guid.cpp | 48 +++++++++++++++++++++++++ src/guid.h | 45 +++++++++++++++++++++++ src/script/cpp_api/s_base.cpp | 30 +++++++++++++--- src/script/lua_api/l_object.cpp | 15 ++++++++ src/script/lua_api/l_object.h | 3 ++ src/script/scripting_server.cpp | 3 ++ src/server/luaentity_sao.cpp | 28 +++++++++++++-- src/server/luaentity_sao.h | 10 +++--- src/server/player_sao.cpp | 1 + src/server/player_sao.h | 2 ++ src/server/serveractiveobject.h | 4 +++ src/serverenvironment.h | 6 ++++ src/unittest/mock_serveractiveobject.h | 17 ++++++++- 19 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 src/guid.cpp create mode 100644 src/guid.h diff --git a/builtin/game/features.lua b/builtin/game/features.lua index 1b329c7a3..4837f29bd 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -47,6 +47,7 @@ core.features = { particle_blend_clip = true, remove_item_match_meta = true, httpfetch_additional_methods = true, + object_guids = true, } function core.has_feature(arg) diff --git a/doc/lua_api.md b/doc/lua_api.md index ca3a5fee7..d387412b9 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -5810,6 +5810,8 @@ Utilities remove_item_match_meta = true, -- The HTTP API supports the HEAD and PATCH methods (5.12.0) httpfetch_additional_methods = true, + -- objects have get_guid method (5.13.0) + object_guids = true, } ``` @@ -7848,7 +7850,11 @@ Global tables Note: changes to initial properties will only affect entities spawned afterwards, as they are only read when spawning. * `core.object_refs` - * Map of object references, indexed by active object id + * Map of active object references, indexed by active object id + * Obsolete: Use `core.objects_by_guid` instead. + GUIDs are strictly more useful than active object IDs. +* `core.objects_by_guid` + * Map of active object references, indexed by object GUID * `core.luaentities` * Map of Lua entities, indexed by active object id * `core.registered_abms` @@ -8546,6 +8552,14 @@ child will follow movement and rotation of that bone. -- Default: false } ``` +* `get_guid()`: returns a global unique identifier (a string) + * For players, this is a player name. + * For Lua entities, this is a uniquely generated string, guaranteed not to collide with player names. + * Example: `@bGh3p2AbRE29Mb4biqX6OA` + * GUIDs only use printable ASCII characters. + * GUIDs are persisted internally between object reloads; their format is guaranteed not to change. + Thus you can store GUIDs to identify objects persistently. + #### Lua entity only (no-op for other objects) diff --git a/doc/world_format.md b/doc/world_format.md index c519361d9..c7bd0f6cd 100644 --- a/doc/world_format.md +++ b/doc/world_format.md @@ -594,9 +594,11 @@ Object types: * `s32` yaw * 1000 Since protocol version 37: -* `u8` `version2` (=1) +* `u8` `version2` (=1 or 2) * `s32` pitch * 1000 * `s32` roll * 1000 +* if version2 >= 2: + * u8[16] guid # Itemstring Format diff --git a/games/devtest/mods/unittests/entity.lua b/games/devtest/mods/unittests/entity.lua index fad7d52e9..eed6ff334 100644 --- a/games/devtest/mods/unittests/entity.lua +++ b/games/devtest/mods/unittests/entity.lua @@ -255,3 +255,21 @@ local function test_item_drop(_, pos) assert(itemstack_ret:equals(itemstack_src)) end unittests.register("test_item_drop", test_item_drop, {map=true}) + +local function test_entity_guid(_, pos) + log = {} + + local obj0 = core.add_entity(pos, "unittests:callbacks") + check_log({"on_activate(0)"}) + local obj1 = core.add_entity(pos, "unittests:callbacks") + check_log({"on_activate(0)"}) + + assert(core.objects_by_guid[obj0:get_guid()] == obj0) + assert(core.objects_by_guid[obj1:get_guid()] == obj1) + + obj0:remove() + check_log({"on_deactivate(true)"}) + obj1:remove() + check_log({"on_deactivate(true)"}) +end +unittests.register("test_entity_guid", test_entity_guid, {map=true}) diff --git a/games/devtest/mods/unittests/player.lua b/games/devtest/mods/unittests/player.lua index f8945f320..b6dcec86f 100644 --- a/games/devtest/mods/unittests/player.lua +++ b/games/devtest/mods/unittests/player.lua @@ -204,3 +204,11 @@ local function run_player_hotbar_clamp_tests(player) player:hud_set_hotbar_itemcount(old_bar_size) end unittests.register("test_player_hotbar_clamp", run_player_hotbar_clamp_tests, {player=true}) + +-- +-- Player get GUID +-- +local function test_player_guid_tests(player) + assert(player:get_guid()==player:get_player_name()) +end +unittests.register("test_player_guid", test_player_guid_tests, {player=true}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1231f49ba..2bca969c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -455,6 +455,7 @@ set(common_SRCS environment.cpp filesys.cpp gettext.cpp + guid.cpp inventorymanager.cpp itemdef.cpp light.cpp diff --git a/src/guid.cpp b/src/guid.cpp new file mode 100644 index 000000000..f9580458c --- /dev/null +++ b/src/guid.cpp @@ -0,0 +1,48 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 SFENCE + +#include "guid.h" +#include +#include +#include + +#include "exceptions.h" +#include "util/base64.h" +#include "log.h" + +std::string MyGUID::base64() const +{ + return base64_encode(std::string_view(&bytes[0], bytes.size())); +} + +void MyGUID::serialize(std::ostringstream &os) const +{ + os.write(&bytes[0], bytes.size()); +} + +void MyGUID::deSerialize(std::istream &is) +{ + is.read(&bytes[0], bytes.size()); +} + +GUIDGenerator::GUIDGenerator() : + m_uniform(0, UINT64_MAX) +{ + if (m_rand.entropy() <= 0.01) + warningstream << + "The system's provided random generator reports low entropy." + "GUID generator can be affected. Suggest a system upgrade." + << std::endl; +} + +MyGUID GUIDGenerator::next() +{ + u64 rand1 = m_uniform(m_rand); + u64 rand2 = m_uniform(m_rand); + + std::array bytes; + std::memcpy(&bytes[0], reinterpret_cast(&rand1), 8); + std::memcpy(&bytes[8], reinterpret_cast(&rand2), 8); + return MyGUID{bytes}; +} diff --git a/src/guid.h b/src/guid.h new file mode 100644 index 000000000..87ea2ccf7 --- /dev/null +++ b/src/guid.h @@ -0,0 +1,45 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 SFENCE + +#pragma once + +#include "irrlichttypes.h" +#include "util/basic_macros.h" +#include +#include +#include + +class ServerEnvironment; + +/** + * A global unique identifier. + * It is global because it stays valid forever. + * It is unique because there are no collisions. + */ +struct MyGUID { + std::array bytes; + + std::string base64() const; + void serialize(std::ostringstream &os) const; + void deSerialize(std::istream &is); +}; + +class GUIDGenerator { + DISABLE_CLASS_COPY(GUIDGenerator) + +public: + + GUIDGenerator(); + + /** + * Generates the next GUID, which it will never return again. + * @return the new GUID + */ + MyGUID next(); + +private: + + std::random_device m_rand; + std::uniform_int_distribution m_uniform; +}; diff --git a/src/script/cpp_api/s_base.cpp b/src/script/cpp_api/s_base.cpp index ba931b22a..336ffc25f 100644 --- a/src/script/cpp_api/s_base.cpp +++ b/src/script/cpp_api/s_base.cpp @@ -406,17 +406,17 @@ void ScriptApiBase::setOriginFromTableRaw(int index, const char *fxn) /* * How ObjectRefs are handled in Lua: * When an active object is created, an ObjectRef is created on the Lua side - * and stored in core.object_refs[id]. + * and stored in core.object_refs[id] and in core.objects_by_guids[GUID]. * Methods that require an ObjectRef to a certain object retrieve it from that * table instead of creating their own.(*) * When an active object is removed, the existing ObjectRef is invalidated - * using ::set_null() and removed from the core.object_refs table. + * using ::set_null() and removed from the core.object_refs and + * core.object_by_guids tables. * (*) An exception to this are NULL ObjectRefs and anonymous ObjectRefs * for objects without ID. * It's unclear what the latter are needed for and their use is problematic * since we lose control over the ref and the contained pointer. */ - void ScriptApiBase::addObjectReference(ServerActiveObject *cobj) { SCRIPTAPI_PRECHECKHEADER @@ -434,7 +434,18 @@ void ScriptApiBase::addObjectReference(ServerActiveObject *cobj) // object_refs[id] = object lua_pushinteger(L, cobj->getId()); // Push id - lua_pushvalue(L, object); // Copy object to top of stack + lua_pushvalue(L, object); + lua_settable(L, objectstable); + + // Get core.objects_by_guid table + lua_getglobal(L, "core"); + lua_getfield(L, -1, "objects_by_guid"); + luaL_checktype(L, -1, LUA_TTABLE); + objectstable = lua_gettop(L); + + // objects_by_guid[guid] = object + lua_pushstring(L, cobj->getGUID().c_str()); + lua_pushvalue(L, object); lua_settable(L, objectstable); } @@ -445,6 +456,7 @@ void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj) // Get core.object_refs table lua_getglobal(L, "core"); + int core = lua_gettop(L); lua_getfield(L, -1, "object_refs"); luaL_checktype(L, -1, LUA_TTABLE); int objectstable = lua_gettop(L); @@ -460,6 +472,16 @@ void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj) lua_pushinteger(L, cobj->getId()); // Push id lua_pushnil(L); lua_settable(L, objectstable); + + // Get core.objects_by_guid + lua_getfield(L, core, "objects_by_guid"); + luaL_checktype(L, -1, LUA_TTABLE); + objectstable = lua_gettop(L); + + // Set objects_by_guid[guid] = nil + lua_pushstring(L, cobj->getGUID().c_str()); + lua_pushnil(L); + lua_settable(L, objectstable); } void ScriptApiBase::objectrefGetOrCreate(lua_State *L, ServerActiveObject *cobj) diff --git a/src/script/lua_api/l_object.cpp b/src/script/lua_api/l_object.cpp index 19c513dd3..78cf2a983 100644 --- a/src/script/lua_api/l_object.cpp +++ b/src/script/lua_api/l_object.cpp @@ -119,6 +119,20 @@ int ObjectRef::l_is_valid(lua_State *L) return 1; } +// get_guid() +int ObjectRef::l_get_guid(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + ObjectRef *ref = checkObject(L, 1); + ServerActiveObject *sao = getobject(ref); + if (sao == nullptr) + return 0; + + std::string guid = sao->getGUID(); + lua_pushlstring(L, guid.c_str(), guid.size()); + return 1; +} + // get_pos(self) int ObjectRef::l_get_pos(lua_State *L) { @@ -2836,6 +2850,7 @@ luaL_Reg ObjectRef::methods[] = { // ServerActiveObject luamethod(ObjectRef, remove), luamethod(ObjectRef, is_valid), + luamethod(ObjectRef, get_guid), luamethod_aliased(ObjectRef, get_pos, getpos), luamethod_aliased(ObjectRef, set_pos, setpos), luamethod(ObjectRef, add_pos), diff --git a/src/script/lua_api/l_object.h b/src/script/lua_api/l_object.h index f97d7d2da..5a3db157a 100644 --- a/src/script/lua_api/l_object.h +++ b/src/script/lua_api/l_object.h @@ -61,6 +61,9 @@ private: // is_valid(self) static int l_is_valid(lua_State *L); + // get_guid() + static int l_get_guid(lua_State *L); + // get_pos(self) static int l_get_pos(lua_State *L); diff --git a/src/script/scripting_server.cpp b/src/script/scripting_server.cpp index f30def03d..e13178eae 100644 --- a/src/script/scripting_server.cpp +++ b/src/script/scripting_server.cpp @@ -62,6 +62,9 @@ ServerScripting::ServerScripting(Server* server): lua_newtable(L); lua_setfield(L, -2, "object_refs"); + lua_newtable(L); + lua_setfield(L, -2, "objects_by_guid"); + lua_newtable(L); lua_setfield(L, -2, "luaentities"); diff --git a/src/server/luaentity_sao.cpp b/src/server/luaentity_sao.cpp index 0ad3daba6..261783f6e 100644 --- a/src/server/luaentity_sao.cpp +++ b/src/server/luaentity_sao.cpp @@ -50,7 +50,14 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &d rotation.X = readF1000(is); rotation.Z = readF1000(is); - // if (version2 < 2) + if (version2 < 2) { + m_guid = env->getGUIDGenerator().next(); + break; + } + + m_guid.deSerialize(is); + + // if (version2 < 3) // break; // break; @@ -70,6 +77,14 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &d m_rotation = rotation; } +LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name, + const std::string &state) : + UnitSAO(env, pos), + m_init_name(name), m_init_state(state), + m_guid(env->getGUIDGenerator().next()) +{ +} + LuaEntitySAO::~LuaEntitySAO() { if(m_registered){ @@ -294,11 +309,13 @@ void LuaEntitySAO::getStaticData(std::string *result) const writeF1000(os, m_rotation.Y); // version2. Increase this variable for new values - writeU8(os, 1); // PROTOCOL_VERSION >= 37 + writeU8(os, 2); // PROTOCOL_VERSION >= 37 writeF1000(os, m_rotation.X); writeF1000(os, m_rotation.Z); + m_guid.serialize(os); + // *result = os.str(); @@ -414,6 +431,13 @@ u16 LuaEntitySAO::getHP() const return m_hp; } +std::string LuaEntitySAO::getGUID() +{ + // The "@" ensures that entity GUIDs are easily recognizable + // and makes it obvious that they can't collide with player names. + return "@" + m_guid.base64(); +} + void LuaEntitySAO::setVelocity(v3f velocity) { m_velocity = velocity; diff --git a/src/server/luaentity_sao.h b/src/server/luaentity_sao.h index 9ea1d3f22..2a46fbc4f 100644 --- a/src/server/luaentity_sao.h +++ b/src/server/luaentity_sao.h @@ -6,6 +6,7 @@ #pragma once #include "unit_sao.h" +#include "guid.h" class LuaEntitySAO : public UnitSAO { @@ -15,11 +16,7 @@ public: LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &data); // Used by the Lua API LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name, - const std::string &state) : - UnitSAO(env, pos), - m_init_name(name), m_init_state(state) - { - } + const std::string &state); ~LuaEntitySAO(); ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_LUAENTITY; } @@ -47,6 +44,7 @@ public: void setHP(s32 hp, const PlayerHPChangeReason &reason); u16 getHP() const; + std::string getGUID() override; /* LuaEntitySAO-specific */ void setVelocity(v3f velocity); @@ -86,6 +84,8 @@ private: std::string m_init_state; bool m_registered = false; + MyGUID m_guid; + v3f m_velocity; v3f m_acceleration; diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp index 068b2b29f..1334443a1 100644 --- a/src/server/player_sao.cpp +++ b/src/server/player_sao.cpp @@ -14,6 +14,7 @@ PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t p bool is_singleplayer): UnitSAO(env_, v3f(0,0,0)), m_player(player_), + m_player_name(player_->getName()), m_peer_id_initial(peer_id_), m_is_singleplayer(is_singleplayer) { diff --git a/src/server/player_sao.h b/src/server/player_sao.h index a19177a7e..a75f2bd3c 100644 --- a/src/server/player_sao.h +++ b/src/server/player_sao.h @@ -78,6 +78,7 @@ public: void addPos(const v3f &added_pos) override; void moveTo(v3f pos, bool continuous) override; void setPlayerYaw(const float yaw); + std::string getGUID() override { return m_player_name; } // Data should not be sent at player initialization void setPlayerYawAndSend(const float yaw); void setLookPitch(const float pitch); @@ -182,6 +183,7 @@ private: std::string generateUpdatePhysicsOverrideCommand() const; RemotePlayer *m_player = nullptr; + std::string m_player_name; ///< used as GUID session_t m_peer_id_initial = 0; ///< only used to initialize RemotePlayer // Cheat prevention diff --git a/src/server/serveractiveobject.h b/src/server/serveractiveobject.h index da3dc17bd..ba7863420 100644 --- a/src/server/serveractiveobject.h +++ b/src/server/serveractiveobject.h @@ -141,6 +141,10 @@ public: virtual u16 getHP() const { return 0; } + /// Always returns the same unique string for the same object. + /// Because these strings are very short, copying them is not expensive. + virtual std::string getGUID() = 0; + virtual void setArmorGroups(const ItemGroupList &armor_groups) {} virtual const ItemGroupList &getArmorGroups() const diff --git a/src/serverenvironment.h b/src/serverenvironment.h index 04153e944..b080e0a9f 100644 --- a/src/serverenvironment.h +++ b/src/serverenvironment.h @@ -10,6 +10,8 @@ #include "activeobject.h" #include "environment.h" #include "servermap.h" +#include "guid.h" +#include "map.h" #include "settings.h" #include "server/activeobjectmgr.h" #include "server/blockmodifier.h" @@ -123,6 +125,9 @@ public: float getSendRecommendedInterval() { return m_recommended_send_interval; } + GUIDGenerator & getGUIDGenerator() + { return m_guid_generator; } + // Save players void saveLoadedPlayers(bool force = false); void savePlayer(RemotePlayer *player); @@ -357,6 +362,7 @@ private: server::ActiveObjectMgr m_ao_manager; // on_mapblocks_changed map event receiver OnMapblocksChangedReceiver m_on_mapblocks_changed_receiver; + GUIDGenerator m_guid_generator; // Outgoing network message buffer for active objects std::queue m_active_object_messages; // Some timers diff --git a/src/unittest/mock_serveractiveobject.h b/src/unittest/mock_serveractiveobject.h index 9d886b4eb..613c2c420 100644 --- a/src/unittest/mock_serveractiveobject.h +++ b/src/unittest/mock_serveractiveobject.h @@ -2,16 +2,31 @@ // SPDX-License-Identifier: LGPL-2.1-or-later // Copyright (C) 2022 Minetest core developers & community +#include "guid.h" +#include "serverenvironment.h" #include +#include class MockServerActiveObject : public ServerActiveObject { public: MockServerActiveObject(ServerEnvironment *env = nullptr, v3f p = v3f()) : - ServerActiveObject(env, p) {} + ServerActiveObject(env, p) + { + if (env) + m_guid = "mock:" + env->getGUIDGenerator().next().base64(); + } virtual ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_TEST; } virtual bool getCollisionBox(aabb3f *toset) const { return false; } virtual bool getSelectionBox(aabb3f *toset) const { return false; } virtual bool collideWithObjects() const { return false; } + virtual std::string getGUID() + { + assert(!m_guid.empty()); + return m_guid; + } + +private: + std::string m_guid; };