fix(Scripts/Dalaran): Eject only from the quarters (#27666)

This commit is contained in:
llupa
2026-09-17 14:11:11 +02:00
committed by GitHub
parent 0e6a5d322c
commit 41f475e9da
4 changed files with 269 additions and 25 deletions
+1
View File
@@ -165,6 +165,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1
| 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; 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; a wipe to Thorim's melee with a death-persistent damage shield on the last victim spawns no Cache of Storms while his hard-reset despawn runs, and a lethal hit on the respawned Thorim still yields | P2 | covered (`TestAC_27095_*`, `TestAC_27602_*`, `TestAC_27590_*`, `TestAC_27539_*`, `TestUlduar_BrightleafSunBeams*`, `TestUlduar_FreyaWardLasherOutlivesSummonDuration`, `TestUlduar_ThorimEvadeDespawnDoesNotYield`); Kologarn Charge `blocked-harness` (bridge Z after Charge) | #26266 #27095 #27539 #27590 #27602 |
| world/dalaran | Dalaran's faction guards eject a hostile player from the Silver Enclave and Sunreaver's Sanctuary, and from nowhere else — public mailboxes, the street, the sewers and the neutral inn are left alone. Both directions are asserted: only checking public ground would pass a build where the guards never fire. One case parks a pet inside a quarter with its owner outside, since every other case is a lone player and would not notice the guard reading the owner's position instead of the trespasser's | P2 | covered (`TestAC_4467_*`) | #4467 |
| 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 |
---
@@ -0,0 +1,203 @@
//go:build e2e
package dalaran_test
import (
"testing"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/azerothcore/AzerothGhost/client"
"github.com/azerothcore/AzerothGhost/e2e/e2eharness"
"github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta"
)
const (
dalaranMap uint32 = 571
spellSummonImp = 688
settleWindow = 9 * time.Second
)
// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/4467
//
// Dalaran's faction guards used to decide "is this player trespassing" from how
// close a fruit vendor was and which way the guard happened to face, which ejected
// players standing on public ground — a mailbox, the street, the sewers — while
// leaving most of each sanctum unwatched.
//
// They now eject on where the player is: the sanctum area ids, plus the WMO groups
// that make up each quarter's buildings, since most of that ground reports the
// plain Dalaran area id and cannot be told from neutral space by area alone.
//
// Both directions are asserted, because each alone is easy to satisfy wrongly.
// Checking only that public ground is safe would pass a build where the guards
// never fire; checking only the sanctums would pass the original bug untouched.
type trespassCase struct {
name string
x, y, z float32
wantEject bool
why string
}
func TestAC_4467_DalaranGuardsOnlyEjectFromRestrictedAreas(t *testing.T) {
meta.Begin(t, meta.TestMeta{
Tags: []string{"med", "world", "issue", "serial"},
Runtime: "med",
Issue: 4467,
Category: "world/dalaran",
})
horde := []trespassCase{
{"MailboxByHerosWelcome", 5740.3, 680.9, 644.7, false, "public mailbox outside the inn"},
// Deliberately a few yards off the trespasser destination (5758.79, 678.359,
// 642.726): standing on it would make an ejection a zero-yard move that the
// displacement oracle below could never see.
{"StreetOutsideHerosWelcome", 5750.0, 678.8, 642.8, false, "the public street beside the spell's destination"},
{"UnderbellySewers", 5759.2, 716.1, 618.6, false, "the public sewers"},
{"SilverEnclaveThreshold", 5754.83, 718.36, 641.71, true, "past the guards into the Enclave"},
{"SilverEnclaveCourtyard", 5740.3, 739.6, 641.9, true, "Alliance sanctum"},
{"DeepSilverEnclave", 5671.43, 724.48, 653.41, true, "deep Alliance quarter"},
{"InsideAHerosWelcome", 5725.674, 683.2177, 646.565, true, "the Alliance inn"},
}
alliance := []trespassCase{
{"MailboxBySunreaverSide", 5893.2, 528.2, 641.4, false, "public mailbox on the Horde side"},
{"LegerdemainLoungeAlliance", 5847.97, 635.43, 647.57, false, "neutral inn, 35.4y from a guard so really evaluated"},
{"SunreaversSanctuary", 5862.0, 520.3, 655.5, true, "Horde sanctum"},
{"InsideTheFilthyAnimal", 5892.3, 505.75, 641.65, true, "the Horde inn"},
}
run := func(t *testing.T, race uint8, cases []trespassCase) {
// GM mode must be off: npc_mageguard_dalaran skips IsGameMaster(), so a GM
// is never ejected and every case would pass vacuously.
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "Tres",
Race: race,
Class: e2eharness.ClassWarrior,
Level: 80,
})
e2eharness.MustGM(t, bot.World, ".gm off")
// The whole oracle rests on GM mode being off - IsGameMaster() short-circuits
// the guard check - so ack it on the world thread rather than assume it took.
bot.FlushWorld(t)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
e2eharness.TeleportGo(t, bot.World, c.x, c.y, c.z, dalaranMap)
ejected := false
deadline := time.Now().Add(settleWindow)
for time.Now().Before(deadline) {
ax, ay, az, _ := bot.Pos()
if e2eharness.Distance3D(ax, ay, az, c.x, c.y, c.z) > 10 {
ejected = true
break
}
time.Sleep(250 * time.Millisecond)
}
switch {
case c.wantEject && !ejected:
e2eharness.Assertf(t, "%s (%.2f, %.2f, %.2f): a hostile player was not ejected from %s; "+
"the restricted ground is unguarded", c.name, c.x, c.y, c.z, c.why)
case !c.wantEject && ejected:
e2eharness.Assertf(t, "%s (%.2f, %.2f, %.2f): a player standing on %s was teleported away; "+
"this is the AC#4467 regression — public ground is not the faction quarter",
c.name, c.x, c.y, c.z, c.why)
default:
t.Logf("PASS %-26s ejected=%-5v (%s)", c.name, ejected, c.why)
}
})
}
}
t.Run("Horde", func(t *testing.T) { run(t, e2eharness.RaceOrc, horde) })
t.Run("Alliance", func(t *testing.T) { run(t, e2eharness.RaceHuman, alliance) })
t.Run("PetInsideOwnerOutside", petInsideOwnerOutside)
}
// The guard classifies and casts on who, not on who's owner, so a controlled unit
// inside a quarter is ejected on its own footing even while its owner stands on
// public ground. Every other case here is a lone player, where the two positions
// are identical - this is the only one that would catch a regression reading the
// owner instead.
func petInsideOwnerOutside(t *testing.T) {
bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{
Prefix: "TresP",
Race: e2eharness.RaceOrc,
Class: e2eharness.ClassWarlock,
Level: 80,
LearnAllClass: true,
})
bot.Learn(t, spellSummonImp)
_ = bot.CastOrGM(t, spellSummonImp, 0, 20*time.Second)
pet := bot.WaitPlayerPet(t, 25*time.Second)
petPos := func() (float32, float32, float32, bool) {
for _, u := range bot.World.GetNearbyUnits(200) {
if u.GUID == pet {
return u.PosX, u.PosY, u.PosZ, true
}
}
return 0, 0, 0, false
}
// Park owner and pet inside the Enclave under GM - the guard reads
// IsGameMaster() from the owner, so nothing fires while positioning.
e2eharness.MustGM(t, bot.World, ".gm on")
e2eharness.TeleportGo(t, bot.World, 5740.3, 739.6, 641.9, dalaranMap)
time.Sleep(3 * time.Second)
stay := client.MakePetActionButton(client.PetCommandStay, client.PetActCommand)
if err := bot.World.PetAction(pet, stay, 0); err != nil {
e2eharness.HarnessFailf(t, "pet stay: %v", err)
}
time.Sleep(2 * time.Second)
// Owner out to the public mailbox; the pet stays behind inside the quarter.
e2eharness.TeleportGo(t, bot.World, 5740.3, 680.9, 644.7, dalaranMap)
time.Sleep(3 * time.Second)
px, py, pz, ok := petPos()
if !ok {
e2eharness.HarnessFailf(t, "pet is no longer tracked; it followed or despawned, so this case proves nothing")
}
if e2eharness.Distance3D(px, py, pz, 5740.3, 739.6, 641.9) > 10 {
e2eharness.HarnessFailf(t, "pet did not hold position inside the quarter (%.1f, %.1f, %.1f)", px, py, pz)
}
e2eharness.MustGM(t, bot.World, ".gm off")
bot.FlushWorld(t)
// A stationary unit is never re-evaluated - MoveInLineOfSight fires on
// relocation - so command Follow to make it move while still inside.
follow := client.MakePetActionButton(client.PetCommandFollow, client.PetActCommand)
if err := bot.World.PetAction(pet, follow, 0); err != nil {
e2eharness.HarnessFailf(t, "pet follow: %v", err)
}
ejected := false
deadline := time.Now().Add(settleWindow)
for time.Now().Before(deadline) {
x, y, z, still := petPos()
if !still {
break
}
// Ejection lands on the trespasser destination, ~19y from the owner, so it
// is distinguishable from the pet simply walking back.
if e2eharness.Distance3D(x, y, z, 5758.79, 678.36, 642.73) < 5 {
ejected = true
break
}
time.Sleep(250 * time.Millisecond)
}
ox, oy, oz, _ := bot.Pos()
if e2eharness.Distance3D(ox, oy, oz, 5740.3, 680.9, 644.7) > 10 {
e2eharness.Assertf(t, "the owner was teleported off public ground at the mailbox; only the pet was trespassing")
}
if !ejected {
e2eharness.Assertf(t, "a pet left inside the Silver Enclave was not ejected while its owner stood on public ground; "+
"the guard is reading the owner's position instead of the trespasser's")
}
t.Logf("PASS PetInsideOwnerOutside pet ejected, owner untouched on public ground")
}