diff --git a/config.lua.dist b/config.lua.dist index 57257748195..e7dabfcccaa 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -207,6 +207,7 @@ onlyPremiumAccount = false -- NOTE: buyBlessCommandFee will add fee when player buy bless by command (!bless), active changing value between 1 and 100 (fee percent. ex: 3 = 3%, 30 = 30%) -- NOTE: teleportPlayerToVocationRoom will enable oressa to teleport player to his/her room vocation -- NOTE: toggleReceiveReward = true, will enable players to choose one of reward exercise weapon by command !reward +-- NOTE: randomMonsterSpawn = true, will enable monsters from the same spawn to be randomized between them, thus making a variable hunt weatherRain = false thunderEffect = false allConsoleLog = false @@ -222,6 +223,7 @@ buyAolCommandFee = 0 buyBlessCommandFee = 0 teleportPlayerToVocationRoom = true toggleReceiveReward = false +randomMonsterSpawn = false -- Teleport summon -- Set to true will never remove the summon diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 700f5bfdb0c..582531a3afd 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -173,6 +173,7 @@ enum ConfigKey_t : uint16_t { PVP_RATE_DAMAGE_REDUCTION_PER_LEVEL, PVP_RATE_DAMAGE_TAKEN_PER_LEVEL, PZ_LOCKED, + RANDOM_MONSTER_SPAWN, RATE_ATTACK_SPEED, RATE_BOSS_ATTACK, RATE_BOSS_DEFENSE, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index d90c89f0e26..fb2f11d3dea 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -44,6 +44,7 @@ bool ConfigManager::load() { loadBoolConfig(L, OPTIMIZE_DATABASE, "startupDatabaseOptimization", true); loadBoolConfig(L, TOGGLE_MAP_CUSTOM, "toggleMapCustom", true); loadBoolConfig(L, TOGGLE_MAINTAIN_MODE, "toggleMaintainMode", false); + loadBoolConfig(L, RANDOM_MONSTER_SPAWN, "randomMonsterSpawn", false); loadStringConfig(L, MAINTAIN_MODE_MESSAGE, "maintainModeMessage", ""); loadStringConfig(L, IP, "ip", "127.0.0.1"); diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp index 9549ae5930e..05435d6b4a8 100644 --- a/src/creatures/monsters/spawns/spawn_monster.cpp +++ b/src/creatures/monsters/spawns/spawn_monster.cpp @@ -95,17 +95,13 @@ bool SpawnsMonster::loadFromXML(const std::string &filemonstername) { boostedrate = 2; } - uint32_t interval = pugi::cast(childMonsterNode.attribute("spawntime").value()) * 1000 * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN, __FUNCTION__) * boostedrate * eventschedule)); - if (interval >= MONSTER_MINSPAWN_INTERVAL && interval <= MONSTER_MAXSPAWN_INTERVAL) { - spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, static_cast(interval)); - } else { - if (interval <= MONSTER_MINSPAWN_INTERVAL) { - g_logger().warn("[SpawnsMonster::loadFromXml] - {} {} spawntime cannot be less than {} seconds, set to {} by default.", nameAttribute.as_string(), pos.toString(), MONSTER_MINSPAWN_INTERVAL / 1000, MONSTER_MINSPAWN_INTERVAL / 1000); - spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, MONSTER_MINSPAWN_INTERVAL); - } else { - g_logger().warn("[SpawnsMonster::loadFromXml] - {} {} spawntime can not be more than {} seconds", nameAttribute.as_string(), pos.toString(), MONSTER_MAXSPAWN_INTERVAL / 1000); - } + pugi::xml_attribute weightAttribute = childMonsterNode.attribute("weight"); + uint32_t weight = 1; + if (weightAttribute) { + weight = pugi::cast(weightAttribute.value()); } + + spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, pugi::cast(childMonsterNode.attribute("spawntime").value()) * 1000, weight); } } } @@ -150,8 +146,7 @@ void SpawnMonster::startSpawnMonsterCheck() { } SpawnMonster::~SpawnMonster() { - for (const auto &it : spawnedMonsterMap) { - std::shared_ptr monster = it.second; + for (const auto &[_, monster] : spawnedMonsterMap) { monster->setSpawnMonster(nullptr); } } @@ -170,72 +165,100 @@ bool SpawnMonster::isInSpawnMonsterZone(const Position &pos) { return SpawnsMonster::isInZone(centerPos, radius, pos); } -bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, const std::shared_ptr monsterType, const Position &pos, Direction dir, bool startup /*= false*/) { +bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr monsterType, bool startup /*= false*/) { + if (spawnedMonsterMap.contains(spawnMonsterId)) { + return false; + } auto monster = std::make_shared(monsterType); if (startup) { // No need to send out events to the surrounding since there is no one out there to listen! - if (!g_game().internalPlaceCreature(monster, pos, true)) { + if (!g_game().internalPlaceCreature(monster, sb.pos, true)) { return false; } } else { - if (!g_game().placeCreature(monster, pos, false, true)) { + if (!g_game().placeCreature(monster, sb.pos, false, true)) { return false; } } - monster->setDirection(dir); + monster->setDirection(sb.direction); monster->setSpawnMonster(this); - monster->setMasterPos(pos); + monster->setMasterPos(sb.pos); - spawnedMonsterMap.insert(spawned_pair(spawnMonsterId, monster)); - spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME(); - g_events().eventMonsterOnSpawn(monster, pos); - g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, pos); + spawnedMonsterMap[spawnMonsterId] = monster; + sb.lastSpawn = OTSYS_TIME(); + g_events().eventMonsterOnSpawn(monster, sb.pos); + g_callbacks().executeCallback(EventCallback_t::monsterOnSpawn, &EventCallback::monsterOnSpawn, monster, sb.pos); return true; } -void SpawnMonster::startup() { - for (const auto &it : spawnMonsterMap) { - uint32_t spawnMonsterId = it.first; - const spawnBlock_t &sb = it.second; - spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction, true); +void SpawnMonster::startup(bool delayed) { + if (g_configManager().getBoolean(RANDOM_MONSTER_SPAWN, __FUNCTION__)) { + for (auto it = spawnMonsterMap.begin(); it != spawnMonsterMap.end(); ++it) { + auto &[spawnMonsterId, sb] = *it; + for (auto &[monsterType, weight] : sb.monsterTypes) { + if (monsterType->isBoss()) { + continue; + } + for (auto otherIt = std::next(it); otherIt != spawnMonsterMap.end(); ++otherIt) { + auto &[id, otherSb] = *otherIt; + if (id == spawnMonsterId) { + continue; + } + if (otherSb.hasBoss()) { + continue; + } + if (otherSb.monsterTypes.contains(monsterType)) { + weight += otherSb.monsterTypes[monsterType]; + } + otherSb.monsterTypes.emplace(monsterType, weight); + sb.monsterTypes.emplace(monsterType, weight); + } + } + } + } + for (auto &[spawnMonsterId, sb] : spawnMonsterMap) { + const auto &mType = sb.getMonsterType(); + if (!mType) { + continue; + } + if (delayed) { + g_dispatcher().addEvent(std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, mType, 0, true), "SpawnMonster::startup"); + } else { + scheduleSpawn(spawnMonsterId, sb, mType, 0, true); + } } } void SpawnMonster::checkSpawnMonster() { - checkSpawnMonsterEvent = 0; + if (checkSpawnMonsterEvent == 0) { + return; + } + checkSpawnMonsterEvent = 0; cleanup(); - uint32_t spawnMonsterCount = 0; - - for (auto &it : spawnMonsterMap) { - uint32_t spawnMonsterId = it.first; - if (spawnedMonsterMap.find(spawnMonsterId) != spawnedMonsterMap.end()) { + for (auto &[spawnMonsterId, sb] : spawnMonsterMap) { + if (spawnedMonsterMap.contains(spawnMonsterId)) { continue; } - spawnBlock_t &sb = it.second; - if (!sb.monsterType->canSpawn(sb.pos)) { + const auto &mType = sb.getMonsterType(); + if (!mType) { + continue; + } + if (!mType->canSpawn(sb.pos) || (mType->info.isBlockable && findPlayer(sb.pos))) { sb.lastSpawn = OTSYS_TIME(); continue; } + if (OTSYS_TIME() < sb.lastSpawn + sb.interval) { + continue; + } - if (OTSYS_TIME() >= sb.lastSpawn + sb.interval) { - if (sb.monsterType->info.isBlockable && findPlayer(sb.pos)) { - sb.lastSpawn = OTSYS_TIME(); - continue; - } - - if (sb.monsterType->info.isBlockable) { - spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction); - } else { - scheduleSpawn(spawnMonsterId, sb, 3 * NONBLOCKABLE_SPAWN_MONSTER_INTERVAL); - } - - if (++spawnMonsterCount >= static_cast(g_configManager().getNumber(RATE_SPAWN, __FUNCTION__))) { - break; - } + if (mType->info.isBlockable) { + spawnMonster(spawnMonsterId, sb, mType, true); + } else { + scheduleSpawn(spawnMonsterId, sb, mType, 3 * NONBLOCKABLE_SPAWN_MONSTER_INTERVAL); } } @@ -244,30 +267,29 @@ void SpawnMonster::checkSpawnMonster() { } } -void SpawnMonster::scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, uint16_t interval) { +void SpawnMonster::scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr mType, uint16_t interval, bool startup /*= false*/) { if (interval <= 0) { - spawnMonster(spawnMonsterId, sb.monsterType, sb.pos, sb.direction); + spawnMonster(spawnMonsterId, sb, mType, startup); } else { g_game().addMagicEffect(sb.pos, CONST_ME_TELEPORT); - g_dispatcher().scheduleEvent(1400, std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, interval - NONBLOCKABLE_SPAWN_MONSTER_INTERVAL), "SpawnMonster::scheduleSpawn"); + g_dispatcher().scheduleEvent(NONBLOCKABLE_SPAWN_MONSTER_INTERVAL, std::bind(&SpawnMonster::scheduleSpawn, this, spawnMonsterId, sb, mType, interval - NONBLOCKABLE_SPAWN_MONSTER_INTERVAL, startup), "SpawnMonster::scheduleSpawn"); } } void SpawnMonster::cleanup() { - auto it = spawnedMonsterMap.begin(); - while (it != spawnedMonsterMap.end()) { - uint32_t spawnMonsterId = it->first; - std::shared_ptr monster = it->second; - if (!monster || monster->isRemoved()) { - spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME(); - it = spawnedMonsterMap.erase(it); - } else { - ++it; + std::vector removeList; + for (const auto &[spawnMonsterId, monster] : spawnedMonsterMap) { + if (monster == nullptr || monster->isRemoved()) { + removeList.push_back(spawnMonsterId); } } + for (const auto &spawnMonsterId : removeList) { + spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME(); + spawnedMonsterMap.erase(spawnMonsterId); + } } -bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval) { +bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t scheduleInterval, uint32_t weight /*= 1*/) { std::string variant = ""; for (const auto &zone : Zone::getZones(pos)) { if (!zone->getMonsterVariant().empty()) { @@ -281,34 +303,77 @@ bool SpawnMonster::addMonster(const std::string &name, const Position &pos, Dire return false; } - this->interval = std::min(this->interval, scheduleInterval); - - spawnBlock_t sb; - sb.monsterType = monsterType; - sb.pos = pos; - sb.direction = dir; - sb.interval = scheduleInterval; - sb.lastSpawn = 0; + uint32_t eventschedule = g_eventsScheduler().getSpawnMonsterSchedule(); + std::string boostedMonster = g_game().getBoostedMonsterName(); + int32_t boostedrate = 1; + if (name == boostedMonster) { + boostedrate = 2; + } + // eventschedule is a whole percentage, so we need to multiply by 100 to match the order of magnitude of the other values + scheduleInterval = scheduleInterval * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN, __FUNCTION__) * boostedrate * eventschedule)); + if (scheduleInterval < MONSTER_MINSPAWN_INTERVAL) { + g_logger().warn("[SpawnsMonster::addMonster] - {} {} spawntime cannot be less than {} seconds, set to {} by default.", name, pos.toString(), MONSTER_MINSPAWN_INTERVAL / 1000, MONSTER_MINSPAWN_INTERVAL / 1000); + scheduleInterval = MONSTER_MINSPAWN_INTERVAL; + } else if (scheduleInterval > MONSTER_MAXSPAWN_INTERVAL) { + g_logger().warn("[SpawnsMonster::addMonster] - {} {} spawntime can not be more than {} seconds, set to {} by default", name, pos.toString(), MONSTER_MAXSPAWN_INTERVAL / 1000, MONSTER_MAXSPAWN_INTERVAL / 1000); + scheduleInterval = MONSTER_MAXSPAWN_INTERVAL; + } + this->interval = std::gcd(this->interval, scheduleInterval); + spawnBlock_t* sb = nullptr; uint32_t spawnMonsterId = spawnMonsterMap.size() + 1; - spawnMonsterMap[spawnMonsterId] = sb; + for (auto &[id, maybeSb] : spawnMonsterMap) { + if (maybeSb.pos == pos) { + sb = &maybeSb; + spawnMonsterId = id; + break; + } + } + if (sb) { + if (sb->monsterTypes.contains(monsterType)) { + g_logger().error("[SpawnMonster] Monster {} already exists in spawn block at {}", name, pos.toString()); + return false; + } + if (monsterType->isBoss() && sb->monsterTypes.size() > 0) { + g_logger().error("[SpawnMonster] Boss monster {} has been added to spawn block with other monsters. This is not allowed.", name); + return false; + } + if (sb->hasBoss()) { + g_logger().error("[SpawnMonster] Monster {} has been added to spawn block with a boss. This is not allowed.", name); + return false; + } + } + if (!sb) { + sb = &spawnMonsterMap.emplace(spawnMonsterId, spawnBlock_t()).first->second; + } + sb->monsterTypes.emplace(monsterType, weight); + sb->pos = pos; + sb->direction = dir; + sb->interval = scheduleInterval; + sb->lastSpawn = 0; return true; } void SpawnMonster::removeMonster(std::shared_ptr monster) { - for (auto it = spawnedMonsterMap.begin(), end = spawnedMonsterMap.end(); it != end; ++it) { - if (it->second == monster) { - spawnedMonsterMap.erase(it); + uint32_t spawnMonsterId = 0; + for (const auto &[id, m] : spawnedMonsterMap) { + if (m == monster) { + spawnMonsterId = id; break; } } + spawnedMonsterMap.erase(spawnMonsterId); } void SpawnMonster::setMonsterVariant(const std::string &variant) { for (auto &it : spawnMonsterMap) { - auto variantName = variant + it.second.monsterType->typeName; - auto variantType = g_monsters().getMonsterType(variantName, false); - it.second.monsterType = variantType ? variantType : it.second.monsterType; + std::unordered_map, uint32_t> monsterTypes; + for (const auto &[monsterType, weight] : it.second.monsterTypes) { + auto variantName = variant + monsterType->typeName; + auto variantType = g_monsters().getMonsterType(variantName, false); + monsterTypes.emplace(variantType, weight); + } + it.second.monsterTypes = monsterTypes; } } @@ -318,3 +383,44 @@ void SpawnMonster::stopEvent() { checkSpawnMonsterEvent = 0; } } + +std::shared_ptr spawnBlock_t::getMonsterType() const { + if (monsterTypes.empty()) { + return nullptr; + } + uint32_t totalWeight = 0; + for (const auto &[mType, weight] : monsterTypes) { + if (!mType) { + continue; + } + if (mType->isBoss()) { + if (monsterTypes.size() > 1) { + g_logger().warn("[SpawnMonster] Boss monster {} has been added to spawn block with other monsters. This is not allowed.", mType->name); + } + return mType; + } + totalWeight += weight; + } + uint32_t randomWeight = uniform_random(0, totalWeight - 1); + // order monsters by weight DESC + std::vector, uint32_t>> orderedMonsterTypes(monsterTypes.begin(), monsterTypes.end()); + std::sort(orderedMonsterTypes.begin(), orderedMonsterTypes.end(), [](const auto &a, const auto &b) { + return a.second > b.second; + }); + for (const auto &[mType, weight] : orderedMonsterTypes) { + if (randomWeight < weight) { + return mType; + } + randomWeight -= weight; + } + return nullptr; +} + +bool spawnBlock_t::hasBoss() const { + for (const auto &[monsterType, weight] : monsterTypes) { + if (monsterType->isBoss()) { + return true; + } + } + return false; +} diff --git a/src/creatures/monsters/spawns/spawn_monster.hpp b/src/creatures/monsters/spawns/spawn_monster.hpp index 749856525d9..9ea29e4f317 100644 --- a/src/creatures/monsters/spawns/spawn_monster.hpp +++ b/src/creatures/monsters/spawns/spawn_monster.hpp @@ -17,10 +17,13 @@ class MonsterType; struct spawnBlock_t { Position pos; - std::shared_ptr monsterType; + std::unordered_map, uint32_t> monsterTypes; int64_t lastSpawn; uint32_t interval; Direction direction; + + std::shared_ptr getMonsterType() const; + bool hasBoss() const; }; class SpawnMonster { @@ -33,13 +36,13 @@ class SpawnMonster { SpawnMonster(const SpawnMonster &) = delete; SpawnMonster &operator=(const SpawnMonster &) = delete; - bool addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t interval); + bool addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t interval, uint32_t weight = 1); void removeMonster(std::shared_ptr monster); uint32_t getInterval() const { return interval; } - void startup(); + void startup(bool delayed = false); void startSpawnMonsterCheck(); void stopEvent(); @@ -55,12 +58,10 @@ class SpawnMonster { private: // map of the spawned creatures - using SpawnedMap = std::multimap>; - using spawned_pair = SpawnedMap::value_type; - SpawnedMap spawnedMonsterMap; + phmap::parallel_flat_hash_map_m> spawnedMonsterMap; // map of creatures in the spawn - std::map spawnMonsterMap; + phmap::parallel_flat_hash_map_m spawnMonsterMap; Position centerPos; int32_t radius; @@ -69,9 +70,9 @@ class SpawnMonster { uint32_t checkSpawnMonsterEvent = 0; static bool findPlayer(const Position &pos); - bool spawnMonster(uint32_t spawnMonsterId, const std::shared_ptr monsterType, const Position &pos, Direction dir, bool startup = false); + bool spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr monsterType, bool startup = false); void checkSpawnMonster(); - void scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, uint16_t interval); + void scheduleSpawn(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr monsterType, uint16_t interval, bool startup = false); }; class SpawnsMonster { diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index 480072f427b..e02a1ce4056 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -96,6 +96,7 @@ class Task { "Raids::checkRaids", "SpawnMonster::checkSpawnMonster", "SpawnMonster::scheduleSpawn", + "SpawnMonster::startup", "SpawnNpc::checkSpawnNpc", "Webhook::run", "Protocol::sendRecvMessageCallback", diff --git a/src/pch.hpp b/src/pch.hpp index 04f692a0bf1..ea0233ee1a8 100644 --- a/src/pch.hpp +++ b/src/pch.hpp @@ -41,6 +41,7 @@ #include #include #include +#include // -------------------- // System Includes