From 516af2d687489b5ac71d7686c35d792cd823c119 Mon Sep 17 00:00:00 2001 From: llupa <41073314+llupa@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:36:06 +0200 Subject: [PATCH] fix(DB/Quest): Show objective marker on Zaxxis mobs (#27569) --- .../rev_1788943514347882000.sql | 10 ++ e2e/README.md | 1 + .../objectives/questitem_marker_e2e_test.go | 120 ++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 data/sql/updates/pending_db_world/rev_1788943514347882000.sql create mode 100644 e2e/suites/quests/objectives/questitem_marker_e2e_test.go diff --git a/data/sql/updates/pending_db_world/rev_1788943514347882000.sql b/data/sql/updates/pending_db_world/rev_1788943514347882000.sql new file mode 100644 index 0000000000..1a84d1a8c1 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1788943514347882000.sql @@ -0,0 +1,10 @@ +-- +-- Quests 10262 and 10308 ask for Zaxxis Insignias, but the three Zaxxis mobs that drop them +-- advertise no quest item, so the client shows no objective when you hover them. The marker +-- comes from creature_questitem, which nothing referenced for this item. +-- +DELETE FROM `creature_questitem` WHERE (`CreatureEntry` IN (18875, 19641, 19642)) AND (`Idx` = 0); +INSERT INTO `creature_questitem` (`CreatureEntry`, `Idx`, `ItemId`, `VerifiedBuild`) VALUES +(18875, 0, 29209, 0), +(19641, 0, 29209, 0), +(19642, 0, 29209, 0); diff --git a/e2e/README.md b/e2e/README.md index 6d94d98e58..146fc01a59 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -156,6 +156,7 @@ go test -tags=e2e ./local/... -count=1 -v -timeout 30m -parallel 1 | quests/lifecycle | STAY_ALIVE fail on death; status after save/relog | P1 | covered (`TestAC_26549_*`) | #26549 | | quests/escort | find spawned unit; follow-NPC despawns on logout | P2 | covered (`TestAC_24450_*`) | #24450 | | quests/frostmourne | scrying-orb vision runs; Muradin leaves the cavern and despawns; quest 12478 COMPLETE | P2 | covered (`TestAC_25760_*`); dialogue order and duplicate line `blocked-harness` (no monster-say capture) | #25760 | +| quests/objectives | a mob that drops a quest item advertises it, so the client shows the objective on hover (`creature_questitem` -> `SMSG_CREATURE_QUERY_RESPONSE.questItems`) | P2 | covered (`TestAC_27553_*`), decoding the response through a raw packet hook since the harness has no dispatch case for it | #27553 | | items/equip | visible-item slot after EquipEntry; additem; survives relog | P2 | covered | — | | protocol/session | pos; item/quest load; money save/relog | P1 | covered; GM vis persist `blocked-harness` (extra_flags after relog) | #25793 | | protocol/teleport | cross-map; named; GoCreatureID | P1 | covered | — | diff --git a/e2e/suites/quests/objectives/questitem_marker_e2e_test.go b/e2e/suites/quests/objectives/questitem_marker_e2e_test.go new file mode 100644 index 0000000000..a3a7554583 --- /dev/null +++ b/e2e/suites/quests/objectives/questitem_marker_e2e_test.go @@ -0,0 +1,120 @@ +//go:build e2e + +package objectives_test + +import ( + "encoding/binary" + "sync" + "testing" + "time" + + _ "github.com/go-sql-driver/mysql" + + "github.com/azerothcore/AzerothGhost/e2e/e2eharness" + "github.com/azerothcore/azerothcore-wotlk/e2e/internal/meta" +) + +// SMSG_CREATURE_QUERY_RESPONSE. The harness has no dispatch case for this opcode, +// so the payload is read through a raw packet hook: WorldClient.handlePacket calls +// invokePacketHooks for every opcode before its own switch, so unparsed packets are +// still observable. Same approach as suites/spells/immunity. +const smsgCreatureQueryResponse uint16 = 0x0061 + +// Issue: https://github.com/azerothcore/azerothcore-wotlk/issues/27553 +// +// Quests 10262 and 10308 ask for Zaxxis Insignia (29209). Hovering a mob that drops +// it showed no objective, because that marker is not a server-side flag: creatures +// have no equivalent of GameObject::ActivateToQuest, and the client learns which +// quest items a creature carries from the questItems array in +// SMSG_CREATURE_QUERY_RESPONSE, filled from creature_questitem. +// +// This asks the server for each dropping creature and asserts the insignia is among +// the six advertised items. On unfixed data all six are zero. +func TestAC_27553_ZaxxisMobsAdvertiseQuestItem(t *testing.T) { + meta.Begin(t, meta.TestMeta{ + Tags: []string{"short", "quests", "issue", "serial"}, + Runtime: "short", + Issue: 27553, + Category: "quests/objectives", + }) + + const itemZaxxisInsignia = uint32(29209) + mobs := []struct { + entry uint32 + name string + }{ + {18875, "ZaxxisRaider"}, + {19641, "WarpRaiderNesaad"}, + {19642, "ZaxxisStalker"}, + } + + // CMSG_CREATURE_QUERY is STATUS_LOGGEDIN and the handler ignores the guid it is + // given, so the bot can ask from wherever it logs in. No travel or spawn needed. + bot := e2eharness.NewSolo(t, e2eharness.ScenarioOpts{Prefix: "QItem", Level: 80}) + + for _, m := range mobs { + t.Run(m.name, func(t *testing.T) { + items := creatureQuestItems(t, bot, m.entry) + for _, it := range items { + if it == itemZaxxisInsignia { + t.Logf("OK %s (%d) advertises item %d", m.name, m.entry, itemZaxxisInsignia) + return + } + } + e2eharness.Assertf(t, + "%s (%d) advertises %v; expected %d among them, without which the client shows no objective on hover", + m.name, m.entry, items, itemZaxxisInsignia) + }) + } + + bot.AssertWorldAlive(t) +} + +// creatureQuestItems asks the server about one creature and returns the six +// questItems slots. The response ends with questItems[6] followed by movementId, +// so they are the last 28 bytes less that trailing uint32. +func creatureQuestItems(t *testing.T, bot *e2eharness.ScenarioBot, entry uint32) []uint32 { + t.Helper() + + var ( + mu sync.Mutex + got []uint32 + once sync.Once + ) + done := make(chan struct{}) + + cancel := bot.World.AddPacketHook(func(opcode uint16, data []byte) { + // A creature the server does not know answers with entry|0x80000000 and no + // body, which the length check drops. + if opcode != smsgCreatureQueryResponse || len(data) < 32 { + return + } + if binary.LittleEndian.Uint32(data[:4]) != entry { + return + } + tail := data[len(data)-28:] + out := make([]uint32, 6) + for i := range out { + out[i] = binary.LittleEndian.Uint32(tail[i*4 : i*4+4]) + } + mu.Lock() + got = out + mu.Unlock() + once.Do(func() { close(done) }) + }) + defer cancel() + + if err := bot.World.CreatureQuery(entry, 0); err != nil { + e2eharness.HarnessFailf(t, "CreatureQuery(%d): %v", entry, err) + } + + select { + case <-done: + case <-time.After(15 * time.Second): + e2eharness.HarnessFailf(t, "no creature query response for entry %d within 15s", entry) + } + + mu.Lock() + defer mu.Unlock() + return got +}