From 83559df790b7d66435f20d3f8ff4ca2072be425a Mon Sep 17 00:00:00 2001 From: Scarecr0w12 Date: Wed, 1 Apr 2026 13:13:26 -0400 Subject: [PATCH] chore: initial standalone module import --- CHANGELOG.md | 24 + LICENSE | 338 +++++++++++ README.md | 88 ++- conf/gold_rush.conf.dist | 130 +++++ mod-gold-rush.cmake | 6 + src/GoldRush.cpp | 1149 ++++++++++++++++++++++++++++++++++++++ src/loader.cpp | 6 + 7 files changed, 1740 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 conf/gold_rush.conf.dist create mode 100644 mod-gold-rush.cmake create mode 100644 src/GoldRush.cpp create mode 100644 src/loader.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8188264 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to `mod-gold-rush` will be documented in this file. + +## [Unreleased] + +### Added +- Standalone repository documentation in `README.md`. +- Initial `CHANGELOG.md` for tracking module releases. + +### Changed +- Loosened Gold Rush hotspot selection so events can fall back to eligible + live-player zones when configured hotspots are unavailable. +- Improved GM messaging for zone eligibility failures. +- Allowed zone-based fallback when an exact configured sub-area anchor is not + available. + +### Fixed +- Fixed `GoldRush.NodeEntries` parsing so configured node IDs are actually + loaded from `gold_rush.conf`. +- Fixed startup behavior where Gold Rush could appear to have no valid node + entries even when configuration values were present. +- Fixed `teststart` and scheduler flows that were overly strict about allowed + zones. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1d74eb1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure that the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to your +programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it in +new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratuitously or for a fee, you must give the recipients all the rights +that you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three +years, to give any third party, for a charge no more than your +cost of physically performing source distribution, a complete +machine-readable copy of the corresponding source code, to be +distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer +to distribute corresponding source code. (This alternative is +allowed only for noncommercial distribution and only if you +received the program in object code or executable form with such +an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of this License, +you may choose any version ever published by the Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md index b311150..0646f0b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,88 @@ # mod-gold-rush -AzerothCore Gold Rush module for timed open-world resource hotspot events. + +`mod-gold-rush` is an AzerothCore module that runs a timed, server-wide +resource eruption event. When Gold Rush activates, the module selects an +eligible open-world zone, anchors on a live player, spawns temporary resource +nodes near the hotspot, announces the event to the server, and can route +playerbots into the area to create a PvPvE scramble. + +## Features + +- Schedules Gold Rush events on a random interval. +- Announces an active hotspot with approximate location messaging. +- Spawns temporary mixed ore and herb nodes around a live player anchor. +- Supports dynamic zone selection with blacklist-based restrictions. +- Exposes GM commands for status, force-start, test-start, stop, and reroute. +- Routes random playerbots toward the active hotspot with faction pressure. +- Writes diagnostics to a dedicated `gold-rush.log` logger/appender pair. +- Supports verbose event logging for anchor, spawn, and routing behavior. + +## Commands + +### GM commands + +- `.goldrush status` - show current event state and telemetry. +- `.goldrush start [zone or area]` - start Gold Rush in your current eligible + zone or in a configured hotspot by name. +- `.goldrush teststart` - force a hotspot at your current eligible location. +- `.goldrush stop` - end the active event and despawn temporary nodes. +- `.goldrush reroute` - refresh playerbot routing pressure. + +## Installation + +1. Place `mod-gold-rush` inside the AzerothCore `modules/` directory. +2. Re-run CMake so AzerothCore detects the module. +3. Build and install/update your server. +4. Copy `conf/gold_rush.conf.dist` to your generated module config directory as + `gold_rush.conf` if you want local overrides. +5. Restart `worldserver`. + +## Configuration + +The module ships its defaults in `conf/gold_rush.conf.dist`. + +Important options: + +- `GoldRush.Enable` +- `GoldRush.ZonePool` +- `GoldRush.Blacklist` +- `GoldRush.VerboseLogging` +- `GoldRush.MinIntervalMinutes` +- `GoldRush.MaxIntervalMinutes` +- `GoldRush.DurationMinutes` +- `GoldRush.MinNodes` +- `GoldRush.MaxNodes` +- `GoldRush.BotsPerFaction` +- `GoldRush.BotPulseSeconds` +- `GoldRush.SpawnRadiusYards` +- `GoldRush.NodeEntries` +- `GoldRush.StartMessage` +- `GoldRush.EndMessage` +- `GoldRush.Debug` +- `Appender.GoldRushLog` +- `Logger.module.goldrush` + +## Behavior notes + +- Event locations are announced approximately, not with exact coordinates. +- Hotspots are temporary and despawn at event end. +- Eligible anchors must be in open-world, non-city, non-sanctuary zones. +- Blacklisted zones and zone/area pairs are excluded from selection. +- Node selection alternates between ore-like and herb-like entries when + possible. +- If configured hotspots are unavailable, Gold Rush can fall back to eligible + live-player zones. + +## Repository contents + +- `src/` - module implementation and script loader registration. +- `conf/` - distributed module configuration. +- `mod-gold-rush.cmake` - AzerothCore build integration. + +## Status + +This module is actively being iterated on. Recent work includes: + +- looser open-world zone eligibility, +- corrected node entry parsing from config, +- improved test-start behavior and hotspot fallback handling. diff --git a/conf/gold_rush.conf.dist b/conf/gold_rush.conf.dist new file mode 100644 index 0000000..35cacbc --- /dev/null +++ b/conf/gold_rush.conf.dist @@ -0,0 +1,130 @@ +[worldserver] + +################################################################################################### +# GOLD RUSH LOGGING +################################################################################################### + +Appender.GoldRushLog = 2,5,0,gold-rush.log,w +Logger.module.goldrush = 5,GoldRushLog + +################################################################################################### +# GOLD RUSH MODULE +################################################################################################### +# +# GoldRush.Enable +# Description: Enable the Gold Rush world event scheduler +# Default: 1 (enabled) +# 0 (disabled) + +GoldRush.Enable = 1 + +# +# GoldRush.VerboseLogging +# Description: Emit detailed event, spawn, and bot routing logs +# Default: 0 (disabled) + +GoldRush.VerboseLogging = 0 + +# +# GoldRush.MinIntervalMinutes +# Description: Minimum number of minutes between Gold Rush events +# Default: 120 + +GoldRush.MinIntervalMinutes = 120 + +# +# GoldRush.MaxIntervalMinutes +# Description: Maximum number of minutes between Gold Rush events +# Default: 240 + +GoldRush.MaxIntervalMinutes = 240 + +# +# GoldRush.DurationMinutes +# Description: How long the event stays active once it starts +# Default: 20 + +GoldRush.DurationMinutes = 20 + +# +# GoldRush.MinNodes +# Description: Minimum number of temporary nodes to spawn in the hotspot +# Default: 15 + +GoldRush.MinNodes = 15 + +# +# GoldRush.MaxNodes +# Description: Maximum number of temporary nodes to spawn in the hotspot +# Default: 25 + +GoldRush.MaxNodes = 25 + +# +# GoldRush.BotsPerFaction +# Description: Number of random bots per faction to steer toward the event +# Default: 6 + +GoldRush.BotsPerFaction = 6 + +# +# GoldRush.BotPulseSeconds +# Description: How often the module checks whether bot pressure needs a refresh +# Default: 30 + +GoldRush.BotPulseSeconds = 30 + +# +# GoldRush.SpawnRadiusYards +# Description: Radius around the anchor player used for temporary spawns +# Default: 25.0 + +GoldRush.SpawnRadiusYards = 25.0 + +# +# GoldRush.NodeEntries +# Description: Semicolon-separated list of temporary gameobject template entries. +# Defaults to a mixed endgame bundle. +# Default: 191133;190176;190171;190172;189973 + +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|The River's Heart" + +GoldRush.ZonePool = "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|The River's Heart" + +# +# GoldRush.Blacklist +# Description: Semicolon-separated list of zones or zone+area pairs to exclude. +# Default: "Stormwind City; Orgrimmar; Dalaran" + +GoldRush.Blacklist = "Stormwind City; Orgrimmar; Dalaran" + +# +# GoldRush.StartMessage +# Description: World announcement template for the event start. +# Use {} once for the location text. +# Default: "A seismic anomaly has exposed a massive vein of rich minerals in {}!" + +GoldRush.StartMessage = "A seismic anomaly has exposed a massive vein of rich minerals in {}!" + +# +# GoldRush.EndMessage +# Description: World announcement template for the event end. +# Default: "The Gold Rush in {} has been exhausted." + +GoldRush.EndMessage = "The Gold Rush in {} has been exhausted." + +# +# GoldRush.Debug +# Description: Enable informational log output for the event scheduler +# Default: 1 (enabled) +# 0 (disabled) + +GoldRush.Debug = 1 + +################################################################################################### diff --git a/mod-gold-rush.cmake b/mod-gold-rush.cmake new file mode 100644 index 0000000..70414e8 --- /dev/null +++ b/mod-gold-rush.cmake @@ -0,0 +1,6 @@ +AC_ADD_SCRIPT("${CMAKE_CURRENT_LIST_DIR}/src/GoldRush.cpp") +AC_ADD_SCRIPT("${CMAKE_CURRENT_LIST_DIR}/src/loader.cpp") + +CU_SET_PATH("CMAKE_CONFIG_OUTPUT_DIR" "${CMAKE_CURRENT_LIST_DIR}/conf") + +CU_ADD_HOOK(AFTER_WORLDSERVER_CMAKE "${CMAKE_CURRENT_LIST_DIR}/conf/gold_rush.conf.dist") diff --git a/src/GoldRush.cpp b/src/GoldRush.cpp new file mode 100644 index 0000000..481bb75 --- /dev/null +++ b/src/GoldRush.cpp @@ -0,0 +1,1149 @@ +#include "Common.h" +#include "Chat.h" +#include "Config.h" +#include "Containers.h" +#include "DBCStores.h" +#include "GameObject.h" +#include "Log.h" +#include "Map.h" +#include "MapMgr.h" +#include "ObjectAccessor.h" +#include "Tokenize.h" +#include "PlayerbotAI.h" +#include "RandomPlayerbotMgr.h" +#include "Playerbots.h" +#include "ScriptMgr.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Acore::ChatCommands; + +namespace +{ +static char const* const GoldRushLogFilter = "module.goldrush"; + +struct GoldRushSite +{ + std::string ZoneLabel; + std::string AreaLabel; + uint32 ZoneId = 0; + uint32 AreaId = 0; + uint32 MapId = 0; +}; + +struct SpawnedNode +{ + ObjectGuid Guid; + uint32 Entry = 0; +}; + +struct GoldRushTelemetry +{ + uint32 EventsStarted = 0; + uint32 EventsCompleted = 0; + uint32 NodesSpawned = 0; + uint32 NodesRemoved = 0; + uint32 AllianceBotsRouted = 0; + uint32 HordeBotsRouted = 0; + uint32 LastZoneId = 0; + uint32 LastAreaId = 0; + std::string LastLocation; +}; + +class GoldRushConfig +{ +public: + bool Enabled = true; + bool VerboseLogging = false; + uint32 MinIntervalMs = 120 * MINUTE * IN_MILLISECONDS; + uint32 MaxIntervalMs = 240 * MINUTE * IN_MILLISECONDS; + uint32 DurationMs = 20 * MINUTE * IN_MILLISECONDS; + uint32 MinNodeCount = 15; + uint32 MaxNodeCount = 25; + uint32 BotsPerFaction = 6; + uint32 BotPulseMs = 30 * IN_MILLISECONDS; + float SpawnRadius = 25.0f; + std::string NodeEntries; + std::string ZonePool; + std::string Blacklist; + std::string StartMessage = "A seismic anomaly has exposed a massive vein of rich minerals in {}!"; + std::string EndMessage = "The Gold Rush in {} has been exhausted."; + bool Debug = true; +}; + +static std::string Trim(std::string value) +{ + auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) + return {}; + + auto last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); +} + +static std::string Normalize(std::string_view value) +{ + std::string normalized(value); + normalized = Trim(normalized); + + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) + { + return std::tolower(c); + }); + + return normalized; +} + +static bool ContainsWord(std::string const& text, std::string_view needle) +{ + return Normalize(text).find(Normalize(needle)) != std::string::npos; +} + +static AreaTableEntry const* ResolveAreaEntry(std::string const& areaName) +{ + if (areaName.empty()) + return nullptr; + + std::string needle = Normalize(areaName); + LocaleConstant locale = sWorld->GetDefaultDbcLocale(); + + for (uint32 i = 0; i < sAreaTableStore.GetNumRows(); ++i) + { + AreaTableEntry const* area = sAreaTableStore.LookupEntry(i); + if (!area) + continue; + + if (Normalize(area->area_name[locale]) == needle) + return area; + + for (uint32 localeIndex = 0; localeIndex < TOTAL_LOCALES; ++localeIndex) + { + if (localeIndex == locale) + continue; + + if (Normalize(area->area_name[localeIndex]) == needle) + return area; + } + } + + return nullptr; +} + +class GoldRushManager +{ +public: + void LoadConfig() + { + _config.Enabled = sConfigMgr->GetOption("GoldRush.Enable", true); + _config.VerboseLogging = sConfigMgr->GetOption("GoldRush.VerboseLogging", false); + _config.MinIntervalMs = sConfigMgr->GetOption("GoldRush.MinIntervalMinutes", 120) * MINUTE * IN_MILLISECONDS; + _config.MaxIntervalMs = sConfigMgr->GetOption("GoldRush.MaxIntervalMinutes", 240) * MINUTE * IN_MILLISECONDS; + _config.DurationMs = sConfigMgr->GetOption("GoldRush.DurationMinutes", 20) * MINUTE * IN_MILLISECONDS; + _config.MinNodeCount = sConfigMgr->GetOption("GoldRush.MinNodes", 15); + _config.MaxNodeCount = sConfigMgr->GetOption("GoldRush.MaxNodes", 25); + _config.BotsPerFaction = sConfigMgr->GetOption("GoldRush.BotsPerFaction", 6); + _config.BotPulseMs = sConfigMgr->GetOption("GoldRush.BotPulseSeconds", 30) * IN_MILLISECONDS; + _config.SpawnRadius = sConfigMgr->GetOption("GoldRush.SpawnRadiusYards", 25.0f); + _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|The River's Heart"); + _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."); + _config.Debug = sConfigMgr->GetOption("GoldRush.Debug", true); + + _nodeEntries = BuildNodeEntries(_config.NodeEntries); + _blockedZones = BuildBlockedZones(_config.Blacklist); + _sites.clear(); + _sitesInitialized = false; + _active = false; + _currentSite = {}; + _hotspot = {}; + _spawnedNodes.clear(); + _routedBotIds.clear(); + _telemetry = {}; + _timeUntilNextEventMs = 0; + _eventTimeRemainingMs = 0; + _nextBotPulseMs = _config.BotPulseMs; + + if (!_config.Enabled) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush module disabled by configuration."); + return; + } + + if (_nodeEntries.empty()) + { + LOG_WARN(GoldRushLogFilter, "Gold Rush loaded but no valid node template entries were configured."); + return; + } + + if (_config.VerboseLogging) + LOG_INFO(GoldRushLogFilter, "Gold Rush verbose logging enabled."); + } + + void Update(uint32 diff) + { + if (!_config.Enabled || _nodeEntries.empty()) + return; + + if (!EnsureSitesInitialized()) + return; + + if (_active) + { + if (_eventTimeRemainingMs <= diff) + { + EndEvent(); + return; + } + + _eventTimeRemainingMs -= diff; + + if (_nextBotPulseMs <= diff) + { + MaintainBotPressure(); + _nextBotPulseMs = _config.BotPulseMs; + } + else + { + _nextBotPulseMs -= diff; + } + + return; + } + + if (_timeUntilNextEventMs <= diff) + { + StartEvent(); + return; + } + + _timeUntilNextEventMs -= diff; + } + + bool ForceStart(Player* anchor, std::string const& locationFilter = {}) + { + if (!_config.Enabled || _nodeEntries.empty()) + return false; + + if (!EnsureSitesInitialized()) + return false; + + GoldRushSite site = locationFilter.empty() ? BuildEligibleSiteFromPlayer(anchor) : ResolveSiteByName(locationFilter); + + if (!site.ZoneId && anchor) + site = BuildEligibleSiteFromPlayer(anchor); + + if (!site.ZoneId || !anchor || !anchor->IsInWorld() || !anchor->GetMap()) + return false; + + return StartEvent(anchor, site); + } + + bool ForceTestStart(Player* anchor) + { + if (!_config.Enabled || _nodeEntries.empty()) + { + LOG_WARN(GoldRushLogFilter, "Gold Rush test start rejected: module disabled or no node templates configured."); + return false; + } + + if (!EnsureSitesInitialized()) + return false; + + if (!anchor || !anchor->IsInWorld() || !anchor->GetMap()) + anchor = FindAnyWorldPlayer(); + + if (!anchor || !anchor->IsInWorld() || !anchor->GetMap()) + { + LOG_WARN(GoldRushLogFilter, "Gold Rush test start rejected: no valid world player anchor was available."); + return false; + } + + GoldRushSite site = BuildEligibleSiteFromPlayer(anchor); + if (!site.ZoneId) + { + LOG_WARN(GoldRushLogFilter, "Gold Rush test start rejected: {} is not in an eligible world zone.", + anchor->GetName()); + return false; + } + + _currentSite = site; + _hotspot = WorldPosition(anchor->GetMapId(), anchor->GetPositionX(), anchor->GetPositionY(), anchor->GetPositionZ(), anchor->GetOrientation()); + + bool spawnedNodes = SpawnHotspot(anchor); + + _active = true; + _eventTimeRemainingMs = _config.DurationMs; + _nextBotPulseMs = 0; + ++_telemetry.EventsStarted; + _telemetry.LastZoneId = _currentSite.ZoneId; + _telemetry.LastAreaId = _currentSite.AreaId; + _telemetry.LastLocation = BuildLocationText(_currentSite); + + if (_config.VerboseLogging) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush test start anchored at map {} zone {} area {} by {}.", + _currentSite.MapId, _currentSite.ZoneId, _currentSite.AreaId, anchor->GetName()); + } + + ChatHandler(nullptr).SendWorldText(Acore::StringFormat(_config.StartMessage, FormatLocationForAnnouncement(_currentSite, anchor))); + + if (_config.Debug) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush test start triggered in {} for {} minute(s) with {} temporary node(s).", + _telemetry.LastLocation, + _config.DurationMs / (MINUTE * IN_MILLISECONDS), + _spawnedNodes.size()); + } + + if (!spawnedNodes && _config.VerboseLogging) + LOG_INFO(GoldRushLogFilter, "Gold Rush test start continued without spawned nodes; hotspot validation only."); + + RouteBots(); + MaintainBotPressure(); + return true; + } + + Player* FindAnyWorldPlayer() const + { + std::shared_lock lock(*HashMapHolder::GetLock()); + for (auto const& [guid, player] : ObjectAccessor::GetPlayers()) + { + if (player && player->IsInWorld() && player->GetMap()) + return player; + } + + return nullptr; + } + + bool ForceStop() + { + if (!_active) + return false; + + EndEvent(true); + return true; + } + + std::string GetStatusText() const + { + std::ostringstream out; + out << "Gold Rush: "; + out << (_active ? "ACTIVE" : "IDLE"); + if (_active) + { + out << " | Location: " << _telemetry.LastLocation; + out << " | Remaining: " << (_eventTimeRemainingMs / IN_MILLISECONDS) << "s"; + out << " | Nodes: " << _spawnedNodes.size(); + out << " | Routed A/H: " << _telemetry.AllianceBotsRouted << "/" << _telemetry.HordeBotsRouted; + } + out << " | Events started/completed: " << _telemetry.EventsStarted << "/" << _telemetry.EventsCompleted; + out << " | Nodes spawned/removed: " << _telemetry.NodesSpawned << "/" << _telemetry.NodesRemoved; + return out.str(); + } + + void RefreshBotPressure() + { + MaintainBotPressure(); + } + +private: + bool EnsureSitesInitialized() + { + if (_sitesInitialized) + return true; + + _sites = BuildSites(_config.ZonePool); + _sitesInitialized = true; + _timeUntilNextEventMs = RollIntervalMs(); + + if (_sites.empty()) + { + LOG_WARN(GoldRushLogFilter, + "Gold Rush loaded without configured hotspot zones; falling back to eligible live-player zones."); + return true; + } + + LOG_INFO(GoldRushLogFilter, "Gold Rush loaded with {} hotspot(s), {} node template(s), interval {}-{} minute(s), duration {} minute(s).", + _sites.size(), + _nodeEntries.size(), + _config.MinIntervalMs / (MINUTE * IN_MILLISECONDS), + _config.MaxIntervalMs / (MINUTE * IN_MILLISECONDS), + _config.DurationMs / (MINUTE * IN_MILLISECONDS)); + + return true; + } + + static std::unordered_set BuildBlockedZones(std::string const& blacklist) + { + std::unordered_set blocked; + for (std::string_view blockedZone : Acore::Tokenize(blacklist, ';', true)) + { + std::string normalized = Normalize(blockedZone); + if (!normalized.empty()) + blocked.insert(std::move(normalized)); + } + + return blocked; + } + + static bool IsValidZoneArea(AreaTableEntry const* zoneArea) + { + if (!zoneArea) + return false; + + if (zoneArea->IsSanctuary()) + return false; + + uint32 flags = zoneArea->flags; + return (flags & (AREA_FLAG_CAPITAL | AREA_FLAG_CITY | AREA_FLAG_SLAVE_CAPITAL)) == 0; + } + + bool IsBlockedSite(GoldRushSite const& site) const + { + std::string zoneKey = Normalize(site.ZoneLabel.empty() ? GetAreaLabelById(site.ZoneId) : site.ZoneLabel); + std::string areaKey = Normalize(site.AreaLabel); + std::string fullKey = areaKey.empty() ? zoneKey : Acore::StringFormat("{} near {}", zoneKey, areaKey); + + return _blockedZones.find(zoneKey) != _blockedZones.end() || + (!areaKey.empty() && _blockedZones.find(fullKey) != _blockedZones.end()); + } + + bool IsEligibleAnchor(Player const* player) const + { + if (!player || !player->IsInWorld() || !player->GetMap()) + return false; + + MapEntry const* mapEntry = player->GetMap()->GetEntry(); + if (!mapEntry || !mapEntry->IsWorldMap()) + return false; + + GoldRushSite site = BuildSiteFromAnchor(player); + if (!site.ZoneId) + return false; + + AreaTableEntry const* zoneArea = sAreaTableStore.LookupEntry(site.ZoneId); + if (!IsValidZoneArea(zoneArea)) + return false; + + return !IsBlockedSite(site); + } + + GoldRushSite BuildEligibleSiteFromPlayer(Player const* player) const + { + if (!IsEligibleAnchor(player)) + return {}; + + return BuildSiteFromAnchor(player); + } + + std::vector BuildSites(std::string const& zones) const + { + + std::vector sites; + for (std::string_view zoneToken : Acore::Tokenize(zones, ';', true)) + { + std::string token = Trim(std::string(zoneToken)); + if (token.empty()) + continue; + + std::vector parts = Acore::Tokenize(token, '|', true); + + GoldRushSite site; + site.ZoneLabel = parts.empty() ? token : Trim(std::string(parts[0])); + if (parts.size() > 1) + site.AreaLabel = Trim(std::string(parts[1])); + + if (site.ZoneLabel.empty()) + continue; + + AreaTableEntry const* zoneArea = ResolveAreaEntry(site.ZoneLabel); + AreaTableEntry const* areaEntry = site.AreaLabel.empty() ? zoneArea : ResolveAreaEntry(site.AreaLabel); + if (!zoneArea) + continue; + + AreaTableEntry const* rootZone = zoneArea->zone ? sAreaTableStore.LookupEntry(zoneArea->zone) : zoneArea; + if (!rootZone) + continue; + + if (!site.AreaLabel.empty() && !areaEntry) + continue; + + site.ZoneId = rootZone->ID; + site.AreaId = !site.AreaLabel.empty() && areaEntry ? areaEntry->ID : 0; + site.MapId = rootZone->mapid; + + if (!IsValidZoneArea(rootZone) || IsBlockedSite(site)) + continue; + + sites.push_back(std::move(site)); + } + + return sites; + } + + static std::vector BuildNodeEntries(std::string const& entries) + { + std::vector nodeEntries; + for (std::string_view token : Acore::Tokenize(entries, ';', true)) + { + std::string trimmed = Trim(std::string(token)); + if (trimmed.empty()) + continue; + + if (Optional entry = Acore::StringTo(trimmed); entry && *entry) + nodeEntries.push_back(*entry); + } + + return nodeEntries; + } + + void LogVerbose(std::string const& message) const + { + if (_config.VerboseLogging) + LOG_INFO(GoldRushLogFilter, "{}", message); + } + + static std::string BuildLocationText(GoldRushSite const& site) + { + if (site.AreaLabel.empty()) + return site.ZoneLabel; + + return Acore::StringFormat("{} near {}", site.ZoneLabel, site.AreaLabel); + } + + static std::string FormatLocationForAnnouncement(GoldRushSite const& site, Player const* anchor) + { + if (anchor && anchor->GetZoneId() == site.ZoneId) + { + if (!site.AreaLabel.empty()) + return Acore::StringFormat("{} near {}", site.ZoneLabel, site.AreaLabel); + + return site.ZoneLabel; + } + + return BuildLocationText(site); + } + + uint32 RollIntervalMs() const + { + uint32 minMs = std::min(_config.MinIntervalMs, _config.MaxIntervalMs); + uint32 maxMs = std::max(_config.MinIntervalMs, _config.MaxIntervalMs); + + if (maxMs <= minMs) + return minMs; + + return RandomValue(minMs, maxMs); + } + + static uint32 RandomValue(uint32 minValue, uint32 maxValue) + { + static std::mt19937 rng{ std::random_device{}() }; + std::uniform_int_distribution distribution(minValue, maxValue); + return distribution(rng); + } + + static float RandomFloat(float minValue, float maxValue) + { + static std::mt19937 rng{ std::random_device{}() }; + std::uniform_real_distribution distribution(minValue, maxValue); + return distribution(rng); + } + + static bool IsHotspotNodeEntry(uint32 entry) + { + GameObjectTemplate const* goinfo = sObjectMgr->GetGameObjectTemplate(entry); + if (!goinfo) + return false; + + std::string name = Normalize(goinfo->name); + return ContainsWord(name, "vein") || ContainsWord(name, "lotus") || + ContainsWord(name, "clover") || ContainsWord(name, "thorn") || + ContainsWord(name, "bloom"); + } + + static std::vector SplitByCategory(std::vector const& entries, bool herbs) + { + std::vector result; + for (uint32 entry : entries) + { + if (!entry) + continue; + + GameObjectTemplate const* goinfo = sObjectMgr->GetGameObjectTemplate(entry); + 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"); + + if (herbs && isHerb) + result.push_back(entry); + else if (!herbs && isOre) + result.push_back(entry); + } + + return result; + } + + GoldRushSite ResolveSiteByName(std::string const& name) const + { + std::string needle = Normalize(name); + for (GoldRushSite const& site : _sites) + { + if (Normalize(site.ZoneLabel) == needle || Normalize(site.AreaLabel) == needle || + Normalize(BuildLocationText(site)) == needle) + { + return site; + } + } + + return {}; + } + + static GoldRushSite BuildSiteFromAnchor(Player const* anchor) + { + GoldRushSite site; + if (!anchor) + return site; + + site.ZoneId = anchor->GetZoneId(); + site.AreaId = anchor->GetAreaId(); + site.MapId = anchor->GetMapId(); + site.ZoneLabel = GetAreaLabelById(site.ZoneId); + site.AreaLabel = site.AreaId ? GetAreaLabelById(site.AreaId) : std::string(); + return site; + } + + static std::string GetAreaLabelById(uint32 areaId) + { + if (!areaId) + return {}; + + LocaleConstant locale = sWorld->GetDefaultDbcLocale(); + if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(areaId)) + return area->area_name[locale]; + + return {}; + } + + GoldRushSite SelectSiteForPlayer(Player const* player) const + { + GoldRushSite playerSite = BuildEligibleSiteFromPlayer(player); + if (playerSite.ZoneId) + return playerSite; + + if (!_sites.empty()) + return Acore::Containers::SelectRandomContainerElement(_sites); + + if (Player* fallback = FindAnyEligibleWorldPlayer()) + return BuildEligibleSiteFromPlayer(fallback); + + return {}; + } + + Player* FindAnchorPlayer(GoldRushSite const& site) const + { + std::shared_lock lock(*HashMapHolder::GetLock()); + Player* zoneFallback = nullptr; + + for (auto const& [guid, player] : ObjectAccessor::GetPlayers()) + { + if (!IsEligibleAnchor(player)) + continue; + + if (site.MapId && player->GetMapId() != site.MapId) + continue; + + if (site.ZoneId && player->GetZoneId() != site.ZoneId) + continue; + + if (site.AreaId) + { + if (player->GetAreaId() == site.AreaId) + return player; + + if (!zoneFallback) + zoneFallback = player; + + continue; + } + + return player; + } + + return zoneFallback; + } + + Player* FindAnyEligibleWorldPlayer() const + { + std::shared_lock lock(*HashMapHolder::GetLock()); + std::vector candidates; + + for (auto const& [guid, player] : ObjectAccessor::GetPlayers()) + { + if (IsEligibleAnchor(player)) + candidates.push_back(player); + } + + if (candidates.empty()) + return nullptr; + + return Acore::Containers::SelectRandomContainerElement(candidates); + } + + bool StartEvent(Player* anchorOverride = nullptr, GoldRushSite siteOverride = {}) + { + if (_nodeEntries.empty()) + return false; + + if (!siteOverride.ZoneId) + siteOverride = anchorOverride ? BuildEligibleSiteFromPlayer(anchorOverride) : SelectSiteForPlayer(anchorOverride); + + if (!siteOverride.ZoneId) + siteOverride = SelectSiteForPlayer(anchorOverride); + + if (!siteOverride.ZoneId) + { + if (Player* dynamicAnchor = FindAnyEligibleWorldPlayer()) + { + anchorOverride = dynamicAnchor; + siteOverride = BuildEligibleSiteFromPlayer(dynamicAnchor); + } + } + + if (!siteOverride.ZoneId) + { + _timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush could not find any eligible world zone with a live player anchor; retrying in 5 minutes."); + return false; + } + + _currentSite = siteOverride; + + Player* anchor = anchorOverride && IsEligibleAnchor(anchorOverride) ? anchorOverride : FindAnchorPlayer(_currentSite); + if (!anchor || !anchor->GetMap()) + { + anchor = FindAnyEligibleWorldPlayer(); + if (!anchor || !anchor->GetMap()) + { + _timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not find an anchor player; retrying in 5 minutes.", BuildLocationText(_currentSite)); + return false; + } + } + + GoldRushSite anchorSite = BuildEligibleSiteFromPlayer(anchor); + if (!anchorSite.ZoneId) + { + _timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush rejected anchor {} because the zone is no longer eligible; retrying in 5 minutes.", + anchor->GetName()); + return false; + } + + _currentSite = anchorSite; + + _hotspot = WorldPosition(anchor->GetMapId(), anchor->GetPositionX(), anchor->GetPositionY(), anchor->GetPositionZ(), anchor->GetOrientation()); + + if (!SpawnHotspot(anchor)) + { + _timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not spawn nodes; retrying in 5 minutes.", BuildLocationText(_currentSite)); + return false; + } + + _active = true; + _eventTimeRemainingMs = _config.DurationMs; + _nextBotPulseMs = 0; + ++_telemetry.EventsStarted; + _telemetry.LastZoneId = _currentSite.ZoneId; + _telemetry.LastAreaId = _currentSite.AreaId; + _telemetry.LastLocation = BuildLocationText(_currentSite); + + if (_config.VerboseLogging) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush event anchored at map {} zone {} area {} by {}.", + _currentSite.MapId, _currentSite.ZoneId, _currentSite.AreaId, anchor->GetName()); + } + + std::string announcementLocation = FormatLocationForAnnouncement(_currentSite, anchor); + ChatHandler(nullptr).SendWorldText(Acore::StringFormat(_config.StartMessage, announcementLocation)); + + if (_config.Debug) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush started in {} for {} minute(s) with {} temporary node(s).", + _telemetry.LastLocation, + _config.DurationMs / (MINUTE * IN_MILLISECONDS), + _spawnedNodes.size()); + } + + RouteBots(); + MaintainBotPressure(); + return true; + } + + void EndEvent(bool forced = false) + { + if (!_active) + return; + + std::string location = BuildLocationText(_currentSite); + ChatHandler(nullptr).SendWorldText(Acore::StringFormat(_config.EndMessage, location)); + + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush ended in {}{}.", location, forced ? " (forced)" : ""); + + DespawnHotspotNodes(); + ClearBotRouting(); + + _active = false; + _currentSite = {}; + _hotspot = WorldPosition(); + _eventTimeRemainingMs = 0; + _timeUntilNextEventMs = RollIntervalMs(); + _nextBotPulseMs = _config.BotPulseMs; + ++_telemetry.EventsCompleted; + } + + bool SpawnHotspot(Player* anchor) + { + if (!anchor || !anchor->GetMap()) + return false; + + Map* map = anchor->GetMap(); + uint32 minNodes = std::min(_config.MinNodeCount, _config.MaxNodeCount); + uint32 maxNodes = std::max(_config.MinNodeCount, _config.MaxNodeCount); + 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; + 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()); + } + + constexpr float twoPi = 6.28318530717958647692f; + for (uint32 i = 0; i < nodeCount; ++i) + { + std::vector const* pool = nullptr; + if (useHerb && !herbEntries.empty()) + pool = &herbEntries; + else if (!useHerb && !oreEntries.empty()) + pool = &oreEntries; + else + pool = &fallbackEntries; + + useHerb = !useHerb; + + if (!pool || pool->empty()) + continue; + + uint32 entry = Acore::Containers::SelectRandomContainerElement(*pool); + float angle = RandomFloat(0.0f, twoPi); + float distance = RandomFloat(0.0f, _config.SpawnRadius); + float x = anchor->GetPositionX() + (std::cos(angle) * distance); + float y = anchor->GetPositionY() + (std::sin(angle) * distance); + float z = anchor->GetPositionZ(); + + if (GameObject* go = map->SummonGameObject(entry, x, y, z, anchor->GetOrientation(), 0.0f, 0.0f, 0.0f, 0.0f, _config.DurationMs, true)) + { + go->SetRespawnTime(_config.DurationMs); + go->SetSpawnedByDefault(false); + _spawnedNodes.push_back({ go->GetGUID(), entry }); + ++_telemetry.NodesSpawned; + + if (_config.VerboseLogging) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush node spawned: entry {} at ({:.2f}, {:.2f}, {:.2f}) on map {}.", + entry, x, y, z, map->GetId()); + } + } + } + + return !_spawnedNodes.empty(); + } + + void DespawnHotspotNodes() + { + for (SpawnedNode const& node : _spawnedNodes) + { + if (!node.Guid) + continue; + + if (Map* map = sMapMgr->FindMap(_currentSite.MapId, 0)) + { + if (GameObject* go = map->GetGameObject(node.Guid)) + { + go->DespawnOrUnsummon(); + ++_telemetry.NodesRemoved; + } + } + } + + _spawnedNodes.clear(); + } + + void RouteBots() + { + if (!_active) + return; + + std::vector candidates = sRandomPlayerbotMgr.GetPlayers(); + std::vector alliance; + std::vector horde; + + for (Player* bot : candidates) + { + if (!bot || !bot->IsInWorld() || bot->GetLevel() < 80 || !sRandomPlayerbotMgr.IsRandomBot(bot)) + continue; + + if (bot->GetTeamId() == TEAM_ALLIANCE) + alliance.push_back(bot); + else + horde.push_back(bot); + } + + RouteFactionBots(alliance, TEAM_ALLIANCE); + RouteFactionBots(horde, TEAM_HORDE); + + if (_config.VerboseLogging) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush bot routing scan complete: {} alliance candidate(s), {} horde candidate(s), {} routed bot(s) total.", + alliance.size(), horde.size(), _routedBotIds.size()); + } + } + + void RouteFactionBots(std::vector& bots, TeamId team) + { + if (bots.empty()) + return; + + std::shuffle(bots.begin(), bots.end(), std::mt19937{ std::random_device{}() }); + + uint32 targetCount = std::min(_config.BotsPerFaction, bots.size()); + uint32 routedCount = 0; + + for (Player* bot : bots) + { + if (routedCount >= targetCount) + break; + + if (!bot || _routedBotIds.find(bot->GetGUID().GetCounter()) != _routedBotIds.end()) + continue; + + if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot)) + { + WorldPosition pos(_hotspot.GetMapId(), _hotspot.GetPositionX(), _hotspot.GetPositionY(), _hotspot.GetPositionZ()); + botAI->rpgInfo.ChangeToGoGrind(pos); + botAI->ChangeStrategy("new rpg", BOT_STATE_NON_COMBAT); + botAI->ChangeStrategy("new rpg", BOT_STATE_COMBAT); + _routedBotIds.insert(bot->GetGUID().GetCounter()); + ++routedCount; + + if (team == TEAM_ALLIANCE) + ++_telemetry.AllianceBotsRouted; + else + ++_telemetry.HordeBotsRouted; + + if (_config.VerboseLogging) + { + LOG_INFO(GoldRushLogFilter, "Gold Rush routed {} bot {} toward {}.", + team == TEAM_ALLIANCE ? "Alliance" : "Horde", + bot->GetName(), + _telemetry.LastLocation); + } + } + } + } + + void MaintainBotPressure() + { + if (!_active) + return; + + uint32 allianceCount = 0; + uint32 hordeCount = 0; + + for (ObjectGuid::LowType guid : _routedBotIds) + { + Player* bot = ObjectAccessor::FindPlayer(ObjectGuid::Create(guid)); + if (!bot || !bot->IsInWorld()) + continue; + + if (bot->GetTeamId() == TEAM_ALLIANCE) + ++allianceCount; + else + ++hordeCount; + } + + if (allianceCount < _config.BotsPerFaction || hordeCount < _config.BotsPerFaction) + { + if (_config.Debug) + LOG_INFO(GoldRushLogFilter, "Gold Rush bot pressure low: alliance {}, horde {}. Recruiting reinforcements.", allianceCount, hordeCount); + + RouteBots(); + } + } + + void ClearBotRouting() + { + for (ObjectGuid::LowType guid : _routedBotIds) + { + Player* bot = ObjectAccessor::FindPlayer(ObjectGuid::Create(guid)); + if (!bot) + continue; + + if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot)) + botAI->rpgInfo.ChangeToIdle(); + + if (_config.VerboseLogging) + LOG_INFO(GoldRushLogFilter, "Gold Rush released routed bot {} back to idle.", bot->GetName()); + } + + _routedBotIds.clear(); + } + + std::vector const& GetSites() const { return _sites; } + + GoldRushConfig _config; + GoldRushTelemetry _telemetry; + std::unordered_set _blockedZones; + std::vector _sites; + std::vector _nodeEntries; + GoldRushSite _currentSite; + WorldPosition _hotspot; + std::vector _spawnedNodes; + std::unordered_set _routedBotIds; + uint32 _timeUntilNextEventMs = 0; + uint32 _eventTimeRemainingMs = 0; + uint32 _nextBotPulseMs = 0; + bool _sitesInitialized = false; + bool _active = false; +}; + +GoldRushManager g_GoldRushManager; + +class GoldRushWorldScript : public WorldScript +{ +public: + GoldRushWorldScript() : WorldScript("GoldRushWorldScript", { WORLDHOOK_ON_AFTER_CONFIG_LOAD, WORLDHOOK_ON_UPDATE }) { } + + void OnAfterConfigLoad(bool /*reload*/) override + { + g_GoldRushManager.LoadConfig(); + } + + void OnUpdate(uint32 diff) override + { + g_GoldRushManager.Update(diff); + } +}; + +class GoldRushCommandScript : public CommandScript +{ +public: + GoldRushCommandScript() : CommandScript("GoldRushCommandScript") { } + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable goldRushCommandTable = { + { "status", HandleStatusCommand, SEC_GAMEMASTER, Console::Yes }, + { "start", HandleStartCommand, SEC_GAMEMASTER, Console::No }, + { "teststart", HandleTestStartCommand, SEC_GAMEMASTER, Console::No }, + { "stop", HandleStopCommand, SEC_GAMEMASTER, Console::Yes }, + { "reroute", HandleRerouteCommand, SEC_GAMEMASTER, Console::No }, + }; + + static ChatCommandTable commandTable = { + { "goldrush", goldRushCommandTable }, + }; + + return commandTable; + } + + static bool HandleStatusCommand(ChatHandler* handler, char const* /*args*/) + { + handler->PSendSysMessage("{}", g_GoldRushManager.GetStatusText()); + return true; + } + + static bool HandleStartCommand(ChatHandler* handler, char const* args) + { + Player* player = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr; + std::string location = args ? Trim(args) : std::string(); + + if (g_GoldRushManager.ForceStart(player, location)) + { + handler->PSendSysMessage("Gold Rush started."); + return true; + } + + handler->PSendSysMessage("Gold Rush could not be started. Make sure you are in an eligible world zone or name a configured hotspot."); + return false; + } + + static bool HandleTestStartCommand(ChatHandler* handler, char const* /*args*/) + { + Player* player = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr; + + LOG_INFO(GoldRushLogFilter, "Gold Rush teststart command received from {}.", player ? player->GetName() : "console"); + + if (g_GoldRushManager.ForceTestStart(player)) + { + handler->PSendSysMessage("Gold Rush test start triggered at your current location."); + return true; + } + + LOG_WARN(GoldRushLogFilter, "Gold Rush teststart command failed for {}.", player ? player->GetName() : "console"); + handler->PSendSysMessage("Gold Rush test start could not be triggered. Make sure you are in an eligible world zone."); + return false; + } + + static bool HandleStopCommand(ChatHandler* handler, char const* /*args*/) + { + if (g_GoldRushManager.ForceStop()) + { + handler->PSendSysMessage("Gold Rush stopped."); + return true; + } + + handler->PSendSysMessage("Gold Rush is not active."); + return false; + } + + static bool HandleRerouteCommand(ChatHandler* handler, char const* /*args*/) + { + g_GoldRushManager.RefreshBotPressure(); + handler->PSendSysMessage("Gold Rush bot routing refreshed."); + return true; + } +}; +} // namespace + +void AddSC_mod_gold_rush() +{ + new GoldRushWorldScript(); + new GoldRushCommandScript(); +} diff --git a/src/loader.cpp b/src/loader.cpp new file mode 100644 index 0000000..4a206ad --- /dev/null +++ b/src/loader.cpp @@ -0,0 +1,6 @@ +void AddSC_mod_gold_rush(); + +void Addmod_gold_rushScripts() +{ + AddSC_mod_gold_rush(); +}