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
+
+#
+###################################################################################################