diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c8298..f86e6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,43 @@ All notable changes to `mod-gold-rush` will be documented in this file. ## [Unreleased — hallsworth fork] +### Added +- Per-zone resource nodes. `GoldRush.ZonePool` entries can now carry their own + node and bonus pools (`ZoneName|NodeEntries|BonusEntries`) instead of every + hotspot drawing from one global list -- an event in Un'Goro spawns Un'Goro- + appropriate herbs/ore, Sholazar spawns Northrend resources, etc. `gold_rush.conf.dist` + ships with all ~57 valid WotLK 3.3.5a open-world zones pre-populated (see + `docs/zone-specific-resources-spec.md` for how each zone's resource list was + derived and verified). New `GoldRush.BonusChancePercent` (default 15) config + controls how often a spawn pulls from a zone's rarer bonus pool instead of its + normal one. A zone left without node/bonus data still falls back to the + 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 + `GetSession()->SendPacket()` unconditionally, so a null session was an + immediate null-pointer crash. Confirmed via a symbolized backtrace against a + captured coredump on the live server (two crashes within ~24h). Replaced with + `sWorldSessionMgr->SendServerMessage(SERVER_MSG_STRING, text)`, the session-free + broadcast path the built-in `.announce` GM command uses. - Bot routing is no longer a silent no-op. `Engine::ChangeStrategy` dispatches on the first character of the strategy name and ignores anything without a `+`/`-`/`~`/`?` prefix, so the bare `"new rpg"` calls did nothing. Now sends `"+new rpg"`, and only diff --git a/conf/gold_rush.conf.dist b/conf/gold_rush.conf.dist index a95e86e..864d6f3 100644 --- a/conf/gold_rush.conf.dist +++ b/conf/gold_rush.conf.dist @@ -109,6 +109,14 @@ GoldRush.BotPulseSeconds = 30 GoldRush.SpawnRadiusYards = 25.0 +# +# GoldRush.BonusChancePercent +# Description: Per-node chance (0-100) to use a zone's BonusEntries pool +# instead of the normal node pool. Set to 0 to disable bonus nodes. +# Default: 15 +# +GoldRush.BonusChancePercent = 15 + # # GoldRush.NodeEntries # Description: Semicolon-separated list of temporary gameobject template entries. @@ -119,11 +127,13 @@ GoldRush.NodeEntries = 191133;190176;190171;190172;189973 # # GoldRush.ZonePool -# Description: Semicolon-separated list of eligible zones. -# Use Zone|ApproximateSubArea for a better announcement. -# Default: "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|River's Heart" +# Description: Semicolon-separated list of eligible zones in the format +# ZoneName|NodeEntries|BonusEntries. Empty segments are allowed; +# a zone without entries falls back to GoldRush.NodeEntries. +# This file uses the v2 2-segment format from docs/zone-specific-resources-spec.md. +# Default: "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" -GoldRush.ZonePool = "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|River's Heart" +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" # # GoldRush.Blacklist diff --git a/docs/herb-ore-category-fix-spec.md b/docs/herb-ore-category-fix-spec.md new file mode 100644 index 0000000..8f44440 --- /dev/null +++ b/docs/herb-ore-category-fix-spec.md @@ -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 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. diff --git a/docs/herb-pool-scoping-regression.md b/docs/herb-pool-scoping-regression.md new file mode 100644 index 0000000..977d22c --- /dev/null +++ b/docs/herb-pool-scoping-regression.md @@ -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. diff --git a/docs/zone-specific-resources-spec.md b/docs/zone-specific-resources-spec.md new file mode 100644 index 0000000..18fa7c8 --- /dev/null +++ b/docs/zone-specific-resources-spec.md @@ -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. + +
+One zone per line (source of truth for future edits) + +``` +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 +``` + +
+ +--- + +## 3. Code mechanism (unchanged from v1 — already specced, just restating for completeness) + +- `GoldRushSite` struct (`GoldRush.cpp:44`) needs `std::vector + NodeEntries` and `std::vector 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). diff --git a/src/GoldRush.cpp b/src/GoldRush.cpp index 31ec8c2..e8837a0 100644 --- a/src/GoldRush.cpp +++ b/src/GoldRush.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -41,10 +42,53 @@ 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 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; std::string AreaLabel; + std::vector NodeEntries; + std::vector BonusEntries; uint32 ZoneId = 0; uint32 AreaId = 0; uint32 MapId = 0; @@ -85,6 +129,7 @@ public: uint32 GraceMs = 15 * MINUTE * IN_MILLISECONDS; uint32 MinPlayersOnline = 1; float SpawnRadius = 25.0f; + uint32 BonusChancePercent = 15; std::string NodeEntries; std::string ZonePool; std::string Blacklist; @@ -151,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 SplitZonePool(std::string_view zones) +{ + std::vector 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: @@ -181,8 +253,19 @@ public: _config.GraceMs = sConfigMgr->GetOption("GoldRush.GraceMinutes", 15) * MINUTE * IN_MILLISECONDS; _config.MinPlayersOnline = std::max(1, sConfigMgr->GetOption("GoldRush.MinPlayersOnline", 1)); _config.SpawnRadius = sConfigMgr->GetOption("GoldRush.SpawnRadiusYards", 25.0f); + _config.BonusChancePercent = std::min(100, sConfigMgr->GetOption("GoldRush.BonusChancePercent", 15)); _config.NodeEntries = sConfigMgr->GetOption("GoldRush.NodeEntries", "191133;190176;190171;190172;189973"); - _config.ZonePool = sConfigMgr->GetOption("GoldRush.ZonePool", "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|River's Heart"); + // No hardcoded fallback zone list here on purpose: the real default lives in + // gold_rush.conf.dist (the full 57-zone v2 pool from + // docs/zone-specific-resources-spec.md). That file uses the 2-segment + // ZoneName|NodeEntries|BonusEntries format; a hardcoded fallback string here + // previously still used the old ZoneName|AreaLabel format, which the current + // parser reads as NodeEntries and silently fails to parse as node IDs (safe -- + // BuildNodeEntries uses a non-throwing parse -- but pointless). If ZonePool is + // ever missing from the loaded config entirely, an empty pool here just means + // no configured hotspots, matching the "eligible live-player zones" fallback + // path already used elsewhere in this module rather than a stale example. + _config.ZonePool = sConfigMgr->GetOption("GoldRush.ZonePool", ""); _config.Blacklist = sConfigMgr->GetOption("GoldRush.Blacklist", "Stormwind City; Orgrimmar; Dalaran"); _config.StartMessage = sConfigMgr->GetOption("GoldRush.StartMessage", "A seismic anomaly has exposed a massive vein of rich minerals in {}!"); _config.EndMessage = sConfigMgr->GetOption("GoldRush.EndMessage", "The Gold Rush in {} has been exhausted."); @@ -473,14 +556,46 @@ 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 BuildSites(std::string const& zones) const { - + // CONFIG FORMAT CHOICE: this module uses the v2 "2-segment" ZonePool format + // described in docs/zone-specific-resources-spec.md: + // ZoneName|NodeEntries|BonusEntries + // The original v1 spec used a "4-segment" format: + // ZoneName|AreaLabel|NodeEntries|BonusEntries + // We deliberately do NOT include an AreaLabel segment because every zone + // name in the spec was verified against AzerothCore's AreaTable.dbc and can + // be resolved directly; a guessed subzone label could silently drop a zone + // from the pool if it fails to resolve. If a future spec re-introduces + // area labels, change this parser to expect 4 segments and adjust + // BuildLocationText/FormatLocationForAnnouncement accordingly. std::vector 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()) @@ -491,13 +606,14 @@ private: GoldRushSite site; site.ZoneLabel = parts.empty() ? token : Trim(std::string(parts[0])); if (parts.size() > 1) - site.AreaLabel = Trim(std::string(parts[1])); + site.NodeEntries = BuildNodeEntries(Trim(std::string(parts[1]))); + if (parts.size() > 2) + site.BonusEntries = BuildNodeEntries(Trim(std::string(parts[2]))); if (site.ZoneLabel.empty()) continue; AreaTableEntry const* zoneArea = ResolveAreaEntry(site.ZoneLabel); - AreaTableEntry const* areaEntry = site.AreaLabel.empty() ? zoneArea : ResolveAreaEntry(site.AreaLabel); if (!zoneArea) continue; @@ -505,11 +621,8 @@ private: if (!rootZone) continue; - if (!site.AreaLabel.empty() && !areaEntry) - continue; - site.ZoneId = rootZone->ID; - site.AreaId = !site.AreaLabel.empty() && areaEntry ? areaEntry->ID : 0; + site.AreaId = 0; // no area label in the 2-segment config format site.MapId = rootZone->mapid; if (!IsValidZoneArea(rootZone) || IsBlockedSite(site)) @@ -613,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); @@ -927,15 +1054,20 @@ private: uint32 nodeCount = RandomValue(minNodes, maxNodes); nodeCount = std::max(1, nodeCount); - std::vector oreEntries = SplitByCategory(_nodeEntries, false); - std::vector herbEntries = SplitByCategory(_nodeEntries, true); - std::vector fallbackEntries = _nodeEntries; + // Use zone-specific node pool when the current site has entries configured; + // otherwise fall back to the global GoldRush.NodeEntries list exactly as before. + std::vector siteNormalPool = _currentSite.NodeEntries.empty() ? _nodeEntries : _currentSite.NodeEntries; + std::vector siteBonusPool = _currentSite.BonusEntries; + + std::vector oreEntries = SplitByCategory(siteNormalPool, false); + std::vector herbEntries = SplitByCategory(siteNormalPool, true); + std::vector fallbackEntries = siteNormalPool; bool useHerb = true; if (_config.VerboseLogging) { - LOG_INFO(GoldRushLogFilter, "Gold Rush spawning {} node(s) using {} ore entry(s), {} herb entry(s), and {} fallback entry(s).", - nodeCount, oreEntries.size(), herbEntries.size(), fallbackEntries.size()); + LOG_INFO(GoldRushLogFilter, "Gold Rush spawning {} node(s) using {} ore entry(s), {} herb entry(s), {} fallback entry(s), and {} bonus entry(s).", + nodeCount, oreEntries.size(), herbEntries.size(), fallbackEntries.size(), siteBonusPool.size()); } // Map::SummonGameObject feeds this straight into GameObject::SetRespawnTime(int32), @@ -961,7 +1093,11 @@ private: if (!pool || pool->empty()) continue; - uint32 entry = Acore::Containers::SelectRandomContainerElement(*pool); + uint32 entry = 0; + if (!siteBonusPool.empty() && RandomValue(1u, 100u) <= _config.BonusChancePercent) + entry = Acore::Containers::SelectRandomContainerElement(siteBonusPool); + else + entry = Acore::Containers::SelectRandomContainerElement(*pool); // Try a handful of candidate spots; a node that cannot be placed on real ground // within line of sight of the anchor is skipped rather than left floating.