Fix bot routing no-op, 1000x node lifetime, reload orphaning, floating spawns, and empty-server scheduling #1

Merged
yrtria merged 2 commits from fix/review-round-1 into main 2026-09-03 11:01:12 -06:00
3 changed files with 123 additions and 11 deletions
Showing only changes of commit 2857b0580e - Show all commits

View file

@ -21,7 +21,17 @@ All notable changes to `mod-gold-rush` will be documented in this file.
ground height, rejects cliffs and lower floors, requires line of sight to the anchor, ground height, rejects cliffs and lower floors, requires line of sight to the anchor,
and retries a few times before giving up on a node. and retries a few times before giving up on a node.
### Added
- `GoldRush.RetryMinutes` (default 5), `GoldRush.GraceMinutes` (default 15) and
`GoldRush.MinPlayersOnline` (default 1).
### Changed ### Changed
- The scheduler no longer collapses to a fixed retry poll on an empty server. Previously
every failed attempt overwrote the rolled Min/MaxInterval with a hard-coded 5 minutes
and stayed there, so the first player to log in got an event announced on top of them
within one retry window. It now holds while the world is below `MinPlayersOnline`, and
waits `GraceMinutes` once the world repopulates before running. GM-forced starts
(`.goldrush start` / `teststart`) bypass both checks.
- Node scatter uses a square-root radius so points spread evenly across the disc - Node scatter uses a square-root radius so points spread evenly across the disc
instead of bunching near the anchor. instead of bunching near the anchor.
- `DespawnHotspotNodes` resolves the map once instead of per node, and warns if the - `DespawnHotspotNodes` resolves the map once instead of per node, and warns if the

View file

@ -46,6 +46,34 @@ GoldRush.MaxIntervalMinutes = 240
GoldRush.DurationMinutes = 20 GoldRush.DurationMinutes = 20
#
# GoldRush.RetryMinutes
# Description: How long to wait before trying again when a scheduled event cannot
# start (nobody online, no eligible zone, nowhere to place nodes).
# The rolled Min/MaxInterval is not consumed while holding.
# Default: 5
GoldRush.RetryMinutes = 5
#
# GoldRush.GraceMinutes
# Description: After the server has been sitting below MinPlayersOnline, wait this
# long once it repopulates before running an event. Stops the first
# person to log in from having a Gold Rush announced on top of them.
# Set to 0 to roll a fresh Min/MaxInterval instead.
# Default: 15
GoldRush.GraceMinutes = 15
#
# GoldRush.MinPlayersOnline
# Description: Minimum number of players standing in an event-eligible zone before
# a scheduled event may run. Playerbots count toward this. GM-forced
# starts ignore it.
# Default: 1
GoldRush.MinPlayersOnline = 1
# #
# GoldRush.MinNodes # GoldRush.MinNodes
# Description: Minimum number of temporary nodes to spawn in the hotspot # Description: Minimum number of temporary nodes to spawn in the hotspot

View file

@ -80,6 +80,9 @@ public:
uint32 MaxNodeCount = 25; uint32 MaxNodeCount = 25;
uint32 BotsPerFaction = 6; uint32 BotsPerFaction = 6;
uint32 BotPulseMs = 30 * IN_MILLISECONDS; uint32 BotPulseMs = 30 * IN_MILLISECONDS;
uint32 RetryMs = 5 * MINUTE * IN_MILLISECONDS;
uint32 GraceMs = 15 * MINUTE * IN_MILLISECONDS;
uint32 MinPlayersOnline = 1;
float SpawnRadius = 25.0f; float SpawnRadius = 25.0f;
std::string NodeEntries; std::string NodeEntries;
std::string ZonePool; std::string ZonePool;
@ -173,6 +176,9 @@ public:
_config.MaxNodeCount = sConfigMgr->GetOption<uint32>("GoldRush.MaxNodes", 25); _config.MaxNodeCount = sConfigMgr->GetOption<uint32>("GoldRush.MaxNodes", 25);
_config.BotsPerFaction = sConfigMgr->GetOption<uint32>("GoldRush.BotsPerFaction", 6); _config.BotsPerFaction = sConfigMgr->GetOption<uint32>("GoldRush.BotsPerFaction", 6);
_config.BotPulseMs = sConfigMgr->GetOption<uint32>("GoldRush.BotPulseSeconds", 30) * IN_MILLISECONDS; _config.BotPulseMs = sConfigMgr->GetOption<uint32>("GoldRush.BotPulseSeconds", 30) * IN_MILLISECONDS;
_config.RetryMs = std::max<uint32>(1, sConfigMgr->GetOption<uint32>("GoldRush.RetryMinutes", 5)) * MINUTE * IN_MILLISECONDS;
_config.GraceMs = sConfigMgr->GetOption<uint32>("GoldRush.GraceMinutes", 15) * MINUTE * IN_MILLISECONDS;
_config.MinPlayersOnline = std::max<uint32>(1, sConfigMgr->GetOption<uint32>("GoldRush.MinPlayersOnline", 1));
_config.SpawnRadius = sConfigMgr->GetOption<float>("GoldRush.SpawnRadiusYards", 25.0f); _config.SpawnRadius = sConfigMgr->GetOption<float>("GoldRush.SpawnRadiusYards", 25.0f);
_config.NodeEntries = sConfigMgr->GetOption<std::string>("GoldRush.NodeEntries", "191133;190176;190171;190172;189973"); _config.NodeEntries = sConfigMgr->GetOption<std::string>("GoldRush.NodeEntries", "191133;190176;190171;190172;189973");
_config.ZonePool = sConfigMgr->GetOption<std::string>("GoldRush.ZonePool", "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|The River's Heart"); _config.ZonePool = sConfigMgr->GetOption<std::string>("GoldRush.ZonePool", "Un'Goro Crater|Fire Plume Ridge; Winterspring|Frostfire Hot Springs; Eastern Plaguelands|Terrorweb Tunnel; Sholazar Basin|The River's Heart");
@ -191,6 +197,7 @@ public:
_spawnedNodes.clear(); _spawnedNodes.clear();
_routedBotIds.clear(); _routedBotIds.clear();
_telemetry = {}; _telemetry = {};
_worldWasEmpty = false;
_timeUntilNextEventMs = 0; _timeUntilNextEventMs = 0;
_eventTimeRemainingMs = 0; _eventTimeRemainingMs = 0;
_nextBotPulseMs = _config.BotPulseMs; _nextBotPulseMs = _config.BotPulseMs;
@ -267,7 +274,7 @@ public:
if (!site.ZoneId || !anchor || !anchor->IsInWorld() || !anchor->GetMap()) if (!site.ZoneId || !anchor || !anchor->IsInWorld() || !anchor->GetMap())
return false; return false;
return StartEvent(anchor, site); return StartEvent(anchor, site, false);
} }
bool ForceTestStart(Player* anchor) bool ForceTestStart(Player* anchor)
@ -724,11 +731,72 @@ private:
return Acore::Containers::SelectRandomContainerElement(candidates); return Acore::Containers::SelectRandomContainerElement(candidates);
} }
bool StartEvent(Player* anchorOverride = nullptr, GoldRushSite siteOverride = {}) // Counts players (bots included) standing somewhere an event could legally anchor.
// Only called when the schedule expires, never per world tick.
uint32 CountEligibleAnchors() const
{
uint32 count = 0;
std::shared_lock lock(*HashMapHolder<Player>::GetLock());
for (auto const& [guid, player] : ObjectAccessor::GetPlayers())
{
if (IsEligibleAnchor(player))
++count;
}
return count;
}
bool StartEvent(Player* anchorOverride = nullptr, GoldRushSite siteOverride = {}, bool scheduled = true)
{ {
if (_nodeEntries.empty()) if (_nodeEntries.empty())
return false; return false;
// On an empty (or near-empty) server the scheduler used to burn its rolled
// interval down to a flat retry and hold there, so the first person to log in
// got an event dropped on their head within one retry window. Hold the schedule
// while nobody is around, then give the world a grace period once it repopulates.
// GM-forced starts skip all of this.
if (scheduled)
{
uint32 const eligible = CountEligibleAnchors();
if (eligible < _config.MinPlayersOnline)
{
if (!_worldWasEmpty)
{
_worldWasEmpty = true;
if (_config.Debug)
{
LOG_INFO(GoldRushLogFilter, "Gold Rush is holding: {} eligible anchor(s) online, {} required. Rechecking every {} minute(s).",
eligible, _config.MinPlayersOnline, _config.RetryMs / (MINUTE * IN_MILLISECONDS));
}
}
else
{
LogVerbose("Gold Rush still holding; no eligible anchors online.");
}
_timeUntilNextEventMs = _config.RetryMs;
return false;
}
if (_worldWasEmpty)
{
_worldWasEmpty = false;
_timeUntilNextEventMs = _config.GraceMs ? _config.GraceMs : RollIntervalMs();
if (_config.Debug)
{
LOG_INFO(GoldRushLogFilter, "Gold Rush sees {} eligible anchor(s) after an idle period; waiting {} minute(s) before the next event.",
eligible, _timeUntilNextEventMs / (MINUTE * IN_MILLISECONDS));
}
return false;
}
}
if (!siteOverride.ZoneId) if (!siteOverride.ZoneId)
siteOverride = anchorOverride ? BuildEligibleSiteFromPlayer(anchorOverride) : SelectSiteForPlayer(anchorOverride); siteOverride = anchorOverride ? BuildEligibleSiteFromPlayer(anchorOverride) : SelectSiteForPlayer(anchorOverride);
@ -746,9 +814,11 @@ private:
if (!siteOverride.ZoneId) if (!siteOverride.ZoneId)
{ {
_timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; _worldWasEmpty = true;
_timeUntilNextEventMs = _config.RetryMs;
if (_config.Debug) if (_config.Debug)
LOG_INFO(GoldRushLogFilter, "Gold Rush could not find any eligible world zone with a live player anchor; retrying in 5 minutes."); LOG_INFO(GoldRushLogFilter, "Gold Rush could not find any eligible world zone with a live player anchor; retrying in {} minute(s).",
_config.RetryMs / (MINUTE * IN_MILLISECONDS));
return false; return false;
} }
@ -760,9 +830,11 @@ private:
anchor = FindAnyEligibleWorldPlayer(); anchor = FindAnyEligibleWorldPlayer();
if (!anchor || !anchor->GetMap()) if (!anchor || !anchor->GetMap())
{ {
_timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; _worldWasEmpty = true;
_timeUntilNextEventMs = _config.RetryMs;
if (_config.Debug) if (_config.Debug)
LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not find an anchor player; retrying in 5 minutes.", BuildLocationText(_currentSite)); LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not find an anchor player; retrying in {} minute(s).",
BuildLocationText(_currentSite), _config.RetryMs / (MINUTE * IN_MILLISECONDS));
return false; return false;
} }
} }
@ -770,10 +842,10 @@ private:
GoldRushSite anchorSite = BuildEligibleSiteFromPlayer(anchor); GoldRushSite anchorSite = BuildEligibleSiteFromPlayer(anchor);
if (!anchorSite.ZoneId) if (!anchorSite.ZoneId)
{ {
_timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; _timeUntilNextEventMs = _config.RetryMs;
if (_config.Debug) if (_config.Debug)
LOG_INFO(GoldRushLogFilter, "Gold Rush rejected anchor {} because the zone is no longer eligible; retrying in 5 minutes.", LOG_INFO(GoldRushLogFilter, "Gold Rush rejected anchor {} because the zone is no longer eligible; retrying in {} minute(s).",
anchor->GetName()); anchor->GetName(), _config.RetryMs / (MINUTE * IN_MILLISECONDS));
return false; return false;
} }
@ -783,9 +855,10 @@ private:
if (!SpawnHotspot(anchor)) if (!SpawnHotspot(anchor))
{ {
_timeUntilNextEventMs = 5 * MINUTE * IN_MILLISECONDS; _timeUntilNextEventMs = _config.RetryMs;
if (_config.Debug) if (_config.Debug)
LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not spawn nodes; retrying in 5 minutes.", BuildLocationText(_currentSite)); LOG_INFO(GoldRushLogFilter, "Gold Rush hotspot in {} could not spawn nodes; retrying in {} minute(s).",
BuildLocationText(_currentSite), _config.RetryMs / (MINUTE * IN_MILLISECONDS));
return false; return false;
} }
@ -1105,6 +1178,7 @@ private:
uint32 _timeUntilNextEventMs = 0; uint32 _timeUntilNextEventMs = 0;
uint32 _eventTimeRemainingMs = 0; uint32 _eventTimeRemainingMs = 0;
uint32 _nextBotPulseMs = 0; uint32 _nextBotPulseMs = 0;
bool _worldWasEmpty = false;
bool _sitesInitialized = false; bool _sitesInitialized = false;
bool _active = false; bool _active = false;
}; };