feat(Scripts/Commands): Implement npc/gameobject load commands (#24644)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Andrew
2026-02-08 08:36:35 -03:00
committed by GitHub
parent e0b50798a0
commit 2b563fc0e4
5 changed files with 428 additions and 0 deletions

View File

@@ -2422,6 +2422,164 @@ void ObjectMgr::LoadCreatures()
LOG_INFO("server.loading", " ");
}
// Loads a single creature spawn from DB into the cache.
// Creature::LoadCreatureFromDB() reads from cache (GetCreatureData()), not from DB directly,
// so this must be called first for spawns not loaded at startup.
CreatureData const* ObjectMgr::LoadCreatureDataFromDB(ObjectGuid::LowType spawnId)
{
CreatureData const* data = GetCreatureData(spawnId);
if (data)
return data;
QueryResult result = WorldDatabase.Query("SELECT creature.guid, id1, id2, id3, map, equipment_id, "
"position_x, position_y, position_z, orientation, spawntimesecs, wander_distance, "
"currentwaypoint, curhealth, curmana, MovementType, spawnMask, phaseMask, "
"creature.npcflag, creature.unit_flags, creature.dynamicflags, creature.ScriptName "
"FROM creature WHERE creature.guid = {}", spawnId);
if (!result)
return nullptr;
Field* fields = result->Fetch();
uint32 id1 = fields[1].Get<uint32>();
uint32 id2 = fields[2].Get<uint32>();
uint32 id3 = fields[3].Get<uint32>();
CreatureTemplate const* cInfo = GetCreatureTemplate(id1);
if (!cInfo)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) with non-existing creature entry {} in id1 field, skipped.", spawnId, id1);
return nullptr;
}
if (id2 && !GetCreatureTemplate(id2))
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) with non-existing creature entry {} in id2 field, skipped.", spawnId, id2);
return nullptr;
}
if (id3 && !GetCreatureTemplate(id3))
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) with non-existing creature entry {} in id3 field, skipped.", spawnId, id3);
return nullptr;
}
if (!id2 && id3)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) with creature entry {} in id3 field but no entry in id2 field, skipped.", spawnId, id3);
return nullptr;
}
CreatureData& creatureData = _creatureDataStore[spawnId];
creatureData.id1 = id1;
creatureData.id2 = id2;
creatureData.id3 = id3;
creatureData.mapid = fields[4].Get<uint16>();
creatureData.equipmentId = fields[5].Get<int8>();
creatureData.posX = fields[6].Get<float>();
creatureData.posY = fields[7].Get<float>();
creatureData.posZ = fields[8].Get<float>();
creatureData.orientation = fields[9].Get<float>();
creatureData.spawntimesecs = fields[10].Get<uint32>();
creatureData.wander_distance = fields[11].Get<float>();
creatureData.currentwaypoint = fields[12].Get<uint32>();
creatureData.curhealth = fields[13].Get<uint32>();
creatureData.curmana = fields[14].Get<uint32>();
creatureData.movementType = fields[15].Get<uint8>();
creatureData.spawnMask = fields[16].Get<uint8>();
creatureData.phaseMask = fields[17].Get<uint32>();
creatureData.npcflag = fields[18].Get<uint32>();
creatureData.unit_flags = fields[19].Get<uint32>();
creatureData.dynamicflags = fields[20].Get<uint32>();
creatureData.ScriptId = GetScriptId(fields[21].Get<std::string>());
if (!creatureData.ScriptId)
creatureData.ScriptId = cInfo->ScriptID;
MapEntry const* mapEntry = sMapStore.LookupEntry(creatureData.mapid);
if (!mapEntry)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) that spawned at non-existing map (Id: {}), skipped.", spawnId, creatureData.mapid);
_creatureDataStore.erase(spawnId);
return nullptr;
}
if (mapEntry->IsRaid() && creatureData.spawntimesecs >= 7 * DAY && creatureData.spawntimesecs < 14 * DAY)
creatureData.spawntimesecs = 14 * DAY;
bool ok = true;
for (uint32 diff = 0; diff < MAX_DIFFICULTY - 1 && ok; ++diff)
{
if (_difficultyEntries[diff].find(id1) != _difficultyEntries[diff].end() ||
_difficultyEntries[diff].find(id2) != _difficultyEntries[diff].end() ||
_difficultyEntries[diff].find(id3) != _difficultyEntries[diff].end())
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {}) that is listed as difficulty {} template (Entries: {}, {}, {}) in `creature_template`, skipped.",
spawnId, diff + 1, id1, id2, id3);
ok = false;
}
}
if (!ok)
{
_creatureDataStore.erase(spawnId);
return nullptr;
}
if (creatureData.equipmentId != 0)
{
if (!GetEquipmentInfo(id1, creatureData.equipmentId) ||
(id2 && !GetEquipmentInfo(id2, creatureData.equipmentId)) ||
(id3 && !GetEquipmentInfo(id3, creatureData.equipmentId)))
{
LOG_ERROR("sql.sql", "Table `creature` has creature (Entries: {}, {}, {}) with equipment_id {} not found in table `creature_equip_template`, set to no equipment.",
id1, id2, id3, creatureData.equipmentId);
creatureData.equipmentId = 0;
}
}
if (creatureData.movementType >= MAX_DB_MOTION_TYPE)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {} Entries: {}, {}, {}) with wrong movement generator type ({}), set to IDLE.",
spawnId, id1, id2, id3, creatureData.movementType);
creatureData.movementType = IDLE_MOTION_TYPE;
}
if (creatureData.wander_distance < 0.0f)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {} Entries: {}, {}, {}) with `wander_distance`< 0, set to 0.",
spawnId, id1, id2, id3);
creatureData.wander_distance = 0.0f;
}
else if (creatureData.movementType == RANDOM_MOTION_TYPE)
{
if (creatureData.wander_distance == 0.0f)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {} Entries: {}, {}, {}) with `MovementType`=1 (random movement) but with `wander_distance`=0, replace by idle movement type (0).",
spawnId, id1, id2, id3);
creatureData.movementType = IDLE_MOTION_TYPE;
}
}
else if (creatureData.movementType == IDLE_MOTION_TYPE)
{
if (creatureData.wander_distance != 0.0f)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {} Entries: {}, {}, {}) with `MovementType`=0 (idle) have `wander_distance`<>0, set to 0.",
spawnId, id1, id2, id3);
creatureData.wander_distance = 0.0f;
}
}
if (creatureData.phaseMask == 0)
{
LOG_ERROR("sql.sql", "Table `creature` has creature (SpawnId: {} Entries: {}, {}, {}) with `phaseMask`=0 (not visible for anyone), set to 1.",
spawnId, id1, id2, id3);
creatureData.phaseMask = 1;
}
return &creatureData;
}
void ObjectMgr::LoadCreatureSparring()
{
uint32 oldMSTime = getMSTime();
@@ -2764,6 +2922,128 @@ void ObjectMgr::LoadGameobjects()
LOG_INFO("server.loading", " ");
}
// Loads a single gameobject spawn from DB into the cache.
// GameObject::LoadGameObjectFromDB() reads from cache (GetGameObjectData()), not from DB directly,
// so this must be called first for spawns not loaded at startup.
GameObjectData const* ObjectMgr::LoadGameObjectDataFromDB(ObjectGuid::LowType spawnId)
{
GameObjectData const* data = GetGameObjectData(spawnId);
if (data)
return data;
QueryResult result = WorldDatabase.Query("SELECT gameobject.guid, id, map, position_x, position_y, position_z, orientation, "
"rotation0, rotation1, rotation2, rotation3, spawntimesecs, animprogress, state, spawnMask, phaseMask, "
"ScriptName "
"FROM gameobject WHERE gameobject.guid = {}", spawnId);
if (!result)
return nullptr;
Field* fields = result->Fetch();
uint32 entry = fields[1].Get<uint32>();
GameObjectTemplate const* gInfo = GetGameObjectTemplate(entry);
if (!gInfo)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {}) with non-existing gameobject entry {}, skipped.", spawnId, entry);
return nullptr;
}
if (gInfo->displayId && !sGameObjectDisplayInfoStore.LookupEntry(gInfo->displayId))
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry {} GoType: {}) with an invalid displayId ({}), not loaded.",
spawnId, entry, gInfo->type, gInfo->displayId);
return nullptr;
}
GameObjectData& goData = _gameObjectDataStore[spawnId];
goData.id = entry;
goData.mapid = fields[2].Get<uint16>();
goData.posX = fields[3].Get<float>();
goData.posY = fields[4].Get<float>();
goData.posZ = fields[5].Get<float>();
goData.orientation = fields[6].Get<float>();
goData.rotation.x = fields[7].Get<float>();
goData.rotation.y = fields[8].Get<float>();
goData.rotation.z = fields[9].Get<float>();
goData.rotation.w = fields[10].Get<float>();
goData.spawntimesecs = fields[11].Get<int32>();
goData.animprogress = fields[12].Get<uint8>();
goData.artKit = 0;
goData.ScriptId = GetScriptId(fields[16].Get<std::string>());
if (!goData.ScriptId)
goData.ScriptId = gInfo->ScriptId;
MapEntry const* mapEntry = sMapStore.LookupEntry(goData.mapid);
if (!mapEntry)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) spawned on a non-existing map (Id: {}), skipped.", spawnId, entry, goData.mapid);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (goData.spawntimesecs == 0 && gInfo->IsDespawnAtAction())
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with `spawntimesecs` (0) value, but the gameobject is marked as despawnable at action.", spawnId, entry);
}
uint32 go_state = fields[13].Get<uint8>();
if (go_state >= MAX_GO_STATE)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid `state` ({}) value, skipped.", spawnId, entry, go_state);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
goData.go_state = GOState(go_state);
goData.spawnMask = fields[14].Get<uint8>();
goData.phaseMask = fields[15].Get<uint32>();
if (goData.rotation.x < -1.0f || goData.rotation.x > 1.0f)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid rotationX ({}) value, skipped.", spawnId, entry, goData.rotation.x);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (goData.rotation.y < -1.0f || goData.rotation.y > 1.0f)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid rotationY ({}) value, skipped.", spawnId, entry, goData.rotation.y);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (goData.rotation.z < -1.0f || goData.rotation.z > 1.0f)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid rotationZ ({}) value, skipped.", spawnId, entry, goData.rotation.z);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (goData.rotation.w < -1.0f || goData.rotation.w > 1.0f)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid rotationW ({}) value, skipped.", spawnId, entry, goData.rotation.w);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (!MapMgr::IsValidMapCoord(goData.mapid, goData.posX, goData.posY, goData.posZ, goData.orientation))
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with invalid coordinates, skipped.", spawnId, entry);
_gameObjectDataStore.erase(spawnId);
return nullptr;
}
if (goData.phaseMask == 0)
{
LOG_ERROR("sql.sql", "Table `gameobject` has gameobject (GUID: {} Entry: {}) with `phaseMask`=0 (not visible for anyone), set to 1.", spawnId, entry);
goData.phaseMask = 1;
}
return &goData;
}
void ObjectMgr::AddGameobjectToGrid(ObjectGuid::LowType guid, GameObjectData const* data)
{
uint8 mask = data->spawnMask;

View File

@@ -1226,6 +1226,21 @@ public:
[[nodiscard]] CreatureSparringContainer const& GetSparringData() const { return _creatureSparringStore; }
CreatureData& NewOrExistCreatureData(ObjectGuid::LowType spawnId) { return _creatureDataStore[spawnId]; }
/**
* @brief Loads a single creature spawn entry from the database into the data store cache.
*
* This is needed as a prerequisite for Creature::LoadCreatureFromDB(), which reads
* from the in-memory cache (via GetCreatureData()) rather than querying the DB itself.
* For spawns not loaded during server startup, this method populates the cache so that
* Creature::LoadCreatureFromDB() can then create the live entity.
*
* Returns the cached data if already loaded, or nullptr if the spawn doesn't exist
* or fails validation.
*
* @param spawnId The creature spawn GUID to load.
* @return Pointer to the cached CreatureData, or nullptr on failure.
*/
CreatureData const* LoadCreatureDataFromDB(ObjectGuid::LowType spawnId);
void DeleteCreatureData(ObjectGuid::LowType spawnId);
[[nodiscard]] ObjectGuid GetLinkedRespawnGuid(ObjectGuid guid) const
{
@@ -1311,6 +1326,21 @@ public:
[[nodiscard]] QuestGreeting const* GetQuestGreeting(TypeID type, uint32 id) const;
GameObjectData& NewGOData(ObjectGuid::LowType guid) { return _gameObjectDataStore[guid]; }
/**
* @brief Loads a single gameobject spawn entry from the database into the data store cache.
*
* This is needed as a prerequisite for GameObject::LoadGameObjectFromDB(), which reads
* from the in-memory cache (via GetGameObjectData()) rather than querying the DB itself.
* For spawns not loaded during server startup, this method populates the cache so that
* GameObject::LoadGameObjectFromDB() can then create the live entity.
*
* Returns the cached data if already loaded, or nullptr if the spawn doesn't exist
* or fails validation.
*
* @param spawnId The gameobject spawn GUID to load.
* @return Pointer to the cached GameObjectData, or nullptr on failure.
*/
GameObjectData const* LoadGameObjectDataFromDB(ObjectGuid::LowType spawnId);
void DeleteGOData(ObjectGuid::LowType guid);
[[nodiscard]] ModuleString const* GetModuleString(std::string module, uint32 id) const

View File

@@ -50,6 +50,7 @@ public:
{ "turn", HandleGameObjectTurnCommand, SEC_ADMINISTRATOR, Console::No },
{ "add temp", HandleGameObjectAddTempCommand, SEC_GAMEMASTER, Console::No },
{ "add", HandleGameObjectAddCommand, SEC_ADMINISTRATOR, Console::No },
{ "load", HandleGameObjectLoadCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "set phase", HandleGameObjectSetPhaseCommand, SEC_ADMINISTRATOR, Console::No },
{ "set state", HandleGameObjectSetStateCommand, SEC_ADMINISTRATOR, Console::No },
{ "respawn", HandleGameObjectRespawn, SEC_GAMEMASTER, Console::No }
@@ -144,6 +145,64 @@ public:
return true;
}
static bool HandleGameObjectLoadCommand(ChatHandler* handler, GameObjectSpawnId spawnId)
{
if (!spawnId)
return false;
if (sObjectMgr->GetGameObjectData(spawnId))
{
handler->SendErrorMessage("Gameobject spawn {} is already loaded.", uint32(spawnId));
return false;
}
GameObjectData const* data = sObjectMgr->LoadGameObjectDataFromDB(spawnId);
if (!data)
{
handler->SendErrorMessage("Gameobject spawn {} not found in the database.", uint32(spawnId));
return false;
}
if (sPoolMgr->IsPartOfAPool<GameObject>(spawnId))
{
handler->SendErrorMessage("Gameobject spawn {} is part of a pool and cannot be manually loaded.", uint32(spawnId));
return false;
}
QueryResult eventResult = WorldDatabase.Query("SELECT guid FROM game_event_gameobject WHERE guid = {}", uint32(spawnId));
if (eventResult)
{
handler->SendErrorMessage("Gameobject spawn {} is managed by the game event system and cannot be manually loaded.", uint32(spawnId));
return false;
}
Map* map = sMapMgr->FindBaseNonInstanceMap(data->mapid);
if (!map)
{
handler->SendErrorMessage("Gameobject spawn {} is on a non-continent map (ID: {}). Only continent maps are supported.", uint32(spawnId), data->mapid);
return false;
}
GameObjectTemplate const* objectInfo = sObjectMgr->GetGameObjectTemplate(data->id);
if (!objectInfo)
{
handler->SendErrorMessage("Gameobject template not found for entry {}.", data->id);
return false;
}
GameObject* object = sObjectMgr->IsGameObjectStaticTransport(objectInfo->entry) ? new StaticTransport() : new GameObject();
if (!object->LoadGameObjectFromDB(spawnId, map, true))
{
delete object;
handler->SendErrorMessage("Failed to load gameobject spawn {}.", uint32(spawnId));
return false;
}
sObjectMgr->AddGameobjectToGrid(spawnId, data);
handler->PSendSysMessage("Gameobject spawn {} loaded successfully.", uint32(spawnId));
return true;
}
// add go, temp only
static bool HandleGameObjectAddTempCommand(ChatHandler* handler, GameObjectEntry objectId, Optional<uint64> spawntime)
{

View File

@@ -21,9 +21,11 @@
#include "CreatureGroups.h"
#include "GameTime.h"
#include "Language.h"
#include "MapMgr.h"
#include "ObjectMgr.h"
#include "Pet.h"
#include "Player.h"
#include "PoolMgr.h"
#include "TargetedMovementGenerator.h" // for HandleNpcUnFollowCommand
#include "Transport.h"
#include <string>
@@ -192,6 +194,7 @@ public:
{ "add", npcAddCommandTable },
{ "delete", npcDeleteCommandTable },
{ "follow", npcFollowCommandTable },
{ "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, Console::Yes },
{ "set", npcSetCommandTable }
};
static ChatCommandTable commandTable =
@@ -260,6 +263,57 @@ public:
return true;
}
static bool HandleNpcLoadCommand(ChatHandler* handler, CreatureSpawnId spawnId)
{
if (!spawnId)
return false;
if (sObjectMgr->GetCreatureData(spawnId))
{
handler->SendErrorMessage("Creature spawn {} is already loaded.", uint32(spawnId));
return false;
}
CreatureData const* data = sObjectMgr->LoadCreatureDataFromDB(spawnId);
if (!data)
{
handler->SendErrorMessage("Creature spawn {} not found in the database.", uint32(spawnId));
return false;
}
if (sPoolMgr->IsPartOfAPool<Creature>(spawnId))
{
handler->SendErrorMessage("Creature spawn {} is part of a pool and cannot be manually loaded.", uint32(spawnId));
return false;
}
QueryResult eventResult = WorldDatabase.Query("SELECT guid FROM game_event_creature WHERE guid = {}", uint32(spawnId));
if (eventResult)
{
handler->SendErrorMessage("Creature spawn {} is managed by the game event system and cannot be manually loaded.", uint32(spawnId));
return false;
}
Map* map = sMapMgr->FindBaseNonInstanceMap(data->mapid);
if (!map)
{
handler->SendErrorMessage("Creature spawn {} is on a non-continent map (ID: {}). Only continent maps are supported.", uint32(spawnId), data->mapid);
return false;
}
Creature* creature = new Creature();
if (!creature->LoadCreatureFromDB(spawnId, map, true, true))
{
delete creature;
handler->SendErrorMessage("Failed to load creature spawn {}.", uint32(spawnId));
return false;
}
sObjectMgr->AddCreatureToGrid(spawnId, data);
handler->PSendSysMessage("Creature spawn {} loaded successfully.", uint32(spawnId));
return true;
}
//add item in vendorlist
static bool HandleNpcAddVendorItemCommand(ChatHandler* handler, ItemTemplate const* item, Optional<uint32> mc, Optional<uint32> it, Optional<uint32> ec, Optional<bool> addMulti)
{