diff --git a/src/modules/PlayerbotV2/PlayerbotV2.cpp b/src/modules/PlayerbotV2/PlayerbotV2.cpp index 8c94f63207..d4db5f3012 100644 --- a/src/modules/PlayerbotV2/PlayerbotV2.cpp +++ b/src/modules/PlayerbotV2/PlayerbotV2.cpp @@ -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,12 +841,23 @@ void Module::Init() "[PlayerbotV2] AutoResumeOnBoot=true; will batch-login marked bots over multiple ticks (cap {}).", auto_resume_pending_); } - auto_spawn_pending_ = Services::Config().auto_spawn_on_boot(); - if (auto_spawn_pending_) + else { - TC_LOG_INFO("server.loading", - "[PlayerbotV2] AutoSpawnOnBoot={}; will create new bots on first world tick to reach that target (hard cap 200).", - auto_spawn_pending_); + auto_resume_pending_ = 0; + } + if (Services::Config().fleet_bots()) + { + auto_spawn_pending_ = Services::Config().auto_spawn_on_boot(); + if (auto_spawn_pending_) + { + TC_LOG_INFO("server.loading", + "[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()); @@ -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(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. - Services::Guilds().Tick(getMSTime()); + // + 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{}; diff --git a/src/modules/PlayerbotV2/README.md b/src/modules/PlayerbotV2/README.md index 9586619e03..5592ba590d 100644 --- a/src/modules/PlayerbotV2/README.md +++ b/src/modules/PlayerbotV2/README.md @@ -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 diff --git a/src/modules/PlayerbotV2/Services.cpp b/src/modules/PlayerbotV2/Services.cpp index ad4a740104..154b79e698 100644 --- a/src/modules/PlayerbotV2/Services.cpp +++ b/src/modules/PlayerbotV2/Services.cpp @@ -662,7 +662,12 @@ void Init() h->craft_orders->LoadFromDb(); h->ai_pool->start(); - h->fleet->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; } diff --git a/src/modules/PlayerbotV2/Util/ConfigReader.cpp b/src/modules/PlayerbotV2/Util/ConfigReader.cpp index 222e10b55f..ac5dc572b4 100644 --- a/src/modules/PlayerbotV2/Util/ConfigReader.cpp +++ b/src/modules/PlayerbotV2/Util/ConfigReader.cpp @@ -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 diff --git a/src/modules/PlayerbotV2/Util/ConfigReader.h b/src/modules/PlayerbotV2/Util/ConfigReader.h index a6bb9ee964..775aaebcbe 100644 --- a/src/modules/PlayerbotV2/Util/ConfigReader.h +++ b/src/modules/PlayerbotV2/Util/ConfigReader.h @@ -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; diff --git a/src/modules/PlayerbotV2/conf/playerbot.conf.dist b/src/modules/PlayerbotV2/conf/playerbot.conf.dist index 477dde16ae..8eaee5d7bd 100644 --- a/src/modules/PlayerbotV2/conf/playerbot.conf.dist +++ b/src/modules/PlayerbotV2/conf/playerbot.conf.dist @@ -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