Skip to content

refactor(Core/DB): normalize creature table by extracting multi-ID spawns#25197

Open
Nyeriah wants to merge 6 commits intoazerothcore:masterfrom
Nyeriah:multispawn
Open

refactor(Core/DB): normalize creature table by extracting multi-ID spawns#25197
Nyeriah wants to merge 6 commits intoazerothcore:masterfrom
Nyeriah:multispawn

Conversation

@Nyeriah
Copy link
Copy Markdown
Member

@Nyeriah Nyeriah commented Mar 23, 2026

Changes Proposed:

This PR normalizes the creature table by extracting the rarely-used multi-ID spawning system into a dedicated table.

  • Core (units, players, creatures, game systems).
  • Scripts (bosses, spell scripts, creature scripts).
  • Database (SAI, creatures, etc).

Database Normalization

The creature table currently stores three creature template entry columns: id1, id2, and id3. 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 id2 or id3. 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:

  • Creates a new creature_multispawn table with a composite primary key (spawnId, entry) to store alternate creature template entries for the rare spawns that need them
  • Renames id1 back to id in the creature table, restoring the original column name
  • Drops id2 and id3 from the creature table
  • Migrates existing data — all non-zero id2/id3 values are inserted into creature_multispawn before the columns are dropped

The C++ CreatureData struct retains id2/id3 fields 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.

  • AI tools (e.g. ChatGPT, Claude, or similar) were used entirely or partially in preparing this pull request. Claude Opus 4.6 (Claude Code) was used.

Issues Addressed:

N/A — this is a schema improvement.

SOURCE:

The changes have been validated through:

  • Live research (checked on live servers, e.g Classic WotLK, Retail, etc.)
  • Sniffs (remember to share them with the open source community!)
  • Video evidence, knowledge databases or other public sources (e.g forums, Wowhead, etc.)
  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick).

Tests Performed:

This PR has been:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author/has been live on production servers.
  • This pull request requires further testing and may have edge cases to be tested.

How to Test the Changes:

  • This pull request can be tested by following the reproduction steps provided in the linked issue
  • This pull request requires further testing. Provide steps to test your changes. If it requires any specific setup e.g multiple players please specify it as well.
  1. Apply the pending SQL migration
  2. Verify creatures with single entries spawn normally
  3. Verify creatures that previously had id2/id3 values still randomly pick between entries on spawn and respawn
  4. Test .npc info on both single-entry and multi-entry spawns
  5. Test .npc add to verify new creature spawns work correctly

Known Issues and TODO List:

  • Modules with custom SQL referencing id1, id2, id3 columns will need updates

How 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.

…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>
Copilot AI review requested due to automatic review settings March 23, 2026 04:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::id1id and updated references across scripts/commands/core.
  • Updated creature loading to read base entry from creature.id and load variant entries from creature_multispawn into id2/id3.
  • Added SQL migration creating creature_multispawn, migrating id2/id3 data, and dropping old columns from creature.

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.

Comment on lines +9 to +13
INSERT INTO `creature_multispawn` (`spawnId`, `entry`)
SELECT `guid`, `id2` FROM `creature` WHERE `id2` != 0;

-- Migrate id3 entries
INSERT INTO `creature_multispawn` (`spawnId`, `entry`)
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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`)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — using INSERT IGNORE now to handle potential id2 == id3 duplicates safely.

Comment on lines +2607 to +2624
// 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());
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +2501 to +2507
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;
}

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — aliased as spawnId AS guid.

Comment on lines +2537 to +2544
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;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@github-actions github-actions bot added DB related to the SQL database CORE Related to the core Script Refers to C++ Scripts for the Core file-cpp Used to trigger the matrix build labels Mar 23, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sudlud
Copy link
Copy Markdown
Member

sudlud commented Mar 23, 2026

Will Claude provide fixes for the 100 broken modules 🫣

@Nyeriah
Copy link
Copy Markdown
Member Author

Nyeriah commented Mar 23, 2026

The change is a simple rename (id1id) — modules just need to replace data->id1 with data->id (or .id1 with .id). Most modules don't even reference id1/id2/id3 at all. For the few that do, it's a one-line find-and-replace each.

@Nyeriah
Copy link
Copy Markdown
Member Author

Nyeriah commented Mar 23, 2026

Only one module in the CI suite is affected — mod-guildhouse (2 occurrences of id1). Fix PR submitted: azerothcore/mod-guildhouse#77

@sudlud
Copy link
Copy Markdown
Member

sudlud commented Mar 23, 2026

Alright, currently module ci is failing on build already, so it's not showing the failing sql files yet

@Nyeriah
Copy link
Copy Markdown
Member Author

Nyeriah commented Mar 23, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE Related to the core DB related to the SQL database file-cpp Used to trigger the matrix build Script Refers to C++ Scripts for the Core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants