e2eharness

package
v1.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package e2eharness provides a reusable library for live-stack 3.3.5a e2e tests against AzerothCore (standalone or behind a client gateway). Downstream modules import this package and write their own tests; see e2e/EXAMPLES.md.

Index

Constants

View Source
const (
	// Default password used by ensure-account helpers.
	DefaultPassword = "test"

	// LootThresholdUncommon is the AC minimum valid CMSG_LOOT_METHOD threshold
	// (ITEM_QUALITY_UNCOMMON). Core rejects lower values; greys/whites never roll.
	LootThresholdUncommon uint8 = 2

	// CreatureGroupLootFixture is Crimson Templar (15209): 100% Abyssal Crest (20513),
	// Quality=Uncommon, no MULTI_DROP / quest conditions — reliable SMSG_LOOT_START_ROLL
	// under group/NBG with LootThresholdUncommon. Prefer over outdoor critters (e.g. 3098).
	CreatureGroupLootFixture uint32 = 15209
	// ItemGroupLootFixture is the guaranteed crest on CreatureGroupLootFixture.
	ItemGroupLootFixture uint32 = 20513

	// MinPetitionSigns matches charserver default MIN_PETITION_SIGNS.
	MinPetitionSigns = 9

	// Guild charter item entry (Guild Charter).
	ItemGuildCharter = 5863

	// Linen Cloth — stackable, tradable, cheap test deposit.
	ItemLinenCloth = 2589

	// Stormwind guild vault (gameobject type 34) — from acore_world:
	//   SELECT guid,id,map,position_x,position_y,position_z,orientation
	//   FROM gameobject g JOIN gameobject_template gt ON gt.entry=g.id
	//   WHERE gt.type=34 AND g.map=0 AND g.guid=41911;
	StormwindGuildVaultGUIDLow = uint32(41911)
	StormwindGuildVaultEntry   = uint32(187329)
	StormwindGuildVaultMap     = uint32(0)
	StormwindGuildVaultX       = float32(-8934.91)
	StormwindGuildVaultY       = float32(618.273)
	StormwindGuildVaultZ       = float32(100.589)
	StormwindGuildVaultO       = float32(0.506145)

	// Stormwind tabard designer (guild charter) — from acore_world:
	//   SELECT c.guid,c.id,ct.name,c.map,c.position_x,c.position_y,c.position_z,c.orientation
	//   FROM creature c JOIN creature_template ct ON ct.entry=c.id
	//   WHERE c.guid=79681;  -- Aldwin Laughlin, npcflag has TABARDDESIGNER|PETITIONER
	StormwindTabardDesignerEntry   = uint32(4974)
	StormwindTabardDesignerGUIDLow = uint32(79681)
	StormwindTabardDesignerMap     = uint32(0)
	StormwindTabardDesignerX       = float32(-8885.25)
	StormwindTabardDesignerY       = float32(614.395)
	StormwindTabardDesignerZ       = float32(95.3576)
	StormwindTabardDesignerO       = float32(3.52556)

	// Default guild charter cost (10 gold) and bank first tab (100 gold).
	GuildCharterCostCopper = uint32(100000)
	GuildBankFirstTabCost  = uint32(1000000)
	// Backpack: bag=255, slots 23..38 (INVENTORY_SLOT_ITEM_START..END-1).
	InventoryBagBackpack   = uint8(255)
	InventorySlotItemStart = uint8(23)
	InventorySlotItemEnd   = uint8(39)
)

Gameplay / protocol constants for guild e2e.

View Source
const (
	SpellSummonPlayer                  = client.SpellSummonPlayer
	SpellCreateMeetingStonePortal      = client.SpellCreateMeetingStonePortal
	GameObjectMeetingStoneSummonPortal = client.GameObjectMeetingStoneSummonPortal
	// SpellRitualOfSummoning is the EffectSummonPlayer spell (7720), not the warlock channel.
	SpellRitualOfSummoning = client.SpellRitualOfSummoning
)

Spell / GO constants for the ritual summon path (see RitualSummon).

View Source
const (
	CreatureRisenGhoul         uint32 = 26125 // DK Raise Dead
	CreatureRisenAlly          uint32 = 30230 // Raise Ally
	CreatureArmyOfTheDeadGhoul uint32 = 24207 // Army of the Dead
)

Known temporary summon entries left by e2e casts (not DB spawns — live only).

View Source
const (
	RaceHuman    uint8 = 1
	RaceOrc      uint8 = 2
	RaceDwarf    uint8 = 3
	RaceNightElf uint8 = 4
	RaceUndead   uint8 = 5
	RaceTauren   uint8 = 6
	RaceGnome    uint8 = 7
	RaceTroll    uint8 = 8
	RaceBloodElf uint8 = 10
	RaceDraenei  uint8 = 11

	ClassWarrior     uint8 = 1
	ClassPaladin     uint8 = 2
	ClassHunter      uint8 = 3
	ClassRogue       uint8 = 4
	ClassPriest      uint8 = 5
	ClassDeathKnight uint8 = 6
	ClassShaman      uint8 = 7
	ClassMage        uint8 = 8
	ClassWarlock     uint8 = 9
	ClassDruid       uint8 = 11
)

3.3.5a race / class IDs used by character create.

View Source
const (
	// Spells
	SpellCharge          = 100   // Charge (warrior)
	SpellIntercept       = 20252 // Intercept
	SpellBattleStance    = 2457
	SpellDefensiveStance = 71  // Defensive Stance (warrior tank form)
	SpellTaunt           = 355 // Taunt — 3.3.5a requires Defensive Stance (not Battle)

	SpellSweepingStrikes      = 12328
	SpellExecute              = 5308
	SpellRaiseDead            = 46584 // DK Raise Dead
	SpellBloodStrike          = 45902
	SpellBloodTap             = 45529
	SpellDismissPet           = 2641 // Dismiss Pet (hunter/warlock fallback)
	SpellGroundingTotem       = 8177
	SpellGroundingTotemEffect = 8178  // aura on the totem (consumed wrongly by AoE)
	SpellRainOfFire           = 5740  // rank 1; prefer SpellRainOfFireMax after learn-all
	SpellRainOfFireMax        = 47820 // Rain of Fire rank 7 (WotLK)
	SpellHellfire             = 1949  // warlock self-channel (reliable CancelCast probe)
	SpellHellfireMax          = 47823 // Hellfire rank 5 (WotLK)
	SpellBlendingInAura       = 45614 // Imbued Scourge Shroud effect
	SpellSummonTargetDummy    = 4071  // Target Dummy (engineering item spell)
	SpellSummonAdvDummy       = 4072  // Advanced Target Dummy
	SpellSummonMasterDummy    = 19805 // Masterwork Target Dummy
	SpellMountSwiftGryphon    = 32235 // Swift Blue Gryphon (flying)

	// Items
	ItemImbuedScourgeShroud = 34782
	ItemTargetDummy         = 4366
	ItemAdvTargetDummy      = 4392
	ItemMasterTargetDummy   = 16023
	ItemCorpseDust          = 37201 // Raise Dead reagent

	// Creatures
	CreatureTargetDummy         = 2673 // L1 low-HP; L80 autoattack often oneshots before combat flag
	CreatureAdvTargetDummy      = 2674
	CreatureMasterTargetDummy   = 12426
	CreatureHeroicTrainingDummy = 31146 // L83 high HealthModifier; stable Engage target
	CreatureKologarn            = 32930
	CreatureYorusBarleybrew     = 6166 // Rethban Gauntlet related
	CreatureGroundingTotem      = 5925

	// Quests
	QuestRethbanGauntlet = 1699  // QUEST_FLAGS_STAY_ALIVE (0x1)
	QuestBlendingIn      = 11633 // Borean Tundra cloak quest
	QuestWasteNotWantNot = 10055

	// Quest status (character_queststatus.status)
	QuestStatusNone       uint8 = 0
	QuestStatusComplete   uint8 = 1
	QuestStatusIncomplete uint8 = 3
	QuestStatusFailed     uint8 = 5

	// Maps / positions from AC issues
	MapEasternKingdoms = 0
	MapKalimdor        = 1
	MapOutland         = 530
	MapNorthrend       = 571
	MapUlduar          = 603
)

Spell / item / quest constants referenced by AC-issue scenario tests.

View Source
const CorpseReclaimRadius float32 = 39

CorpseReclaimRadius is AC CORPSE_RECLAIM_RADIUS (MiscHandler) — server silently rejects CMSG_RECLAIM_CORPSE if the ghost is farther than this.

View Source
const CreatureMechanoHog uint32 = 29929

CreatureMechanoHog is VehicleId=318 but unit_flags UNINTERACTIBLE — not a good click fixture. Kept as name alias only for docs; prefer CreatureStormwindSteed.

View Source
const CreatureStormwindSteed uint32 = 33217

CreatureStormwindSteed is a friendly outdoor vehicle (VehicleId=349, SPELLCLICK, no zone script). Prefer for pad-local EnterVehicle smoke; always despawn via Spawn cleanup. Note: Mechano-hog (29929) is UNINTERACTIBLE and will not accept SpellClick.

View Source
const DefaultCastTimeout = 10 * time.Second

DefaultCastTimeout is the default cast wait used by CastOrGM / CastRetries.

View Source
const DefaultGroupTimeout = 10 * time.Second

DefaultGroupTimeout is used by invite/list waiters when timeout <= 0.

View Source
const DefaultInWorldTimeout = 30 * time.Second

DefaultInWorldTimeout is used by WaitInWorld when timeout <= 0.

View Source
const DefaultNearPadDist float32 = 15

DefaultNearPadDist is the default max distance for AssertNearPad after .go xyz.

View Source
const DefaultSummonTimeout = 15 * time.Second

DefaultSummonTimeout is used by summon waiters when timeout <= 0.

View Source
const DefaultTradeTimeout = 10 * time.Second

DefaultTradeTimeout is used by trade waiters when timeout <= 0.

View Source
const DefaultVehicleTimeout = 10 * time.Second

DefaultVehicleTimeout is used by Enter/Exit vehicle waiters when timeout <= 0.

View Source
const (
	// GameObjectGiftOfTheObserver is Algalon's chest (GO 194821) used by #26894 loot tests.
	// `.gobject add` is a persistent DB spawn — always DespawnGameObjectSpawn after use.
	GameObjectGiftOfTheObserver uint32 = 194821
)

Game object / creature entries that e2e has been known to litter.

View Source
const MaxParallelLogins = 3

MaxParallelLogins caps concurrent bot logins to avoid authserver/world thrash. Authserver is single-threaded for sockets; world AUTH_CHALLENGE is cheap, but parallel enter-world + retries can amplify session_key races if clients leak. 3 is enough for typical multi-bot suites without saturating auth.

View Source
const SpellRideVehicleHardcoded uint32 = 46598

SpellRideVehicleHardcoded is VEHICLE_SPELL_RIDE_HARDCODED (46598) — control-vehicle aura.

Variables

View Source
var (
	AuthAddr = envOr("E2E_AUTH_ADDR", "127.0.0.1:3724")
	AuthDSN  = envOr("E2E_AUTH_DSN", "acore:acore@tcp(127.0.0.1:3306)/acore_auth")
	CharDSN  = envOr("E2E_CHAR_DSN", "acore:acore@tcp(127.0.0.1:3306)/acore_characters")
	WorldDSN = envOr("E2E_WORLD_DSN", "acore:acore@tcp(127.0.0.1:3306)/acore_world")
)

Connection settings for the target AzerothCore environment. Override via env without code changes (point at your AC or gateway entrypoint):

E2E_AUTH_ADDR  (default 127.0.0.1:3724)
E2E_AUTH_DSN   (default acore:acore@tcp(127.0.0.1:3306)/acore_auth)
E2E_CHAR_DSN   (default acore:acore@tcp(127.0.0.1:3306)/acore_characters)
E2E_WORLD_DSN  (default acore:acore@tcp(127.0.0.1:3306)/acore_world)
View Source
var CombatPads []Position3

CombatPads is the list of isolation positions (for iteration / tests).

View Source
var IsolationPads = []NamedPad{
	{Name: "Tower1", Pos: Position3{X: -9110.266, Y: 470.96655, Z: 137.20119, O: 0.85138357, Map: MapEasternKingdoms}},
	{Name: "Tower2", Pos: Position3{X: -9043.385, Y: 376.87408, Z: 137.45674, O: 0.7139602, Map: MapEasternKingdoms}},
	{Name: "AbandonHouse", Pos: Position3{X: -9297.518, Y: 652.88934, Z: 131.09396, O: 4.582049, Map: MapEasternKingdoms}},
	{Name: "NagrandArena", Pos: Position3{X: -2048.0647, Y: 6647.59, Z: 13.057503, O: 3.3466177, Map: MapOutland}},
	{Name: "FloatingIsland1", Pos: Position3{X: -2140.973, Y: 7758.02, Z: 154.28343, O: 0.2717825, Map: MapOutland}},
	{Name: "FloatingIsland2", Pos: Position3{X: -2500.6123, Y: 8585.174, Z: 189.1715, O: 1.3281429, Map: MapOutland}},
	{Name: "FloatingIsland3", Pos: Position3{X: -3095.3198, Y: 8858.52, Z: -162.47665, O: 3.8885531, Map: MapOutland}},
	{Name: "InMountains1", Pos: Position3{X: 1626.4886, Y: -3638.4875, Z: 215.53114, O: 4.8191752, Map: MapKalimdor}},
	{Name: "InMountains2", Pos: Position3{X: 1153.9037, Y: -2586.8608, Z: 252.40727, O: 3.7353268, Map: MapKalimdor}},
	{Name: "InMountains3", Pos: Position3{X: -1945.4078, Y: -3252.7837, Z: 186.5974, O: 2.235215, Map: MapKalimdor}},
	{Name: "ElwynnRidge", Pos: Position3{X: -8778.654, Y: -1538.7914, Z: 262.44308, O: 4.0515337, Map: MapEasternKingdoms}},
	{Name: "BurningSteppes", Pos: Position3{X: -8565.869, Y: -1749.3431, Z: 261.832, O: 5.3553014, Map: MapEasternKingdoms}},
	{Name: "Mulgore1", Pos: Position3{X: -1352.331, Y: 443.73453, Z: 84.17812, O: 1.4135575, Map: MapKalimdor}},
	{Name: "Mulgore3", Pos: Position3{X: -1346.4855, Y: 607.259, Z: 177.06775, O: 5.291848, Map: MapKalimdor}},
	{Name: "Mulgore4", Pos: Position3{X: -702.8023, Y: 431.2039, Z: 178.8715, O: 5.9869184, Map: MapKalimdor}},
	{Name: "Mulgore5", Pos: Position3{X: -499.61398, Y: 314.7678, Z: 178.87077, O: 5.36645, Map: MapKalimdor}},
	{Name: "MulgoreNorth", Pos: Position3{X: -320.3727, Y: 356.96695, Z: 129.69864, O: 5.986917, Map: MapKalimdor}},
	{Name: "Boulderslide", Pos: Position3{X: -248.6124, Y: -20.980942, Z: 68.75001, O: 5.3389664, Map: MapKalimdor}},
	{Name: "Stonetalon1", Pos: Position3{X: -316.0851, Y: -184.31485, Z: 88.23334, O: 4.5300026, Map: MapKalimdor}},
	{Name: "Stonetalon2", Pos: Position3{X: 866.51086, Y: -56.30018, Z: 229.49576, O: 6.03796, Map: MapKalimdor}},
	{Name: "Talondeep", Pos: Position3{X: 1684.0914, Y: -724.9744, Z: 212.20335, O: 3.952735, Map: MapKalimdor}},
	{Name: "Ashenvale1", Pos: Position3{X: 1771.8456, Y: -598.2746, Z: 219.08276, O: 5.884814, Map: MapKalimdor}},
	{Name: "FelwoodSouth", Pos: Position3{X: 3396.1155, Y: -1939.8342, Z: 441.16708, O: 6.273577, Map: MapKalimdor}},
	{Name: "MorlosAran", Pos: Position3{X: 3642.7356, Y: -1818.3837, Z: 432.13553, O: 3.2812169, Map: MapKalimdor}},
	{Name: "HyjalApproach", Pos: Position3{X: 4103.3022, Y: -2080.1174, Z: 776.1862, O: 0.81508166, Map: MapKalimdor}},
	{Name: "Hyjal1", Pos: Position3{X: 4516.697, Y: -2312.1873, Z: 1137.8563, O: 2.959214, Map: MapKalimdor}},
	{Name: "Hyjal2", Pos: Position3{X: 4591.0117, Y: -2661.0393, Z: 1133.3715, O: 3.6935647, Map: MapKalimdor}},
}

