Compare commits

...
Sign in to create a new pull request.

12 commits

Author SHA1 Message Date
4ce7ac2b99 Merge pull request 'bug: wrong-zone resources spawning after PR #7' (#8) from fix/herb-pool-scoping-regression into main 2026-09-09 20:16:12 -06:00
Troll (Hermes Agent)
5ffac92934 fix: zone pool parser truncated every zone to its first entry
GoldRush.ZonePool separates zones with '; ' (semicolon + space) but the
node/bonus entry IDs within a zone with a bare ';'. BuildSites() split on
the bare ';', so each zone's entry list was cut down to its first ID --
which is always a herb. That is why events spawned a single wrong-zone
herb and no ore at all (PR #8).

Split on the two-character '; ' zone delimiter instead, so every zone
keeps its full node and bonus pools.
2026-09-10 02:15:44 +00:00
Claude
a0a3574b14 docs: report wrong-zone resource spawning after PR #7
Live-tested in Durotar after the herb/ore categorization fix merged
and rebuilt: spawned Mageroyal almost exclusively, which is not in
Durotar's configured pool (verified the live config is correct and
uncorrupted -- Peacebloom/Silverleaf/Earthroot/Copper Vein). No ore
appeared either, despite Copper Vein being in the list and containing
the word 'vein' -- which should have matched even the pre-PR-#7
keyword heuristic, suggesting this isn't purely a categorization
issue.

Hypothesis: herb/ore pool building may now filter against the full
set of IDs the lookup table knows about, rather than the current
site's own configured entries. Flagged for direct verification against
the merged code rather than assumed.
2026-09-10 01:45:19 +00:00
138a79d338 Merge pull request 'fix: herb/ore categorization broken for most v2 zones' (#7) from fix/herb-ore-category-lookup into main
Reviewed-on: #7
2026-09-09 07:33:19 -06:00
Troll (Hermes Agent)
24b0815fc6 fix: herb/ore categorization via static ID lookup table
SplitByCategory() guessed herb/ore from display-name keywords written
for the original 5-entry Northrend pool. Outland/Classic ore uses
'Deposit' not 'Vein', and most herb names match no keyword, so both
buckets came back empty for almost every v2 zone and the code silently
fell back to the unsplit pool (confirmed live in Hellfire Peninsula).

Replace the keyword guess with a static ID->category lookup table built
from the DB-verified entry IDs in docs/zone-specific-resources-spec.md.
The old keyword check is retained only as a fallback for entries not in
the table.
2026-09-08 23:04:04 +00:00
Claude
5d292b3d9c docs: fix spec for broken herb/ore categorization
SplitByCategory() guesses herb/ore purely from keywords in the
display name (lotus/clover/thorn/bloom for herb, vein for ore) --
written for the original 5-entry Northrend default pool, never
updated for the ~70 entries the v2 zone-resources feature (PR #5)
introduced. Confirmed live in Hellfire Peninsula: none of its 4
resources (Felweed, Dreaming Glory, Fel Iron Deposit, Adamantite
Deposit) match any keyword, so both herb and ore buckets come back
empty and every zone silently falls back to the unsplit pool --
breaking the intended herb/ore alternation for the large majority of
the 57 configured zones.

Spec provides the full herb/ore classification for every entry ID
already verified against the live DB when the zone pool was built,
so this can be implemented as a static ID lookup instead of
continuing to patch an ever-fragile keyword list.
2026-09-08 22:55:58 +00:00
fa79526eb6 Merge pull request 'fix: player-anchored events never got zone-specific resources' (#6) from fix/player-anchored-site-missing-zone-resources into main
Reviewed-on: #6
2026-09-04 18:13:14 -06:00
Claude
c9a6bf5f58 fix: player-anchored events never got zone-specific resources
SelectSiteForPlayer() tries to anchor an event on a live player's
current position FIRST (BuildEligibleSiteFromPlayer ->
BuildSiteFromAnchor), only falling back to a random pick from _sites
when no eligible player is available. Since there is almost always
an eligible player online, this is the common path in practice --
but BuildSiteFromAnchor only reads the player's live zone/area IDs
from the world state; it has no access to the NodeEntries/
BonusEntries that BuildSites() parsed from GoldRush.ZonePool into
_sites. Every player-anchored event therefore got an empty
NodeEntries/BonusEntries regardless of ZonePool configuration, and
SpawnHotspot()'s (correct, intentional) fallback-to-global-pool logic
silently kicked in every time.

This is why testing in Un'Goro Crater after the v2 zone-resources
feature shipped still produced Titanium Vein and Northrend herbs --
the module correctly identified the player's zone as Un'Goro
(ZoneId/ZoneLabel were right), it just never looked up Un'Goro's
configured resource pool to attach it to that site.

Fix: after building the site from the player's live position, look
up a matching entry in _sites by ZoneId and inherit its NodeEntries/
BonusEntries. A zone that's an eligible anchor but has no configured
entry in _sites still correctly falls through to the global pool --
that fallback path was never the problem, only the missing lookup
for zones that ARE configured.
2026-09-05 00:11:05 +00:00
28248f0fa7 Merge pull request 'feat: zone-specific resource pools across 57 zones (v2 spec)' (#5) from feature/zone-specific-resources-v2 into main 2026-09-04 14:25:28 -06:00
3a15d9af36 Merge pull request 'docs: zone-specific resource nodes spec (full zone coverage)' (#4) from docs/zone-specific-resources-spec into main 2026-09-04 14:25:20 -06:00
Claude
d386536d27 docs: expand zone-specific resources to full zone coverage (v2)
Extends the spec from 4 hardcoded hotspots to essentially every valid
open-world zone in WotLK 3.3.5a -- ~50 zones total.

Every zone name was validated against AzerothCore's own AreaTable.dbc
reference for 3.3.5a before being included. The original request list
contained several Cataclysm-only zones (Twilight Highlands, Abyssal
Depths, Shimmering Expanse, Kelp'thar Forest, Ruins of Gilneas, the
Northern/Southern Barrens split) that don't exist in this game
version -- those were dropped rather than guessed at. Two zones that
do exist (Plaguelands: The Scarlet Enclave, Mount Hyjal) were
deliberately excluded as impractical for a shared open-world event
(phased/instanced content, not open zones in 3.3.5a).

All ~70 resource entry IDs are individually verified against live
acore_world.gameobject_template, organized by the real Vanilla/TBC/
WotLK herbalism and mining tier progression -- not guessed from
memory. Confirmed, notably, that no 'Rich' ore variant exists for
Copper/Tin/Silver/Iron/Mithril in this database (only Thorium+ tiers
got them), matching actual game design rather than assuming symmetry
across ore tiers.

Same underlying code mechanism as v1 (GoldRushSite gains NodeEntries/
BonusEntries, SpawnHotspot prefers per-site pool with fallback to
global) -- this commit only expands the data, not the approach.
2026-09-04 20:05:27 +00:00
Claude
915b771b78 docs: zone-specific resource nodes spec
Makes node selection zone-aware instead of drawing from one global
pool for every hotspot. Includes real herb/ore entries verified
against the live acore_world database for all four configured
hotspots (Un'Goro Crater, Winterspring, Eastern Plaguelands, Sholazar
Basin), plus a rare 'bonus' tier per zone (Black Lotus for the three
Classic zones -- a real, accurate spawn-zone match, not an arbitrary
pick).

Extends the existing pipe-delimited ZonePool config format with two
optional segments rather than introducing a new config key, and
explicitly calls out backward compatibility for hotspots that don't
specify zone-specific resources.
2026-09-04 19:41:16 +00:00
5 changed files with 528 additions and 5 deletions

View file

@ -17,6 +17,22 @@ All notable changes to `mod-gold-rush` will be documented in this file.
global `GoldRush.NodeEntries` pool exactly as before.
### Fixed
- Zone pool parsing no longer truncates every zone to its first resource entry.
`GoldRush.ZonePool` separates zones with `"; "` (semicolon + space) but the
node/bonus entry IDs within a zone with a bare `";"`. `BuildSites()` split on
the bare `";"`, so each zone's entry list was cut down to its first ID — which
is always a herb. That is why events spawned a single wrong-zone herb and no
ore at all (reported in PR #8). The parser now splits on the two-character
`"; "` zone delimiter, so every zone keeps its full node and bonus pools.
- Herb/ore categorization no longer guesses from display-name keywords. The old
`SplitByCategory()` matched "lotus"/"clover"/"thorn"/"bloom" for herbs and
"vein" for ore, which only worked for the original Northrend default pool and
silently fell back to the unsplit pool for almost every v2 zone (Outland/Classic
ore uses "Deposit", not "Vein"; most herb names match no keyword). Replaced with
a static ID→category lookup table built from the DB-verified entry IDs in
`docs/zone-specific-resources-spec.md`, with the old keyword check retained only
as a fallback for entries not in the table. See
`docs/herb-ore-category-fix-spec.md`.
- Event start/end announcements no longer crash the server. `StartEvent()` and
`EndEvent()` both used `ChatHandler(nullptr).SendWorldText(...)` to broadcast
server-wide -- `ChatHandler::SendWorldText` assumes a real session and calls

View file

@ -0,0 +1,114 @@
# Fix: herb/ore categorization breaks for almost every zone in the v2 pool
## The bug, confirmed by reading the code
`SplitByCategory()` (`GoldRush.cpp:648`) decides whether a resource entry
is a herb or ore purely by matching keywords in its display name:
```cpp
bool isHerb = ContainsWord(name, "lotus") || ContainsWord(name, "clover") || ContainsWord(name, "thorn") || ContainsWord(name, "bloom");
bool isOre = ContainsWord(name, "vein");
```
This was written against the *original 5-entry Northrend default pool*
(Goldclover→"clover", Icethorn→"thorn", Frost Lotus→"lotus", Titanium
**Vein**→"vein") and never updated when the v2 zone-resources feature (PR
#5) introduced ~70 new resource names across 57 zones.
**Confirmed failure case**: tested live in Hellfire Peninsula. Its
configured pool is Felweed, Dreaming Glory, Fel Iron **Deposit**,
Adamantite **Deposit**. None of these match any herb keyword, and neither
ore entry contains "vein" — most Outland/Classic ore uses "Deposit," not
"Vein." So both `herbEntries` and `oreEntries` come back empty, and
`SpawnHotspot()` (`GoldRush.cpp:998-1017`) falls back to
`fallbackEntries` (the whole site pool, unsplit) for every node. The
intended herb/ore alternation silently never happens for the large
majority of the 57 configured zones — only the handful whose names happen
to contain one of the four hardcoded herb words or "vein" categorize
correctly at all.
**Observed symptom**: the test run in Hellfire produced overwhelmingly
Felweed with no observed ore, despite the fallback pool containing ore
entries. Whether that's pure sampling variance or a second, separate
issue (see "Worth checking after this fix" below) isn't confirmed from
reading the code alone — don't assume the placement-bias theory without
testing after this fix ships.
## Fix: stop guessing category from the name, use a known lookup table
Every entry ID used across the 57-zone pool was already individually
verified against `acore_world.gameobject_template` when the pool was
built (`docs/zone-specific-resources-spec.md`). Rather than continue
patching an ever-growing, ever-fragile keyword list — the next zone added
will just as easily introduce another unmatched name — replace the
name-based guess with a static ID→category lookup built from data that's
already verified correct.
**Add a lookup table** (e.g. `static const std::unordered_map<uint32,
bool> IsHerbById` or similar, `true` = herb, `false` = ore), populated
from every entry in the tables below. Change `SplitByCategory` to check
this table first; **keep the existing name-keyword check as a fallback
only** for any entry not found in the table (defensive — covers the
original global-pool default entries and anything added by hand later
without an explicit table update).
### Herb entries (mark `true`)
```
1618, 3724, 1617, 3725, 1619, 3726, // Peacebloom, Silverleaf, Earthroot
1620, 3727, 1621, 3729, 2045, 1622, 3730, // Mageroyal, Briarthorn, Stranglekelp, Bruiseweed
1623, 1624, 2041, 2042, 2046, 2043, 2866, // Wild Steelbloom, Kingsblood, Liferoot, Fadeleaf, Goldthorn, Khadgar's Whisker, Firebloom
142140, 180165, 142141, 176642, // Purple Lotus, Arthas' Tears
142142, 176636, 180164, 142143, 183046, // Sungrass, Blindweed
142144, 142145, 176637, 176587, 176641, // Ghost Mushroom, Gromsblood, Plaguebloom
176583, 176638, 180167, 176584, 176639, // Golden Sansam, Dreamfoil
180168, 176586, 176640, 180166, 176588, // Mountain Silversage, Icecap
191303, 176589, // Firethorn, Black Lotus
181270, 183044, 181271, 183045, 181275, // Felweed, Dreaming Glory, Ragveil
183043, 181277, 181276, 181279, 181280, 181281, // Terocone, Flame Cap, Netherbloom, Nightmare Vine, Mana Thistle
189973, 190171, 190172, 190176, 190170, // Goldclover, Lichbloom, Icethorn, Frost Lotus, Talandra's Rose
191019, 190169 // Adder's Tongue, Tiger Lily
```
### Ore entries (mark `false`)
```
1731, 2055, 3763, 103713, 181248, // Copper Vein
1732, 2054, 3764, 103711, 181249, // Tin Vein
1733, 105569, // Silver Vein
1735, // Iron Deposit
1734, 150080, 181109, // Gold Vein
2040, 150079, 176645, // Mithril Deposit
2047, 150081, 181108, // Truesilver Deposit
324, 150082, 176643, 175404, // Small/Rich Thorium Vein
165658, // Dark Iron Deposit
181555, 181556, 181569, 181570, 181557, // Fel Iron, Adamantite (+ Rich), Khorium
189978, 189979, 189980, 189981, 191133 // Cobalt/Saronite Deposit (+ Rich), Titanium Vein
```
(This is every entry that appears anywhere in `gold_rush.conf.dist`'s
`GoldRush.ZonePool` plus the module's own default global
`GoldRush.NodeEntries` — cross-check against both files while
implementing, in case an entry was missed here.)
## Worth checking after this fix — don't build blind for this part
Once real ID-based categorization is in, re-test Hellfire Peninsula (or
another Outland zone) with `.goldrush teststart` a few times. If ore
still rarely/never appears despite `oreEntries` now correctly populated,
look at the placement-validation loop (`GoldRush.cpp:1019` onward) —
specifically the `MaxVerticalDrift` cliff/rock-face rejection. Ore
deposits are commonly placed on steep terrain in the actual game world;
if placement attempts for ore entries are failing validation far more
often than herb entries, that's a second, separate bug worth its own fix
— but confirm it's actually happening before changing that logic, rather
than assuming.
## Testing
- Hellfire Peninsula (the confirmed failure case) should now show a mix
of Felweed/Dreaming Glory *and* Fel Iron/Adamantite, not all-herb.
- Spot-check one or two zones whose original names *did* match the old
keyword list (e.g. Un'Goro Crater — Firethorn contains "thorn", Black
Lotus contains "lotus") to confirm the table-based path didn't regress
anything that used to work by keyword-coincidence.
- Confirm entries not in either table (if any exist) still fall through
to the old keyword check rather than being silently dropped.

View file

@ -0,0 +1,56 @@
# Bug: events spawning resources from the wrong zone entirely
## Observed, live-tested after PR #7 merged and rebuilt
`.goldrush teststart` in **Durotar** spawned **Mageroyal** almost
exclusively. Mageroyal is not in Durotar's configured pool at all —
Durotar's `GoldRush.ZonePool` entry is:
```
Durotar|1618;3724;1617;3725;1619;3726;1731;2055;3763
```
(Peacebloom, Silverleaf, Earthroot, Copper Vein — verified directly
against the live config file, not corrupted.) Mageroyal (1620/3727)
belongs to a different zone's list entirely (The Barrens). No ore
(Copper Vein) appeared either, despite being in Durotar's list and
containing "vein" — a keyword that should have matched even under the
*old*, pre-PR-#7 categorization logic, which is a strong hint this isn't
purely a categorization problem.
Same no-ore symptom was also observed in Zangarmarsh (Outland) before
this Durotar test — so it isn't zone-specific.
## Hypothesis — check this first, don't assume
The live config data is confirmed correct, and the herb that *did* spawn
is a real herb from the correct global category, just from the **wrong
zone's list**. That points at a scoping bug: somewhere in how
`herbEntries`/`oreEntries` get built, the code may now be filtering
against the *entire universe* of IDs the lookup table (from PR #7) knows
about as herbs, rather than filtering `_currentSite`'s own configured pool
by category. That would explain a wrong-zone herb appearing in Durotar,
and — if there's an analogous issue on the ore side, or the ore bucket
ends up empty/never selected due to how the fix restructured the
category-split logic — could also explain the missing ore.
**Please verify directly against the actual merged code** (I haven't
re-opened the PR 7 diff for this report) exactly how the herb/ore split
is being built post-fix:
- Is it `SplitByCategory(_currentSite.NodeEntries or siteNormalPool, ...)`
— filtering the *site's own* entries by category — or did the fix
change this to build from some broader/global set?
- If the lookup table itself is being used correctly for
categorization, but something else changed which *pool* it's applied
to, that's the actual regression to find and fix.
## Testing once fixed
- Durotar: only Peacebloom/Silverleaf/Earthroot/Copper Vein should
appear, nothing from any other zone.
- Confirm ore (Copper Vein) actually shows up in Durotar, and Fel
Iron/Adamantite show up in Zangarmarsh — both were absent in testing
so far.
- Spot-check one more zone on a different continent (e.g. Sholazar
Basin) to confirm it isn't pulling in entries from unrelated zones
either.

View file

@ -0,0 +1,232 @@
# Zone-specific resource nodes — implementation spec (v2: full zone coverage)
**This supersedes the original 4-zone version of this spec.** Same
underlying mechanism (see section 3, unchanged), but `GoldRush.ZonePool`
now covers essentially every valid open-world zone in the game instead of
just four, each with resources appropriate to that zone. Any hotspot not
explicitly listed here falls back to the existing global `NodeEntries`
pool, exactly as before.
---
## 0. Zone list validation — do this before using this spec, not after
The starting zone list for this spec came from a hand-written file that
included several zones that **do not exist in WotLK 3.3.5a** — Cataclysm
additions/renames (Twilight Highlands, Abyssal Depths, Shimmering Expanse,
Kelp'thar Forest, Ruins of Gilneas, the Northern/Southern Barrens split,
"Northern Stranglethorn" as a standalone zone, Uldum as a full zone rather
than a Tanaris subzone, "Ahn'Qiraj: The Fallen Kingdom" which doesn't match
any real zone name).
**Every zone name below was cross-checked against AzerothCore's own
published `AreaTable.dbc` reference for version 3.3.5a**
(https://www.azerothcore.org/wiki/areatable) — not memory, not assumption.
If this spec is ever extended with more zones later, verify the same way
before adding them; guessing zone names that don't exist in this exact
game version has already cost real time once in this project.
**Two zones were confirmed real but deliberately excluded** as impractical
for a shared open-world event:
- **Plaguelands: The Scarlet Enclave** — real, but phased, instanced, and
faction-separated (the Death Knight starting experience). An event here
would be invisible to almost every player.
- **Mount Hyjal** — raid-instance content in 3.3.5a, not an open leveling
zone (it didn't become open world until Cataclysm).
**Hrothgar's Landing** is real and included, but flagged as niche — it's a
small island gated behind Argent Tournament rank progression, so events
there will reach a much smaller audience than the rest of the list.
---
## 1. Resource universe — every entry below verified against live `acore_world.gameobject_template` (type=3)
### Classic herbs (Eastern Kingdoms / Kalimdor)
| Tier (~level) | Herb | Entries |
|---|---|---|
| 1-10 | Peacebloom | 1618, 3724 |
| 1-10 | Silverleaf | 1617, 3725 |
| 1-10 | Earthroot | 1619, 3726 |
| 10-20 | Mageroyal | 1620, 3727 |
| 15-25 | Briarthorn | 1621, 3729 |
| 15-25 | Stranglekelp (coastal only) | 2045 |
| 15-25 | Bruiseweed | 1622, 3730 |
| 20-30 | Wild Steelbloom | 1623 |
| 20-30 | Kingsblood | 1624 |
| 30-40 | Liferoot | 2041 |
| 30-40 | Fadeleaf | 2042 |
| 35-45 | Goldthorn | 2046 |
| 35-45 | Khadgar's Whisker | 2043 |
| 40-50 | Firebloom | 2866 |
| 44-54 | Purple Lotus | 142140, 180165 |
| 44-54 | Arthas' Tears | 142141, 176642 |
| 44-54 | Sungrass | 142142, 176636, 180164 |
| 44-54 | Blindweed | 142143, 183046 |
| 45-55 | Ghost Mushroom | 142144 |
| 45-55 | Gromsblood | 142145, 176637 |
| 45-55 | Plaguebloom | 176587, 176641 |
| 48-60 | Golden Sansam | 176583, 176638, 180167 |
| 48-60 | Dreamfoil | 176584, 176639, 180168 |
| 48-60 | Mountain Silversage | 176586, 176640, 180166 |
| 48-60 | Icecap | 176588 |
| 48-60 | Firethorn (Un'Goro only) | 191303 |
| 55+ | Black Lotus (rare/bonus) | 176589 |
### Classic ore
| Tier | Ore | Entries |
|---|---|---|
| 1-15 | Copper Vein | 1731, 2055, 3763, 103713, 181248 |
| 15-25 | Tin Vein | 1732, 2054, 3764, 103711, 181249 |
| 15-25 | Silver Vein | 1733, 105569 |
| 25-35 | Iron Deposit | 1735 |
| 35-45 | Gold Vein | 1734, 150080, 181109 |
| 35-45 | Mithril Deposit | 2040, 150079, 176645 |
| 45-55 | Truesilver Deposit | 2047, 150081, 181108 |
| 50-60 | Small Thorium Vein | 324, 150082, 176643 |
| 50-60 | Rich Thorium Vein (bonus) | 175404 |
| 45-55 | Dark Iron Deposit (Searing Gorge / Burning Steppes only) | 165658 |
**Note**: no "Rich" variant exists for Copper/Tin/Silver/Iron/Mithril in
this database — confirmed by direct query, not assumed. Only Thorium and
the Outland tiers below got Rich variants in the actual game design.
### Outland herbs/ore (all zones, ~58-70)
| Herb | Entries | Ore | Entries |
|---|---|---|---|
| Felweed | 181270, 183044 | Fel Iron Deposit | 181555 |
| Dreaming Glory | 181271, 183045 | Adamantite Deposit | 181556 |
| Ragveil (Zangarmarsh-flavored) | 181275, 183043 | Rich Adamantite Deposit (bonus) | 181569, 181570 |
| Terocone (Nagrand/Blade's Edge-flavored) | 181277 | Khorium Vein (bonus) | 181557 |
| Flame Cap | 181276 | | |
| Netherbloom | 181279 | | |
| Nightmare Vine | 181280 | | |
| Mana Thistle | 181281 | | |
### Northrend herbs/ore (already verified in v1 of this spec)
Goldclover 189973 · Lichbloom 190171 · Icethorn 190172 · Frost Lotus 190176
· Talandra's Rose 190170 · Adder's Tongue 191019 · Tiger Lily 190169 ·
Titanium Vein 191133 · Cobalt Deposit 189978 · Rich Cobalt Deposit 189979 ·
Saronite Deposit 189980 · Rich Saronite Deposit 189981
---
## 2. Full `GoldRush.ZonePool` replacement
One line per zone, `;`-separated, using the format
`ZoneName|NodeEntries|BonusEntries` (no `AreaLabel` segment — using bare
zone names only, since these came verbatim from the AreaTable.dbc dump and
are guaranteed to resolve; a guessed subzone name could silently drop a
zone from the pool if wrong).
```
GoldRush.ZonePool = Dun Morogh|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Elwynn Forest|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Tirisfal Glades|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Durotar|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Teldrassil|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Mulgore|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Azuremyst Isle|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Bloodmyst Isle|1618;3724;1617;3725;1619;3726;1731;2055;3763|; Loch Modan|1620;3727;1621;3729;1731;2055;1732;2054|; Westfall|1620;3727;1621;3729;1731;2055;1732;2054|; Silverpine Forest|1620;3727;1621;3729;1731;2055;1732;2054|; Darkshore|1620;3727;1621;3729;1731;2055;1732;2054|; The Barrens|1620;3727;1621;3729;1731;2055;1732;2054|; Redridge Mountains|1622;3730;1623;1732;2054;1733;105569|; Duskwood|1622;3730;1623;1732;2054;1733;105569|; Ashenvale|1622;3730;1621;3729;1732;2054;1733;105569|; Stranglethorn Vale|1622;3730;1623;2045;1732;2054;1733;105569|; Wetlands|1624;1735|; Hillsbrad Foothills|1624;1735|; Stonetalon Mountains|1624;1735|; Thousand Needles|1624;1735|; The Hinterlands|2041;2042;1735;1734;150080|; Arathi Highlands|2041;2042;1735;1734;150080|; Desolace|2041;2042;1735;1734;150080|; Western Plaguelands|2046;2043;1734;150080;2040;150079|; Swamp of Sorrows|2046;2043;1734;150080;2040;150079|; Dustwallow Marsh|2046;2043;1734;150080;2040;150079|; Badlands|2866;2040;150079;176645|; Searing Gorge|2866;165658;2040;150079;176645|; Feralas|2866;2040;150079;176645|; Blasted Lands|142140;180165;142141;176642;2047;150081;181108|; Burning Steppes|142140;180165;142141;176642;165658;2047;150081;181108|; Tanaris|142142;176636;180164;142143;183046;2047;150081;181108|; Azshara|142140;180165;142141;176642;2047;150081;181108|; Eastern Plaguelands|142144;176587;176641;324;150082;176643;175404|176589; Felwood|142145;176637;324;150082;176643;175404|176589; Silithus|176588;142145;176637;324;150082;176643;175404|176589; Un'Goro Crater|191303;324;150082;176643;175404|176589; Winterspring|142140;180165;142142;176636;180164;324;150082;176643;175404|176589; Isle of Quel'Danas|181270;183044;181271;183045;181279;181555;181556|181557; Hellfire Peninsula|181270;183044;181271;183045;181555;181556|181569;181570; Zangarmarsh|181270;183044;181275;183043;181555;181556|181569;181570; Terokkar Forest|181270;183044;181271;183045;181555;181556|181569;181570; Nagrand|181279;181277;181556|181557; Blade's Edge Mountains|181277;181280;181556|181557; Netherstorm|181280;181281;181276;181557|181557; Shadowmoon Valley|181281;181276;181557|181557; Borean Tundra|189973;191019;189978|189979; Howling Fjord|189973;191019;189978|189979; Dragonblight|190169;190170;189978;189980|189979;189981; Grizzly Hills|190169;190170;189978;189980|189979;189981; Zul'Drak|190171;190172;189980;191133|189981; Storm Peaks|190171;190172;189980;191133|189981; Icecrown|190176;190171;190172;189973;190170;191019;191133|189979;189981; Crystalsong Forest|190176;190171;190172;189973;190170;191019;191133|189979;189981; Sholazar Basin|190171;190172;189973;190170;191019;189978;189979;189980;189981;191133|189979;189981; Hrothgar's Landing|190176;190171;190172;191133|189979;189981
```
**This is long and easy to typo when hand-editing later.** Recommend
keeping a formatted, one-zone-per-line copy in this file (below) as the
source of truth, and only generating the single-line config value from it
mechanically — don't hand-edit the config's one-liner directly for future
changes.
<details>
<summary>One zone per line (source of truth for future edits)</summary>
```
Dun Morogh | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Elwynn Forest | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Tirisfal Glades | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Durotar | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Teldrassil | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Mulgore | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Azuremyst Isle | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Bloodmyst Isle | 1618;3724;1617;3725;1619;3726;1731;2055;3763
Loch Modan | 1620;3727;1621;3729;1731;2055;1732;2054
Westfall | 1620;3727;1621;3729;1731;2055;1732;2054
Silverpine Forest | 1620;3727;1621;3729;1731;2055;1732;2054
Darkshore | 1620;3727;1621;3729;1731;2055;1732;2054
The Barrens | 1620;3727;1621;3729;1731;2055;1732;2054
Redridge Mountains | 1622;3730;1623;1732;2054;1733;105569
Duskwood | 1622;3730;1623;1732;2054;1733;105569
Ashenvale | 1622;3730;1621;3729;1732;2054;1733;105569
Stranglethorn Vale | 1622;3730;1623;2045;1732;2054;1733;105569
Wetlands | 1624;1735
Hillsbrad Foothills | 1624;1735
Stonetalon Mountains | 1624;1735
Thousand Needles | 1624;1735
The Hinterlands | 2041;2042;1735;1734;150080
Arathi Highlands | 2041;2042;1735;1734;150080
Desolace | 2041;2042;1735;1734;150080
Western Plaguelands | 2046;2043;1734;150080;2040;150079
Swamp of Sorrows | 2046;2043;1734;150080;2040;150079
Dustwallow Marsh | 2046;2043;1734;150080;2040;150079
Badlands | 2866;2040;150079;176645
Searing Gorge | 2866;165658;2040;150079;176645 (Dark Iron)
Feralas | 2866;2040;150079;176645
Blasted Lands | 142140;180165;142141;176642;2047;150081;181108
Burning Steppes | 142140;180165;142141;176642;165658;2047;150081;181108 (Dark Iron)
Tanaris | 142142;176636;180164;142143;183046;2047;150081;181108
Azshara | 142140;180165;142141;176642;2047;150081;181108
Eastern Plaguelands | 142144;176587;176641;324;150082;176643;175404 | bonus: 176589
Felwood | 142145;176637;324;150082;176643;175404 | bonus: 176589
Silithus | 176588;142145;176637;324;150082;176643;175404 | bonus: 176589
Un'Goro Crater | 191303;324;150082;176643;175404 | bonus: 176589
Winterspring | 142140;180165;142142;176636;180164;324;150082;176643;175404 | bonus: 176589
Isle of Quel'Danas | 181270;183044;181271;183045;181279;181555;181556 | bonus: 181557
Hellfire Peninsula | 181270;183044;181271;183045;181555;181556 | bonus: 181569;181570
Zangarmarsh | 181270;183044;181275;183043;181555;181556 | bonus: 181569;181570
Terokkar Forest | 181270;183044;181271;183045;181555;181556 | bonus: 181569;181570
Nagrand | 181279;181277;181556 | bonus: 181557
Blade's Edge Mountains | 181277;181280;181556 | bonus: 181557
Netherstorm | 181280;181281;181276;181557 | bonus: 181557
Shadowmoon Valley | 181281;181276;181557 | bonus: 181557
Borean Tundra | 189973;191019;189978 | bonus: 189979
Howling Fjord | 189973;191019;189978 | bonus: 189979
Dragonblight | 190169;190170;189978;189980 | bonus: 189979;189981
Grizzly Hills | 190169;190170;189978;189980 | bonus: 189979;189981
Zul'Drak | 190171;190172;189980;191133 | bonus: 189981
Storm Peaks | 190171;190172;189980;191133 | bonus: 189981
Icecrown | 190176;190171;190172;189973;190170;191019;191133 | bonus: 189979;189981
Crystalsong Forest | 190176;190171;190172;189973;190170;191019;191133 | bonus: 189979;189981
Sholazar Basin | 190171;190172;189973;190170;191019;189978;189979;189980;189981;191133 | bonus: 189979;189981
Hrothgar's Landing (niche) | 190176;190171;190172;191133 | bonus: 189979;189981
```
</details>
---
## 3. Code mechanism (unchanged from v1 — already specced, just restating for completeness)
- `GoldRushSite` struct (`GoldRush.cpp:44`) needs `std::vector<uint32>
NodeEntries` and `std::vector<uint32> BonusEntries`, defaulting empty.
- `BuildSites()` (`GoldRush.cpp:479`) parses the 2nd and 3rd
`|`-delimited segments (in this all-zone-name version, `NodeEntries` is
segment 2 and `BonusEntries` is segment 3, since no `AreaLabel` segment
is used here — adjust the parser accordingly, or use an empty middle
segment as a placeholder if keeping the original 4-segment
`Zone|Area|NodeEntries|BonusEntries` format is preferred for consistency
with a future entry that does want an area label. Pick whichever's
cleaner; document the choice in code comments either way, since the two
format variants look easy to confuse later.
- `SpawnHotspot()` (`GoldRush.cpp:919`) uses `_currentSite.NodeEntries`
when non-empty, falling back to global `_nodeEntries` otherwise.
- Bonus mixing: small per-node chance (`GoldRush.BonusChancePercent`,
~10-15%) to pull from `_currentSite.BonusEntries` instead of the normal
pool for that one node.
- **Backward compatibility required**: a hotspot with no node/bonus
segments must still fall back to the global pool.
## 4. Testing
Given the scale (50+ zones instead of 4), don't try to manually
`.goldrush teststart` every single one before shipping. Reasonable
approach:
- Spot-check one zone per tier (a starting zone, a mid-level zone, a
high-level Classic zone, one Outland zone, one Northrend zone) to
confirm the parsing and per-zone resource selection actually works.
- Confirm the fallback path still works for a hotspot deliberately left
without node/bonus data.
- Trust the DB-verified entry IDs for the rest rather than clicking
through all fifty — the risk at this point is a config-parsing bug
(same for every zone), not a wrong item ID (individually verified above).

View file

@ -24,6 +24,7 @@
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@ -41,6 +42,47 @@ static constexpr float MaxVerticalDrift = 20.0f; // reject ground this far
// Mirrors INVALID_HEIGHT from GridTerrainData.h without taking a dependency on that header.
static constexpr float InvalidHeightSentinel = -99999.0f;
// Static ID -> category lookup for herb/ore classification. Every entry ID used
// across the 57-zone GoldRush.ZonePool (plus the module's own default global
// GoldRush.NodeEntries) was individually verified against
// acore_world.gameobject_template when the pool was built
// (docs/zone-specific-resources-spec.md). Guessing the category from the display
// name (the old keyword-list approach) broke for almost every zone: Outland/Classic
// ore uses "Deposit" rather than "Vein", and most herb names don't contain any of
// the four hardcoded herb keywords. true = herb, false = ore.
static const std::unordered_map<uint32, bool> IsHerbById = {
// Classic herbs
{ 1618, true }, { 3724, true }, { 1617, true }, { 3725, true }, { 1619, true }, { 3726, true }, // Peacebloom, Silverleaf, Earthroot
{ 1620, true }, { 3727, true }, { 1621, true }, { 3729, true }, { 2045, true }, { 1622, true }, { 3730, true }, // Mageroyal, Briarthorn, Stranglekelp, Bruiseweed
{ 1623, true }, { 1624, true }, { 2041, true }, { 2042, true }, { 2046, true }, { 2043, true }, { 2866, true }, // Wild Steelbloom, Kingsblood, Liferoot, Fadeleaf, Goldthorn, Khadgar's Whisker, Firebloom
{ 142140, true }, { 180165, true }, { 142141, true }, { 176642, true }, // Purple Lotus, Arthas' Tears
{ 142142, true }, { 176636, true }, { 180164, true }, { 142143, true }, { 183046, true }, // Sungrass, Blindweed
{ 142144, true }, { 142145, true }, { 176637, true }, { 176587, true }, { 176641, true }, // Ghost Mushroom, Gromsblood, Plaguebloom
{ 176583, true }, { 176638, true }, { 180167, true }, { 176584, true }, { 176639, true }, // Golden Sansam, Dreamfoil
{ 180168, true }, { 176586, true }, { 176640, true }, { 180166, true }, { 176588, true }, // Mountain Silversage, Icecap
{ 191303, true }, { 176589, true }, // Firethorn, Black Lotus
// Outland herbs
{ 181270, true }, { 183044, true }, { 181271, true }, { 183045, true }, { 181275, true }, // Felweed, Dreaming Glory, Ragveil
{ 183043, true }, { 181277, true }, { 181276, true }, { 181279, true }, { 181280, true }, { 181281, true }, // Terocone, Flame Cap, Netherbloom, Nightmare Vine, Mana Thistle
// Northrend herbs
{ 189973, true }, { 190171, true }, { 190172, true }, { 190176, true }, { 190170, true }, // Goldclover, Lichbloom, Icethorn, Frost Lotus, Talandra's Rose
{ 191019, true }, { 190169, true }, // Adder's Tongue, Tiger Lily
// Classic ore
{ 1731, false }, { 2055, false }, { 3763, false }, { 103713, false }, { 181248, false }, // Copper Vein
{ 1732, false }, { 2054, false }, { 3764, false }, { 103711, false }, { 181249, false }, // Tin Vein
{ 1733, false }, { 105569, false }, // Silver Vein
{ 1735, false }, // Iron Deposit
{ 1734, false }, { 150080, false }, { 181109, false }, // Gold Vein
{ 2040, false }, { 150079, false }, { 176645, false }, // Mithril Deposit
{ 2047, false }, { 150081, false }, { 181108, false }, // Truesilver Deposit
{ 324, false }, { 150082, false }, { 176643, false }, { 175404, false }, // Small/Rich Thorium Vein
{ 165658, false }, // Dark Iron Deposit
// Outland ore
{ 181555, false }, { 181556, false }, { 181569, false }, { 181570, false }, { 181557, false }, // Fel Iron, Adamantite (+ Rich), Khorium
// Northrend ore
{ 189978, false }, { 189979, false }, { 189980, false }, { 189981, false }, { 191133, false }, // Cobalt/Saronite Deposit (+ Rich), Titanium Vein
};
struct GoldRushSite
{
std::string ZoneLabel;
@ -154,6 +196,33 @@ static AreaTableEntry const* ResolveAreaEntry(std::string const& areaName)
return nullptr;
}
// The GoldRush.ZonePool value separates zones with "; " (semicolon + space) and
// the node/bonus entry IDs within a zone with a bare ";". Acore::Tokenize only
// splits on a single character, so splitting on ';' cut every zone's entry list
// down to its first ID -- the bug behind the wrong-zone/incomplete resource
// spawning reported after PR #7 (every zone's first entry is a herb, so ore
// never appeared and only one herb type spawned). Split on the two-character
// zone delimiter instead.
static std::vector<std::string_view> SplitZonePool(std::string_view zones)
{
std::vector<std::string_view> result;
size_t start = 0;
while (start < zones.size())
{
size_t end = zones.find("; ", start);
if (end == std::string_view::npos)
{
result.push_back(zones.substr(start));
break;
}
result.push_back(zones.substr(start, end - start));
start = end + 2; // skip the "; " delimiter
}
return result;
}
class GoldRushManager
{
public:
@ -487,7 +556,29 @@ private:
if (!IsEligibleAnchor(player))
return {};
return BuildSiteFromAnchor(player);
GoldRushSite site = BuildSiteFromAnchor(player);
// BuildSiteFromAnchor only knows the player's live zone/area IDs -- it has
// no access to the per-zone NodeEntries/BonusEntries parsed from
// GoldRush.ZonePool into _sites. SelectSiteForPlayer() tries a player
// anchor FIRST and only falls back to picking randomly from _sites when no
// eligible player is available, so without this lookup, the common case
// (a live player standing in a configured zone) silently lost its
// zone-specific resources and fell back to the global pool every time --
// exactly the bug reported after the v2 zone-resources feature shipped:
// events kept spawning the default Northrend-flavored global pool
// regardless of which zone they were actually anchored in.
for (GoldRushSite const& configured : _sites)
{
if (configured.ZoneId == site.ZoneId)
{
site.NodeEntries = configured.NodeEntries;
site.BonusEntries = configured.BonusEntries;
break;
}
}
return site;
}
std::vector<GoldRushSite> BuildSites(std::string const& zones) const
@ -504,7 +595,7 @@ private:
// area labels, change this parser to expect 4 segments and adjust
// BuildLocationText/FormatLocationForAnnouncement accordingly.
std::vector<GoldRushSite> sites;
for (std::string_view zoneToken : Acore::Tokenize(zones, ';', true))
for (std::string_view zoneToken : SplitZonePool(zones))
{
std::string token = Trim(std::string(zoneToken));
if (token.empty())
@ -635,9 +726,23 @@ private:
if (!goinfo)
continue;
std::string name = Normalize(goinfo->name);
bool isHerb = ContainsWord(name, "lotus") || ContainsWord(name, "clover") || ContainsWord(name, "thorn") || ContainsWord(name, "bloom");
bool isOre = ContainsWord(name, "vein");
// Prefer the verified ID -> category lookup table. The old name-keyword
// guess is kept only as a defensive fallback for entries not in the table
// (e.g. the original global-pool defaults or anything added by hand later
// without an explicit table update).
bool isHerb;
auto const it = IsHerbById.find(entry);
if (it != IsHerbById.end())
{
isHerb = it->second;
}
else
{
std::string name = Normalize(goinfo->name);
isHerb = ContainsWord(name, "lotus") || ContainsWord(name, "clover") || ContainsWord(name, "thorn") || ContainsWord(name, "bloom");
}
bool isOre = !isHerb;
if (herbs && isHerb)
result.push_back(entry);