diff --git a/data/sql/updates/pending_db_world/rev_1738958242619.sql b/data/sql/updates/pending_db_world/rev_1738958242619.sql new file mode 100644 index 000000000..d62e49c51 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1738958242619.sql @@ -0,0 +1,5 @@ +-- +DELETE FROM `command` WHERE `name` IN ('npc load', 'gobject load'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('npc load', 3, 'Syntax: .npc load #spawnId\nLoad a creature spawn from the database into the world by its GUID.'), +('gobject load', 3, 'Syntax: .gobject load #spawnId\nLoad a gameobject spawn from the database into the world by its GUID.'); diff --git a/src/server/game/Globals/ObjectMgr.cpp b/src/server/game/Globals/ObjectMgr.cpp index 977d5436d..70164e6d9 100644 --- a/src/server/game/Globals/ObjectMgr.cpp +++ b/src/server/game/Globals/ObjectMgr.cpp @@ -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 id2 = fields[2].Get(); + uint32 id3 = fields[3].Get(); + + 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(); + creatureData.equipmentId = fields[5].Get(); + creatureData.posX = fields[6].Get(); + creatureData.posY = fields[7].Get(); + creatureData.posZ = fields[8].Get(); + creatureData.orientation = fields[9].Get(); + creatureData.spawntimesecs = fields[10].Get(); + creatureData.wander_distance = fields[11].Get(); + creatureData.currentwaypoint = fields[12].Get(); + creatureData.curhealth = fields[13].Get(); + creatureData.curmana = fields[14].Get(); + creatureData.movementType = fields[15].Get(); + creatureData.spawnMask = fields[16].Get(); + creatureData.phaseMask = fields[17].Get(); + creatureData.npcflag = fields[18].Get(); + creatureData.unit_flags = fields[19].Get(); + creatureData.dynamicflags = fields[20].Get(); + creatureData.ScriptId = GetScriptId(fields[21].Get()); + + 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(); + + 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(); + goData.posX = fields[3].Get(); + goData.posY = fields[4].Get(); + goData.posZ = fields[5].Get(); + goData.orientation = fields[6].Get(); + goData.rotation.x = fields[7].Get(); + goData.rotation.y = fields[8].Get(); + goData.rotation.z = fields[9].Get(); + goData.rotation.w = fields[10].Get(); + goData.spawntimesecs = fields[11].Get(); + goData.animprogress = fields[12].Get(); + goData.artKit = 0; + goData.ScriptId = GetScriptId(fields[16].Get()); + + 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(); + 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(); + goData.phaseMask = fields[15].Get(); + + 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; diff --git a/src/server/game/Globals/ObjectMgr.h b/src/server/game/Globals/ObjectMgr.h index 4ac1e33b6..d9f458293 100644 --- a/src/server/game/Globals/ObjectMgr.h +++ b/src/server/game/Globals/ObjectMgr.h @@ -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 diff --git a/src/server/scripts/Commands/cs_gobject.cpp b/src/server/scripts/Commands/cs_gobject.cpp index e41d93d07..46db7cf4c 100644 --- a/src/server/scripts/Commands/cs_gobject.cpp +++ b/src/server/scripts/Commands/cs_gobject.cpp @@ -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(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 spawntime) { diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 66c50537e..e933d4be1 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -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 @@ -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(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 mc, Optional it, Optional ec, Optional addMulti) {