feat(Core/Scripts): Add gameobject_summon_groups with quaternion rotation support (#24708)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-02-15 15:05:00 -06:00
committed by GitHub
parent dcafad1a41
commit ce74c0b19c
12 changed files with 342 additions and 1 deletions

View File

@@ -3332,6 +3332,14 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u
}
break;
}
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP:
{
if (!GetBaseObject())
break;
GetBaseObject()->SummonGameObjectGroup(e.action.gameobjectGroup.group);
break;
}
default:
LOG_ERROR("sql.sql", "SmartScript::ProcessAction: Entry {} SourceType {}, Event {}, Unhandled Action type {}", e.entryOrGuid, e.GetScriptType(), e.event_id, e.GetActionType());
break;

View File

@@ -886,6 +886,7 @@ bool SmartAIMgr::CheckUnusedActionParams(SmartScriptHolder const& e)
case SMART_ACTION_DISABLE_REWARD: return sizeof(SmartAction::reward);
case SMART_ACTION_SET_ANIM_TIER: return sizeof(SmartAction::animTier);
case SMART_ACTION_SET_GOSSIP_MENU: return sizeof(SmartAction::setGossipMenu);
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP: return sizeof(SmartAction::gameobjectGroup);
case SMART_ACTION_DISMOUNT: return NO_PARAMS;
default:
LOG_WARN("sql.sql", "SmartAIMgr: entryorguid {} source_type {} id {} action_type {} is using an action with no unused params specified in SmartAIMgr::CheckUnusedActionParams(), please report this.",
@@ -2042,6 +2043,7 @@ bool SmartAIMgr::IsEventValid(SmartScriptHolder& e)
case SMART_ACTION_MOVEMENT_RESUME:
case SMART_ACTION_WORLD_SCRIPT:
case SMART_ACTION_SET_GOSSIP_MENU:
case SMART_ACTION_SUMMON_GAMEOBJECT_GROUP:
break;
default:
LOG_ERROR("sql.sql", "SmartAIMgr: Not handled action_type({}), event_type({}), Entry {} SourceType {} Event {}, skipped.", e.GetActionType(), e.GetEventType(), e.entryOrGuid, e.GetScriptType(), e.event_id);

View File

@@ -725,8 +725,9 @@ enum SMART_ACTION
SMART_ACTION_DISABLE_REWARD = 238, // reputation 0/1, loot 0/1
SMART_ACTION_SET_ANIM_TIER = 239, // animtier
SMART_ACTION_SET_GOSSIP_MENU = 240, // gossipMenuId
SMART_ACTION_SUMMON_GAMEOBJECT_GROUP = 241, // group
SMART_ACTION_AC_END = 241, // placeholder
SMART_ACTION_AC_END = 242, // placeholder
};
enum class SmartActionSummonCreatureFlags
@@ -1517,6 +1518,11 @@ struct SmartAction
{
uint32 gossipMenuId;
} setGossipMenu;
struct
{
uint32 group;
} gameobjectGroup;
};
};

View File

