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,
remove_item_match_meta = true,
httpfetch_additional_methods = true,
object_guids = true,
}
function core.has_feature(arg)

View file

@ -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)

View file

@ -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

View file

@ -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})

View file

@ -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})

View file

@ -455,6 +455,7 @@ set(common_SRCS
environment.cpp
filesys.cpp
gettext.cpp
guid.cpp
inventorymanager.cpp
itemdef.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:
* 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)

View file

@ -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<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)
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),

View file

@ -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);

View file

@ -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");

View file

@ -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;
// <read new values>
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);
// <write new values>
*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;

View file

@ -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;

View file

@ -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)
{

View file

@ -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

View file

@ -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

View file

@ -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<ActiveObjectMessage> m_active_object_messages;
// Some timers

View file

@ -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 <server/serveractiveobject.h>
#include <string>
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;
};