Similar to the VMAP fix, this caches failed MMAP load attempts using a
bitset per TerrainInfo instance. This prevents the server from repeatedly
attempting to load MMAP tiles that don't exist (e.g., unused dungeon maps,
boost experience maps, etc.), which was causing significant slowdowns.
Changes:
- Add _mmapLoadFailed bitset to TerrainInfo class
- Skip LoadMMapImpl early if grid already marked as failed
- Cache FileNotFound, VersionMismatch, ReadFromFileFailed, and
LibraryError results
- Change expected "FileNotFound" log level from WARN to DEBUG
- Update analysis documentation with fix status
Impact: Near-instant returns for grids with missing MMAP data instead of
repeated file I/O attempts.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
Problem: VMAP loading for maps without data (Boost Experience, phased zones,
newer dungeons) was retried on EVERY access, causing:
- Thousands of file access attempts per session
- Log spam with "Could not load VMAP" errors
- Server slowdown from repeated disk I/O
Solution: Added _vmapLoadFailed bitset to TerrainInfo that caches failed
VMAP load attempts, similar to existing _gridFileExists pattern for maps.
Changes:
- TerrainMgr.h: Added _vmapLoadFailed bitset
- TerrainMgr.cpp: Check bitset before load, cache failures, reduce log level
Maps affected: 1949, 1950 (8.0 Boost), 1554, 1557 (7.0 Boost), 1465 (Tanaan),
2648, 2649, 2662, 2669 (TWW dungeons), and others without VMAP data.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
PROBLEM: Extreme lag with 100+ bots caused by SpatialGridManager issues:
1. DATA RACE BUG in GetGrid():
- Writing lastAccessTime under shared_lock using const_cast
- Multiple threads writing same memory location concurrently = undefined behavior
- Could cause crashes, memory corruption, or silent data corruption
2. PERFORMANCE ISSUE in GetGrid():
- Called thousands of times per second (100+ bots × multiple calls per update)
- Each call: acquire shared_lock + chrono::steady_clock::now()
- Unnecessary overhead for a keep-alive timestamp that's rarely checked
3. Same issues in UpdateGrid() and TouchGrid()
SOLUTION:
1. GetGrid(): Remove lastAccessTime update entirely
- This method is for READ-ONLY grid access
- lastAccessTime is only used for cleanup of inactive grids
- No need to update on every access
2. UpdateGrid(): Split into 3 phases
- Phase 1: Get grid pointer under shared_lock (fast)
- Phase 2: Update grid WITHOUT holding manager lock
- Phase 3: Update lastAccessTime under exclusive lock
3. TouchGrid(): Use unique_lock (exclusive) for write operation
- This method explicitly updates lastAccessTime
- Must use exclusive lock for write operations
IMPACT:
- Eliminates ~1000+ chrono::now() calls per second
- Fixes potential memory corruption from data race
- Reduces lock contention on SpatialGridManager
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
PROBLEM 1: "Pool bot failed to login via BotSpawner"
- Bots in maintenance state after warmup failure
PROBLEM 2: "No account ID for bot (not in slot or CharacterCache)"
- Bots with account_id = 0 in playerbot_instance_pool table
- CharacterCache doesn't have the account association
ROOT CAUSE:
The ON DUPLICATE KEY UPDATE clause in SyncToDatabase() did NOT include
`account_id`, so:
1. Bot initially saved with account_id = 0 (bug in some code path)
2. WarmUpBot corrects it from CharacterCache and updates the slot
3. SyncToDatabase INSERTs the correct account_id, BUT
4. ON DUPLICATE KEY UPDATE doesn't update account_id column
5. On restart, LoadFromDatabase loads account_id = 0 again
SOLUTION:
1. Add `account_id = VALUES(account_id)` to ON DUPLICATE KEY UPDATE
- Now account_id corrections are persisted properly
2. Add account_id repair in LoadFromDatabase()
- If account_id = 0, try to get it from CharacterCache
- Log warning if CharacterCache also has no account
This ensures bots with previously corrupted account_id values
will be repaired on next load and the fix will be persisted.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
PROBLEM: QuestCompletion used std::mutex for read-heavy data structures
which blocked ALL readers when ANY read was happening. With 100+ bots
calling quest-related functions from ThreadPool workers, this caused
severe lock contention and task delays.
AFFECTED MUTEXES (all read-heavy patterns):
- _pausedBotsMutex: Checked on EVERY quest event (HandleQuestEvent)
- _questOrderMutex: Read frequently during quest prioritization
- _objectiveOrderMutex: Read during objective sequencing
SOLUTION:
1. Changed _pausedBotsMutex from std::mutex to std::shared_mutex
- Read operations (find) use std::shared_lock (concurrent readers OK)
- Write operations (insert/erase) use std::unique_lock (exclusive)
2. Changed _questOrderMutex from std::mutex to std::shared_mutex
- All operations use std::unique_lock (mostly writes in current code)
- Future optimization: use shared_lock for read-only accesses
3. Changed _objectiveOrderMutex from std::mutex to std::shared_mutex
- All operations use std::unique_lock (writes only currently)
This eliminates a major ThreadPool bottleneck where every bot's
quest event processing was blocking on mutex acquisition.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
PROBLEM: HumanizationConfig used std::mutex which blocked ALL readers
when ANY read was happening. With 100+ concurrent bots calling
GetActivityConfig() and GetHourlyActivityMultiplier() from ThreadPool
workers, this caused severe lock contention and task delays.
SOLUTION:
1. Changed std::mutex to std::shared_mutex for read-heavy access
2. Use std::unique_lock only during Load()/Reload() (rare operation)
3. Use std::shared_lock for GetActivityConfig() (multiple readers OK)
4. Made GetHourlyActivityMultiplier() lock-free since _hourlyMultipliers
is set once during Load() and never modified at runtime
5. Changed C-array to std::array<float, 24> for better type safety
This eliminates a major ThreadPool bottleneck where every bot's
HumanizationManager::Update() was blocking on mutex acquisition.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
Root cause: ThreadPool::WaitForCompletion could block world thread for
35+ seconds (5s + 30s), leaving insufficient buffer before FreezeDetector
triggers at 60 seconds. Combined with other World::Update operations,
total time exceeded 60s causing forced crash.
Changes:
- BotWorldSessionMgr: Reduce wait timeouts from 5s+30s to 2s+8s (10s max)
- ThreadPool::WaitForCompletion: Add hard cap of 15 seconds regardless
of caller-specified timeout
- ThreadPool.h: Change default timeout from milliseconds::max() to 10s
This ensures world thread never blocks more than 15 seconds in
WaitForCompletion, leaving 45+ seconds buffer for FreezeDetector.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
TrinityCore 12.0 API changes applied:
InnkeeperInteractionManager.cpp:
- REST API: Use RestMgr for rest flag management instead of Player methods
- SetRestFlag/HasRestFlag now via player->GetRestMgr()
- SetRestState takes (RestTypes, PlayerRestState) not old REST_TYPE_IN_TAVERN
- GetRestBonus via RestMgr instead of GetRestState()
- Homebind: Use player->m_homebind directly (GetHomebind() removed)
- AreaFlags: Use LinkedChat instead of Capital for city detection
- Added DB2Stores.h and RestMgr.h includes
InnkeeperInteractionManager.h:
- Fixed CalculateDistanceToQuestZones signature to include mapId parameter
BotMovementController.cpp:
- Fixed include paths for BotMovement subdirectory structure
Co-Authored-By: Claude Opus 4.5 <[email protected]>