fix(DB/Quest): Show objective marker on Zaxxis mobs (#27569)

This commit is contained in:
llupa
2026-09-09 18:36:06 -03:00
committed by GitHub
parent 501dd458b6
commit 516af2d687
3 changed files with 131 additions and 0 deletions
+1
View File
@@ -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 | — |
@@ -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
}