IsolationPads are far-apart world locations so `go test ./...` can run packages in parallel without sharing one Stormwind AOI. Assigned stickily per suite folder.

Source: operator-captured coords (SW towers, Elwynn house, Nagrand islands, Durotar peaks, Elwynn/Burning Steppes cliffs, Mulgore, Stonetalon, Ashenvale, Felwood, Hyjal).

View Source
var PadStormwindOutskirts = IsolationPads[2].Pos // AbandonHouse

PadStormwindOutskirts is a legacy alias for AbandonHouse (Elwynn isolation pad). Prefer PackagePad(t) so parallel packages do not share one location.

View Source
var PreferredPackagePads = map[string]string{
	"combat/threat":              "Tower1",
	"combat/death":               "Tower2",
	"combat/pets":                "AbandonHouse",
	"combat/charm":               "NagrandArena",
	"combat/vehicles":            "FloatingIsland1",
	"social/loot":                "FloatingIsland2",
	"social/group":               "FloatingIsland3",
	"social/trade":               "InMountains1",
	"spells/cast":                "InMountains2",
	"spells/effects":             "InMountains3",
	"spells/aura":                "Boulderslide",
	"quests/escort":              "Mulgore1",
	"quests/lifecycle":           "Mulgore3",
	"items/equip":                "Stonetalon1",
	"smoke":                      "ElwynnRidge",
	"protocol/session":           "Mulgore4",
	"protocol/teleport":          "Mulgore5",
	"guild/charter_bank":         "Stonetalon2",
	"instances/northrend/ulduar": "Ashenvale1",
}

PreferredPackagePads maps suite keys (relative to e2e/suites/ or "smoke") to pad names. Keep this 1:1 with IsolationPads for combat-heavy packages. Suites not listed get an unused pad if any remain, otherwise a stable hash share.

Functions

func ActivateGuildBank

func ActivateGuildBank(t *testing.T, leader *Session) (bankerGUID uint64, list *client.GuildBankList)

ActivateGuildBank teleports to the SW vault, resolves banker GUID, activates, and returns the first SMSG_GUILD_BANK_LIST. Asserts non-nil list; prefers FullUpdate (logs WARNING if only a partial list arrives).

func AddItem

func AddItem(t *testing.T, w *client.WorldClient, entry, count uint32)

AddItem grants item(s) via GM .additem (live world inventory; defaults to self). Callers that need inventory placement must wait on SMSG_ITEM_PUSH_RESULT.

func AddItemForBankDeposit

func AddItemForBankDeposit(t *testing.T, sess *Session, entry, count uint32) (bag, slot uint8)

AddItemForBankDeposit .additem's entry and returns bag/slot from SMSG_ITEM_PUSH_RESULT. Strips residual stacks first so shared-fixture withdraws do not cause 0xFFFFFFFF stack merges.

func AddQuest

func AddQuest(t *testing.T, w *client.WorldClient, questID uint32)

AddQuest adds a quest to the player's log via GM `.quest add`.

func ApplyAura

func ApplyAura(t *testing.T, w *client.WorldClient, spellID uint32)

ApplyAura applies an aura via GM `.aura <spell>` and waits until SelfHasAura reports it (requires correct SMSG_AURA_UPDATE parsing).

func AssertAuraConsumed

func AssertAuraConsumed(t *testing.T, w *client.WorldClient, spellID uint32, timeout time.Duration, issue int)

AssertAuraConsumed fails with CONFIRMED BUG if spellID is still present after timeout.

func AssertAuraRemains

func AssertAuraRemains(t *testing.T, w *client.WorldClient, spellID uint32, after time.Duration, issue int)

AssertAuraRemains fails with CONFIRMED BUG if spellID is missing after waiting `after`. Use after an action that must NOT consume the aura (e.g. mount while Blending In).

func AssertBugf

func AssertBugf(t *testing.T, issue int, format string, args ...any)

AssertBugf fails a product oracle. issue must be >0; otherwise Assertf.

func AssertIntervalNotAccelerated

func AssertIntervalNotAccelerated(t *testing.T, issue int, fromEvent, fromBaseline time.Duration, opts IntervalBugOpts)

AssertIntervalNotAccelerated fails with CONFIRMED BUG when both clocks look early. fromEvent is time since the kill/event; fromBaseline is time since the newer wave spawn.

func AssertNoIdleTargeters

func AssertNoIdleTargeters(t *testing.T, issue int, samples []TargetHoldSample, idleHold time.Duration, sampleEvery time.Duration)

AssertNoIdleTargeters fails with CONFIRMED BUG when any attacker held a valid target for at least idleHold without ever dealing HP damage to it.

func AssertQuestStatusEqual

func AssertQuestStatusEqual(t *testing.T, db *sql.DB, sess *Session, questID uint32, want uint8)

AssertQuestStatusEqual saves and asserts quest status.

func AssertUnitAuraStable

func AssertUnitAuraStable(t *testing.T, w *client.WorldClient, guid uint64, spellID uint32, window time.Duration, issue int)

AssertUnitAuraStable samples a unit aura every 50ms for `window`. Fails with CONFIRMED BUG if the aura is missing on two consecutive samples (debounce transient flicker gaps).

func Assertf

func Assertf(t *testing.T, format string, args ...any)

Assertf fails a post-drive behavioural oracle (not setup). Prefer over Preconditionf after the scenario has already been exercised.

func AttackUntilHealthBelow

func AttackUntilHealthBelow(t *testing.T, s *Session, targetGUID uint64, maxHealthFrac float64, timeout time.Duration)

AttackUntilHealthBelow swings at target until its health drops below threshold or timeout. Uses client object cache health.

func AuraPresent

func AuraPresent(w *client.WorldClient, spellID uint32) bool

AuraPresent is a non-fatal snapshot check.

func BankListItemCount

func BankListItemCount(list *client.GuildBankList, entry uint32) int

BankListItemCount returns how many non-empty slots match entry (0 = any non-empty).

func Base26

func Base26(v uint64, width int) string

Base26 encodes v as lowercase letters (width digits).

func BuyGuildBankTab

func BuyGuildBankTab(t *testing.T, leader *Session, bankerGUID uint64, tab uint8) *client.GuildBankList

BuyGuildBankTab purchases tab index and waits for an updated SMSG_GUILD_BANK_LIST.

func BuyGuildCharterWorld

func BuyGuildCharterWorld(t *testing.T, w *client.WorldClient, charDB *sql.DB, ownerGUID uint64, guildName string) (petitionItemGUID uint64, petitionID uint32)

BuyGuildCharterWorld is a thin wrapper for callers that only have WorldClient. Prefer BuyGuildCharter with a Session so item-push waiters work.

func CharacterInventoryCount

func CharacterInventoryCount(db *sql.DB, guid uint64, entry uint32) (int, error)

CharacterInventoryCount sums item_instance.count for entry in character_inventory.

func CharacterMoney

func CharacterMoney(db *sql.DB, guid uint64) (uint32, error)

CharacterMoney reads characters.money for guid.

func CheatGod

func CheatGod(t *testing.T, w *client.WorldClient)

CheatGod enables god mode via `.cheat god on`.

func CheatPower

func CheatPower(t *testing.T, w *client.WorldClient)

CheatPower enables infinite power (rage/energy/mana) via `.cheat power on`.

func CleanupGuildForLeader

func CleanupGuildForLeader(db *sql.DB, leaderGUID uint64)

CleanupGuildIfEmpty deletes guild rows when the guild has no members left (and always removes bank state for the guild if leader matches).

func CleanupPlayer

func CleanupPlayer(db *sql.DB, guid uint64) error

CleanupPlayer removes guild membership and petition rows for a character guid.

func CleanupSessionsGuildState

func CleanupSessionsGuildState(t *testing.T, charDB *sql.DB, sessions []*Session)

CleanupSessionsGuildState clears guild/petition leftovers for every session.

func CombatReady

func CombatReady(t *testing.T, w *client.WorldClient, opts CombatReadyOpts)

CombatReady prepares a bot for real combat:

  • `.gm off` so NPCs can aggro (GM mode blocks threat / can evade mid-fight)
  • optional god / power cheats

Account GM security still allows `.damage` / `.go` without GM *mode*. Do NOT call `.gm on` mid-fight — bosses may evade/reset.

func CombatReadyDefaults

func CombatReadyDefaults(t *testing.T, w *client.WorldClient)

CombatReadyDefaults is CombatReady with god on (power off).

func CompleteTrade

func CompleteTrade(t *testing.T, a, b *ScenarioBot)

CompleteTrade dual-accepts an open trade and waits for TRADE_COMPLETE on both sides.

Protocol (AC TradeHandler): first AcceptTrade marks that side accepted and notifies the partner with TRADE_ACCEPT; second AcceptTrade while the partner is still accepted runs the complete path. We arm complete handlers first, then send both AcceptTrade packets back-to-back (no fixed sleep). Retries on BACK_TO_TRADE while the window stays open (item/gold settle races or TRADE_DISTANCE checks under pad noise).

func ComputeSRP6

func ComputeSRP6(username, password string) (salt, verifier []byte)

ComputeSRP6 returns salt and verifier matching ToCloud9 authserver / AzerothCore: v = g^x mod N with x = SHA1(salt | SHA1(username:password)) as little-endian int.

func ConfirmedBugf

func ConfirmedBugf(t *testing.T, issue int, format string, args ...any)

ConfirmedBugf fails with the standard AC-issue bug marker. issue is the AzerothCore issue or PR number (e.g. 27095).

func CountGuildBankItems

func CountGuildBankItems(db *sql.DB, guildID uint32, tab int) (int, error)

CountGuildBankItems returns number of rows in guild_bank_item for guild/tab (tab < 0 = all).

func CountGuildBankTabs

func CountGuildBankTabs(db *sql.DB, guildID uint32) (int, error)

CountGuildBankTabs returns purchased tab count.

func CountLivingByEntry

func CountLivingByEntry(w *client.WorldClient, maxDist float32, entries ...uint32) map[uint32]int

CountLivingByEntry counts living units per entry.

func CountLivingWithRetry

func CountLivingWithRetry(w *client.WorldClient, maxDist float32, entries []uint32, timeout time.Duration) (n int, guids []uint64)

CountLivingWithRetry re-polls living count for entries until non-zero or timeout. Use after mass DESTROY when the object cache may lag.

func DamageGM

func DamageGM(t *testing.T, w *client.WorldClient, targetGUID uint64, amount uint32)

DamageGM applies `.damage <amount>` to targetGUID. Does NOT toggle GM mode.

func DamageKillGM

func DamageKillGM(t *testing.T, w *client.WorldClient, guids []uint64, amount uint32, timeout time.Duration)

DamageKillGM damages each GUID until hp==0 or timeout. Does NOT toggle GM mode.

func DepositItemToBank

func DepositItemToBank(t *testing.T, leader *Session, bankerGUID uint64, tab, slot, bag, bagSlot uint8, entry uint32) *client.GuildBankList

DepositItemToBank deposits bag/slot into bank and waits until entry count rises.

func Die

func Die(t *testing.T, w *client.WorldClient)

Die kills the player via GM `.die` (requires target self). Prefer DieMust on ScenarioBot — bare Die can no-op if selection is not applied yet.

func DisbandParty

func DisbandParty(t *testing.T, bots ...*ScenarioBot)

DisbandParty has every bot leave (best-effort) so leftover groups don't leak across tests.

func Distance3D

func Distance3D(x1, y1, z1, x2, y2, z2 float32) float32

Distance3D is Euclidean distance between two points.

func DrainPlayerMoney

func DrainPlayerMoney(t *testing.T, w *client.WorldClient)

DrainPlayerMoney best-effort zeros player copper via repeated negative .modify money so deposit-insufficient tests are not polluted by fixture funding. Uses modest deltas (large negatives have been observed to misbehave on some cores).

func EnableGM

func EnableGM(t *testing.T, w *client.WorldClient)

EnableGM turns on GM mode via chat (requires account GM level).

func EnableHostilePvP

func EnableHostilePvP(t *testing.T, a, b *ScenarioBot)

EnableHostilePvP enables PvP on both bots (for cross-faction combat).

func EngageUntilCombat

func EngageUntilCombat(t *testing.T, w *client.WorldClient, targetGUID uint64, timeout time.Duration)

EngageUntilCombat faces and attacks target until the pull is observed, or timeout. Pull is accepted when either:

  • UNIT_FLAG_IN_COMBAT is set on the target, or
  • target HP drops while still alive (training dummies often take .damage / swings without ever setting IN_COMBAT — Heroic Training Dummy is the common case), or
  • the unit is targeting this player (thrash pads may clear IN_COMBAT briefly).

Falls back to `.damage 1` (without enabling GM mode) if swings alone do not pull.

If the unit dies before a pull is observed (common with L1 target dummies vs L80 autoattack), fails immediately with a oneshot precondition instead of burning the full timeout swinging a corpse.

func EnsureAccount

func EnsureAccount(db *sql.DB, username, password string) error

EnsureAccount creates or updates an account with the given password (SRP6).

func EnsureBotAccounts

func EnsureBotAccounts(t *testing.T, authDB *sql.DB, idents []BotIdent)

EnsureBotAccounts creates accounts and sets GM level 3 for each identity.

func EnsureGuildBankTabReady

func EnsureGuildBankTabReady(t *testing.T, leader *Session, bankerGUID uint64, list *client.GuildBankList) *client.GuildBankList

EnsureGuildBankTabReady hard-fails unless the leader has at least one purchased bank tab visible in SMSG_GUILD_BANK_LIST TabInfos (after optional buy + re-list).

func FaceUnit

func FaceUnit(t *testing.T, w *client.WorldClient, targetGUID uint64)

FaceUnit turns the player toward a tracked unit GUID (MSG_MOVE_SET_FACING).

func FillBackpackWithJunk

func FillBackpackWithJunk(t *testing.T, sess *Session)

FillBackpackWithJunk best-effort floods bags via .additem (for full-bag withdraw tests).

func FirstBankListItemSlot

func FirstBankListItemSlot(list *client.GuildBankList, entry uint32) (uint8, bool)

FirstBankListItemSlot returns the first slot with the given entry, or false.

func FlushWorld

func FlushWorld(t *testing.T, w *client.WorldClient)

FlushWorld sends `.gps` and waits for SMSG_MESSAGECHAT.

Same-session CMSG_MESSAGECHAT GM commands are handled in order on the world thread. A sys-message after `.gps` means every MustGM issued on this bot before this call has already been applied (.pvp on, .gm off, .learn, …). It does not wait for another bot's commands — call it on each session that queued setup.

func FormParty

func FormParty(t *testing.T, leader *ScenarioBot, members ...*ScenarioBot)

FormParty invites each member and has them accept; waits for leader roster size. All bots should be same faction and co-located. Leader is inviter. Retries invite under pad load (invite can be dropped if name lookup races post-tele).

func FormPartyAtPad

func FormPartyAtPad(t *testing.T, pad Position3, leader *ScenarioBot, members ...*ScenarioBot)

FormPartyAtPad teleports leader+members to pad then FormParty. Common multi-bot setup for group/loot/threat tests.

func FormatGUID

func FormatGUID(guid uint64) string

FormatGUID is a short hex GUID for logs.

func GiveShamanTotems

func GiveShamanTotems(t *testing.T, w *client.WorldClient)

GiveShamanTotems adds the four basic totem items required to cast totems.

func GoCreatureGUID

func GoCreatureGUID(t *testing.T, w *client.WorldClient, spawnGUID uint32)

GoCreatureGUID teleports to a creature by DB spawn guid (`.go creature N`).

func GoCreatureID

func GoCreatureID(t *testing.T, w *client.WorldClient, entry uint32)

GoCreatureID teleports to a creature by template entry (`.go creature id N`). Prefer this after `.tele` when the pad is short of melee range.

func GuildBankMoney

func GuildBankMoney(db *sql.DB, guildID uint32) (uint64, error)

GuildBankMoney returns guild.BankMoney for guildID.

func HardDisconnectAndProbe

func HardDisconnectAndProbe(t *testing.T, victim, probe *ScenarioBot, issue int)

HardDisconnectAndProbe closes victim without logout and probes world via probe bot.

func HarnessFailf

func HarnessFailf(t *testing.T, format string, args ...any)

HarnessFailf fails for infra problems (timeouts, cache empty, cast never started) that are not themselves the AC bug under test.

func ItemGUID

func ItemGUID(low uint32) uint64

ItemGUID packs a HighGuid::Item ObjectGuid from the item_instance low guid.

func LatestCreatureSpawnNear

func LatestCreatureSpawnNear(db *sql.DB, entry uint32, mapID uint32, x, y, radius float32) (spawnID uint32, ok bool, err error)

LatestCreatureSpawnNear returns the highest creature.guid for entry near (x,y) on map.

func LatestGameObjectSpawnNear

func LatestGameObjectSpawnNear(db *sql.DB, entry uint32, mapID uint32, x, y, radius float32) (spawnID uint32, ok bool, err error)

LatestGameObjectSpawnNear returns the highest gameobject.guid for entry near (x,y) on map.

func LearnAllMyClass

func LearnAllMyClass(t *testing.T, w *client.WorldClient)

LearnAllMyClass runs `.learn all my class` (class spells + talents ranks available).

func LearnSpell

func LearnSpell(t *testing.T, w *client.WorldClient, spellID uint32)

LearnSpell learns a single spell by ID via GM and waits until KnowsSpell.

func ListCreatureSpawnsNear

func ListCreatureSpawnsNear(db *sql.DB, entry uint32, mapID uint32, x, y, radius float32) ([]uint32, error)

ListCreatureSpawnsNear returns all creature.guid for entry near (x,y) on map.

func ListGameObjectSpawnsNear

func ListGameObjectSpawnsNear(db *sql.DB, entry uint32, mapID uint32, x, y, radius float32) ([]uint32, error)

ListGameObjectSpawnsNear returns all gameobject.guid for entry near (x,y) on map.

func LivingByEntries

func LivingByEntries(w *client.WorldClient, maxDist float32, entries ...uint32) []uint64

LivingByEntries returns GUIDs of living units matching entries.

func ModMoney

func ModMoney(t *testing.T, w *client.WorldClient, copper uint32)

ModMoney adds copper via GM `.modify money` (live world path). AC's modify money targets the selected player only — select self first. Single command only (do not also send `.mod money`) to avoid double credit.

func MustCastSuccess

func MustCastSuccess(t *testing.T, s *Session, spellID uint32, targetGUID uint64, timeout time.Duration)

MustCastSuccess casts and fails the test unless SMSG_SPELL_GO is received.

func MustGM

func MustGM(t *testing.T, w *client.WorldClient, cmd string)

MustGM sends a GM command. No fixed sleep — callers that need completion must wait on the matching packet (teleport seq, item push, bank list, …).

func MustGMTeleport

func MustGMTeleport(t *testing.T, w *client.WorldClient, cmd string)

MustGMTeleport sends a GM teleport (`.go …`) and waits for self near/far teleport completion via SMSG_MOVE_TELEPORT / SMSG_NEW_WORLD phase cycle.

func MustQuestStatus

func MustQuestStatus(t *testing.T, db *sql.DB, charGUID uint64, questID uint32) (status uint8, ok bool)

MustQuestStatus fails the test on SQL error.

func OpenAuthDB

func OpenAuthDB() (*sql.DB, error)

OpenAuthDB opens the auth database (acore_auth).

func OpenCharDB

func OpenCharDB() (*sql.DB, error)

OpenCharDB opens the characters database (acore_characters).

func OpenTestDBs

func OpenTestDBs(t *testing.T) (authDB, charDB *sql.DB)

OpenTestDBs opens auth + character DBs or fails the test.

func OpenTrade

func OpenTrade(t *testing.T, initiator, target *ScenarioBot)

OpenTrade: initiator InitiateTrade → target BEGIN_TRADE → CMSG_BEGIN_TRADE → both OPEN_WINDOW. Retries under suite load. Failures report both bots' last statuses; TradeStatusBusy==0 so "none" is used when no SMSG_TRADE_STATUS was ever seen.

func OpenWorldDB

func OpenWorldDB() (*sql.DB, error)

OpenWorldDB opens the world database (acore_world) — used for spawn-id cleanup of persistent `.npc add` / `.gobject add` residue.

func Position

func Position(w *client.WorldClient) (x, y, z float32, mapID uint32)

Position returns current player position.

func Preconditionf

func Preconditionf(t *testing.T, format string, args ...any)

Preconditionf fails the test: setup did not reach a state where the issue can be evaluated. Prefer this over a bare Fatalf for missing NPCs, failed auras, etc.

func ProbeWorldAlive

func ProbeWorldAlive(t *testing.T, probe *ScenarioBot, issue int)

ProbeWorldAlive checks that a probe bot's world session still responds. Use after risky casts that may crash the worldserver.

func QueryGuildBankTab

func QueryGuildBankTab(t *testing.T, leader *Session, bankerGUID uint64, tab uint8, fullUpdate bool) *client.GuildBankList

QueryGuildBankTab sends CMSG_GUILD_BANK_QUERY_TAB and waits for SMSG_GUILD_BANK_LIST.

func QuestStatus

func QuestStatus(db *sql.DB, charGUID uint64, questID uint32) (status uint8, ok bool, err error)

QuestStatus returns character_queststatus.status for guid+quest, or (0, false) if no row (quest never taken / wiped).

Call SaveCharacter first for online players so the worldserver flushes.

func QuestStatusName

func QuestStatusName(status uint8) string

QuestStatusName is a debug label for status bytes.

func ReleaseSpirit

func ReleaseSpirit(t *testing.T, w *client.WorldClient)

ReleaseSpirit sends CMSG_REPOP_REQUEST after death.

func Require

func Require(t *testing.T, ok bool, format string, args ...any)

Require fatals as a precondition when ok is false.

func RequireUnit

func RequireUnit(t *testing.T, guid uint64, what string)

RequireUnit fatals as a precondition when guid is 0.

func ResolveStormwindGuildVaultGUID

func ResolveStormwindGuildVaultGUID(t *testing.T, w *client.WorldClient) uint64

ResolveStormwindGuildVaultGUID teleports to the vault and returns a banker GUID. Prefer live UPDATE_OBJECT GUID when the vault is visible to the client; otherwise pack DB spawn-id (server CanPlayerInteractWithGO resolves spawn-id store).

func RitualSummon

func RitualSummon(t *testing.T, initiator, helper, farTarget *ScenarioBot) (portalGUID uint64)

RitualSummon runs the 3-role summon portal path (meeting-stone GO 179944):

  1. initiator — selects farTarget, spawns portal, first click (ritual owner)
  2. helper — nearby second click (reqParticipants=2 completes the ritual)
  3. farTarget — after ~5s ritual cooldown, receives SMSG_SUMMON_REQUEST → AcceptSummon

initiator and helper must be co-located (same map + instance); farTarget may be elsewhere (FindPlayer). Prefer placing all three in a dungeon instance first — outdoor PackagePad is the wrong fixture for bind/summon tests. All three must share a party (castersGrouped=1).

Arm farTarget.ArmSummonRequest before calling. The server only fires EffectSummonPlayer after the GO update cooldown (~5s) once unique participants are full.

func SampleUntil

func SampleUntil(t *testing.T, every, timeout time.Duration, fn func() bool)

SampleUntil calls fn every `every` until it returns true or timeout (harness fail).

func SanitizeCharName

func SanitizeCharName(s string) string

SanitizeCharName keeps only letters and ensures 2..12 length for 3.3.5 create. Digits are rejected by the server as CHAR_NAME_MIXED_LANGUAGES (0x5D).

func SaveCharacter

func SaveCharacter(t *testing.T, w *client.WorldClient)

SaveCharacter forces a DB flush via `.save` so quest/char queries see live state.

func SeedGuildCharter

func SeedGuildCharter(db *sql.DB, ownerGUID uint64, guildName string) (itemLow uint32, petitionID uint32, err error)

SeedGuildCharter inserts a guild charter item + petition row for ownerGUID. The owner must be offline (or re-login after seed) so worldserver loads the item.

func SeedStackableItem

func SeedStackableItem(db *sql.DB, ownerGUID uint64, itemEntry, count uint32) (itemLow uint32, bag uint32, slot uint8, err error)

SeedStackableItem places a tradable stackable item into the first free backpack slot. Character should be offline (or re-login after seed).

func SessionAlive

func SessionAlive(s *Session) bool

SessionAlive reports whether the world session is still usable (socket open).

func SetGM

func SetGM(db *sql.DB, username string, level int) error

SetGM sets GM level for an account on all realms (RealmID=-1).

func SetLevel

func SetLevel(t *testing.T, w *client.WorldClient, level int)

SetLevel sets character level via GM `.character level N` (absolute). Falls back to `.levelup` delta if the absolute command is unavailable.

func SetupGuildLeader

func SetupGuildLeader(t *testing.T, accountPrefix string) (leader *Session, setup GuildSetup, charDB *sql.DB)

SetupGuildLeader creates a full guild (1 leader + MinPetitionSigns signers) via the charter protocol and returns the leader session + setup metadata. Use for bank-focused tests that need a guild but are not testing charter itself. Sessions are closed via t.Cleanup. For package-shared bank fixtures prefer SetupGuildLeaderKeepAlive + explicit Close of returned sessions.

func SetupGuildLeaderKeepAlive

func SetupGuildLeaderKeepAlive(t *testing.T, accountPrefix string) (leader *Session, setup GuildSetup, authDB, charDB *sql.DB)

SetupGuildLeaderKeepAlive is like SetupGuildLeader but does not register t.Cleanup on sessions or DBs, and closes signers after turn-in (leader stays up). Caller must Close the returned leader and DBs (e.g. from TestMain).

func ShowPetitionSignatures

func ShowPetitionSignatures(t *testing.T, sess *Session, petitionGUID uint64) *client.PetitionShowSignatures

ShowPetitionSignatures requests and returns SMSG_PETITION_SHOW_SIGNATURES.

func SignPetition

func SignPetition(t *testing.T, signer *Session, petitionGUID uint64) *client.PetitionSignResults

SignPetition sends CMSG_PETITION_SIGN and asserts SMSG_PETITION_SIGN_RESULTS OK on signer.

func SoftPass

func SoftPass(t *testing.T, reason string, format string, args ...any)

SoftPass is disabled by default: it fails the test so suites cannot greenwash unjudgeable fixtures. Opt in only for temporary local debugging via E2E_ALLOW_SOFT_PASS=1 (still logs SOFT-PASS for grepping).

Prefer Preconditionf / Assertf / ConfirmedBugf instead of SoftPass in new tests.

func SoftPassf

func SoftPassf(t *testing.T, format string, args ...any)

SoftPassf is SoftPass with reason taken from the format string prefix.

func SoftWarnf

func SoftWarnf(t *testing.T, format string, args ...any)

SoftWarnf logs a non-fatal WARNING (soft-pass diagnostics).

func SpawnNPC

func SpawnNPC(t *testing.T, w *client.WorldClient, entry uint32)

SpawnNPC spawns a temporary creature via `.npc add temp <entry>`.

func SpawnNPCAndWait

func SpawnNPCAndWait(t *testing.T, w *client.WorldClient, entry uint32, timeout time.Duration) uint64

SpawnNPCAndWait spawns a temp creature and waits for its live GUID in the object cache.

func SpellFailReasonName

func SpellFailReasonName(reason uint8) string

SpellFailReasonName maps 3.3.5a SpellCastResult codes to short names (AC SharedDefines).

func TeleNamed

func TeleNamed(t *testing.T, w *client.WorldClient, name string)

TeleNamed runs `.tele <name>` and waits for far transfer completion. Packet-driven: WaitForTeleportAfter (SMSG_NEW_WORLD / MOVE_TELEPORT) then PhaseInWorld. Callers that need nearby units must still WaitUnit / WaitNearbyUnitByEntry after return.

func TeleportAll

func TeleportAll(t *testing.T, bots []*ScenarioBot, x, y, z float32, mapID uint32)

TeleportAll teleports every bot to the same coordinates.

func TeleportAllPad

func TeleportAllPad(t *testing.T, bots []*ScenarioBot, pad Position3)

TeleportAllPad teleports every bot to pad via .go xyz (same as TeleportPad per bot). Prefer this over TeleportAll(..., pad.X, pad.Y, pad.Z, pad.Map).

func TeleportGo

func TeleportGo(t *testing.T, w *client.WorldClient, x, y, z float32, mapID uint32)

TeleportGo teleports the player with AC `.go xyz x y z [map]` (live world). Note: bare `.go x y z` is not a valid AC command (needs the `xyz` subcommand). Waits for self teleport completion (SMSG_MOVE_TELEPORT / SMSG_NEW_WORLD). Near-tele does not wipe the client object cache (see handleMoveTeleportAck), but destination creates arrive asynchronously — callers that need nearby units must still poll the object cache after this returns.

func TeleportPad

func TeleportPad(t *testing.T, w *client.WorldClient, pad Position3)

TeleportPad teleports to a named pad via .go xyz.

func TeleportToStormwindGuildVault

func TeleportToStormwindGuildVault(t *testing.T, w *client.WorldClient)

TeleportToStormwindGuildVault places the player at guild vault guid=41911 coordinates (acore_world gameobject) via `.go xyz`.

func TeleportToStormwindTabardDesigner

func TeleportToStormwindTabardDesigner(t *testing.T, w *client.WorldClient) uint64

TeleportToStormwindTabardDesigner places the player at Aldwin Laughlin (acore_world creature guid=79681, entry 4974) via one `.go creature` and waits until the live unit is in the client object cache. Returns that live ObjectGuid.

Near-tele ACK returns before SMSG_UPDATE_OBJECT fills nearby units. Wait once for entry 4974 — do not re-tele, offset, or .respawn while waiting (another tele restarts AOI and commonly leaves only patrollers/critters in cache).

func TeleportXYZ

func TeleportXYZ(t *testing.T, w *client.WorldClient, x, y, z float32, mapID uint32)

TeleportXYZ is TeleportGo with a clearer scenario name.

func TryNearbyGameObjectByEntry

func TryNearbyGameObjectByEntry(t *testing.T, w *client.WorldClient, entry uint32, timeout time.Duration) uint64

TryNearbyGameObjectByEntry is like WaitNearbyGameObjectByEntry but returns 0 on timeout.

func TurnInPetition

func TurnInPetition(t *testing.T, owner *Session, petitionGUID uint64, timeout time.Duration) uint32

TurnInPetition sends CMSG_TURN_IN_PETITION and returns SMSG_TURN_IN_PETITION_RESULTS.

func UniqueGuildName

func UniqueGuildName(prefix string) string

UniqueGuildName returns a short unique guild name for this run.

func UniqueLetterNames

func UniqueLetterNames(preferred string, n int) []string

UniqueLetterNames returns candidate pure-letter character names.

func UnitHasAura

func UnitHasAura(w *client.WorldClient, guid uint64, spellID uint32) bool

UnitHasAura reports whether a tracked unit currently has spellID as an aura.

func UnitHealth

func UnitHealth(w *client.WorldClient, guid uint64) (hp, max uint32)

UnitHealth returns cached health for a unit GUID (0 if unknown).

func UnitInCombat

func UnitInCombat(w *client.WorldClient, guid uint64) bool

UnitInCombat reports UNIT_FLAG_IN_COMBAT on a tracked unit.

func UnitIsPvP

func UnitIsPvP(w *client.WorldClient, guid uint64) bool

UnitIsPvP reports UNIT_BYTE2_FLAG_PVP on a cached unit (false if unknown).

func UnitTargetGUID

func UnitTargetGUID(w *client.WorldClient, guid uint64) uint64

UnitTargetGUID reads UNIT_FIELD_TARGET (2×uint32) from a tracked unit, or 0.

func UnitTargetGUIDFromObj

func UnitTargetGUIDFromObj(o *client.WorldObject) uint64

UnitTargetGUIDFromObj reads UNIT_FIELD_TARGET from an object.

func WaitAlive

func WaitAlive(t *testing.T, w *client.WorldClient, timeout time.Duration)

WaitAlive is the package-level helper.

func WaitAura

func WaitAura(t *testing.T, w *client.WorldClient, spellID uint32, timeout time.Duration)

WaitAura waits until SelfHasAura(spellID) is true.

func WaitAuraGone

func WaitAuraGone(t *testing.T, w *client.WorldClient, spellID uint32, timeout time.Duration) bool

WaitAuraGone waits until SelfHasAura(spellID) is false.

func WaitAuraGoneFatal

func WaitAuraGoneFatal(t *testing.T, w *client.WorldClient, spellID uint32, timeout time.Duration)

WaitAuraGoneFatal is WaitAuraGone that fatals on timeout (aligned with WaitAura).

func WaitBankListItemCount

func WaitBankListItemCount(t *testing.T, leader *Session, bankerGUID uint64, tab uint8, entry uint32, wantCount int, timeout time.Duration) *client.GuildBankList

WaitBankListItemCount polls until the tab list has exactly wantCount of entry.

func WaitBankMoneyAtLeast

func WaitBankMoneyAtLeast(t *testing.T, leader *Session, bankerGUID uint64, want uint64, timeout time.Duration) uint64

WaitBankMoneyAtLeast polls until bank Money >= want.

func WaitBankMoneyAtMost

func WaitBankMoneyAtMost(t *testing.T, leader *Session, bankerGUID uint64, want uint64, timeout time.Duration) uint64

WaitBankMoneyAtMost polls until bank Money <= want.

func WaitDead

func WaitDead(t *testing.T, w *client.WorldClient, timeout time.Duration)

WaitDead polls until player health is 0 (or timeout).

func WaitInWorld

func WaitInWorld(t *testing.T, w *client.WorldClient, timeout time.Duration)

WaitInWorld is the package-level helper for a bare WorldClient.

func WaitNearbyGameObjectByEntry

func WaitNearbyGameObjectByEntry(t *testing.T, w *client.WorldClient, entry uint32, timeout time.Duration) uint64

WaitNearbyGameObjectByEntry polls for a gameobject create with the given entry. AC GameObject ObjectGuids use runtime counters (not creature/gameobject.guid spawn ids).

func WaitNearbyUnitAnyEntry

func WaitNearbyUnitAnyEntry(t *testing.T, w *client.WorldClient, entries []uint32, timeout time.Duration) (guid uint64, entry uint32)

WaitNearbyUnitAnyEntry waits for any of the given template entries. Returns guid and the matched entry.

func WaitNearbyUnitByEntry

func WaitNearbyUnitByEntry(t *testing.T, w *client.WorldClient, entry uint32, timeout time.Duration) uint64

WaitNearbyUnitByEntry polls the WorldClient object cache for a unit with the given template entry (from live SMSG_UPDATE_OBJECT after teleport). Modern AC assigns runtime ObjectGuid counters (not DB spawn ids), so petition buy / vendor interact must use the live GUID. Entry match uses OBJECT_FIELD_ENTRY when present, else the 24-bit entry packed into map-specific Unit ObjectGuids.

func WaitNearbyUnitEntry

func WaitNearbyUnitEntry(t *testing.T, w *client.WorldClient, entry uint32, timeout time.Duration) uint64

WaitNearbyUnitEntry waits for a live unit with the given template entry. Deprecated: prefer ScenarioBot.WaitUnit.

func WaitQuestStatus

func WaitQuestStatus(t *testing.T, db *sql.DB, charGUID uint64, questID uint32, want uint8, timeout time.Duration) uint8

WaitQuestStatus polls DB until status matches want. Prefer AssertQuestStatusEqual after SaveCharacter for one-shot checks.

func WaitSelfPvP

func WaitSelfPvP(t *testing.T, w *client.WorldClient, timeout time.Duration)

WaitSelfPvP waits until this session's own player object is PvP-flagged.

func WaitUnitAura

func WaitUnitAura(t *testing.T, w *client.WorldClient, guid uint64, spellID uint32, timeout time.Duration)

WaitUnitAura waits until UnitHasAura is true (or fails the test).

func WaitUnitCombat

func WaitUnitCombat(t *testing.T, w *client.WorldClient, guid uint64, timeout time.Duration)

WaitUnitCombat waits until UnitInCombat is true.

func WaitUnitDead

func WaitUnitDead(t *testing.T, w *client.WorldClient, guid uint64, timeout time.Duration)

WaitUnitDead waits until unit health is 0 or object gone.

func WaitUnitHealthChanged

func WaitUnitHealthChanged(t *testing.T, w *client.WorldClient, guid uint64, before uint32, timeout time.Duration) (hp uint32)

WaitUnitHealthChanged waits until unit health differs from before.

func WaitUnitPvP

func WaitUnitPvP(t *testing.T, w *client.WorldClient, guid uint64, timeout time.Duration)

WaitUnitPvP waits until the unit is PvP-flagged in this client's object cache.

func WaitUnitTarget

func WaitUnitTarget(t *testing.T, w *client.WorldClient, guid, wantTarget uint64, timeout time.Duration)

WaitUnitTarget is the package-level waiter used by multi-bot threat tests.

func WithdrawItemFromBank

func WithdrawItemFromBank(t *testing.T, leader *Session, bankerGUID uint64, tab, slot uint8, entry uint32) *client.GuildBankList

WithdrawItemFromBank withdraws bank tab/slot and waits until entry count drops.

Types

type BotIdent

type BotIdent struct {
	Account  string
	CharName string
	Race     uint8
	Class    uint8
}

BotIdent is a unique account + character name pair for one e2e bot. Race/Class are optional (0 = Human Warrior defaults at login).

func MakeBotIdents

func MakeBotIdents(prefix string, n int) []BotIdent

MakeBotIdents builds n unique account/char pairs with a shared run suffix. Character names are pure letters (server rejects digits as mixed languages).

func MakeBotIdentsRaceClass

func MakeBotIdentsRaceClass(prefix string, n int, race, class uint8) []BotIdent

MakeBotIdentsRaceClass is MakeBotIdents with race/class on each identity.

type BotSpec

type BotSpec struct {
	Role          string // optional label (e.g. "shaman", "victim")
	Race          uint8
	Class         uint8
	Level         int
	LearnAllClass bool
}

BotSpec describes one bot inside a heterogeneous multi-bot scenario. Zero Race/Class default to Human/Warrior; zero Level skips leveling.

type CharterBuyResult

type CharterBuyResult struct {
	PetitionItemGUID uint64
	PetitionID       uint32 // DB petition_id when available; SMSG may use item low
	ItemLow          uint32
	ItemPush         *client.ItemPushResult
	ShowEmpty        *client.PetitionShowSignatures
}

CharterBuyResult is the protocol outcome of CMSG_PETITION_BUY.

func BuyGuildCharter

func BuyGuildCharter(t *testing.T, sess *Session, charDB *sql.DB, guildName string) CharterBuyResult

BuyGuildCharter teleports to the SW tabard designer and sends CMSG_PETITION_BUY. Primary success is protocol:

  1. SMSG_ITEM_PUSH_RESULT with entry=Guild Charter (5863) — required (hard fail if missing)
  2. SMSG_PETITION_SHOW_SIGNATURES empty after CMSG_PETITION_SHOW_SIGNATURES

The petition item ObjectGuid is not present in ITEM_PUSH; charDB is used only *after* a successful push to resolve petitionguid/item low for subsequent petition opcodes (identity). DB alone is never treated as buy success. No SeedGuildCharter.

type CombatReadyOpts

type CombatReadyOpts struct {
	// God enables `.cheat god on`.
	God bool
	// Power enables `.cheat power on`.
	Power bool
}

CombatReadyOpts configures CombatReady.

type DamageTracker

type DamageTracker struct {
	// contains filtered or unexported fields
}

DamageTracker records SMSG_ATTACKERSTATEUPDATE damage by victim GUID. Install via ArmDamageTracker; safe with Session packet waiters via multi-subscriber AddPacketHook (no OnPacket clobber).

func ArmDamageTracker

func ArmDamageTracker(s *Session) *DamageTracker

ArmDamageTracker registers a packet hook to accumulate auto-attack damage. Safe with ArmAllWaiters (multi-subscriber AddPacketHook — no clobber). Hook stays for session lifetime (cancel not returned; e2e tests are short-lived).

type GuildSetup

type GuildSetup struct {
	Leader       *Session
	Signers      []*Session
	All          []*Session
	GuildName    string
	PetitionGUID uint64
	ItemLow      uint32
}

GuildSetup is a leader (+ optional signers) after successful guild creation.

func CreateGuildViaCharter

func CreateGuildViaCharter(t *testing.T, leader *Session, signers []*Session, charDB *sql.DB, guildName string, minSigns int) GuildSetup

CreateGuildViaCharter buys a charter, collects minSigns signatures, shows, and turns in. minSigns should match charserver MIN_PETITION_SIGNS (default 9). Requires len(signers) >= minSigns. Asserts SMSG_TURN_IN_PETITION_RESULTS == OK.

type IntervalBugOpts

type IntervalBugOpts struct {
	// MaxFromEvent is the post-kill (or post-event) window that indicates the bug
	// (default 20s — accelerated ~5s misfire).
	MaxFromEvent time.Duration
	// MaxFromBaseline is the max acceptable gap from the baseline spawn
	// (default 45s — half of a normal ~60s wave).
	MaxFromBaseline time.Duration
}

IntervalBugOpts configures AssertIntervalNotAccelerated.

type LoginOptions

type LoginOptions struct {
	AuthAddr string
	User     string
	Password string
	CharName string
	Race     uint8
	Class    uint8
}

LoginOptions controls character login/create.

type NamedPad

type NamedPad struct {
	Name string
	Pos  Position3
}

NamedPad is a hand-picked isolation location for package-parallel e2e.

type Position3

type Position3 struct {
	X, Y, Z float32
	Map     uint32
	// O is optional facing (radians); 0 is fine when unused.
	O float32
}

Position3 is a map position used by pads and StartPad opts.

func PackagePad

func PackagePad(t testing.TB) Position3

PackagePad returns the isolation pad for this test's package (suite folder). All tests in the same package share one pad for the process lifetime so multi-bot scenarios stay co-located; different packages get different pads when possible.

Use with: go test ./... -parallel 1 (serial tests per package, parallel packages).

func PadFor

func PadFor(t testing.TB) Position3

PadFor is an alias of PackagePad (package-level isolation). Prefer PackagePad for clarity.

type ScenarioBot

type ScenarioBot struct {
	*Session
	AuthDB  *sql.DB
	CharDB  *sql.DB
	WorldDB *sql.DB // optional; opened on demand for spawn-id cleanup
	Ident   BotIdent
	Role    string // optional label from BotSpec.Role
}

ScenarioBot is a single logged-in bot prepared for a scenario test. Prefer NewScenario / NewSolo over ad-hoc LoginBot calls. Methods on ScenarioBot (Teleport, Cast, Die, …) hide World vs Session choice.

func ByRole

func ByRole(t *testing.T, bots []*ScenarioBot, role string) *ScenarioBot

ByRole returns the first bot whose Role matches, or fatals.

func NewScenario

func NewScenario(t *testing.T, opt ScenarioOpts) []*ScenarioBot

NewScenario opens DBs, creates accounts, logs bots in, and applies setup. Sessions and DBs are closed via t.Cleanup.

func NewSolo

func NewSolo(t *testing.T, opt ScenarioOpts) *ScenarioBot

NewSolo is NewScenario with a single bot; returns that bot. If opt.Bots is set, only Bots[0] is used.

func (*ScenarioBot) AcceptGroup

func (b *ScenarioBot) AcceptGroup(t *testing.T)

AcceptGroup sends CMSG_GROUP_ACCEPT.

func (*ScenarioBot) AcceptSummon

func (b *ScenarioBot) AcceptSummon(t *testing.T, summonerGUID uint64)

AcceptSummon sends CMSG_SUMMON_RESPONSE agree=true for the pending (or given) summoner.

func (*ScenarioBot) AcceptTrade

func (b *ScenarioBot) AcceptTrade(t *testing.T)

AcceptTrade clicks accept on the open trade window.

func (*ScenarioBot) AcceptTradeWindow

func (b *ScenarioBot) AcceptTradeWindow(t *testing.T, timeout time.Duration)

AcceptTradeWindow handles TRADE_STATUS_BEGIN_TRADE by sending CMSG_BEGIN_TRADE and waits for TRADE_STATUS_OPEN_WINDOW on this bot. Prefer OpenTrade which arms before initiate (no race / no sleep).

func (*ScenarioBot) AddItem

func (b *ScenarioBot) AddItem(t *testing.T, entry, count uint32)

AddItem grants items via GM .additem (no push wait).

func (*ScenarioBot) AddItemWait

func (b *ScenarioBot) AddItemWait(t *testing.T, entry, count uint32) (bag, slot uint8)

AddItemWait grants items and waits for SMSG_ITEM_PUSH_RESULT for that entry.

func (*ScenarioBot) AddQuest

func (b *ScenarioBot) AddQuest(t *testing.T, questID uint32)

AddQuest adds a quest via GM and waits until character_queststatus is INCOMPLETE. Bare `.quest add` is async; under pad thrash a following Save can miss the row.

func (*ScenarioBot) Alive

func (b *ScenarioBot) Alive() bool

Alive reports whether the world session still looks usable.

func (*ScenarioBot) ApplyAura

func (b *ScenarioBot) ApplyAura(t *testing.T, spellID uint32)

ApplyAura applies spellID via GM and waits until SelfHasAura.

func (*ScenarioBot) ArmGroupDecline

func (b *ScenarioBot) ArmGroupDecline() (wait func(timeout time.Duration) (string, bool), cancel func())

ArmGroupDecline arms SMSG_GROUP_DECLINE on the inviting leader (sent when invitee declines). Arm on the leader before invitee.DeclineGroup, then wait so GetGroupInvite is cleared server-side before the next Invite (WaitNotInGroup alone is insufficient — invite ≠ membership).

func (*ScenarioBot) ArmGroupInvite

func (b *ScenarioBot) ArmGroupInvite() (wait func(timeout time.Duration) (string, bool), cancel func())

ArmGroupInvite arms SMSG_GROUP_INVITE before Invite (Arm → Invite → wait). Prefer over WaitGroupInvite, which only arms after the call and can miss a fast packet.

func (*ScenarioBot) ArmLootRollOutcome

func (b *ScenarioBot) ArmLootRollOutcome(itemID uint32) (won <-chan client.LootRollWon, allPassed <-chan client.LootAllPassed, cancel func())

ArmLootRollOutcome arms LOOT_ROLL_WON and LOOT_ALL_PASSED before casting votes. Prefer Arm → Roll* → select/wait (same pattern as ArmLootStartRoll / OpenTrade). itemID 0 matches any item. Cancel removes both hooks (call via t.Cleanup).

func (*ScenarioBot) ArmLootStartRoll

func (b *ScenarioBot) ArmLootStartRoll() (wait func(itemID uint32, timeout time.Duration) (client.LootStartRoll, bool), cancel func())

ArmLootStartRoll installs a start-roll waiter before opening loot. Returns wait and cancel. Prefer: wait, cancel := ArmLootStartRoll(); defer cancel(); ...; wait(...) If wait is never called, cancel still removes the hook (use t.Cleanup(cancel)).

func (*ScenarioBot) ArmSummonRequest

func (b *ScenarioBot) ArmSummonRequest() (wait func(timeout time.Duration) (client.SummonRequest, error), cancel func())

ArmSummonRequest arms SMSG_SUMMON_REQUEST before the action that generates it.

func (*ScenarioBot) AssertAuraConsumed

func (b *ScenarioBot) AssertAuraConsumed(t *testing.T, spellID uint32, timeout time.Duration, issue int)

AssertAuraConsumed is AssertAuraConsumed on the player.

func (*ScenarioBot) AssertAuraRemains

func (b *ScenarioBot) AssertAuraRemains(t *testing.T, spellID uint32, after time.Duration, issue int)

AssertAuraRemains is AssertAuraRemains on the player.

func (*ScenarioBot) AssertHasAura

func (b *ScenarioBot) AssertHasAura(t *testing.T, spellID uint32)

AssertHasAura fatals if the player lacks spellID.

func (*ScenarioBot) AssertInventoryAtLeast

func (b *ScenarioBot) AssertInventoryAtLeast(t *testing.T, entry uint32, min int)

AssertInventoryAtLeast fatals if CharDB stack count for entry is below min after Save.

func (*ScenarioBot) AssertMoneyAtLeast

func (b *ScenarioBot) AssertMoneyAtLeast(t *testing.T, minCopper uint32)

AssertMoneyAtLeast fatals if CharDB money after Save is below minCopper. Prefer this over live PlayerMoney() for durable economy asserts (trade/session). Polls after save — single-shot SELECT races the worldserver flush.

func (*ScenarioBot) AssertMoneyEqual

func (b *ScenarioBot) AssertMoneyEqual(t *testing.T, copper uint32)

AssertMoneyEqual fatals if CharDB money after Save is not exactly copper.

func (*ScenarioBot) AssertMoved

func (b *ScenarioBot) AssertMoved(t *testing.T, fromX, fromY, fromZ, minDist float32) float32

AssertMoved fatals when the bot has not moved at least minDist from (fromX,fromY,fromZ). Returns the measured distance. minDist <= 0 defaults to 1 yard. Use for charge / knockback / escort motion smoke checks.

func (*ScenarioBot) AssertNear

func (b *ScenarioBot) AssertNear(t *testing.T, x, y, z, maxDist float32)

AssertNear fatals as precondition when the bot is farther than maxDist from (x,y,z). maxDist <= 0 defaults to DefaultNearPadDist.

func (*ScenarioBot) AssertNearPad

func (b *ScenarioBot) AssertNearPad(t *testing.T, pad Position3, maxDist float32)

AssertNearPad fatals when the bot is not near pad (and optionally wrong map). maxDist <= 0 defaults to DefaultNearPadDist. Map must match pad.Map.

func (*ScenarioBot) AssertNoAura

func (b *ScenarioBot) AssertNoAura(t *testing.T, spellID uint32)

AssertNoAura fatals if the player still has spellID.

func (*ScenarioBot) AssertNoPlayerPet

func (b *ScenarioBot) AssertNoPlayerPet(t *testing.T)

AssertNoPlayerPet fatals if a pet GUID is still present.

func (*ScenarioBot) AssertQuestStatus

func (b *ScenarioBot) AssertQuestStatus(t *testing.T, questID uint32, want uint8)

AssertQuestStatus saves and asserts character_queststatus.

func (*ScenarioBot) AssertUnitTarget

func (b *ScenarioBot) AssertUnitTarget(t *testing.T, guid, wantTarget uint64)

AssertUnitTarget fatals if unit target is not wantTarget.

func (*ScenarioBot) AssertWorldAlive

func (b *ScenarioBot) AssertWorldAlive(t *testing.T)

AssertWorldAlive pings the world via this bot (for crash tests without a probe).

func (*ScenarioBot) Attack

func (b *ScenarioBot) Attack(t *testing.T, targetGUID uint64)

Attack swings at targetGUID (sets target first).

func (*ScenarioBot) AttackUntil

func (b *ScenarioBot) AttackUntil(t *testing.T, targetGUID uint64, maxHealthFrac float64, timeout time.Duration)

AttackUntil swings until target health fraction is at or below maxHealthFrac.

func (*ScenarioBot) AuraStacks

func (b *ScenarioBot) AuraStacks(spellID uint32) int

AuraStacks returns stack count for spellID on the player (0 if absent).

func (*ScenarioBot) CancelAura

func (b *ScenarioBot) CancelAura(t *testing.T, spellID uint32)

CancelAura sends CMSG_CANCEL_AURA for spellID.

func (*ScenarioBot) CancelCast

func (b *ScenarioBot) CancelCast(t *testing.T)

CancelCast sends CMSG_CANCEL_CAST.

func (*ScenarioBot) CancelCastWhenChanneling

func (b *ScenarioBot) CancelCastWhenChanneling(t *testing.T, spellID uint32, timeout time.Duration) bool

CancelCastWhenChanneling waits for channel then CancelCast. Returns false if never channeling.

func (*ScenarioBot) CancelTrade

func (b *ScenarioBot) CancelTrade(t *testing.T)

CancelTrade cancels the open trade.

func (*ScenarioBot) CaptureCreatureSpawnID

func (b *ScenarioBot) CaptureCreatureSpawnID(t *testing.T, entry uint32) uint32

CaptureCreatureSpawnID records the latest DB spawn for entry near the bot (after `.npc add`). Retries until settle — immediate SELECT after chat often races an empty result.

func (*ScenarioBot) Cast

func (b *ScenarioBot) Cast(t *testing.T, spellID uint32, targetGUID uint64, timeout time.Duration) SpellCastResult

Cast casts spellID at targetGUID and waits for cast result.

func (*ScenarioBot) CastAtPosition

func (b *ScenarioBot) CastAtPosition(t *testing.T, spellID uint32, x, y, z float32, timeout time.Duration) SpellCastResult

CastAtPosition casts a ground-targeted spell and waits for cast result.

func (*ScenarioBot) CastMust

func (b *ScenarioBot) CastMust(t *testing.T, spellID uint32, targetGUID uint64, timeout time.Duration)

CastMust fails unless the cast succeeds (SMSG_SPELL_GO).

func (*ScenarioBot) CastOrGM

func (b *ScenarioBot) CastOrGM(t *testing.T, spellID uint32, targetGUID uint64, timeout time.Duration) SpellCastResult

CastOrGM casts via the client path; on failure or timeout falls back to `.cast self` / targeted GM cast.

func (*ScenarioBot) CastRetries

func (b *ScenarioBot) CastRetries(t *testing.T, spellID uint32, targetGUID uint64, n int, each time.Duration) (SpellCastResult, bool)

CastRetries attempts Cast up to n times; returns last result and whether any succeeded.

func (*ScenarioBot) CastSelfGM

func (b *ScenarioBot) CastSelfGM(t *testing.T, spellID uint32)

CastSelfGM forces a self-cast via `.cast self <spell>` (bypasses client cast path). Selects the player first — AC requires a selected unit for `.cast self`.

func (*ScenarioBot) ChannelSpell

func (b *ScenarioBot) ChannelSpell() uint32

ChannelSpell returns UNIT_CHANNEL_SPELL on the player (0 if idle).

func (*ScenarioBot) CheatGod

func (b *ScenarioBot) CheatGod(t *testing.T)

CheatGod enables god mode.

func (*ScenarioBot) CheatPower

func (b *ScenarioBot) CheatPower(t *testing.T)

CheatPower enables infinite power.

func (*ScenarioBot) CleanupOwnedSummons

func (b *ScenarioBot) CleanupOwnedSummons(t *testing.T)

CleanupOwnedSummons dismisses the controlled pet and despawns nearby units this player summoned/created (Risen Ghoul, guardians, totems, etc.). Soft — safe from t.Cleanup after socket close. Prefer calling while still InWorld.

func (*ScenarioBot) CloseHard

func (b *ScenarioBot) CloseHard(t *testing.T)

CloseHard is HardDisconnect — explicit name for authors comparing graceful Close paths. Does NOT send logout; see HardDisconnect.

func (*ScenarioBot) CombatReady

func (b *ScenarioBot) CombatReady(t *testing.T)

CombatReady turns GM mode off and enables god (and optional power). Prefer this over manual .gm off + CheatGod before pulls.

func (*ScenarioBot) CombatReadyFull

func (b *ScenarioBot) CombatReadyFull(t *testing.T)

CombatReadyFull is CombatReady with god + power.

func (*ScenarioBot) CombatStop

func (b *ScenarioBot) CombatStop(t *testing.T)

CombatStop clears combat via GM `.combatstop` (self).

func (*ScenarioBot) Damage

func (b *ScenarioBot) Damage(t *testing.T, targetGUID uint64, amount uint32)

Damage applies `.damage` without enabling GM mode.

func (*ScenarioBot) DamageKill

func (b *ScenarioBot) DamageKill(t *testing.T, guids []uint64, amount uint32, timeout time.Duration)

DamageKill damages guids until dead without enabling GM mode.

func (*ScenarioBot) DamageToFraction

func (b *ScenarioBot) DamageToFraction(t *testing.T, guid uint64, frac float64, timeout time.Duration)

DamageToFraction damages guid until hp/max <= frac (e.g. 0.49 for below half). When frac > 0, leaves at least 1 HP (never finishes the unit as "below half"). Caps each hit so a large .damage cannot overshoot to 0 from full.

func (*ScenarioBot) DeclineGroup

func (b *ScenarioBot) DeclineGroup(t *testing.T)

DeclineGroup sends CMSG_GROUP_DECLINE.

func (*ScenarioBot) DeclineGroupFrom

func (b *ScenarioBot) DeclineGroupFrom(t *testing.T, leader *ScenarioBot)

DeclineGroupFrom waits for the inviting leader to observe SMSG_GROUP_DECLINE after b declines.

func (*ScenarioBot) DeclineSummon

func (b *ScenarioBot) DeclineSummon(t *testing.T, summonerGUID uint64)

DeclineSummon sends CMSG_SUMMON_RESPONSE agree=false.

func (*ScenarioBot) DespawnCreatureSpawn

func (b *ScenarioBot) DespawnCreatureSpawn(t *testing.T, spawnID uint32)

DespawnCreatureSpawn removes a persistent creature spawn completely:

  1. SQL DELETE always (sync; survives Session.Close) — this is the real cleanup
  2. Optional live `.npc delete N` only if the socket is still open (never MustGM)

Cleanup must not call MustGM: t.Cleanup order can run after Close, and MustGM fatals with "use of closed network connection".

func (*ScenarioBot) DespawnGameObjectSpawn

func (b *ScenarioBot) DespawnGameObjectSpawn(t *testing.T, spawnID uint32)

DespawnGameObjectSpawn removes a persistent GO: SQL DELETE + optional live delete.

func (*ScenarioBot) DespawnNPC

func (b *ScenarioBot) DespawnNPC(t *testing.T, guid uint64)

DespawnNPC select+`.npc delete` for a unit still in the client cache (when no DB id). Soft only — never fails the test if the socket is already closed.

func (*ScenarioBot) DespawnNearbyEntry

func (b *ScenarioBot) DespawnNearbyEntry(t *testing.T, entry uint32, maxDist float32)

DespawnNearbyEntry deletes cache units of entry AND every matching DB spawn near the bot.

func (*ScenarioBot) DespawnNearbyGameObjectEntry

func (b *ScenarioBot) DespawnNearbyGameObjectEntry(t *testing.T, entry uint32, maxDist float32)

DespawnNearbyGameObjectEntry deletes all DB gameobjects of entry near the bot.

func (*ScenarioBot) Die

func (b *ScenarioBot) Die(t *testing.T)

Die kills the bot via GM `.die`.

func (*ScenarioBot) DieAndRepop

func (b *ScenarioBot) DieAndRepop(t *testing.T)

DieAndRepop dies, waits for death, and releases spirit. After repop the client is teleported to a graveyard; waits on TeleportSeq / PhaseInWorld.

func (*ScenarioBot) DieMust

func (b *ScenarioBot) DieMust(t *testing.T, timeout time.Duration)

DieMust ensures player Health()==0 then WaitDead. Under concurrent pad load a single `.die` often no-ops (selection not applied yet, god cheat, or GM thrash). Prefer DieMust over bare Die.

Strategy (re-select self before every attempt):

  1. god off + combatstop (account GM perms still allow .die/.damage)
  2. retry `.die` / `.damage 100 pct` / absolute damage

Never use `.modify hp 1` — that sets max HP to 1 and can leave the bot stuck at 1/1 alive.

func (*ScenarioBot) DisbandGroup

func (b *ScenarioBot) DisbandGroup(t *testing.T)

DisbandGroup is LeaveGroup for the leader path (same opcode; dissolves when rules say so).

func (*ScenarioBot) DismissPet

func (b *ScenarioBot) DismissPet(t *testing.T, petGUID uint64)

DismissPet sends CMSG_PET_ACTION COMMAND_ABANDON for the current (or given) pet. Soft when the socket is closed (cleanup path) — does not fail the test.

func (*ScenarioBot) DistFrom

func (b *ScenarioBot) DistFrom(x, y, z float32) float32

DistFrom returns 3D distance from the bot's current position to (x,y,z).

func (*ScenarioBot) EnablePvP

func (b *ScenarioBot) EnablePvP(t *testing.T)

EnablePvP disables GM mode and flags the player for PvP via CMSG_TOGGLE_PVP (AC has no `.pvp on` command). FlushWorld acks `.gm off`; WaitSelfPvP waits for UNIT_BYTE2_FLAG_PVP on this session's own object.

func (*ScenarioBot) Engage

func (b *ScenarioBot) Engage(t *testing.T, targetGUID uint64, timeout time.Duration)

Engage faces/attacks until the target is in combat.

func (*ScenarioBot) EnterPlayerVehicle

func (b *ScenarioBot) EnterPlayerVehicle(t *testing.T, player *ScenarioBot, timeout time.Duration)

EnterPlayerVehicle boards another player's vehicle kit (raid/party path).

func (*ScenarioBot) EnterVehicle

func (b *ScenarioBot) EnterVehicle(t *testing.T, vehicleGUID uint64, timeout time.Duration) uint64

EnterVehicle boards vehicleGUID via SpellClick, then GM-cast ride fallback if needed. Returns the controlled vehicle GUID (may match vehicleGUID). Turns GM mode off before board (many vehicles ignore GM clickers).

func (*ScenarioBot) EquipEntry

func (b *ScenarioBot) EquipEntry(t *testing.T, entry, count uint32)

EquipEntry adds items and auto-equips from backpack slots (best-effort).

func (*ScenarioBot) ExitVehicle

func (b *ScenarioBot) ExitVehicle(t *testing.T, timeout time.Duration)

ExitVehicle sends CMSG_REQUEST_VEHICLE_EXIT and waits until not on vehicle.

func (*ScenarioBot) Face

func (b *ScenarioBot) Face(t *testing.T, targetGUID uint64)

Face turns toward a unit GUID.

func (*ScenarioBot) FindUnit

func (b *ScenarioBot) FindUnit(entry uint32, maxDist float32) uint64

FindUnit returns a nearby unit GUID by entry, or 0.

func (*ScenarioBot) FlushWorld

func (b *ScenarioBot) FlushWorld(t *testing.T)

FlushWorld is ScenarioBot.FlushWorld.

func (*ScenarioBot) FormPartyWith

func (b *ScenarioBot) FormPartyWith(t *testing.T, members ...*ScenarioBot)

FormPartyWith is FormParty(leader, members...) for chained multi-bot setup. Prefer package FormParty when the leader is known.

func (*ScenarioBot) GM

func (b *ScenarioBot) GM(t *testing.T, cmd string)

GM sends a GM command.

func (*ScenarioBot) GameObjectUse

func (b *ScenarioBot) GameObjectUse(t *testing.T, guid uint64)

GameObjectUse sends CMSG_GAMEOBJ_USE for guid.

func (*ScenarioBot) GiveTotems

func (b *ScenarioBot) GiveTotems(t *testing.T)

GiveTotems adds shaman totem tools.

func (*ScenarioBot) GoCreatureGUID

func (b *ScenarioBot) GoCreatureGUID(t *testing.T, spawnGUID uint32)

GoCreatureGUID teleports to `.go creature <spawnGUID>`.

func (*ScenarioBot) GoCreatureID

func (b *ScenarioBot) GoCreatureID(t *testing.T, entry uint32)

GoCreatureID teleports to `.go creature id <entry>`.

func (*ScenarioBot) GroupMembers

func (b *ScenarioBot) GroupMembers() []client.GroupMember

GroupMembers returns other party members from the last list (not including self).

func (*ScenarioBot) GroupState

func (b *ScenarioBot) GroupState() client.GroupState

GroupState returns the last SMSG_GROUP_LIST snapshot.

func (*ScenarioBot) HardDisconnect

func (b *ScenarioBot) HardDisconnect(t *testing.T)

HardDisconnect forcibly closes the world socket without CMSG_LOGOUT_REQUEST / WaitForLogout. Use for crash / "session drop" probes (charm logout, vehicle passenger disconnect, SESS hard-drop). Prefer Relog for graceful logout+reenter.

Semantics:

  • No logout packet is sent — server sees an abrupt TCP drop.
  • The bot's Session is unusable afterward (do not Cast/GM on it).
  • Probe world health with a *different* bot via ProbeWorldAlive / AssertWorldAlive.

CloseHard is an alias with the same no-logout semantics.

func (*ScenarioBot) HasAura

func (b *ScenarioBot) HasAura(spellID uint32) bool

HasAura reports whether the bot currently has spellID.

func (*ScenarioBot) HasPendingSummon

func (b *ScenarioBot) HasPendingSummon() bool

HasPendingSummon reports a cached SMSG_SUMMON_REQUEST.

func (*ScenarioBot) InGroup

func (b *ScenarioBot) InGroup() bool

InGroup reports last SMSG_GROUP_LIST membership.

func (*ScenarioBot) InitiateTrade

func (b *ScenarioBot) InitiateTrade(t *testing.T, other *ScenarioBot)

InitiateTrade opens a trade with other (sends CMSG_INITIATE_TRADE). The other bot must call AcceptTradeWindow (BEGIN_TRADE → CMSG_BEGIN_TRADE).

func (*ScenarioBot) InventoryCount

func (b *ScenarioBot) InventoryCount(t *testing.T, entry uint32) int

InventoryCount returns sum of item stacks for entry after Save (CharDB).

func (*ScenarioBot) InventoryCountNoSave

func (b *ScenarioBot) InventoryCountNoSave(t *testing.T, entry uint32) int

InventoryCountNoSave queries CharDB without forcing Save (online may be stale).

func (*ScenarioBot) Invite

func (b *ScenarioBot) Invite(t *testing.T, member *ScenarioBot)

Invite sends CMSG_GROUP_INVITE for member.Name from the inviting bot.

func (*ScenarioBot) IsChanneling

func (b *ScenarioBot) IsChanneling() bool

IsChanneling reports UNIT_CHANNEL_SPELL != 0 on the player.

func (*ScenarioBot) IsGroupLeader

func (b *ScenarioBot) IsGroupLeader() bool

IsGroupLeader reports whether this bot is the party leader (from group list).

func (*ScenarioBot) IsOnVehicle

func (b *ScenarioBot) IsOnVehicle() bool

IsOnVehicle reports whether the bot controls/charms a vehicle.

func (*ScenarioBot) LeaderResetInstances

func (b *ScenarioBot) LeaderResetInstances(t *testing.T, timeout time.Duration)

LeaderResetInstances sends CMSG_RESET_INSTANCES from the bot (must be group leader or solo). Optionally arms a waiter for SMSG_INSTANCE_RESET / FAILED; non-fatal if no packet (server silent paths exist).

func (*ScenarioBot) Learn

func (b *ScenarioBot) Learn(t *testing.T, spellID uint32)

Learn learns a spell and waits until KnowsSpell reports it.

func (*ScenarioBot) LearnAll

func (b *ScenarioBot) LearnAll(t *testing.T)

LearnAll runs `.learn all my class`.

func (*ScenarioBot) LeaveGroup

func (b *ScenarioBot) LeaveGroup(t *testing.T)

LeaveGroup sends CMSG_GROUP_DISBAND (leave party on 3.3.5a).

func (*ScenarioBot) LootRelease

func (b *ScenarioBot) LootRelease(t *testing.T, lootGUID uint64)

LootRelease closes the loot window.

func (*ScenarioBot) LootTakeItem

func (b *ScenarioBot) LootTakeItem(t *testing.T, slot uint8)

LootTakeItem takes slot via CMSG_AUTOSTORE_LOOT_ITEM.

func (*ScenarioBot) MasterLootGive

func (b *ScenarioBot) MasterLootGive(t *testing.T, lootGUID uint64, slot uint8, target *ScenarioBot)

MasterLootGive assigns a loot slot to target (leader/master only).

func (*ScenarioBot) ModMoney

func (b *ScenarioBot) ModMoney(t *testing.T, copper uint32)

ModMoney is ScenarioBot wrapper for package ModMoney.

func (*ScenarioBot) MoneyAfterSave

func (b *ScenarioBot) MoneyAfterSave(t *testing.T) uint32

MoneyAfterSave flushes character and returns characters.money for this bot. Polls CharDB briefly — `.save` is async and a single immediate SELECT often races to 0.

func (*ScenarioBot) NearbyUnits

func (b *ScenarioBot) NearbyUnits(maxDist float32) []*client.WorldObject

NearbyUnits returns tracked units within maxDist.

func (*ScenarioBot) OpenLoot

func (b *ScenarioBot) OpenLoot(t *testing.T, lootGUID uint64, timeout time.Duration) []client.LootItem

OpenLoot sends CMSG_LOOT and waits for OnLootOpened (or times out fatally).

func (*ScenarioBot) PendingSummon

func (b *ScenarioBot) PendingSummon() client.SummonRequest

PendingSummon returns the last summon request snapshot.

func (*ScenarioBot) PetAttack

func (b *ScenarioBot) PetAttack(t *testing.T, targetGUID uint64)

PetAttack commands the pet to attack targetGUID. Resolves pet via UNIT_FIELD_SUMMON, then SUMMONEDBY/CREATEDBY fallback (same as WaitPlayerPet).

func (*ScenarioBot) PlayerMoney

func (b *ScenarioBot) PlayerMoney() uint32

PlayerMoney returns live copper from PLAYER_FIELD_COINAGE (0 until first self update).

func (*ScenarioBot) PlayerPetGUID

func (b *ScenarioBot) PlayerPetGUID() uint64

PlayerPetGUID returns UNIT_FIELD_SUMMON on the player (0 if none).

func (*ScenarioBot) PlayerPower

func (b *ScenarioBot) PlayerPower() (current, max uint32)

PlayerPower returns current and max power (mana/rage/energy from UnitFieldPower1).

func (*ScenarioBot) Pos

func (b *ScenarioBot) Pos() (x, y, z float32, mapID uint32)

Pos returns current position.

func (*ScenarioBot) QuestStatus

func (b *ScenarioBot) QuestStatus(t *testing.T, questID uint32) (status uint8, ok bool)

QuestStatus returns current DB quest status (call Save first for online chars).

func (*ScenarioBot) QuestStatusAfterSave

func (b *ScenarioBot) QuestStatusAfterSave(t *testing.T, questID uint32) (status uint8, ok bool)

QuestStatusAfterSave saves then returns quest status.

func (*ScenarioBot) ReclaimCorpse

func (b *ScenarioBot) ReclaimCorpse(t *testing.T)

ReclaimCorpse sends CMSG_RECLAIM_CORPSE once (resurrect at corpse when in range). Prefer ReclaimCorpseMust — a single send often races mid-teleport and is ignored.

func (*ScenarioBot) ReclaimCorpseMust

func (b *ScenarioBot) ReclaimCorpseMust(t *testing.T, corpseX, corpseY, corpseZ float32, mapID uint32, timeout time.Duration)

ReclaimCorpseMust follows AC reclaim protocol after ReleaseSpirit:

  1. Wait SMSG_CORPSE_RECLAIM_DELAY (server legal time) — do not spam reclaim.
  2. Teleport to corpse and wait PhaseInWorld + within CorpseReclaimRadius (39).
  3. Send CMSG_RECLAIM_CORPSE a few times until Health()>0.

