Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: weighted random spawns #1848

Merged
merged 5 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -222,6 +223,7 @@ buyAolCommandFee = 0
buyBlessCommandFee = 0
teleportPlayerToVocationRoom = true
toggleReceiveReward = false
randomMonsterSpawn = false

-- Teleport summon
-- Set to true will never remove the summon
Expand Down
1 change: 1 addition & 0 deletions src/config/config_definitions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/config/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
260 changes: 183 additions & 77 deletions src/creatures/monsters/spawns/spawn_monster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,13 @@
boostedrate = 2;
}

uint32_t interval = pugi::cast<uint32_t>(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<uint32_t>(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<uint32_t>(weightAttribute.value());
}

spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, pugi::cast<uint32_t>(childMonsterNode.attribute("spawntime").value()) * 1000, weight);
}
}
}
Expand Down Expand Up @@ -150,8 +146,7 @@
}

SpawnMonster::~SpawnMonster() {
for (const auto &it : spawnedMonsterMap) {
std::shared_ptr<Monster> monster = it.second;
for (const auto &[_, monster] : spawnedMonsterMap) {

Check warning on line 149 in src/creatures/monsters/spawns/spawn_monster.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] src/creatures/monsters/spawns/spawn_monster.cpp#L149

Unused variable: _
Raw output
src/creatures/monsters/spawns/spawn_monster.cpp:149:Unused variable: _
monster->setSpawnMonster(nullptr);
}
}
Expand All @@ -170,72 +165,100 @@
return SpawnsMonster::isInZone(centerPos, radius, pos);
}

bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, const std::shared_ptr<MonsterType> monsterType, const Position &pos, Direction dir, bool startup /*= false*/) {
bool SpawnMonster::spawnMonster(uint32_t spawnMonsterId, spawnBlock_t &sb, const std::shared_ptr<MonsterType> monsterType, bool startup /*= false*/) {
if (spawnedMonsterMap.contains(spawnMonsterId)) {
return false;
}
auto monster = std::make_shared<Monster>(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;

Check warning on line 198 in src/creatures/monsters/spawns/spawn_monster.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] src/creatures/monsters/spawns/spawn_monster.cpp#L198

Variable 'spawnMonsterId' is not assigned a value.
Raw output
src/creatures/monsters/spawns/spawn_monster.cpp:198:Variable 'spawnMonsterId' is not assigned a value.
for (auto &[monsterType, weight] : sb.monsterTypes) {
if (monsterType->isBoss()) {
continue;
}
for (auto otherIt = std::next(it); otherIt != spawnMonsterMap.end(); ++otherIt) {
auto &[id, otherSb] = *otherIt;

Check warning on line 204 in src/creatures/monsters/spawns/spawn_monster.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] src/creatures/monsters/spawns/spawn_monster.cpp#L204

Variable 'id' is not assigned a value.
Raw output
src/creatures/monsters/spawns/spawn_monster.cpp:204:Variable 'id' is not assigned a value.
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<uint32_t>(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);
}
}

Expand All @@ -244,30 +267,29 @@
}
}

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<MonsterType> 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> monster = it->second;
if (!monster || monster->isRemoved()) {
spawnMonsterMap[spawnMonsterId].lastSpawn = OTSYS_TIME();
it = spawnedMonsterMap.erase(it);
} else {
++it;
std::vector<uint32_t> 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()) {
Expand All @@ -281,34 +303,77 @@
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) {

Check warning on line 325 in src/creatures/monsters/spawns/spawn_monster.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] src/creatures/monsters/spawns/spawn_monster.cpp#L325

Variable 'id' is not assigned a value.
Raw output
src/creatures/monsters/spawns/spawn_monster.cpp:325:Variable 'id' is not assigned a value.
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> 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) {

Check warning on line 359 in src/creatures/monsters/spawns/spawn_monster.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

[cppcheck] src/creatures/monsters/spawns/spawn_monster.cpp#L359

Variable 'id' is not assigned a value.
Raw output
src/creatures/monsters/spawns/spawn_monster.cpp:359:Variable 'id' is not assigned a value.
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<std::shared_ptr<MonsterType>, 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;
}
}

Expand All @@ -318,3 +383,44 @@
checkSpawnMonsterEvent = 0;
}
}

std::shared_ptr<MonsterType> 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<std::pair<std::shared_ptr<MonsterType>, 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;
}
Loading
Loading