diff --git a/cmake/modules/BaseConfig.cmake b/cmake/modules/BaseConfig.cmake index 14dc6e22153..0dc74b85ae0 100644 --- a/cmake/modules/BaseConfig.cmake +++ b/cmake/modules/BaseConfig.cmake @@ -36,6 +36,7 @@ find_package(pugixml CONFIG REQUIRED) find_package(spdlog REQUIRED) find_package(unofficial-argon2 CONFIG REQUIRED) find_package(unofficial-libmariadb CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) find_path(BOOST_DI_INCLUDE_DIRS "boost/di.hpp") diff --git a/cmake/modules/CanaryLib.cmake b/cmake/modules/CanaryLib.cmake index 916970c37ea..3e6bfc67eb0 100644 --- a/cmake/modules/CanaryLib.cmake +++ b/cmake/modules/CanaryLib.cmake @@ -85,6 +85,7 @@ target_link_libraries(${PROJECT_NAME}_lib spdlog::spdlog unofficial::argon2::libargon2 unofficial::libmariadb + nlohmann_json::nlohmann_json protobuf ) diff --git a/data/json/events.json b/data/json/events.json new file mode 100644 index 00000000000..4dcfff18ed1 --- /dev/null +++ b/data/json/events.json @@ -0,0 +1,179 @@ +{ + "events": [ + { + "name": "Forge Time", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "forge_time.lua", + "description": "Increases the success rate of forges by 20%.", + "colors": { + "colordark": "#2b3e50", + "colorlight": "#3d5a73" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Double Bestiary", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "double_bestiary.lua", + "description": "Doubles the bestiary counter when defeating monsters.", + "colors": { + "colordark": "#3a4f2a", + "colorlight": "#4f713a" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Fast Exercise", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "fast_exercise.lua", + "description": "Exercise weapons are faster, doubling their speed.", + "colors": { + "colordark": "#5a3a2a", + "colorlight": "#7b4f3a" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "50% Loot Bonus", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "", + "ingame": { + "lootrate": 150 + }, + "description": "Increases loot by 50%.", + "colors": { + "colordark": "#2b1e10", + "colorlight": "#4a2e18" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "50% Exp Bonus", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "", + "ingame": { + "exprate": 150 + }, + "description": "Increases experience by 50%.", + "colors": { + "colordark": "#234d00", + "colorlight": "#3a7500" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Boss Cooldown Reduction", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "boss_cooldown.lua", + "description": "Reduces boss cooldown time by 50%.", + "colors": { + "colordark": "#2d3c5a", + "colorlight": "#4a5f7d" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Double Exp", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "", + "ingame": { + "exprate": 200 + }, + "description": "Double experience when hunting monsters.", + "colors": { + "colordark": "#002d00", + "colorlight": "#004400" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + }, + { + "name": "Double Loot", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "", + "ingame": { + "lootrate": 200 + }, + "description": "Doubles the amount of loot obtained from monsters.", + "colors": { + "colordark": "#2a1d00", + "colorlight": "#4c3300" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + }, + { + "name": "Double Bosstiary", + "startdate": "08/11/2024", + "enddate": "11/11/2024", + "script": "double_bosstiary.lua", + "description": "Doubles the bestiary counter for bosses.", + "colors": { + "colordark": "#3a005a", + "colorlight": "#4e0075" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Fast Respawn", + "startdate": "11/01/2024", + "enddate": "11/15/2024", + "script": "", + "ingame": { + "spawnrate": 200 + }, + "description": "Monsters respawn twice as fast.", + "colors": { + "colordark": "#4d2f00", + "colorlight": "#6e3e00" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + } + ] +} diff --git a/data/json/eventscheduler/README.md b/data/json/eventscheduler/README.md new file mode 100644 index 00000000000..d79ee8922d6 --- /dev/null +++ b/data/json/eventscheduler/README.md @@ -0,0 +1,146 @@ +# Events Scheduler Configuration - README + +This README provides instructions on how to configure and use the `events.json` file for scheduling events in the Otservbr server environment. + +## Overview + +The `events.json` file allows you to schedule various in-game events, defining their properties such as start and end dates, experience rate, loot rate, and more. This allows you to create custom in-game experiences at specific times. + +The file contains an array of events, with each event having various attributes that control the properties of the event. + +## JSON Structure + +Here is a breakdown of the `events.json` structure: + +```json +{ + "events": [ + { + "name": "Otservbr example 1", + "startdate": "11/03/2020", + "enddate": "12/30/2025", + "script": "example.lua", + "ingame": { + "exprate": 100, + "lootrate": 100, + "bosslootrate": 100, + "spawnrate": 100, + "skillrate": 100 + }, + "description": "Otserver br example 1 description double exp and a half, double loot !chance!, regular spawn and double skill", + "colors": { + "colordark": "#235c00", + "colorlight": "#2d7400" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 0 + } + }, + { + "name": "Otservbr example 2", + "startdate": "2/2/2022", + "enddate": "12/31/2025", + "script": "", + "ingame": { + "exprate": 100, + "lootrate": 100, + "bosslootrate": 100, + "spawnrate": 100, + "skillrate": 100 + }, + "description": "Otserver br example 2 description 50% less exp, triple loot !chance!, 50% faster spawn and regular skill", + "colors": { + "colordark": "#735D10", + "colorlight": "#8B6D05" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 0 + } + } + ] +} +``` + +## Attributes Explained + +### Event Attributes + +- **name**: The name of the event. +- **startdate**: The start date of the event in the format `mm/dd/yyyy`. +- **enddate**: The end date of the event in the format `mm/dd/yyyy`. +- **script**: The script to be executed during the event (optional). + +### In-game Attributes (`ingame`) + +- **exprate**: The experience rate during the event. +- **lootrate**: The loot rate during the event. +- **bosslootrate**: The boss loot rate during the event. +- **spawnrate**: The spawn rate of monsters during the event. +- **skillrate**: The skill rate during the event. + +### Description + +- **description**: A textual description of the event, explaining its features. + +### Colors (`colors`) + +- **colordark**: The dark color associated with the event (in hexadecimal). +- **colorlight**: The light color associated with the event (in hexadecimal). + +### Event Details (`details`) + +- **displaypriority**: The priority level for displaying the event. +- **isseasonal**: Indicates whether the event is seasonal (`0` for no, `1` for yes). +- **specialevent**: Indicates whether the event is considered a special event (`0` for no, `1` for yes). + +## How to Add a New Event + +To add a new event: +1. Open the `events.json` file. +2. Add a new object to the `events` array with the appropriate details. Make sure to follow the structure outlined above. +3. Ensure that the `startdate` and `enddate` are in the correct format (`mm/dd/yyyy`), and adjust the in-game rates and other properties as needed. + +### Example +Here is an example of a new event you could add: + +```json +{ + "name": "Double XP Weekend", + "startdate": "01/01/2026", + "enddate": "01/03/2026", + "script": "double_xp.lua", + "ingame": { + "exprate": 200, + "lootrate": 100, + "bosslootrate": 150, + "spawnrate": 120, + "skillrate": 150 + }, + "description": "Enjoy a double XP weekend with increased boss loot and spawn rates!", + "colors": { + "colordark": "#1a2b3c", + "colorlight": "#3c4d5e" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } +} +``` + +## Notes +- Make sure the JSON file is well-formed. You can use an online JSON validator to check for syntax errors. +- Always back up your `events.json` file before making major changes. + +## Troubleshooting +- **JSON Parsing Errors**: If you encounter JSON parsing errors, check for missing commas or incorrect formatting in your file. +- **Missing Event Details**: Ensure all required attributes are properly defined for each event. + +## Contact +For additional help, contact the support team or refer to the project's documentation. + diff --git a/data/json/eventscheduler/events.json b/data/json/eventscheduler/events.json new file mode 100644 index 00000000000..d64ca377176 --- /dev/null +++ b/data/json/eventscheduler/events.json @@ -0,0 +1,184 @@ +{ + "events": [ + { + "name": "Forge Time", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "forgechance": 120 + }, + "description": "Increases the success rate of forges by 20%.", + "colors": { + "colordark": "#2b3e50", + "colorlight": "#3d5a73" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Double Bestiary", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "doublebestiary": true + }, + "description": "Doubles the bestiary counter when defeating monsters.", + "colors": { + "colordark": "#3a4f2a", + "colorlight": "#4f713a" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Fast Exercise", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "doubleexercise": true + }, + "description": "Exercise weapons are faster, doubling their speed.", + "colors": { + "colordark": "#5a3a2a", + "colorlight": "#7b4f3a" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "50% Loot Bonus", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "lootrate": 150 + }, + "description": "Increases loot by 50%.", + "colors": { + "colordark": "#2b1e10", + "colorlight": "#4a2e18" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "50% Exp Bonus", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "exprate": 150 + }, + "description": "Increases experience by 50%.", + "colors": { + "colordark": "#234d00", + "colorlight": "#3a7500" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Boss Cooldown Reduction", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "bosscooldown": 150 + }, + "description": "Reduces boss cooldown time by 50%.", + "colors": { + "colordark": "#2d3c5a", + "colorlight": "#4a5f7d" + }, + "details": { + "displaypriority": 6, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Double Exp", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "exprate": 200 + }, + "description": "Double experience when hunting monsters.", + "colors": { + "colordark": "#002d00", + "colorlight": "#004400" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + }, + { + "name": "Double Loot", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "lootrate": 200 + }, + "description": "Doubles the amount of loot obtained from monsters.", + "colors": { + "colordark": "#2a1d00", + "colorlight": "#4c3300" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + }, + { + "name": "Double Bosstiary", + "startdate": "11/12/2024", + "enddate": "11/17/2024", + "ingame": { + "doublebosstiary": true + }, + "description": "Doubles the bestiary counter for bosses.", + "colors": { + "colordark": "#3a005a", + "colorlight": "#4e0075" + }, + "details": { + "displaypriority": 5, + "isseasonal": 0, + "specialevent": 1 + } + }, + { + "name": "Fast Respawn", + "startdate": "11/01/2024", + "enddate": "11/15/2024", + "ingame": { + "spawnrate": 200 + }, + "description": "Monsters respawn twice as fast.", + "colors": { + "colordark": "#4d2f00", + "colorlight": "#6e3e00" + }, + "details": { + "displaypriority": 6, + "isseasonal": 1, + "specialevent": 1 + } + } + ] +} diff --git a/data/json/eventscheduler/scripts/example.lua b/data/json/eventscheduler/scripts/example.lua new file mode 100644 index 00000000000..c39ef24c2ea --- /dev/null +++ b/data/json/eventscheduler/scripts/example.lua @@ -0,0 +1,4 @@ +local globalEvent = GlobalEvent("EventScheduleExample") +function globalEvent.onStartup() end + +globalEvent:register() diff --git a/data/scripts/actions/items/exercise_training_weapons.lua b/data/scripts/actions/items/exercise_training_weapons.lua index 3c62d7c1183..675ca681d5e 100644 --- a/data/scripts/actions/items/exercise_training_weapons.lua +++ b/data/scripts/actions/items/exercise_training_weapons.lua @@ -121,8 +121,15 @@ local function exerciseTrainingEvent(playerId, tilePosition, weaponId, dummyId) return false end + local eventSpeedMultiplier = 1 + local scopedFastExercise = KV.scoped("eventscheduler"):get("fast-exercise") + if scopedFastExercise then + eventSpeedMultiplier = 0.5 + logger.debug("Fast exercise is enabled.") + end + local vocation = player:getVocation() - _G.OnExerciseTraining[playerId].event = addEvent(exerciseTrainingEvent, vocation:getBaseAttackSpeed() / configManager.getFloat(configKeys.RATE_EXERCISE_TRAINING_SPEED), playerId, tilePosition, weaponId, dummyId) + _G.OnExerciseTraining[playerId].event = addEvent(exerciseTrainingEvent, (vocation:getBaseAttackSpeed() / configManager.getFloat(configKeys.RATE_EXERCISE_TRAINING_SPEED)) * eventSpeedMultiplier, playerId, tilePosition, weaponId, dummyId) return true end diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 8dfbcfc954c..ba50f0e6e68 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -349,7 +349,6 @@ void CanaryServer::loadModules() { // Load XML folder dependencies (order matters) modulesLoadHelper(g_vocations().loadFromXml(), "XML/vocations.xml"); - modulesLoadHelper(g_eventsScheduler().loadScheduleEventFromXml(), "XML/events.xml"); modulesLoadHelper(Outfits::getInstance().loadFromXml(), "XML/outfits.xml"); modulesLoadHelper(Familiars::getInstance().loadFromXml(), "XML/familiars.xml"); modulesLoadHelper(g_imbuements().loadFromXml(), "XML/imbuements.xml"); @@ -376,6 +375,10 @@ void CanaryServer::loadModules() { modulesLoadHelper(g_scripts().loadScripts(datapackFolder + "/monster", false, false), datapackFolder + "/monster"); modulesLoadHelper((g_npcs().load(false, true)), "npc"); + // It needs to be loaded after the revscript is read in order to use the scripting interface + modulesLoadHelper(g_eventsScheduler().loadScheduleEventFromXml(), "XML/events.xml"); + modulesLoadHelper(g_eventsScheduler().loadScheduleEventFromJson(), "json/eventscheduler/events.json"); + g_game().loadBoostedCreature(); g_ioBosstiary().loadBoostedBoss(); g_ioprey().initializeTaskHuntOptions(); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 4335db2bd53..b5d56e25ebd 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -5714,6 +5714,14 @@ void Player::addBestiaryKill(const std::shared_ptr &mType) { return; } uint32_t kills = g_configManager().getNumber(BESTIARY_KILL_MULTIPLIER); + + auto scopedDoubleBestiary = g_kv().scoped("eventscheduler")->get("double-bestiary"); + bool doubleBestiaryEnabled = scopedDoubleBestiary && scopedDoubleBestiary->get(); + if (doubleBestiaryEnabled) { + kills *= 2; + g_logger().info("[{}] double bestiary is enabled.", std::source_location::current().function_name()); + } + if (isConcoctionActive(Concoction_t::BestiaryBetterment)) { kills *= 2; } @@ -5725,6 +5733,14 @@ void Player::addBosstiaryKill(const std::shared_ptr &mType) { return; } uint32_t kills = g_configManager().getNumber(BOSSTIARY_KILL_MULTIPLIER); + + auto scopedDoubleBosstiary = g_kv().scoped("eventscheduler")->get("double-bosstiary"); + bool doubleBosstiaryEnabled = scopedDoubleBosstiary && scopedDoubleBosstiary->get(); + if (doubleBosstiaryEnabled) { + kills *= 2; + g_logger().info("[{}] double bosstiary is enabled.", std::source_location::current().function_name()); + } + if (g_ioBosstiary().getBoostedBossId() == mType->info.raceid) { kills *= g_configManager().getNumber(BOOSTED_BOSS_KILL_BONUS); } diff --git a/src/game/game.cpp b/src/game/game.cpp index 7a492348649..cf37c63c131 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -9531,6 +9531,11 @@ void Game::playerForgeFuseItems(uint32_t playerId, ForgeAction_t actionType, uin uint8_t coreCount = (usedCore ? 1 : 0) + (reduceTierLoss ? 1 : 0); auto baseSuccess = static_cast(g_configManager().getNumber(FORGE_BASE_SUCCESS_RATE)); + if (const auto scopedForgeChance = g_kv().scoped("eventscheduler")->get("forge-chance")) { + auto forgeChance = static_cast(scopedForgeChance->getNumber()); + g_logger().info("Base success: {}, forge chance: {}", baseSuccess, forgeChance); + baseSuccess += forgeChance; + } auto coreSuccess = usedCore ? g_configManager().getNumber(FORGE_BONUS_SUCCESS_RATE) : 0; auto finalRate = baseSuccess + coreSuccess; auto roll = static_cast(uniform_random(1, 100)) <= finalRate; diff --git a/src/game/scheduling/events_scheduler.cpp b/src/game/scheduling/events_scheduler.cpp index 4a9bb017335..01eca7cda8f 100644 --- a/src/game/scheduling/events_scheduler.cpp +++ b/src/game/scheduling/events_scheduler.cpp @@ -12,6 +12,164 @@ #include "config/configmanager.hpp" #include "lua/scripts/scripts.hpp" +#include + +bool EventsScheduler::loadScheduleEventFromJson() { + g_kv().scoped("eventscheduler")->remove("forge-chance"); + g_kv().scoped("eventscheduler")->remove("double-bestiary"); + g_kv().scoped("eventscheduler")->remove("double-bosstiary"); + g_kv().scoped("eventscheduler")->remove("fast-exercise"); + g_kv().scoped("eventscheduler")->remove("boss-cooldown"); + + using json = nlohmann::json; + auto coreFolder = g_configManager().getString(CORE_DIRECTORY); + auto folder = coreFolder + "/json/eventscheduler/events.json"; + std::ifstream file(folder); + if (!file.is_open()) { + g_logger().error("{} - Unable to open file '{}'", __FUNCTION__, folder); + consoleHandlerExit(); + return false; + } + + json eventsJson; + try { + file >> eventsJson; + } catch (const json::parse_error &e) { + g_logger().error("{} - JSON parsing error in file '{}': {}", __FUNCTION__, folder, e.what()); + consoleHandlerExit(); + return false; + } + + time_t t = time(nullptr); + const tm* timePtr = localtime(&t); + int daysMath = ((timePtr->tm_year + 1900) * 365) + ((timePtr->tm_mon + 1) * 30) + (timePtr->tm_mday); + + phmap::flat_hash_set loadedScripts; + std::map eventsOnSameDay; + + for (const auto &event : eventsJson["events"]) { + std::string eventScript = event.contains("script") && !event["script"].is_null() ? event["script"].get() : ""; + std::string eventName = event.value("name", ""); + + if (!event.contains("startdate") || !event.contains("enddate")) { + g_logger().warn("{} - Missing 'startdate' or 'enddate' for event '{}'", __FUNCTION__, eventName); + continue; + } + + int startYear, startMonth, startDay, endYear, endMonth, endDay; + if (sscanf(event["startdate"].get().c_str(), "%d/%d/%d", &startMonth, &startDay, &startYear) != 3 || sscanf(event["enddate"].get().c_str(), "%d/%d/%d", &endMonth, &endDay, &endYear) != 3) { + g_logger().warn("{} - Invalid date format for event '{}'", __FUNCTION__, eventName); + continue; + } + + int startDays = (startYear * 365) + (startMonth * 30) + startDay; + int endDays = (endYear * 365) + (endMonth * 30) + endDay; + + if (daysMath < startDays || daysMath > endDays) { + continue; + } + + if (!eventScript.empty()) { + if (loadedScripts.contains(eventScript)) { + g_logger().warn("{} - Script declaration '{}' is duplicated in '{}'", __FUNCTION__, eventScript, folder); + continue; + } + + loadedScripts.insert(eventScript); + std::filesystem::path filePath = std::filesystem::current_path() / coreFolder / "json" / "scripts" / eventScript; + + if (!std::filesystem::exists(filePath) || !std::filesystem::is_regular_file(filePath)) { + g_logger().warn("{} - Cannot find script file '{}'", __FUNCTION__, filePath.string()); + return false; + } + + if (!g_scripts().loadEventSchedulerScripts(filePath)) { + g_logger().warn("{} - Cannot load the file '{}' on '{}/scripts/'", __FUNCTION__, eventScript, coreFolder); + return false; + } + } + + EventRates currentEventRates = { + static_cast(event.contains("ingame") && event["ingame"].contains("exprate") ? event["ingame"].value("exprate", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("lootrate") ? event["ingame"].value("lootrate", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("bosslootrate") ? event["ingame"].value("bosslootrate", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("spawnrate") ? event["ingame"].value("spawnrate", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("skillrate") ? event["ingame"].value("skillrate", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("forgechance") ? event["ingame"].value("forge-chance", 100) : 100), + static_cast(event.contains("ingame") && event["ingame"].contains("bosscooldown") ? event["ingame"].value("bosscooldown", 100) : 100), + event.contains("ingame") && event["ingame"].contains("doublebestiary") ? event["ingame"].value("doublebestiary", false) : false, + event.contains("ingame") && event["ingame"].contains("doublebosstiary") ? event["ingame"].value("doublebosstiary", false) : false, + event.contains("ingame") && event["ingame"].contains("fastexercise") ? event["ingame"].value("fastexercise", false) : false, + }; + + for (const auto &[existingEventName, rates] : eventsOnSameDay) { + std::vector modifiedRates; + + if (rates.exprate != 100 && currentEventRates.exprate != 100 && rates.exprate == currentEventRates.exprate) { + modifiedRates.emplace_back("exprate"); + g_eventsScheduler().setExpSchedule(rates.exprate); + } + if (rates.lootrate != 100 && currentEventRates.lootrate != 100 && rates.lootrate == currentEventRates.lootrate) { + modifiedRates.emplace_back("lootrate"); + g_eventsScheduler().setLootSchedule(rates.lootrate); + } + if (rates.bosslootrate != 100 && currentEventRates.bosslootrate != 100 && rates.bosslootrate == currentEventRates.bosslootrate) { + modifiedRates.emplace_back("bosslootrate"); + g_eventsScheduler().setBossLootSchedule(rates.bosslootrate); + } + if (rates.spawnrate != 100 && currentEventRates.spawnrate != 100 && rates.spawnrate == currentEventRates.spawnrate) { + modifiedRates.emplace_back("spawnrate"); + g_eventsScheduler().setSpawnMonsterSchedule(rates.spawnrate); + } + if (rates.skillrate != 100 && currentEventRates.skillrate != 100 && rates.skillrate == currentEventRates.skillrate) { + modifiedRates.emplace_back("skillrate"); + g_eventsScheduler().setSkillSchedule(rates.skillrate); + } + + // KV changes + if (rates.forgeChance != 100 && currentEventRates.forgeChance != 100 && rates.forgeChance == currentEventRates.forgeChance) { + modifiedRates.emplace_back("forge-chance"); + g_kv().scoped("eventscheduler")->set("forge-chance", rates.forgeChance - 100); + } + + if (rates.doubleBestiary != false && currentEventRates.doubleBestiary != false && rates.doubleBestiary == currentEventRates.doubleBestiary) { + modifiedRates.emplace_back("double-bestiary"); + g_kv().scoped("eventscheduler")->set("double-bestiary", true); + } + + if (rates.doubleBossTiary != false && currentEventRates.doubleBossTiary != false && rates.doubleBossTiary == currentEventRates.doubleBossTiary) { + modifiedRates.emplace_back("double-bosstiary"); + g_kv().scoped("eventscheduler")->set("double-bosstiary", true); + } + + if (rates.fastExercise != false && currentEventRates.fastExercise != false && rates.fastExercise == currentEventRates.fastExercise) { + modifiedRates.emplace_back("fast-exercise"); + g_kv().scoped("eventscheduler")->set("fast-exercise", true); + } + + if (rates.bosscooldown != 100 && currentEventRates.bosscooldown != 100 && rates.bosscooldown == currentEventRates.bosscooldown) { + modifiedRates.emplace_back("bosscooldown"); + g_kv().scoped("eventscheduler")->set("boss-cooldown", rates.bosscooldown - 100); + } + + if (!modifiedRates.empty()) { + std::string ratesString = join(modifiedRates, ", "); + g_logger().warn("{} - Events '{}' and '{}' have the same rates [{}] on the same day.", __FUNCTION__, eventName, existingEventName, ratesString); + } + } + + eventsOnSameDay[eventName] = currentEventRates; + eventScheduler.emplace_back(EventScheduler(eventName, startDays, endDays)); + } + + for (const auto &event : eventScheduler) { + if (daysMath >= event.startDays && daysMath <= event.endDays) { + g_logger().info("Active EventScheduler: {}", event.name); + } + } + return true; +} + bool EventsScheduler::loadScheduleEventFromXml() { pugi::xml_document doc; auto folder = g_configManager().getString(CORE_DIRECTORY) + "/XML/events.xml"; @@ -53,8 +211,10 @@ bool EventsScheduler::loadScheduleEventFromXml() { } loadedScripts.insert(eventScript); - if (!eventScript.empty() && !g_scripts().loadEventSchedulerScripts(eventScript)) { - g_logger().warn("{} - Can not load the file '{}' on '/events/scripts/scheduler/'", __FUNCTION__, eventScript); + auto coreFolder = g_configManager().getString(CORE_DIRECTORY); + std::filesystem::path filePath = std::filesystem::current_path() / coreFolder / "XML" / "events" / "scheduler" / "scripts" / eventScript; + if (!g_scripts().loadEventSchedulerScripts(filePath)) { + g_logger().warn("{} - Cannot load the file '{}' on '/events/scripts/scheduler/'", __FUNCTION__, eventScript); return false; } diff --git a/src/game/scheduling/events_scheduler.hpp b/src/game/scheduling/events_scheduler.hpp index fe56aeb9a12..4206896c005 100644 --- a/src/game/scheduling/events_scheduler.hpp +++ b/src/game/scheduling/events_scheduler.hpp @@ -24,6 +24,11 @@ struct EventRates { uint32_t bosslootrate = 100; uint32_t spawnrate = 100; uint16_t skillrate = 100; + uint8_t forgeChance = 100; + uint8_t bosscooldown = 100; + bool doubleBestiary {}; + bool doubleBossTiary {}; + bool fastExercise {}; }; class EventsScheduler { @@ -38,6 +43,8 @@ class EventsScheduler { return inject(); } + bool loadScheduleEventFromJson(); + // Event schedule xml load bool loadScheduleEventFromXml(); diff --git a/src/lua/scripts/scripts.cpp b/src/lua/scripts/scripts.cpp index f4a141253c4..06d6d742440 100644 --- a/src/lua/scripts/scripts.cpp +++ b/src/lua/scripts/scripts.cpp @@ -42,26 +42,19 @@ void Scripts::clearAllScripts() const { g_monsters().clear(); } -bool Scripts::loadEventSchedulerScripts(const std::string &fileName) { - auto coreFolder = g_configManager().getString(CORE_DIRECTORY); - const auto dir = std::filesystem::current_path() / coreFolder / "events" / "scripts" / "scheduler"; - if (!std::filesystem::exists(dir) || !std::filesystem::is_directory(dir)) { - g_logger().warn("{} - Can not load folder 'scheduler' on {}/events/scripts'", __FUNCTION__, coreFolder); +bool Scripts::loadEventSchedulerScripts(const std::filesystem::path &filePath) { + if (!std::filesystem::exists(filePath) || !std::filesystem::is_regular_file(filePath)) { + g_logger().warn("{} - Cannot load file '{}'", __FUNCTION__, filePath.string()); return false; } - const std::filesystem::recursive_directory_iterator endit; - for (std::filesystem::recursive_directory_iterator it(dir); it != endit; ++it) { - if (std::filesystem::is_regular_file(*it) && it->path().extension() == ".lua") { - if (it->path().filename().string() == fileName) { - if (scriptInterface.loadFile(it->path().string(), it->path().filename().string()) == -1) { - g_logger().error(it->path().string()); - g_logger().error(scriptInterface.getLastLuaError()); - continue; - } - return true; - } + if (filePath.extension() == ".lua") { + if (scriptInterface.loadFile(filePath.string(), filePath.filename().string()) == -1) { + g_logger().error(filePath.string()); + g_logger().error(scriptInterface.getLastLuaError()); + return false; } + return true; } return false; diff --git a/src/lua/scripts/scripts.hpp b/src/lua/scripts/scripts.hpp index 2efedb04232..42cc9a294e8 100644 --- a/src/lua/scripts/scripts.hpp +++ b/src/lua/scripts/scripts.hpp @@ -21,7 +21,7 @@ class Scripts { void clearAllScripts() const; - bool loadEventSchedulerScripts(const std::string &fileName); + bool loadEventSchedulerScripts(const std::filesystem::path &filePath); bool loadScripts(std::string_view folderName, bool isLib, bool reload); LuaScriptInterface &getScriptInterface() { return scriptInterface; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 2f7ff9e2479..53cb5cbc544 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -9203,7 +9203,15 @@ void ProtocolGame::sendBosstiaryCooldownTimer() { if (!timerValue || !timerValue.has_value()) { continue; } + + auto scheduleTimerOpt = g_kv().scoped("eventscheduler")->get("boss-cooldown"); + uint8_t schedulePercentage = 0; + if (scheduleTimerOpt) { + schedulePercentage = static_cast(scheduleTimerOpt->getNumber()); + } + auto timer = timerValue->getNumber(); + timer = static_cast(timer * schedulePercentage / 100); uint64_t sendTimer = timer > 0 ? static_cast(timer) : 0; msg.add(bossRaceId); // bossRaceId msg.add(sendTimer); // Boss cooldown in seconds diff --git a/vcpkg.json b/vcpkg.json index e5a83b21263..fdcffbbc9b6 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -20,6 +20,7 @@ "zlib", "bshoshany-thread-pool", "atomic-queue", + "nlohmann-json", { "name": "opentelemetry-cpp", "default-features": true,