Root cause of long “timeouts”: Unit::Kill(player,player) from .die sets PvP corpse (SetPvPDeath(true)), so delay is often 30s when Death.CorpseReclaimDelay.PvP=1. We honor the delay packet instead of guessing a fixed sleep.

func (*ScenarioBot) ReleaseSpirit

func (b *ScenarioBot) ReleaseSpirit(t *testing.T)

ReleaseSpirit sends CMSG_REPOP_REQUEST.

func (*ScenarioBot) Relog

func (b *ScenarioBot) Relog(t *testing.T)

Relog logs the bot out of the world and back in on the same account/character. Rebinds b.Session fields; keeps AuthDB, CharDB, Ident, Role. Registers the new session with t.Cleanup via LoginBots. Waits until the new session is PhaseInWorld before returning.

The old session is Closed; do not use the previous *Session pointer afterward (the ScenarioBot pointer itself is updated in place).

func (*ScenarioBot) RollGreed

func (b *ScenarioBot) RollGreed(t *testing.T, roll client.LootStartRoll)

func (*ScenarioBot) RollNeed

func (b *ScenarioBot) RollNeed(t *testing.T, roll client.LootStartRoll)

RollNeed / RollGreed / RollPass vote on a start-roll snapshot.

func (*ScenarioBot) RollPass

func (b *ScenarioBot) RollPass(t *testing.T, roll client.LootStartRoll)

func (*ScenarioBot) Save

func (b *ScenarioBot) Save(t *testing.T)

Save forces a character DB flush.

func (*ScenarioBot) SetLeader

func (b *ScenarioBot) SetLeader(t *testing.T, member *ScenarioBot)

SetLeader promotes member via CMSG_GROUP_SET_LEADER.

func (*ScenarioBot) SetLootMethod

func (b *ScenarioBot) SetLootMethod(t *testing.T, method uint8, lootMaster uint64, threshold uint8)

SetLootMethod sets party loot method (leader). See client.LootMethod* constants. threshold must be >= client.ItemQualityUncommon (2); AC silently rejects lower values (method may not apply). Prefer e2eharness.LootThresholdUncommon.

func (*ScenarioBot) SetMoney

func (b *ScenarioBot) SetMoney(t *testing.T, copper uint32)

SetMoney sets absolute copper via GM `.modify money` relative to current DB/live value. Prefer ModMoney for additive funding; this drains then adds to approximate `copper`. Verifies CharDB (not only live coinage) — `.modify money` + `.save` can lag under thrash.

func (*ScenarioBot) SetSkill

func (b *ScenarioBot) SetSkill(t *testing.T, skillID, level, max uint32)

SetSkill sets a skill via GM `.setskill <id> <level> <max>`.

func (*ScenarioBot) SetTradeGold

func (b *ScenarioBot) SetTradeGold(t *testing.T, copper uint32)

SetTradeGold sets offered copper in the open trade. Same settle wait as SetTradeItem (server unaccepts → BACK_TO_TRADE).

func (*ScenarioBot) SetTradeItem

func (b *ScenarioBot) SetTradeItem(t *testing.T, tradeSlot, bag, invSlot uint8)

SetTradeItem places an inventory item into trade slot. Server unaccepts both sides (BACK_TO_TRADE). Must wait for that status — not OPEN_WINDOW from OpenTrade — or CompleteTrade accepts before the item lands and races unaccept.

func (*ScenarioBot) Spawn

func (b *ScenarioBot) Spawn(t *testing.T, entry uint32, timeout time.Duration) uint64

Spawn places a combat/pull fixture (training dummy, etc.) via persistent `.npc add` and registers SQL+live cleanup. Returns the live client GUID.

Not `.npc add temp`: temps are invisible to the creature table, live ~120s, and select-delete cleanup races Session.Close — that is why Heroic Training Dummies piled up.

func (*ScenarioBot) SpawnGameObject

func (b *ScenarioBot) SpawnGameObject(t *testing.T, entry uint32) uint32

SpawnGameObject runs `.gobject add <entry>`, resolves DB spawn id (with settle+retry), registers t.Cleanup that SQL-deletes + live-deletes, returns spawn id (0 if unresolved).

func (*ScenarioBot) SpawnKillLootable

func (b *ScenarioBot) SpawnKillLootable(t *testing.T, entry uint32, timeout time.Duration) uint64

SpawnKillLootable spawns entry, kills it, waits until the corpse is lootable.

Important:

  • Uses `.npc add` (persistent DB spawn) because `.npc add temp` is TEMPSUMMON_CORPSE_DESPAWN — corpse is removed instantly and cannot be looted.
  • Captures DB spawn id and registers t.Cleanup `.npc delete <id>` so residue is removed even after the client drops the unit from cache.
  • Despawns leftover same-entry units nearby before spawn (pad pollution).
  • Does not re-teleport after kill; open loot while still next to the corpse.
  • Leaves bot CombatReady (gm off + god) after kill for normal loot interaction.

func (*ScenarioBot) SpawnPersistent

func (b *ScenarioBot) SpawnPersistent(t *testing.T, entry uint32, timeout time.Duration) (liveGUID uint64, spawnID uint32)

SpawnPersistent adds a DB creature (`.npc add`, not temp), waits for live GUID, captures spawn id, registers SQL+live cleanup. Use for training dummies / pull targets.

Do NOT use `.npc add temp` for e2e fixtures: temps are not in `creature` table, live ~120s (TEMPSUMMON_CORPSE_DESPAWN), and select-delete races Session.Close.

func (*ScenarioBot) SpellClick

func (b *ScenarioBot) SpellClick(t *testing.T, guid uint64)

SpellClick sends CMSG_SPELLCLICK for guid (creature vehicle board path).

func (*ScenarioBot) TeleNamed

func (b *ScenarioBot) TeleNamed(t *testing.T, name string)

TeleNamed runs `.tele <name>` and waits for transfer.

func (*ScenarioBot) Teleport

func (b *ScenarioBot) Teleport(t *testing.T, x, y, z float32, mapID uint32)

Teleport moves the bot with `.go xyz` and waits for teleport completion.

func (*ScenarioBot) TeleportPad

func (b *ScenarioBot) TeleportPad(t *testing.T, pad Position3)

TeleportPad teleports to a Position3 pad.

func (*ScenarioBot) TryCast

func (b *ScenarioBot) TryCast(t *testing.T, spellID uint32, targetGUID uint64, timeout time.Duration) (SpellCastResult, error)

TryCast is like Cast but returns an error on timeout instead of failing the test. Use for hang-detection scenarios (e.g. Charge on oversized targets).

func (*ScenarioBot) TryOpenLoot

func (b *ScenarioBot) TryOpenLoot(t *testing.T, lootGUID uint64, timeout time.Duration) (items []client.LootItem, ok bool)

TryOpenLoot sends CMSG_LOOT and waits for loot open. Returns ok=false on timeout (does not fatal). Prefer this when outdoor corpses may not be lootable. Uses AddLootOpenedHook (race-safe multi-subscriber).

func (*ScenarioBot) TryWaitAuraGone

func (b *ScenarioBot) TryWaitAuraGone(t *testing.T, spellID uint32, timeout time.Duration) bool

TryWaitAuraGone is soft WaitAuraGone (returns false on timeout).

func (*ScenarioBot) TryWaitChanneling

func (b *ScenarioBot) TryWaitChanneling(t *testing.T, spellID uint32, timeout time.Duration) bool

TryWaitChanneling returns true if channeling (optional spellID match) within timeout.

func (*ScenarioBot) Uninvite

func (b *ScenarioBot) Uninvite(t *testing.T, memberName string)

Uninvite kicks or cancels invite by character name.

func (*ScenarioBot) UnitAuraStacks

func (b *ScenarioBot) UnitAuraStacks(guid uint64, spellID uint32) int

UnitAuraStacks returns stack count for spellID on a tracked unit.

func (*ScenarioBot) UnitHP

func (b *ScenarioBot) UnitHP(guid uint64) (hp, max uint32)

UnitHP returns cached health/max for a unit.

func (*ScenarioBot) UnitHasAura

func (b *ScenarioBot) UnitHasAura(guid uint64, spellID uint32) bool

UnitHasAura reports whether a tracked unit has spellID.

func (*ScenarioBot) UnitInCombat

func (b *ScenarioBot) UnitInCombat(guid uint64) bool

UnitInCombat reports combat flag on a unit.

func (*ScenarioBot) UnitTarget

func (b *ScenarioBot) UnitTarget(guid uint64) uint64

UnitTarget returns UNIT_FIELD_TARGET for a unit.

func (*ScenarioBot) UnitsByEntry

func (b *ScenarioBot) UnitsByEntry(maxDist float32, entries ...uint32) []UnitSnap

UnitsByEntry returns living unit snaps for entries.

func (*ScenarioBot) VehicleGUID

func (b *ScenarioBot) VehicleGUID() uint64

VehicleGUID returns the current vehicle GUID or 0.

func (*ScenarioBot) WaitAlive

func (b *ScenarioBot) WaitAlive(t *testing.T, timeout time.Duration)

WaitAlive waits until player health > 0.

func (*ScenarioBot) WaitAuraGone

func (b *ScenarioBot) WaitAuraGone(t *testing.T, spellID uint32, timeout time.Duration)

WaitAuraGone waits until the player loses spellID; fatals on timeout.

func (*ScenarioBot) WaitChanneling

func (b *ScenarioBot) WaitChanneling(t *testing.T, spellID uint32, timeout time.Duration)

WaitChanneling waits until the player is channeling (optional spellID filter; 0 = any).

func (*ScenarioBot) WaitDead

func (b *ScenarioBot) WaitDead(t *testing.T, timeout time.Duration)

WaitDead blocks until health is 0.

func (*ScenarioBot) WaitGameObject

func (b *ScenarioBot) WaitGameObject(t *testing.T, entry uint32, timeout time.Duration) uint64

WaitGameObject waits for a nearby gameobject entry in the object cache.

func (*ScenarioBot) WaitGroupInvite

func (b *ScenarioBot) WaitGroupInvite(t *testing.T, timeout time.Duration) string

WaitGroupInvite waits for SMSG_GROUP_INVITE; returns inviter name. Prefer ArmGroupInvite before Invite — this helper arms only after the call.

func (*ScenarioBot) WaitGroupList

func (b *ScenarioBot) WaitGroupList(t *testing.T, wantInGroup bool, minMembers int, timeout time.Duration) client.GroupState

WaitGroupList waits until InGroup matches wantInGroup, or MemberCount >= minMembers when minMembers>0.

func (*ScenarioBot) WaitInWorld

func (b *ScenarioBot) WaitInWorld(t *testing.T, timeout time.Duration)

WaitInWorld blocks until the bot is in PhaseInWorld (gameplay-ready). Use after Relog, far .tele / map transfer, or any path that may leave the session in PhaseFarTransfer / PhaseNearTeleport / PhaseLoading. Prefer this over fixed sleeps after login or teleport.

func (*ScenarioBot) WaitIsGroupLeader

func (b *ScenarioBot) WaitIsGroupLeader(t *testing.T, timeout time.Duration)

WaitIsGroupLeader waits until IsGroupLeader is true (after SetLeader transfer).

func (*ScenarioBot) WaitLootAllPassed

func (b *ScenarioBot) WaitLootAllPassed(t *testing.T, itemID uint32, timeout time.Duration) client.LootAllPassed

WaitLootAllPassed waits for SMSG_LOOT_ALL_PASSED. Prefer ArmLootRollOutcome before RollPass — arms only after the call.

func (*ScenarioBot) WaitLootMethod

func (b *ScenarioBot) WaitLootMethod(t *testing.T, method uint8, timeout time.Duration) client.GroupState

WaitLootMethod waits until GroupState.LootMethod matches method while InGroup.

func (*ScenarioBot) WaitLootRollWon

func (b *ScenarioBot) WaitLootRollWon(t *testing.T, itemID uint32, timeout time.Duration) client.LootRollWon

WaitLootRollWon waits for SMSG_LOOT_ROLL_WON matching optional itemID (0=any). Prefer ArmLootRollOutcome before Roll* — this helper arms only after the call, so it can miss a resolution that already fired.

func (*ScenarioBot) WaitLootStartRoll

func (b *ScenarioBot) WaitLootStartRoll(t *testing.T, itemID uint32, timeout time.Duration) client.LootStartRoll

WaitLootStartRoll waits for SMSG_LOOT_START_ROLL matching optional itemID (0 = any).

func (*ScenarioBot) WaitNear

func (b *ScenarioBot) WaitNear(t *testing.T, x, y, z, maxDist float32, timeout time.Duration)

WaitNear waits until the bot is within maxDist of (x,y,z).

func (*ScenarioBot) WaitNewUnits

func (b *ScenarioBot) WaitNewUnits(t *testing.T, known map[uint64]struct{}, entries []uint32, timeout time.Duration) []UnitSnap

WaitNewUnits waits for new living units of the given entries.

func (*ScenarioBot) WaitNoPlayerPet

func (b *ScenarioBot) WaitNoPlayerPet(t *testing.T, timeout time.Duration)

WaitNoPlayerPet waits until UNIT_FIELD_SUMMON is cleared.

func (*ScenarioBot) WaitNotChanneling

func (b *ScenarioBot) WaitNotChanneling(t *testing.T, timeout time.Duration)

WaitNotChanneling waits until UNIT_CHANNEL_SPELL is 0.

func (*ScenarioBot) WaitNotInGroup

func (b *ScenarioBot) WaitNotInGroup(t *testing.T, timeout time.Duration)

WaitNotInGroup waits until the bot is out of a party.

func (*ScenarioBot) WaitNotOnVehicle

func (b *ScenarioBot) WaitNotOnVehicle(t *testing.T, timeout time.Duration)

WaitNotOnVehicle waits until the bot is off the vehicle.

func (*ScenarioBot) WaitOnVehicle

func (b *ScenarioBot) WaitOnVehicle(t *testing.T, timeout time.Duration) uint64

WaitOnVehicle waits until IsOnVehicle; returns vehicle GUID.

func (*ScenarioBot) WaitPlayerMoney

func (b *ScenarioBot) WaitPlayerMoney(t *testing.T, minCopper uint32, timeout time.Duration) uint32

WaitPlayerMoney waits until live PLAYER_FIELD_COINAGE is >= minCopper. Use after ModMoney/SetMoney when reading PlayerMoney() (not CharDB).

func (*ScenarioBot) WaitPlayerPet

func (b *ScenarioBot) WaitPlayerPet(t *testing.T, timeout time.Duration) uint64

WaitPlayerPet waits until UNIT_FIELD_SUMMON is non-zero (or returns existing).

func (*ScenarioBot) WaitSelfPvP

func (b *ScenarioBot) WaitSelfPvP(t *testing.T, timeout time.Duration)

WaitSelfPvP waits until this bot's own player object is PvP-flagged.

func (*ScenarioBot) WaitSummonRequest

func (b *ScenarioBot) WaitSummonRequest(t *testing.T, timeout time.Duration) client.SummonRequest

WaitSummonRequest waits for SMSG_SUMMON_REQUEST (prefer ArmSummonRequest before the cast).

func (*ScenarioBot) WaitTradeCancelled

func (b *ScenarioBot) WaitTradeCancelled(t *testing.T, timeout time.Duration) client.TradeStatusInfo

WaitTradeCancelled waits for cancel / far / close-class terminal statuses, or until TradeOpen becomes false (server may drop the window without a packet we observe across map transfers).

func (*ScenarioBot) WaitTradeComplete

func (b *ScenarioBot) WaitTradeComplete(t *testing.T, timeout time.Duration)

WaitTradeComplete waits for TRADE_STATUS_TRADE_COMPLETE.

func (*ScenarioBot) WaitTradeOpen

func (b *ScenarioBot) WaitTradeOpen(t *testing.T, timeout time.Duration)

WaitTradeOpen waits until TradeOpen() is true or last status is OPEN_WINDOW.

func (*ScenarioBot) WaitTradeStatus

func (b *ScenarioBot) WaitTradeStatus(t *testing.T, want uint32, timeout time.Duration) client.TradeStatusInfo

WaitTradeStatus waits until OnTradeStatus delivers a matching status (or any if want==^0). Arm before the action that produces the status when possible (FormParty-style).

func (*ScenarioBot) WaitUnit

func (b *ScenarioBot) WaitUnit(t *testing.T, entry uint32, timeout time.Duration) uint64

WaitUnit waits for a nearby unit with the given template entry.

func (*ScenarioBot) WaitUnitAny

func (b *ScenarioBot) WaitUnitAny(t *testing.T, timeout time.Duration, entries ...uint32) uint64

WaitUnitAny waits for any of the given entries; returns the live GUID.

func (*ScenarioBot) WaitUnitAura

func (b *ScenarioBot) WaitUnitAura(t *testing.T, guid uint64, spellID uint32, timeout time.Duration)

WaitUnitAura waits until a unit has the given aura.

func (*ScenarioBot) WaitUnitCombat

func (b *ScenarioBot) WaitUnitCombat(t *testing.T, guid uint64, timeout time.Duration)

WaitUnitCombat waits until the unit is in combat.

func (*ScenarioBot) WaitUnitDead

func (b *ScenarioBot) WaitUnitDead(t *testing.T, guid uint64, timeout time.Duration)

WaitUnitDead waits until the unit is dead or gone.

func (*ScenarioBot) WaitUnitGUID

func (b *ScenarioBot) WaitUnitGUID(t *testing.T, guid uint64, timeout time.Duration)

WaitUnitGUID waits until guid appears in this bot's object cache (multi-bot spawn sync).

func (*ScenarioBot) WaitUnitHPKnown

func (b *ScenarioBot) WaitUnitHPKnown(t *testing.T, guid uint64, timeout time.Duration) (hp, max uint32)

WaitUnitHPKnown waits until unit has max HP > 0 in object cache.

func (*ScenarioBot) WaitUnitLootable

func (b *ScenarioBot) WaitUnitLootable(t *testing.T, guid uint64, timeout time.Duration)

WaitUnitLootable waits until unit has UNIT_DYNFLAG_LOOTABLE (or dead corpse dynflag). Preferred after Damage/DamageKill/WaitUnitDead before OpenLoot:

bot.DamageKill(t, []uint64{guid}, 0, 10*time.Second)
bot.WaitUnitLootable(t, guid, 15*time.Second)
items := bot.OpenLoot(t, guid, 10*time.Second)

Soft-continues if dynflags never appear (some cores delay UPDATE_OBJECT).

func (*ScenarioBot) WaitUnitPvP

func (b *ScenarioBot) WaitUnitPvP(t *testing.T, guid uint64, timeout time.Duration)

WaitUnitPvP waits until guid is PvP-flagged in this bot's object cache.

func (*ScenarioBot) WaitUnitTarget

func (b *ScenarioBot) WaitUnitTarget(t *testing.T, guid, wantTarget uint64, timeout time.Duration)

WaitUnitTarget waits until unit guid's UNIT_FIELD_TARGET equals wantTarget (wantTarget==0 waits for clear target).

type ScenarioOpts

type ScenarioOpts struct {
	// Prefix for unique account names (short, letters preferred).
	Prefix string
	// Bots, when non-empty, defines each bot individually (overrides Count/Race/Class).
	Bots []BotSpec
	// Count of bots when Bots is empty (default 1).
	Count int
	// Race/Class apply to all homogeneous bots when non-zero (default Human Warrior).
	Race  uint8
	Class uint8
	// Level applied via GM after login when > 0 (homogeneous only; use BotSpec.Level otherwise).
	Level int
	// LearnAllClass runs `.learn all my class` when true (homogeneous only).
	LearnAllClass bool
	// SkipGM leaves GM mode off (default is GM on — needed for .go/.learn/.quest).
	SkipGM bool
	// CombatReady runs CombatReadyDefaults after login (gm off + god).
	// Use for fight tests; leave false when you need GM mode for setup first.
	CombatReady bool
	// CombatReadyFull runs CombatReady with god+power after login.
	CombatReadyFull bool
	// StartPad, when non-nil, teleports every bot there after setup.
	StartPad *Position3
}

ScenarioOpts configures a multi-bot scenario fixture.

Prefer Bots for multi-role scenarios (different race/class per bot). Count/Race/Class/Level/LearnAllClass apply when Bots is empty (homogeneous).

type Session

type Session struct {
	World *client.WorldClient
	GUID  uint64
	Name  string
	User  string
	// contains filtered or unexported fields
}

Session is a logged-in world session for one bot character.

func LoginAllianceBots

func LoginAllianceBots(t *testing.T, idents []BotIdent) []*Session

LoginAllianceBots logs in bots via LoginBots, defaulting each identity to Human Warrior when Race/Class are zero. Prefer LoginBots for race-aware scenarios.

func LoginAllianceBotsNoCleanup

func LoginAllianceBotsNoCleanup(t *testing.T, idents []BotIdent) []*Session

LoginAllianceBotsNoCleanup is like LoginAllianceBots but does not register t.Cleanup(Close). Callers (package-shared fixtures) must Close sessions themselves.

func LoginBot

func LoginBot(t *testing.T, opt LoginOptions) (*Session, error)

LoginBot authenticates, connects to the first realm, creates a character if needed, and enters world. On any failure after the world client is created, the world socket is closed so retries cannot leave zombie connections or re-auth with a new session_key while an old CMSG_AUTH_SESSION is in flight.

func LoginBots

func LoginBots(t *testing.T, idents []BotIdent) []*Session

LoginBots logs in each identity using its Race/Class (defaults Human/Warrior). Sessions are registered with t.Cleanup(Close).

func (*Session) ArmAllWaiters

func (s *Session) ArmAllWaiters()

ArmAllWaiters installs / re-arms the unified packet dispatcher for petition + bank + item push. Preferred entry point (ArmPetitionWaiters / ArmBankWaiters are thin aliases for call-site readability).

Contract: Arm → Send → Wait. Do not re-Arm while Wait is outstanding.

func (*Session) ArmBankWaiters

func (s *Session) ArmBankWaiters()

ArmBankWaiters is an alias of ArmAllWaiters (unified dispatcher).

func (*Session) ArmPetitionWaiters

func (s *Session) ArmPetitionWaiters()

ArmPetitionWaiters is an alias of ArmAllWaiters (unified dispatcher).

func (*Session) ArmSpellWaiter

func (s *Session) ArmSpellWaiter()

ArmSpellWaiter installs a spell-result hook to feed a buffered channel. Call before CastSpell; then WaitSpell / WaitSpellSuccess. Re-arming replaces the channel (same contract as bank waiters: never re-arm during Wait). Uses AddSpellCastResultHook once (race-safe fan-out); re-arms only refresh spellCh.

func (*Session) Close

func (s *Session) Close()

Close closes the world socket.

func (*Session) DrainBankLists

func (s *Session) DrainBankLists()

DrainBankLists consumes any buffered bank list packets.

func (*Session) DrainItemPushes

func (s *Session) DrainItemPushes()

DrainItemPushes consumes any buffered item-push packets.

func (*Session) SetLogf

func (s *Session) SetLogf(logf func(format string, args ...interface{}))

SetLogf rebinds session diagnostic logging (e.g. when a package-shared leader is reused across tests so waiter warnings attach to the active testing.T).

func (*Session) WaitBankList

func (s *Session) WaitBankList(d time.Duration) (*client.GuildBankList, error)

WaitBankList waits for SMSG_GUILD_BANK_LIST.

func (*Session) WaitItemPush

func (s *Session) WaitItemPush(d time.Duration) (*client.ItemPushResult, error)

WaitItemPush waits for any SMSG_ITEM_PUSH_RESULT.

func (*Session) WaitItemPushEntry

func (s *Session) WaitItemPushEntry(entry uint32, d time.Duration) (*client.ItemPushResult, error)

WaitItemPushEntry waits for SMSG_ITEM_PUSH_RESULT with the given item entry.

func (*Session) WaitMoneyWithdrawn

func (s *Session) WaitMoneyWithdrawn(d time.Duration) (int32, error)

WaitMoneyWithdrawn waits for MSG_GUILD_BANK_MONEY_WITHDRAWN.

func (*Session) WaitShowSignatures

func (s *Session) WaitShowSignatures(d time.Duration) (*client.PetitionShowSignatures, error)

WaitShowSignatures waits for SMSG_PETITION_SHOW_SIGNATURES.

func (*Session) WaitSignResults

func (s *Session) WaitSignResults(d time.Duration) (*client.PetitionSignResults, error)

WaitSignResults waits for SMSG_PETITION_SIGN_RESULTS.

func (*Session) WaitSpell

func (s *Session) WaitSpell(d time.Duration) (SpellCastResult, error)

WaitSpell waits for any spell cast result.

func (*Session) WaitSpellID

func (s *Session) WaitSpellID(spellID uint32, d time.Duration) (SpellCastResult, error)

WaitSpellID waits for a cast result for the given spell ID.

func (*Session) WaitTurnIn

func (s *Session) WaitTurnIn(d time.Duration) (uint32, error)

WaitTurnIn waits for SMSG_TURN_IN_PETITION_RESULTS.

type SpawnSet

type SpawnSet struct {
	// Kind is the kind key from KindOf (or the entry as string when KindOf is nil).
	Kind   string
	Entry  uint32 // first/dominant entry seen in the batch
	Guids  []uint64
	SpawnT time.Time
}

SpawnSet is one batch of ally/add GUIDs detected by SpawnSetTracker.

type SpawnSetTracker

type SpawnSetTracker struct {
	Entries      []uint32
	MaxDist      float32
	SameSetGrace time.Duration
	// KindOf maps template entry → kind key. Nil means each entry is its own kind.
	KindOf func(entry uint32) string
	// contains filtered or unexported fields
}

SpawnSetTracker groups newly seen living units of given entries into sets. Units of the same kind (via KindOf, default: entry id) arriving within SameSetGrace merge into the open set; a different kind or grace expiry opens a new set. Use KindOf to merge multi-entry packs (e.g. Freya Trio).

func NewSpawnSetTracker

func NewSpawnSetTracker(entries []uint32, sameSetGrace time.Duration) *SpawnSetTracker

NewSpawnSetTracker builds a tracker. sameSetGrace defaults to 3s when <=0.

func (*SpawnSetTracker) Known

func (tr *SpawnSetTracker) Known() map[uint64]struct{}

Known returns the set of all GUIDs absorbed so far.

func (*SpawnSetTracker) Poll

func (tr *SpawnSetTracker) Poll(w *client.WorldClient, now time.Time)

Poll absorbs currently living matching units into sets. Call frequently.

func (*SpawnSetTracker) Sets

func (tr *SpawnSetTracker) Sets() []SpawnSet

Sets returns a copy of recorded spawn sets.

func (*SpawnSetTracker) WaitNextNew

func (tr *SpawnSetTracker) WaitNextNew(t *testing.T, w *client.WorldClient, knownAtStart map[uint64]struct{}, timeout time.Duration) []UnitSnap

WaitNextNew waits for units not in knownAtStart (caller snapshot), returns them.

func (*SpawnSetTracker) WaitSets

func (tr *SpawnSetTracker) WaitSets(t *testing.T, w *client.WorldClient, n int, timeout time.Duration) []SpawnSet

WaitSets polls until n sets are recorded or timeout (harness fail).

type SpellCastResult

type SpellCastResult struct {
	SpellID    uint32
	Success    bool
	FailReason uint8
}

SpellCastResult is the outcome of a CMSG_CAST_SPELL attempt.

func CastAndWait

func CastAndWait(t *testing.T, s *Session, spellID uint32, targetGUID uint64, timeout time.Duration) SpellCastResult

CastAndWait casts spell at target and waits for success (SMSG_SPELL_GO) or fail.

func CastAtPositionAndWait

func CastAtPositionAndWait(t *testing.T, s *Session, spellID uint32, x, y, z float32, timeout time.Duration) SpellCastResult

CastAtPositionAndWait casts a ground-targeted spell and waits for cast result.

type TargetHoldSample

type TargetHoldSample struct {
	Attacker  uint64
	Entry     uint32
	Target    uint64
	HoldTicks int
	HurtTicks int
	EverHurt  bool
}

TargetHoldSample is one attacker's observed targeting behaviour.

func ObserveUnitTargets

func ObserveUnitTargets(
	t *testing.T,
	w *client.WorldClient,
	attackerEntries []uint32,
	targetOK func(entry uint32) bool,
	every, for_ time.Duration,
) []TargetHoldSample

ObserveUnitTargets samples attackers matching attackerEntries. For each living attacker whose UNIT_FIELD_TARGET points at a unit where targetOK(entry) is true, it tracks consecutive hold ticks and whether that target's HP dropped.

type UnitSnap

type UnitSnap struct {
	GUID      uint64
	Entry     uint32
	Health    uint32
	MaxHealth uint32
	InCombat  bool
	Target    uint64
}

UnitSnap is a point-in-time view of a tracked unit for observation helpers.

func UnitsByEntry

func UnitsByEntry(w *client.WorldClient, maxDist float32, entries ...uint32) []UnitSnap

UnitsByEntry returns living units (hp>0) matching any of the given entries within maxDist (0 = no distance filter, same as GetNearbyUnits(0)).

func WaitNewUnits

func WaitNewUnits(t *testing.T, w *client.WorldClient, known map[uint64]struct{}, entries []uint32, maxDist float32, timeout time.Duration) []UnitSnap

WaitNewUnits waits until at least one living unit with an allowed entry appears whose GUID is not in known. New GUIDs are added to known before return. Returns the new units (may be more than one if a pack spawns together).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL