refactor(Core/DB): normalize creature table by extracting multi-ID spawns#25197
refactor(Core/DB): normalize creature table by extracting multi-ID spawns#25197Nyeriah wants to merge 6 commits intoazerothcore:masterfrom
Conversation
…awns Move the rarely-used id2/id3 columns from the creature table into a new creature_multispawn table, and rename id1 back to id. Only ~737 out of 5.3M+ spawns use multiple entries, so this eliminates two wasted columns on 99.99% of rows while preserving full functionality. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Normalizes creature spawns by renaming creature.id1 back to id, moving multi-entry spawn variants into a new creature_multispawn table, and updating server code to load/consume the new schema while keeping existing random-entry behavior.
Changes:
- Renamed
CreatureData::id1→idand updated references across scripts/commands/core. - Updated creature loading to read base entry from
creature.idand load variant entries fromcreature_multispawnintoid2/id3. - Added SQL migration creating
creature_multispawn, migratingid2/id3data, and dropping old columns fromcreature.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/scripts/Northrend/IcecrownCitadel/instance_icecrown_citadel.cpp | Switch spawn entry lookup from id1 to id. |
| src/server/scripts/Northrend/IcecrownCitadel/icecrown_citadel.cpp | Update script logic to use CreatureData::id. |
| src/server/scripts/Northrend/FrozenHalls/PitOfSaron/instance_pit_of_saron.cpp | Switch spawn entry lookup from id1 to id. |
| src/server/scripts/Kalimdor/TempleOfAhnQiraj/temple_of_ahnqiraj.cpp | Use CreatureData::id when building HighGuid. |
| src/server/scripts/Commands/cs_wp.cpp | Update prepared statement argument count for new query. |
| src/server/scripts/Commands/cs_tele.cpp | Use id in in-memory filtering and SQL query joins. |
| src/server/scripts/Commands/cs_npc.cpp | Spawn creation uses data.id; adjust DB query field indices after schema change. |
| src/server/scripts/Commands/cs_go.cpp | Use CreatureData::id for entry matching. |
| src/server/game/OutdoorPvP/OutdoorPvP.cpp | Use CreatureData::id when resolving capture point spawns. |
| src/server/game/Maps/ZoneScript.h | Default entry resolver now returns data->id. |
| src/server/game/Handlers/AuctionHouseHandler.cpp | Auctioneer template lookup uses CreatureData::id. |
| src/server/game/Globals/ObjectMgr.cpp | Load creatures from new schema; add loading of variants from creature_multispawn. |
| src/server/game/Events/GameEventMgr.cpp | Update event vendor/model equip loading for new creature schema. |
| src/server/game/Entities/Creature/CreatureData.h | Rename id1 to id; annotate id2/id3 as multispawn variants. |
| src/server/game/Entities/Creature/Creature.cpp | Save/load/respawn logic updated to use data->id as primary template ID. |
| src/server/game/Conditions/ConditionMgr.cpp | Condition validation now compares against CreatureData::id. |
| src/server/game/AI/SmartScripts/SmartScriptMgr.cpp | SAI validation uses CreatureData::id. |
| src/server/database/Database/Implementation/WorldDatabase.cpp | Update prepared statements to match new creature schema + multispawn lookup. |
| data/sql/updates/pending_db_world/rev_8337814054007982.sql | Introduce creature_multispawn and migrate/drop old repeating columns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| INSERT INTO `creature_multispawn` (`spawnId`, `entry`) | ||
| SELECT `guid`, `id2` FROM `creature` WHERE `id2` != 0; | ||
|
|
||
| -- Migrate id3 entries | ||
| INSERT INTO `creature_multispawn` (`spawnId`, `entry`) |
There was a problem hiding this comment.
These inserts can fail and abort the migration if duplicates exist (e.g., id2 == id3 for a spawn, or repeated values across runs), because (spawnId, entry) is the primary key. Use INSERT IGNORE / ON DUPLICATE KEY handling, or filter duplicates in the SELECT (e.g., exclude id3 rows where id3 = id2) to make the migration idempotent and robust.
| INSERT INTO `creature_multispawn` (`spawnId`, `entry`) | |
| SELECT `guid`, `id2` FROM `creature` WHERE `id2` != 0; | |
| -- Migrate id3 entries | |
| INSERT INTO `creature_multispawn` (`spawnId`, `entry`) | |
| INSERT IGNORE INTO `creature_multispawn` (`spawnId`, `entry`) | |
| SELECT `guid`, `id2` FROM `creature` WHERE `id2` != 0; | |
| -- Migrate id3 entries | |
| INSERT IGNORE INTO `creature_multispawn` (`spawnId`, `entry`) |
There was a problem hiding this comment.
Fixed — using INSERT IGNORE now to handle potential id2 == id3 duplicates safely.
| // Load alternate entries from creature_multispawn | ||
| QueryResult variantResult = WorldDatabase.Query("SELECT entry FROM creature_multispawn WHERE spawnId = {}", spawnId); | ||
| if (variantResult) | ||
| { | ||
| do | ||
| { | ||
| uint32 variantEntry = variantResult->Fetch()[0].Get<uint32>(); | ||
| if (!GetCreatureTemplate(variantEntry)) | ||
| { | ||
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has creature (SpawnId: {}) with non-existing entry {}, skipped.", spawnId, variantEntry); | ||
| continue; | ||
| } | ||
| if (!creatureData.id2) | ||
| creatureData.id2 = variantEntry; | ||
| else if (!creatureData.id3) | ||
| creatureData.id3 = variantEntry; | ||
| } while (variantResult->NextRow()); | ||
| } |
There was a problem hiding this comment.
The lazy-load path (LoadCreatureDataFromDB) diverges from the main bulk load (LoadCreatures) validation/behavior: it does not check difficulty-entry constraints, does not validate equipment compatibility, and does not detect/log >2 variants (it silently ignores extras after filling id2/id3). This can lead to inconsistent runtime behavior depending on whether a spawn was loaded via bulk load or on-demand. Consider reusing the same validation logic as LoadCreatures (including logging when >2 variants exist) and making the variant row order deterministic (e.g., ORDER BY entry) so id2/id3 assignment is stable.
There was a problem hiding this comment.
Fixed — LoadCreatureDataFromDB now has the same validation as the bulk loader: difficulty entry checks, equipment validation, overflow logging, and ORDER BY entry for deterministic assignment.
| CreatureData* data = const_cast<CreatureData*>(GetCreatureData(spawnId)); | ||
| if (!data) | ||
| { | ||
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has entry for non-existing creature spawn (SpawnId: {}), skipped.", spawnId); | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
Avoid const_cast here. Since variants are being applied during the load phase, prefer retrieving a mutable reference/pointer directly from _creatureDataStore (e.g., via find(spawnId) and modifying the stored CreatureData). This removes const-correctness hazards and makes it clearer that the data is intentionally mutable in this context.
| CreatureData* data = const_cast<CreatureData*>(GetCreatureData(spawnId)); | |
| if (!data) | |
| { | |
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has entry for non-existing creature spawn (SpawnId: {}), skipped.", spawnId); | |
| continue; | |
| } | |
| auto creatureDataIt = _creatureDataStore.find(spawnId); | |
| if (creatureDataIt == _creatureDataStore.end()) | |
| { | |
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has entry for non-existing creature spawn (SpawnId: {}), skipped.", spawnId); | |
| continue; | |
| } | |
| CreatureData* data = &creatureDataIt->second; |
There was a problem hiding this comment.
Fixed — using _creatureDataStore.find() directly now, no more const_cast.
| PrepareStatement(WORLD_SEL_WAYPOINT_SCRIPT_BY_ID, "SELECT guid, delay, command, datalong, datalong2, dataint, x, y, z, o FROM waypoint_scripts WHERE id = ?", CONNECTION_SYNCH); | ||
| PrepareStatement(WORLD_SEL_ITEM_TEMPLATE_BY_NAME, "SELECT entry FROM item_template WHERE name = ?", CONNECTION_SYNCH); | ||
| PrepareStatement(WORLD_SEL_CREATURE_BY_ID, "SELECT guid FROM creature WHERE id1 = ? OR id2 = ? OR id3 = ?", CONNECTION_SYNCH); | ||
| PrepareStatement(WORLD_SEL_CREATURE_BY_ID, "SELECT guid FROM creature WHERE id = ? UNION SELECT spawnId FROM creature_multispawn WHERE entry = ?", CONNECTION_SYNCH); |
There was a problem hiding this comment.
In the UNION, the second SELECT returns spawnId without aliasing it to match the first column name. While most consumers fetch by index, aliasing improves clarity and avoids subtle issues in any code path that relies on column names. Consider: ... UNION SELECT spawnId AS guid ....
There was a problem hiding this comment.
Fixed — aliased as spawnId AS guid.
| if (!data->id2) | ||
| data->id2 = entry; | ||
| else if (!data->id3) | ||
| data->id3 = entry; | ||
| else | ||
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has more than 2 variant entries for creature (SpawnId: {}), extra entry {} skipped.", spawnId, entry); | ||
|
|
||
| ++variantCount; |
There was a problem hiding this comment.
variantCount is incremented even when the variant entry is not actually stored (e.g., when a spawn already has both id2 and id3 and the entry is skipped). Increment only when an entry is successfully assigned to id2/id3 to keep the load log accurate.
| if (!data->id2) | |
| data->id2 = entry; | |
| else if (!data->id3) | |
| data->id3 = entry; | |
| else | |
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has more than 2 variant entries for creature (SpawnId: {}), extra entry {} skipped.", spawnId, entry); | |
| ++variantCount; | |
| bool stored = false; | |
| if (!data->id2) | |
| { | |
| data->id2 = entry; | |
| stored = true; | |
| } | |
| else if (!data->id3) | |
| { | |
| data->id3 = entry; | |
| stored = true; | |
| } | |
| else | |
| { | |
| LOG_ERROR("sql.sql", "Table `creature_multispawn` has more than 2 variant entries for creature (SpawnId: {}), extra entry {} skipped.", spawnId, entry); | |
| } | |
| if (stored) | |
| ++variantCount; |
There was a problem hiding this comment.
Fixed — variantCount now only increments when an entry is actually stored in id2/id3.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use INSERT IGNORE for migration to handle id2==id3 duplicates - Remove const_cast, use _creatureDataStore.find() directly - Only increment variantCount when entry is actually stored - Add validation parity to LoadCreatureDataFromDB (difficulty, equipment, overflow) - Add ORDER BY entry for deterministic id2/id3 assignment - Alias spawnId AS guid in UNION query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Will Claude provide fixes for the 100 broken modules 🫣 |
|
The change is a simple rename ( |
|
Only one module in the CI suite is affected — |
|
Alright, currently module ci is failing on build already, so it's not showing the failing sql files yet |
|
Module SQL fix PRs for the
|
|
Module fix PRs for removed
|
Changes Proposed:
This PR normalizes the
creaturetable by extracting the rarely-used multi-ID spawning system into a dedicated table.Database Normalization
The
creaturetable currently stores three creature template entry columns:id1,id2, andid3. This was introduced in PR #10115 to support spawn points that randomly pick between multiple creature templates on spawn/respawn.However, only ~737 out of 5.3M+ creature spawns (~0.014%) actually use
id2orid3. The remaining 99.99% of rows carry two permanently-zero columns, violating database normalization principles (specifically 1NF — the columns represent a repeating group of the same attribute type).This PR:
creature_multispawntable with a composite primary key(spawnId, entry)to store alternate creature template entries for the rare spawns that need themid1back toidin thecreaturetable, restoring the original column nameid2andid3from thecreaturetableid2/id3values are inserted intocreature_multispawnbefore the columns are droppedThe C++
CreatureDatastruct retainsid2/id3fields internally, populated from the new table during loading, so all downstream spawn logic (GetRandomId, respawn entry selection, etc.) remains unchanged.AI-assisted Pull Requests
Important
While the use of AI tools when preparing pull requests is not prohibited, contributors must clearly disclose when such tools have been used and specify the model involved.
Contributors are also expected to fully understand the changes they are submitting and must be able to explain and justify those changes when requested by maintainers.
Issues Addressed:
N/A — this is a schema improvement.
SOURCE:
The changes have been validated through:
Tests Performed:
This PR has been:
How to Test the Changes:
id2/id3values still randomly pick between entries on spawn and respawn.npc infoon both single-entry and multi-entry spawns.npc addto verify new creature spawns work correctlyKnown Issues and TODO List:
id1,id2,id3columns will need updatesHow to Test AzerothCore PRs
When a PR is ready to be tested, it will be marked as [WAITING TO BE TESTED].
You can help by testing PRs and writing your feedback here on the PR's page on GitHub. Follow the instructions here:
http://www.azerothcore.org/wiki/How-to-test-a-PR
REMEMBER: when testing a PR that changes something generic (i.e. a part of code that handles more than one specific thing), the tester should not only check that the PR does its job (e.g. fixing spell XXX) but especially check that the PR does not cause any regression (i.e. introducing new bugs).
For example: if a PR fixes spell X by changing a part of code that handles spells X, Y, and Z, we should not only test X, but we should test Y and Z as well.