@@ -19,6 +19,7 @@
#define AZEROTHCORE_TEMPSUMMON_H
#include "Creature.h"
#include "G3D/Quat.h"
enum SummonerType
{
@@ -36,6 +37,15 @@ struct TempSummonData
uint32 time; ///< Despawn time, usable only with certain temp summon types
};
/// Stores data for temp gameobject summons
struct GameObjectSummonData
{
uint32 entry;
Position pos;
G3D::Quat rot;
uint32 respawnTime; ///< Duration in seconds; passed to SummonGameObject's respawnTime parameter
};
class TempSummon : public Creature
{
public:

View File

@@ -2284,6 +2284,18 @@ void Map::SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list /*= null
list->push_back(summon);
}
void Map::SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list /*= nullptr*/)
{
std::vector<GameObjectSummonData> const* data = sObjectMgr->GetGameObjectSummonGroup(GetId(), SUMMONER_TYPE_MAP, group);
if (!data)
return;
for (std::vector<GameObjectSummonData>::const_iterator itr = data->begin(); itr != data->end(); ++itr)
if (GameObject* go = SummonGameObject(itr->entry, itr->pos.GetPositionX(), itr->pos.GetPositionY(), itr->pos.GetPositionZ(), itr->pos.GetOrientation(), itr->rot.x, itr->rot.y, itr->rot.z, itr->rot.w, itr->respawnTime))
if (list)
list->push_back(go);
}
TempSummon* WorldObject::SummonCreature(uint32 id, float x, float y, float z, float ang, TempSummonType spwtype, uint32 despwtime, SummonPropertiesEntry const* properties, bool visibleBySummonerOnly)
{
if (!x && !y && !z)
@@ -2440,6 +2452,20 @@ void WorldObject::SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list
list->push_back(summon);
}
void WorldObject::SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list /*= nullptr*/)
{
ASSERT((IsGameObject() || IsCreature()) && "Only GOs and creatures can summon gameobject groups!");
std::vector<GameObjectSummonData> const* data = sObjectMgr->GetGameObjectSummonGroup(GetEntry(), IsGameObject() ? SUMMONER_TYPE_GAMEOBJECT : SUMMONER_TYPE_CREATURE, group);
if (!data)
return;
for (std::vector<GameObjectSummonData>::const_iterator itr = data->begin(); itr != data->end(); ++itr)
if (GameObject* go = SummonGameObject(itr->entry, itr->pos.GetPositionX(), itr->pos.GetPositionY(), itr->pos.GetPositionZ(), itr->pos.GetOrientation(), itr->rot.x, itr->rot.y, itr->rot.z, itr->rot.w, itr->respawnTime))
if (list)
list->push_back(go);
}
Creature* WorldObject::FindNearestCreature(uint32 entry, float range, bool alive) const
{
Creature* creature = nullptr;

View File

@@ -630,6 +630,7 @@ public:
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, float rotation0, float rotation1, float rotation2, float rotation3, uint32 respawnTime, bool checkTransport = true, GOSummonType summonType = GO_SUMMON_TIMED_OR_CORPSE_DESPAWN);
Creature* SummonTrigger(float x, float y, float z, float ang, uint32 dur, bool setLevel = false, CreatureAI * (*GetAI)(Creature*) = nullptr);
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
void SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list = nullptr);
[[nodiscard]] Creature* FindNearestCreature(uint32 entry, float range, bool alive = true) const;
[[nodiscard]] GameObject* FindNearestGameObject(uint32 entry, float range, bool onlySpawned = false) const;

View File

