diff --git a/e2e/README.md b/e2e/README.md index dbf57b1a18..334890dcf9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -164,7 +164,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1 | guild/charter_bank | charter buy+turn-in | P2 | covered | — | | instances/bind_reset | party tele; ritual summon | P2 | covered; post-reset summon `blocked-harness` (AcceptSummon after reset) | #10708 | | instances/classic/stratholme | Timmy remains hidden while a relevant Square Scarlet lives, then emerges after the area is clear | P2 | covered (`TestAC_26363_TimmyEmergesAfterSquareCleared`) | #26363 | -| instances/ulduar | named tele; Freya wave interval; a Laughing Skull's Lunatic Gaze stops at the brain room's geometry instead of draining sanity through it; Elder Brightleaf's Unstable Sun Beams despawn on their own timer instead of outliving him, one lands under the player rather than all on the elder, and a wave stays capped however many players stand in range; Psychosis and Malady of the Mind stop picking a player once they are at 40 Sanity or less; Algalon holds still for 3s once his Big Bang cast lands instead of resuming melee and Quantum Strike on the next tick | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestUlduar_BrightleafSunBeams*`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27539 #27590 #27602 | +| instances/ulduar | named tele; Freya wave interval; a Laughing Skull's Lunatic Gaze stops at the brain room's geometry instead of draining sanity through it; Elder Brightleaf's Unstable Sun Beams despawn on their own timer instead of outliving him, one lands under the player rather than all on the elder, and a wave stays capped however many players stand in range; Psychosis and Malady of the Mind stop picking a player once they are at 40 Sanity or less; Algalon holds still for 3s once his Big Bang cast lands instead of resuming melee and Quantum Strike on the next tick; a Writhing Lasher stays up past 62947's own 10s summon duration, because the despawn type its AI sets in `IsSummonedBy` is no longer overwritten on the way out of the summon call | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestUlduar_BrightleafSunBeams*`, `TestUlduar_FreyaWardLasherOutlivesSummonDuration`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27539 #27590 #27602 | | world/gameevents | Call to Arms banners at the Dalaran portals belong to the side they stand on, and the already-correct Warsong set is unchanged. **Wants an exclusive realm**: starting a holiday re-anchors its schedule in the running worldserver until restart; holidays already running are left alone | P2 | covered (`TestAC_24380_*`); Shattrath's 23 positions `gap` | #24380 | --- diff --git a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go index c2197824dc..0ff0946ba4 100644 --- a/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go +++ b/e2e/suites/instances/northrend/ulduar/ulduar_e2e_test.go @@ -1050,3 +1050,89 @@ func TestAC_27539_AlgalonBigBangStasis(t *testing.T) { t.Logf("PASS Big Bang stasis: no damage for %s after the cast, then Algalon resumed", stasisWindow) } + +// Freya's Ward summons its adds with 62947, whose own summon duration is 10s, and +// npc_freya_ward_summon raises them to TEMPSUMMON_MANUAL_DESPAWN so they stay until the encounter +// clears them. Spell::EffectSummonType re-applies that duration once the summon call returns, which +// is after IsSummonedBy has run, so an override applied from inside that hook is discarded and the +// lashers vanish 10s in while the raid is still fighting them. +// The IsSummonedBy override arrived with https://github.com/azerothcore/azerothcore-wotlk/pull/27567 +func TestUlduar_FreyaWardLasherOutlivesSummonDuration(t *testing.T) { + meta.Begin(t, meta.TestMeta{ + Tags: []string{"med", "instances"}, + Runtime: "med", + Category: "instances/northrend/ulduar", + }) + + const ( + npcFreyaWardReticle = uint32(33366) + npcFreyaWard = uint32(33367) + npcWrithingLasher = uint32(33387) + npcWardOfLife = uint32(34275) + + addSearchRange = float32(100) + + // npc_freya_ward resets its cast timer to 25s and casts at 29s, so the first lasher lands + // ~4s after the ward spawns. One extra 29s cycle covers a ward that missed that window. + firstLasherWindow = 40 * time.Second + + // 62947 carries SpellDuration 1 (10s). A lasher still in the cache this long after it + // appeared is not running on the spell's own timer. Do not shorten it below 10s: that is + // the despawn this guards, and a shorter window would pass on an unfixed core. + outliveWindow = 25 * time.Second + ) + + bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{ + Prefix: "FWard", Race: e2eharness.RaceHuman, Level: 80, + }) + + // Stay GM through the raid enter, and use the brain room floor: it is empty, so nothing else + // pulls the bot while the lasher is being timed. + bot.Teleport(t, 1930.0, -120.0, 240.07, e2eharness.MapUlduar) + if _, _, _, m := bot.Pos(); m != e2eharness.MapUlduar { + e2eharness.Preconditionf(t, "not in Ulduar after brain room tele map=%d", m) + } + + // The ward only casts while a reticle is within 60y of it, so the reticle goes down first. + reticle := bot.Spawn(t, npcFreyaWardReticle, 30*time.Second) + ward := bot.Spawn(t, npcFreyaWard, 30*time.Second) + + // Drops GM so the lasher's DoZoneInCombat can pick the bot up the way it picks up a raid, and + // turns on god mode, which is what keeps the bot standing there for the whole window. + bot.CombatReady(t) + + lasher := bot.WaitUnit(t, npcWrithingLasher, firstLasherWindow) + if lasher == 0 { + e2eharness.Preconditionf(t, "Freya's Ward 0x%X never summoned a Writhing Lasher next to reticle 0x%X", ward, reticle) + } + appeared := time.Now() + t.Logf("Writhing Lasher 0x%X summoned by ward 0x%X", lasher, ward) + + // Pin the GUID: the ward keeps summoning every 29s, so "some lasher is in the cache" would + // still be true on an unfixed core and would hide the despawn. + deadline := appeared.Add(outliveWindow) + for time.Now().Before(deadline) { + if bot.World.GetObject(lasher) == nil { + e2eharness.Assertf(t, "Writhing Lasher 0x%X left the world %s after it was summoned — IsSummonedBy set TEMPSUMMON_MANUAL_DESPAWN but 62947's own 10s duration was restored over it", + lasher, time.Since(appeared).Round(time.Millisecond)) + } + time.Sleep(250 * time.Millisecond) + } + + hp, maxHP := bot.UnitHP(lasher) + if hp == 0 { + e2eharness.Preconditionf(t, "Writhing Lasher 0x%X is a corpse (%d/%d) — something killed it, so its lifetime proves nothing", lasher, hp, maxHP) + } + t.Logf("PASS Writhing Lasher 0x%X still up %s after summon (hp=%d/%d)", lasher, outliveWindow, hp, maxHP) + + // A MANUAL_DESPAWN summon has nothing of its own to remove it, and deleting the ward does not + // take its adds with it, so they have to be killed here. The ward dies first: while it lives it + // summons another pair every 29s, and a sweep taken before that would race the next cycle. + bot.DamageKill(t, []uint64{ward}, 10_000_000, 30*time.Second) + var adds []uint64 + for _, add := range bot.UnitsByEntry(addSearchRange, npcWrithingLasher, npcWardOfLife) { + adds = append(adds, add.GUID) + } + bot.DamageKill(t, adds, 10_000_000, 30*time.Second) + bot.AssertWorldAlive(t) +} diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp index 8775a68768..39e89f731b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_flame_leviathan.cpp @@ -939,7 +939,14 @@ struct npc_freya_ward_summon : public ScriptedAI void IsSummonedBy(WorldObject* /*summoner*/) override { - me->ToTempSummon()->SetTempSummonType(TEMPSUMMON_MANUAL_DESPAWN); + // Deferred a tick on purpose: Spell::EffectSummonType re-applies the summon spell's own + // duration (10s for the lashers, 3s for the wards) once the summon call returns, which + // would overwrite anything this hook sets. + me->m_Events.AddEventAtOffset([this]() + { + me->ToTempSummon()->SetTempSummonType(TEMPSUMMON_MANUAL_DESPAWN); + }, 1ms); + DoZoneInCombat(); }