1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00

Add GUIDs and unit tests.

This commit is contained in:
SFENCE 2025-02-06 20:44:27 +01:00
parent aba2b6638e
commit 1df282c635
19 changed files with 246 additions and 14 deletions

View file

@ -47,6 +47,7 @@ core.features = {
particle_blend_clip = true, particle_blend_clip = true,
remove_item_match_meta = true, remove_item_match_meta = true,
httpfetch_additional_methods = true, httpfetch_additional_methods = true,
object_guids = true,
} }
function core.has_feature(arg) function core.has_feature(arg)

View file

@ -5810,6 +5810,8 @@ Utilities
remove_item_match_meta = true, remove_item_match_meta = true,
-- The HTTP API supports the HEAD and PATCH methods (5.12.0) -- The HTTP API supports the HEAD and PATCH methods (5.12.0)
httpfetch_additional_methods = true, 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, Note: changes to initial properties will only affect entities spawned afterwards,
as they are only read when spawning. as they are only read when spawning.
* `core.object_refs` * `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` * `core.luaentities`
* Map of Lua entities, indexed by active object id * Map of Lua entities, indexed by active object id
* `core.registered_abms` * `core.registered_abms`
@ -8546,6 +8552,14 @@ child will follow movement and rotation of that bone.
-- Default: false -- 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) #### Lua entity only (no-op for other objects)

View file

@ -594,9 +594,11 @@ Object types:
* `s32` yaw * 1000 * `s32` yaw * 1000
Since protocol version 37: Since protocol version 37:
* `u8` `version2` (=1) * `u8` `version2` (=1 or 2)
* `s32` pitch * 1000 * `s32` pitch * 1000
* `s32` roll * 1000 * `s32` roll * 1000
* if version2 >= 2:
* u8[16] guid
# Itemstring Format # Itemstring Format

View file

@ -255,3 +255,21 @@ local function test_item_drop(_, pos)
assert(itemstack_ret:equals(itemstack_src)) assert(itemstack_ret:equals(itemstack_src))
end end
unittests.register("test_item_drop", test_item_drop, {map=true}) 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})

View file

@ -204,3 +204,11 @@ local function run_player_hotbar_clamp_tests(player)
player:hud_set_hotbar_itemcount(old_bar_size) player:hud_set_hotbar_itemcount(old_bar_size)
end end
unittests.register("test_player_hotbar_clamp", run_player_hotbar_clamp_tests, {player=true}) 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})

View file

@ -455,6 +455,7 @@ set(common_SRCS
environment.cpp environment.cpp
filesys.cpp filesys.cpp
gettext.cpp gettext.cpp
guid.cpp
inventorymanager.cpp inventorymanager.cpp
itemdef.cpp itemdef.cpp
light.cpp light.cpp

48
src/guid.cpp Normal file
View file

@ -0,0 +1,48 @@
// Luanti
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) 2024 SFENCE
#include "guid.h"
#include <cstring>
#include <sstream>
#include <string_view>
#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<char, 16> bytes;
std::memcpy(&bytes[0], reinterpret_cast<char*>(&rand1), 8);
std::memcpy(&bytes[8], reinterpret_cast<char*>(&rand2), 8);
return MyGUID{bytes};
}

45
src/guid.h Normal file
View file

@ -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 <random>
#include <string>
#include <array>
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<char, 16> 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<u64> m_uniform;
};

View file

@ -406,17 +406,17 @@ void ScriptApiBase::setOriginFromTableRaw(int index, const char *fxn)
/* /*
* How ObjectRefs are handled in Lua: * How ObjectRefs are handled in Lua:
* When an active object is created, an ObjectRef is created on the Lua side * 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 * Methods that require an ObjectRef to a certain object retrieve it from that
* table instead of creating their own.(*) * table instead of creating their own.(*)
* When an active object is removed, the existing ObjectRef is invalidated * 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 * (*) An exception to this are NULL ObjectRefs and anonymous ObjectRefs
* for objects without ID. * for objects without ID.
* It's unclear what the latter are needed for and their use is problematic * 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. * since we lose control over the ref and the contained pointer.
*/ */
void ScriptApiBase::addObjectReference(ServerActiveObject *cobj) void ScriptApiBase::addObjectReference(ServerActiveObject *cobj)
{ {
SCRIPTAPI_PRECHECKHEADER SCRIPTAPI_PRECHECKHEADER
@ -434,7 +434,18 @@ void ScriptApiBase::addObjectReference(ServerActiveObject *cobj)
// object_refs[id] = object // object_refs[id] = object
lua_pushinteger(L, cobj->getId()); // Push id 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); lua_settable(L, objectstable);
} }
@ -445,6 +456,7 @@ void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj)
// Get core.object_refs table // Get core.object_refs table
lua_getglobal(L, "core"); lua_getglobal(L, "core");
int core = lua_gettop(L);
lua_getfield(L, -1, "object_refs"); lua_getfield(L, -1, "object_refs");
luaL_checktype(L, -1, LUA_TTABLE); luaL_checktype(L, -1, LUA_TTABLE);
int objectstable = lua_gettop(L); int objectstable = lua_gettop(L);
@ -460,6 +472,16 @@ void ScriptApiBase::removeObjectReference(ServerActiveObject *cobj)
lua_pushinteger(L, cobj->getId()); // Push id lua_pushinteger(L, cobj->getId()); // Push id
lua_pushnil(L); lua_pushnil(L);
lua_settable(L, objectstable); 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) void ScriptApiBase::objectrefGetOrCreate(lua_State *L, ServerActiveObject *cobj)