@@ -2224,6 +2224,87 @@ void ObjectMgr::LoadTempSummons()
LOG_INFO("server.loading", " ");
}
void ObjectMgr::LoadGameObjectSummons()
{
uint32 oldMSTime = getMSTime();
_goSummonDataStore.clear();
// 0 1 2 3 4 5 6 7 8 9 10 11 12
QueryResult result = WorldDatabase.Query("SELECT summonerId, summonerType, groupId, entry, position_x, position_y, position_z, orientation, rotation0, rotation1, rotation2, rotation3, respawnTime FROM gameobject_summon_groups");
if (!result)
{
LOG_WARN("server.loading", ">> Loaded 0 gameobject summons. DB table `gameobject_summon_groups` is empty.");
return;
}
uint32 count = 0;
do
{
Field* fields = result->Fetch();
uint32 summonerId = fields[0].Get<uint32>();
SummonerType summonerType = SummonerType(fields[1].Get<uint8>());
uint8 group = fields[2].Get<uint8>();
switch (summonerType)
{
case SUMMONER_TYPE_CREATURE:
if (!GetCreatureTemplate(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for creature summoner type, skipped.", summonerId);
continue;
}
break;
case SUMMONER_TYPE_GAMEOBJECT:
if (!GetGameObjectTemplate(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for gameobject summoner type, skipped.", summonerId);
continue;
}
break;
case SUMMONER_TYPE_MAP:
if (!sMapStore.LookupEntry(summonerId))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has summoner with non existing entry {} for map summoner type, skipped.", summonerId);
continue;
}
break;
default:
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has unhandled summoner type {} for summoner {}, skipped.", summonerType, summonerId);
continue;
}
GameObjectSummonData data;
data.entry = fields[3].Get<uint32>();
if (!GetGameObjectTemplate(data.entry))
{
LOG_ERROR("sql.sql", "Table `gameobject_summon_groups` has gameobject in group [Summoner ID: {}, Summoner Type: {}, Group ID: {}] with non existing gameobject entry {}, skipped.", summonerId, summonerType, group, data.entry);
continue;
}
float posX = fields[4].Get<float>();
float posY = fields[5].Get<float>();
float posZ = fields[6].Get<float>();
float orientation = fields[7].Get<float>();
data.pos.Relocate(posX, posY, posZ, orientation);
data.rot = G3D::Quat(fields[8].Get<float>(), fields[9].Get<float>(), fields[10].Get<float>(), fields[11].Get<float>());
data.respawnTime = fields[12].Get<uint32>();
TempSummonGroupKey key(summonerId, summonerType, group);
_goSummonDataStore[key].push_back(data);
++count;
} while (result->NextRow());
LOG_INFO("server.loading", ">> Loaded {} Gameobject Summons in {} ms", count, GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
}
void ObjectMgr::LoadCreatures()
{
uint32 oldMSTime = getMSTime();

View File

@@ -507,6 +507,7 @@ typedef std::map<ObjectGuid, ObjectGuid> LinkedRespawnContainer;
typedef std::unordered_map<ObjectGuid::LowType, CreatureData> CreatureDataContainer;
typedef std::unordered_map<ObjectGuid::LowType, GameObjectData> GameObjectDataContainer;
typedef std::map<TempSummonGroupKey, std::vector<TempSummonData> > TempSummonDataContainer;
typedef std::map<TempSummonGroupKey, std::vector<GameObjectSummonData> > GameObjectSummonDataContainer;
typedef std::unordered_map<uint32, CreatureLocale> CreatureLocaleContainer;
typedef std::unordered_map<uint32, GameObjectLocale> GameObjectLocaleContainer;
typedef std::unordered_map<uint32, ItemLocale> ItemLocaleContainer;
@@ -1036,6 +1037,7 @@ public:
void LoadGameObjectQuestItems();
void LoadCreatureQuestItems();
void LoadTempSummons();
void LoadGameObjectSummons();
void LoadCreatures();
void LoadCreatureSparring();
void LoadLinkedRespawn();
@@ -1208,6 +1210,15 @@ public:
return nullptr;
}
[[nodiscard]] std::vector<GameObjectSummonData> const* GetGameObjectSummonGroup(uint32 summonerId, SummonerType summonerType, uint8 group) const
{
GameObjectSummonDataContainer::const_iterator itr = _goSummonDataStore.find(TempSummonGroupKey(summonerId, summonerType, group));
if (itr != _goSummonDataStore.end())
return &itr->second;
return nullptr;
}
[[nodiscard]] BroadcastText const* GetBroadcastText(uint32 id) const
{
BroadcastTextContainer::const_iterator itr = _broadcastTextStore.find(id);
@@ -1635,6 +1646,8 @@ private:
GameObjectTemplateAddonContainer _gameObjectTemplateAddonStore;
/// Stores temp summon data grouped by summoner's entry, summoner's type and group id
TempSummonDataContainer _tempSummonDataStore;
/// Stores gameobject summon data grouped by summoner's entry, summoner's type and group id
GameObjectSummonDataContainer _goSummonDataStore;
BroadcastTextContainer _broadcastTextStore;
ItemTemplateContainer _itemTemplateStore;

View File

@@ -337,6 +337,7 @@ public:
GameObject* SummonGameObject(uint32 entry, float x, float y, float z, float ang, float rotation0, float rotation1, float rotation2, float rotation3, uint32 respawnTime, bool checkTransport = true);
GameObject* SummonGameObject(uint32 entry, Position const& pos, float rotation0 = 0.0f, float rotation1 = 0.0f, float rotation2 = 0.0f, float rotation3 = 0.0f, uint32 respawnTime = 100, bool checkTransport = true);
void SummonCreatureGroup(uint8 group, std::list<TempSummon*>* list = nullptr);
void SummonGameObjectGroup(uint8 group, std::list<GameObject*>* list = nullptr);
Corpse* GetCorpse(ObjectGuid const& guid);
Creature* GetCreature(ObjectGuid const& guid);

View File

@@ -570,6 +570,9 @@ void World::SetInitialWorldSettings()
LOG_INFO("server.loading", "Loading Temporary Summon Data...");
sObjectMgr->LoadTempSummons(); // must be after LoadCreatureTemplates() and LoadGameObjectTemplates()
LOG_INFO("server.loading", "Loading Gameobject Summon Data...");
sObjectMgr->LoadGameObjectSummons(); // must be after LoadCreatureTemplates() and LoadGameObjectTemplates()
LOG_INFO("server.loading", "Loading Pet Levelup Spells...");
sSpellMgr->LoadPetLevelupSpellMap();

View File

@@ -0,0 +1,141 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ObjectMgr.h"
#include "SmartScriptMgr.h"
#include "TemporarySummon.h"
#include "WorldMock.h"
#include "gtest/gtest.h"
class GameObjectSummonGroupTest : public ::testing::Test
{
protected:
void SetUp() override
{
_previousWorld = std::move(sWorld);
auto* worldMock =
new ::testing::NiceMock<WorldMock>();
ON_CALL(*worldMock, getIntConfig(::testing::_))
.WillByDefault(::testing::Return(0));
sWorld.reset(worldMock);
}
void TearDown() override
{
sWorld = std::move(_previousWorld);
}
std::unique_ptr<IWorld> _previousWorld;
};
TEST_F(GameObjectSummonGroupTest, DataStructStoresFields)
{
GameObjectSummonData data;
data.entry = 2332;
data.pos.Relocate(-14652.38f, 146.51f, 3.50f, 0.35f);
data.rot = G3D::Quat(0.0f, 0.0f, 0.17f, 0.98f);
data.respawnTime = 120;
EXPECT_EQ(data.entry, 2332u);
EXPECT_FLOAT_EQ(data.pos.GetPositionX(), -14652.38f);
EXPECT_FLOAT_EQ(data.pos.GetPositionY(), 146.51f);
EXPECT_FLOAT_EQ(data.pos.GetPositionZ(), 3.50f);
EXPECT_FLOAT_EQ(data.pos.GetOrientation(), 0.35f);
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
EXPECT_FLOAT_EQ(data.rot.z, 0.17f);
EXPECT_FLOAT_EQ(data.rot.w, 0.98f);
EXPECT_EQ(data.respawnTime, 120u);
}
TEST_F(GameObjectSummonGroupTest, QuaternionIdentity)
{
GameObjectSummonData data;
data.rot = G3D::Quat(0.0f, 0.0f, 0.0f, 1.0f);
EXPECT_FLOAT_EQ(data.rot.x, 0.0f);
EXPECT_FLOAT_EQ(data.rot.y, 0.0f);
EXPECT_FLOAT_EQ(data.rot.z, 0.0f);
EXPECT_FLOAT_EQ(data.rot.w, 1.0f);
}
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForMissing)
{
auto const* result = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_CREATURE, 0);
EXPECT_EQ(result, nullptr);
}
TEST_F(GameObjectSummonGroupTest, AccessorReturnsNullForAllTypes)
{
auto const* r1 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_CREATURE, 0);
auto const* r2 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_GAMEOBJECT, 0);
auto const* r3 = sObjectMgr->GetGameObjectSummonGroup(
99999, SUMMONER_TYPE_MAP, 0);
EXPECT_EQ(r1, nullptr);
EXPECT_EQ(r2, nullptr);
EXPECT_EQ(r3, nullptr);
}
TEST_F(GameObjectSummonGroupTest, DifferentGroupsAreIndependent)
{
auto const* g0 = sObjectMgr->GetGameObjectSummonGroup(
2289, SUMMONER_TYPE_GAMEOBJECT, 0);
auto const* g1 = sObjectMgr->GetGameObjectSummonGroup(
2289, SUMMONER_TYPE_GAMEOBJECT, 1);
// Both should be null since DB isn't loaded in tests,
// but they should be independent lookups
EXPECT_EQ(g0, nullptr);
EXPECT_EQ(g1, nullptr);
}
TEST_F(GameObjectSummonGroupTest, SmartActionEnumValue)
{
EXPECT_EQ(SMART_ACTION_SUMMON_GAMEOBJECT_GROUP, 241);
EXPECT_EQ(SMART_ACTION_AC_END, 242);
}
TEST_F(GameObjectSummonGroupTest, SmartActionUnionSize)
{
SmartAction action{};
action.gameobjectGroup.group = 5;
EXPECT_EQ(action.gameobjectGroup.group, 5u);
}
TEST_F(GameObjectSummonGroupTest, TempSummonGroupKeyOrdering)
{
TempSummonGroupKey k1(100, SUMMONER_TYPE_CREATURE, 0);
TempSummonGroupKey k2(100, SUMMONER_TYPE_GAMEOBJECT, 0);
TempSummonGroupKey k3(100, SUMMONER_TYPE_CREATURE, 1);
TempSummonGroupKey k4(200, SUMMONER_TYPE_CREATURE, 0);
// std::tuple ordering: summoner ID first, then type, then group
EXPECT_LT(k1, k2); // same id, creature < gameobject
EXPECT_LT(k1, k3); // same id+type, group 0 < 1
EXPECT_LT(k1, k4); // id 100 < 200
}
TEST_F(GameObjectSummonGroupTest, SummonerTypeValues)
{
EXPECT_EQ(SUMMONER_TYPE_CREATURE, 0);
EXPECT_EQ(SUMMONER_TYPE_GAMEOBJECT, 1);
EXPECT_EQ(SUMMONER_TYPE_MAP, 2);
}