v1: a discover-as-you-explore fast travel network, in the spirit of FFXIV's Aetheryte system, built
as a chat command rather than a spell. See PLAN.md for the roadmap and the reasoning behind the
design, including two rounds of in-game-testing surprises this started as before landing here.
mod_waygate_network_locations (world DB) and are loaded once at startup
(WaygateNetwork_World::OnLoadCustomDatabaseTable) โ adding one is a plain SQL insert + restart
(or .waygate reload), not a code change. Ships with the 8 original capital cities (IDs 1-8),
Shattrath City (Outland, ID 9) and Dalaran (Northrend, ID 10), and full Eastern Kingdoms, Kalimdor,
Outland, and Northrend quest-hub batches (IDs 11-27, 28-45, 46-61, 62-74) โ see "Custom ID ranges"
below.mod_waygate_network_discovered), with a chat message confirming it plus a sound
(WaygateNetwork.UnlockSoundId, see Config). Their very first-ever attunement also gets a one-line
tip about .waygate and macro-binding it. Entering the destination's zone doesn't attune it on its
own anymore โ see "Waygate Stones" below for why, and what it does instead..waygate summons a short-lived, harmless "Waygate Portal" NPC next to you (entry
911001, despawns after 15s or as soon as you finish) and immediately opens a two-page gossip
menu against it โ no wait yet: first a list of continents you have anything attuned in (see
"Continent grouping" below), then โ after picking one โ the attuned destinations within it, each
showing its gold/silver/copper cost (real coin icons via WoW's |T...|t inline texture markup).
Picking a destination is where the wait happens (see "Cast time" below) โ hold still for a few
seconds and it charges that cost and teleports you; picking a destination you can no longer afford
just tells you so and does nothing. The portal despawns once you pick something; "ยซ Back"
(destination page only) returns to the continent list; "I changed my mind..." (continent page
only โ "ยซ Back" already covers backing out of the destination page) closes the menu and despawns
the portal, free. Both pages always have 2+ options so there's never a single-choice list. See "Why
a summoned NPC" below for why the portal exists at all, and "Travel cost" below for how the price
is worked out.WaygateNetwork.Enable disables the whole thing live โ both the command (HandleWaygateCommand)
and the actual teleport (npc_waygate_portal::OnGossipSelect) re-check the flag, so flipping it
off mid-session stops already-attuned characters from still traveling.mod_waygate_network_discovered rows
(OnPlayerDeleteFromDB), so a recycled guid can't inherit a stranger's attunements.#ifdef MOD_PLAYERBOTS (compiles fine with or without that
module present)..waygate reload (GM-only) re-loads mod_waygate_network_locations without a full worldserver
restart.WaygateNetwork.Debug (GM-only regardless of the flag) makes .waygate list every known
destination instead of just attuned ones, tagged [debug], and skips the attunement check on
travel โ for reviewing/testing new locations without visiting each one first..waygate itself is gated behind a quest chain ("The Ley Wardens", see below) โ GM debug mode
is the only bypass. Attunement keeps working regardless, so finishing the chain doesn't leave you
staring at an empty menu.v1 originally taught every class a self-cast spell, Call Waygate (910000), that opened the
gossip menu on cast. In-game testing (2026-08-13) showed the spell was learned correctly server-side
(confirmed in character_spell) but never appeared in the spellbook and couldn't be cast โ including
via macro. Root cause: AzerothCore's spell_dbc SQL table is the server's internal spell data; it
has no relationship to the client's own local Spell.dbc file. A from-scratch ID like 910000 has
no entry in the client's copy, so the client has no name/icon/anything to render for it, no matter
how correctly the server tracks that the character knows it. There's no client patch distributed
here to fix that. See PLAN.md's "Deferred / rejected" section for what would actually be needed to
make a real custom spell work, if that's ever worth revisiting.
The .waygate command sidesteps that problem โ chat commands are handled server-side only, so
there's no client-DBC dependency.
The first version of the command opened the gossip menu using the player's own GUID as the gossip
sender (the core's HandleGossipSelectOptionOpcode does explicitly support that โ see PLAN.md). This
also failed in-game (2026-08-13, same session as the spell fix): no gossip window ever appeared, yet
selecting still silently resolved to Ironforge. Every other gossip flow in the game is sourced from
an actual Creature/GameObject the player has interacted with; a menu sourced from the player's own
character, opened out of nowhere via a chat command, isn't something the client's Gossip UI seems to
render correctly, even though the server-side protocol nominally allows it.
Fixed by summoning a real, if short-lived, NPC (npc_waygate_portal, entry 911001, faction 35 โ
non-hostile to everyone) right next to the player and sourcing the gossip menu from it instead.
This is the exhaustively-proven-working path (every vendor/questgiver in the game already validates
it), so it carries no such uncertainty. The portal despawns itself after a destination is picked, or
after 15 seconds as a safety net if the player never interacts with it (e.g. closes the window).
Attunement used to be entirely passive โ WaygateNetwork_Player::OnPlayerUpdateZone matched a
character's current zone against mod_waygate_network_locations and auto-attuned it, no need to
find or interact with anything. As of this version, that's replaced with a real, permanently-spawned
Waygate Stone (gameobject_template entries 913001-913074, one per destination, entry = 913000 + destination id) standing at each destination's exact location โ a character has to
actually find and interact with it (go_waygate_standing_stone::OnGossipHello) to attune, not just
clip through the zone border. Fits the Ley Wardens lore tighter (the quest chain's own text already
calls these things "standing stones") and makes attunement a deliberate, findable moment rather than
an invisible side effect of exploring.
Travel destinations now match the stones' real positions (v1.24) โ v1.23's hand-tuned stone
positions live in gameobject, but .waygate actually teleports players using
mod_waygate_network_locations, which still held each destination's original coordinates.
Corrected by setting each destination's position_x/y/z/orientation to a point 1 yard directly in
front of its stone (along the stone's own orientation), facing back toward it on arrival โ
computed directly from the stones' now-current positions, not re-walked by hand. Takes effect
immediately via the existing .waygate reload GM command, no server restart needed (unlike stone
position/rotation changes themselves, which do need one).
Stone orientation had a real mesh-offset bug, now fixed (v1.25) โ the "Ancient Stone Marker"
model renders its own front 90 degrees ahead of whatever orientation is stored, confirmed by
checking multiple stones after a restart ruled out stale data. Every stone's stored orientation is
now offset -90 degrees to compensate, so the model's visual facing matches the direction originally
intended by each .gps reading. The destination-offset math above deliberately keeps using the
pre-correction direction (already confirmed correct on its own), not this cosmetic fix.
Model: displayId 7789, the real "Ancient Stone Marker" (gameobject_template entry 188469),
type = 10 (GAMEOBJECT_TYPE_GOOBER). Two real bugs before landing here:
displayId 9501 โ turned out
invalid on this server: .gobject add logged "have invalid displayId (9501), not spawned" for
both our clone and the real, unmodified 19501 entry, confirming it wasn't a scripting bug but
a missing DBC record โ this server's actual loaded GameObjectDisplayInfo.dbc is a reduced client
build (~3790 of a normal ~9000+ display records), and 9501 isn't one of them.displayId 236), rendered fine but couldn't be
interacted with at all โ because it's type = 5 (GAMEOBJECT_TYPE_GENERIC), and every real
"click this object, run custom logic via OnGossipHello" GameObjectScript already in this
codebase (go_southfury_moonstone, go_tele_to_dalaran_crystal, etc.) turned out to use
type = 10 (GAMEOBJECT_TYPE_GOOBER) instead. GENERIC is plain, non-interactive scenery โ the
client most likely never offers a "use" prompt for it at all, so OnGossipHello never had a
chance to fire regardless of how correctly it was wired server-side.Both fixed by "Ancient Stone Marker": displayId 7789 is confirmed present in this server's real
DBC, type = 10 matches every real working precedent, and its one non-zero field (Data3 =
autoCloseTime, a cosmetic 3-second "activated" reset) is irrelevant here since OnGossipHello
returning true suppresses the default open/close behavior before it would ever run. A couple of
thematically-tempting Titan-flavored alternates (Titan Relic, The Discs of Norgannon, Ulduar Teleporter) were also considered and rejected along the way โ all carry real hidden
SmartGameObjectAI/spell-focus/quest-link behavior a plain override isn't guaranteed to fully
suppress.
Each of the 74 gets its own dedicated template entry rather than sharing one: the same
one-custom-ID-per-spawn pattern already used for this module's NPCs (901000, 911001,
911002-911006), so the C++ side can resolve which destination a given stone represents from its
own template entry (go->GetEntry() - WAYGATE_STONE_ENTRY_BASE) rather than its spawn guid or
position. Real gameobject.guid values in this client's local DB already run into the millions โ
too real a collision risk to use as a deterministic per-destination lookup key the way this module's
earlier NPC work could safely do with low, self-chosen ranges.
Positioning: every stone originally reused its destination's coordinates verbatim, which
themselves were sourced by copying a real landmark NPC's exact position back in the v1.11-v1.14
continent batches โ so every stone spawned clipped inside/underneath an NPC, and in three spots
(Echo of the Ley Wardens, Windcaller Tyese, The First Warden all sit at a destination's exact
coordinates too) inside a Ley Wardens quest NPC as well. Fixed by nudging each stone 3 yards along
its destination's own saved orientation, and the three affected quest NPCs 3 yards the opposite
way โ the original landmark NPC stays exactly where it was, the stone and the quest NPC end up 6
yards apart on either side of it. That first pass was a heuristic, not individually terrain-verified
(no vmap/LOS data available to this tooling) โ since superseded: every stone has since been
individually walked and repositioned by hand in-game (2026-08-21, see "Known v1 limitations" below).
Successfully attuning also plays a one-shot cosmetic flash (SendPlaySpellVisual,
WaygateNetwork.AttuneVisualKitId, default kit 3394 โ the same real Teleport-family visual kit's
ImpactKit already relied on for the travel channel's kit 267, keeping the two moments visually
related) plus a one-shot animation (HandleEmoteCommand(EMOTE_ONESHOT_USE_STANDING), the real
generic "reach out and use a standing object" gesture) on the player, on top of the existing sound
and chat message.
Stones are gated the same way .waygate itself is. Interacting with one before the Ley Wardens
chain is complete doesn't attune anything โ it opens a gossip popup with in-world flavor text
instead ("You feel it the moment your hand touches the stone...", npc_text 911012), pointing
toward the Hillsbrad Foothills in a general sense without naming Aemos directly (the .waygate
command's own dormant-network message already does that โ this is often a character's first hint
that anything's going on, before they've ever typed the command, so it stays a level more mysterious).
WaygateNetwork.RequireIntroQuest covers both gates together; GM debug mode is the only bypass for
either.
OnPlayerUpdateZone still fires on zone-entry, but now only as a hint ("You sense a waygate stone somewhere nearby...") for an un-attuned destination โ a nudge, not a trigger. It still repeats every
time an un-attuned character re-enters a different un-attuned zone on foot (intentional, not a
bug), but two guards keep it from spamming (v1.21): skipped entirely while on a taxi (IsInFlight())
โ a single flight crossing several un-attuned destination zones back to back turned out to be the
actual source of "several messages in a row" reports, not one zone re-triggering โ and deduped
against the last destination actually hinted (Player::CustomData, session-scoped, not persisted),
so re-entering the same zone doesn't repeat the message.
Each destination has a continent column (ENUM('Eastern Kingdoms','Kalimdor','Outland','Northrend')
in the world DB) that drives the two-page menu โ added once the list grew past the original 8
capitals, so a flat list wouldn't keep scaling. This is a curated label, not derived from the raw
map column โ Exodar and Silvermoon share map = 530 with genuine Outland zones purely as a
client-data quirk (their actual continents, Azuremyst Isle and Eversong Woods, got bundled onto that
map ID by Blizzard), so grouping by raw map would put them in a nonsensical "Outland" bucket from a
player's point of view. They're tagged Kalimdor and Eastern Kingdoms respectively instead โ set
continent deliberately for any new destination, don't infer it from map.
Each destination also has a faction column (ENUM('Alliance','Horde','Neutral'), default
Neutral) so a character can never end up dumped in the middle of an enemy capital just to get
jumped by guards. WaygateFactionAllowed checks it against Player::GetTeamId() โ an Alliance/
Horde destination is filtered out of the menu (GetSelectableDestinations) and re-checked again in
BeginWaygateChannel/FinishWaygateChannel (same "don't trust the gossip action alone" defense the
attunement check already uses) for anyone not on that faction; Neutral destinations (the two
sanctuary cities, Shattrath and Dalaran) are always open to everyone. The 8 original capitals are
tagged Alliance (Stormwind, Ironforge, Darnassus, The Exodar) or Horde (Undercity, Orgrimmar,
Thunder Bluff, Silvermoon City) accordingly โ set faction deliberately for any new destination,
same as continent.
GM debug mode (WaygateNetwork.Debug) is the only override โ it already lists/travels to every
destination regardless of attunement (see "Admin/debug tooling" in PLAN.md), and that bypass covers
faction too, so a GM can still review or test an opposing-faction destination without needing a
second character.
.waygate doesn't do anything until a character completes a 5-quest chain, mostly dialogue but with
one real combat objective. WaygateNetwork.RequireIntroQuest (default on) controls this; GM debug
mode is the only other bypass. The check itself is a single guard clause in HandleWaygateCommand:
player->GetQuestRewardStatus(QUEST_WAYGATE_REACTIVATION) โ no custom CreatureScript hooks needed
anywhere in the chain, core's default gossip/quest UI handles a plain accept/turn-in questgiver
entirely from creature_queststarter/creature_questender + quest_template*. The one exception is
the kill-credit objective on quest 2 (RequiredNpcOrGo1/RequiredNpcOrGoCount1 on quest_template
โ positive value = creature kill credit, still no custom C++ needed).
Redesigned (2026-08-16) to stay entirely within Eastern Kingdoms and Kalimdor, unlockable early
(MinLevel = 15, QuestLevel = 20 โ paced so most characters finish around level 20), starting in a
zone with both a real Alliance and Horde town nearby. Story: Aemos, a sentient stone construct โ
the last surviving guardian of the network, newly and only partly reawakened โ is found in the
Hillsbrad Foothills, the contested no-man's-land between Southshore and Tarren Mill. Deliberately not
a member of either faction, so neither side "owns" the lore (AllowableRaces = 0 on every quest).
Each quest is accepted from one NPC and turned in to the next leg's NPC rather than doubling back,
so the chain is one long trip across the world rather than repeated out-and-back errands, closing
with a return to Aemos for the reactivation itself:
Coordinates are real and DB-verified, not guessed: Aemos sits clear of the real levelled wildlife that already populates the Hillsbrad no-man's-land (Starving Mountain Lion/Vicious Gray Bear, both 20-23, an apt match for the chain's pace). Borin Ironquill sits a few yards from the real Explorers' League dig camp at Uldaman's entrance (High Explorer Magellas et al.) without overlapping any of them. Windcaller Tyese reuses Waygate destination 28's zone (Astranaar) but is offset ~4 yards from Innkeeper Kimlya, who sits at that destination's exact coordinates โ the same overlap this module already fixed once for the Waygate Stones (see "Waygate Stones" below). The First Warden sits a few yards from the real Horde outpost Windshear Crag (Innkeeper Jayka).
Borin Ironquill occupies the NPC slot originally used by "Echo of the Ley Wardens" โ repurposed, not
newly added, since the "spectral echo" framing didn't fit revealing something older than the Ley
Wardens themselves (an echo of the order wouldn't know that). The former fourth leg, "Nazrek the
Bent" (the old Outland contact), is retired entirely with no replacement โ the new geography only
needs three intermediate nodes. The Ironbound Sentinel's stats are modeled directly on Mor'Ladim
(creature_template entry 522, Duskwood's real solo-able open-world elite) rather than invented โ
same HealthModifier/DamageModifier/level/rank โ and its display (6026) is reused from the real
"Earthen Guardian" creature, which only ever spawns inside the Uldaman instance, so there's no
open-world collision risk.
An alternative to flight paths, not a replacement โ deliberately priced a bit above a comparable
flight so flying is still the better deal for a route you can actually fly. Same-map trips cost
WaygateNetwork.CostPerYard copper per yard of straight-line distance from the player's current
position to the destination; cross-map trips (different continents) cost a flat
WaygateNetwork.CrossMapCostCopper, since straight-line distance isn't meaningful across different
coordinate spaces. WaygateNetwork.MinCostCopper is a floor so short hops still cost something.
The defaults are a starting point, not a precise calibration: this client's real TaxiPath.dbc
was checked directly (not guessed) โ real flight prices range ~50c (Stormwind โ Ironforge) to
~12000c for the longest single-continent legs, and aren't a clean function of distance. The default
CostPerYard (0.02) puts Stormwind โ Ironforge at ~84c, about 1.7x that real 50c price โ the one
real anchor point actually available. Other routes will drift from that exact ratio. Tune the config
values against what you observe in-game rather than trusting these as scientifically calibrated.
Debug mode (WaygateNetwork.Debug, GM-only) travels free โ it's for reviewing/testing locations,
not for dodging the cost.
Picking a destination starts a WaygateNetwork.ChannelMs-long wait (default 4 seconds) before
you're actually charged and teleported โ no real spell involved at all. It's a plain,
fully server-controlled wait: a chat message ("Hold still... the waygate reaches for you."), an
optional one-shot cosmetic visual (SendPlaySpellVisual, WaygateNetwork.ChannelVisualKitId), and a
looping "channeling" animation on the player (SetEmoteState(EMOTE_STATE_SPELL_CHANNEL_OMNI) โ
Blizzard's own generic spell-channel stance, cleared unconditionally once the wait ends, success or
not) โ all purely cosmetic, zero gameplay side effects โ plus a timer via the player's own event
scheduler (player->m_Events.AddEventAtOffset, BeginWaygateChannel/FinishWaygateChannel). Move
away from where the wait started (checked by straight-line distance, allowing 2 yards of slack, plus
a same-map check) and it cancels with no charge; entering combat at either end blocks it too. Cost is
only charged once the wait actually completes.
This replaced an earlier design that hijacked a real spell's cast bar (first the Mage
Teleport: <City> family, then Hearthstone, ID 8690) purely to get a genuine cast bar/interrupt
UI for free. It worked in principle โ real cast time pulled directly from this client's Spell.dbc,
a conditional SpellScript gated on a pending-entry marker so a real cast of the donor spell was
never touched โ but in-game testing found overriding the donor's actual effect
(PreventHitEffect) wasn't reliable: the real Hearthstone "teleport home" effect and its ~30-minute
cooldown still fired some of the time despite the guard, in a way that was hard to fully pin down or
trust. See PLAN.md's "v1.7"/"v1.8" sections for the full blow-by-blow (including why generic monster
"Teleport" spells and other "Casting"/"Channeling"-named spells were checked and ruled out earlier in
that exploration). data/sql/db-world/updates/2026_08_15_03.sql removes the now-unused
spell_script_names binding that approach had added to spell 8690.
WaygateNetwork.ChannelVisualKitId defaults to 267 โ real data, not guessed: traced through this
client's binary Spell.dbc -> SpellVisual.dbc -> SpellVisualKit.dbc, it's the Mage Teleport:
family's actual sustained casting-loop visual (the glowing rune circle spun under the caster's feet
while the cast bar fills), in-game confirmed to look right for this use. Preview any other kit
in-game (GM-only) with .debug play visual <id> if you want to try alternatives.CreatureDisplayID 26484, reused from existing Alterac Valley Portal
content) is a placeholder, not custom-built for this module.acore_characters synchronously (CharacterDatabase.Query, not the
async callback pattern) โ fine at this server's scale, but not the core's preferred pattern for a
hot path.|T...|t) in gossip text hasn't actually been eyeballed in-game yet โ same "not
verified against a real client" caveat as everything else in this module until you say otherwise.data/sql/db-world/updates/2026_08_21_02.sql) โ v1.17's blind 3-yard-offset heuristic is retired;
every stone now sits at a real, walked-and-verified position rather than an estimate.npc_text, no creature attached to it directly)npc_waygate_portal, the summon-only Waygate Portal NPC)911003 originally held "Echo of the
Ley Wardens," repurposed in place into Borin Ironquill rather than retired (see "Intro quest chain"
above); 911005 ("Nazrek the Bent") is retired entirely and no longer in use. quest_greeting rows
reuse these same entries directly (keyed by NPC entry, not a separate ID range).912000-912004 are the five Ley Wardens
quests (912005 retired in the 2026-08-16 redesign, not reused), leaving room for a future second
chain without renumbering.gameobject_template IDs 913001-913074 โ one Waygate Stone per destination
(entry = 913000 + mod_waygate_network_locations.id), see "Waygate Stones" above.911xxx text/data space alongside 911000.mod_waygate_network_locations IDs 1-8 are reserved for the original capital cities, 9-10
for Shattrath City / Dalaran, 11-27 for the Eastern Kingdoms quest-hub batch, 28-45 for the
Kalimdor batch, 46-61 for the Outland batch, 62-74 for the Northrend batch (all referenced
by existing mod_waygate_network_discovered rows) โ new destinations should start at 75.Spell ID 910000 (Call Waygate) is retired โ see "Why a command, not a spell" above. Its spell_dbc
/ spell_script_names rows are removed by data/sql/db-world/updates/2026_08_13_01.sql, and any
character that had learned it gets it unlearned by
data/sql/db-characters/updates/2026_08_13_01.sql. Don't reuse 910000 for anything else without
checking those files.
This module doesn't use or reserve any real spell ID anymore โ see "Cast time" above for why the
earlier Hearthstone-hijack approach (spell ID 8690) was dropped, and
data/sql/db-world/updates/2026_08_15_03.sql for the cleanup of the binding it had added.
See conf/mod_waygate_network.conf.dist:
WaygateNetwork.Enable (default 1) โ toggles the whole module: attuning destinations on
zone-enter and the .waygate travel menu.WaygateNetwork.Debug (default 0) โ GM-only "show all locations" mode, see above.WaygateNetwork.UnlockSoundId (default 1519, TaxiNodeDiscovered) โ SoundEntries.dbc ID
played on attunement. Real data, not guessed: traced through this client's binary
SoundEntries.dbc, it's Blizzard's own sound for discovering a new flight point โ the closest real
in-game equivalent to unlocking a new fast-travel destination.WaygateNetwork.AttuneVisualKitId (default 3394) โ cosmetic SendPlaySpellVisual flash on
attunement, see "Waygate Stones" above.WaygateNetwork.CostPerYard (default 0.02), WaygateNetwork.CrossMapCostCopper (default
20000), WaygateNetwork.MinCostCopper (default 25) โ see "Travel cost" above.WaygateNetwork.ChannelMs (default 4000), WaygateNetwork.ChannelVisualKitId (default 267) โ
see "Cast time" above.WaygateNetwork.RequireIntroQuest (default 1) โ gates both .waygate and the Waygate Stones
themselves behind the Ley Wardens quest chain, see "Intro quest chain" and "Waygate Stones" above.