Fix bot routing no-op, 1000x node lifetime, reload orphaning, floating spawns, and empty-server scheduling #1
3 changed files with 123 additions and 11 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue