diff --git a/conf/mod-double-xp-weekend.conf.dist b/conf/mod-double-xp-weekend.conf.dist index 9328aa2..59df975 100644 --- a/conf/mod-double-xp-weekend.conf.dist +++ b/conf/mod-double-xp-weekend.conf.dist @@ -62,6 +62,8 @@ XPWeekend.IsDKStartZoneRequired = 0 # XPWeekend.IsJoyousJourneysActive = 0 # Promotional event, adds yet another layer of bonus on top of the current bonus (yay!) +# Ignored while a window is set with the `.weekendxp joyousjourneys schedule` +# command; the scheduled window decides whether the event is active instead. # Default: 0 (Disabled) XPWeekend.IsJoyousJourneysActive = 0 diff --git a/data/sql/db-world/mod-weekend-xp-texts.sql b/data/sql/db-world/mod-weekend-xp-texts.sql index 723a2d8..5cc7d29 100644 --- a/data/sql/db-world/mod-weekend-xp-texts.sql +++ b/data/sql/db-world/mod-weekend-xp-texts.sql @@ -41,6 +41,66 @@ INSERT INTO `module_string_locale` (`module`, `id`, `locale`, `string`) VALUES ('mod-weekend-xp', 1, 'ruRU', 'Указано неверное значение. Пожалуйста, укажите значение от 0 до {}'), ('mod-weekend-xp', 2, 'ruRU', 'Применяемый к вам множитель: {}.\nТекущая конфигурация weekendxp:\nAnnounce {}\nAlwaysEnabled {}\nQuestOnly {}\nMaxLevel {}\nxpAmount {}\nIndividualXPEnabled {}\nEnabled {}\nMaxAllowedRate {}'); +DELETE FROM `module_string` WHERE `id` IN (3, 4, 5, 6, 7, 8) AND `module` = "mod-weekend-xp"; +INSERT INTO `module_string` (`module`, `id`, `string`) VALUES +("mod-weekend-xp", 3, 'No Joyous Journeys schedule is set. The event follows the XPWeekend.IsJoyousJourneysActive config (currently: {}).'), +("mod-weekend-xp", 4, 'Joyous Journeys is scheduled from {} to {} (inclusive). Currently active: {}.'), +("mod-weekend-xp", 5, 'Joyous Journeys schedule cleared. The event now follows the XPWeekend.IsJoyousJourneysActive config.'), +("mod-weekend-xp", 6, 'Invalid date ''{}''. Use the YYYY-MM-DD format.'), +("mod-weekend-xp", 7, 'The end date must not be before the start date.'), +("mod-weekend-xp", 8, '|cff00ccffThe Joyous Journeys event is active until {}! Experience gains have been increased. Type .weekendxp j off to disable it.|r'); + +DELETE FROM `module_string_locale` WHERE `module` = 'mod-weekend-xp' AND `id` IN (3, 4, 5, 6, 7, 8); +INSERT INTO `module_string_locale` (`module`, `id`, `locale`, `string`) VALUES +('mod-weekend-xp', 3, 'koKR', '설정된 기쁨의 여정 일정이 없습니다. 이벤트는 XPWeekend.IsJoyousJourneysActive 설정을 따릅니다 (현재: {}).'), +('mod-weekend-xp', 4, 'koKR', '기쁨의 여정이 {}부터 {}까지 (양일 포함) 예정되어 있습니다. 현재 활성화 여부: {}.'), +('mod-weekend-xp', 5, 'koKR', '기쁨의 여정 일정이 삭제되었습니다. 이벤트는 이제 XPWeekend.IsJoyousJourneysActive 설정을 따릅니다.'), +('mod-weekend-xp', 6, 'koKR', '잘못된 날짜 ''{}''입니다. YYYY-MM-DD 형식을 사용하세요.'), +('mod-weekend-xp', 7, 'koKR', '종료 날짜는 시작 날짜보다 빠를 수 없습니다.'), +('mod-weekend-xp', 8, 'koKR', '|cff00ccff기쁨의 여정 이벤트가 {}까지 진행됩니다! 경험치 획득량이 증가했습니다. 비활성화하려면 .weekendxp j off 를 입력하세요.|r'), +('mod-weekend-xp', 3, 'frFR', 'Aucune période n''est définie pour Joyeux Voyages. L''événement suit la configuration XPWeekend.IsJoyousJourneysActive (actuellement : {}).'), +('mod-weekend-xp', 4, 'frFR', 'Joyeux Voyages est programmé du {} au {} (inclus). Actuellement actif : {}.'), +('mod-weekend-xp', 5, 'frFR', 'La période de Joyeux Voyages a été supprimée. L''événement suit désormais la configuration XPWeekend.IsJoyousJourneysActive.'), +('mod-weekend-xp', 6, 'frFR', 'Date invalide ''{}''. Utilisez le format YYYY-MM-DD.'), +('mod-weekend-xp', 7, 'frFR', 'La date de fin ne doit pas être antérieure à la date de début.'), +('mod-weekend-xp', 8, 'frFR', '|cff00ccffL''événement Joyeux Voyages est actif jusqu''au {} ! Les gains d''expérience ont été augmentés. Tapez .weekendxp j off pour le désactiver.|r'), +('mod-weekend-xp', 3, 'deDE', 'Es ist kein Zeitraum für Freudige Reisen festgelegt. Das Event folgt der Einstellung XPWeekend.IsJoyousJourneysActive (aktuell: {}).'), +('mod-weekend-xp', 4, 'deDE', 'Freudige Reisen ist vom {} bis zum {} (einschließlich) geplant. Aktuell aktiv: {}.'), +('mod-weekend-xp', 5, 'deDE', 'Der Zeitraum für Freudige Reisen wurde entfernt. Das Event folgt nun wieder der Einstellung XPWeekend.IsJoyousJourneysActive.'), +('mod-weekend-xp', 6, 'deDE', 'Ungültiges Datum ''{}''. Bitte das Format YYYY-MM-DD verwenden.'), +('mod-weekend-xp', 7, 'deDE', 'Das Enddatum darf nicht vor dem Startdatum liegen.'), +('mod-weekend-xp', 8, 'deDE', '|cff00ccffDas Event Freudige Reisen ist bis zum {} aktiv! Die Erfahrungsgewinne wurden erhöht. Gib .weekendxp j off ein, um es zu deaktivieren.|r'), +('mod-weekend-xp', 3, 'zhCN', '未设置欢乐之旅日程。活动遵循 XPWeekend.IsJoyousJourneysActive 配置(当前:{})。'), +('mod-weekend-xp', 4, 'zhCN', '欢乐之旅已安排在 {} 至 {}(含)期间。当前是否激活:{}。'), +('mod-weekend-xp', 5, 'zhCN', '欢乐之旅日程已清除。活动现在遵循 XPWeekend.IsJoyousJourneysActive 配置。'), +('mod-weekend-xp', 6, 'zhCN', '无效日期 ''{}''。请使用 YYYY-MM-DD 格式。'), +('mod-weekend-xp', 7, 'zhCN', '结束日期不能早于开始日期。'), +('mod-weekend-xp', 8, 'zhCN', '|cff00ccff欢乐之旅活动将持续到 {}!经验值获取已提升。输入 .weekendxp j off 可将其关闭。|r'), +('mod-weekend-xp', 3, 'zhTW', '未設定歡樂之旅日程。活動遵循 XPWeekend.IsJoyousJourneysActive 設定(目前:{})。'), +('mod-weekend-xp', 4, 'zhTW', '歡樂之旅已安排在 {} 至 {}(含)期間。目前是否啟用:{}。'), +('mod-weekend-xp', 5, 'zhTW', '歡樂之旅日程已清除。活動現在遵循 XPWeekend.IsJoyousJourneysActive 設定。'), +('mod-weekend-xp', 6, 'zhTW', '無效日期 ''{}''。請使用 YYYY-MM-DD 格式。'), +('mod-weekend-xp', 7, 'zhTW', '結束日期不能早於開始日期。'), +('mod-weekend-xp', 8, 'zhTW', '|cff00ccff歡樂之旅活動將持續到 {}!經驗值獲得已提升。輸入 .weekendxp j off 可將其停用。|r'), +('mod-weekend-xp', 3, 'esES', 'No hay un periodo establecido para Viajes Alegres. El evento sigue la configuración XPWeekend.IsJoyousJourneysActive (actualmente: {}).'), +('mod-weekend-xp', 4, 'esES', 'Viajes Alegres está programado del {} al {} (inclusive). Actualmente activo: {}.'), +('mod-weekend-xp', 5, 'esES', 'Se ha eliminado el periodo de Viajes Alegres. El evento vuelve a seguir la configuración XPWeekend.IsJoyousJourneysActive.'), +('mod-weekend-xp', 6, 'esES', 'Fecha no válida ''{}''. Usa el formato YYYY-MM-DD.'), +('mod-weekend-xp', 7, 'esES', 'La fecha de fin no debe ser anterior a la fecha de inicio.'), +('mod-weekend-xp', 8, 'esES', '|cff00ccff¡El evento Viajes Alegres está activo hasta el {}! La experiencia obtenida ha aumentado. Escribe .weekendxp j off para desactivarlo.|r'), +('mod-weekend-xp', 3, 'esMX', 'No hay un periodo establecido para Viajes Alegres. El evento sigue la configuración XPWeekend.IsJoyousJourneysActive (actualmente: {}).'), +('mod-weekend-xp', 4, 'esMX', 'Viajes Alegres está programado del {} al {} (inclusive). Actualmente activo: {}.'), +('mod-weekend-xp', 5, 'esMX', 'Se eliminó el periodo de Viajes Alegres. El evento vuelve a seguir la configuración XPWeekend.IsJoyousJourneysActive.'), +('mod-weekend-xp', 6, 'esMX', 'Fecha no válida ''{}''. Usa el formato YYYY-MM-DD.'), +('mod-weekend-xp', 7, 'esMX', 'La fecha de fin no debe ser anterior a la fecha de inicio.'), +('mod-weekend-xp', 8, 'esMX', '|cff00ccff¡El evento Viajes Alegres está activo hasta el {}! La experiencia obtenida aumentó. Escribe .weekendxp j off para desactivarlo.|r'), +('mod-weekend-xp', 3, 'ruRU', 'Расписание события «Радостные путешествия» не задано. Событие следует настройке XPWeekend.IsJoyousJourneysActive (сейчас: {}).'), +('mod-weekend-xp', 4, 'ruRU', 'Событие «Радостные путешествия» запланировано с {} по {} (включительно). Сейчас активно: {}.'), +('mod-weekend-xp', 5, 'ruRU', 'Расписание события «Радостные путешествия» удалено. Событие снова следует настройке XPWeekend.IsJoyousJourneysActive.'), +('mod-weekend-xp', 6, 'ruRU', 'Неверная дата ''{}''. Используйте формат YYYY-MM-DD.'), +('mod-weekend-xp', 7, 'ruRU', 'Дата окончания не должна быть раньше даты начала.'), +('mod-weekend-xp', 8, 'ruRU', '|cff00ccffСобытие «Радостные путешествия» активно до {}! Получаемый опыт увеличен. Введите .weekendxp j off, чтобы отключить его.|r'); + DELETE FROM `command` WHERE `name` IN ('weekendxp rate'); INSERT INTO `command` (`name`, `security`, `help`) VALUES ('weekendxp rate', 0, 'Syntax: weekendxp rate $value \nSet your experience rate up to the allowed value.'); @@ -48,3 +108,7 @@ INSERT INTO `command` (`name`, `security`, `help`) VALUES DELETE FROM `command` WHERE `name` IN ('weekendxp config'); INSERT INTO `command` (`name`, `security`, `help`) VALUES ('weekendxp config', 0, 'Syntax: weekendxp config\nDisplays the current configuration for the weekendxp mod.'); + +DELETE FROM `command` WHERE `name` IN ('weekendxp joyousjourneys schedule'); +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('weekendxp joyousjourneys schedule', 3, 'Syntax: weekendxp joyousjourneys schedule [$start $end || clear]\nSchedules the Joyous Journeys event. Dates use the YYYY-MM-DD format and are inclusive; the scheduled window overrides the XPWeekend.IsJoyousJourneysActive config.\nWithout arguments the current schedule is shown, "clear" removes it.'); diff --git a/src/mod-double-xp-weekend.cpp b/src/mod-double-xp-weekend.cpp index 98c623f..e929a7c 100644 --- a/src/mod-double-xp-weekend.cpp +++ b/src/mod-double-xp-weekend.cpp @@ -2,6 +2,9 @@ #include "ScriptMgr.h" #include "Player.h" #include "Chat.h" +#include "Timer.h" +#include "WorldState.h" +#include #include using namespace Acore::ChatCommands; @@ -18,6 +21,14 @@ enum WeekendXP WD_SUNDAY = 0, }; +// Indices into the characters DB `worldstates` table. Must be unique across the +// core (see WorldStateDefines.h, custom entries start at 20001) and other modules. +enum WeekendXPWorldStates +{ + WS_JOYOUS_JOURNEYS_START = 97301, // unix timestamp, first second of the event + WS_JOYOUS_JOURNEYS_END = 97302, // unix timestamp, first second AFTER the event +}; + class DoubleXpWeekend { @@ -138,7 +149,87 @@ class DoubleXpWeekend return true; } - bool IsJoyousJourneysActive() const { return sConfigMgr->GetOption("XPWeekend.IsJoyousJourneysActive", false); } + // When a window was set via `.weekendxp joyousjourneys schedule`, the + // scheduled window decides whether the event is active. Otherwise the + // static config flag is used. + bool IsJoyousJourneysActive() const + { + if (HasJoyousJourneysSchedule()) + { + time_t now = time(nullptr); + return now >= JoyousJourneysStart() && now < JoyousJourneysEnd(); + } + + return sConfigMgr->GetOption("XPWeekend.IsJoyousJourneysActive", false); + } + + bool HasJoyousJourneysSchedule() const { return JoyousJourneysEnd() != 0; } + + std::string JoyousJourneysEndDateString() const + { + // The stored end is the first second after the event, so the last + // (inclusive) event day is one second earlier. + return FormatDate(JoyousJourneysEnd() - 1); + } + + bool HandleJoyousJourneysScheduleCommand(ChatHandler* handler, Optional startDate, Optional endDate) const + { + // PSendModuleSysMessage drops the message when there is no session, and this + // command is available from the console, so parse and send separately. + if (!startDate) + { + if (!HasJoyousJourneysSchedule()) + { + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 3, + sConfigMgr->GetOption("XPWeekend.IsJoyousJourneysActive", false))); + } + else + { + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 4, + FormatDate(JoyousJourneysStart()), JoyousJourneysEndDateString(), IsJoyousJourneysActive())); + } + + return true; + } + + if (StringEqualI(*startDate, "clear")) + { + sWorldState->setWorldState(WS_JOYOUS_JOURNEYS_START, 0); + sWorldState->setWorldState(WS_JOYOUS_JOURNEYS_END, 0); + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 5)); + return true; + } + + if (!endDate) + { + return false; // show command usage + } + + Optional start = ParseDate(*startDate); + Optional endStart = ParseDate(*endDate); + + if (!start || !endStart) + { + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 6, !start ? *startDate : *endDate)); + handler->SetSentErrorMessage(true); + return true; + } + + if (*endStart < *start) + { + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 7)); + handler->SetSentErrorMessage(true); + return true; + } + + sWorldState->setWorldState(WS_JOYOUS_JOURNEYS_START, uint64(*start)); + sWorldState->setWorldState(WS_JOYOUS_JOURNEYS_END, uint64(*endStart + DAY)); + handler->SendSysMessage(handler->PGetParseModuleString("mod-weekend-xp", 4, + FormatDate(JoyousJourneysStart()), JoyousJourneysEndDateString(), IsJoyousJourneysActive())); + + return true; + } + float ConfigJoyousJourneysXPRate() const { return sConfigMgr->GetOption("XPWeekend.JoyousJourneysXPRate", 1.0f); } float ConfigJoyousJourneysRepRate() const { return sConfigMgr->GetOption("XPWeekend.JoyousJourneysRepRate", 1.10f); } bool ExcludeInsaneReps() const { return sConfigMgr->GetOption("XPWeekend.ExcludeInsaneReps", true); } @@ -161,6 +252,42 @@ class DoubleXpWeekend return player->IsQuestRewarded(13188) || player->IsQuestRewarded(13189); } + time_t JoyousJourneysStart() const { return time_t(sWorldState->getWorldState(WS_JOYOUS_JOURNEYS_START)); } + time_t JoyousJourneysEnd() const { return time_t(sWorldState->getWorldState(WS_JOYOUS_JOURNEYS_END)); } + + // Parses YYYY-MM-DD into the first second of that day in server local time. + static Optional ParseDate(std::string const& text) + { + unsigned int year, month, day; + char extra; + if (sscanf(text.c_str(), "%4u-%2u-%2u%c", &year, &month, &day, &extra) != 3) + { + return std::nullopt; + } + + tm date{}; + date.tm_year = int(year) - 1900; + date.tm_mon = int(month) - 1; + date.tm_mday = int(day); + date.tm_isdst = -1; + + time_t stamp = mktime(&date); + + // mktime normalizes out-of-range fields (e.g. 2026-02-30 becomes March 2nd), + // so a changed day/month means the input was not a real calendar date. + if (stamp == time_t(-1) || date.tm_mday != int(day) || date.tm_mon != int(month) - 1) + { + return std::nullopt; + } + + return stamp; + } + + static std::string FormatDate(time_t when) + { + return Acore::Time::TimeToTimestampStr(Seconds(when), "%Y-%m-%d"); + } + void PlayerSettingSetRate(Player* player, float rate) const { // HACK PlayerSetting seems to store uint32 only, so save our `float` as if it was a `uint32` @@ -285,7 +412,8 @@ class weekendxp_commandscript : public CommandScript { { "weekendxp rate", HandleSetXPBonusCommand, SEC_PLAYER, Console::No }, { "weekendxp config", HandleGetCurrentConfigCommand, SEC_PLAYER, Console::No }, - { "weekendxp joyousjourneys", HandleJoyousJourneysCommand, SEC_PLAYER, Console::No } + { "weekendxp joyousjourneys", HandleJoyousJourneysCommand, SEC_PLAYER, Console::No }, + { "weekendxp joyousjourneys schedule", HandleJoyousJourneysScheduleCommand, SEC_ADMINISTRATOR, Console::Yes } }; return commandTable; @@ -317,6 +445,12 @@ class weekendxp_commandscript : public CommandScript handler->PSendSysMessage("Joyous Journeys experience boost {}.", !enable ? "disabled" : "enabled"); return true; } + + static bool HandleJoyousJourneysScheduleCommand(ChatHandler* handler, Optional startDate, Optional endDate) + { + DoubleXpWeekend* mod = DoubleXpWeekend::instance(); + return mod->HandleJoyousJourneysScheduleCommand(handler, startDate, endDate); + } }; class DoubleXpWeekendPlayerScript : public PlayerScript @@ -335,7 +469,12 @@ class DoubleXpWeekendPlayerScript : public PlayerScript mod->OnPlayerLogin(player, &handler); if (mod->IsJoyousJourneysActive() && mod->ConfigJoyousJourneysXPRate()) - handler.PSendSysMessage("|cff00ccffThe Joyous Journeys event is active! Experience gains have been increased. Type .weekendxp j off to disable it.|r"); + { + if (mod->HasJoyousJourneysSchedule()) + handler.PSendModuleSysMessage("mod-weekend-xp", 8, mod->JoyousJourneysEndDateString()); + else + handler.PSendSysMessage("|cff00ccffThe Joyous Journeys event is active! Experience gains have been increased. Type .weekendxp j off to disable it.|r"); + } } void OnPlayerGiveXP(Player* player, uint32& amount, Unit* /*victim*/, uint8 xpSource) override