All fixes are additive, reversible, and placed on empty/custom ID
bands so they don’t collide with a stock TrinityCore install. Files live in
../../sql/.
File: sql/01_quest_earthen_intro_fix.sql
Symptom: a fresh Earthen (allied race) character finishes the first quest of the Isle of Dorn “Awakening” intro, then no NPC offers anything — the intro is a linear phased corridor, so the player is stranded.
Root cause: the chain is
79200 "Who am I?" → 79201 "The Analysis Interface" → 83328 "The Analysis Interface".
Quest 79201 is a deprecated duplicate (identical title to 83328) with no
quest-starter and no quest-ender anywhere — it cannot be obtained or completed.
But 83328 (Foreman Uzjax) is gated on 79201 (PrevQuestID = 79201), so the chain
dead-locks.
Fix: bypass the phantom → chain flows 79200 → 83328 directly.
Reload live with reload quest_template (no restart).
Bug class: a real but giver-less/ender-less quest set as another quest’s
PrevQuestID hard-blocks the chain. Note that sentinel PrevQuestID values
(999999, 99999, …) are not this bug — those IDs don’t exist, so the
engine skips the prerequisite. Only prerequisites that exist but are
un-obtainable block.
File: sql/02_quest_gameobject_unblock.sql
+28 gameobject_template and +89 gameobject spawns (guid band 8500000+)
for objects that quests need but that were missing from the base DB, unblocking
~10 quests. DisplayIDs sourced authentically; coordinates converted from map%
to world via UiMapAssignment for build 68275, Z/orientation taken from the
nearest existing spawn on the same map. Needs a worldserver restart.
File: sql/03_raid_instance_bindings.sql
Binds 5 raids — Dragon Soul (967), End Time (938), Hour of Twilight (940), Well of Eternity (939), Throne of the Four Winds (754) — to their compiled instance script. This enables lockout, DungeonEncounter journal completion, and boss-state persistence (tracked/clearable).
Limitation: these instance scripts are stubs; they do not add boss combat
AI (no boss_*.cpp exists for them). Bosses stay on default AI. Needs a
restart.
File: sql/04_harandar_graveyards.sql
12 world_safe_locs + 12 graveyard_zone links + 12 Spirit Healers (creature
guid band 11000773+) for Harandar (map 2694), one of the newest zones, so
death/resurrect works there. Cherry-picked as a self-contained additive file.
Needs a restart.
File: sql/05_world_events_realign.sql
Symptom: by 2026 the yearly holidays fire 2-4 days early and the Darkmoon Faire is about two weeks out of step.
Root cause: TrinityCore stores a holiday as an anchor date plus a fixed
occurence in minutes. A stock TDB anchors these in 2022-2023, and 525600
minutes is 365 days flat — no leap quarter — so even fixed-date holidays slide
roughly a day every four years. Worse, anything the real calendar defines by
rule (Darkmoon Faire is the first Sunday of the month, Noblegarden follows
Easter, the Lunar Festival follows the lunar new year) drifts away from that rule
entirely.
Fix: re-anchor each holiday to its next genuine occurrence, so later years
land correctly too. The file backs the table up before touching it. Needs a
restart — game_event is read at startup.
Note when checking your work: an event is active when
MOD(minutes since start, occurence) < length — not when “now” falls between
start_time and end_time. Those two columns bound the whole repeating series,
not one occurrence.
File: sql/06_remove_stray_spawns.sql
Symptom: raid bosses standing in starter zones, event props at the Stormwind gates with no event attached, and Blizzard’s own QA test creatures loose in the world.
Root cause: an artifact of sniffed data — a client saw a creature somewhere during a scripted moment, and the capture recorded it as a permanent resident.
Why it matters: a level-90 boss at the Stormwind gates is sudden death for anything levelling past it. The companion bots in this repo were dying to exactly that.
Fix: removes 30 such spawns. The file backs up every row it deletes first, so the revert is a single INSERT. Needs a restart.
How they were identified: these legacy entries often 404 on external databases because they are not in live retail data at all. The company an NPC keeps is the more reliable signal — the neighbours around a spawn identify its true home.
File: sql/07_npc_frozen_patrols.sql
Symptom: a guard or wandering mob never moves. Not a pathing problem — the creature never starts moving at all.
Root cause: creature.MovementType = 2 means “follow a waypoint path”, and
the path id comes from creature_addon.PathId. When the spawn has no addon row,
or PathId is 0, or PathId names a waypoint_path that does not exist,
WaypointMovementGenerator::DoInitialize() cannot load a path and returns false.
The generator never initialises, so the creature stands still for as long as the
grid is loaded, logging couldn't load path for ... each time.
Scale: 3,924 of 10,993 waypoint spawns — about a third of every patrolling
NPC in the world. The spawn row is the anomaly, not the template: 3,773 of them
belong to a creature_template whose own MovementType is 0 (idle).
Fix: make the spawn honest — wander_distance > 0 becomes random movement in
that radius, otherwise idle. This is the same mapping TrinityCore applies when it
repairs other contradictory MovementType/wander_distance combinations at load.
Real patrol routes cannot be recovered; the waypoint data was never there. Needs
a restart.
File: sql/08_npc_missing_models.sql
Symptom: the log repeats Creature (Entry: N) has no model defined ... can't
load. The creature does not exist in the world — it cannot be seen, targeted or
killed.
Root cause: a creature needs at least one creature_template_model row.
889 spawned entries across 1,908 spawns had none.
Why it matters: most are invisible utility NPCs — kill-credit counters and quest-objective bunnies. They are meant to be invisible, but they still have to exist: a quest objective that counts kills of a credit NPC can never complete if that NPC cannot spawn.
Fix: give every spawned entry with no model the canonical invisible display
11686 — the model TrinityCore’s own invisible stalkers use. Scope is limited to
entries that are actually spawned. Needs a restart.
File: sql/09_npc_broken_spawns.sql
Four defects, each with its own backup:
creature_template — the engine skips them; a template cannot be
reconstructed from a spawn row, so they are removed (261).vehicle_template_accessory; that
mechanism does not read the creature table, so the passenger is unaffected.GetMapHeight() searches
about 50 yards and snaps the creature to the ground, so a near-enough estimate
self-corrects. Only spawns with fewer than 3 neighbours to derive a height from
are removed (86).Needs a restart.
File: sql/10_npc_orphan_references.sql
creature_addon rows naming a missing waypoint path (387), addon rows for a
spawn that no longer exists (26), and formations whose leader or member is gone
(237 rows have a missing leader and 355 a missing member — 386 distinct rows,
since some are missing both). A formation with a missing leader never forms up, so the members
that should march behind it stand on their own spawn points instead.
Run after 07 and 09 — 09 removes broken spawns, which turns their addon rows into orphans that this file then collects. Needs a restart.
File: sql/hotfixes/01_map_difficulty_unlock.sql
Applies to the hotfixes database, not world.
Symptom: eight maps are completely empty in game, and the log repeats
Table \creature` has creature (GUID: N) that is not spawned in any difficulty,
skipped.` thousands of times.
Root cause: ObjectMgr::LoadCreatures builds the set of legal difficulties
for each map from sMapDifficultyStore, then keeps only the spawnDifficulties
tokens that are in it. For these maps the intersection is empty — six are retired
scenarios whose MapDifficulty records Blizzard removed from the client data, and
two have a record for a difficulty that is not in the spawn strings. The spawn
rows are fine; the map has no difficulty to spawn them into.
Fix: add one MapDifficulty row per map with DifficultyID 0. DB2 stores are
file-plus-database — DB2Store::LoadFromDB runs the loader twice, once for
VerifiedBuild > 0 and once for <= 0 — so a custom row with VerifiedBuild 0 is
merged into the store by design.
Do not use DifficultyID 1. Difficulty.db2 has no record 0, so
GetDefaultMapDifficulty() filters these rows out and they cannot influence how
an instance is created or scaled. A real difficulty is selectable and would
override Blizzard’s own tuning for the map.
Recovers: 1,218 creature and 1,073 gameobject spawns, including Darkmaul
Citadel — the Exile’s Reach dungeon, which had never spawned a single NPC.
Nothing is broadcast to game clients: those come from the separate hotfix_data
table, which this file does not touch. Needs a restart.
You target a quest giver in Stormwind and there is a second, identical one
underneath it. Clicking cycles between them; one may be tapped or in combat while
the other is not. Nothing in the engine deduplicates creature — rows accumulated
from repeated imports of overlapping data sets, so the same spawn exists more than
once under different guids.
Measured: 2,598 groups, 3,403 redundant spawns removed.
The hard part is what not to delete. Grouping on coordinates alone flags 7,319
groups / 8,379 spawns — and nearly 5,000 of those are healthy data: different
phases (players at different story stages see different NPCs in the same spot),
different model or equipment (visibly different creatures), and groups where some
members belong to game_event_creature, spawn_group or pool_members and
others do not (the engine picks one — that is variety, not duplication). This fix
requires all thirteen fields that decide what a player sees to match, and skips
mixed-membership groups entirely.
The survivor is the formation leader where the group has one, otherwise the
lowest guid; dependent creature_addon and creature_formations rows go with
the removed spawns, so no orphans are created.
Known limit: spawnDifficulties is a comma-separated list whose element order
is not normalised, and the grouping compares it as a string. Two identical spawns
whose lists are written in a different order are not detected — 4 pairs out of
3,403 on our database. The same quirk means that after a revert some restored
rows no longer group with their twin, so verify a revert by row count, not by
re-counting duplicate groups. Needs a restart.
The log fills with Could not load VMAP name:City of Threads, id:2669, ...,
repeating for as long as the server runs — 1,367 lines in one 2h46m run on our
realm, across Priory of the Sacred Flame, City of Threads, The Dawnbreaker, The
Rookery and Vale of Eternal Blossoms. Inside the affected tiles the engine has no
line-of-sight, height or collision data at all.
A DB2 hotfix row replaces the whole record from the .db2 file; it does not
merge field by field. hotfixes.map held 18 rows empty apart from their ID —
Directory NULL, MapName NULL, WdtFileDataID 0, and the damaging part,
ParentMapID = 0 and CosmeticParentMapID = 0.
Zero is not “no parent” — that is -1. Zero is map 0, Azeroth. So the engine believed Azeroth was those instances’ parent. A child map with no geometry of its own for a tile borrows the parent’s and pairs it with its own index file; borrowing from the wrong map gives two files describing different places, the spawn counts disagree, and the tile is rejected outright.
Measured on tile 26_29 of City of Threads: the map’s own index file holds 2,494 entries, its real parent Khaz Algar holds 2,494 — exact — and Azeroth holds 22. Every affected tile shows the same shape.
The rows were absent from hotfix_data, so they were never sent to game clients:
the damage was server-side only. Deleting them lets the engine read the real
record from Map.db2.
Result: 1,367 errors in the run before, 0 after. Only rows empty in all five fields are touched, so a deliberate override cannot be caught by accident. Needs a restart.
Apply all: scripts/apply_fixes.sh (add HOTFIXES_DB=hotfixes to include
sql/hotfixes/). Every file is safe to run twice — backups are created only if
absent, so a second run cannot overwrite your original values — and every file
ends with a revert block in comments. Always back up your world DB first.
Every file in this document was verified by rolling a copy of a real database back to its broken state, applying the fix, confirming the defect count reaches zero, applying it a second time, and then running the documented revert to check the original numbers come back exactly.