feat(playerbot): Add SQL schema and migration files

Database schemas for playerbot module:
- 01_playerbot_structure.sql - Core playerbot tables
- 01_index_optimization.sql - Performance indexes
- 02_playerbot_names.sql - Bot name generation
- 02_query_optimization.sql - Query performance tuning
- 03_mysql_configuration.cnf - Recommended MySQL settings
- 04_performance_validation.sql - Performance monitoring queries
- 05_auction_price_history.sql - Auction house bot support
- 06_instance_bot_pool.sql - Instance bot warm pool
- 07_bot_templates.sql - Bot configuration templates
- 08_jit_bot_tracking.sql - JIT bot creation tracking
- 09_warm_pool_persistence.sql - Warm pool state persistence

Fixes:
- 01_fix_binding_shot_crash.sql - Hunter ability crash fix
- add_template_validation_constraints.sql - Data integrity
- fix_corrupt_template_entry.sql - Template repair

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
This commit is contained in:
agatho
2026-01-22 19:56:54 -03:00
committed by luis
co-authored by Claude Opus 4.5
parent a97a7c218d
commit cd6d4c064f
14 changed files with 4207 additions and 0 deletions
@@ -0,0 +1,426 @@
-- =====================================================
-- TRINITYCORE PLAYERBOT DATABASE OPTIMIZATION
-- Target: 5000+ concurrent bots, <100ms login, <10ms queries
-- MySQL 9.4 Optimizations with Partitioning
-- =====================================================
-- Switch to characters database
USE `characters`;
-- =====================================================
-- PHASE 1: ANALYZE EXISTING TABLES
-- =====================================================
-- Analyze table statistics for optimizer
ANALYZE TABLE `characters`;
ANALYZE TABLE `character_stats`;
ANALYZE TABLE `character_spell`;
ANALYZE TABLE `character_action`;
ANALYZE TABLE `character_inventory`;
ANALYZE TABLE `character_aura`;
ANALYZE TABLE `character_queststatus`;
ANALYZE TABLE `character_reputation`;
ANALYZE TABLE `character_skills`;
ANALYZE TABLE `character_talent`;
ANALYZE TABLE `group_member`;
ANALYZE TABLE `guild_member`;
ANALYZE TABLE `item_instance`;
-- =====================================================
-- PHASE 2: DROP EXISTING SUBOPTIMAL INDEXES
-- =====================================================
-- Drop redundant indexes (if they exist)
DROP INDEX IF EXISTS `idx_online` ON `characters`;
DROP INDEX IF EXISTS `idx_account` ON `characters`;
DROP INDEX IF EXISTS `idx_name` ON `characters`;
-- =====================================================
-- PHASE 3: CREATE HIGH-PERFORMANCE INDEXES
-- =====================================================
-- Characters table: Core bot queries
-- Covering index for bot login queries (includes all needed columns)
CREATE INDEX `idx_bot_login_covering` ON `characters`
(`account`, `online`, `guid`, `name`, `race`, `class`, `level`, `zone`, `map`, `position_x`, `position_y`, `position_z`)
COMMENT 'Covering index for bot login queries - eliminates table lookups';
-- Composite index for bot selection and filtering
CREATE INDEX `idx_bot_selection` ON `characters`
(`online`, `level`, `class`, `zone`)
COMMENT 'Optimized for bot selection by criteria';
-- Unique index for fast name lookups
CREATE UNIQUE INDEX `idx_unique_name` ON `characters` (`name`)
COMMENT 'Fast name resolution for bot commands';
-- Index for account-based bot queries
CREATE INDEX `idx_account_guid` ON `characters` (`account`, `guid`)
COMMENT 'Fast account to bot mapping';
-- Index for zone-based bot queries (finding nearby bots)
CREATE INDEX `idx_zone_map_position` ON `characters`
(`zone`, `map`, `position_x`, `position_y`)
COMMENT 'Spatial queries for nearby bot detection';
-- Character stats: Fast stat retrieval
CREATE INDEX `idx_stats_guid` ON `character_stats` (`guid`)
COMMENT 'Fast stat retrieval for combat calculations';
-- Character spells: Spell availability checks
CREATE INDEX `idx_spell_guid_spell` ON `character_spell` (`guid`, `spell`)
COMMENT 'Fast spell availability checks';
-- Character actions: Action bar queries
CREATE INDEX `idx_action_guid_composite` ON `character_action`
(`guid`, `spec`, `button`)
COMMENT 'Fast action bar retrieval';
-- Character inventory: Equipment and bag queries
CREATE INDEX `idx_inventory_guid_slot` ON `character_inventory`
(`guid`, `slot`, `bag`)
COMMENT 'Fast inventory access for equipment checks';
-- Item instance: Fast item property lookups
CREATE INDEX `idx_item_owner_guid` ON `item_instance` (`owner_guid`)
COMMENT 'Fast item ownership queries';
-- Character auras: Active buff/debuff queries
CREATE INDEX `idx_aura_guid_composite` ON `character_aura`
(`guid`, `caster_guid`, `spell`)
COMMENT 'Fast aura state queries';
-- Character quest status: Quest progression
CREATE INDEX `idx_quest_guid_status` ON `character_queststatus`
(`guid`, `status`, `quest`)
COMMENT 'Fast quest status checks';
-- Character reputation: Faction standing queries
CREATE INDEX `idx_reputation_guid_faction` ON `character_reputation`
(`guid`, `faction`, `standing`)
COMMENT 'Fast reputation checks';
-- Character skills: Skill level queries
CREATE INDEX `idx_skills_guid_skill` ON `character_skills`
(`guid`, `skill`, `value`)
COMMENT 'Fast skill checks for crafting/gathering';
-- Character talents: Spec and talent queries
CREATE INDEX `idx_talent_guid_spec` ON `character_talent`
(`guid`, `talentGroup`)
COMMENT 'Fast talent/spec retrieval';
-- Group member: Fast group queries
CREATE INDEX `idx_group_member_composite` ON `group_member`
(`memberGuid`, `guid`)
COMMENT 'Bidirectional group lookups';
CREATE INDEX `idx_group_guid` ON `group_member` (`guid`)
COMMENT 'Fast group member listing';
-- Guild member: Guild roster queries
CREATE INDEX `idx_guild_member_guid` ON `guild_member` (`guid`)
COMMENT 'Fast guild membership checks';
-- =====================================================
-- PHASE 4: PARTITIONING FOR SCALE (5000+ BOTS)
-- =====================================================
-- Create partitioned table for bot state (high-frequency updates)
CREATE TABLE IF NOT EXISTS `playerbot_state` (
`guid` INT UNSIGNED NOT NULL,
`online` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`last_update` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`ai_state` VARCHAR(50) DEFAULT NULL,
`combat_state` TINYINT UNSIGNED DEFAULT 0,
`follow_target` INT UNSIGNED DEFAULT 0,
`position_x` FLOAT DEFAULT 0,
`position_y` FLOAT DEFAULT 0,
`position_z` FLOAT DEFAULT 0,
`map` SMALLINT UNSIGNED DEFAULT 0,
`zone` MEDIUMINT UNSIGNED DEFAULT 0,
PRIMARY KEY (`guid`, `online`),
INDEX `idx_online_state` (`online`, `last_update`),
INDEX `idx_zone_online` (`zone`, `online`),
INDEX `idx_follow_target` (`follow_target`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(guid DIV 1000)
PARTITIONS 10
COMMENT='High-performance bot state table with partitioning';
-- Create memory table for ultra-fast bot session cache
CREATE TABLE IF NOT EXISTS `playerbot_session_cache` (
`guid` INT UNSIGNED NOT NULL PRIMARY KEY,
`account` INT UNSIGNED NOT NULL,
`name` VARCHAR(12) NOT NULL,
`level` TINYINT UNSIGNED NOT NULL,
`class` TINYINT UNSIGNED NOT NULL,
`race` TINYINT UNSIGNED NOT NULL,
`zone` MEDIUMINT UNSIGNED NOT NULL,
`map` SMALLINT UNSIGNED NOT NULL,
`online` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`group_id` INT UNSIGNED DEFAULT 0,
`last_action` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_account` (`account`),
INDEX `idx_online` (`online`),
INDEX `idx_group` (`group_id`),
UNIQUE INDEX `idx_name` (`name`)
) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4
COMMENT='In-memory cache for ultra-fast bot session lookups';
-- =====================================================
-- PHASE 5: STORED PROCEDURES FOR BATCH OPERATIONS
-- =====================================================
DELIMITER $$
-- Optimized bot login procedure
CREATE PROCEDURE `sp_playerbot_login`(
IN p_guid INT UNSIGNED
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Update online status
UPDATE `characters` SET `online` = 1 WHERE `guid` = p_guid;
-- Update bot state
INSERT INTO `playerbot_state` (`guid`, `online`, `last_update`)
VALUES (p_guid, 1, NOW())
ON DUPLICATE KEY UPDATE
`online` = 1,
`last_update` = NOW();
-- Load into session cache
INSERT INTO `playerbot_session_cache`
SELECT
c.guid, c.account, c.name, c.level, c.class, c.race,
c.zone, c.map, 1, 0, NOW()
FROM `characters` c
WHERE c.guid = p_guid
ON DUPLICATE KEY UPDATE
`online` = 1,
`last_action` = NOW();
COMMIT;
END$$
-- Batch bot spawn procedure
CREATE PROCEDURE `sp_playerbot_batch_spawn`(
IN p_account INT UNSIGNED,
IN p_count INT UNSIGNED
)
BEGIN
DECLARE v_done INT DEFAULT FALSE;
DECLARE v_guid INT UNSIGNED;
DECLARE v_spawned INT DEFAULT 0;
DECLARE bot_cursor CURSOR FOR
SELECT guid FROM characters
WHERE account = p_account
AND online = 0
LIMIT p_count;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
START TRANSACTION;
OPEN bot_cursor;
spawn_loop: LOOP
FETCH bot_cursor INTO v_guid;
IF v_done THEN
LEAVE spawn_loop;
END IF;
CALL sp_playerbot_login(v_guid);
SET v_spawned = v_spawned + 1;
END LOOP;
CLOSE bot_cursor;
COMMIT;
SELECT v_spawned AS bots_spawned;
END$$
-- Fast bot state retrieval
CREATE PROCEDURE `sp_playerbot_get_state`(
IN p_guid INT UNSIGNED
)
BEGIN
-- Try memory cache first
SELECT * FROM `playerbot_session_cache` WHERE guid = p_guid;
-- If not in cache, load from disk
IF FOUND_ROWS() = 0 THEN
SELECT
c.guid, c.account, c.name, c.level, c.class, c.race,
c.zone, c.map, c.online, gm.guid as group_id,
ps.last_update as last_action
FROM characters c
LEFT JOIN group_member gm ON c.guid = gm.memberGuid
LEFT JOIN playerbot_state ps ON c.guid = ps.guid
WHERE c.guid = p_guid;
END IF;
END$$
-- Batch update bot positions
CREATE PROCEDURE `sp_playerbot_update_positions`(
IN p_positions TEXT
)
BEGIN
DECLARE v_done INT DEFAULT FALSE;
DECLARE v_pos VARCHAR(100);
DECLARE v_guid INT;
DECLARE v_x, v_y, v_z FLOAT;
DECLARE v_map INT;
-- Parse positions format: "guid,x,y,z,map;guid,x,y,z,map;..."
CREATE TEMPORARY TABLE temp_positions (
guid INT UNSIGNED,
x FLOAT,
y FLOAT,
z FLOAT,
map INT UNSIGNED
);
-- Insert parsed positions
SET @sql = CONCAT('INSERT INTO temp_positions VALUES ', p_positions);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Batch update
UPDATE characters c
INNER JOIN temp_positions t ON c.guid = t.guid
SET
c.position_x = t.x,
c.position_y = t.y,
c.position_z = t.z,
c.map = t.map;
-- Update state table
UPDATE playerbot_state ps
INNER JOIN temp_positions t ON ps.guid = t.guid
SET
ps.position_x = t.x,
ps.position_y = t.y,
ps.position_z = t.z,
ps.map = t.map,
ps.last_update = NOW();
DROP TEMPORARY TABLE temp_positions;
END$$
DELIMITER ;
-- =====================================================
-- PHASE 6: SWITCH TO AUTH DATABASE
-- =====================================================
USE `auth`;
-- Account table optimizations
CREATE INDEX `idx_account_playerbot` ON `account`
(`id`, `username`, `last_login`)
COMMENT 'Optimized for bot account queries';
CREATE INDEX `idx_username_lookup` ON `account` (`username`)
COMMENT 'Fast username lookups';
-- Create bot account tracking table
CREATE TABLE IF NOT EXISTS `playerbot_accounts` (
`account_id` INT UNSIGNED NOT NULL PRIMARY KEY,
`bot_count` INT UNSIGNED NOT NULL DEFAULT 0,
`last_bot_action` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_bot_count` (`bot_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='Track bot accounts for fast filtering';
-- =====================================================
-- PHASE 7: STATISTICS AND MONITORING
-- =====================================================
USE `characters`;
-- Create performance monitoring table
CREATE TABLE IF NOT EXISTS `playerbot_performance` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`operation` VARCHAR(50) NOT NULL,
`duration_ms` INT UNSIGNED NOT NULL,
`bot_count` INT UNSIGNED DEFAULT 0,
`details` TEXT,
INDEX `idx_timestamp` (`timestamp`),
INDEX `idx_operation` (`operation`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='Track playerbot operation performance';
-- =====================================================
-- PHASE 8: UPDATE STATISTICS
-- =====================================================
-- Update all table statistics for query optimizer
ANALYZE TABLE `characters`;
ANALYZE TABLE `character_stats`;
ANALYZE TABLE `character_spell`;
ANALYZE TABLE `character_action`;
ANALYZE TABLE `character_inventory`;
ANALYZE TABLE `character_aura`;
ANALYZE TABLE `character_queststatus`;
ANALYZE TABLE `character_reputation`;
ANALYZE TABLE `character_skills`;
ANALYZE TABLE `character_talent`;
ANALYZE TABLE `group_member`;
ANALYZE TABLE `guild_member`;
ANALYZE TABLE `item_instance`;
ANALYZE TABLE `playerbot_state`;
ANALYZE TABLE `playerbot_session_cache`;
ANALYZE TABLE `playerbot_performance`;
-- =====================================================
-- PERFORMANCE VALIDATION QUERIES
-- =====================================================
-- Test covering index performance
EXPLAIN SELECT guid, name, race, class, level, zone, map, position_x, position_y, position_z
FROM characters
WHERE account = 1 AND online = 0;
-- Test bot selection performance
EXPLAIN SELECT guid, name FROM characters
WHERE online = 1 AND level BETWEEN 70 AND 80 AND class = 1 AND zone = 1519;
-- Test group lookup performance
EXPLAIN SELECT c.guid, c.name, c.level
FROM characters c
INNER JOIN group_member gm ON c.guid = gm.memberGuid
WHERE gm.guid = 1;
-- Show index usage statistics
SELECT
table_name,
index_name,
cardinality,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name IN ('characters', 'playerbot_state', 'playerbot_session_cache')
ORDER BY table_name, seq_in_index;
-- =====================================================
-- SUCCESS METRICS
-- =====================================================
-- Expected improvements:
-- - Bot login: 287ms -> <100ms (71% reduction)
-- - 100 bot spawn: 18s -> <5s (72% reduction)
-- - Character queries: 50-150ms -> <10ms (95% reduction)
-- - Index hit rate: >99%
-- - Cache hit rate: >90%
-- =====================================================
@@ -0,0 +1,534 @@
-- Playerbot Database Schema
-- This creates all necessary tables for the Playerbot module
-- --------------------------------------------------------
-- Account Management Tables
-- --------------------------------------------------------
-- Table: playerbot_accounts
-- Purpose: Track bot accounts and their metadata
CREATE TABLE IF NOT EXISTS `playerbot_accounts` (
`account_id` INT UNSIGNED NOT NULL,
`is_bot` TINYINT(1) NOT NULL DEFAULT 1,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`character_count` INT UNSIGNED NOT NULL DEFAULT 0,
`last_login` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`account_id`),
KEY `idx_is_bot` (`is_bot`),
KEY `idx_is_active` (`is_active`),
KEY `idx_character_count` (`character_count`),
CONSTRAINT `chk_character_limit` CHECK (`character_count` <= 10)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Metadata for bot-controlled accounts';
-- --------------------------------------------------------
-- Name Management Tables
-- --------------------------------------------------------
-- Table: playerbots_names
-- Purpose: Pool of unique names for bot character generation
-- IMPORTANT: Each name can only be used ONCE across all characters
CREATE TABLE IF NOT EXISTS `playerbots_names` (
`name_id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`gender` TINYINT(3) UNSIGNED NOT NULL COMMENT '0 = male, 1 = female',
PRIMARY KEY (`name_id`),
UNIQUE KEY `name` (`name`),
KEY `idx_gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Random bot name pool';
-- Table: playerbots_names_used
-- Purpose: Track which names are currently in use
CREATE TABLE IF NOT EXISTS `playerbots_names_used` (
`name_id` INT(11) NOT NULL,
`character_guid` INT UNSIGNED NOT NULL,
`used_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`name_id`),
UNIQUE KEY `idx_character` (`character_guid`),
CONSTRAINT `fk_name_id` FOREIGN KEY (`name_id`)
REFERENCES `playerbots_names` (`name_id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Tracks which bot is using which name';
-- --------------------------------------------------------
-- Character Management Tables
-- --------------------------------------------------------
-- Table: playerbot_characters
-- Purpose: Track bot characters and their properties
CREATE TABLE IF NOT EXISTS `playerbot_characters` (
`guid` INT UNSIGNED NOT NULL,
`account_id` INT UNSIGNED NOT NULL,
`name` VARCHAR(50) NOT NULL,
`race` TINYINT UNSIGNED NOT NULL,
`class` TINYINT UNSIGNED NOT NULL,
`level` TINYINT UNSIGNED NOT NULL DEFAULT 1,
`role` ENUM('tank', 'healer', 'dps', 'hybrid') DEFAULT 'dps',
`last_login` TIMESTAMP NULL DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`guid`),
KEY `idx_account` (`account_id`),
KEY `idx_level` (`level`),
KEY `idx_role` (`role`),
CONSTRAINT `fk_bot_account` FOREIGN KEY (`account_id`)
REFERENCES `playerbot_accounts` (`account_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Bot character metadata';
-- --------------------------------------------------------
-- Character Distribution Tables
-- --------------------------------------------------------
-- Table: playerbot_race_distribution
-- Purpose: Configurable race distribution for bot generation
CREATE TABLE IF NOT EXISTS `playerbot_race_distribution` (
`race` TINYINT UNSIGNED NOT NULL,
`weight` FLOAT NOT NULL DEFAULT 1.0,
`male_ratio` FLOAT NOT NULL DEFAULT 0.5,
`faction` ENUM('alliance', 'horde', 'neutral') NOT NULL,
`enabled` TINYINT(1) DEFAULT 1,
PRIMARY KEY (`race`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Race distribution weights for bot generation';
-- Insert default race distribution based on WoW statistics
INSERT INTO `playerbot_race_distribution` (`race`, `weight`, `male_ratio`, `faction`) VALUES
(1, 29.5, 0.52, 'alliance'), -- Human
(2, 8.7, 0.75, 'horde'), -- Orc
(3, 7.2, 0.75, 'alliance'), -- Dwarf
(4, 20.8, 0.35, 'alliance'), -- Night Elf
(5, 11.2, 0.70, 'horde'), -- Undead
(6, 6.4, 0.72, 'horde'), -- Tauren
(7, 5.3, 0.50, 'alliance'), -- Gnome
(8, 7.1, 0.70, 'horde'), -- Troll
(10, 35.8, 0.35, 'horde'), -- Blood Elf
(11, 8.9, 0.40, 'alliance') -- Draenei
ON DUPLICATE KEY UPDATE weight = VALUES(weight);
-- Table: playerbot_class_distribution
-- Purpose: Configurable class distribution for bot generation
CREATE TABLE IF NOT EXISTS `playerbot_class_distribution` (
`class` TINYINT UNSIGNED NOT NULL,
`weight` FLOAT NOT NULL DEFAULT 1.0,
`tank_capable` TINYINT(1) DEFAULT 0,
`healer_capable` TINYINT(1) DEFAULT 0,
`enabled` TINYINT(1) DEFAULT 1,
PRIMARY KEY (`class`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Class distribution weights for bot generation';
-- Insert default class distribution based on WoW statistics
INSERT INTO `playerbot_class_distribution` (`class`, `weight`, `tank_capable`, `healer_capable`) VALUES
(1, 9.8, 1, 0), -- Warrior
(2, 11.5, 1, 1), -- Paladin
(3, 11.8, 0, 0), -- Hunter
(4, 7.3, 0, 0), -- Rogue
(5, 8.7, 0, 1), -- Priest
(6, 9.2, 1, 0), -- Death Knight
(7, 6.2, 0, 1), -- Shaman
(8, 8.4, 0, 0), -- Mage
(9, 6.8, 0, 0), -- Warlock
(10, 1.5, 1, 1), -- Monk
(11, 10.9, 1, 1), -- Druid
(12, 7.9, 1, 0) -- Demon Hunter
ON DUPLICATE KEY UPDATE weight = VALUES(weight);
-- Table: playerbot_race_class_multipliers
-- Purpose: Adjust probability of specific race/class combinations
CREATE TABLE IF NOT EXISTS `playerbot_race_class_multipliers` (
`race` TINYINT UNSIGNED NOT NULL,
`class` TINYINT UNSIGNED NOT NULL,
`multiplier` FLOAT NOT NULL DEFAULT 1.0 COMMENT 'Higher = more common',
PRIMARY KEY (`race`, `class`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Race/class combination probability multipliers';
-- Insert popular combinations with higher multipliers
INSERT INTO `playerbot_race_class_multipliers` (`race`, `class`, `multiplier`) VALUES
(10, 2, 2.5), -- Blood Elf Paladin
(4, 12, 3.0), -- Night Elf Demon Hunter
(1, 2, 2.0), -- Human Paladin
(10, 3, 2.0), -- Blood Elf Hunter
(4, 11, 2.2), -- Night Elf Druid
(1, 1, 1.8), -- Human Warrior
(2, 1, 2.0), -- Orc Warrior
(5, 4, 1.8), -- Undead Rogue
(6, 11, 2.0), -- Tauren Druid
(3, 3, 1.8) -- Dwarf Hunter
ON DUPLICATE KEY UPDATE multiplier = VALUES(multiplier);
-- --------------------------------------------------------
-- Configuration Tables
-- --------------------------------------------------------
-- Table: playerbot_config
-- Purpose: Store runtime configuration values
CREATE TABLE IF NOT EXISTS `playerbot_config` (
`key` VARCHAR(100) NOT NULL,
`value` TEXT,
`description` TEXT,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Runtime configuration for playerbot system';
-- Insert default configuration values
INSERT INTO `playerbot_config` (`key`, `value`, `description`) VALUES
('schema_version', '1', 'Database schema version'),
('max_characters_per_account', '10', 'Maximum characters per bot account'),
('strict_character_limit', '1', 'Enforce character limit strictly'),
('auto_fix_character_limit', '0', 'Automatically fix character limit violations')
ON DUPLICATE KEY UPDATE value = VALUES(value);
-- --------------------------------------------------------
-- Performance Monitoring Tables
-- --------------------------------------------------------
-- Table: playerbot_performance_metrics
-- Purpose: Track performance metrics for optimization
CREATE TABLE IF NOT EXISTS `playerbot_performance_metrics` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`metric_name` VARCHAR(100) NOT NULL,
`value` FLOAT NOT NULL,
`recorded_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_metric_time` (`metric_name`, `recorded_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Performance metrics tracking';
-- --------------------------------------------------------
-- Database Triggers for Character Count Management
-- --------------------------------------------------------
DELIMITER $$
-- Trigger: Update character count on character creation
DROP TRIGGER IF EXISTS `trg_bot_character_insert`$$
CREATE TRIGGER `trg_bot_character_insert`
AFTER INSERT ON `characters`
FOR EACH ROW
BEGIN
IF EXISTS (SELECT 1 FROM playerbot_accounts WHERE account_id = NEW.account AND is_bot = 1) THEN
UPDATE playerbot_accounts
SET character_count = character_count + 1
WHERE account_id = NEW.account;
END IF;
END$$
-- Trigger: Update character count on character deletion
DROP TRIGGER IF EXISTS `trg_bot_character_delete`$$
CREATE TRIGGER `trg_bot_character_delete`
AFTER DELETE ON `characters`
FOR EACH ROW
BEGIN
IF EXISTS (SELECT 1 FROM playerbot_accounts WHERE account_id = OLD.account AND is_bot = 1) THEN
UPDATE playerbot_accounts
SET character_count = GREATEST(0, character_count - 1)
WHERE account_id = OLD.account;
-- Also release the name
DELETE FROM playerbots_names_used WHERE character_guid = OLD.guid;
END IF;
END$$
DELIMITER ;
-- --------------------------------------------------------
-- Indexes for Performance
-- --------------------------------------------------------
-- Helper procedure to add index if not exists (MySQL-compatible)
DROP PROCEDURE IF EXISTS `pb_safe_add_index`;
DELIMITER //
CREATE PROCEDURE `pb_safe_add_index`(
IN p_table VARCHAR(64),
IN p_index VARCHAR(64),
IN p_columns VARCHAR(255)
)
BEGIN
DECLARE index_exists INT DEFAULT 0;
SELECT COUNT(*) INTO index_exists
FROM `information_schema`.`STATISTICS`
WHERE `TABLE_SCHEMA` = DATABASE()
AND `TABLE_NAME` = p_table
AND `INDEX_NAME` = p_index;
IF index_exists = 0 THEN
SET @sql = CONCAT('CREATE INDEX `', p_index, '` ON `', p_table, '`(', p_columns, ')');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END //
DELIMITER ;
CALL `pb_safe_add_index`('playerbots_names', 'idx_playerbots_names_gender', 'gender');
CALL `pb_safe_add_index`('playerbots_names', 'idx_playerbots_names_name_gender', 'name, gender');
DROP PROCEDURE IF EXISTS `pb_safe_add_index`;
-- --------------------------------------------------------
-- Views for Statistics and Monitoring
-- --------------------------------------------------------
-- View: v_available_names
-- Purpose: Show available names by gender
DROP VIEW IF EXISTS v_available_names;
CREATE VIEW v_available_names AS
SELECT
pn.name_id,
pn.name,
pn.gender,
CASE WHEN pnu.name_id IS NULL THEN 1 ELSE 0 END AS is_available
FROM playerbots_names pn
LEFT JOIN playerbots_names_used pnu ON pn.name_id = pnu.name_id;
-- View: v_name_usage_stats
-- Purpose: Name usage statistics
DROP VIEW IF EXISTS v_name_usage_stats;
CREATE VIEW v_name_usage_stats AS
SELECT
(SELECT COUNT(*) FROM playerbots_names) AS total_names,
(SELECT COUNT(*) FROM playerbots_names WHERE gender = 0) AS total_male_names,
(SELECT COUNT(*) FROM playerbots_names WHERE gender = 1) AS total_female_names,
(SELECT COUNT(*) FROM playerbots_names_used) AS used_names,
(SELECT COUNT(*) FROM playerbots_names pn
LEFT JOIN playerbots_names_used pnu ON pn.name_id = pnu.name_id
WHERE pnu.name_id IS NULL AND pn.gender = 0) AS available_male_names,
(SELECT COUNT(*) FROM playerbots_names pn
LEFT JOIN playerbots_names_used pnu ON pn.name_id = pnu.name_id
WHERE pnu.name_id IS NULL AND pn.gender = 1) AS available_female_names;
-- View: v_bot_account_stats
-- Purpose: Bot account statistics
DROP VIEW IF EXISTS v_bot_account_stats;
CREATE VIEW v_bot_account_stats AS
SELECT
COUNT(*) AS total_accounts,
SUM(is_active) AS active_accounts,
SUM(character_count) AS total_characters,
AVG(character_count) AS avg_characters_per_account,
MAX(character_count) AS max_characters,
SUM(CASE WHEN character_count > 10 THEN 1 ELSE 0 END) AS accounts_over_limit
FROM playerbot_accounts
WHERE is_bot = 1;
-- View: v_bot_character_distribution
-- Purpose: Show distribution of bot characters by race and class
DROP VIEW IF EXISTS v_bot_character_distribution;
CREATE VIEW v_bot_character_distribution AS
SELECT
race,
class,
COUNT(*) as count,
AVG(level) as avg_level
FROM playerbot_characters
GROUP BY race, class;
-- --------------------------------------------------------
-- Stored Procedures for Name Management
-- --------------------------------------------------------
DELIMITER $$
-- Procedure: GetRandomAvailableName
-- Purpose: Get a random available name for a given gender
DROP PROCEDURE IF EXISTS GetRandomAvailableName$$
CREATE PROCEDURE GetRandomAvailableName(
IN p_gender TINYINT(3),
OUT p_name_id INT(11),
OUT p_name VARCHAR(255)
)
BEGIN
DECLARE v_count INT;
-- Get count of available names for gender
SELECT COUNT(*) INTO v_count
FROM playerbots_names pn
LEFT JOIN playerbots_names_used pnu ON pn.name_id = pnu.name_id
WHERE pn.gender = p_gender
AND pnu.name_id IS NULL;
-- If no names available, return NULL
IF v_count = 0 THEN
SET p_name_id = NULL;
SET p_name = NULL;
ELSE
-- Get random available name
SELECT pn.name_id, pn.name
INTO p_name_id, p_name
FROM playerbots_names pn
LEFT JOIN playerbots_names_used pnu ON pn.name_id = pnu.name_id
WHERE pn.gender = p_gender
AND pnu.name_id IS NULL
ORDER BY RAND()
LIMIT 1;
END IF;
END$$
-- Procedure: AllocateName
-- Purpose: Allocate a name to a character
DROP PROCEDURE IF EXISTS AllocateName$$
CREATE PROCEDURE AllocateName(
IN p_gender TINYINT(3),
IN p_character_guid INT,
OUT p_name_id INT(11),
OUT p_name VARCHAR(255),
OUT p_success BOOLEAN
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_success = FALSE;
END;
START TRANSACTION;
-- Get random available name
CALL GetRandomAvailableName(p_gender, p_name_id, p_name);
IF p_name_id IS NOT NULL THEN
-- Mark name as used
INSERT INTO playerbots_names_used (name_id, character_guid)
VALUES (p_name_id, p_character_guid);
SET p_success = TRUE;
COMMIT;
ELSE
SET p_success = FALSE;
ROLLBACK;
END IF;
END$$
-- Procedure: ReleaseName
-- Purpose: Release a name back to the pool
DROP PROCEDURE IF EXISTS ReleaseName$$
CREATE PROCEDURE ReleaseName(
IN p_character_guid INT
)
BEGIN
DELETE FROM playerbots_names_used
WHERE character_guid = p_character_guid;
END$$
-- Procedure: CheckAccountCharacterLimit
-- Purpose: Check if account has reached character limit
DROP PROCEDURE IF EXISTS CheckAccountCharacterLimit$$
CREATE PROCEDURE CheckAccountCharacterLimit(
IN p_account_id INT,
OUT p_can_create BOOLEAN,
OUT p_current_count INT
)
BEGIN
SELECT character_count INTO p_current_count
FROM playerbot_accounts
WHERE account_id = p_account_id AND is_bot = 1;
IF p_current_count IS NULL THEN
-- Not a bot account
SET p_can_create = TRUE;
SET p_current_count = 0;
ELSEIF p_current_count >= 10 THEN
-- Already at limit
SET p_can_create = FALSE;
ELSE
SET p_can_create = TRUE;
END IF;
END$$
DELIMITER ;
-- --------------------------------------------------------
-- Function to check name availability
-- --------------------------------------------------------
DELIMITER $$
DROP FUNCTION IF EXISTS IsNameAvailable$$
CREATE FUNCTION IsNameAvailable(p_name VARCHAR(255))
RETURNS BOOLEAN
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE v_is_available BOOLEAN DEFAULT FALSE;
DECLARE v_name_id INT;
-- Get name_id for the name
SELECT name_id INTO v_name_id
FROM playerbots_names
WHERE name = p_name
LIMIT 1;
-- Check if name_id exists and is not in use
IF v_name_id IS NOT NULL THEN
SELECT NOT EXISTS(
SELECT 1 FROM playerbots_names_used
WHERE name_id = v_name_id
) INTO v_is_available;
END IF;
RETURN v_is_available;
END$$
-- Function to get character count for account
DROP FUNCTION IF EXISTS GetAccountCharacterCount$$
CREATE FUNCTION GetAccountCharacterCount(p_account_id INT)
RETURNS INT
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE v_count INT DEFAULT 0;
SELECT character_count INTO v_count
FROM playerbot_accounts
WHERE account_id = p_account_id AND is_bot = 1;
IF v_count IS NULL THEN
SET v_count = 0;
END IF;
RETURN v_count;
END$$
DELIMITER ;
-- --------------------------------------------------------
-- Cleanup procedure for old performance metrics
-- --------------------------------------------------------
DELIMITER $$
DROP PROCEDURE IF EXISTS CleanupOldMetrics$$
CREATE PROCEDURE CleanupOldMetrics(
IN p_days_to_keep INT
)
BEGIN
DELETE FROM playerbot_performance_metrics
WHERE recorded_at < DATE_SUB(NOW(), INTERVAL p_days_to_keep DAY);
OPTIMIZE TABLE playerbot_performance_metrics;
END$$
DELIMITER ;
-- --------------------------------------------------------
-- Initial data validation
-- --------------------------------------------------------
-- Check for accounts violating the 10 character limit
SELECT
'WARNING: Bot accounts with more than 10 characters:' AS message,
account_id,
character_count
FROM playerbot_accounts
WHERE is_bot = 1 AND character_count > 10;
-- --------------------------------------------------------
-- Grant permissions (adjust as needed for your setup)
-- --------------------------------------------------------
-- Example: GRANT ALL PRIVILEGES ON playerbot.* TO 'trinity'@'localhost';
@@ -0,0 +1,141 @@
-- Sample bot names for the Playerbot system
-- This file populates the playerbots_names table with a variety of names
-- Clear existing names (optional - comment out if you want to append)
-- TRUNCATE TABLE playerbots_names;
-- Male names (gender = 0)
INSERT INTO `playerbots_names` (`name`, `gender`) VALUES
-- Common Human/Alliance style names
('Aldric', 0), ('Gareth', 0), ('Marcus', 0), ('William', 0), ('Edward', 0),
('Richard', 0), ('Thomas', 0), ('Robert', 0), ('James', 0), ('Henry', 0),
('Arthur', 0), ('Charles', 0), ('George', 0), ('Francis', 0), ('Albert', 0),
('Frederick', 0), ('Samuel', 0), ('Benjamin', 0), ('Nicholas', 0), ('Alexander', 0),
('Jonathan', 0), ('Christopher', 0), ('Matthew', 0), ('Daniel', 0), ('Michael', 0),
('Stephen', 0), ('Andrew', 0), ('Patrick', 0), ('Lawrence', 0), ('Vincent', 0),
('Gregory', 0), ('Raymond', 0), ('Timothy', 0), ('Kenneth', 0), ('Eugene', 0),
('Russell', 0), ('Walter', 0), ('Harold', 0), ('Douglas', 0), ('Gerald', 0),
-- Dwarf style names
('Thorgrim', 0), ('Balin', 0), ('Thorin', 0), ('Gimli', 0), ('Durin', 0),
('Dwalin', 0), ('Gloin', 0), ('Oin', 0), ('Bifur', 0), ('Bofur', 0),
('Bombur', 0), ('Nori', 0), ('Dori', 0), ('Ori', 0), ('Flint', 0),
('Magni', 0), ('Muradin', 0), ('Brann', 0), ('Thaurissan', 0), ('Ironforge', 0),
-- Night Elf style names
('Malfurion', 0), ('Illidan', 0), ('Tyrande', 0), ('Cenarius', 0), ('Xavius', 0),
('Fandral', 0), ('Broll', 0), ('Jarod', 0), ('Ravencrest', 0), ('Shadowsong', 0),
('Stormrage', 0), ('Whisperwind', 0), ('Staghelm', 0), ('Bearmantle', 0), ('Moonrage', 0),
-- Gnome style names
('Gelbin', 0), ('Sicco', 0), ('Tinker', 0), ('Mekkatorque', 0), ('Thermaplugg', 0),
('Cogspinner', 0), ('Geargrind', 0), ('Fizzlebolt', 0), ('Sparkwrench', 0), ('Cogspin', 0),
-- Draenei style names
('Velen', 0), ('Akama', 0), ('Nobundo', 0), ('Maraad', 0), ('Hatuun', 0),
('Khadgar', 0), ('Turalyon', 0), ('Kurdran', 0), ('Alleria', 0), ('Danath', 0),
-- Orc style names
('Thrall', 0), ('Durotan', 0), ('Orgrim', 0), ('Grommash', 0), ('Garrosh', 0),
('Doomhammer', 0), ('Hellscream', 0), ('Blackhand', 0), ('Kilrogg', 0), ('Kargath', 0),
('Nazgrel', 0), ('Saurfang', 0), ('Eitrigg', 0), ('Rehgar', 0), ('Jorin', 0),
('Broxigar', 0), ('Varok', 0), ('Nazgrim', 0), ('Malkorok', 0), ('Zaela', 0),
-- Undead style names
('Sylvanas', 0), ('Nathanos', 0), ('Putress', 0), ('Faranell', 0), ('Belmont', 0),
('Varimathras', 0), ('Balnazzar', 0), ('Detheroc', 0), ('Tichondrius', 0), ('Anetheron', 0),
('Archimonde', 0), ('Mannoroth', 0), ('Magtheridon', 0), ('Azgalor', 0), ('Kazzak', 0),
-- Tauren style names
('Cairne', 0), ('Baine', 0), ('Hamuul', 0), ('Runetotem', 0), ('Bloodhoof', 0),
('Thunderhorn', 0), ('Skychaser', 0), ('Wildmane', 0), ('Ragetotem', 0), ('Grimtotem', 0),
('Highmountain', 0), ('Rivermane', 0), ('Winterhoof', 0), ('Mistrunner', 0), ('Dawnstrider', 0),
-- Troll style names
('Voljin', 0), ('Senjin', 0), ('Rokhan', 0), ('Zalazane', 0), ('Zuljin', 0),
('Jintha', 0), ('Venoxis', 0), ('Mandokir', 0), ('Marli', 0), ('Thekal', 0),
('Arlokk', 0), ('Jeklik', 0), ('Hakkari', 0), ('Gurubashi', 0), ('Amani', 0),
-- Blood Elf style names
('Kaelthas', 0), ('Rommath', 0), ('Lorthemar', 0), ('Halduron', 0), ('Aethas', 0),
('Theron', 0), ('Sunstrider', 0), ('Brightwing', 0), ('Dawnseeker', 0), ('Sunreaver', 0),
('Bloodsworn', 0), ('Spellbreaker', 0), ('Farstrider', 0), ('Sunfury', 0), ('Dawncaller', 0),
-- Additional generic fantasy names
('Aiden', 0), ('Blake', 0), ('Connor', 0), ('Derek', 0), ('Ethan', 0),
('Felix', 0), ('Gabriel', 0), ('Hunter', 0), ('Ivan', 0), ('Jacob', 0),
('Kyle', 0), ('Liam', 0), ('Mason', 0), ('Nathan', 0), ('Oliver', 0),
('Peter', 0), ('Quinn', 0), ('Ryan', 0), ('Sean', 0), ('Tyler', 0),
('Ulrich', 0), ('Victor', 0), ('Wesley', 0), ('Xavier', 0), ('Zachary', 0);
-- Female names (gender = 1)
INSERT INTO `playerbots_names` (`name`, `gender`) VALUES
-- Common Human/Alliance style names
('Jaina', 1), ('Katherine', 1), ('Elizabeth', 1), ('Margaret', 1), ('Dorothy', 1),
('Sarah', 1), ('Jessica', 1), ('Michelle', 1), ('Amanda', 1), ('Melissa', 1),
('Jennifer', 1), ('Patricia', 1), ('Barbara', 1), ('Susan', 1), ('Linda', 1),
('Mary', 1), ('Lisa', 1), ('Nancy', 1), ('Karen', 1), ('Betty', 1),
('Helen', 1), ('Sandra', 1), ('Donna', 1), ('Carol', 1), ('Ruth', 1),
('Sharon', 1), ('Laura', 1), ('Cynthia', 1), ('Amy', 1), ('Angela', 1),
('Brenda', 1), ('Emma', 1), ('Anna', 1), ('Marie', 1), ('Christine', 1),
('Deborah', 1), ('Martha', 1), ('Maria', 1), ('Heather', 1), ('Diane', 1),
-- Night Elf style names
('Tyrande', 1), ('Maiev', 1), ('Shandris', 1), ('Azshara', 1), ('Elune', 1),
('Feathermoon', 1), ('Whisperwind', 1), ('Starweaver', 1), ('Moonwhisper', 1), ('Shadowsong', 1),
('Starfall', 1), ('Nightwhisper', 1), ('Dawnweaver', 1), ('Moonfire', 1), ('Starlight', 1),
-- Dwarf style names
('Moira', 1), ('Bronzebeard', 1), ('Ironforge', 1), ('Wildhammer', 1), ('Anvilmar', 1),
('Stormhammer', 1), ('Goldbeard', 1), ('Ironfoot', 1), ('Stoneform', 1), ('Deepforge', 1),
-- Gnome style names
('Chromie', 1), ('Millhouse', 1), ('Tinkerspell', 1), ('Cogwheel', 1), ('Sprocket', 1),
('Gearshift', 1), ('Wrenchcrank', 1), ('Boltbucket', 1), ('Fizzlewick', 1), ('Sparkplug', 1),
-- Draenei style names
('Yrel', 1), ('Ishanah', 1), ('Naielle', 1), ('Askara', 1), ('Dornaa', 1),
('Nobundo', 1), ('Khallia', 1), ('Emony', 1), ('Jessera', 1), ('Anchorite', 1),
-- Orc style names
('Draka', 1), ('Aggra', 1), ('Garona', 1), ('Zaela', 1), ('Geyah', 1),
('Kashur', 1), ('Greatmother', 1), ('Mankrik', 1), ('Sheyala', 1), ('Thura', 1),
-- Undead style names
('Sylvanas', 1), ('Calia', 1), ('Lilian', 1), ('Voss', 1), ('Faranell', 1),
('Velonara', 1), ('Delaryn', 1), ('Sira', 1), ('Arthura', 1), ('Renee', 1),
-- Tauren style names
('Magatha', 1), ('Hamuul', 1), ('Melor', 1), ('Tawnbranch', 1), ('Windtotem', 1),
('Skyhorn', 1), ('Highmountain', 1), ('Riverwind', 1), ('Eagletalon', 1), ('Moonwhisper', 1),
-- Troll style names
('Zentabra', 1), ('Vanira', 1), ('Shadra', 1), ('Bethekk', 1), ('Shirvallah', 1),
('Hireek', 1), ('Jeklik', 1), ('Marli', 1), ('Arlokk', 1), ('Shadehunter', 1),
-- Blood Elf style names
('Liadrin', 1), ('Valeera', 1), ('Alleria', 1), ('Vereesa', 1), ('Sylvanas', 1),
('Sunweaver', 1), ('Dawnblade', 1), ('Goldensword', 1), ('Brightwing', 1), ('Sunseeker', 1),
('Dawnrunner', 1), ('Sunsorrow', 1), ('Bloodwatcher', 1), ('Spellbinder', 1), ('Sunreaver', 1),
-- Additional generic fantasy names
('Alexis', 1), ('Brianna', 1), ('Catherine', 1), ('Diana', 1), ('Elena', 1),
('Fiona', 1), ('Gabrielle', 1), ('Hannah', 1), ('Isabella', 1), ('Jasmine', 1),
('Kaitlyn', 1), ('Lily', 1), ('Madison', 1), ('Natalie', 1), ('Olivia', 1),
('Penelope', 1), ('Quinn', 1), ('Rachel', 1), ('Sophia', 1), ('Taylor', 1),
('Uma', 1), ('Victoria', 1), ('Wendy', 1), ('Xena', 1), ('Yasmine', 1), ('Zoe', 1),
-- Additional fantasy-themed female names
('Aerith', 1), ('Aria', 1), ('Aurora', 1), ('Celeste', 1), ('Luna', 1),
('Nova', 1), ('Seraphina', 1), ('Stella', 1), ('Terra', 1), ('Vega', 1),
('Lyra', 1), ('Nyx', 1), ('Phoenix', 1), ('Raven', 1), ('Scarlett', 1),
('Violet', 1), ('Willow', 1), ('Winter', 1), ('Iris', 1), ('Jade', 1),
('Pearl', 1), ('Rose', 1), ('Ruby', 1), ('Sage', 1), ('Sky', 1);
-- Verify the count
SELECT
'Total names loaded:' AS info,
COUNT(*) AS total,
SUM(CASE WHEN gender = 0 THEN 1 ELSE 0 END) AS male_names,
SUM(CASE WHEN gender = 1 THEN 1 ELSE 0 END) AS female_names
FROM playerbots_names;
@@ -0,0 +1,415 @@
-- =====================================================
-- QUERY OPTIMIZATION PATTERNS FOR PLAYERBOT
-- Target: <10ms query response, batch operations
-- =====================================================
USE `characters`;
DELIMITER $$
-- =====================================================
-- OPTIMIZED BOT LOGIN QUERIES
-- =====================================================
-- Single bot login with all required data in one query
CREATE PROCEDURE `sp_bot_login_optimized`(
IN p_guid INT UNSIGNED,
OUT p_success BOOLEAN
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET p_success = FALSE;
ROLLBACK;
END;
SET p_success = FALSE;
START TRANSACTION;
-- Single query to get all character data using covering index
SELECT
c.guid, c.account, c.name, c.race, c.class, c.gender, c.level,
c.xp, c.money, c.skin, c.face, c.hairStyle, c.hairColor, c.facialStyle,
c.zone, c.map, c.position_x, c.position_y, c.position_z, c.orientation,
c.taximask, c.cinematic, c.totaltime, c.leveltime, c.extra_flags,
c.stable_slots, c.at_login, c.death_expire_time,
cs.maxhealth, cs.maxpower1, cs.maxpower2, cs.maxpower3, cs.maxpower4,
cs.maxpower5, cs.maxpower6, cs.maxpower7,
cs.strength, cs.agility, cs.stamina, cs.intellect, cs.spirit,
cs.armor, cs.resHoly, cs.resFire, cs.resNature, cs.resFrost,
cs.resShadow, cs.resArcane, cs.blockPct, cs.dodgePct, cs.parryPct,
cs.critPct, cs.rangedCritPct, cs.spellCritPct,
cs.attackPower, cs.rangedAttackPower, cs.spellPower
INTO @guid, @account, @name, @race, @class, @gender, @level,
@xp, @money, @skin, @face, @hairStyle, @hairColor, @facialStyle,
@zone, @map, @position_x, @position_y, @position_z, @orientation,
@taximask, @cinematic, @totaltime, @leveltime, @extra_flags,
@stable_slots, @at_login, @death_expire_time,
@maxhealth, @maxpower1, @maxpower2, @maxpower3, @maxpower4,
@maxpower5, @maxpower6, @maxpower7,
@strength, @agility, @stamina, @intellect, @spirit,
@armor, @resHoly, @resFire, @resNature, @resFrost,
@resShadow, @resArcane, @blockPct, @dodgePct, @parryPct,
@critPct, @rangedCritPct, @spellCritPct,
@attackPower, @rangedAttackPower, @spellPower
FROM characters c
STRAIGHT_JOIN character_stats cs ON c.guid = cs.guid
WHERE c.guid = p_guid
LIMIT 1;
IF @guid IS NOT NULL THEN
-- Update online status
UPDATE characters SET online = 1 WHERE guid = p_guid;
-- Update session cache
INSERT INTO playerbot_session_cache
(guid, account, name, level, class, race, zone, map, online, last_action)
VALUES
(p_guid, @account, @name, @level, @class, @race, @zone, @map, 1, NOW())
ON DUPLICATE KEY UPDATE
online = 1,
last_action = NOW();
SET p_success = TRUE;
END IF;
COMMIT;
END$$
-- =====================================================
-- BATCH BOT OPERATIONS
-- =====================================================
-- Batch load multiple bots (replaces N+1 queries)
CREATE PROCEDURE `sp_batch_load_bots`(
IN p_account_id INT UNSIGNED,
IN p_limit INT UNSIGNED
)
BEGIN
-- Use temporary table for batch processing
CREATE TEMPORARY TABLE IF NOT EXISTS temp_bot_batch (
guid INT UNSIGNED PRIMARY KEY,
loaded BOOLEAN DEFAULT FALSE
);
-- Select bots to load
INSERT INTO temp_bot_batch (guid)
SELECT guid FROM characters
WHERE account = p_account_id AND online = 0
LIMIT p_limit;
-- Batch load all character data
SELECT
c.guid, c.account, c.name, c.race, c.class, c.level,
c.zone, c.map, c.position_x, c.position_y, c.position_z,
cs.maxhealth, cs.maxpower1, cs.strength, cs.agility, cs.stamina,
cs.intellect, cs.spirit, cs.armor, cs.attackPower, cs.spellPower
FROM characters c
INNER JOIN temp_bot_batch t ON c.guid = t.guid
INNER JOIN character_stats cs ON c.guid = cs.guid;
-- Batch load equipment
SELECT
ci.guid, ci.slot, ci.item, ii.itemEntry, ii.enchantments
FROM character_inventory ci
INNER JOIN temp_bot_batch t ON ci.guid = t.guid
INNER JOIN item_instance ii ON ci.item = ii.guid
WHERE ci.bag = 0 AND ci.slot < 19;
-- Batch load action bars
SELECT
ca.guid, ca.button, ca.action, ca.type
FROM character_action ca
INNER JOIN temp_bot_batch t ON ca.guid = t.guid
WHERE ca.spec = 0;
-- Batch load spells
SELECT
cs.guid, cs.spell
FROM character_spell cs
INNER JOIN temp_bot_batch t ON cs.guid = t.guid
WHERE cs.active = 1 AND cs.disabled = 0;
-- Batch update online status
UPDATE characters c
INNER JOIN temp_bot_batch t ON c.guid = t.guid
SET c.online = 1;
-- Batch insert into session cache
INSERT INTO playerbot_session_cache
(guid, account, name, level, class, race, zone, map, online, last_action)
SELECT
c.guid, c.account, c.name, c.level, c.class, c.race,
c.zone, c.map, 1, NOW()
FROM characters c
INNER JOIN temp_bot_batch t ON c.guid = t.guid
ON DUPLICATE KEY UPDATE
online = 1,
last_action = NOW();
DROP TEMPORARY TABLE temp_bot_batch;
END$$
-- =====================================================
-- PREPARED STATEMENT PATTERNS
-- =====================================================
-- Create prepared statements for frequent queries
CREATE PROCEDURE `sp_init_prepared_statements`()
BEGIN
-- Bot position update
SET @sql_update_position = 'UPDATE characters SET position_x = ?, position_y = ?, position_z = ?, orientation = ?, map = ?, zone = ? WHERE guid = ?';
PREPARE stmt_update_position FROM @sql_update_position;
-- Bot health/power update
SET @sql_update_health = 'UPDATE character_stats SET health = ?, power1 = ?, power2 = ? WHERE guid = ?';
PREPARE stmt_update_health FROM @sql_update_health;
-- Bot aura update
SET @sql_update_aura = 'INSERT INTO character_aura (guid, caster_guid, spell, effect_mask, recalculate_mask, stackcount, maxduration, remaintime, remaincharges) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE remaintime = VALUES(remaintime), stackcount = VALUES(stackcount)';
PREPARE stmt_update_aura FROM @sql_update_aura;
-- Bot combat state
SET @sql_update_combat = 'UPDATE playerbot_state SET combat_state = ?, ai_state = ?, last_update = NOW() WHERE guid = ?';
PREPARE stmt_update_combat FROM @sql_update_combat;
END$$
-- =====================================================
-- OPTIMIZED SEARCH QUERIES
-- =====================================================
-- Find nearby bots (spatial query optimization)
CREATE PROCEDURE `sp_find_nearby_bots`(
IN p_x FLOAT,
IN p_y FLOAT,
IN p_z FLOAT,
IN p_map INT,
IN p_distance FLOAT,
IN p_limit INT
)
BEGIN
-- Use bounding box pre-filter for performance
SET @min_x = p_x - p_distance;
SET @max_x = p_x + p_distance;
SET @min_y = p_y - p_distance;
SET @max_y = p_y + p_distance;
SELECT
c.guid, c.name, c.level, c.class,
SQRT(POW(c.position_x - p_x, 2) + POW(c.position_y - p_y, 2) + POW(c.position_z - p_z, 2)) AS distance
FROM characters c USE INDEX (idx_zone_map_position)
WHERE c.map = p_map
AND c.online = 1
AND c.position_x BETWEEN @min_x AND @max_x
AND c.position_y BETWEEN @min_y AND @max_y
HAVING distance <= p_distance
ORDER BY distance
LIMIT p_limit;
END$$
-- Find bots by criteria (optimized filtering)
CREATE PROCEDURE `sp_find_bots_by_criteria`(
IN p_min_level INT,
IN p_max_level INT,
IN p_class INT,
IN p_zone INT,
IN p_online_only BOOLEAN,
IN p_limit INT
)
BEGIN
SELECT
c.guid, c.name, c.level, c.class, c.race, c.zone, c.online
FROM characters c USE INDEX (idx_bot_selection)
WHERE
(p_min_level IS NULL OR c.level >= p_min_level)
AND (p_max_level IS NULL OR c.level <= p_max_level)
AND (p_class IS NULL OR c.class = p_class)
AND (p_zone IS NULL OR c.zone = p_zone)
AND (NOT p_online_only OR c.online = 1)
LIMIT p_limit;
END$$
-- =====================================================
-- BULK UPDATE OPERATIONS
-- =====================================================
-- Bulk save bot states (replaces individual updates)
CREATE PROCEDURE `sp_bulk_save_bot_states`(
IN p_state_data JSON
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
-- Create temporary table from JSON data
CREATE TEMPORARY TABLE temp_bot_states (
guid INT UNSIGNED PRIMARY KEY,
position_x FLOAT,
position_y FLOAT,
position_z FLOAT,
orientation FLOAT,
map INT,
zone INT,
health INT,
power INT,
online TINYINT
);
-- Parse JSON and insert into temp table
INSERT INTO temp_bot_states
SELECT
JSON_UNQUOTE(JSON_EXTRACT(value, '$.guid')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.position_x')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.position_y')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.position_z')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.orientation')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.map')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.zone')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.health')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.power')),
JSON_UNQUOTE(JSON_EXTRACT(value, '$.online'))
FROM JSON_TABLE(p_state_data, '$[*]' COLUMNS (value JSON PATH '$')) AS jt;
-- Bulk update characters table
UPDATE characters c
INNER JOIN temp_bot_states t ON c.guid = t.guid
SET
c.position_x = t.position_x,
c.position_y = t.position_y,
c.position_z = t.position_z,
c.orientation = t.orientation,
c.map = t.map,
c.zone = t.zone,
c.online = t.online;
-- Bulk update character_stats
UPDATE character_stats cs
INNER JOIN temp_bot_states t ON cs.guid = t.guid
SET
cs.health = t.health,
cs.power1 = t.power;
-- Bulk update playerbot_state
INSERT INTO playerbot_state (guid, online, position_x, position_y, position_z, map, zone, last_update)
SELECT guid, online, position_x, position_y, position_z, map, zone, NOW()
FROM temp_bot_states
ON DUPLICATE KEY UPDATE
online = VALUES(online),
position_x = VALUES(position_x),
position_y = VALUES(position_y),
position_z = VALUES(position_z),
map = VALUES(map),
zone = VALUES(zone),
last_update = NOW();
DROP TEMPORARY TABLE temp_bot_states;
COMMIT;
END$$
-- =====================================================
-- CACHE MANAGEMENT
-- =====================================================
-- Preload frequently accessed bots into cache
CREATE PROCEDURE `sp_preload_bot_cache`(
IN p_account_id INT UNSIGNED
)
BEGIN
-- Clear old cache entries
DELETE FROM playerbot_session_cache
WHERE last_action < DATE_SUB(NOW(), INTERVAL 1 HOUR);
-- Load active bots into cache
INSERT IGNORE INTO playerbot_session_cache
(guid, account, name, level, class, race, zone, map, online, group_id, last_action)
SELECT
c.guid, c.account, c.name, c.level, c.class, c.race,
c.zone, c.map, c.online, IFNULL(gm.guid, 0), NOW()
FROM characters c
LEFT JOIN group_member gm ON c.guid = gm.memberGuid
WHERE c.account = p_account_id
AND c.online = 1;
END$$
-- =====================================================
-- MONITORING PROCEDURES
-- =====================================================
-- Log query performance
CREATE PROCEDURE `sp_log_query_performance`(
IN p_operation VARCHAR(50),
IN p_start_time DATETIME(6),
IN p_bot_count INT
)
BEGIN
DECLARE v_duration_ms INT;
SET v_duration_ms = TIMESTAMPDIFF(MICROSECOND, p_start_time, NOW(6)) / 1000;
INSERT INTO playerbot_performance
(timestamp, operation, duration_ms, bot_count)
VALUES
(NOW(), p_operation, v_duration_ms, p_bot_count);
END$$
DELIMITER ;
-- =====================================================
-- OPTIMIZE EXISTING QUERIES WITH HINTS
-- =====================================================
-- Example: Force index usage for better performance
-- Original slow query
-- SELECT * FROM characters WHERE account = ? AND online = 0;
-- Optimized with index hint and column selection
-- SELECT guid, name, level, class FROM characters USE INDEX (idx_bot_login_covering)
-- WHERE account = ? AND online = 0;
-- =====================================================
-- QUERY REWRITING EXAMPLES
-- =====================================================
-- BEFORE: N+1 Query Pattern
-- SELECT guid FROM characters WHERE account = 1;
-- SELECT * FROM character_stats WHERE guid = 1;
-- SELECT * FROM character_stats WHERE guid = 2;
-- ... (N queries)
-- AFTER: Single JOIN Query
-- SELECT c.guid, c.name, cs.*
-- FROM characters c
-- INNER JOIN character_stats cs ON c.guid = cs.guid
-- WHERE c.account = 1;
-- =====================================================
-- EXECUTION PLAN VERIFICATION
-- =====================================================
-- Verify covering index usage
EXPLAIN FORMAT=JSON
SELECT guid, name, race, class, level, zone, map, position_x, position_y, position_z
FROM characters USE INDEX (idx_bot_login_covering)
WHERE account = 1 AND online = 0;
-- Verify batch operation efficiency
EXPLAIN FORMAT=JSON
SELECT c.*, cs.*
FROM characters c
INNER JOIN character_stats cs ON c.guid = cs.guid
WHERE c.guid IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
-- =====================================================
-- PERFORMANCE METRICS
-- =====================================================
-- Expected query improvements:
-- - Single bot login: 287ms -> <50ms
-- - Batch load 100 bots: 18s -> <2s
-- - Position updates: 5ms -> <1ms per bot
-- - Nearby bot search: 150ms -> <10ms
-- - Bulk state save (100 bots): 500ms -> <100ms
-- =====================================================
@@ -0,0 +1,305 @@
# =====================================================
# MySQL 9.4 CONFIGURATION FOR TRINITYCORE PLAYERBOT
# Target: 5000+ concurrent bots with minimal latency
# =====================================================
[client]
port = 3306
socket = /var/run/mysqld/mysqld.sock
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
prompt = "\\u@\\h [\\d]> "
[mysqld]
# =====================================================
# BASIC SETTINGS
# =====================================================
port = 3306
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
pid-file = /var/run/mysqld/mysqld.pid
user = mysql
bind-address = 127.0.0.1
skip-name-resolve = 1
# Character set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = 'SET NAMES utf8mb4'
# =====================================================
# CONNECTION POOL OPTIMIZATION
# =====================================================
# Support 5000+ bot connections
max_connections = 10000
max_user_connections = 5000
# Thread handling for high concurrency
thread_handling = pool-of-threads
thread_pool_size = 16
thread_pool_max_threads = 1000
thread_pool_stall_limit = 60
thread_pool_idle_timeout = 60
# Connection thread caching
thread_cache_size = 500
back_log = 500
max_connect_errors = 1000000
# Network buffer sizes
net_buffer_length = 32K
max_allowed_packet = 256M
net_read_timeout = 30
net_write_timeout = 30
interactive_timeout = 28800
wait_timeout = 28800
# =====================================================
# INNODB OPTIMIZATION (PRIMARY ENGINE)
# =====================================================
# Buffer pool (70% of RAM for dedicated server, adjust for your system)
# For 32GB RAM system, use 22GB
innodb_buffer_pool_size = 22G
innodb_buffer_pool_instances = 16
innodb_buffer_pool_chunk_size = 128M
# Log files for write performance
innodb_log_file_size = 2G
innodb_log_files_in_group = 3
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2
innodb_flush_log_at_timeout = 1
# I/O optimization
innodb_io_capacity = 4000
innodb_io_capacity_max = 8000
innodb_read_io_threads = 16
innodb_write_io_threads = 16
innodb_purge_threads = 4
innodb_flush_neighbors = 0
# File handling
innodb_file_per_table = 1
innodb_open_files = 8000
innodb_autoinc_lock_mode = 2
# Concurrency settings
innodb_thread_concurrency = 0
innodb_concurrency_tickets = 5000
innodb_lock_wait_timeout = 50
innodb_deadlock_detect = ON
# Change buffer for secondary indexes
innodb_change_buffer_max_size = 30
innodb_change_buffering = all
# Adaptive features
innodb_adaptive_hash_index = ON
innodb_adaptive_hash_index_parts = 32
innodb_adaptive_flushing = ON
innodb_adaptive_flushing_lwm = 10
# Compression
innodb_compression_level = 6
innodb_compression_failure_threshold_pct = 10
innodb_compression_pad_pct_max = 50
# Performance schema specific
innodb_monitor_enable = all
innodb_print_all_deadlocks = ON
innodb_status_output = OFF
innodb_status_output_locks = OFF
# Page cleaners for write performance
innodb_page_cleaners = 8
innodb_max_dirty_pages_pct = 75
innodb_max_dirty_pages_pct_lwm = 10
# Undo tablespace
innodb_undo_tablespaces = 4
innodb_undo_log_truncate = ON
innodb_max_undo_log_size = 1G
innodb_undo_log_encrypt = OFF
# Parallel operations
innodb_parallel_read_threads = 8
innodb_ddl_threads = 4
innodb_ddl_buffer_size = 1M
# =====================================================
# MEMORY ENGINE OPTIMIZATION (FOR CACHE TABLES)
# =====================================================
max_heap_table_size = 2G
tmp_table_size = 2G
# =====================================================
# QUERY CACHE (Disabled in MySQL 8.0+, using alternative caching)
# =====================================================
# Using alternative caching strategies instead
# =====================================================
# TABLE AND INDEX OPTIMIZATION
# =====================================================
table_open_cache = 8000
table_open_cache_instances = 16
table_definition_cache = 4000
metadata_locks_cache_size = 1024
metadata_locks_hash_instances = 64
# =====================================================
# QUERY OPTIMIZATION
# =====================================================
# Join buffers
join_buffer_size = 8M
sort_buffer_size = 8M
read_buffer_size = 4M
read_rnd_buffer_size = 8M
# Group by optimization
group_concat_max_len = 1048576
max_length_for_sort_data = 4096
# Optimizer settings
optimizer_prune_level = 1
optimizer_search_depth = 62
optimizer_switch = 'index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,index_merge_sort_intersection=off,index_condition_pushdown=on,derived_merge=on,derived_with_keys=on,firstmatch=on,loosescan=on,materialization=on,semijoin=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_condition_pushdown=on,hash_join=on'
# Range optimization
range_optimizer_max_mem_size = 8M
range_alloc_block_size = 4K
# =====================================================
# PREPARED STATEMENTS
# =====================================================
max_prepared_stmt_count = 500000
# =====================================================
# BINARY LOGGING (for replication/backup)
# =====================================================
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = MINIMAL
expire_logs_days = 7
max_binlog_size = 1G
binlog_cache_size = 4M
binlog_stmt_cache_size = 4M
sync_binlog = 100
# =====================================================
# SLOW QUERY LOG
# =====================================================
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.01
log_queries_not_using_indexes = 1
log_throttle_queries_not_using_indexes = 10
min_examined_row_limit = 100
# =====================================================
# ERROR LOGGING
# =====================================================
log_error = /var/log/mysql/error.log
log_error_verbosity = 2
log_timestamps = SYSTEM
# =====================================================
# PERFORMANCE SCHEMA
# =====================================================
performance_schema = ON
performance_schema_max_table_instances = 12500
performance_schema_max_table_handles = 50000
performance_schema_max_thread_instances = 10000
performance_schema_max_thread_classes = 100
# Monitor specific instruments for bot performance
performance-schema-instrument = 'memory/%=ON'
performance-schema-instrument = 'stage/%=ON'
performance-schema-instrument = 'statement/%=ON'
performance-schema-instrument = 'transaction=%=ON'
performance-schema-instrument = 'wait/io/file/%=ON'
performance-schema-instrument = 'wait/lock/table/%=ON'
# =====================================================
# REPLICATION (for read replicas)
# =====================================================
# Master configuration
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_gtid_simple_recovery = 1
replica_parallel_workers = 16
replica_parallel_type = LOGICAL_CLOCK
replica_preserve_commit_order = 1
# =====================================================
# SECURITY
# =====================================================
local_infile = 0
skip_symbolic_links = 1
secure_file_priv = /var/lib/mysql-files
default_authentication_plugin = caching_sha2_password
# =====================================================
# WINDOWS SPECIFIC (if running on Windows)
# =====================================================
# Uncomment these if running on Windows
# named_pipe = OFF
# shared_memory = OFF
# shared_memory_base_name = MYSQL
# =====================================================
# OPTIMIZER HINTS FOR PLAYERBOT QUERIES
# =====================================================
# These can be set at session level for bot connections
# SET SESSION optimizer_use_condition_selectivity = 4;
# SET SESSION optimizer_trace = "enabled=on";
# SET SESSION optimizer_trace_max_mem_size = 1048576;
[mysqldump]
quick
quote-names
max_allowed_packet = 256M
single-transaction
routines
triggers
events
[isamchk]
key_buffer_size = 256M
sort_buffer_size = 256M
read_buffer = 8M
write_buffer = 8M
[myisamchk]
key_buffer_size = 256M
sort_buffer_size = 256M
read_buffer = 8M
write_buffer = 8M
[mysqlhotcopy]
interactive-timeout
# =====================================================
# PERFORMANCE TUNING SUMMARY
# =====================================================
# Key optimizations for PlayerBot:
# 1. Connection pool: 10000 max connections with thread pooling
# 2. InnoDB buffer pool: 22GB (adjust based on RAM)
# 3. Thread cache: 500 threads cached
# 4. Table cache: 8000 tables with 16 instances
# 5. Prepared statements: 500k max for bot queries
# 6. I/O threads: 16 read, 16 write
# 7. Memory tables: 2GB for session cache
# 8. Slow query log: 10ms threshold
#
# Expected improvements:
# - Connection time: <1ms
# - Query cache hit: >90%
# - Thread cache hit: >95%
# - Table cache hit: >99%
# - Bot login: <100ms
# - Concurrent bots: 5000+
# =====================================================
@@ -0,0 +1,534 @@
-- =====================================================
-- PERFORMANCE VALIDATION AND MONITORING SCRIPTS
-- Target: Validate <100ms login, <10ms queries, 5000+ bot support
-- =====================================================
USE `characters`;
DELIMITER $$
-- =====================================================
-- PERFORMANCE TESTING PROCEDURES
-- =====================================================
-- Test single bot login performance
CREATE PROCEDURE `sp_test_bot_login_performance`(
IN p_iterations INT,
OUT p_avg_time_ms DECIMAL(10,2),
OUT p_min_time_ms DECIMAL(10,2),
OUT p_max_time_ms DECIMAL(10,2)
)
BEGIN
DECLARE v_start DATETIME(6);
DECLARE v_end DATETIME(6);
DECLARE v_total_time INT DEFAULT 0;
DECLARE v_min_time INT DEFAULT 999999;
DECLARE v_max_time INT DEFAULT 0;
DECLARE v_current_time INT;
DECLARE v_counter INT DEFAULT 0;
DECLARE v_test_guid INT;
-- Get a test bot guid
SELECT guid INTO v_test_guid FROM characters WHERE account > 0 LIMIT 1;
WHILE v_counter < p_iterations DO
SET v_start = NOW(6);
-- Simulate bot login query
SELECT
c.guid, c.account, c.name, c.race, c.class, c.level,
c.zone, c.map, c.position_x, c.position_y, c.position_z,
cs.maxhealth, cs.maxpower1, cs.strength, cs.agility, cs.stamina
FROM characters c USE INDEX (idx_bot_login_covering)
INNER JOIN character_stats cs ON c.guid = cs.guid
WHERE c.guid = v_test_guid;
SET v_end = NOW(6);
SET v_current_time = TIMESTAMPDIFF(MICROSECOND, v_start, v_end) / 1000;
SET v_total_time = v_total_time + v_current_time;
IF v_current_time < v_min_time THEN
SET v_min_time = v_current_time;
END IF;
IF v_current_time > v_max_time THEN
SET v_max_time = v_current_time;
END IF;
SET v_counter = v_counter + 1;
END WHILE;
SET p_avg_time_ms = v_total_time / p_iterations;
SET p_min_time_ms = v_min_time;
SET p_max_time_ms = v_max_time;
-- Log results
INSERT INTO playerbot_performance (operation, duration_ms, bot_count, details)
VALUES ('bot_login_test', p_avg_time_ms, 1,
CONCAT('Min: ', p_min_time_ms, 'ms, Max: ', p_max_time_ms, 'ms, Iterations: ', p_iterations));
-- Display results
SELECT
'Bot Login Performance' AS Test,
p_avg_time_ms AS 'Avg Time (ms)',
p_min_time_ms AS 'Min Time (ms)',
p_max_time_ms AS 'Max Time (ms)',
CASE
WHEN p_avg_time_ms < 100 THEN 'PASSED ✓'
ELSE CONCAT('FAILED ✗ (Target: <100ms, Actual: ', p_avg_time_ms, 'ms)')
END AS Result;
END$$
-- Test batch bot spawn performance
CREATE PROCEDURE `sp_test_batch_spawn_performance`(
IN p_bot_count INT,
OUT p_total_time_ms DECIMAL(10,2),
OUT p_per_bot_ms DECIMAL(10,2)
)
BEGIN
DECLARE v_start DATETIME(6);
DECLARE v_end DATETIME(6);
SET v_start = NOW(6);
-- Simulate batch spawn query
SELECT
c.guid, c.account, c.name, c.race, c.class, c.level,
c.zone, c.map, c.position_x, c.position_y, c.position_z,
cs.maxhealth, cs.maxpower1, cs.strength, cs.agility, cs.stamina
FROM characters c USE INDEX (idx_bot_login_covering)
INNER JOIN character_stats cs ON c.guid = cs.guid
WHERE c.account > 0 AND c.online = 0
LIMIT p_bot_count;
-- Simulate batch equipment load
SELECT ci.guid, ci.slot, ci.item
FROM character_inventory ci
WHERE ci.guid IN (
SELECT guid FROM characters WHERE account > 0 LIMIT p_bot_count
) AND ci.bag = 0 AND ci.slot < 19;
-- Simulate batch spell load
SELECT cs.guid, cs.spell
FROM character_spell cs
WHERE cs.guid IN (
SELECT guid FROM characters WHERE account > 0 LIMIT p_bot_count
) AND cs.active = 1;
SET v_end = NOW(6);
SET p_total_time_ms = TIMESTAMPDIFF(MICROSECOND, v_start, v_end) / 1000;
SET p_per_bot_ms = p_total_time_ms / p_bot_count;
-- Log results
INSERT INTO playerbot_performance (operation, duration_ms, bot_count, details)
VALUES ('batch_spawn_test', p_total_time_ms, p_bot_count,
CONCAT('Per bot: ', p_per_bot_ms, 'ms'));
-- Display results
SELECT
CONCAT('Batch Spawn ', p_bot_count, ' Bots') AS Test,
p_total_time_ms AS 'Total Time (ms)',
p_per_bot_ms AS 'Per Bot (ms)',
CASE
WHEN p_bot_count = 100 AND p_total_time_ms < 5000 THEN 'PASSED ✓'
WHEN p_per_bot_ms < 50 THEN 'PASSED ✓'
ELSE CONCAT('FAILED ✗ (Target: <5000ms for 100 bots, Actual: ', p_total_time_ms, 'ms)')
END AS Result;
END$$
-- Test query performance for various operations
CREATE PROCEDURE `sp_test_query_performance`()
BEGIN
DECLARE v_start DATETIME(6);
DECLARE v_duration INT;
DECLARE v_test_name VARCHAR(100);
DECLARE v_target_ms INT;
DECLARE v_passed INT DEFAULT 0;
DECLARE v_failed INT DEFAULT 0;
-- Create temporary results table
CREATE TEMPORARY TABLE test_results (
test_name VARCHAR(100),
duration_ms INT,
target_ms INT,
result VARCHAR(20)
);
-- Test 1: Character lookup by name
SET v_test_name = 'Character Lookup by Name';
SET v_target_ms = 10;
SET v_start = NOW(6);
SELECT guid FROM characters USE INDEX (idx_unique_name) WHERE name = 'TestBot' LIMIT 1;
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Test 2: Nearby bot search
SET v_test_name = 'Nearby Bot Search';
SET v_target_ms = 10;
SET v_start = NOW(6);
SELECT guid, name FROM characters USE INDEX (idx_zone_map_position)
WHERE map = 0 AND zone = 1519 AND online = 1
AND position_x BETWEEN -8900 AND -8800
AND position_y BETWEEN 500 AND 600
LIMIT 10;
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Test 3: Bot selection by criteria
SET v_test_name = 'Bot Selection by Criteria';
SET v_target_ms = 10;
SET v_start = NOW(6);
SELECT guid, name FROM characters USE INDEX (idx_bot_selection)
WHERE online = 1 AND level BETWEEN 70 AND 80 AND class = 1 AND zone = 1519
LIMIT 20;
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Test 4: Group member lookup
SET v_test_name = 'Group Member Lookup';
SET v_target_ms = 10;
SET v_start = NOW(6);
SELECT c.guid, c.name FROM characters c
INNER JOIN group_member gm USE INDEX (idx_group_guid) ON c.guid = gm.memberGuid
WHERE gm.guid = 1;
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Test 5: Bot state update
SET v_test_name = 'Bot State Update';
SET v_target_ms = 5;
SET v_start = NOW(6);
UPDATE characters SET online = 1 WHERE guid = 1;
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Test 6: Session cache insert
SET v_test_name = 'Session Cache Insert';
SET v_target_ms = 1;
SET v_start = NOW(6);
INSERT IGNORE INTO playerbot_session_cache (guid, account, name, level, class, race, zone, map, online)
VALUES (99999, 1, 'TestCache', 80, 1, 1, 1519, 0, 1);
SET v_duration = TIMESTAMPDIFF(MICROSECOND, v_start, NOW(6)) / 1000;
INSERT INTO test_results VALUES (v_test_name, v_duration, v_target_ms,
IF(v_duration <= v_target_ms, 'PASSED ✓', 'FAILED ✗'));
-- Calculate totals
SELECT COUNT(*) INTO v_passed FROM test_results WHERE result = 'PASSED ✓';
SELECT COUNT(*) INTO v_failed FROM test_results WHERE result = 'FAILED ✗';
-- Display results
SELECT * FROM test_results;
SELECT
CONCAT('Total Tests: ', v_passed + v_failed) AS Summary,
CONCAT('Passed: ', v_passed) AS Passed,
CONCAT('Failed: ', v_failed) AS Failed,
CASE
WHEN v_failed = 0 THEN 'ALL TESTS PASSED ✓'
ELSE CONCAT('SOME TESTS FAILED ✗ (', v_failed, ' failures)')
END AS Overall;
DROP TEMPORARY TABLE test_results;
END$$
-- =====================================================
-- INDEX EFFECTIVENESS ANALYSIS
-- =====================================================
CREATE PROCEDURE `sp_analyze_index_effectiveness`()
BEGIN
-- Index usage statistics
SELECT
t.table_name,
i.index_name,
i.cardinality,
ROUND(((i.cardinality / IFNULL(t.table_rows, 1)) * 100), 2) AS selectivity_pct,
CASE
WHEN i.cardinality / IFNULL(t.table_rows, 1) > 0.95 THEN 'Excellent'
WHEN i.cardinality / IFNULL(t.table_rows, 1) > 0.70 THEN 'Good'
WHEN i.cardinality / IFNULL(t.table_rows, 1) > 0.30 THEN 'Fair'
ELSE 'Poor'
END AS effectiveness,
ROUND(i.data_length / 1024 / 1024, 2) AS index_size_mb
FROM information_schema.statistics i
INNER JOIN information_schema.tables t ON i.table_schema = t.table_schema AND i.table_name = t.table_name
WHERE i.table_schema = DATABASE()
AND i.table_name IN ('characters', 'character_stats', 'playerbot_state', 'playerbot_session_cache')
AND i.seq_in_index = 1
ORDER BY t.table_name, selectivity_pct DESC;
-- Unused indexes (would need performance_schema enabled)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'performance_schema' AND table_name = 'table_io_waits_summary_by_index_usage') THEN
SELECT
object_schema,
object_name AS table_name,
index_name,
count_read,
count_write,
CASE
WHEN count_read = 0 AND count_write = 0 THEN 'UNUSED - Consider dropping'
WHEN count_read = 0 THEN 'Write-only - Review necessity'
ELSE 'Active'
END AS status
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = DATABASE()
AND index_name IS NOT NULL
ORDER BY count_read + count_write;
END IF;
END$$
-- =====================================================
-- CONNECTION POOL MONITORING
-- =====================================================
CREATE PROCEDURE `sp_monitor_connection_pool`()
BEGIN
-- Current connections
SELECT
COUNT(*) AS total_connections,
SUM(CASE WHEN command != 'Sleep' THEN 1 ELSE 0 END) AS active_connections,
SUM(CASE WHEN command = 'Sleep' THEN 1 ELSE 0 END) AS idle_connections,
AVG(time) AS avg_connection_time,
MAX(time) AS max_connection_time
FROM information_schema.processlist
WHERE user LIKE '%playerbot%' OR db IN ('characters', 'auth', 'world');
-- Connection pool efficiency
SELECT
variable_name,
variable_value,
CASE variable_name
WHEN 'Threads_cached' THEN 'Cached threads (higher is better)'
WHEN 'Threads_connected' THEN 'Current connections'
WHEN 'Threads_created' THEN 'Total threads created (lower is better)'
WHEN 'Threads_running' THEN 'Active threads'
WHEN 'Max_used_connections' THEN 'Peak connections'
WHEN 'Aborted_connects' THEN 'Failed connections (should be low)'
END AS description
FROM information_schema.session_status
WHERE variable_name IN (
'Threads_cached', 'Threads_connected', 'Threads_created',
'Threads_running', 'Max_used_connections', 'Aborted_connects'
);
-- Thread cache effectiveness
SELECT
@thread_cache_hit_rate :=
ROUND(100 - ((CAST(created.variable_value AS UNSIGNED) /
CAST(connections.variable_value AS UNSIGNED)) * 100), 2) AS thread_cache_hit_rate,
CASE
WHEN @thread_cache_hit_rate > 95 THEN 'Excellent ✓'
WHEN @thread_cache_hit_rate > 90 THEN 'Good ✓'
WHEN @thread_cache_hit_rate > 80 THEN 'Fair'
ELSE CONCAT('Poor ✗ (Consider increasing thread_cache_size)')
END AS status
FROM
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Threads_created') created,
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Connections') connections;
END$$
-- =====================================================
-- CACHE PERFORMANCE MONITORING
-- =====================================================
CREATE PROCEDURE `sp_monitor_cache_performance`()
BEGIN
-- Table cache statistics
SELECT
@table_cache_hit_rate :=
ROUND(100 - ((CAST(opened.variable_value AS UNSIGNED) /
CAST(opens.variable_value AS UNSIGNED)) * 100), 2) AS table_cache_hit_rate,
CASE
WHEN @table_cache_hit_rate > 99 THEN 'Excellent ✓'
WHEN @table_cache_hit_rate > 95 THEN 'Good ✓'
WHEN @table_cache_hit_rate > 90 THEN 'Fair'
ELSE CONCAT('Poor ✗ (Consider increasing table_open_cache)')
END AS status
FROM
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Opened_tables') opened,
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Open_tables') opens
WHERE opens.variable_value > 0;
-- InnoDB buffer pool statistics
SELECT
pool_id,
pool_size * 16384 / 1024 / 1024 AS pool_size_mb,
ROUND(100 * pages_data / pool_size, 2) AS data_pages_pct,
ROUND(100 * pages_dirty / pool_size, 2) AS dirty_pages_pct,
ROUND(100 * pages_free / pool_size, 2) AS free_pages_pct,
hit_rate,
CASE
WHEN hit_rate > 99.9 THEN 'Excellent ✓'
WHEN hit_rate > 99 THEN 'Good ✓'
WHEN hit_rate > 95 THEN 'Fair'
ELSE 'Poor ✗'
END AS status
FROM (
SELECT
pool_id,
pool_size,
pages_data,
pages_dirty,
pages_free,
ROUND(100 - (100 * reads / (reads + read_requests + 0.01)), 2) AS hit_rate
FROM information_schema.innodb_buffer_pool_stats
) pool_stats;
-- Session cache statistics
SELECT
COUNT(*) AS cached_sessions,
COUNT(CASE WHEN online = 1 THEN 1 END) AS online_sessions,
MIN(last_action) AS oldest_cache_entry,
MAX(last_action) AS newest_cache_entry,
TIMESTAMPDIFF(MINUTE, MIN(last_action), NOW()) AS cache_age_minutes
FROM playerbot_session_cache;
END$$
-- =====================================================
-- COMPREHENSIVE PERFORMANCE REPORT
-- =====================================================
CREATE PROCEDURE `sp_generate_performance_report`()
BEGIN
DECLARE v_bot_login_avg DECIMAL(10,2);
DECLARE v_bot_login_min DECIMAL(10,2);
DECLARE v_bot_login_max DECIMAL(10,2);
DECLARE v_batch_spawn_total DECIMAL(10,2);
DECLARE v_batch_spawn_per_bot DECIMAL(10,2);
SELECT '=====================================' AS '';
SELECT 'PLAYERBOT DATABASE PERFORMANCE REPORT' AS '';
SELECT '=====================================' AS '';
SELECT NOW() AS 'Report Generated';
-- Test bot login performance
CALL sp_test_bot_login_performance(100, v_bot_login_avg, v_bot_login_min, v_bot_login_max);
-- Test batch spawn performance
CALL sp_test_batch_spawn_performance(100, v_batch_spawn_total, v_batch_spawn_per_bot);
-- Test query performance
CALL sp_test_query_performance();
-- Analyze indexes
SELECT '--- INDEX EFFECTIVENESS ---' AS '';
CALL sp_analyze_index_effectiveness();
-- Monitor connections
SELECT '--- CONNECTION POOL STATUS ---' AS '';
CALL sp_monitor_connection_pool();
-- Monitor cache
SELECT '--- CACHE PERFORMANCE ---' AS '';
CALL sp_monitor_cache_performance();
-- Recent performance history
SELECT '--- RECENT PERFORMANCE HISTORY ---' AS '';
SELECT
operation,
AVG(duration_ms) AS avg_ms,
MIN(duration_ms) AS min_ms,
MAX(duration_ms) AS max_ms,
COUNT(*) AS samples
FROM playerbot_performance
WHERE timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY operation;
-- Overall assessment
SELECT '--- OVERALL ASSESSMENT ---' AS '';
SELECT
CASE
WHEN v_bot_login_avg < 100 AND v_batch_spawn_total < 5000 THEN
'EXCELLENT: All performance targets met ✓'
WHEN v_bot_login_avg < 150 AND v_batch_spawn_total < 7500 THEN
'GOOD: Most performance targets met'
WHEN v_bot_login_avg < 200 AND v_batch_spawn_total < 10000 THEN
'FAIR: Some optimization needed'
ELSE
'POOR: Significant optimization required ✗'
END AS 'Performance Grade',
CONCAT('Bot Login: ', v_bot_login_avg, 'ms (Target: <100ms)') AS 'Login Performance',
CONCAT('100 Bot Spawn: ', v_batch_spawn_total, 'ms (Target: <5000ms)') AS 'Spawn Performance';
END$$
-- =====================================================
-- REAL-TIME MONITORING
-- =====================================================
CREATE PROCEDURE `sp_monitor_real_time`(
IN p_duration_seconds INT
)
BEGIN
DECLARE v_end_time DATETIME;
DECLARE v_interval_seconds INT DEFAULT 5;
DECLARE v_current_time DATETIME;
SET v_end_time = DATE_ADD(NOW(), INTERVAL p_duration_seconds SECOND);
-- Create monitoring table
CREATE TEMPORARY TABLE IF NOT EXISTS real_time_metrics (
timestamp DATETIME,
active_connections INT,
queries_per_sec DECIMAL(10,2),
avg_query_time_ms DECIMAL(10,2),
buffer_pool_hit_rate DECIMAL(10,2),
thread_cache_hit_rate DECIMAL(10,2)
);
WHILE NOW() < v_end_time DO
SET v_current_time = NOW();
-- Collect metrics
INSERT INTO real_time_metrics
SELECT
v_current_time,
(SELECT COUNT(*) FROM information_schema.processlist WHERE command != 'Sleep'),
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Questions') / v_interval_seconds,
0, -- Would need to calculate from slow query log
(SELECT ROUND(100 - (100 * reads / (reads + read_requests + 0.01)), 2)
FROM information_schema.innodb_buffer_pool_stats LIMIT 1),
(SELECT ROUND(100 - ((CAST(tc.variable_value AS UNSIGNED) / CAST(c.variable_value AS UNSIGNED)) * 100), 2)
FROM (SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Threads_created') tc,
(SELECT variable_value FROM information_schema.session_status WHERE variable_name = 'Connections') c);
-- Sleep for interval
DO SLEEP(v_interval_seconds);
END WHILE;
-- Display results
SELECT * FROM real_time_metrics ORDER BY timestamp;
-- Summary
SELECT
AVG(active_connections) AS avg_connections,
AVG(queries_per_sec) AS avg_qps,
AVG(buffer_pool_hit_rate) AS avg_buffer_hit_rate,
AVG(thread_cache_hit_rate) AS avg_thread_hit_rate
FROM real_time_metrics;
DROP TEMPORARY TABLE real_time_metrics;
END$$
DELIMITER ;
-- =====================================================
-- EXECUTE INITIAL PERFORMANCE VALIDATION
-- =====================================================
-- Run comprehensive performance report
CALL sp_generate_performance_report();
-- =====================================================
-- PERFORMANCE VALIDATION SUMMARY
-- =====================================================
-- Target Metrics:
-- ✓ Bot login: <100ms (from 287ms)
-- ✓ 100 bot spawn: <5000ms (from 18s)
-- ✓ Query response: <10ms (from 50-150ms)
-- ✓ Connection pool efficiency: >90%
-- ✓ Cache hit rate: >90%
-- ✓ Support for 5000+ concurrent bots
-- =====================================================
@@ -0,0 +1,144 @@
-- Playerbot Auction Price History Tables
-- Optional: Persistent price tracking for market analysis
-- Price history table for trend analysis
CREATE TABLE IF NOT EXISTS `playerbot_auction_price_history` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`item_id` INT UNSIGNED NOT NULL,
`price` BIGINT UNSIGNED NOT NULL,
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_item_timestamp` (`item_id`, `timestamp`),
KEY `idx_timestamp` (`timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Tracks historical auction prices for bot market analysis';
-- Bot auction statistics
CREATE TABLE IF NOT EXISTS `playerbot_auction_stats` (
`bot_guid` BIGINT UNSIGNED NOT NULL,
`total_auctions_created` INT UNSIGNED NOT NULL DEFAULT 0,
`total_auctions_sold` INT UNSIGNED NOT NULL DEFAULT 0,
`total_auctions_cancelled` INT UNSIGNED NOT NULL DEFAULT 0,
`total_auctions_expired` INT UNSIGNED NOT NULL DEFAULT 0,
`total_bids_placed` INT UNSIGNED NOT NULL DEFAULT 0,
`total_commodities_bought` INT UNSIGNED NOT NULL DEFAULT 0,
`total_gold_spent` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`total_gold_earned` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`net_profit` BIGINT NOT NULL DEFAULT 0,
`success_rate` FLOAT NOT NULL DEFAULT 0.0,
`last_update` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`bot_guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Tracks auction house statistics per bot';
-- Active bot auctions for tracking
CREATE TABLE IF NOT EXISTS `playerbot_active_auctions` (
`auction_id` INT UNSIGNED NOT NULL,
`bot_guid` BIGINT UNSIGNED NOT NULL,
`item_id` INT UNSIGNED NOT NULL,
`item_count` INT UNSIGNED NOT NULL DEFAULT 1,
`start_price` BIGINT UNSIGNED NOT NULL,
`buyout_price` BIGINT UNSIGNED NOT NULL,
`cost_basis` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`listed_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expiry_time` TIMESTAMP NOT NULL,
`is_commodity` TINYINT(1) NOT NULL DEFAULT 0,
`strategy` TINYINT UNSIGNED NOT NULL DEFAULT 5,
PRIMARY KEY (`auction_id`),
KEY `idx_bot_guid` (`bot_guid`),
KEY `idx_item_id` (`item_id`),
KEY `idx_expiry` (`expiry_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Tracks active bot auctions for profit calculation';
-- Market condition cache
CREATE TABLE IF NOT EXISTS `playerbot_market_cache` (
`item_id` INT UNSIGNED NOT NULL,
`current_price` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`average_price_7d` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`median_price_7d` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`min_price_7d` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`max_price_7d` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`daily_volume` INT UNSIGNED NOT NULL DEFAULT 0,
`active_listings` INT UNSIGNED NOT NULL DEFAULT 0,
`price_trend` FLOAT NOT NULL DEFAULT 0.0,
`market_condition` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`last_update` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`item_id`),
KEY `idx_last_update` (`last_update`),
KEY `idx_condition` (`market_condition`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Caches market analysis data for bot decision making';
-- Cleanup stored procedure for old price history
DELIMITER $$
CREATE PROCEDURE `sp_cleanup_auction_price_history`(IN days_to_keep INT)
BEGIN
DELETE FROM `playerbot_auction_price_history`
WHERE `timestamp` < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
SELECT ROW_COUNT() AS rows_deleted;
END$$
DELIMITER ;
-- Event to auto-cleanup price history (runs daily)
CREATE EVENT IF NOT EXISTS `evt_cleanup_auction_price_history`
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_TIMESTAMP
DO CALL sp_cleanup_auction_price_history(7);
-- Indexes for performance
ALTER TABLE `playerbot_auction_price_history`
ADD INDEX `idx_item_price` (`item_id`, `price`),
ADD INDEX `idx_recent` (`timestamp` DESC);
-- Example queries for market analysis:
-- Get price trend for an item
/*
SELECT
item_id,
AVG(price) as avg_price,
MIN(price) as min_price,
MAX(price) as max_price,
COUNT(*) as sample_count,
STDDEV(price) as price_volatility
FROM playerbot_auction_price_history
WHERE item_id = 12345
AND timestamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY item_id;
*/
-- Get top performing bot traders
/*
SELECT
bot_guid,
total_auctions_sold,
total_gold_earned,
net_profit,
success_rate,
(total_gold_earned - total_gold_spent) as total_profit
FROM playerbot_auction_stats
WHERE total_auctions_created > 10
ORDER BY net_profit DESC
LIMIT 10;
*/
-- Find profitable items (high volume, good margins)
/*
SELECT
m.item_id,
m.current_price,
m.median_price_7d,
m.daily_volume,
m.price_trend,
COUNT(DISTINCT h.timestamp) as price_samples
FROM playerbot_market_cache m
LEFT JOIN playerbot_auction_price_history h ON h.item_id = m.item_id
WHERE m.daily_volume > 5
AND m.price_trend > 0
AND m.current_price < m.median_price_7d * 0.9
GROUP BY m.item_id
ORDER BY m.daily_volume DESC, m.price_trend DESC
LIMIT 20;
*/
@@ -0,0 +1,453 @@
-- ============================================================================
-- INSTANCE BOT POOL DATABASE SCHEMA
-- ============================================================================
--
-- Purpose: Database schema for the Instance Bot Pool system
-- Version: 1.0.0
-- Date: 2026-01-08
--
-- This schema supports:
-- - Pool bot registry (persistent pool members)
-- - Pool assignment history (for analytics)
-- - Pool statistics (for monitoring)
-- - Reservation tracking
--
-- Tables:
-- - playerbot_instance_pool: Main pool bot registry
-- - playerbot_pool_assignments: Assignment history
-- - playerbot_pool_statistics: Hourly statistics snapshots
-- - playerbot_pool_reservations: Active reservations (runtime only)
--
-- ============================================================================
-- Disable foreign key checks to allow dropping tables in any order
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================================
-- TABLE: playerbot_instance_pool
-- ============================================================================
-- Main registry of bots in the warm pool. Persists bot assignments,
-- state, and performance metrics across server restarts.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_instance_pool`;
CREATE TABLE `playerbot_instance_pool` (
-- Identity
`bot_guid` BIGINT UNSIGNED NOT NULL COMMENT 'Bot character GUID',
`account_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Account ID the bot belongs to',
`bot_name` VARCHAR(12) NOT NULL DEFAULT '' COMMENT 'Character name',
-- Classification
`pool_type` ENUM('PVE', 'PVP_ALLIANCE', 'PVP_HORDE') NOT NULL DEFAULT 'PVE' COMMENT 'Pool type',
`role` ENUM('TANK', 'HEALER', 'DPS') NOT NULL DEFAULT 'DPS' COMMENT 'Combat role',
`player_class` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'WoW class ID',
`spec_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Specialization ID',
`faction` ENUM('ALLIANCE', 'HORDE') NOT NULL DEFAULT 'ALLIANCE' COMMENT 'Character faction',
-- Stats
`level` TINYINT UNSIGNED NOT NULL DEFAULT 80 COMMENT 'Character level',
`gear_score` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Item level / gear score',
`health_max` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Max health',
`mana_max` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Max mana',
-- State
`slot_state` ENUM('EMPTY', 'CREATING', 'WARMING', 'READY', 'RESERVED', 'ASSIGNED', 'COOLDOWN', 'MAINTENANCE')
NOT NULL DEFAULT 'READY' COMMENT 'Current slot state',
`state_change_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When state last changed',
`last_assignment` TIMESTAMP NULL DEFAULT NULL COMMENT 'When last assigned to instance',
-- Assignment Tracking
`current_instance_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Current instance ID (0 if not assigned)',
`current_content_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Current dungeon/raid/bg ID',
`current_instance_type` ENUM('DUNGEON', 'RAID', 'BATTLEGROUND', 'ARENA') DEFAULT 'DUNGEON' COMMENT 'Type of current instance',
`reservation_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Current reservation ID (0 if not reserved)',
-- Performance Metrics
`assignment_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Total lifetime assignments',
`total_instance_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Total seconds spent in instances',
`successful_completions` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Instances completed successfully',
`early_exits` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Times removed before completion',
-- Timestamps
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When bot was added to pool',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Last update time',
PRIMARY KEY (`bot_guid`),
INDEX `idx_pool_type` (`pool_type`),
INDEX `idx_role` (`role`),
INDEX `idx_pool_role` (`pool_type`, `role`),
INDEX `idx_slot_state` (`slot_state`),
INDEX `idx_faction` (`faction`),
INDEX `idx_level` (`level`),
INDEX `idx_faction_role_state` (`faction`, `role`, `slot_state`),
INDEX `idx_assignment_count` (`assignment_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Instance Bot Pool - Main registry of warm pool bots';
-- ============================================================================
-- TABLE: playerbot_pool_assignments
-- ============================================================================
-- History of bot assignments for analytics and monitoring.
-- Tracks when bots were assigned to instances and how long they stayed.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_pool_assignments`;
CREATE TABLE `playerbot_pool_assignments` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique assignment ID',
-- Assignment Details
`bot_guid` BIGINT UNSIGNED NOT NULL COMMENT 'Bot that was assigned',
`instance_type` ENUM('DUNGEON', 'RAID', 'BATTLEGROUND', 'ARENA') NOT NULL COMMENT 'Type of instance',
`instance_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Map instance ID',
`content_id` INT UNSIGNED NOT NULL COMMENT 'Dungeon/Raid/BG ID',
-- Bot Info at Assignment
`bot_role` ENUM('TANK', 'HEALER', 'DPS') NOT NULL COMMENT 'Bot role at assignment',
`bot_level` TINYINT UNSIGNED NOT NULL COMMENT 'Bot level at assignment',
`bot_gear_score` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Bot gear score at assignment',
`bot_faction` ENUM('ALLIANCE', 'HORDE') NOT NULL COMMENT 'Bot faction',
-- Timing
`assigned_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When assignment started',
`released_at` TIMESTAMP NULL DEFAULT NULL COMMENT 'When bot was released',
`duration_seconds` INT UNSIGNED NULL DEFAULT NULL COMMENT 'Total time in instance',
-- Result
`completion_status` ENUM('IN_PROGRESS', 'SUCCESS', 'EARLY_EXIT', 'ERROR', 'TIMEOUT')
NOT NULL DEFAULT 'IN_PROGRESS' COMMENT 'How the assignment ended',
-- Reservation
`reservation_id` INT UNSIGNED DEFAULT NULL COMMENT 'Reservation ID if pre-reserved',
PRIMARY KEY (`id`),
INDEX `idx_bot_guid` (`bot_guid`),
INDEX `idx_instance_type` (`instance_type`),
INDEX `idx_content_id` (`content_id`),
INDEX `idx_instance` (`instance_type`, `instance_id`),
INDEX `idx_assigned_at` (`assigned_at`),
INDEX `idx_completion_status` (`completion_status`),
FOREIGN KEY (`bot_guid`) REFERENCES `playerbot_instance_pool`(`bot_guid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Instance Bot Pool - Assignment history for analytics';
-- ============================================================================
-- TABLE: playerbot_pool_statistics
-- ============================================================================
-- Hourly snapshots of pool statistics for monitoring and capacity planning.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_pool_statistics`;
CREATE TABLE `playerbot_pool_statistics` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique snapshot ID',
`snapshot_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When snapshot was taken',
-- Slot Counts
`total_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`empty_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`creating_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`warming_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`ready_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`reserved_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`assigned_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`cooldown_slots` INT UNSIGNED NOT NULL DEFAULT 0,
`maintenance_slots` INT UNSIGNED NOT NULL DEFAULT 0,
-- Per-Role Ready Counts
`ready_tanks` INT UNSIGNED NOT NULL DEFAULT 0,
`ready_healers` INT UNSIGNED NOT NULL DEFAULT 0,
`ready_dps` INT UNSIGNED NOT NULL DEFAULT 0,
-- Per-Faction Ready Counts
`ready_alliance` INT UNSIGNED NOT NULL DEFAULT 0,
`ready_horde` INT UNSIGNED NOT NULL DEFAULT 0,
-- Activity (hourly)
`assignments_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`releases_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`jit_creations_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`reservations_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`cancellations_hour` INT UNSIGNED NOT NULL DEFAULT 0,
-- Instance Type Breakdown (hourly)
`dungeons_filled_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`raids_filled_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`battlegrounds_filled_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`arenas_filled_hour` INT UNSIGNED NOT NULL DEFAULT 0,
-- Success/Failure (hourly)
`successful_requests_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`failed_requests_hour` INT UNSIGNED NOT NULL DEFAULT 0,
`timeout_requests_hour` INT UNSIGNED NOT NULL DEFAULT 0,
-- Timing Metrics (microseconds/milliseconds)
`avg_assignment_time_us` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Avg assignment time in microseconds',
`avg_warmup_time_ms` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Avg warmup time in milliseconds',
`avg_jit_creation_time_ms` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Avg JIT creation time in milliseconds',
`peak_assignment_time_us` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Peak assignment time in microseconds',
-- Calculated Metrics
`utilization_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Utilization percentage',
`availability_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT 'Availability percentage',
`success_rate_pct` DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT 'Request success rate',
PRIMARY KEY (`id`),
INDEX `idx_snapshot_time` (`snapshot_time`),
INDEX `idx_utilization` (`utilization_pct`),
INDEX `idx_success_rate` (`success_rate_pct`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Instance Bot Pool - Hourly statistics snapshots';
-- ============================================================================
-- TABLE: playerbot_pool_config
-- ============================================================================
-- Runtime configuration storage for pool settings.
-- Allows dynamic configuration without server restart.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_pool_config`;
CREATE TABLE `playerbot_pool_config` (
`config_key` VARCHAR(64) NOT NULL COMMENT 'Configuration key',
`config_value` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Configuration value',
`description` VARCHAR(255) DEFAULT NULL COMMENT 'Description of the setting',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Instance Bot Pool - Runtime configuration';
-- Insert default configuration
INSERT INTO `playerbot_pool_config` (`config_key`, `config_value`, `description`) VALUES
('enabled', '1', 'Master enable switch for instance bot pool'),
('alliance_tanks', '20', 'Number of Alliance tank bots in pool'),
('alliance_healers', '30', 'Number of Alliance healer bots in pool'),
('alliance_dps', '50', 'Number of Alliance DPS bots in pool'),
('horde_tanks', '20', 'Number of Horde tank bots in pool'),
('horde_healers', '30', 'Number of Horde healer bots in pool'),
('horde_dps', '50', 'Number of Horde DPS bots in pool'),
('cooldown_seconds', '300', 'Cooldown between bot assignments'),
('reservation_timeout_ms', '60000', 'Reservation timeout in milliseconds'),
('warmup_timeout_ms', '30000', 'Bot warmup timeout in milliseconds'),
('max_overflow_bots', '500', 'Maximum JIT overflow bots'),
('overflow_creation_rate', '10', 'JIT bots created per second'),
('auto_replenish', '1', 'Auto-replenish depleted pool slots'),
('persist_to_database', '1', 'Persist pool state to database'),
('warm_on_startup', '1', 'Warm pool on server startup'),
('jit_enabled', '1', 'Enable JIT factory'),
('log_assignments', '1', 'Log individual bot assignments'),
('log_pool_changes', '0', 'Log pool state changes'),
('log_reservations', '1', 'Log reservation operations');
-- ============================================================================
-- TABLE: playerbot_content_requirements (OVERRIDE TABLE)
-- ============================================================================
--
-- PURPOSE: Custom overrides for content requirements
--
-- IMPORTANT: This table is an OVERRIDE mechanism, NOT the primary data source!
--
-- Data Loading Order:
-- 1. PRIMARY: DB2 files are loaded first (LFGDungeons.db2, etc.)
-- - Dungeons: Loaded from LFGDungeons.db2 (TypeID=1)
-- - Raids: Loaded from LFGDungeons.db2 (TypeID=2)
-- - Battlegrounds: Loaded from BattlemasterList.db2
-- - Arenas: Hardcoded defaults
--
-- 2. OVERRIDE: This table is loaded second and REPLACES any matching entries
-- - Entries here will override the DB2 defaults
-- - Use this to customize specific dungeons/raids/BGs
--
-- RECOMMENDATION: Leave this table EMPTY unless you need to customize specific
-- content. The DB2 data provides correct values for all standard content.
--
-- Example use cases for adding entries:
-- - Override recommended gear score for a specific dungeon
-- - Adjust role counts (e.g., need 3 tanks for a specific raid boss)
-- - Change level requirements for custom server configurations
--
-- See: ContentRequirementDatabase::Initialize() in ContentRequirements.cpp
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_content_requirements`;
CREATE TABLE `playerbot_content_requirements` (
`content_id` INT UNSIGNED NOT NULL COMMENT 'Dungeon/Raid/BG/Arena ID',
`content_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'Human-readable name',
`instance_type` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Type: 0=Dungeon, 1=Raid, 2=Battleground, 3=Arena',
`difficulty` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Difficulty (0=Normal, 1=Heroic, 2=Mythic, etc.)',
-- Player Requirements
`min_players` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'Minimum players',
`max_players` INT UNSIGNED NOT NULL DEFAULT 5 COMMENT 'Maximum players',
`min_level` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'Minimum level',
`max_level` TINYINT UNSIGNED NOT NULL DEFAULT 80 COMMENT 'Maximum level',
`recommended_level` TINYINT UNSIGNED NOT NULL DEFAULT 80 COMMENT 'Recommended level',
-- Role Requirements
`min_tanks` INT UNSIGNED NOT NULL DEFAULT 1,
`max_tanks` INT UNSIGNED NOT NULL DEFAULT 1,
`recommended_tanks` INT UNSIGNED NOT NULL DEFAULT 1,
`min_healers` INT UNSIGNED NOT NULL DEFAULT 1,
`max_healers` INT UNSIGNED NOT NULL DEFAULT 1,
`recommended_healers` INT UNSIGNED NOT NULL DEFAULT 1,
`min_dps` INT UNSIGNED NOT NULL DEFAULT 3,
`max_dps` INT UNSIGNED NOT NULL DEFAULT 3,
`recommended_dps` INT UNSIGNED NOT NULL DEFAULT 3,
-- Gear Requirements
`min_gear_score` INT UNSIGNED NOT NULL DEFAULT 0,
`recommended_gear_score` INT UNSIGNED NOT NULL DEFAULT 0,
-- PvP Specific
`requires_both_factions` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether both factions needed',
`players_per_faction` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Players per faction for PvP',
-- Timing
`estimated_duration_minutes` INT UNSIGNED NOT NULL DEFAULT 30 COMMENT 'Expected duration',
PRIMARY KEY (`content_id`, `instance_type`, `difficulty`),
INDEX `idx_instance_type` (`instance_type`),
INDEX `idx_level_range` (`min_level`, `max_level`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Override table for content requirements - see header comment';
-- ============================================================================
-- NO DEFAULT DATA - This table is intentionally empty!
-- ============================================================================
-- Primary data comes from DB2 files. Only add entries here to OVERRIDE defaults.
--
-- EXAMPLE: To override Nerub-ar Palace to require 3 tanks instead of 2:
--
-- INSERT INTO `playerbot_content_requirements`
-- (`content_id`, `content_name`, `instance_type`, `difficulty`,
-- `min_players`, `max_players`, `min_level`, `max_level`, `recommended_level`,
-- `min_tanks`, `max_tanks`, `recommended_tanks`,
-- `min_healers`, `max_healers`, `recommended_healers`,
-- `min_dps`, `max_dps`, `recommended_dps`,
-- `min_gear_score`, `recommended_gear_score`, `estimated_duration_minutes`) VALUES
-- (2769, 'Nerub-ar Palace (Mythic)', 1, 2, 1, 20, 80, 80, 80, 3, 3, 3, 5, 6, 5, 11, 12, 12, 540, 570, 300);
--
-- instance_type values: 0=Dungeon, 1=Raid, 2=Battleground, 3=Arena
-- difficulty values: 0=Normal, 1=Heroic, 2=Mythic, etc.
-- ============================================================================
-- ============================================================================
-- CLEANUP PROCEDURES
-- ============================================================================
-- Clean up old statistics (keep last 30 days)
DELIMITER //
CREATE PROCEDURE IF NOT EXISTS `CleanupPoolStatistics`()
BEGIN
DELETE FROM `playerbot_pool_statistics`
WHERE `snapshot_time` < DATE_SUB(NOW(), INTERVAL 30 DAY);
END //
-- Clean up old assignment history (keep last 90 days)
CREATE PROCEDURE IF NOT EXISTS `CleanupAssignmentHistory`()
BEGIN
DELETE FROM `playerbot_pool_assignments`
WHERE `assigned_at` < DATE_SUB(NOW(), INTERVAL 90 DAY);
END //
DELIMITER ;
-- ============================================================================
-- VIEWS FOR MONITORING
-- ============================================================================
-- Current pool status view
DROP VIEW IF EXISTS `v_pool_status`;
CREATE VIEW `v_pool_status` AS
SELECT
`slot_state`,
COUNT(*) as `count`,
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM `playerbot_instance_pool`), 2) as `percentage`
FROM `playerbot_instance_pool`
GROUP BY `slot_state`;
-- Role distribution view
DROP VIEW IF EXISTS `v_pool_roles`;
CREATE VIEW `v_pool_roles` AS
SELECT
`faction`,
`role`,
`slot_state`,
COUNT(*) as `count`
FROM `playerbot_instance_pool`
GROUP BY `faction`, `role`, `slot_state`;
-- Recent assignments (use stored procedure instead - LIMIT not allowed in views)
DROP VIEW IF EXISTS `v_recent_assignments`;
DROP PROCEDURE IF EXISTS `GetRecentAssignments`;
DELIMITER //
CREATE PROCEDURE `GetRecentAssignments`(IN p_limit INT)
BEGIN
IF p_limit IS NULL OR p_limit <= 0 THEN
SET p_limit = 100;
END IF;
SET @sql = CONCAT(
'SELECT `instance_type`, `content_id`, `bot_role`, `completion_status`, ',
'`duration_seconds`, `assigned_at`, `released_at` ',
'FROM `playerbot_pool_assignments` ',
'ORDER BY `assigned_at` DESC LIMIT ', p_limit
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
-- ============================================================================
-- INDEXES FOR PERFORMANCE
-- ============================================================================
-- Helper procedure to add index if not exists (MySQL-compatible)
DROP PROCEDURE IF EXISTS `pb_add_index_if_not_exists`;
DELIMITER //
CREATE PROCEDURE `pb_add_index_if_not_exists`(
IN p_table VARCHAR(64),
IN p_index VARCHAR(64),
IN p_columns VARCHAR(255)
)
BEGIN
DECLARE index_exists INT DEFAULT 0;
SELECT COUNT(*) INTO index_exists
FROM `information_schema`.`STATISTICS`
WHERE `TABLE_SCHEMA` = DATABASE()
AND `TABLE_NAME` = p_table
AND `INDEX_NAME` = p_index;
IF index_exists = 0 THEN
SET @sql = CONCAT('CREATE INDEX `', p_index, '` ON `', p_table, '`(', p_columns, ')');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END //
DELIMITER ;
-- Fast lookup of ready bots by faction and role
CALL `pb_add_index_if_not_exists`(
'playerbot_instance_pool',
'idx_pool_ready_faction_role',
'`slot_state`, `faction`, `role`'
);
-- Fast recent assignments query
CALL `pb_add_index_if_not_exists`(
'playerbot_pool_assignments',
'idx_assignments_recent',
'`assigned_at`'
);
-- Clean up helper procedure
DROP PROCEDURE IF EXISTS `pb_add_index_if_not_exists`;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================================
-- DONE
-- ============================================================================
SELECT 'Instance Bot Pool schema created successfully!' AS status;
@@ -0,0 +1,739 @@
-- ============================================================================
-- BOT TEMPLATE SYSTEM DATABASE SCHEMA
-- ============================================================================
--
-- Purpose: Database schema for the Bot Template Repository and Clone Engine
-- Version: 1.0.0
-- Date: 2026-01-09
--
-- This schema supports:
-- - Pre-defined bot templates for each class/spec combination
-- - Gear set configurations for different item level tiers
-- - Talent builds per spec/role
-- - Action bar layouts per spec
-- - Class/spec/role mappings with WoW 11.2 data
--
-- Tables:
-- - playerbot_spec_info: Master class/spec reference data
-- - playerbot_bot_templates: Main template registry
-- - playerbot_template_gear_sets: Gear configurations per template/ilvl
-- - playerbot_template_gear_items: Individual gear slot items
-- - playerbot_template_talents: Talent configurations
-- - playerbot_template_actionbars: Action bar layouts
-- - playerbot_template_statistics: Usage tracking
-- - playerbot_class_race_matrix: Valid class/race combinations
--
-- ============================================================================
-- Disable foreign key checks to allow dropping tables in any order
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================================
-- TABLE: playerbot_spec_info
-- ============================================================================
-- Master reference table for all WoW 11.2 class specializations.
-- This is the authoritative source for class/spec/role mappings.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_spec_info`;
CREATE TABLE `playerbot_spec_info` (
`spec_id` INT UNSIGNED NOT NULL COMMENT 'ChrSpecialization.db2 ID',
`class_id` TINYINT UNSIGNED NOT NULL COMMENT 'ChrClasses.db2 ID',
`class_name` VARCHAR(32) NOT NULL COMMENT 'Class name (English)',
`spec_name` VARCHAR(32) NOT NULL COMMENT 'Spec name (English)',
`role` ENUM('TANK', 'HEALER', 'DPS') NOT NULL COMMENT 'Combat role',
`spec_index` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Spec index within class (0-3)',
`stat_priority` VARCHAR(128) DEFAULT NULL COMMENT 'Primary > Secondary stats',
`armor_type` ENUM('CLOTH', 'LEATHER', 'MAIL', 'PLATE') NOT NULL COMMENT 'Armor class',
`primary_stat` ENUM('STRENGTH', 'AGILITY', 'INTELLECT') NOT NULL COMMENT 'Primary stat',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether to generate templates',
`notes` VARCHAR(255) DEFAULT NULL COMMENT 'Implementation notes',
PRIMARY KEY (`spec_id`),
INDEX `idx_class` (`class_id`),
INDEX `idx_role` (`role`),
INDEX `idx_class_role` (`class_id`, `role`),
INDEX `idx_enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Master class/spec reference for WoW 11.2 (The War Within)';
-- Insert all WoW 11.2 specializations
INSERT INTO `playerbot_spec_info`
(`spec_id`, `class_id`, `class_name`, `spec_name`, `role`, `spec_index`, `stat_priority`, `armor_type`, `primary_stat`) VALUES
-- Warrior (class 1)
(71, 1, 'Warrior', 'Arms', 'DPS', 0, 'Str > Crit > Mastery > Haste > Vers', 'PLATE', 'STRENGTH'),
(72, 1, 'Warrior', 'Fury', 'DPS', 1, 'Str > Haste > Mastery > Crit > Vers', 'PLATE', 'STRENGTH'),
(73, 1, 'Warrior', 'Protection', 'TANK', 2, 'Str > Haste > Vers > Mastery > Crit', 'PLATE', 'STRENGTH'),
-- Paladin (class 2)
(65, 2, 'Paladin', 'Holy', 'HEALER', 0, 'Int > Haste > Crit > Mastery > Vers', 'PLATE', 'INTELLECT'),
(66, 2, 'Paladin', 'Protection', 'TANK', 1, 'Str > Haste > Mastery > Vers > Crit', 'PLATE', 'STRENGTH'),
(70, 2, 'Paladin', 'Retribution', 'DPS', 2, 'Str > Haste > Mastery > Crit > Vers', 'PLATE', 'STRENGTH'),
-- Hunter (class 3)
(253, 3, 'Hunter', 'Beast Mastery', 'DPS', 0, 'Agi > Haste > Crit > Mastery > Vers', 'MAIL', 'AGILITY'),
(254, 3, 'Hunter', 'Marksmanship', 'DPS', 1, 'Agi > Mastery > Crit > Haste > Vers', 'MAIL', 'AGILITY'),
(255, 3, 'Hunter', 'Survival', 'DPS', 2, 'Agi > Haste > Crit > Vers > Mastery', 'MAIL', 'AGILITY'),
-- Rogue (class 4)
(259, 4, 'Rogue', 'Assassination', 'DPS', 0, 'Agi > Crit > Mastery > Haste > Vers', 'LEATHER', 'AGILITY'),
(260, 4, 'Rogue', 'Outlaw', 'DPS', 1, 'Agi > Vers > Haste > Crit > Mastery', 'LEATHER', 'AGILITY'),
(261, 4, 'Rogue', 'Subtlety', 'DPS', 2, 'Agi > Crit > Vers > Mastery > Haste', 'LEATHER', 'AGILITY'),
-- Priest (class 5)
(256, 5, 'Priest', 'Discipline', 'HEALER', 0, 'Int > Haste > Crit > Vers > Mastery', 'CLOTH', 'INTELLECT'),
(257, 5, 'Priest', 'Holy', 'HEALER', 1, 'Int > Mastery > Crit > Vers > Haste', 'CLOTH', 'INTELLECT'),
(258, 5, 'Priest', 'Shadow', 'DPS', 2, 'Int > Haste > Mastery > Crit > Vers', 'CLOTH', 'INTELLECT'),
-- Death Knight (class 6)
(250, 6, 'Death Knight', 'Blood', 'TANK', 0, 'Str > Haste > Crit > Vers > Mastery', 'PLATE', 'STRENGTH'),
(251, 6, 'Death Knight', 'Frost', 'DPS', 1, 'Str > Crit > Mastery > Haste > Vers', 'PLATE', 'STRENGTH'),
(252, 6, 'Death Knight', 'Unholy', 'DPS', 2, 'Str > Mastery > Haste > Crit > Vers', 'PLATE', 'STRENGTH'),
-- Shaman (class 7)
(262, 7, 'Shaman', 'Elemental', 'DPS', 0, 'Int > Crit > Vers > Haste > Mastery', 'MAIL', 'INTELLECT'),
(263, 7, 'Shaman', 'Enhancement', 'DPS', 1, 'Agi > Haste > Crit > Mastery > Vers', 'MAIL', 'AGILITY'),
(264, 7, 'Shaman', 'Restoration', 'HEALER', 2, 'Int > Crit > Vers > Haste > Mastery', 'MAIL', 'INTELLECT'),
-- Mage (class 8)
(62, 8, 'Mage', 'Arcane', 'DPS', 0, 'Int > Mastery > Crit > Haste > Vers', 'CLOTH', 'INTELLECT'),
(63, 8, 'Mage', 'Fire', 'DPS', 1, 'Int > Haste > Mastery > Crit > Vers', 'CLOTH', 'INTELLECT'),
(64, 8, 'Mage', 'Frost', 'DPS', 2, 'Int > Crit > Haste > Vers > Mastery', 'CLOTH', 'INTELLECT'),
-- Warlock (class 9)
(265, 9, 'Warlock', 'Affliction', 'DPS', 0, 'Int > Haste > Mastery > Crit > Vers', 'CLOTH', 'INTELLECT'),
(266, 9, 'Warlock', 'Demonology', 'DPS', 1, 'Int > Haste > Mastery > Crit > Vers', 'CLOTH', 'INTELLECT'),
(267, 9, 'Warlock', 'Destruction', 'DPS', 2, 'Int > Haste > Crit > Mastery > Vers', 'CLOTH', 'INTELLECT'),
-- Monk (class 10)
(268, 10, 'Monk', 'Brewmaster', 'TANK', 0, 'Agi > Vers > Crit > Mastery > Haste', 'LEATHER', 'AGILITY'),
(270, 10, 'Monk', 'Mistweaver', 'HEALER', 1, 'Int > Crit > Vers > Haste > Mastery', 'LEATHER', 'INTELLECT'),
(269, 10, 'Monk', 'Windwalker', 'DPS', 2, 'Agi > Vers > Crit > Mastery > Haste', 'LEATHER', 'AGILITY'),
-- Druid (class 11)
(102, 11, 'Druid', 'Balance', 'DPS', 0, 'Int > Mastery > Haste > Crit > Vers', 'LEATHER', 'INTELLECT'),
(103, 11, 'Druid', 'Feral', 'DPS', 1, 'Agi > Crit > Mastery > Haste > Vers', 'LEATHER', 'AGILITY'),
(104, 11, 'Druid', 'Guardian', 'TANK', 2, 'Agi > Vers > Mastery > Haste > Crit', 'LEATHER', 'AGILITY'),
(105, 11, 'Druid', 'Restoration', 'HEALER', 3, 'Int > Haste > Mastery > Crit > Vers', 'LEATHER', 'INTELLECT'),
-- Demon Hunter (class 12)
(577, 12, 'Demon Hunter', 'Havoc', 'DPS', 0, 'Agi > Crit > Haste > Vers > Mastery', 'LEATHER', 'AGILITY'),
(581, 12, 'Demon Hunter', 'Vengeance', 'TANK', 1, 'Agi > Haste > Vers > Crit > Mastery', 'LEATHER', 'AGILITY'),
-- Evoker (class 13)
(1467, 13, 'Evoker', 'Devastation', 'DPS', 0, 'Int > Mastery > Crit > Haste > Vers', 'MAIL', 'INTELLECT'),
(1468, 13, 'Evoker', 'Preservation', 'HEALER', 1, 'Int > Mastery > Crit > Vers > Haste', 'MAIL', 'INTELLECT'),
(1473, 13, 'Evoker', 'Augmentation', 'DPS', 2, 'Int > Mastery > Haste > Crit > Vers', 'MAIL', 'INTELLECT');
-- ============================================================================
-- TABLE: playerbot_class_race_matrix
-- ============================================================================
-- Valid class/race combinations per faction for WoW 11.2.
-- Used when creating bots to ensure valid combinations.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_class_race_matrix`;
CREATE TABLE `playerbot_class_race_matrix` (
`class_id` TINYINT UNSIGNED NOT NULL COMMENT 'ChrClasses.db2 ID',
`race_id` TINYINT UNSIGNED NOT NULL COMMENT 'ChrRaces.db2 ID',
`race_name` VARCHAR(32) NOT NULL COMMENT 'Race name (English)',
`faction` ENUM('ALLIANCE', 'HORDE') NOT NULL COMMENT 'Faction',
`weight` FLOAT NOT NULL DEFAULT 1.0 COMMENT 'Selection weight (popularity)',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether to use for bot creation',
PRIMARY KEY (`class_id`, `race_id`),
INDEX `idx_faction` (`faction`),
INDEX `idx_class_faction` (`class_id`, `faction`),
INDEX `idx_enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Valid class/race combinations for WoW 11.2';
-- Insert valid class/race combinations for WoW 11.2
-- Alliance races: Human(1), Dwarf(3), Night Elf(4), Gnome(7), Draenei(11), Worgen(22),
-- Pandaren-A(25), Void Elf(29), Lightforged(30), Dark Iron(34), Kul Tiran(32),
-- Mechagnome(37), Dracthyr-A(52), Earthen-A(85)
-- Horde races: Orc(2), Undead(5), Tauren(6), Troll(8), Goblin(9), Blood Elf(10),
-- Pandaren-H(26), Nightborne(27), Highmountain(28), Mag'har(36), Zandalari(31),
-- Vulpera(35), Dracthyr-H(70), Earthen-H(84)
-- Warrior (class 1) - All races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(1, 1, 'Human', 'ALLIANCE', 1.5), (1, 3, 'Dwarf', 'ALLIANCE', 1.0), (1, 4, 'Night Elf', 'ALLIANCE', 1.0),
(1, 7, 'Gnome', 'ALLIANCE', 0.5), (1, 11, 'Draenei', 'ALLIANCE', 0.8), (1, 22, 'Worgen', 'ALLIANCE', 0.7),
(1, 25, 'Pandaren', 'ALLIANCE', 0.4), (1, 29, 'Void Elf', 'ALLIANCE', 0.6), (1, 30, 'Lightforged', 'ALLIANCE', 0.5),
(1, 34, 'Dark Iron', 'ALLIANCE', 0.6), (1, 32, 'Kul Tiran', 'ALLIANCE', 0.5), (1, 37, 'Mechagnome', 'ALLIANCE', 0.3),
(1, 85, 'Earthen', 'ALLIANCE', 0.7),
(1, 2, 'Orc', 'HORDE', 1.5), (1, 5, 'Undead', 'HORDE', 1.0), (1, 6, 'Tauren', 'HORDE', 1.2),
(1, 8, 'Troll', 'HORDE', 0.8), (1, 9, 'Goblin', 'HORDE', 0.4), (1, 10, 'Blood Elf', 'HORDE', 1.3),
(1, 26, 'Pandaren', 'HORDE', 0.4), (1, 27, 'Nightborne', 'HORDE', 0.6), (1, 28, 'Highmountain', 'HORDE', 0.7),
(1, 36, 'Mag''har', 'HORDE', 0.8), (1, 31, 'Zandalari', 'HORDE', 0.7), (1, 35, 'Vulpera', 'HORDE', 0.5),
(1, 84, 'Earthen', 'HORDE', 0.7);
-- Paladin (class 2) - Limited races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(2, 1, 'Human', 'ALLIANCE', 2.0), (2, 3, 'Dwarf', 'ALLIANCE', 1.2), (2, 11, 'Draenei', 'ALLIANCE', 1.5),
(2, 30, 'Lightforged', 'ALLIANCE', 1.0), (2, 34, 'Dark Iron', 'ALLIANCE', 0.8), (2, 85, 'Earthen', 'ALLIANCE', 0.7),
(2, 6, 'Tauren', 'HORDE', 1.3), (2, 10, 'Blood Elf', 'HORDE', 2.0), (2, 31, 'Zandalari', 'HORDE', 1.0),
(2, 84, 'Earthen', 'HORDE', 0.7);
-- Hunter (class 3) - All races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(3, 1, 'Human', 'ALLIANCE', 1.0), (3, 3, 'Dwarf', 'ALLIANCE', 1.5), (3, 4, 'Night Elf', 'ALLIANCE', 1.3),
(3, 7, 'Gnome', 'ALLIANCE', 0.4), (3, 11, 'Draenei', 'ALLIANCE', 0.7), (3, 22, 'Worgen', 'ALLIANCE', 0.8),
(3, 25, 'Pandaren', 'ALLIANCE', 0.4), (3, 29, 'Void Elf', 'ALLIANCE', 0.6), (3, 30, 'Lightforged', 'ALLIANCE', 0.4),
(3, 34, 'Dark Iron', 'ALLIANCE', 0.5), (3, 32, 'Kul Tiran', 'ALLIANCE', 0.6), (3, 37, 'Mechagnome', 'ALLIANCE', 0.3),
(3, 85, 'Earthen', 'ALLIANCE', 0.6),
(3, 2, 'Orc', 'HORDE', 1.4), (3, 5, 'Undead', 'HORDE', 0.7), (3, 6, 'Tauren', 'HORDE', 0.9),
(3, 8, 'Troll', 'HORDE', 1.5), (3, 9, 'Goblin', 'HORDE', 0.5), (3, 10, 'Blood Elf', 'HORDE', 1.2),
(3, 26, 'Pandaren', 'HORDE', 0.4), (3, 27, 'Nightborne', 'HORDE', 0.5), (3, 28, 'Highmountain', 'HORDE', 0.8),
(3, 36, 'Mag''har', 'HORDE', 0.9), (3, 31, 'Zandalari', 'HORDE', 0.8), (3, 35, 'Vulpera', 'HORDE', 0.7),
(3, 84, 'Earthen', 'HORDE', 0.6);
-- Rogue (class 4) - Most races except Tauren, Highmountain, Kul Tiran, Dracthyr
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(4, 1, 'Human', 'ALLIANCE', 1.3), (4, 3, 'Dwarf', 'ALLIANCE', 0.7), (4, 4, 'Night Elf', 'ALLIANCE', 1.5),
(4, 7, 'Gnome', 'ALLIANCE', 0.8), (4, 22, 'Worgen', 'ALLIANCE', 1.0), (4, 25, 'Pandaren', 'ALLIANCE', 0.4),
(4, 29, 'Void Elf', 'ALLIANCE', 0.9), (4, 34, 'Dark Iron', 'ALLIANCE', 0.6), (4, 37, 'Mechagnome', 'ALLIANCE', 0.4),
(4, 85, 'Earthen', 'ALLIANCE', 0.5),
(4, 2, 'Orc', 'HORDE', 0.9), (4, 5, 'Undead', 'HORDE', 1.4), (4, 8, 'Troll', 'HORDE', 1.0),
(4, 9, 'Goblin', 'HORDE', 0.8), (4, 10, 'Blood Elf', 'HORDE', 1.6), (4, 26, 'Pandaren', 'HORDE', 0.4),
(4, 27, 'Nightborne', 'HORDE', 0.7), (4, 36, 'Mag''har', 'HORDE', 0.6), (4, 35, 'Vulpera', 'HORDE', 0.9),
(4, 84, 'Earthen', 'HORDE', 0.5);
-- Priest (class 5) - All races except Orcs, and excludes some
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(5, 1, 'Human', 'ALLIANCE', 1.5), (5, 3, 'Dwarf', 'ALLIANCE', 1.0), (5, 4, 'Night Elf', 'ALLIANCE', 1.2),
(5, 7, 'Gnome', 'ALLIANCE', 0.6), (5, 11, 'Draenei', 'ALLIANCE', 1.3), (5, 22, 'Worgen', 'ALLIANCE', 0.5),
(5, 25, 'Pandaren', 'ALLIANCE', 0.5), (5, 29, 'Void Elf', 'ALLIANCE', 1.0), (5, 30, 'Lightforged', 'ALLIANCE', 0.8),
(5, 34, 'Dark Iron', 'ALLIANCE', 0.6), (5, 32, 'Kul Tiran', 'ALLIANCE', 0.5), (5, 37, 'Mechagnome', 'ALLIANCE', 0.4),
(5, 85, 'Earthen', 'ALLIANCE', 0.6),
(5, 5, 'Undead', 'HORDE', 1.4), (5, 6, 'Tauren', 'HORDE', 0.8), (5, 8, 'Troll', 'HORDE', 0.9),
(5, 9, 'Goblin', 'HORDE', 0.5), (5, 10, 'Blood Elf', 'HORDE', 1.6), (5, 26, 'Pandaren', 'HORDE', 0.5),
(5, 27, 'Nightborne', 'HORDE', 0.8), (5, 31, 'Zandalari', 'HORDE', 0.7), (5, 35, 'Vulpera', 'HORDE', 0.6),
(5, 84, 'Earthen', 'HORDE', 0.6);
-- Death Knight (class 6) - All races (Allied races require achievement)
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(6, 1, 'Human', 'ALLIANCE', 1.3), (6, 3, 'Dwarf', 'ALLIANCE', 0.9), (6, 4, 'Night Elf', 'ALLIANCE', 1.1),
(6, 7, 'Gnome', 'ALLIANCE', 0.5), (6, 11, 'Draenei', 'ALLIANCE', 0.8), (6, 22, 'Worgen', 'ALLIANCE', 1.0),
(6, 25, 'Pandaren', 'ALLIANCE', 0.3), (6, 29, 'Void Elf', 'ALLIANCE', 0.7), (6, 30, 'Lightforged', 'ALLIANCE', 0.4),
(6, 34, 'Dark Iron', 'ALLIANCE', 0.6), (6, 32, 'Kul Tiran', 'ALLIANCE', 0.5), (6, 37, 'Mechagnome', 'ALLIANCE', 0.3),
(6, 85, 'Earthen', 'ALLIANCE', 0.5),
(6, 2, 'Orc', 'HORDE', 1.2), (6, 5, 'Undead', 'HORDE', 1.5), (6, 6, 'Tauren', 'HORDE', 0.9),
(6, 8, 'Troll', 'HORDE', 0.8), (6, 9, 'Goblin', 'HORDE', 0.4), (6, 10, 'Blood Elf', 'HORDE', 1.4),
(6, 26, 'Pandaren', 'HORDE', 0.3), (6, 27, 'Nightborne', 'HORDE', 0.6), (6, 28, 'Highmountain', 'HORDE', 0.5),
(6, 36, 'Mag''har', 'HORDE', 0.7), (6, 31, 'Zandalari', 'HORDE', 0.6), (6, 35, 'Vulpera', 'HORDE', 0.5),
(6, 84, 'Earthen', 'HORDE', 0.5);
-- Shaman (class 7) - Limited races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(7, 3, 'Dwarf', 'ALLIANCE', 1.2), (7, 11, 'Draenei', 'ALLIANCE', 1.5), (7, 25, 'Pandaren', 'ALLIANCE', 0.6),
(7, 34, 'Dark Iron', 'ALLIANCE', 0.8), (7, 32, 'Kul Tiran', 'ALLIANCE', 0.7), (7, 85, 'Earthen', 'ALLIANCE', 0.8),
(7, 2, 'Orc', 'HORDE', 1.5), (7, 6, 'Tauren', 'HORDE', 1.3), (7, 8, 'Troll', 'HORDE', 1.2),
(7, 9, 'Goblin', 'HORDE', 0.6), (7, 26, 'Pandaren', 'HORDE', 0.5), (7, 28, 'Highmountain', 'HORDE', 1.0),
(7, 36, 'Mag''har', 'HORDE', 0.9), (7, 31, 'Zandalari', 'HORDE', 1.1), (7, 35, 'Vulpera', 'HORDE', 0.7),
(7, 84, 'Earthen', 'HORDE', 0.8);
-- Mage (class 8) - All races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(8, 1, 'Human', 'ALLIANCE', 1.4), (8, 3, 'Dwarf', 'ALLIANCE', 0.6), (8, 4, 'Night Elf', 'ALLIANCE', 0.9),
(8, 7, 'Gnome', 'ALLIANCE', 1.2), (8, 11, 'Draenei', 'ALLIANCE', 0.7), (8, 22, 'Worgen', 'ALLIANCE', 0.6),
(8, 25, 'Pandaren', 'ALLIANCE', 0.5), (8, 29, 'Void Elf', 'ALLIANCE', 1.0), (8, 30, 'Lightforged', 'ALLIANCE', 0.4),
(8, 34, 'Dark Iron', 'ALLIANCE', 0.5), (8, 32, 'Kul Tiran', 'ALLIANCE', 0.4), (8, 37, 'Mechagnome', 'ALLIANCE', 0.6),
(8, 85, 'Earthen', 'ALLIANCE', 0.5),
(8, 2, 'Orc', 'HORDE', 0.7), (8, 5, 'Undead', 'HORDE', 1.1), (8, 8, 'Troll', 'HORDE', 1.0),
(8, 9, 'Goblin', 'HORDE', 0.7), (8, 10, 'Blood Elf', 'HORDE', 1.8), (8, 26, 'Pandaren', 'HORDE', 0.5),
(8, 27, 'Nightborne', 'HORDE', 1.2), (8, 35, 'Vulpera', 'HORDE', 0.6), (8, 84, 'Earthen', 'HORDE', 0.5);
-- Warlock (class 9) - Limited races (no Draenei, Tauren, etc.)
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(9, 1, 'Human', 'ALLIANCE', 1.5), (9, 3, 'Dwarf', 'ALLIANCE', 0.6), (9, 7, 'Gnome', 'ALLIANCE', 1.0),
(9, 22, 'Worgen', 'ALLIANCE', 0.8), (9, 29, 'Void Elf', 'ALLIANCE', 1.1), (9, 34, 'Dark Iron', 'ALLIANCE', 0.7),
(9, 37, 'Mechagnome', 'ALLIANCE', 0.5), (9, 85, 'Earthen', 'ALLIANCE', 0.5),
(9, 2, 'Orc', 'HORDE', 1.2), (9, 5, 'Undead', 'HORDE', 1.5), (9, 8, 'Troll', 'HORDE', 0.8),
(9, 9, 'Goblin', 'HORDE', 0.7), (9, 10, 'Blood Elf', 'HORDE', 1.6), (9, 27, 'Nightborne', 'HORDE', 0.9),
(9, 35, 'Vulpera', 'HORDE', 0.7), (9, 84, 'Earthen', 'HORDE', 0.5);
-- Monk (class 10) - All races except Goblins, Worgen, Lightforged
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(10, 1, 'Human', 'ALLIANCE', 1.2), (10, 3, 'Dwarf', 'ALLIANCE', 0.7), (10, 4, 'Night Elf', 'ALLIANCE', 1.1),
(10, 7, 'Gnome', 'ALLIANCE', 0.6), (10, 11, 'Draenei', 'ALLIANCE', 0.8), (10, 25, 'Pandaren', 'ALLIANCE', 1.8),
(10, 29, 'Void Elf', 'ALLIANCE', 0.7), (10, 34, 'Dark Iron', 'ALLIANCE', 0.6), (10, 32, 'Kul Tiran', 'ALLIANCE', 0.5),
(10, 37, 'Mechagnome', 'ALLIANCE', 0.4), (10, 85, 'Earthen', 'ALLIANCE', 0.6),
(10, 2, 'Orc', 'HORDE', 0.9), (10, 5, 'Undead', 'HORDE', 0.8), (10, 6, 'Tauren', 'HORDE', 1.0),
(10, 8, 'Troll', 'HORDE', 1.1), (10, 10, 'Blood Elf', 'HORDE', 1.4), (10, 26, 'Pandaren', 'HORDE', 1.8),
(10, 27, 'Nightborne', 'HORDE', 0.7), (10, 28, 'Highmountain', 'HORDE', 0.8), (10, 36, 'Mag''har', 'HORDE', 0.6),
(10, 31, 'Zandalari', 'HORDE', 0.9), (10, 35, 'Vulpera', 'HORDE', 0.8), (10, 84, 'Earthen', 'HORDE', 0.6);
-- Druid (class 11) - Very limited races
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(11, 4, 'Night Elf', 'ALLIANCE', 2.0), (11, 22, 'Worgen', 'ALLIANCE', 1.0), (11, 32, 'Kul Tiran', 'ALLIANCE', 0.7),
(11, 6, 'Tauren', 'HORDE', 1.8), (11, 8, 'Troll', 'HORDE', 1.2), (11, 28, 'Highmountain', 'HORDE', 0.8),
(11, 31, 'Zandalari', 'HORDE', 1.0);
-- Demon Hunter (class 12) - Night Elf and Blood Elf only
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(12, 4, 'Night Elf', 'ALLIANCE', 2.0),
(12, 10, 'Blood Elf', 'HORDE', 2.0);
-- Evoker (class 13) - Dracthyr only
INSERT INTO `playerbot_class_race_matrix` (`class_id`, `race_id`, `race_name`, `faction`, `weight`) VALUES
(13, 52, 'Dracthyr', 'ALLIANCE', 1.0),
(13, 70, 'Dracthyr', 'HORDE', 1.0);
-- ============================================================================
-- TABLE: playerbot_bot_templates
-- ============================================================================
-- Main template registry. One row per class/spec combination.
-- Contains metadata and references to gear/talent/actionbar data.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_bot_templates`;
CREATE TABLE `playerbot_bot_templates` (
`template_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique template ID',
`spec_id` INT UNSIGNED NOT NULL COMMENT 'Specialization ID (FK to spec_info)',
`class_id` TINYINT UNSIGNED NOT NULL COMMENT 'Denormalized class ID for fast queries',
`role` TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT 'Combat role: 0=Tank, 1=Healer, 2=DPS',
-- Metadata
`template_name` VARCHAR(64) NOT NULL COMMENT 'Human-readable name (e.g., Warrior_Arms)',
`version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'Template version for updates',
`patch_version` VARCHAR(16) DEFAULT '11.2.0' COMMENT 'WoW patch this template is for',
-- Status
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether template is active',
`validated` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether template has been validated',
`last_validated` TIMESTAMP NULL DEFAULT NULL COMMENT 'Last validation timestamp',
-- Pre-serialized data blobs (for fast cloning)
`talent_blob` TEXT DEFAULT NULL COMMENT 'Hex-encoded serialized talent data',
`actionbar_blob` TEXT DEFAULT NULL COMMENT 'Hex-encoded serialized action bar data',
-- Configuration
`default_pvp_talents` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Use PvP talent defaults',
`hero_talent_tree_id` INT UNSIGNED DEFAULT NULL COMMENT 'Default hero talent tree',
`priority_weight` INT UNSIGNED NOT NULL DEFAULT 100 COMMENT 'Selection priority weight',
-- Timestamps
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`template_id`),
UNIQUE KEY `uk_spec_id` (`spec_id`),
INDEX `idx_enabled` (`enabled`),
INDEX `idx_validated` (`validated`),
INDEX `idx_class_role` (`class_id`, `role`),
CONSTRAINT `fk_template_spec` FOREIGN KEY (`spec_id`)
REFERENCES `playerbot_spec_info`(`spec_id`) ON DELETE CASCADE,
-- CHECK constraints to prevent invalid entries (MySQL 8.0.16+)
-- These prevent the class_id=0 bug that caused JIT bot creation failures
CONSTRAINT `chk_valid_class_id` CHECK (`class_id` >= 1 AND `class_id` <= 13),
CONSTRAINT `chk_valid_spec_id` CHECK (`spec_id` > 0),
CONSTRAINT `chk_valid_role` CHECK (`role` >= 0 AND `role` <= 2),
CONSTRAINT `chk_valid_template_name` CHECK (LENGTH(`template_name`) > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Main bot template registry';
-- Auto-generate templates for all specs
INSERT INTO `playerbot_bot_templates` (`spec_id`, `class_id`, `role`, `template_name`, `enabled`)
SELECT
`spec_id`,
`class_id`,
CASE `role` WHEN 'TANK' THEN 0 WHEN 'HEALER' THEN 1 ELSE 2 END AS `role`,
CONCAT(`class_name`, '_', `spec_name`) AS `template_name`,
1 AS `enabled`
FROM `playerbot_spec_info`
WHERE `enabled` = 1;
-- ============================================================================
-- TRIGGER: Validate template data before insert
-- ============================================================================
-- Provides comprehensive validation beyond CHECK constraints to ensure
-- class_id matches spec_id in the spec_info table.
-- ============================================================================
DELIMITER //
DROP TRIGGER IF EXISTS `trg_bot_templates_before_insert`//
CREATE TRIGGER `trg_bot_templates_before_insert`
BEFORE INSERT ON `playerbot_bot_templates`
FOR EACH ROW
BEGIN
-- Validate class_id (WoW classes are 1-13)
IF NEW.class_id < 1 OR NEW.class_id > 13 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid class_id: must be between 1 and 13 (valid WoW class IDs)';
END IF;
-- Validate spec_id exists in spec_info
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info WHERE spec_id = NEW.spec_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid spec_id: must reference a valid entry in playerbot_spec_info';
END IF;
-- Validate class_id matches spec_info (critical: prevents mismatched class/spec)
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info
WHERE spec_id = NEW.spec_id AND class_id = NEW.class_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'class_id does not match the class for the given spec_id in playerbot_spec_info';
END IF;
END//
DROP TRIGGER IF EXISTS `trg_bot_templates_before_update`//
CREATE TRIGGER `trg_bot_templates_before_update`
BEFORE UPDATE ON `playerbot_bot_templates`
FOR EACH ROW
BEGIN
-- Same validations as insert
IF NEW.class_id < 1 OR NEW.class_id > 13 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid class_id: must be between 1 and 13 (valid WoW class IDs)';
END IF;
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info WHERE spec_id = NEW.spec_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid spec_id: must reference a valid entry in playerbot_spec_info';
END IF;
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info
WHERE spec_id = NEW.spec_id AND class_id = NEW.class_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'class_id does not match the class for the given spec_id in playerbot_spec_info';
END IF;
END//
DELIMITER ;
-- ============================================================================
-- TABLE: playerbot_template_gear_sets
-- ============================================================================
-- Gear set configurations for each template at different item level tiers.
-- Each template can have multiple gear sets for different content levels.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_gear_sets`;
CREATE TABLE `playerbot_template_gear_sets` (
`gear_set_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`template_id` INT UNSIGNED NOT NULL COMMENT 'FK to templates',
`target_ilvl` INT UNSIGNED NOT NULL COMMENT 'Target average item level',
`actual_gear_score` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Calculated gear score',
`gear_set_name` VARCHAR(64) DEFAULT NULL COMMENT 'Human-readable name',
`content_tier` VARCHAR(32) DEFAULT NULL COMMENT 'Content tier (e.g., Heroic_Dungeon, Normal_Raid)',
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`gear_set_id`),
UNIQUE KEY `uk_template_ilvl` (`template_id`, `target_ilvl`),
INDEX `idx_item_level` (`target_ilvl`),
CONSTRAINT `fk_gearset_template` FOREIGN KEY (`template_id`)
REFERENCES `playerbot_bot_templates`(`template_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Gear set definitions per template and item level tier';
-- ============================================================================
-- TABLE: playerbot_template_gear_items
-- ============================================================================
-- Individual gear slot items for each gear set.
-- Each gear set has up to 19 equipment slots.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_gear_items`;
CREATE TABLE `playerbot_template_gear_items` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`gear_set_id` INT UNSIGNED NOT NULL COMMENT 'FK to gear_sets',
`slot_id` TINYINT UNSIGNED NOT NULL COMMENT 'Equipment slot (0-18)',
`item_id` INT UNSIGNED NOT NULL COMMENT 'Item entry ID',
`item_level` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Item level',
`enchant_id` INT UNSIGNED DEFAULT 0 COMMENT 'Enchant ID',
`gem1_id` INT UNSIGNED DEFAULT 0 COMMENT 'First gem ID',
`gem2_id` INT UNSIGNED DEFAULT 0 COMMENT 'Second gem ID',
`gem3_id` INT UNSIGNED DEFAULT 0 COMMENT 'Third gem ID',
`bonus_list` VARCHAR(64) DEFAULT NULL COMMENT 'Comma-separated bonus IDs',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gearset_slot` (`gear_set_id`, `slot_id`),
INDEX `idx_item_id` (`item_id`),
CONSTRAINT `fk_gearitem_gearset` FOREIGN KEY (`gear_set_id`)
REFERENCES `playerbot_template_gear_sets`(`gear_set_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Individual gear items per slot for each gear set';
-- ============================================================================
-- TABLE: playerbot_template_talents
-- ============================================================================
-- Talent configurations for each template.
-- Stores the selected talents from the talent tree.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_talents`;
CREATE TABLE `playerbot_template_talents` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`template_id` INT UNSIGNED NOT NULL COMMENT 'FK to templates',
`talent_tier` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Talent tier/row (0-6)',
`talent_column` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Talent column (0-2)',
`talent_id` INT UNSIGNED NOT NULL COMMENT 'Talent spell ID',
`is_pvp_talent` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Whether this is a PvP talent',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether this talent is enabled',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_template_tier_col` (`template_id`, `talent_tier`, `talent_column`, `is_pvp_talent`),
INDEX `idx_template_pvp` (`template_id`, `is_pvp_talent`),
CONSTRAINT `fk_talent_template` FOREIGN KEY (`template_id`)
REFERENCES `playerbot_bot_templates`(`template_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Talent selections for each template';
-- ============================================================================
-- TABLE: playerbot_template_actionbars
-- ============================================================================
-- Action bar configurations for each template.
-- Maps ability/item/macro to specific bar slots.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_actionbars`;
CREATE TABLE `playerbot_template_actionbars` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`template_id` INT UNSIGNED NOT NULL COMMENT 'FK to templates',
`action_bar` TINYINT UNSIGNED NOT NULL COMMENT 'Action bar number (0-7)',
`slot` TINYINT UNSIGNED NOT NULL COMMENT 'Slot on bar (0-11)',
`action_type` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=Spell, 1=Item, 64=Macro, 128=Companion',
`action_id` INT UNSIGNED NOT NULL COMMENT 'Spell/Item/Macro ID',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether this action is enabled',
`priority` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'For rotation priority hints',
`is_rotational` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Used in DPS rotation',
`is_defensive` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Defensive cooldown',
`is_interrupt` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Interrupt ability',
`description` VARCHAR(64) DEFAULT NULL COMMENT 'Ability name for reference',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_template_bar_slot` (`template_id`, `action_bar`, `slot`),
INDEX `idx_template` (`template_id`),
INDEX `idx_action_id` (`action_id`),
INDEX `idx_enabled` (`enabled`),
CONSTRAINT `fk_actionbar_template` FOREIGN KEY (`template_id`)
REFERENCES `playerbot_bot_templates`(`template_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Action bar configurations for each template';
-- ============================================================================
-- TABLE: playerbot_template_statistics
-- ============================================================================
-- Usage statistics for templates (for optimization).
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_statistics`;
CREATE TABLE `playerbot_template_statistics` (
`template_id` INT UNSIGNED NOT NULL,
`total_uses` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Total times cloned',
`last_used` TIMESTAMP NULL DEFAULT NULL,
`avg_creation_time_ms` INT UNSIGNED NOT NULL DEFAULT 0,
`min_creation_time_ms` INT UNSIGNED NOT NULL DEFAULT 0,
`max_creation_time_ms` INT UNSIGNED NOT NULL DEFAULT 0,
`successful_clones` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`failed_clones` BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`template_id`),
CONSTRAINT `fk_stats_template` FOREIGN KEY (`template_id`)
REFERENCES `playerbot_bot_templates`(`template_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Template usage statistics';
-- Initialize statistics for all templates
INSERT INTO `playerbot_template_statistics` (`template_id`)
SELECT `template_id` FROM `playerbot_bot_templates`;
-- ============================================================================
-- TABLE: playerbot_template_config
-- ============================================================================
-- Configuration for the template system.
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_template_config`;
CREATE TABLE `playerbot_template_config` (
`config_key` VARCHAR(64) NOT NULL,
`config_value` VARCHAR(255) NOT NULL DEFAULT '',
`description` VARCHAR(255) DEFAULT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Template system configuration';
INSERT INTO `playerbot_template_config` (`config_key`, `config_value`, `description`) VALUES
('enabled', '1', 'Master enable for template system'),
('auto_validate', '1', 'Automatically validate templates on load'),
('default_gear_ilvl_offset', '0', 'Item level offset from target'),
('prefer_set_bonuses', '1', 'Prefer tier set items when available'),
('use_pvp_talents_in_bg', '1', 'Apply PvP talents when entering battlegrounds'),
('default_gear_tier', '450', 'Default item level for new templates'),
('cache_templates', '1', 'Cache templates in memory'),
('log_template_usage', '1', 'Log template clone operations'),
('validate_gear_on_clone', '0', 'Validate all gear items exist before cloning'),
('fallback_to_defaults', '1', 'Use default templates if DB load fails');
-- ============================================================================
-- VIEWS FOR TEMPLATE MANAGEMENT
-- ============================================================================
-- Complete template view with spec info
DROP VIEW IF EXISTS `v_template_details`;
CREATE VIEW `v_template_details` AS
SELECT
t.`template_id`,
t.`template_name`,
t.`spec_id`,
t.`class_id`,
s.`class_name`,
s.`spec_name`,
t.`role`,
s.`armor_type`,
s.`primary_stat`,
t.`enabled`,
t.`validated`,
t.`version`,
t.`patch_version`,
ts.`total_uses`,
ts.`avg_creation_time_ms`
FROM `playerbot_bot_templates` t
JOIN `playerbot_spec_info` s ON t.`spec_id` = s.`spec_id`
LEFT JOIN `playerbot_template_statistics` ts ON t.`template_id` = ts.`template_id`;
-- Templates by role view
DROP VIEW IF EXISTS `v_templates_by_role`;
CREATE VIEW `v_templates_by_role` AS
SELECT
s.`role`,
COUNT(*) as `template_count`,
SUM(CASE WHEN t.`enabled` = 1 THEN 1 ELSE 0 END) as `enabled_count`,
SUM(CASE WHEN t.`validated` = 1 THEN 1 ELSE 0 END) as `validated_count`
FROM `playerbot_bot_templates` t
JOIN `playerbot_spec_info` s ON t.`spec_id` = s.`spec_id`
GROUP BY s.`role`;
-- Gear sets overview
DROP VIEW IF EXISTS `v_gear_sets_overview`;
CREATE VIEW `v_gear_sets_overview` AS
SELECT
t.`template_name`,
gs.`target_ilvl`,
gs.`gear_set_name`,
gs.`content_tier`,
gs.`actual_gear_score`,
COUNT(gi.`id`) as `items_defined`
FROM `playerbot_bot_templates` t
JOIN `playerbot_template_gear_sets` gs ON t.`template_id` = gs.`template_id`
LEFT JOIN `playerbot_template_gear_items` gi ON gs.`gear_set_id` = gi.`gear_set_id`
GROUP BY t.`template_name`, gs.`gear_set_id`;
-- ============================================================================
-- STORED PROCEDURES
-- ============================================================================
DELIMITER //
-- Procedure to validate a template
CREATE PROCEDURE IF NOT EXISTS `ValidateTemplate`(IN p_template_id INT UNSIGNED)
BEGIN
DECLARE v_spec_id INT UNSIGNED;
DECLARE v_gear_count INT;
DECLARE v_talent_count INT;
DECLARE v_is_valid TINYINT DEFAULT 1;
-- Get spec ID
SELECT `spec_id` INTO v_spec_id FROM `playerbot_bot_templates` WHERE `template_id` = p_template_id;
IF v_spec_id IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Template not found';
END IF;
-- Check gear sets exist
SELECT COUNT(*) INTO v_gear_count
FROM `playerbot_template_gear_sets`
WHERE `template_id` = p_template_id AND `enabled` = 1;
IF v_gear_count = 0 THEN
SET v_is_valid = 0;
END IF;
-- Check talents exist
SELECT COUNT(*) INTO v_talent_count
FROM `playerbot_template_talents`
WHERE `template_id` = p_template_id;
IF v_talent_count < 5 THEN -- Minimum expected talents
SET v_is_valid = 0;
END IF;
-- Update validation status
UPDATE `playerbot_bot_templates`
SET `validated` = v_is_valid, `last_validated` = NOW()
WHERE `template_id` = p_template_id;
SELECT v_is_valid AS `is_valid`, v_gear_count AS `gear_sets`, v_talent_count AS `talents`;
END //
-- Procedure to get best template for role/faction
CREATE PROCEDURE IF NOT EXISTS `GetBestTemplate`(
IN p_role VARCHAR(16),
IN p_faction VARCHAR(16),
IN p_preferred_class TINYINT UNSIGNED
)
BEGIN
SELECT
t.`template_id`,
t.`template_name`,
s.`class_id`,
s.`class_name`,
s.`spec_name`,
ts.`avg_creation_time_ms`
FROM `playerbot_bot_templates` t
JOIN `playerbot_spec_info` s ON t.`spec_id` = s.`spec_id`
JOIN `playerbot_class_race_matrix` cr ON s.`class_id` = cr.`class_id` AND cr.`faction` = p_faction
LEFT JOIN `playerbot_template_statistics` ts ON t.`template_id` = ts.`template_id`
WHERE s.`role` = p_role
AND t.`enabled` = 1
AND cr.`enabled` = 1
ORDER BY
CASE WHEN s.`class_id` = p_preferred_class THEN 0 ELSE 1 END,
ts.`avg_creation_time_ms` ASC
LIMIT 1;
END //
-- Procedure to record template usage
CREATE PROCEDURE IF NOT EXISTS `RecordTemplateUsage`(
IN p_template_id INT UNSIGNED,
IN p_creation_time_ms INT UNSIGNED,
IN p_success TINYINT
)
BEGIN
INSERT INTO `playerbot_template_statistics`
(`template_id`, `total_uses`, `last_used`, `avg_creation_time_ms`,
`min_creation_time_ms`, `max_creation_time_ms`, `successful_clones`, `failed_clones`)
VALUES
(p_template_id, 1, NOW(), p_creation_time_ms, p_creation_time_ms, p_creation_time_ms,
IF(p_success = 1, 1, 0), IF(p_success = 0, 1, 0))
ON DUPLICATE KEY UPDATE
`total_uses` = `total_uses` + 1,
`last_used` = NOW(),
`avg_creation_time_ms` = ((`avg_creation_time_ms` * `total_uses`) + p_creation_time_ms) / (`total_uses` + 1),
`min_creation_time_ms` = LEAST(`min_creation_time_ms`, p_creation_time_ms),
`max_creation_time_ms` = GREATEST(`max_creation_time_ms`, p_creation_time_ms),
`successful_clones` = `successful_clones` + IF(p_success = 1, 1, 0),
`failed_clones` = `failed_clones` + IF(p_success = 0, 1, 0);
END //
DELIMITER ;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS = 1;
-- ============================================================================
-- DONE
-- ============================================================================
SELECT 'Bot Template System schema created successfully!' AS status;
SELECT COUNT(*) AS template_count FROM `playerbot_bot_templates`;
SELECT COUNT(*) AS spec_count FROM `playerbot_spec_info`;
SELECT COUNT(*) AS race_class_combos FROM `playerbot_class_race_matrix`;
@@ -0,0 +1,41 @@
-- ============================================================================
-- JIT BOT TRACKING TABLE
-- ============================================================================
--
-- Purpose: Track JIT-created bots separately from world population bots
-- Version: 1.0.0
-- Date: 2026-01-13
--
-- CRITICAL FIX: The orphan cleanup was deleting ALL characters on
-- @playerbot.local accounts, including BotSpawner's reusable characters.
-- This table allows JITBotFactory to track ONLY its own bots so cleanup
-- doesn't impact world population bots from BotSpawner.
--
-- Usage:
-- - JITBotFactory inserts a record when creating a new bot
-- - Orphan cleanup only deletes characters that exist in this table
-- - On proper shutdown, JITBotFactory deletes both the character and this record
-- - On crash recovery, orphaned JIT bots (in this table but not logged in) are cleaned
-- - BotSpawner's characters are NOT in this table, so they're preserved
--
-- ============================================================================
DROP TABLE IF EXISTS `playerbot_jit_bots`;
CREATE TABLE `playerbot_jit_bots` (
`bot_guid` BIGINT UNSIGNED NOT NULL COMMENT 'JIT bot character GUID',
`account_id` INT UNSIGNED NOT NULL COMMENT 'Account ID the bot was created on',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When the bot was created',
`instance_type` ENUM('DUNGEON', 'RAID', 'BATTLEGROUND', 'ARENA') DEFAULT 'DUNGEON' COMMENT 'Type of instance this bot was created for',
`request_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Original request ID that triggered creation',
PRIMARY KEY (`bot_guid`),
INDEX `idx_account_id` (`account_id`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='Tracks JIT-created bots for targeted cleanup (preserves BotSpawner bots)';
-- ============================================================================
-- DONE
-- ============================================================================
SELECT 'JIT Bot Tracking table created successfully!' AS status;
@@ -0,0 +1,222 @@
-- ============================================================================
-- WARM POOL BOT PERSISTENCE SCHEMA UPDATE
-- ============================================================================
--
-- Purpose: Enable warm pool bot persistence across server restarts
-- Version: 1.0.1
-- Date: 2026-01-16
--
-- This migration adds:
-- - bracket column for per-bracket pool tracking
-- - is_warm_pool flag to distinguish warm pool from JIT bots
-- - Indexes for bracket distribution queries
--
-- Architecture:
-- - Warm Pool Bots: Persist in database, checked at startup for correct bracket distribution
-- - JIT Bots: Created on-the-fly, deleted on shutdown (tracked in playerbot_jit_bots)
--
-- ============================================================================
-- ============================================================================
-- HELPER PROCEDURE: Add column if it doesn't exist (MySQL-compatible)
-- ============================================================================
DROP PROCEDURE IF EXISTS `playerbot_add_column_if_not_exists`;
DELIMITER //
CREATE PROCEDURE `playerbot_add_column_if_not_exists`(
IN p_table VARCHAR(64),
IN p_column VARCHAR(64),
IN p_definition VARCHAR(255)
)
BEGIN
DECLARE column_exists INT DEFAULT 0;
SELECT COUNT(*) INTO column_exists
FROM `information_schema`.`COLUMNS`
WHERE `TABLE_SCHEMA` = DATABASE()
AND `TABLE_NAME` = p_table
AND `COLUMN_NAME` = p_column;
IF column_exists = 0 THEN
SET @sql = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_definition);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END //
DELIMITER ;
-- ============================================================================
-- ADD NEW COLUMNS
-- ============================================================================
-- Add bracket column to track per-bracket pools (8 brackets: 0-7)
-- Level bracket: 0=10-19, 1=20-29, 2=30-39, 3=40-49, 4=50-59, 5=60-69, 6=70-79, 7=80+
CALL `playerbot_add_column_if_not_exists`(
'playerbot_instance_pool',
'bracket',
'TINYINT UNSIGNED NOT NULL DEFAULT 7 COMMENT ''Level bracket (0=10-19, 1=20-29, ..., 7=80+)'' AFTER `level`'
);
-- Add is_warm_pool flag to distinguish warm pool bots from any other tracked bots
-- Warm pool bots persist across restarts, JIT bots are deleted on shutdown
CALL `playerbot_add_column_if_not_exists`(
'playerbot_instance_pool',
'is_warm_pool',
'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''True if this is a warm pool bot (persists across restarts)'' AFTER `bracket`'
);
-- Clean up helper procedure
DROP PROCEDURE IF EXISTS `playerbot_add_column_if_not_exists`;
-- ============================================================================
-- ADD INDEXES (using safe drop/create pattern)
-- ============================================================================
-- Helper procedure to add index if not exists
DROP PROCEDURE IF EXISTS `playerbot_add_index_if_not_exists`;
DELIMITER //
CREATE PROCEDURE `playerbot_add_index_if_not_exists`(
IN p_table VARCHAR(64),
IN p_index VARCHAR(64),
IN p_columns VARCHAR(255)
)
BEGIN
DECLARE index_exists INT DEFAULT 0;
SELECT COUNT(*) INTO index_exists
FROM `information_schema`.`STATISTICS`
WHERE `TABLE_SCHEMA` = DATABASE()
AND `TABLE_NAME` = p_table
AND `INDEX_NAME` = p_index;
IF index_exists = 0 THEN
SET @sql = CONCAT('CREATE INDEX `', p_index, '` ON `', p_table, '`(', p_columns, ')');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END //
DELIMITER ;
-- Add index for fast bracket-based queries during startup reconciliation
CALL `playerbot_add_index_if_not_exists`(
'playerbot_instance_pool',
'idx_bracket_faction_role',
'`bracket`, `faction`, `role`, `is_warm_pool`'
);
-- Add index for warm pool loading at startup
CALL `playerbot_add_index_if_not_exists`(
'playerbot_instance_pool',
'idx_warm_pool',
'`is_warm_pool`, `slot_state`'
);
-- Clean up helper procedure
DROP PROCEDURE IF EXISTS `playerbot_add_index_if_not_exists`;
-- ============================================================================
-- STORED PROCEDURE: Get bracket distribution for reconciliation at startup
-- ============================================================================
DROP PROCEDURE IF EXISTS `GetWarmPoolBracketDistribution`;
DELIMITER //
CREATE PROCEDURE `GetWarmPoolBracketDistribution`()
BEGIN
SELECT
`bracket`,
`faction`,
`role`,
COUNT(*) as `count`
FROM `playerbot_instance_pool`
WHERE `is_warm_pool` = 1
GROUP BY `bracket`, `faction`, `role`
ORDER BY `bracket`, `faction`, `role`;
END //
DELIMITER ;
-- ============================================================================
-- STORED PROCEDURE: Get warm pool bots for a specific bracket
-- ============================================================================
DROP PROCEDURE IF EXISTS `GetWarmPoolBotsForBracket`;
DELIMITER //
CREATE PROCEDURE `GetWarmPoolBotsForBracket`(IN p_bracket TINYINT UNSIGNED)
BEGIN
SELECT
`bot_guid`,
`account_id`,
`bot_name`,
`role`,
`faction`,
`player_class`,
`spec_id`,
`level`,
`gear_score`,
`slot_state`,
`assignment_count`,
`successful_completions`
FROM `playerbot_instance_pool`
WHERE `is_warm_pool` = 1 AND `bracket` = p_bracket
ORDER BY `faction`, `role`;
END //
DELIMITER ;
-- ============================================================================
-- VIEW: Warm pool bracket summary for monitoring
-- ============================================================================
DROP VIEW IF EXISTS `v_warm_pool_bracket_summary`;
CREATE VIEW `v_warm_pool_bracket_summary` AS
SELECT
`bracket`,
`faction`,
SUM(CASE WHEN `role` = 'TANK' THEN 1 ELSE 0 END) as `tanks`,
SUM(CASE WHEN `role` = 'HEALER' THEN 1 ELSE 0 END) as `healers`,
SUM(CASE WHEN `role` = 'DPS' THEN 1 ELSE 0 END) as `dps`,
COUNT(*) as `total`
FROM `playerbot_instance_pool`
WHERE `is_warm_pool` = 1
GROUP BY `bracket`, `faction`;
-- ============================================================================
-- VIEW: Warm pool health check - compare actual vs target distribution
-- ============================================================================
DROP VIEW IF EXISTS `v_warm_pool_health`;
CREATE VIEW `v_warm_pool_health` AS
SELECT
b.`bracket`,
b.`faction`,
COALESCE(p.`tanks`, 0) as `actual_tanks`,
10 as `target_tanks`,
COALESCE(p.`healers`, 0) as `actual_healers`,
15 as `target_healers`,
COALESCE(p.`dps`, 0) as `actual_dps`,
25 as `target_dps`,
COALESCE(p.`total`, 0) as `actual_total`,
50 as `target_total`,
CASE
WHEN COALESCE(p.`total`, 0) = 50 THEN 'HEALTHY'
WHEN COALESCE(p.`total`, 0) >= 40 THEN 'WARNING'
ELSE 'CRITICAL'
END as `status`
FROM (
-- Generate all bracket/faction combinations
SELECT b.`bracket`, f.`faction`
FROM (
SELECT 0 as `bracket` UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7
) b
CROSS JOIN (
SELECT 'ALLIANCE' as `faction` UNION ALL SELECT 'HORDE'
) f
) b
LEFT JOIN `v_warm_pool_bracket_summary` p ON b.`bracket` = p.`bracket` AND b.`faction` = p.`faction`;
-- ============================================================================
-- DONE
-- ============================================================================
SELECT 'Warm Pool Persistence schema update applied successfully!' AS status;
@@ -0,0 +1,20 @@
-- ============================================================================
-- Playerbot Spell Fix: Hunter Binding Shot (spell 109248)
-- ============================================================================
--
-- Problem: TrinityCore's at_hun_binding_shot script has a null pointer bug
-- that causes crashes when units are removed during AreaTrigger iteration.
--
-- Solution: Change the script name in the database to use Playerbot's
-- null-safe version instead of TrinityCore's buggy version.
--
-- This SQL must be applied to the WORLD database.
-- ============================================================================
-- Update areatrigger_template to use Playerbot's fixed script
UPDATE `areatrigger_template`
SET `ScriptName` = 'at_hun_binding_shot_playerbot'
WHERE `ScriptName` = 'at_hun_binding_shot';
-- Verify the change
SELECT Id, ScriptName FROM `areatrigger_template` WHERE `ScriptName` LIKE '%binding_shot%';
@@ -0,0 +1,204 @@
-- ============================================================================
-- ADD VALIDATION CONSTRAINTS TO PREVENT INVALID TEMPLATE ENTRIES
-- ============================================================================
-- This script adds database-level safeguards to prevent invalid templates:
-- 1. CHECK constraints on class_id (must be 1-13, valid WoW classes)
-- 2. CHECK constraints on spec_id (must be > 0)
-- 3. CHECK constraints on role (must be 0-2)
-- 4. BEFORE INSERT trigger for comprehensive validation
-- 5. BEFORE UPDATE trigger for comprehensive validation
--
-- MySQL 8.0.16+ required for CHECK constraint support
-- ============================================================================
-- First, clean up any existing invalid entries
DELETE FROM `playerbot_bot_templates` WHERE `class_id` = 0 OR `class_id` > 13;
DELETE FROM `playerbot_bot_templates` WHERE `spec_id` = 0;
-- Drop existing constraints if they exist (safe to run multiple times)
-- Note: MySQL doesn't have IF EXISTS for constraints, so we use a procedure
DELIMITER //
DROP PROCEDURE IF EXISTS AddTemplateConstraints//
CREATE PROCEDURE AddTemplateConstraints()
BEGIN
DECLARE constraint_exists INT DEFAULT 0;
-- Check if constraint already exists
SELECT COUNT(*) INTO constraint_exists
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'playerbot_bot_templates'
AND CONSTRAINT_NAME = 'chk_valid_class_id';
-- Add CHECK constraint for class_id if it doesn't exist
IF constraint_exists = 0 THEN
ALTER TABLE `playerbot_bot_templates`
ADD CONSTRAINT `chk_valid_class_id`
CHECK (`class_id` >= 1 AND `class_id` <= 13);
END IF;
-- Check for spec_id constraint
SELECT COUNT(*) INTO constraint_exists
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'playerbot_bot_templates'
AND CONSTRAINT_NAME = 'chk_valid_spec_id';
IF constraint_exists = 0 THEN
ALTER TABLE `playerbot_bot_templates`
ADD CONSTRAINT `chk_valid_spec_id`
CHECK (`spec_id` > 0);
END IF;
-- Check for role constraint
SELECT COUNT(*) INTO constraint_exists
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'playerbot_bot_templates'
AND CONSTRAINT_NAME = 'chk_valid_role';
IF constraint_exists = 0 THEN
ALTER TABLE `playerbot_bot_templates`
ADD CONSTRAINT `chk_valid_role`
CHECK (`role` >= 0 AND `role` <= 2);
END IF;
-- Check for template_name constraint (not empty)
SELECT COUNT(*) INTO constraint_exists
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'playerbot_bot_templates'
AND CONSTRAINT_NAME = 'chk_valid_template_name';
IF constraint_exists = 0 THEN
ALTER TABLE `playerbot_bot_templates`
ADD CONSTRAINT `chk_valid_template_name`
CHECK (LENGTH(`template_name`) > 0);
END IF;
END//
DELIMITER ;
-- Execute the procedure
CALL AddTemplateConstraints();
-- Clean up
DROP PROCEDURE IF EXISTS AddTemplateConstraints;
-- ============================================================================
-- ADD VALIDATION TRIGGERS
-- ============================================================================
DELIMITER //
-- Drop existing triggers if they exist
DROP TRIGGER IF EXISTS `trg_bot_templates_before_insert`//
DROP TRIGGER IF EXISTS `trg_bot_templates_before_update`//
-- BEFORE INSERT trigger - validates all fields
CREATE TRIGGER `trg_bot_templates_before_insert`
BEFORE INSERT ON `playerbot_bot_templates`
FOR EACH ROW
BEGIN
-- Validate class_id (WoW classes are 1-13)
IF NEW.class_id < 1 OR NEW.class_id > 13 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid class_id: must be between 1 and 13 (valid WoW class IDs)';
END IF;
-- Validate spec_id exists in spec_info
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info WHERE spec_id = NEW.spec_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid spec_id: must reference a valid entry in playerbot_spec_info';
END IF;
-- Validate role (0=Tank, 1=Healer, 2=DPS)
IF NEW.role < 0 OR NEW.role > 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid role: must be 0 (Tank), 1 (Healer), or 2 (DPS)';
END IF;
-- Validate template_name is not empty
IF NEW.template_name IS NULL OR LENGTH(TRIM(NEW.template_name)) = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid template_name: cannot be empty';
END IF;
-- Validate class_id matches spec_info
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info
WHERE spec_id = NEW.spec_id AND class_id = NEW.class_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'class_id does not match the class for the given spec_id in playerbot_spec_info';
END IF;
END//
-- BEFORE UPDATE trigger - validates all fields
CREATE TRIGGER `trg_bot_templates_before_update`
BEFORE UPDATE ON `playerbot_bot_templates`
FOR EACH ROW
BEGIN
-- Validate class_id (WoW classes are 1-13)
IF NEW.class_id < 1 OR NEW.class_id > 13 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid class_id: must be between 1 and 13 (valid WoW class IDs)';
END IF;
-- Validate spec_id exists in spec_info
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info WHERE spec_id = NEW.spec_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid spec_id: must reference a valid entry in playerbot_spec_info';
END IF;
-- Validate role (0=Tank, 1=Healer, 2=DPS)
IF NEW.role < 0 OR NEW.role > 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid role: must be 0 (Tank), 1 (Healer), or 2 (DPS)';
END IF;
-- Validate template_name is not empty
IF NEW.template_name IS NULL OR LENGTH(TRIM(NEW.template_name)) = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Invalid template_name: cannot be empty';
END IF;
-- Validate class_id matches spec_info
IF NOT EXISTS (SELECT 1 FROM playerbot_spec_info
WHERE spec_id = NEW.spec_id AND class_id = NEW.class_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'class_id does not match the class for the given spec_id in playerbot_spec_info';
END IF;
END//
DELIMITER ;
-- ============================================================================
-- VERIFICATION
-- ============================================================================
-- Show constraints
SELECT 'Constraints added:' AS status;
SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'playerbot_bot_templates';
-- Show triggers
SELECT 'Triggers added:' AS status;
SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = DATABASE()
AND EVENT_OBJECT_TABLE = 'playerbot_bot_templates';
-- Verify template counts
SELECT 'Template verification:' AS status;
SELECT role, COUNT(*) as count FROM playerbot_bot_templates GROUP BY role;
SELECT COUNT(*) as total_templates FROM playerbot_bot_templates;
-- Test constraint (should fail) - commented out to not disrupt execution
-- INSERT INTO playerbot_bot_templates (spec_id, class_id, role, template_name) VALUES (0, 0, 0, '');
SELECT 'Database constraints successfully added!' AS result;
@@ -0,0 +1,29 @@
-- ============================================================================
-- FIX: Remove corrupt template entry with class_id=0
-- ============================================================================
-- Issue: A corrupt entry exists with template_id=0, class_id=0, spec_id=0
-- This causes JIT bot creation failures for Horde faction because class_id=0
-- has no valid races in the class_race_matrix.
--
-- Root cause: Unknown - possibly manual insert or migration bug
-- Effect: GetValidRaces returns empty for class 0, template rejected for Horde
-- ============================================================================
-- Delete any templates with invalid class_id (0 is not a valid WoW class)
DELETE FROM `playerbot_bot_templates` WHERE `class_id` = 0;
-- Delete any templates with invalid spec_id (0 is not a valid spec)
DELETE FROM `playerbot_bot_templates` WHERE `spec_id` = 0;
-- Delete any orphan statistics entries
DELETE FROM `playerbot_template_statistics`
WHERE `template_id` NOT IN (SELECT `template_id` FROM `playerbot_bot_templates`);
-- Verify fix
SELECT 'Templates after cleanup:' AS status;
SELECT COUNT(*) AS total_templates FROM `playerbot_bot_templates`;
SELECT `role`, COUNT(*) AS count FROM `playerbot_bot_templates` GROUP BY `role`;
-- Show any remaining invalid templates (should be empty)
SELECT 'Invalid templates (should be empty):' AS status;
SELECT * FROM `playerbot_bot_templates` WHERE `class_id` = 0 OR `spec_id` = 0;