From 34db7f708629b828999a90b34b3253934f17e42d Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 8 Nov 2024 00:55:56 -0300 Subject: [PATCH 1/4] feat/improve: create "custom loot" for monsters This has two different options: Adding "global" items that can drop on any monster (it is disabled by default) Add specific items for each monster, without having to modify each monster's script. This allows a certain freedom to work with loot, without having to edit each script. Added "const ref" to getBestiaryList, avoid copying large bestiary map. Improved "registerLoot" function to avoid code duplication. Removed "bad" code in "closeContainer", this may crash if the container is used (it is removed right before) New lua function "getMonsterTypeByName". --- data/scripts/lib/register_monster_type.lua | 200 +++++++----------- data/scripts/systems/custom_monster_loot.lua | 74 +++++++ src/creatures/monsters/monsters.cpp | 4 +- src/creatures/players/player.cpp | 2 - src/game/game.cpp | 2 +- src/io/iobestiary.cpp | 2 +- src/io/ioprey.cpp | 4 +- .../functions/core/game/game_functions.cpp | 18 ++ .../functions/core/game/game_functions.hpp | 2 + src/server/network/protocol/protocolgame.cpp | 14 +- 10 files changed, 180 insertions(+), 142 deletions(-) create mode 100644 data/scripts/systems/custom_monster_loot.lua diff --git a/data/scripts/lib/register_monster_type.lua b/data/scripts/lib/register_monster_type.lua index cfb0a6bfaaa..66149b475f2 100644 --- a/data/scripts/lib/register_monster_type.lua +++ b/data/scripts/lib/register_monster_type.lua @@ -344,140 +344,88 @@ function SortLootByChance(loot) end) end -registerMonsterType.loot = function(mtype, mask) - if type(mask.loot) == "table" then - SortLootByChance(mask.loot) - local lootError = false - for _, loot in pairs(mask.loot) do - local parent = Loot() - if loot.name then - if not parent:setIdFromName(loot.name) then - lootError = true - end - else - if not isInteger(loot.id) or loot.id < 1 then - lootError = true - end - parent:setId(loot.id) - end - if loot.subType or loot.charges then - parent:setSubType(loot.subType or loot.charges) - else - local lType = ItemType(loot.name and loot.name or loot.id) - if lType and lType:getCharges() > 1 then - parent:setSubType(lType:getCharges()) - end - end - if loot.chance then - parent:setChance(loot.chance) - end - if loot.minCount then - parent:setMinCount(loot.minCount) - end - if loot.maxCount then - parent:setMaxCount(loot.maxCount) - end - if loot.aid or loot.actionId then - parent:setActionId(loot.aid or loot.actionId) - end - if loot.text or loot.description then - parent:setText(loot.text or loot.description) - end - if loot.name then - parent:setNameItem(loot.name) - end - if loot.article then - parent:setArticle(loot.article) - end - if loot.attack then - parent:setAttack(loot.attack) - end - if loot.defense then - parent:setDefense(loot.defense) - end - if loot.extraDefense or loot.extraDef then - parent:setExtraDefense(loot.extraDefense or loot.extraDef) - end - if loot.armor then - parent:setArmor(loot.armor) +local function configureLootAttributes(lootObject, lootProperties) + if lootProperties.subType or lootProperties.charges then + lootObject:setSubType(lootProperties.subType or lootProperties.charges) + else + local itemType = ItemType(lootProperties.name and lootProperties.name or lootProperties.itemId) + if itemType and itemType:getCharges() > 1 then + lootObject:setSubType(itemType:getCharges()) + end + end + + lootObject:setChance(lootProperties.chance or 0) + lootObject:setMinCount(lootProperties.minCount or 0) + lootObject:setMaxCount(lootProperties.maxCount or 0) + lootObject:setActionId(lootProperties.aid or lootProperties.actionId or 0) + lootObject:setText(lootProperties.text or lootProperties.description or "") + lootObject:setNameItem(lootProperties.name or "") + lootObject:setArticle(lootProperties.article or "") + lootObject:setAttack(lootProperties.attack or 0) + lootObject:setDefense(lootProperties.defense or 0) + lootObject:setExtraDefense(lootProperties.extraDefense or lootProperties.extraDef or 0) + lootObject:setArmor(lootProperties.armor or 0) + lootObject:setShootRange(lootProperties.shootRange or lootProperties.range or 0) + lootObject:setUnique(lootProperties.unique or false) +end + +local function addChildrenLoot(parent, childrenLoot) + SortLootByChance(childrenLoot) + for _, child in pairs(childrenLoot) do + local childLoot = Loot() + if child.name then + if not childLoot:setIdFromName(child.name) then + return true end - if loot.shootRange or loot.range then - parent:setShootRange(loot.shootRange or loot.range) + else + if not isInteger(child.id) or child.id < 1 then + return true end - if loot.unique then - parent:setUnique(loot.unique) + childLoot:setId(child.id) + end + configureLootAttributes(childLoot, child) + parent:addChildLoot(childLoot) + end + return false +end + +function MonsterType:createLoot(lootTable) + SortLootByChance(lootTable) + local lootError = false + + for _, loot in pairs(lootTable) do + local parent = Loot() + if loot.name then + if not parent:setIdFromName(loot.name) then + lootError = true end - if loot.child then - SortLootByChance(loot.child) - for _, children in pairs(loot.child) do - local child = Loot() - if children.name then - if not child:setIdFromName(children.name) then - lootError = true - end - else - if not isInteger(children.id) or children.id < 1 then - lootError = true - end - child:setId(children.id) - end - if children.subType or children.charges then - child:setSubType(children.subType or children.charges) - else - local cType = ItemType(children.name and children.name or children.id) - if cType and cType:getCharges() > 1 then - child:setSubType(cType:getCharges()) - end - end - if children.chance then - child:setChance(children.chance) - end - if children.minCount then - child:setMinCount(children.minCount) - end - if children.maxCount then - child:setMaxCount(children.maxCount) - end - if children.aid or children.actionId then - child:setActionId(children.aid or children.actionId) - end - if children.text or children.description then - child:setText(children.text or children.description) - end - if loot.name then - child:setNameItem(loot.name) - end - if children.article then - child:setArticle(children.article) - end - if children.attack then - child:setAttack(children.attack) - end - if children.defense then - child:setDefense(children.defense) - end - if children.extraDefense or children.extraDef then - child:setExtraDefense(children.extraDefense or children.extraDef) - end - if children.armor then - child:setArmor(children.armor) - end - if children.shootRange or children.range then - child:setShootRange(children.shootRange or children.range) - end - if children.unique then - child:setUnique(children.unique) - end - parent:addChildLoot(child) - end + else + if not isInteger(loot.id) or loot.id < 1 then + lootError = true end - mtype:addLoot(parent) + parent:setId(loot.id) end - if lootError then - logger.warn("[registerMonsterType.loot] - Monster: {} loot could not correctly be load", mtype:name()) + + configureLootAttributes(parent, loot) + + if loot.child and addChildrenLoot(parent, loot.child) then + lootError = true end + + self:addLoot(parent) + end + + if lootError then + logger.warn("[MonsterType:createLoot] - Monster: {} loot could not be loaded correctly", self:getName()) end end + +registerMonsterType.loot = function(mtype, mask) + if type(mask.loot) == "table" then + mtype:createLoot(mask.loot) + end +end + local playerElements = { COMBAT_PHYSICALDAMAGE, COMBAT_ENERGYDAMAGE, COMBAT_EARTHDAMAGE, COMBAT_FIREDAMAGE, COMBAT_ICEDAMAGE, COMBAT_HOLYDAMAGE, COMBAT_DEATHDAMAGE } registerMonsterType.elements = function(mtype, mask) local min = configManager.getNumber(configKeys.MIN_ELEMENTAL_RESISTANCE) diff --git a/data/scripts/systems/custom_monster_loot.lua b/data/scripts/systems/custom_monster_loot.lua new file mode 100644 index 00000000000..5c6163e3fa3 --- /dev/null +++ b/data/scripts/systems/custom_monster_loot.lua @@ -0,0 +1,74 @@ +-- Drops custom loot for all monsters +local allLootConfig = { + --{ id = 6526, chance = 100000 }, -- Example of loot (100% chance) +} + +-- Custom loot for specific monsters (this has the same usage options as normal monster loot) +local customLootConfig = { + ["Dragon"] = { items = { + { name = "platinum coin", chance = 1000, maxCount = 1 }, + } }, +} + +-- Global loot addition for all monsters +local callback = EventCallback("MonsterOnDropLootCustom") + +function callback.monsterOnDropLoot(monster, corpse) + if not monster or not corpse then + return + end + local player = Player(corpse:getCorpseOwner()) + if not player or not player:canReceiveLoot() then + return + end + corpse:addLoot(monster:generateCustomLoot()) +end + +-- Register the callback only if there is loot to be dropped +if #allLootConfig > 0 then + callback:register() +end + +function Monster:generateCustomLoot() + local mType = self:getType() + if not mType then + return {} + end + + local loot = {} + + for _, lootInfo in ipairs(allLootConfig) do + local roll = math.random(1, 10000) + if roll <= lootInfo.chance then + if loot[lootInfo.id] then + loot[lootInfo.id].count = loot[lootInfo.id].count + 1 + else + loot[lootInfo.id] = { count = 1 } + end + end + end + + return loot +end + +local customMonsterLoot = GlobalEvent("CreateCustomMonsterLoot") + +function customMonsterLoot.onStartup() + for monsterName, _ in pairs(customLootConfig) do + local mtype = Game.getMonsterTypeByName(monsterName) + if mtype then + local lootTable = customLootConfig[mtype:getName()] + if not lootTable then + logger.error("[customMonsterLoot.onStartup] - No custom loot found for monster: {}", self:getName()) + return + end + + if #lootTable.items > 0 then + mtype:createLoot(lootTable.items) + logger.debug("[customMonsterLoot.onStartup] - Custom loot registered for monster: {}", mtype:getName()) + end + end + end +end + +customMonsterLoot:register() diff --git a/src/creatures/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp index 87fd967ab46..3aafe11d0bc 100644 --- a/src/creatures/monsters/monsters.cpp +++ b/src/creatures/monsters/monsters.cpp @@ -345,9 +345,7 @@ void Monsters::clear() { std::shared_ptr Monsters::getMonsterType(const std::string &name, bool silent /* = false*/) const { std::string lowerCaseName = asLowerCaseString(name); if (auto it = monsters.find(lowerCaseName); - it != monsters.end() - // We will only return the MonsterType if it match the exact name of the monster - && it->first.find(lowerCaseName) != std::basic_string::npos) { + it != monsters.end()) { return it->second; } if (!silent) { diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 923b153a99a..6aeea0e64b9 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -976,8 +976,6 @@ void Player::closeContainer(uint8_t cid) { removeEmptyRewards(); } openContainers.erase(it); - if (container && container->getID() == ITEM_BROWSEFIELD) { - } } void Player::removeEmptyRewards() { diff --git a/src/game/game.cpp b/src/game/game.cpp index 780ce5b7303..89a989828c1 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -456,7 +456,7 @@ void Game::loadBoostedCreature() { } const auto oldRace = result->getNumber("raceid"); - const auto monsterlist = getBestiaryList(); + const auto &monsterlist = getBestiaryList(); struct MonsterRace { uint16_t raceId { 0 }; diff --git a/src/io/iobestiary.cpp b/src/io/iobestiary.cpp index b18ed181467..a27f9d0a54a 100644 --- a/src/io/iobestiary.cpp +++ b/src/io/iobestiary.cpp @@ -193,7 +193,7 @@ uint16_t IOBestiary::getBestiaryRaceUnlocked(const std::shared_ptr &play } uint16_t count = 0; - std::map besty_l = g_game().getBestiaryList(); + const std::map &besty_l = g_game().getBestiaryList(); for (const auto &it : besty_l) { const auto mtype = g_monsters().getMonsterType(it.second); diff --git a/src/io/ioprey.cpp b/src/io/ioprey.cpp index 65838a1660c..22df49b39c2 100644 --- a/src/io/ioprey.cpp +++ b/src/io/ioprey.cpp @@ -146,7 +146,7 @@ void TaskHuntingSlot::reloadMonsterGrid(std::vector blackList, uint32_ // Disabling task hunting system if the server have less then 36 registered monsters on bestiary because: // - Impossible to generate random lists without duplications on slots. // - Stress the server with unnecessary loops. - std::map bestiary = g_game().getBestiaryList(); + const std::map &bestiary = g_game().getBestiaryList(); if (bestiary.size() < 36) { return; } @@ -574,7 +574,7 @@ void IOPrey::initializeTaskHuntOptions() { } msg.addByte(0xBA); - std::map bestiaryList = g_game().getBestiaryList(); + const std::map &bestiaryList = g_game().getBestiaryList(); msg.add(static_cast(bestiaryList.size())); std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg](auto mType) { const auto mtype = g_monsters().getMonsterType(mType.second); diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 16133833bbc..84c6beabf67 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -84,6 +84,24 @@ int GameFunctions::luaGameCreateNpcType(lua_State* L) { return NpcTypeFunctions::luaNpcTypeCreate(L); } +int GameFunctions::luaGameGetMonsterTypeByName(lua_State* L) { + if (!isString(L, 1)) { + reportErrorFunc("First argument must be a string"); + return 1; + } + + const auto name = getString(L, 1); + const auto &mType = g_monsters().getMonsterType(name); + if (!mType) { + reportErrorFunc(fmt::format("MonsterType with name {} not found", name)); + return 1; + } + + pushUserdata(L, mType); + setMetatable(L, -1, "MonsterType"); + return 1; +} + int GameFunctions::luaGameGetSpectators(lua_State* L) { // Game.getSpectators(position[, multifloor = false[, onlyPlayer = false[, minRangeX = 0[, maxRangeX = 0[, minRangeY = 0[, maxRangeY = 0]]]]]]) const Position &position = getPosition(L, 1); diff --git a/src/lua/functions/core/game/game_functions.hpp b/src/lua/functions/core/game/game_functions.hpp index 3c668c54fb4..8e47b7a318c 100644 --- a/src/lua/functions/core/game/game_functions.hpp +++ b/src/lua/functions/core/game/game_functions.hpp @@ -24,6 +24,7 @@ class GameFunctions final : LuaScriptInterface { registerMethod(L, "Game", "createNpcType", GameFunctions::luaGameCreateNpcType); registerMethod(L, "Game", "createMonsterType", GameFunctions::luaGameCreateMonsterType); + registerMethod(L, "Game", "getMonsterTypeByName", GameFunctions::luaGameGetMonsterTypeByName); registerMethod(L, "Game", "getSpectators", GameFunctions::luaGameGetSpectators); @@ -100,6 +101,7 @@ class GameFunctions final : LuaScriptInterface { private: static int luaGameCreateMonsterType(lua_State* L); static int luaGameCreateNpcType(lua_State* L); + static int luaGameGetMonsterTypeByName(lua_State* L); static int luaGameGetSpectators(lua_State* L); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index b8b67b6335f..4d82784e9ef 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -2314,7 +2314,7 @@ void ProtocolGame::parseBestiarysendRaces() { NetworkMessage msg; msg.addByte(0xd5); msg.add(BESTY_RACE_LAST); - std::map mtype_list = g_game().getBestiaryList(); + const std::map &mtype_list = g_game().getBestiaryList(); for (uint8_t i = BESTY_RACE_FIRST; i <= BESTY_RACE_LAST; i++) { std::string BestClass; uint16_t count = 0; @@ -2357,7 +2357,7 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { auto raceId = msg.get(); std::string Class; std::shared_ptr mtype = nullptr; - std::map mtype_list = g_game().getBestiaryList(); + const std::map &mtype_list = g_game().getBestiaryList(); auto ait = mtype_list.find(raceId); if (ait != mtype_list.end()) { @@ -2396,7 +2396,7 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { newmsg.addByte(mtype->info.bestiaryStars); newmsg.addByte(mtype->info.bestiaryOccurrence); - std::vector lootList = mtype->info.lootItems; + const std::vector &lootList = mtype->info.lootItems; newmsg.addByte(lootList.size()); for (const LootBlock &loot : lootList) { int8_t difficult = g_iobestiary().calculateDifficult(loot.chance); @@ -2495,7 +2495,7 @@ void ProtocolGame::parseCyclopediaMonsterTracker(NetworkMessage &msg) { } // Bestiary tracker logic - const auto bestiaryMonsters = g_game().getBestiaryList(); + const auto &bestiaryMonsters = g_game().getBestiaryList(); auto it = bestiaryMonsters.find(monsterRaceId); if (it != bestiaryMonsters.end()) { const auto mtype = g_monsters().getMonsterType(it->second); @@ -2968,7 +2968,7 @@ void ProtocolGame::parseBestiarysendCreatures(NetworkMessage &msg) { if (search == 1) { auto monsterAmount = msg.get(); - std::map mtype_list = g_game().getBestiaryList(); + const std::map &mtype_list = g_game().getBestiaryList(); for (uint16_t monsterCount = 1; monsterCount <= monsterAmount; monsterCount++) { auto raceid = msg.get(); if (player->getBestiaryKillCount(raceid) > 0) { @@ -7557,7 +7557,7 @@ void ProtocolGame::sendPreyData(const std::unique_ptr &slot) { msg.addByte(outfit.lookAddons); } } else if (slot->state == PreyDataState_ListSelection) { - const std::map bestiaryList = g_game().getBestiaryList(); + const std::map &bestiaryList = g_game().getBestiaryList(); msg.add(static_cast(bestiaryList.size())); std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg](auto mType) { msg.add(mType.first); @@ -8166,7 +8166,7 @@ void ProtocolGame::sendTaskHuntingData(const std::unique_ptr &s }); } else if (slot->state == PreyTaskDataState_ListSelection) { std::shared_ptr user = player; - const std::map bestiaryList = g_game().getBestiaryList(); + const std::map &bestiaryList = g_game().getBestiaryList(); msg.add(static_cast(bestiaryList.size())); std::for_each(bestiaryList.begin(), bestiaryList.end(), [&msg, user](auto mType) { msg.add(mType.first); From dab2192fa3ece16e3153a299c86c0e7e5dd4a78f Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 8 Nov 2024 20:18:19 -0300 Subject: [PATCH 2/4] improve: register loot for all monsters --- data/scripts/systems/custom_monster_loot.lua | 64 +++++--------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/data/scripts/systems/custom_monster_loot.lua b/data/scripts/systems/custom_monster_loot.lua index 5c6163e3fa3..a8b63fcda29 100644 --- a/data/scripts/systems/custom_monster_loot.lua +++ b/data/scripts/systems/custom_monster_loot.lua @@ -1,6 +1,6 @@ -- Drops custom loot for all monsters local allLootConfig = { - --{ id = 6526, chance = 100000 }, -- Example of loot (100% chance) + { id = 6526, chance = 100000, minCount = 1, maxCount = 10}, -- Example of loot (100% chance) } -- Custom loot for specific monsters (this has the same usage options as normal monster loot) @@ -10,63 +10,27 @@ local customLootConfig = { } }, } --- Global loot addition for all monsters -local callback = EventCallback("MonsterOnDropLootCustom") - -function callback.monsterOnDropLoot(monster, corpse) - if not monster or not corpse then - return - end - local player = Player(corpse:getCorpseOwner()) - if not player or not player:canReceiveLoot() then - return - end - corpse:addLoot(monster:generateCustomLoot()) -end - --- Register the callback only if there is loot to be dropped -if #allLootConfig > 0 then - callback:register() -end - -function Monster:generateCustomLoot() - local mType = self:getType() - if not mType then - return {} - end - - local loot = {} - - for _, lootInfo in ipairs(allLootConfig) do - local roll = math.random(1, 10000) - if roll <= lootInfo.chance then - if loot[lootInfo.id] then - loot[lootInfo.id].count = loot[lootInfo.id].count + 1 - else - loot[lootInfo.id] = { count = 1 } - end - end - end - - return loot -end - local customMonsterLoot = GlobalEvent("CreateCustomMonsterLoot") function customMonsterLoot.onStartup() - for monsterName, _ in pairs(customLootConfig) do + for monsterName, lootTable in pairs(customLootConfig) do local mtype = Game.getMonsterTypeByName(monsterName) if mtype then - local lootTable = customLootConfig[mtype:getName()] - if not lootTable then - logger.error("[customMonsterLoot.onStartup] - No custom loot found for monster: {}", self:getName()) - return - end - - if #lootTable.items > 0 then + if lootTable and lootTable.items and #lootTable.items > 0 then mtype:createLoot(lootTable.items) logger.debug("[customMonsterLoot.onStartup] - Custom loot registered for monster: {}", mtype:getName()) end + else + logger.error("[customMonsterLoot.onStartup] - Monster type not found: {}", monsterName) + end + end + + if #allLootConfig > 0 then + for monsterName, mtype in pairs(Game.getMonsterTypes()) do + if mtype then + mtype:createLoot(allLootConfig) + logger.debug("[customMonsterLoot.onStartup] - Global loot registered for monster: {}", mtype:getName()) + end end end end From e4c87a4556c141e8574dff2c55f56a654bf339e0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 8 Nov 2024 23:18:58 +0000 Subject: [PATCH 3/4] Lua code format - (Stylua) --- data/scripts/systems/custom_monster_loot.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/scripts/systems/custom_monster_loot.lua b/data/scripts/systems/custom_monster_loot.lua index a8b63fcda29..36d4794980c 100644 --- a/data/scripts/systems/custom_monster_loot.lua +++ b/data/scripts/systems/custom_monster_loot.lua @@ -1,6 +1,6 @@ -- Drops custom loot for all monsters local allLootConfig = { - { id = 6526, chance = 100000, minCount = 1, maxCount = 10}, -- Example of loot (100% chance) + { id = 6526, chance = 100000, minCount = 1, maxCount = 10 }, -- Example of loot (100% chance) } -- Custom loot for specific monsters (this has the same usage options as normal monster loot) From 46f9120239cbe4f480cf3344df88a9695f6af106 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sun, 10 Nov 2024 17:10:50 -0300 Subject: [PATCH 4/4] fix: conflict --- src/lua/functions/core/game/game_functions.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index 555296996f5..e84096f6e0a 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -36,6 +36,7 @@ void GameFunctions::init(lua_State* L) { Lua::registerMethod(L, "Game", "createNpcType", GameFunctions::luaGameCreateNpcType); Lua::registerMethod(L, "Game", "createMonsterType", GameFunctions::luaGameCreateMonsterType); + Lua::registerMethod(L, "Game", "getMonsterTypeByName", GameFunctions::luaGameGetMonsterTypeByName); Lua::registerMethod(L, "Game", "getSpectators", GameFunctions::luaGameGetSpectators);