View file

@ -119,6 +119,20 @@ int ObjectRef::l_is_valid(lua_State *L)
return 1; return 1;
} }
// get_guid()
int ObjectRef::l_get_guid(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
ObjectRef *ref = checkObject<ObjectRef>(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) // get_pos(self)
int ObjectRef::l_get_pos(lua_State *L) int ObjectRef::l_get_pos(lua_State *L)
{ {
@ -2836,6 +2850,7 @@ luaL_Reg ObjectRef::methods[] = {
// ServerActiveObject // ServerActiveObject
luamethod(ObjectRef, remove), luamethod(ObjectRef, remove),
luamethod(ObjectRef, is_valid), luamethod(ObjectRef, is_valid),
luamethod(ObjectRef, get_guid),
luamethod_aliased(ObjectRef, get_pos, getpos), luamethod_aliased(ObjectRef, get_pos, getpos),
luamethod_aliased(ObjectRef, set_pos, setpos), luamethod_aliased(ObjectRef, set_pos, setpos),
luamethod(ObjectRef, add_pos), luamethod(ObjectRef, add_pos),

View file

@ -61,6 +61,9 @@ private:
// is_valid(self) // is_valid(self)
static int l_is_valid(lua_State *L); static int l_is_valid(lua_State *L);
// get_guid()
static int l_get_guid(lua_State *L);
// get_pos(self) // get_pos(self)
static int l_get_pos(lua_State *L); static int l_get_pos(lua_State *L);

View file

@ -62,6 +62,9 @@ ServerScripting::ServerScripting(Server* server):
lua_newtable(L); lua_newtable(L);
lua_setfield(L, -2, "object_refs"); lua_setfield(L, -2, "object_refs");
lua_newtable(L);
lua_setfield(L, -2, "objects_by_guid");
lua_newtable(L); lua_newtable(L);
lua_setfield(L, -2, "luaentities"); lua_setfield(L, -2, "luaentities");

View file

@ -50,7 +50,14 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &d
rotation.X = readF1000(is); rotation.X = readF1000(is);
rotation.Z = 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;
// <read new values> // <read new values>
break; break;
@ -70,6 +77,14 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &d
m_rotation = rotation; 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() LuaEntitySAO::~LuaEntitySAO()
{ {
if(m_registered){ if(m_registered){
@ -294,11 +309,13 @@ void LuaEntitySAO::getStaticData(std::string *result) const
writeF1000(os, m_rotation.Y); writeF1000(os, m_rotation.Y);
// version2. Increase this variable for new values // 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.X);
writeF1000(os, m_rotation.Z); writeF1000(os, m_rotation.Z);
m_guid.serialize(os);
// <write new values> // <write new values>
*result = os.str(); *result = os.str();
@ -414,6 +431,13 @@ u16 LuaEntitySAO::getHP() const
return m_hp; 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) void LuaEntitySAO::setVelocity(v3f velocity)
{ {
m_velocity = velocity; m_velocity = velocity;

View file

@ -6,6 +6,7 @@
#pragma once #pragma once
#include "unit_sao.h" #include "unit_sao.h"
#include "guid.h"
class LuaEntitySAO : public UnitSAO class LuaEntitySAO : public UnitSAO
{ {
@ -15,11 +16,7 @@ public:
LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &data); LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &data);
// Used by the Lua API // Used by the Lua API
LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name, LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name,
const std::string &state) : const std::string &state);
UnitSAO(env, pos),
m_init_name(name), m_init_state(state)
{
}
~LuaEntitySAO(); ~LuaEntitySAO();
ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_LUAENTITY; } ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_LUAENTITY; }
@ -47,6 +44,7 @@ public:
void setHP(s32 hp, const PlayerHPChangeReason &reason); void setHP(s32 hp, const PlayerHPChangeReason &reason);
u16 getHP() const; u16 getHP() const;
std::string getGUID() override;
/* LuaEntitySAO-specific */ /* LuaEntitySAO-specific */
void setVelocity(v3f velocity); void setVelocity(v3f velocity);
@ -86,6 +84,8 @@ private:
std::string m_init_state; std::string m_init_state;
bool m_registered = false; bool m_registered = false;
MyGUID m_guid;
v3f m_velocity; v3f m_velocity;
v3f m_acceleration; v3f m_acceleration;

View file

@ -14,6 +14,7 @@ PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t p
bool is_singleplayer): bool is_singleplayer):
UnitSAO(env_, v3f(0,0,0)), UnitSAO(env_, v3f(0,0,0)),
m_player(player_), m_player(player_),
m_player_name(player_->getName()),
m_peer_id_initial(peer_id_), m_peer_id_initial(peer_id_),
m_is_singleplayer(is_singleplayer) m_is_singleplayer(is_singleplayer)
{ {

View file

@ -78,6 +78,7 @@ public:
void addPos(const v3f &added_pos) override; void addPos(const v3f &added_pos) override;
void moveTo(v3f pos, bool continuous) override; void moveTo(v3f pos, bool continuous) override;
void setPlayerYaw(const float yaw); void setPlayerYaw(const float yaw);
std::string getGUID() override { return m_player_name; }
// Data should not be sent at player initialization // Data should not be sent at player initialization
void setPlayerYawAndSend(const float yaw); void setPlayerYawAndSend(const float yaw);
void setLookPitch(const float pitch); void setLookPitch(const float pitch);
@ -182,6 +183,7 @@ private:
std::string generateUpdatePhysicsOverrideCommand() const; std::string generateUpdatePhysicsOverrideCommand() const;
RemotePlayer *m_player = nullptr; RemotePlayer *m_player = nullptr;
std::string m_player_name; ///< used as GUID
session_t m_peer_id_initial = 0; ///< only used to initialize RemotePlayer session_t m_peer_id_initial = 0; ///< only used to initialize RemotePlayer
// Cheat prevention // Cheat prevention

View file

@ -141,6 +141,10 @@ public:
virtual u16 getHP() const virtual u16 getHP() const
{ return 0; } { 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 void setArmorGroups(const ItemGroupList &armor_groups)
{} {}
virtual const ItemGroupList &getArmorGroups() const virtual const ItemGroupList &getArmorGroups() const

View file

@ -10,6 +10,8 @@
#include "activeobject.h" #include "activeobject.h"
#include "environment.h" #include "environment.h"
#include "servermap.h" #include "servermap.h"
#include "guid.h"
#include "map.h"
#include "settings.h" #include "settings.h"
#include "server/activeobjectmgr.h" #include "server/activeobjectmgr.h"
#include "server/blockmodifier.h" #include "server/blockmodifier.h"
@ -123,6 +125,9 @@ public:
float getSendRecommendedInterval() float getSendRecommendedInterval()
{ return m_recommended_send_interval; } { return m_recommended_send_interval; }
GUIDGenerator & getGUIDGenerator()
{ return m_guid_generator; }
// Save players // Save players
void saveLoadedPlayers(bool force = false); void saveLoadedPlayers(bool force = false);
void savePlayer(RemotePlayer *player); void savePlayer(RemotePlayer *player);
@ -357,6 +362,7 @@ private:
server::ActiveObjectMgr m_ao_manager; server::ActiveObjectMgr m_ao_manager;
// on_mapblocks_changed map event receiver // on_mapblocks_changed map event receiver
OnMapblocksChangedReceiver m_on_mapblocks_changed_receiver; OnMapblocksChangedReceiver m_on_mapblocks_changed_receiver;
GUIDGenerator m_guid_generator;
// Outgoing network message buffer for active objects // Outgoing network message buffer for active objects
std::queue<ActiveObjectMessage> m_active_object_messages; std::queue<ActiveObjectMessage> m_active_object_messages;
// Some timers // Some timers

View file

@ -2,16 +2,31 @@
// SPDX-License-Identifier: LGPL-2.1-or-later // SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) 2022 Minetest core developers & community // Copyright (C) 2022 Minetest core developers & community
#include "guid.h"
#include "serverenvironment.h"
#include <server/serveractiveobject.h> #include <server/serveractiveobject.h>
#include <string>
class MockServerActiveObject : public ServerActiveObject class MockServerActiveObject : public ServerActiveObject
{ {
public: public:
MockServerActiveObject(ServerEnvironment *env = nullptr, v3f p = v3f()) : 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 ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_TEST; }
virtual bool getCollisionBox(aabb3f *toset) const { return false; } virtual bool getCollisionBox(aabb3f *toset) const { return false; }
virtual bool getSelectionBox(aabb3f *toset) const { return false; } virtual bool getSelectionBox(aabb3f *toset) const { return false; }
virtual bool collideWithObjects() 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;
}; };