From a8f8ba210b2926caaf04746f3c19e91438fd9334 Mon Sep 17 00:00:00 2001 From: SylvaniaCore deploy Date: Fri, 14 Aug 2026 18:03:06 +0200 Subject: [PATCH] Siege des Capitales E1-E3 : ordonnanceur, persistance, commandes GM Nouveau module d evenement quotidien : une horde de playerbots assaille la capitale adverse et tente d abattre son dirigeant. Cette premiere tranche livre le squelette, la persistance et l ordonnancement ; le spawner, l IA d assaut et l eveil du dirigeant suivent (E4-E8). Le serveur tourne sur DestinyCore, sans systeme de modules : le module prend donc la forme d un script module TrinityCore (decouverte CMake automatique via AddCapitalSiegeScripts()), avec le gestionnaire dans la bibliotheque game comme PlayerBotMgr, puisque l IA d assaut devra y acceder depuis BotBGAI. game/CapitalSiege/CapitalSiegeMgr : tirage quotidien d une heure aleatoire dans une plage configurable, alternance stricte de la faction attaquante, machine a etats et arret d urgence si le diff monde depasse le seuil. Le jour est consomme au declenchement et non a la fin, donc un redemarrage du worldserver en plein evenement ne peut pas le rejouer. scripts/CapitalSiege : branchement par WorldScript (aucune modification de World.cpp), et commandes GM .siege status/start/stop/history sous la permission RBAC_PERM_COMMAND_EVENT existante. Persistance dans dc_characters et non dc_world : il s agit d etat mutable ecrit par le serveur, qu une reimportation de la base de contenu ne doit pas effacer. Six requetes preparees ajoutees a CharacterDatabase. Toute ligne d historique restee ouverte est refermee au demarrage du core. MMaps verifies pour les deux capitales : carte 0 (592 tuiles) et carte 1 (715 tuiles), tuiles de Hurlevent et d Orgrimmar presentes. La navigation des bots passe par PathGenerator/MotionMaster, donc Detour, pas les VMaps. Co-Authored-By: Claude Opus 5 --- sql/sylvania/capital_siege.sql | 73 +++ .../Implementation/CharacterDatabase.cpp | 8 + .../Implementation/CharacterDatabase.h | 8 + .../game/CapitalSiege/CapitalSiegeMgr.cpp | 513 ++++++++++++++++++ .../game/CapitalSiege/CapitalSiegeMgr.h | 172 ++++++ .../CapitalSiege/capital_siege_world.cpp | 62 +++ .../capitalsiege_script_loader.cpp | 27 + .../scripts/CapitalSiege/cs_capital_siege.cpp | 192 +++++++ src/server/worldserver/worldserver.conf.dist | 139 ++++- 9 files changed, 1193 insertions(+), 1 deletion(-) create mode 100644 sql/sylvania/capital_siege.sql create mode 100644 src/server/game/CapitalSiege/CapitalSiegeMgr.cpp create mode 100644 src/server/game/CapitalSiege/CapitalSiegeMgr.h create mode 100644 src/server/scripts/CapitalSiege/capital_siege_world.cpp create mode 100644 src/server/scripts/CapitalSiege/capitalsiege_script_loader.cpp create mode 100644 src/server/scripts/CapitalSiege/cs_capital_siege.cpp diff --git a/sql/sylvania/capital_siege.sql b/sql/sylvania/capital_siege.sql new file mode 100644 index 0000000..d3340c7 --- /dev/null +++ b/sql/sylvania/capital_siege.sql @@ -0,0 +1,73 @@ +-- +-- SylvaniaCore - Module "Siege des Capitales" +-- Base : dc_characters (etat mutable ecrit par le worldserver, donc hors de la +-- base de contenu dc_world qui est reimportee a chaque mise a jour). +-- +-- Deux tables : +-- capital_siege_state : ligne unique, l ordonnancement du module. Elle porte +-- le verrou anti-rejeu quotidien et l alternance de +-- faction, qui doivent survivre a un redemarrage. +-- capital_siege_history : une ligne par evenement, ouverte au declenchement et +-- refermee a la fin (ou au demarrage suivant du core +-- si le serveur s est arrete en plein siege). +-- +-- Toutes les ecritures passent par des requetes preparees declarees dans +-- src/server/database/Database/Implementation/CharacterDatabase.{h,cpp}. +-- + +-- --------------------------------------------------------------------------- +-- Etat de l ordonnanceur (une seule ligne, id = 1) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `capital_siege_state` ( + `id` TINYINT(3) UNSIGNED NOT NULL DEFAULT 1 + COMMENT 'Toujours 1 : la table ne contient qu une ligne', + `last_event_day` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Jour serveur (epoch local / 86400) du dernier evenement consomme. Verrou anti-rejeu quotidien', + `last_attacker_team` TINYINT(4) NOT NULL DEFAULT -1 + COMMENT 'Derniere faction attaquante : 0 = Alliance, 1 = Horde, -1 = aucune. Sert a l alternance stricte', + `scheduled_day` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Jour serveur du tirage courant', + `scheduled_time` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Horodatage unix du declenchement tire pour scheduled_day', + `scheduled_team` TINYINT(4) NOT NULL DEFAULT -1 + COMMENT 'Faction attaquante tiree pour scheduled_day : 0 = Alliance, 1 = Horde, -1 = aucune', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='Siege des Capitales : ordonnancement persistant'; + +-- --------------------------------------------------------------------------- +-- Historique des evenements +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `capital_siege_history` ( + `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `start_time` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Horodatage unix du declenchement', + `end_time` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Horodatage unix de la fin, 0 tant que l evenement est en cours', + `attacker_team` TINYINT(4) NOT NULL DEFAULT -1 + COMMENT '0 = Alliance, 1 = Horde', + `target_map` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Carte de la capitale assaillie (0 = Hurlevent, 1 = Orgrimmar)', + `boss_entry` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'creature_template du dirigeant vise', + `outcome` TINYINT(4) NOT NULL DEFAULT 0 + COMMENT '0 = en cours, 1 = victoire, 2 = temps ecoule, 3 = annule GM, 4 = arret d urgence (charge), 5 = interrompu (arret du serveur)', + `duration` INT(10) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Duree effective en secondes', + `bots_spawned` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Nombre de bots effectivement deployes', + `bots_lost` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Nombre de bots tues pendant l assaut', + `triggered_by` VARCHAR(32) NOT NULL DEFAULT 'auto' + COMMENT 'auto = ordonnanceur, sinon nom du GM declencheur', + PRIMARY KEY (`id`), + KEY `idx_outcome` (`outcome`), + KEY `idx_start_time` (`start_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci + COMMENT='Siege des Capitales : historique des evenements'; + +-- Ligne d etat initiale. Aucune faction n a encore attaque : le premier +-- evenement sera donc mene par l Alliance. +INSERT IGNORE INTO `capital_siege_state` + (`id`, `last_event_day`, `last_attacker_team`, `scheduled_day`, `scheduled_time`, `scheduled_team`) + VALUES (1, 0, -1, 0, 0, -1); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index f8e460b..574237b 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -806,6 +806,14 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_UPD_SPECIALIZATION, "UPDATE characters SET primarySpecialization = ? WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_DEL_CHALLENGE_MEMBER, "DELETE FROM challenge_member WHERE member = ?", CONNECTION_ASYNC); + + // Module Siege des Capitales (SylvaniaCore) + PrepareStatement(CHAR_SEL_CAPITAL_SIEGE_STATE, "SELECT last_event_day, last_attacker_team, scheduled_day, scheduled_time, scheduled_team FROM capital_siege_state WHERE id = 1", CONNECTION_SYNCH); + PrepareStatement(CHAR_REP_CAPITAL_SIEGE_STATE, "REPLACE INTO capital_siege_state (id, last_event_day, last_attacker_team, scheduled_day, scheduled_time, scheduled_team) VALUES (1, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_CAPITAL_SIEGE_HISTORY, "INSERT INTO capital_siege_history (start_time, attacker_team, target_map, boss_entry, outcome, triggered_by) VALUES (?, ?, ?, ?, 0, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_CAPITAL_SIEGE_HISTORY_CLOSE, "UPDATE capital_siege_history SET end_time = ?, duration = ?, outcome = ?, bots_spawned = ?, bots_lost = ? WHERE outcome = 0", CONNECTION_ASYNC); + PrepareStatement(CHAR_UPD_CAPITAL_SIEGE_HISTORY_ORPHAN, "UPDATE capital_siege_history SET end_time = ?, outcome = ? WHERE outcome = 0", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_CAPITAL_SIEGE_HISTORY, "SELECT start_time, attacker_team, outcome, duration, bots_spawned, bots_lost, triggered_by FROM capital_siege_history ORDER BY id DESC LIMIT 10", CONNECTION_SYNCH); } CharacterDatabaseConnection::CharacterDatabaseConnection(MySQLConnectionInfo& connInfo, ConnectionFlags connectionFlags) : MySQLConnection(connInfo, connectionFlags) diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index eea6bd2..9b3689d 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -681,6 +681,14 @@ enum CharacterDatabaseStatements : uint32 CHAR_UPD_SPECIALIZATION, + // Module Siege des Capitales (SylvaniaCore) + CHAR_SEL_CAPITAL_SIEGE_STATE, + CHAR_REP_CAPITAL_SIEGE_STATE, + CHAR_INS_CAPITAL_SIEGE_HISTORY, + CHAR_UPD_CAPITAL_SIEGE_HISTORY_CLOSE, + CHAR_UPD_CAPITAL_SIEGE_HISTORY_ORPHAN, + CHAR_SEL_CAPITAL_SIEGE_HISTORY, + MAX_CHARACTERDATABASE_STATEMENTS }; diff --git a/src/server/game/CapitalSiege/CapitalSiegeMgr.cpp b/src/server/game/CapitalSiege/CapitalSiegeMgr.cpp new file mode 100644 index 0000000..9a101e6 --- /dev/null +++ b/src/server/game/CapitalSiege/CapitalSiegeMgr.cpp @@ -0,0 +1,513 @@ +/* + * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "CapitalSiegeMgr.h" +#include "Config.h" +#include "DatabaseEnv.h" +#include "Log.h" +#include "Random.h" +#include "World.h" + +#include +#include +#include + +namespace +{ + // Duree minimale passee au-dessus du seuil de charge avant l arret d urgence. + constexpr uint32 SIEGE_OVERLOAD_GRACE_MS = 10 * IN_MILLISECONDS; +} + +CapitalSiegeMgr::CapitalSiegeMgr() : + m_enabled(false), m_hourMin(18), m_hourMax(24), m_botCount(50), m_botLevel(110), + m_duration(HOUR), m_spawnRate(5), m_pvpMode(1), m_bossLevel(112), m_bossHealthMult(50), + m_maxDiff(400), m_announce(true), + m_lastEventDay(0), m_lastAttackerTeam(TEAM_NEUTRAL), m_scheduledDay(0), + m_scheduledTime(0), m_scheduledTeam(TEAM_NEUTRAL), + m_status(SIEGE_STATUS_IDLE), m_attackerTeam(TEAM_NEUTRAL), m_target(nullptr), + m_elapsed(0), m_updateTimer(0), m_overloadTimer(0), + m_botsSpawned(0), m_botsLost(0), m_startTime(0) +{ + // Les valeurs sont ecrasees par LoadConfig(). Elles ne servent que si le + // fichier de configuration est muet sur une cle. + m_targets[TEAM_ALLIANCE] = { 1, 42283, Position(), Position(), "Orgrimmar" }; + m_targets[TEAM_HORDE] = { 0, 107574, Position(), Position(), "Hurlevent" }; +} + +CapitalSiegeMgr::~CapitalSiegeMgr() { } + +CapitalSiegeMgr* CapitalSiegeMgr::instance() +{ + static CapitalSiegeMgr instance; + return &instance; +} + +char const* CapitalSiegeMgr::GetTeamName(TeamId team) +{ + switch (team) + { + case TEAM_ALLIANCE: return "Alliance"; + case TEAM_HORDE: return "Horde"; + default: return "aucune"; + } +} + +char const* CapitalSiegeMgr::GetOutcomeName(CapitalSiegeOutcome outcome) +{ + switch (outcome) + { + case SIEGE_OUTCOME_RUNNING: return "en cours"; + case SIEGE_OUTCOME_VICTORY: return "victoire"; + case SIEGE_OUTCOME_TIMEOUT: return "echec (temps ecoule)"; + case SIEGE_OUTCOME_CANCELLED: return "annule"; + case SIEGE_OUTCOME_OVERLOAD: return "arret d urgence (charge)"; + case SIEGE_OUTCOME_INTERRUPTED: return "interrompu (arret du serveur)"; + default: return "inconnu"; + } +} + +/******************************************************************************* + * Configuration + ******************************************************************************/ + +void CapitalSiegeMgr::LoadConfig() +{ + m_enabled = sConfigMgr->GetBoolDefault("siege_enable", false); + m_hourMin = sConfigMgr->GetIntDefault("siege_hour_min", 18); + m_hourMax = sConfigMgr->GetIntDefault("siege_hour_max", 24); + m_botCount = sConfigMgr->GetIntDefault("siege_botcount", 50); + m_botLevel = sConfigMgr->GetIntDefault("siege_botlevel", 110); + m_duration = sConfigMgr->GetIntDefault("siege_duration", HOUR); + m_spawnRate = sConfigMgr->GetIntDefault("siege_spawn_rate", 5); + m_pvpMode = sConfigMgr->GetIntDefault("siege_pvp", 1); + m_bossLevel = sConfigMgr->GetIntDefault("siege_boss_level", 112); + m_bossHealthMult = sConfigMgr->GetIntDefault("siege_boss_hp_mult", 50); + m_maxDiff = sConfigMgr->GetIntDefault("siege_maxdiff", 400); + m_announce = sConfigMgr->GetBoolDefault("siege_announce", true); + + // Garde-fous : une plage horaire vide ou inversee rendrait le tirage + // impossible, un nombre de bots absurde noierait le serveur. + if (m_hourMin > 23) + m_hourMin = 23; + if (m_hourMax > 24) + m_hourMax = 24; + if (m_hourMax <= m_hourMin) + { + TC_LOG_ERROR("server.worldserver", "Siege des Capitales: plage horaire invalide (%u-%u), retour a 18-24.", m_hourMin, m_hourMax); + m_hourMin = 18; + m_hourMax = 24; + } + if (m_botCount > 100) + { + TC_LOG_ERROR("server.worldserver", "Siege des Capitales: siege_botcount=%u plafonne a 100.", m_botCount); + m_botCount = 100; + } + if (m_spawnRate == 0) + m_spawnRate = 1; + if (m_duration < MINUTE) + m_duration = MINUTE; + if (m_pvpMode > 2) + m_pvpMode = 1; + + // Cible assaillie par l Alliance : la capitale de la Horde. + m_targets[TEAM_ALLIANCE].mapId = sConfigMgr->GetIntDefault("siege_horde_map", 1); + m_targets[TEAM_ALLIANCE].bossEntry = sConfigMgr->GetIntDefault("siege_horde_boss_entry", 42283); + m_targets[TEAM_ALLIANCE].bossPos.Relocate( + sConfigMgr->GetFloatDefault("siege_horde_boss_x", 1924.4f), + sConfigMgr->GetFloatDefault("siege_horde_boss_y", -4144.1f), + sConfigMgr->GetFloatDefault("siege_horde_boss_z", 40.6f)); + m_targets[TEAM_ALLIANCE].stagingPos.Relocate( + sConfigMgr->GetFloatDefault("siege_horde_staging_x", 1570.0f), + sConfigMgr->GetFloatDefault("siege_horde_staging_y", -4397.4f), + sConfigMgr->GetFloatDefault("siege_horde_staging_z", 16.0f)); + m_targets[TEAM_ALLIANCE].cityName = "Orgrimmar"; + + // Cible assaillie par la Horde : la capitale de l Alliance. + m_targets[TEAM_HORDE].mapId = sConfigMgr->GetIntDefault("siege_alliance_map", 0); + m_targets[TEAM_HORDE].bossEntry = sConfigMgr->GetIntDefault("siege_alliance_boss_entry", 107574); + m_targets[TEAM_HORDE].bossPos.Relocate( + sConfigMgr->GetFloatDefault("siege_alliance_boss_x", -8363.3f), + sConfigMgr->GetFloatDefault("siege_alliance_boss_y", 232.5f), + sConfigMgr->GetFloatDefault("siege_alliance_boss_z", 157.1f)); + m_targets[TEAM_HORDE].stagingPos.Relocate( + sConfigMgr->GetFloatDefault("siege_alliance_staging_x", -8833.1f), + sConfigMgr->GetFloatDefault("siege_alliance_staging_y", 622.8f), + sConfigMgr->GetFloatDefault("siege_alliance_staging_z", 93.9f)); + m_targets[TEAM_HORDE].cityName = "Hurlevent"; + + TC_LOG_INFO("server.loading", "Siege des Capitales: %s (plage %02uh-%02uh, %u bots niveau %u, duree max %u min).", + m_enabled ? "actif" : "inactif", m_hourMin, m_hourMax, m_botCount, m_botLevel, m_duration / MINUTE); +} + +/******************************************************************************* + * Cycle de vie + ******************************************************************************/ + +void CapitalSiegeMgr::LoadState() +{ + // Un evenement laisse ouvert signifie que le worldserver s est arrete en + // plein siege : la ligne d historique est refermee et les bots eventuels + // sont deja hors ligne (leurs sessions ne survivent pas a l arret). + CloseOrphanHistoryRows(); + + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CAPITAL_SIEGE_STATE); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + if (!result) + { + TC_LOG_INFO("server.loading", "Siege des Capitales: aucun etat persiste, premier tirage au prochain tick."); + return; + } + + Field* fields = result->Fetch(); + m_lastEventDay = fields[0].GetUInt32(); + m_lastAttackerTeam = TeamId(fields[1].GetInt8() < 0 ? TEAM_NEUTRAL : fields[1].GetInt8()); + m_scheduledDay = fields[2].GetUInt32(); + m_scheduledTime = time_t(fields[3].GetUInt32()); + m_scheduledTeam = TeamId(fields[4].GetInt8() < 0 ? TEAM_NEUTRAL : fields[4].GetInt8()); + + TC_LOG_INFO("server.loading", "Siege des Capitales: etat recharge (derniere faction attaquante: %s, jour du dernier evenement: %u).", + GetTeamName(m_lastAttackerTeam), m_lastEventDay); +} + +void CapitalSiegeMgr::Update(uint32 diff) +{ + if (!m_enabled) + { + if (IsRunning()) + { + TC_LOG_INFO("server.worldserver", "Siege des Capitales: module desactive en cours d evenement, arret."); + StopSiege(SIEGE_OUTCOME_CANCELLED); + } + return; + } + + // La charge est surveillee a chaque tick monde, pas seulement a la seconde. + bool const overloaded = IsServerOverloaded(diff); + + m_updateTimer += diff; + if (m_updateTimer < IN_MILLISECONDS) + return; + + uint32 const elapsedMs = m_updateTimer; + m_updateTimer = 0; + + if (IsRunning()) + { + if (overloaded) + { + TC_LOG_ERROR("server.worldserver", "Siege des Capitales: arret d urgence, le monde tourne au-dessus de %u ms depuis %u s.", + m_maxDiff, SIEGE_OVERLOAD_GRACE_MS / IN_MILLISECONDS); + StopSiege(SIEGE_OUTCOME_OVERLOAD); + return; + } + UpdateRunningSiege(elapsedMs); + } + else + UpdateSchedule(sWorld->GetGameTime()); +} + +/******************************************************************************* + * Ordonnancement + ******************************************************************************/ + +void CapitalSiegeMgr::UpdateSchedule(time_t now) +{ + uint32 const today = GetServerDay(now); + + if (m_scheduledDay != today) + DrawScheduleForDay(now); + + if (m_lastEventDay == today) // deja joue aujourd hui + return; + if (now < m_scheduledTime) + return; + + // Rattrapage. Si le worldserver etait arrete a l heure tiree, on ne + // declenche que tant qu on est encore dans la plage horaire configuree : + // sinon le siege se lancerait a 7h du matin apres une nuit d arret. + if (!IsInsideWindow(now)) + { + TC_LOG_INFO("server.worldserver", "Siege des Capitales: creneau du jour manque (serveur arrete), report a demain."); + m_lastEventDay = today; + SaveState(); + return; + } + + StartSiege(m_scheduledTeam, "auto"); +} + +void CapitalSiegeMgr::DrawScheduleForDay(time_t now) +{ + m_scheduledDay = GetServerDay(now); + + // Alternance stricte : jamais deux fois la meme faction attaquante. + m_scheduledTeam = (m_lastAttackerTeam == TEAM_ALLIANCE) ? TEAM_HORDE : TEAM_ALLIANCE; + + uint32 const windowLength = (m_hourMax - m_hourMin) * HOUR; + m_scheduledTime = GetDayStart(now) + time_t(m_hourMin * HOUR) + time_t(urand(0, windowLength - 1)); + + SaveState(); + + tm scheduledTm; + time_t const scheduled = m_scheduledTime; + localtime_r(&scheduled, &scheduledTm); + TC_LOG_INFO("server.worldserver", "Siege des Capitales: tirage du jour -> %s a %02u:%02u:%02u.", + GetTeamName(m_scheduledTeam), uint32(scheduledTm.tm_hour), uint32(scheduledTm.tm_min), uint32(scheduledTm.tm_sec)); +} + +bool CapitalSiegeMgr::IsInsideWindow(time_t now) const +{ + tm localTm; + localtime_r(&now, &localTm); + uint32 const hour = uint32(localTm.tm_hour); + return hour >= m_hourMin && hour < m_hourMax; +} + +/******************************************************************************* + * Pilotage + ******************************************************************************/ + +bool CapitalSiegeMgr::StartSiege(TeamId attacker, std::string const& trigger) +{ + if (IsRunning()) + return false; + if (attacker != TEAM_ALLIANCE && attacker != TEAM_HORDE) + return false; + + m_attackerTeam = attacker; + m_target = &m_targets[attacker]; + m_status = SIEGE_STATUS_SPAWNING; + m_elapsed = 0; + m_overloadTimer = 0; + m_botsSpawned = 0; + m_botsLost = 0; + m_startTime = sWorld->GetGameTime(); + + // Le jour est consomme des le declenchement, avant toute autre operation : + // un redemarrage du worldserver en plein evenement ne peut pas le rejouer. + m_lastEventDay = GetServerDay(m_startTime); + m_lastAttackerTeam = attacker; + SaveState(); + OpenHistoryRow(trigger); + + TC_LOG_INFO("server.worldserver", "Siege des Capitales: debut de l assaut %s sur %s (declencheur: %s).", + GetTeamName(attacker), m_target->cityName, trigger.c_str()); + + if (m_announce) + Announce("|cffff2020[Siege des Capitales]|r Une armee de la %s marche sur %s !", GetTeamName(attacker), m_target->cityName); + + // E7 : demarrage du spawn etale de la horde d invasion. + // E8 : eveil du dirigeant de la capitale. + return true; +} + +void CapitalSiegeMgr::StopSiege(CapitalSiegeOutcome outcome) +{ + if (!IsRunning()) + return; + + m_status = SIEGE_STATUS_ENDING; + + // E7 : despawn des bots restants. + // E8 : restauration des drapeaux du dirigeant et purge des aggro de ville. + + CloseHistoryRow(outcome); + + TC_LOG_INFO("server.worldserver", "Siege des Capitales: fin de l assaut %s sur %s apres %u s -> %s.", + GetTeamName(m_attackerTeam), m_target ? m_target->cityName : "?", GetElapsedSeconds(), GetOutcomeName(outcome)); + + if (m_announce) + { + if (outcome == SIEGE_OUTCOME_VICTORY) + Announce("|cffff2020[Siege des Capitales]|r %s est tombee : le dirigeant a ete abattu par la %s !", + m_target ? m_target->cityName : "La capitale", GetTeamName(m_attackerTeam)); + else if (outcome == SIEGE_OUTCOME_TIMEOUT) + Announce("|cff20ff20[Siege des Capitales]|r L assaut sur %s a ete repousse.", m_target ? m_target->cityName : "la capitale"); + } + + m_status = SIEGE_STATUS_IDLE; + m_attackerTeam = TEAM_NEUTRAL; + m_target = nullptr; + m_elapsed = 0; +} + +void CapitalSiegeMgr::UpdateRunningSiege(uint32 diff) +{ + m_elapsed += diff; + + if (m_elapsed >= m_duration * IN_MILLISECONDS) + { + StopSiege(SIEGE_OUTCOME_TIMEOUT); + return; + } + + // E5 : tick du commandant d assaut (progression, ciblage, morts de bots). + // E7 : suite du spawn etale tant que le quota n est pas atteint. +} + +bool CapitalSiegeMgr::IsServerOverloaded(uint32 diff) +{ + if (!m_maxDiff) + return false; + + if (diff > m_maxDiff) + m_overloadTimer += diff; + else + m_overloadTimer = 0; + + return m_overloadTimer >= SIEGE_OVERLOAD_GRACE_MS; +} + +/******************************************************************************* + * Persistance + ******************************************************************************/ + +void CapitalSiegeMgr::SaveState() +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_REP_CAPITAL_SIEGE_STATE); + stmt->setUInt32(0, m_lastEventDay); + stmt->setInt8(1, int8(m_lastAttackerTeam == TEAM_NEUTRAL ? -1 : int8(m_lastAttackerTeam))); + stmt->setUInt32(2, m_scheduledDay); + stmt->setUInt32(3, uint32(m_scheduledTime)); + stmt->setInt8(4, int8(m_scheduledTeam == TEAM_NEUTRAL ? -1 : int8(m_scheduledTeam))); + CharacterDatabase.Execute(stmt); +} + +void CapitalSiegeMgr::OpenHistoryRow(std::string const& trigger) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CAPITAL_SIEGE_HISTORY); + stmt->setUInt32(0, uint32(m_startTime)); + stmt->setInt8(1, int8(m_attackerTeam)); + stmt->setUInt16(2, uint16(m_target ? m_target->mapId : 0)); + stmt->setUInt32(3, m_target ? m_target->bossEntry : 0); + stmt->setString(4, trigger); + CharacterDatabase.Execute(stmt); +} + +void CapitalSiegeMgr::CloseHistoryRow(CapitalSiegeOutcome outcome) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CAPITAL_SIEGE_HISTORY_CLOSE); + stmt->setUInt32(0, uint32(sWorld->GetGameTime())); + stmt->setUInt32(1, GetElapsedSeconds()); + stmt->setInt8(2, int8(outcome)); + stmt->setUInt16(3, uint16(m_botsSpawned)); + stmt->setUInt16(4, uint16(m_botsLost)); + CharacterDatabase.Execute(stmt); +} + +void CapitalSiegeMgr::CloseOrphanHistoryRows() +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_CAPITAL_SIEGE_HISTORY_ORPHAN); + stmt->setUInt32(0, uint32(sWorld->GetGameTime())); + stmt->setInt8(1, int8(SIEGE_OUTCOME_INTERRUPTED)); + CharacterDatabase.Execute(stmt); +} + +/******************************************************************************* + * Consultation + ******************************************************************************/ + +uint32 CapitalSiegeMgr::GetRemainingSeconds() const +{ + if (!IsRunning()) + return 0; + uint32 const elapsed = GetElapsedSeconds(); + return elapsed >= m_duration ? 0 : m_duration - elapsed; +} + +std::string CapitalSiegeMgr::GetStatusText() const +{ + char buffer[256]; + + if (!m_enabled) + return "Siege des Capitales: module desactive (siege_enable = 0)."; + + if (!IsRunning()) + return GetScheduleText(); + + char const* statusName = "?"; + switch (m_status) + { + case SIEGE_STATUS_SPAWNING: statusName = "deploiement"; break; + case SIEGE_STATUS_ASSAULT: statusName = "assaut"; break; + case SIEGE_STATUS_ENDING: statusName = "nettoyage"; break; + default: break; + } + + snprintf(buffer, sizeof(buffer), + "Siege en cours [%s]: %s attaque %s. Ecoule %u s, restant %u s, bots %u deployes / %u perdus.", + statusName, GetTeamName(m_attackerTeam), m_target ? m_target->cityName : "?", + GetElapsedSeconds(), GetRemainingSeconds(), m_botsSpawned, m_botsLost); + return buffer; +} + +std::string CapitalSiegeMgr::GetScheduleText() const +{ + char buffer[256]; + time_t const now = sWorld->GetGameTime(); + + if (m_lastEventDay == GetServerDay(now)) + { + snprintf(buffer, sizeof(buffer), + "Aucun siege en cours. Le creneau du jour est deja consomme (derniere faction attaquante: %s). Prochain tirage demain.", + GetTeamName(m_lastAttackerTeam)); + return buffer; + } + + if (!m_scheduledTime) + return "Aucun siege en cours. Tirage du jour pas encore effectue."; + + tm scheduledTm; + time_t const scheduled = m_scheduledTime; + localtime_r(&scheduled, &scheduledTm); + snprintf(buffer, sizeof(buffer), + "Aucun siege en cours. Prochain assaut: %s a %02u:%02u:%02u (dans %d s).", + GetTeamName(m_scheduledTeam), uint32(scheduledTm.tm_hour), uint32(scheduledTm.tm_min), uint32(scheduledTm.tm_sec), + int32(m_scheduledTime - now)); + return buffer; +} + +/******************************************************************************* + * Utilitaires + ******************************************************************************/ + +time_t CapitalSiegeMgr::GetDayStart(time_t t) +{ + tm localTm; + localtime_r(&t, &localTm); + localTm.tm_hour = 0; + localTm.tm_min = 0; + localTm.tm_sec = 0; + localTm.tm_isdst = -1; + return mktime(&localTm); +} + +uint32 CapitalSiegeMgr::GetServerDay(time_t t) +{ + return uint32(GetDayStart(t) / DAY); +} + +void CapitalSiegeMgr::Announce(char const* format, ...) const +{ + char buffer[512]; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + + sWorld->SendGlobalText(buffer, nullptr); +} diff --git a/src/server/game/CapitalSiege/CapitalSiegeMgr.h b/src/server/game/CapitalSiege/CapitalSiegeMgr.h new file mode 100644 index 0000000..0e48829 --- /dev/null +++ b/src/server/game/CapitalSiege/CapitalSiegeMgr.h @@ -0,0 +1,172 @@ +/* + * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +// SylvaniaCore - Module "Siege des Capitales" +// +// Une fois par jour serveur, a une heure tiree au hasard dans une plage +// configurable, une horde de playerbots de haut niveau assaille la capitale de +// la faction adverse et tente d abattre son dirigeant. La faction attaquante +// alterne strictement d un jour a l autre. +// +// Ce gestionnaire porte l ordonnancement, la persistance et la machine a etats. +// L IA d assaut vit dans CommandSiege, le pilotage GM dans +// scripts/CapitalSiege/cs_capital_siege.cpp. + +#ifndef __CAPITALSIEGEMGR_H__ +#define __CAPITALSIEGEMGR_H__ + +#include "Common.h" +#include "Position.h" +#include "SharedDefines.h" +#include + +class Player; + +enum CapitalSiegeStatus +{ + SIEGE_STATUS_IDLE = 0, // aucun evenement en cours + SIEGE_STATUS_SPAWNING = 1, // vague en cours de connexion / equipement + SIEGE_STATUS_ASSAULT = 2, // progression vers le trone + SIEGE_STATUS_ENDING = 3 // nettoyage en cours +}; + +// Valeurs ecrites telles quelles dans capital_siege_history.outcome +enum CapitalSiegeOutcome +{ + SIEGE_OUTCOME_RUNNING = 0, // ligne ouverte, evenement en cours + SIEGE_OUTCOME_VICTORY = 1, // dirigeant abattu + SIEGE_OUTCOME_TIMEOUT = 2, // duree maximale atteinte + SIEGE_OUTCOME_CANCELLED = 3, // annulation GM + SIEGE_OUTCOME_OVERLOAD = 4, // arret d urgence, serveur sous charge + SIEGE_OUTCOME_INTERRUPTED = 5 // arret/crash du worldserver pendant l evenement +}; + +// Cible d une invasion : la capitale de la faction qui subit l assaut. +struct CapitalSiegeTarget +{ + uint32 mapId; // carte de la capitale + uint32 bossEntry; // creature_template du dirigeant + Position bossPos; // position du trone + Position stagingPos; // point de rassemblement de la horde d invasion + char const* cityName; +}; + +class TC_GAME_API CapitalSiegeMgr +{ +private: + CapitalSiegeMgr(); + ~CapitalSiegeMgr(); + +public: + CapitalSiegeMgr(CapitalSiegeMgr const&) = delete; + CapitalSiegeMgr(CapitalSiegeMgr&&) = delete; + CapitalSiegeMgr& operator=(CapitalSiegeMgr const&) = delete; + CapitalSiegeMgr& operator=(CapitalSiegeMgr&&) = delete; + + static CapitalSiegeMgr* instance(); + + // Cycle de vie ----------------------------------------------------------- + void LoadConfig(); // (re)lecture de worldserver.conf + void LoadState(); // au demarrage du monde + void Update(uint32 diff); // tick monde + + // Pilotage --------------------------------------------------------------- + bool StartSiege(TeamId attacker, std::string const& trigger); + void StopSiege(CapitalSiegeOutcome outcome); + + // Consultation ----------------------------------------------------------- + bool IsEnabled() const { return m_enabled; } + CapitalSiegeStatus GetStatus() const { return m_status; } + bool IsRunning() const { return m_status != SIEGE_STATUS_IDLE; } + TeamId GetAttackerTeam() const { return m_attackerTeam; } + TeamId GetScheduledTeam() const { return m_scheduledTeam; } + CapitalSiegeTarget const* GetTarget() const { return m_target; } + uint32 GetElapsedSeconds() const { return m_elapsed / IN_MILLISECONDS; } + uint32 GetRemainingSeconds() const; + std::string GetStatusText() const; + std::string GetScheduleText() const; + + // Parametres exposes aux etapes suivantes (spawner, IA d assaut, boss) + uint32 GetBotCount() const { return m_botCount; } + uint32 GetBotLevel() const { return m_botLevel; } + uint32 GetSpawnRate() const { return m_spawnRate; } + uint32 GetPvpMode() const { return m_pvpMode; } + uint32 GetBossLevel() const { return m_bossLevel; } + uint32 GetBossHealthMult() const { return m_bossHealthMult; } + + static char const* GetTeamName(TeamId team); + static char const* GetOutcomeName(CapitalSiegeOutcome outcome); + +private: + // Ordonnancement --------------------------------------------------------- + void UpdateSchedule(time_t now); + void DrawScheduleForDay(time_t now); + bool IsInsideWindow(time_t now) const; + + // Deroulement ------------------------------------------------------------ + void UpdateRunningSiege(uint32 diff); + bool IsServerOverloaded(uint32 diff); + + // Persistance ------------------------------------------------------------ + void SaveState(); + void OpenHistoryRow(std::string const& trigger); + void CloseHistoryRow(CapitalSiegeOutcome outcome); + void CloseOrphanHistoryRows(); + + // Utilitaires ------------------------------------------------------------ + static time_t GetDayStart(time_t t); + static uint32 GetServerDay(time_t t); + void Announce(char const* format, ...) const ATTR_PRINTF(2, 3); + +private: + // Configuration + bool m_enabled; + uint32 m_hourMin; + uint32 m_hourMax; + uint32 m_botCount; + uint32 m_botLevel; + uint32 m_duration; // secondes + uint32 m_spawnRate; // bots par seconde + uint32 m_pvpMode; // 0 = PNJ seuls, 1 = joueurs flagges PvP, 2 = tous + uint32 m_bossLevel; + uint32 m_bossHealthMult; + uint32 m_maxDiff; // ms, seuil d arret d urgence (0 = desactive) + bool m_announce; + CapitalSiegeTarget m_targets[2]; // indexe par TeamId de l attaquant + + // Etat persiste + uint32 m_lastEventDay; // dernier jour ou un evenement a ete consomme + TeamId m_lastAttackerTeam; // derniere faction attaquante (alternance) + uint32 m_scheduledDay; // jour du tirage courant + time_t m_scheduledTime; // horodatage du declenchement tire + TeamId m_scheduledTeam; + + // Etat volatil + CapitalSiegeStatus m_status; + TeamId m_attackerTeam; + CapitalSiegeTarget const* m_target; + uint32 m_elapsed; // ms depuis le debut de l evenement + uint32 m_updateTimer; // ms, cadence le tick a la seconde + uint32 m_overloadTimer; // ms passees au-dessus du seuil de charge + uint32 m_botsSpawned; + uint32 m_botsLost; + time_t m_startTime; +}; + +#define sCapitalSiegeMgr CapitalSiegeMgr::instance() + +#endif // __CAPITALSIEGEMGR_H__ diff --git a/src/server/scripts/CapitalSiege/capital_siege_world.cpp b/src/server/scripts/CapitalSiege/capital_siege_world.cpp new file mode 100644 index 0000000..760c964 --- /dev/null +++ b/src/server/scripts/CapitalSiege/capital_siege_world.cpp @@ -0,0 +1,62 @@ +/* + * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +// SylvaniaCore - Module "Siege des Capitales" +// +// Branchement du gestionnaire sur la boucle du monde. Le module ne modifie pas +// World.cpp : tout passe par les hooks WorldScript, dans le sens autorise +// scripts -> game. + +#include "CapitalSiegeMgr.h" +#include "ScriptMgr.h" + +class capital_siege_world : public WorldScript +{ +public: + capital_siege_world() : WorldScript("capital_siege_world") { } + + // Lecture des cles de configuration, au demarrage comme sur .reload config. + void OnConfigLoad(bool /*reload*/) override + { + sCapitalSiegeMgr->LoadConfig(); + } + + // Rechargement de l ordonnancement persiste et fermeture d un evenement + // reste ouvert si le worldserver s est arrete en plein siege. + void OnStartup() override + { + sCapitalSiegeMgr->LoadState(); + } + + void OnUpdate(uint32 diff) override + { + sCapitalSiegeMgr->Update(diff); + } + + // Arret propre : la ligne d historique est refermee tout de suite plutot + // que d attendre le prochain demarrage du core. + void OnShutdown() override + { + if (sCapitalSiegeMgr->IsRunning()) + sCapitalSiegeMgr->StopSiege(SIEGE_OUTCOME_INTERRUPTED); + } +}; + +void AddSC_capital_siege_world() +{ + new capital_siege_world(); +} diff --git a/src/server/scripts/CapitalSiege/capitalsiege_script_loader.cpp b/src/server/scripts/CapitalSiege/capitalsiege_script_loader.cpp new file mode 100644 index 0000000..1314a30 --- /dev/null +++ b/src/server/scripts/CapitalSiege/capitalsiege_script_loader.cpp @@ -0,0 +1,27 @@ +/* + * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +// This is where scripts' loading functions should be declared: + +void AddSC_capital_siege_world(); +void AddSC_capital_siege_commandscript(); + +void AddCapitalSiegeScripts() +{ + AddSC_capital_siege_world(); + AddSC_capital_siege_commandscript(); +} diff --git a/src/server/scripts/CapitalSiege/cs_capital_siege.cpp b/src/server/scripts/CapitalSiege/cs_capital_siege.cpp new file mode 100644 index 0000000..f6d58b0 --- /dev/null +++ b/src/server/scripts/CapitalSiege/cs_capital_siege.cpp @@ -0,0 +1,192 @@ +/* + * This file is part of the DestinyCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +// SylvaniaCore - Module "Siege des Capitales" +// +// Commandes GM : +// .siege status etat courant ou prochain declenchement +// .siege start [alliance|horde] declenchement manuel (faction par defaut : +// celle que l alternance a tiree pour aujourd hui) +// .siege stop annulation de l evenement en cours +// .siege history les dix derniers evenements +// +// Le declenchement manuel consomme le creneau du jour, exactement comme un +// declenchement automatique : pas de double siege dans la meme journee. + +#include "CapitalSiegeMgr.h" +#include "Chat.h" +#include "DatabaseEnv.h" +#include "Player.h" +#include "RBAC.h" +#include "ScriptMgr.h" +#include "WorldSession.h" + +#include +#include + +namespace +{ + std::string FormatTimestamp(uint32 unixTime) + { + if (!unixTime) + return "-"; + + time_t const t = time_t(unixTime); + tm localTm; + localtime_r(&t, &localTm); + + char buffer[32]; + strftime(buffer, sizeof(buffer), "%d/%m %H:%M", &localTm); + return buffer; + } +} + +static bool HandleSiegeStatus(ChatHandler* handler, char const* /*args*/) +{ + handler->SendSysMessage(sCapitalSiegeMgr->GetStatusText().c_str()); + return true; +} + +static bool HandleSiegeStart(ChatHandler* handler, char const* args) +{ + if (!sCapitalSiegeMgr->IsEnabled()) + { + handler->SendSysMessage("Le module Siege des Capitales est desactive (siege_enable = 0)."); + handler->SetSentErrorMessage(true); + return false; + } + + if (sCapitalSiegeMgr->IsRunning()) + { + handler->SendSysMessage("Un siege est deja en cours. Utilisez .siege stop pour l interrompre."); + handler->SetSentErrorMessage(true); + return false; + } + + TeamId attacker = TEAM_NEUTRAL; + if (args && *args) + { + if (!strncmp(args, "alliance", 8)) + attacker = TEAM_ALLIANCE; + else if (!strncmp(args, "horde", 5)) + attacker = TEAM_HORDE; + else + { + handler->SendSysMessage("Syntaxe : .siege start [alliance|horde]"); + handler->SetSentErrorMessage(true); + return false; + } + } + else + { + // Sans argument, on respecte l alternance : la faction qui devait + // attaquer aujourd hui. + attacker = sCapitalSiegeMgr->GetScheduledTeam(); + if (attacker == TEAM_NEUTRAL) + attacker = TEAM_ALLIANCE; + } + + std::string trigger = "GM"; + if (Player* gm = handler->GetSession() ? handler->GetSession()->GetPlayer() : nullptr) + trigger = gm->GetName(); + + if (!sCapitalSiegeMgr->StartSiege(attacker, trigger)) + { + handler->SendSysMessage("Impossible de declencher le siege."); + handler->SetSentErrorMessage(true); + return false; + } + + handler->PSendSysMessage("Siege declenche : assaut %s.", CapitalSiegeMgr::GetTeamName(attacker)); + return true; +} + +static bool HandleSiegeStop(ChatHandler* handler, char const* /*args*/) +{ + if (!sCapitalSiegeMgr->IsRunning()) + { + handler->SendSysMessage("Aucun siege en cours."); + handler->SetSentErrorMessage(true); + return false; + } + + sCapitalSiegeMgr->StopSiege(SIEGE_OUTCOME_CANCELLED); + handler->SendSysMessage("Siege annule et nettoye."); + return true; +} + +static bool HandleSiegeHistory(ChatHandler* handler, char const* /*args*/) +{ + CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CAPITAL_SIEGE_HISTORY); + PreparedQueryResult result = CharacterDatabase.Query(stmt); + if (!result) + { + handler->SendSysMessage("Aucun siege dans l historique."); + return true; + } + + handler->SendSysMessage("Derniers sieges (date, attaquant, issue, duree, bots) :"); + do + { + Field* fields = result->Fetch(); + uint32 const startTime = fields[0].GetUInt32(); + int8 const attackerTeam = fields[1].GetInt8(); + int8 const outcome = fields[2].GetInt8(); + uint32 const duration = fields[3].GetUInt32(); + uint16 const botsSpawned = fields[4].GetUInt16(); + uint16 const botsLost = fields[5].GetUInt16(); + std::string const trigger = fields[6].GetString(); + + handler->PSendSysMessage(" %s | %s | %s | %u s | %u deployes, %u perdus | %s", + FormatTimestamp(startTime).c_str(), + CapitalSiegeMgr::GetTeamName(attackerTeam < 0 ? TEAM_NEUTRAL : TeamId(attackerTeam)), + CapitalSiegeMgr::GetOutcomeName(CapitalSiegeOutcome(outcome)), + duration, uint32(botsSpawned), uint32(botsLost), trigger.c_str()); + } + while (result->NextRow()); + + return true; +} + +class capital_siege_commandscript : public CommandScript +{ +public: + capital_siege_commandscript() : CommandScript("capital_siege_commandscript") { } + + std::vector GetCommands() const override + { + static std::vector siegeCommandTable = + { + { "status", rbac::RBAC_PERM_COMMAND_EVENT, true, &HandleSiegeStatus, "" }, + { "start", rbac::RBAC_PERM_COMMAND_EVENT, true, &HandleSiegeStart, "" }, + { "stop", rbac::RBAC_PERM_COMMAND_EVENT, true, &HandleSiegeStop, "" }, + { "history", rbac::RBAC_PERM_COMMAND_EVENT, true, &HandleSiegeHistory, "" }, + }; + + static std::vector commandTable = + { + { "siege", rbac::RBAC_PERM_COMMAND_EVENT, true, nullptr, "", siegeCommandTable }, + }; + + return commandTable; + } +}; + +void AddSC_capital_siege_commandscript() +{ + new capital_siege_commandscript(); +} diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index c3e89c2..97f9fc9 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -4621,4 +4621,141 @@ pbotbg_idlelogout = 300 pbotasl = 88 # -#################################################################################################### \ No newline at end of file +#################################################################################################### +################################################################################################### +# MODULE SIEGE DES CAPITALES (SylvaniaCore) +# +# siege_enable +# Description: Evenement quotidien d invasion. Une fois par jour serveur, a une +# heure tiree au hasard dans la plage horaire ci-dessous, une horde +# de playerbots assaille la capitale de la faction adverse et tente +# d abattre son dirigeant. La faction attaquante alterne strictement +# d un jour a l autre. +# Default: 0 - Desactive +# 1 - Active +# + +siege_enable = 0 + +# +# siege_hour_min / siege_hour_max +# Description: Plage horaire (heure locale du serveur) dans laquelle l heure de +# declenchement est tiree. Bornes en heures pleines, min inclus, +# max exclu. 18 / 24 = tirage entre 18h00m00s et 23h59m59s. +# Si le worldserver etait arrete a l heure tiree, l evenement n est +# rattrape que tant qu on est encore dans cette plage. +# Default: 18 / 24 +# + +siege_hour_min = 18 +siege_hour_max = 24 + +# +# siege_botcount +# Description: Nombre de playerbots composant la horde d invasion. +# Plafonne a 100 par securite. +# Default: 50 +# + +siege_botcount = 50 + +# +# siege_botlevel +# Description: Niveau auquel les bots de l invasion sont ajustes. +# Default: 110 +# + +siege_botlevel = 110 + +# +# siege_duration +# Description: Duree maximale d un evenement, en secondes. Passe ce delai +# l assaut est declare repousse et les bots restants sont retires. +# Default: 3600 - une heure +# + +siege_duration = 3600 + +# +# siege_spawn_rate +# Description: Nombre de bots connectes et deployes par seconde. Etale le cout +# du spawn pour eviter un pic de charge au declenchement. +# Default: 5 +# + +siege_spawn_rate = 5 + +# +# siege_pvp +# Description: Comportement des bots vis-a-vis des joueurs reels rencontres. +# Default: 1 - N engagent que les joueurs deja flagges PvP +# 0 - Ignorent les joueurs, n attaquent que gardes et PNJ +# 2 - Engagent tout joueur de la faction adverse +# + +siege_pvp = 1 + +# +# siege_boss_level / siege_boss_hp_mult +# Description: Ajustement applique au dirigeant pendant l evenement, en memoire +# uniquement (jamais ecrit en base : un arret du serveur pendant le +# siege laisse la creature intacte). Le multiplicateur de points de +# vie est un pourcentage applique aux PV de base. +# Default: 112 / 50 - niveau 112, PV x50 +# + +siege_boss_level = 112 +siege_boss_hp_mult = 50 + +# +# siege_maxdiff +# Description: Arret d urgence. Si le temps de mise a jour du monde depasse cette +# valeur (en ms) pendant plus de dix secondes consecutives, l +# evenement en cours est interrompu et nettoye. +# 0 = surveillance desactivee. +# Default: 400 +# + +siege_maxdiff = 400 + +# +# siege_announce +# Description: Annonce serveur au declenchement et a la fin de l evenement. +# Default: 1 - Active +# 0 - Desactive +# + +siege_announce = 1 + +# +# Cible assaillie par la Horde : la capitale de l Alliance (Hurlevent). +# siege_alliance_map carte de la capitale +# siege_alliance_boss_entry creature_template du dirigeant +# siege_alliance_boss_* position du trone +# siege_alliance_staging_* point de rassemblement de la horde d invasion +# + +siege_alliance_map = 0 +siege_alliance_boss_entry = 107574 +siege_alliance_boss_x = -8363.3 +siege_alliance_boss_y = 232.5 +siege_alliance_boss_z = 157.1 +siege_alliance_staging_x = -8833.1 +siege_alliance_staging_y = 622.8 +siege_alliance_staging_z = 93.9 + +# +# Cible assaillie par l Alliance : la capitale de la Horde (Orgrimmar). +# + +siege_horde_map = 1 +siege_horde_boss_entry = 42283 +siege_horde_boss_x = 1924.4 +siege_horde_boss_y = -4144.1 +siege_horde_boss_z = 40.6 +siege_horde_staging_x = 1570.0 +siege_horde_staging_y = -4397.4 +siege_horde_staging_z = 16.0 + +# +###################################################################################################