Gate ambient fleet behind PlayerbotsV2.FleetBots (default off: alt-bot-only mode)

- ConfigReader: add PlayerbotsV2.FleetBots switch (default 0) + fleet_bots() accessor
- Module::Init/OnWorldUpdate: skip population shaper, bot guilds, craft-order
  board, BG/LFG queue auto-fill, and AutoResume/AutoSpawn when fleet disabled
- Services: FleetThread is not started in alt-bot-only mode
- conf/playerbot.conf.dist + README document the new key
This commit is contained in:
devbox
2026-08-12 11:17:54 +10:00
parent b853b3ae06
commit 9ed80ca65a
6 changed files with 98 additions and 15 deletions
+58 -8
View File
@@ -811,7 +811,23 @@ void Module::Init()
initialized_ = true;
if (Services::Config().auto_resume_on_boot())
if (!Services::Config().fleet_bots())
{
// PlayerbotsV2.FleetBots=0 (DEFAULT) — alt-bot-only mode. The
// ambient fleet does not run: no auto-resume / auto-spawn of
// population bots at boot, and the fleet subsystems (population
// shaper, guilds, craft-order board, BG/LFG auto-fill) are all
// skipped in OnWorldUpdate. The only bots that enter the world
// are those a player explicitly brings up via the alt creation
// path (.playerbot summon / login / alt ...).
auto_resume_pending_ = 0;
auto_spawn_pending_ = 0;
TC_LOG_INFO("server.loading",
"[PlayerbotV2] PlayerbotsV2.FleetBots=0: ambient fleet disabled — "
"alt-bot creation path only (summon/login/alt). "
"AutoResumeOnBoot/AutoSpawnOnBoot ignored.");
}
else if (Services::Config().auto_resume_on_boot())
{
// B-2: clamp the boot fill to Population.TotalTarget. AutoResumeCap
// is an independent knob (default 100), so a 10-bot target still
@@ -825,6 +841,12 @@ void Module::Init()
"[PlayerbotV2] AutoResumeOnBoot=true; will batch-login marked bots over multiple ticks (cap {}).",
auto_resume_pending_);
}
else
{
auto_resume_pending_ = 0;
}
if (Services::Config().fleet_bots())
{
auto_spawn_pending_ = Services::Config().auto_spawn_on_boot();
if (auto_spawn_pending_)
{
@@ -832,6 +854,11 @@ void Module::Init()
"[PlayerbotV2] AutoSpawnOnBoot={}; will create new bots on first world tick to reach that target (hard cap 200).",
auto_spawn_pending_);
}
}
else
{
auto_spawn_pending_ = 0;
}
// #1B: apply the configured wedge-confirm thresholds + displacement gate.
wedge_watchdog_.set_threshold_ms(Services::Config().wedge_watchdog_threshold_ms());
wedge_watchdog_.set_combat_threshold_ms(Services::Config().wedge_watchdog_combat_threshold_ms());
@@ -1097,7 +1124,7 @@ void Module::OnWorldUpdate(std::chrono::milliseconds diff)
// Spreading the resume kBootSpawnPerTick at a time keeps the bnet
// write rate well below the row-lock contention threshold.
constexpr uint32 kBootResumePerTick = 5;
if (auto_resume_pending_ > 0 && boot_gate_open_)
if (auto_resume_pending_ > 0 && boot_gate_open_ && Services::Config().fleet_bots())
{
const uint32 batch = std::min(auto_resume_pending_, kBootResumePerTick);
// LoginAll's cap is "stop when active_count >= cap". Active count
@@ -1132,7 +1159,7 @@ void Module::OnWorldUpdate(std::chrono::milliseconds diff)
// Hard-capped at AUTO_SPAWN_HARD_CAP overall.
constexpr uint32 AUTO_SPAWN_HARD_CAP = 200;
constexpr uint32 kBootSpawnPerTick = 5;
if (auto_spawn_pending_ && boot_gate_open_)
if (auto_spawn_pending_ && boot_gate_open_ && Services::Config().fleet_bots())
{
const uint32 currentMarked = uint32(Services::Lifecycle().size());
const uint32 target_total = std::min(
@@ -1969,7 +1996,11 @@ void Module::OnWorldUpdate(std::chrono::milliseconds diff)
// 1.5. Stagger-fire any deferred BG ports + LFG proposal accepts whose
// delay has elapsed (see OnBGInvitationReceived /
// OnLfgProposalReceived). MUST run before DrainIntents so the
// freshly-pushed intents fire this same tick.
// freshly-pushed intents fire this same tick. Fleet-only: the
// BG/LFG queue machinery recruits population bots, so in
// alt-bot-only mode (PlayerbotsV2.FleetBots=0) the whole block
// is skipped.
if (Services::Config().fleet_bots())
{
const uint32 t0 = getMSTime();
FireDueBgPorts();
@@ -1998,7 +2029,11 @@ void Module::OnWorldUpdate(std::chrono::milliseconds diff)
// 2.6. Population shaper tick (Phase A of WORLD_POPULATION_PLAN).
// Internally rate-limits to its tick interval (default 60s) so
// this is cheap to call every world frame.
// this is cheap to call every world frame. Fleet-only — the
// shaper owns the ambient world population (JIT spawn / login /
// logout / BG+arena seeding), so it is fully disabled in
// alt-bot-only mode (PlayerbotsV2.FleetBots=0).
if (Services::Config().fleet_bots())
{
const uint32 t0 = getMSTime();
Services::Population().OnWorldTick(static_cast<uint32>(total.count()));
@@ -2008,19 +2043,23 @@ void Module::OnWorldUpdate(std::chrono::milliseconds diff)
// 2b. Bot guild manager tick (GUILD_PLAN.md Phase A.2). Internally
// 60s rate-limited; cheap to call every frame. Elects founders
// when guild slots are open + sweeps stale name reservations
// + aborts founders that exceeded the 30-min FSM budget.
// + aborts founders that exceeded the 30-min FSM budget. This is
// the bot-guild ecosystem — a fleet feature — so it is skipped in
// alt-bot-only mode.
if (Services::Config().fleet_bots())
Services::Guilds().Tick(getMSTime());
// 2c. Craft-order board tick (#4B-2). Ages out stale Claimed orders
// (timeout -> Fail + refund so escrow can't be trapped behind a stuck
// crafter) and prunes finished rows. Self-rate-limited cheaply via a
// local accumulator — the board only needs minute-granularity since
// the claim timeout is 30 min.
// the claim timeout is 30 min. Fleet bot-to-bot economy — skipped in
// alt-bot-only mode.
craft_board_total_ += diff;
if (craft_board_total_ - last_craft_board_at_ >= kCraftBoardIntervalMs)
{
last_craft_board_at_ = craft_board_total_;
if (Services::Initialized())
if (Services::Initialized() && Services::Config().fleet_bots())
Services::CraftOrders().Tick(GameTime::GetGameTimeMS());
}
@@ -3779,6 +3818,11 @@ void Module::OnPartyChat(Player* sender, Group* group, std::string const& msg)
void Module::OnPlayerJoinedBgQueue(Player* player, uint32 bg_type_id, uint8 bracket)
{
if (!initialized_ || !player) return;
// Fleet-only: BG queue filling recruits population bots. In alt-bot-only
// mode (PlayerbotsV2.FleetBots=0) there is no ambient fleet to fill a
// queue with, so the whole fill is skipped.
if (!Services::Config().fleet_bots())
return;
// SOLO bots ignore (they're queued by Filler intent emit, not by core
// path — re-firing the fill for each would recurse). A bot GROUP LEADER
// is the exception (audit B25): a premade (guild BG-night /
@@ -3864,6 +3908,12 @@ void Module::OnPlayerJoinedLfg(Player* player, uint32 dungeon_id, uint8 /*role_m
{
if (!initialized_ || !player) return;
if (player->GetSession() && player->GetSession()->IsBot()) return;
// Fleet-only: LFG fill drafts population bots (and can JIT-spawn new ones).
// In alt-bot-only mode (PlayerbotsV2.FleetBots=0) there is no ambient
// fleet, so bots for a player's queue must come from their own alts —
// skip the auto-fill entirely.
if (!Services::Config().fleet_bots())
return;
Fleet::BotQueueFiller filler;
Fleet::BotQueueFiller::FillRequest req{};
+1
View File
@@ -195,6 +195,7 @@ The `playerbot_v2_altbot` table is auto-created on first `BindAlt()` call via
Configuration lives in `playerbot.conf` (same directory as `worldserver.conf`).
Key settings:
- `PlayerbotsV2.FleetBots` — ambient-fleet master switch (default 0 = alt-bot-only mode; only the alt creation path runs)
- `Playerbot.V2.Population.TotalTarget` — fleet bot population target
- `Playerbot.V2.Population.Shape` — distribution shape (Pyramid, Bell, Flat, MaxHeavy)
- `Playerbot.V2.MaxAltsAsBots` — per-account altbot cap
+5
View File
@@ -662,6 +662,11 @@ void Init()
h->craft_orders->LoadFromDb();
h->ai_pool->start();
// FleetThread is the fleet-layer admin worker. In alt-bot-only mode
// (PlayerbotsV2.FleetBots=0) the ambient fleet is disabled, so the
// thread is never started — nothing posts to it and the worker just
// sits idle waiting on its CV.
if (h->config->fleet_bots())
h->fleet->start();
g_holder = h;
@@ -45,6 +45,7 @@ void ConfigReader::apply_from_loaded_config()
ai_worker_threads_ = c.GetIntDefault ("Playerbot.AiWorkerThreads", 0);
fleet_thread_enabled_ = c.GetBoolDefault("Playerbot.FleetThreadEnabled", true);
fleet_bots_enabled_ = c.GetBoolDefault("PlayerbotsV2.FleetBots", false);
tick_budget_ms_ = c.GetIntDefault ("Playerbot.TickBudgetMs", 10);
// #5 Phase 4 parallel snapshot Build. DEFAULT ON. The 2026-06-15 crash
// (ACCESS_VIOLATION in Build->objective_blacklisted, serial AND parallel) was
@@ -23,6 +23,19 @@ public:
bool fleet_thread_enabled() const { return fleet_thread_enabled_; }
uint32 tick_budget_ms() const { return tick_budget_ms_; }
// Ambient fleet master switch (PlayerbotsV2.FleetBots). When false
// (DEFAULT) the module runs in alt-bot-only mode: NO population-managed
// bots exist out in the world. The BotPopulationManager shaper,
// bot guild ecosystem, bot-to-bot craft-order board, and BG/LFG queue
// auto-fill are all disabled, and the fleet-entrenching GM commands
// (.playerbot spawn/create/loginall/population/wipefleet/pipeline/
// smoketest/dungeontest/reseed) refuse to run. The only bots that enter
// the world are the ones a player explicitly brings up via the alt
// creation path (.playerbot summon / login / alt ...). Set to 1 to
// re-enable the full ambient fleet. Read once at Init; toggling on a
// live server requires a restart.
bool fleet_bots() const { return fleet_bots_enabled_; }
// #5 Phase 4: parallelize BotSnapshotBuilder::Build per Map* across a
// fixed worker pool during the world-thread snapshot window. When false
// the snapshot loop runs exactly as before (serial, world thread). Kept
@@ -282,6 +295,7 @@ private:
// Cached typed values.
uint32 ai_worker_threads_ = 0;
bool fleet_thread_enabled_ = true;
bool fleet_bots_enabled_ = false;
uint32 tick_budget_ms_ = 10;
bool parallel_snapshot_build_ = true;
uint32 snapshot_build_threads_ = 0;
@@ -38,6 +38,18 @@ Playerbot.AiWorkerThreads = 0
# HR-no
Playerbot.FleetThreadEnabled = 1
# Ambient-fleet master switch. When 0 (DEFAULT) the module runs in alt-bot-only
# mode: NO population-managed fleet bots exist out in the world. The population
# shaper, bot guild ecosystem, bot-to-bot craft-order board, and BG/LFG queue
# auto-fill are disabled, and the fleet-entrenching GM commands
# (.playerbot spawn/create/loginall/population/wipefleet/pipeline/smoketest/
# dungeontest/reseed) refuse to run. The only bots that enter the world are the
# ones a player explicitly brings up via the alt creation path
# (.playerbot summon / login / alt ...). Set to 1 to re-enable the full
# ambient fleet. Read once at startup — toggling requires a restart.
# HR-no
PlayerbotsV2.FleetBots = 0
# World tick budget for V2, in milliseconds. Bots skip ticks if exceeded.
# HR-yes
Playerbot.TickBudgetMs = 10