diff --git a/CMakePresets.json b/CMakePresets.json index 92ef2d33c6a..49f631e1921 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -54,6 +54,7 @@ "DEBUG_LOG": "ON", "ASAN_ENABLED": "OFF", "BUILD_STATIC_LIBRARY": "OFF", + "SPEED_UP_BUILD_UNITY": "OFF", "VCPKG_TARGET_TRIPLET": "x64-windows" } }, diff --git a/data-otservbr-global/lib/core/storages.lua b/data-otservbr-global/lib/core/storages.lua index fce8059f7d1..2b8d9a7783d 100644 --- a/data-otservbr-global/lib/core/storages.lua +++ b/data-otservbr-global/lib/core/storages.lua @@ -3079,6 +3079,15 @@ GlobalStorage = { DarashiaWest = 60193, }, }, + TheDreamCourts = { + -- Reserved storage from 60194 - 60196 + FacelessBane = { + -- Global + StepsOn = 60194, + Deaths = 60195, + ResetSteps = 60196, + }, + }, FuryGates = 65000, Yakchal = 65001, PitsOfInfernoLevers = 65002, diff --git a/data-otservbr-global/monster/quests/grave_danger/bosses/lord_azaram.lua b/data-otservbr-global/monster/quests/grave_danger/bosses/lord_azaram.lua index 701a0b6f92b..8507c3a0c1c 100644 --- a/data-otservbr-global/monster/quests/grave_danger/bosses/lord_azaram.lua +++ b/data-otservbr-global/monster/quests/grave_danger/bosses/lord_azaram.lua @@ -17,8 +17,8 @@ monster.events = { "GraveDangerBossDeath", } -monster.health = 75000 -monster.maxHealth = 75000 +monster.health = 300000 +monster.maxHealth = 300000 monster.race = "venom" monster.corpse = 31599 monster.speed = 125 diff --git a/data-otservbr-global/monster/quests/the_dream_courts/bosses/faceless_bane.lua b/data-otservbr-global/monster/quests/the_dream_courts/bosses/faceless_bane.lua index e4d8553c46a..868fe08e756 100644 --- a/data-otservbr-global/monster/quests/the_dream_courts/bosses/faceless_bane.lua +++ b/data-otservbr-global/monster/quests/the_dream_courts/bosses/faceless_bane.lua @@ -2,7 +2,7 @@ local mType = Game.createMonsterType("Faceless Bane") local monster = {} monster.description = "Faceless Bane" -monster.experience = 30000 +monster.experience = 20000 monster.outfit = { lookType = 1119, lookHead = 0, @@ -22,7 +22,11 @@ monster.manaCost = 0 monster.changeTarget = { interval = 4000, - chance = 10, + chance = 20, +} + +monster.reflects = { + { type = COMBAT_DEATHDAMAGE, percent = 90 }, } monster.bosstiary = { @@ -131,11 +135,7 @@ monster.elements = { { type = COMBAT_DROWNDAMAGE, percent = 0 }, { type = COMBAT_ICEDAMAGE, percent = 0 }, { type = COMBAT_HOLYDAMAGE, percent = 0 }, - { type = COMBAT_DEATHDAMAGE, percent = 99 }, -} - -monster.heals = { - { type = COMBAT_DEATHDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 50 }, } monster.immunities = { @@ -149,6 +149,11 @@ mType.onThink = function(monster, interval) end mType.onAppear = function(monster, creature) if monster:getType():isRewardBoss() then + -- reset global storage state to default / ensure sqm's reset for the next team + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.Deaths, -1) + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn, -1) + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.ResetSteps, 1) + monster:registerEvent("facelessBaneImmunity") monster:setReward(true) end end diff --git a/data-otservbr-global/npc/emael.lua b/data-otservbr-global/npc/emael.lua index 79111f8f324..4fff95b1b95 100644 --- a/data-otservbr-global/npc/emael.lua +++ b/data-otservbr-global/npc/emael.lua @@ -69,7 +69,7 @@ local function creatureSayCallback(npc, creature, type, message) npcHandler:say("Ah, I see you killed a lot of dangerous creatures. Here's your podium of vigour!", npc, creature) local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() then + if inbox and #inboxItems < inbox:getMaxCapacity() then local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) if decoKit then decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "Unwrap it in your own house to create a <" .. ItemType(38707):getName() .. ">.") diff --git a/data-otservbr-global/npc/emperor_kruzak.lua b/data-otservbr-global/npc/emperor_kruzak.lua index 4f838396df0..daf0742f615 100644 --- a/data-otservbr-global/npc/emperor_kruzak.lua +++ b/data-otservbr-global/npc/emperor_kruzak.lua @@ -82,7 +82,7 @@ local function creatureSayCallback(npc, creature, type, message) if player:getMoney() + player:getBankBalance() >= 500000000 then local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() then + if inbox and #inboxItems < inbox:getMaxCapacity() then local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) local decoItemName = ItemType(31510):getName() decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item in the Store.\nUnwrap it in your own house to create a " .. decoItemName .. ".") diff --git a/data-otservbr-global/npc/hireling.lua b/data-otservbr-global/npc/hireling.lua index aad7785079d..6897bafdcf7 100644 --- a/data-otservbr-global/npc/hireling.lua +++ b/data-otservbr-global/npc/hireling.lua @@ -521,7 +521,7 @@ function createHirelingType(HirelingName) local inboxItems = inbox:getItems() if player:getFreeCapacity() < itType:getWeight(1) then npcHandler:say("Sorry, but you don't have enough capacity.", npc, creature) - elseif not inbox or #inboxItems > inbox:getMaxCapacity() then + elseif not inbox or #inboxItems >= inbox:getMaxCapacity() then player:getPosition():sendMagicEffect(CONST_ME_POFF) npcHandler:say("Sorry, you don't have enough room on your inbox", npc, creature) elseif not player:removeMoneyBank(15000) then diff --git a/data-otservbr-global/npc/king_tibianus.lua b/data-otservbr-global/npc/king_tibianus.lua index de47e45acca..d9968357fa1 100644 --- a/data-otservbr-global/npc/king_tibianus.lua +++ b/data-otservbr-global/npc/king_tibianus.lua @@ -87,7 +87,7 @@ local function creatureSayCallback(npc, creature, type, message) if player:getMoney() + player:getBankBalance() >= 500000000 then local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() then + if inbox and #inboxItems < inbox:getMaxCapacity() then local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) local decoItemName = ItemType(31510):getName() decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "Unwrap it in your own house to create a " .. decoItemName .. ".") diff --git a/data-otservbr-global/npc/queen_eloise.lua b/data-otservbr-global/npc/queen_eloise.lua index a9397061a56..0467bb6e74a 100644 --- a/data-otservbr-global/npc/queen_eloise.lua +++ b/data-otservbr-global/npc/queen_eloise.lua @@ -77,7 +77,7 @@ local function creatureSayCallback(npc, creature, type, message) if player:getMoney() + player:getBankBalance() >= 500000000 then local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() then + if inbox and #inboxItems < inbox:getMaxCapacity() then local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) local decoItemName = ItemType(31510):getName() decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item in the Store.\nUnwrap it in your own house to create a " .. decoItemName .. ".") diff --git a/data-otservbr-global/npc/walter_jaeger.lua b/data-otservbr-global/npc/walter_jaeger.lua index 6911d7ed323..6b0e26075d6 100644 --- a/data-otservbr-global/npc/walter_jaeger.lua +++ b/data-otservbr-global/npc/walter_jaeger.lua @@ -283,7 +283,7 @@ local function processItemInboxPurchase(player, name, id) local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() then + if inbox and #inboxItems < inbox:getMaxCapacity() then local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) if decoKit then decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item with the Walter Jaeger.\nUnwrap it in your own house to create a <" .. name .. ">.") diff --git a/data-otservbr-global/scripts/creaturescripts/monster/faceless_bane_immunity.lua b/data-otservbr-global/scripts/creaturescripts/monster/faceless_bane_immunity.lua new file mode 100644 index 00000000000..36e1ecd11c3 --- /dev/null +++ b/data-otservbr-global/scripts/creaturescripts/monster/faceless_bane_immunity.lua @@ -0,0 +1,47 @@ +local bossName = "Faceless Bane" + +local function healBoss(creature) + if creature then + creature:addHealth(creature:getMaxHealth()) + creature:getPosition():sendMagicEffect(CONST_ME_BLOCKHIT) + end +end + +local function createSummons(creature) + if creature then + local pos = creature:getPosition() + Game.createMonster("Gazer Spectre", pos, true, false, creature) + Game.createMonster("Ripper Spectre", pos, true, false, creature) + Game.createMonster("Burster Spectre", pos, true, false, creature) + end +end + +local function resetBoss(creature, deaths) + if creature then + healBoss(creature) + createSummons(creature) + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.Deaths, deaths + 1) + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn, 0) + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.ResetSteps, 1) + end +end + +local facelessBaneImmunity = CreatureEvent("facelessBaneImmunity") + +function facelessBaneImmunity.onHealthChange(creature, attacker, primaryDamage, primaryType, secondaryDamage, secondaryType) + if creature and creature:isMonster() and creature:getName() == bossName then + local creatureHealthPercent = (creature:getHealth() * 100) / creature:getMaxHealth() + local facelessBaneDeathsStorage = Game.getStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.Deaths) + + if creatureHealthPercent <= 20 and facelessBaneDeathsStorage < 1 then + resetBoss(creature, facelessBaneDeathsStorage) + return true + elseif Game.getStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn) < 1 then + healBoss(creature) + return true + end + end + return primaryDamage, primaryType, secondaryDamage, secondaryType +end + +facelessBaneImmunity:register() diff --git a/data-otservbr-global/scripts/movements/quests/the_dream_courts/faceless_bane_step_positions.lua b/data-otservbr-global/scripts/movements/quests/the_dream_courts/faceless_bane_step_positions.lua new file mode 100644 index 00000000000..8ebdc47ae6f --- /dev/null +++ b/data-otservbr-global/scripts/movements/quests/the_dream_courts/faceless_bane_step_positions.lua @@ -0,0 +1,114 @@ +local walkedPositions = {} +local lastResetTime = os.time() +local checkTime = false + +local function resetWalkedPositions(checkLastResetTime) + if lastResetTime > os.time() and checkLastResetTime then + return true + end + + walkedPositions = {} + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn, 0) + lastResetTime = os.time() + (1 * 60) +end + +local pipePositions = { + Position(33612, 32568, 13), + Position(33612, 32567, 13), + Position(33612, 32566, 13), + Position(33612, 32565, 13), + Position(33612, 32564, 13), + Position(33612, 32563, 13), + Position(33612, 32562, 13), + Position(33612, 32561, 13), + Position(33612, 32560, 13), + Position(33612, 32559, 13), + Position(33612, 32558, 13), + Position(33612, 32557, 13), + Position(33612, 32556, 13), + Position(33622, 32556, 13), + Position(33622, 32557, 13), + Position(33622, 32558, 13), + Position(33622, 32559, 13), + Position(33622, 32560, 13), + Position(33622, 32561, 13), + Position(33622, 32562, 13), + Position(33622, 32563, 13), + Position(33622, 32564, 13), + Position(33622, 32565, 13), + Position(33622, 32566, 13), + Position(33622, 32567, 13), + Position(33622, 32568, 13), +} + +local function sendEnergyEffect() + for _, position in ipairs(pipePositions) do + position:sendMagicEffect(CONST_ME_PURPLEENERGY) + position:sendSingleSoundEffect(SOUND_EFFECT_TYPE_SPELL_GREAT_ENERGY_BEAM) + end + + return true +end + +local facelessBaneStepPositions = MoveEvent() + +function facelessBaneStepPositions.onStepIn(creature, item, position, fromPosition) + local player = creature:getPlayer() + if not player then + return true + end + + if Game.getStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.ResetSteps) == 1 then + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.ResetSteps, 0) + lastResetTime = os.time() + resetWalkedPositions(true) + end + + if not checkTime then + checkTime = addEvent(resetWalkedPositions, 15 * 1000, false) + end + + if Game.getStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn) < 1 then + if #walkedPositions > 0 then + for _, walkedPos in ipairs(walkedPositions) do + if walkedPos == position then + return true + end + end + end + + position:sendSingleSoundEffect(SOUND_EFFECT_TYPE_SPELL_BUZZ) + position:sendMagicEffect(CONST_ME_YELLOWENERGY) + table.insert(walkedPositions, position) + + if #walkedPositions == 13 then + Game.setStorageValue(GlobalStorage.TheDreamCourts.FacelessBane.StepsOn, 1) + addEvent(resetWalkedPositions, 60 * 1000, true) + sendEnergyEffect() + checkTime = nil + end + end + return true +end + +local facelessBaneSteps = { + Position(33615, 32567, 13), + Position(33613, 32567, 13), + Position(33611, 32563, 13), + Position(33610, 32561, 13), + Position(33611, 32558, 13), + Position(33614, 32557, 13), + Position(33617, 32558, 13), + Position(33620, 32557, 13), + Position(33623, 32558, 13), + Position(33624, 32561, 13), + Position(33623, 32563, 13), + Position(33621, 32567, 13), + Position(33619, 32567, 13), +} + +for _, pos in ipairs(facelessBaneSteps) do + facelessBaneStepPositions:position(pos) +end + +facelessBaneStepPositions:register() diff --git a/data/libs/functions/boss_lever.lua b/data/libs/functions/boss_lever.lua index 3792cdf629d..b95bf7211b0 100644 --- a/data/libs/functions/boss_lever.lua +++ b/data/libs/functions/boss_lever.lua @@ -174,24 +174,36 @@ function BossLever:onUse(player) end if creature:getLevel() < self.requiredLevel then - creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, "All the players need to be level " .. self.requiredLevel .. " or higher.") + local message = "All players need to be level " .. self.requiredLevel .. " or higher." + creature:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) return false end - if self:lastEncounterTime(creature) > os.time() then - local info = lever:getInfoPositions() - for _, v in pairs(info) do - local newPlayer = v.creature - if newPlayer then - local timeLeft = self:lastEncounterTime(newPlayer) - os.time() - newPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You or a member in your team have to wait " .. getTimeInWords(timeLeft) .. " to face " .. self.name .. " again!") - if self:lastEncounterTime(newPlayer) > os.time() then - newPlayer:getPosition():sendMagicEffect(CONST_ME_POFF) + if creature:getGroup():getId() < GROUP_TYPE_GOD and self:lastEncounterTime(creature) > os.time() then + local infoPositions = lever:getInfoPositions() + for _, posInfo in pairs(infoPositions) do + local currentPlayer = posInfo.creature + if currentPlayer then + local lastEncounter = self:lastEncounterTime(currentPlayer) + local currentTime = os.time() + if lastEncounter and currentTime < lastEncounter then + local timeLeft = lastEncounter - currentTime + local timeMessage = getTimeInWords(timeLeft) .. " to face " .. self.name .. " again!" + local message = "You have to wait " .. timeMessage + + if currentPlayer ~= player then + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "A member in your team has to wait " .. timeMessage) + end + + currentPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, message) + currentPlayer:getPosition():sendMagicEffect(CONST_ME_POFF) end end end return false end + self.onUseExtra(creature) return true end) diff --git a/data/libs/systems/hireling.lua b/data/libs/systems/hireling.lua index 8b5784759fa..30134cd7cd0 100644 --- a/data/libs/systems/hireling.lua +++ b/data/libs/systems/hireling.lua @@ -361,7 +361,7 @@ function Hireling:returnToLamp(player_id) local inbox = owner:getStoreInbox() local inboxItems = inbox:getItems() - if not inbox or #inboxItems > inbox:getMaxCapacity() then + if not inbox or #inboxItems >= inbox:getMaxCapacity() then owner:getPosition():sendMagicEffect(CONST_ME_POFF) return owner:sendTextMessage(MESSAGE_FAILURE, "You don't have enough room in your inbox.") end @@ -556,7 +556,7 @@ function Player:addNewHireling(name, sex) local inbox = self:getStoreInbox() local inboxItems = inbox:getItems() - if not inbox or #inboxItems > inbox:getMaxCapacity() then + if not inbox or #inboxItems >= inbox:getMaxCapacity() then self:getPosition():sendMagicEffect(CONST_ME_POFF) self:sendTextMessage(MESSAGE_FAILURE, "You don't have enough room in your inbox.") return false diff --git a/data/modules/scripts/daily_reward/daily_reward.lua b/data/modules/scripts/daily_reward/daily_reward.lua index 09b927cedda..b6a7c16993a 100644 --- a/data/modules/scripts/daily_reward/daily_reward.lua +++ b/data/modules/scripts/daily_reward/daily_reward.lua @@ -454,7 +454,7 @@ function Player.selectDailyReward(self, msg) -- Adding items to store inbox local inbox = self:getStoreInbox() local inboxItems = inbox:getItems() - if not inbox or #inboxItems > inbox:getMaxCapacity() then + if not inbox or #inboxItems >= inbox:getMaxCapacity() then self:sendError("You do not have enough space in your store inbox.") return false end diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index ba7398d9d3e..214aec77d43 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -247,6 +247,11 @@ function onRecvbyte(player, msg, byte) return player:sendCancelMessage("Store don't have offers for rookgaard citizen.") end + if player:isUIExhausted(250) then + player:sendCancelMessage("You are exhausted.") + return + end + if byte == GameStore.RecivedPackets.C_StoreEvent then elseif byte == GameStore.RecivedPackets.C_TransferCoins then parseTransferableCoins(player:getId(), msg) @@ -262,12 +267,6 @@ function onRecvbyte(player, msg, byte) parseRequestTransactionHistory(player:getId(), msg) end - if player:isUIExhausted(250) then - player:sendCancelMessage("You are exhausted.") - return false - end - - player:updateUIExhausted() return true end @@ -306,6 +305,7 @@ function parseTransferableCoins(playerId, msg) GameStore.insertHistory(accountId, GameStore.HistoryTypes.HISTORY_TYPE_NONE, player:getName() .. " transferred you this amount.", amount, GameStore.CoinType.Transferable) GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, "You transferred this amount to " .. reciver, -1 * amount, GameStore.CoinType.Transferable) openStore(playerId) + player:updateUIExhausted() end function parseOpenStore(playerId, msg) @@ -396,6 +396,7 @@ function parseRequestStoreOffers(playerId, msg) addPlayerEvent(sendShowStoreOffers, 250, playerId, searchResultsCategory) end + player:updateUIExhausted() end function parseBuyStoreOffer(playerId, msg) @@ -532,6 +533,8 @@ function parseBuyStoreOffer(playerId, msg) sendUpdatedStoreBalances(playerId) return addPlayerEvent(sendStorePurchaseSuccessful, 650, playerId, message) end + + player:updateUIExhausted() return true end @@ -540,11 +543,13 @@ function parseOpenTransactionHistory(playerId, msg) local page = 1 GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE = msg:getByte() sendStoreTransactionHistory(playerId, page, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) + player:updateUIExhausted() end function parseRequestTransactionHistory(playerId, msg) local page = msg:getU32() sendStoreTransactionHistory(playerId, page + 1, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) + player:updateUIExhausted() end local function getCategoriesRook() diff --git a/data/npclib/npc_system/npc_handler.lua b/data/npclib/npc_system/npc_handler.lua index 8f4d5f2ca48..31aaa88faaf 100644 --- a/data/npclib/npc_system/npc_handler.lua +++ b/data/npclib/npc_system/npc_handler.lua @@ -480,7 +480,6 @@ if NpcHandler == nil then -- If is npc shop, send shop window and parse default message (if not have callback on the npc) if npc:isMerchant() then - npc:closeShopWindow(player) npc:openShopWindow(player) self:say(msg, npc, player) end diff --git a/data/scripts/eventcallbacks/README.md b/data/scripts/eventcallbacks/README.md index 601653574bd..ae5de046bd2 100644 --- a/data/scripts/eventcallbacks/README.md +++ b/data/scripts/eventcallbacks/README.md @@ -14,8 +14,8 @@ Event callbacks are available for several categories of game entities, such as ` ### These are the functions available to use - `(bool)` `creatureOnChangeOutfit` -- `(bool)` `creatureOnAreaCombat` -- `(bool)` `creatureOnTargetCombat` +- `(ReturnValue)` `creatureOnAreaCombat` +- `(ReturnValue)` `creatureOnTargetCombat` - `(void)` `creatureOnHear` - `(void)` `creatureOnDrainHealth` - `(bool)` `partyOnJoin` @@ -66,7 +66,7 @@ local callback = EventCallback() function callback.creatureOnAreaCombat(creature, tile, isAggressive) -- custom behavior when a creature enters combat area - return true + return RETURNVALUE_NOERROR end callback:register() @@ -131,14 +131,36 @@ Here is an example of a boolean event callback: ```lua local callback = EventCallback() +function callback.playerOnMoveItem(player, item, count, fromPos, toPos, fromCylinder, toCylinder) + if item:getId() == ITEM_PARCEL then + --Custom behavior when the player moves a parcel. + return false + end + return true +end + +callback:register() +``` + +### In this example, when a player moves an item, the function checks if the item is a parcel and apply a custom behaviour, returning false making it impossible to move, stopping the associated function on the C++ side. + +## ReturnValue Event Callbacks + +Some event callbacks are expected to return a enum value, in this case, the enum ReturnValue. If the return is different of RETURNVALUE_NOERROR, it will stop the execution of the next callbacks. + +Here is an example of a ReturnValue event callback: + +```lua +local callback = EventCallback() + function callback.creatureOnAreaCombat(creature, tile, isAggressive) -- if the creature is not aggressive, stop the execution of the C++ function if not isAggressive then - return false + return RETURNVALUE_NOTPOSSIBLE end -- custom behavior when an aggressive creature enters a combat area - return true + return RETURNVALUE_NOERROR end callback:register() @@ -146,6 +168,7 @@ callback:register() ### In this example, when a non-aggressive creature enters a combat area, the creatureOnAreaCombat function returns false, stopping the associated function on the C++ side. + ## Multiple Callbacks for the Same Event You can define multiple callbacks for the same event type. This allows you to encapsulate different behaviors in separate callbacks, making your code more modular and easier to manage. diff --git a/data/scripts/eventcallbacks/creature/on_area_combat.lua b/data/scripts/eventcallbacks/creature/on_area_combat.lua index a6295074df3..f68cc95ccad 100644 --- a/data/scripts/eventcallbacks/creature/on_area_combat.lua +++ b/data/scripts/eventcallbacks/creature/on_area_combat.lua @@ -1,7 +1,7 @@ local callback = EventCallback() function callback.creatureOnAreaCombat(creature, tile, isAggressive) - return true + return RETURNVALUE_NOERROR end callback:register() diff --git a/data/scripts/talkactions/god/create_monster.lua b/data/scripts/talkactions/god/manage_monster.lua similarity index 83% rename from data/scripts/talkactions/god/create_monster.lua rename to data/scripts/talkactions/god/manage_monster.lua index ce5869a2eee..da110d59265 100644 --- a/data/scripts/talkactions/god/create_monster.lua +++ b/data/scripts/talkactions/god/manage_monster.lua @@ -56,7 +56,7 @@ local createMonster = TalkAction("/m") -- @param param: String containing the command parameters. -- Format: "/m monstername, monstercount, [fiendish/influenced level], spawnRadius, [forceCreate]" -- Example: "/m rat, 10, fiendish, 5, true" --- @param: the last param is by default "false", if add "," or any value i'ts set to true +-- @param: the last param is by default "false", if add "," or any value it's set to true -- @return true if the command is executed successfully, false otherwise. function createMonster.onSay(player, words, param) -- create log @@ -127,3 +127,33 @@ end createMonster:separator(" ") createMonster:groupType("god") createMonster:register() + +----------------- Rename monster name ----------------- +local setMonsterName = TalkAction("/setmonstername") + +-- @function setMonsterName.onSay +-- @desc TalkAction to rename nearby monsters within a radius of 4 sqm. +-- Format: "/setmonstername newName" +function setMonsterName.onSay(player, words, param) + if param == "" then + player:sendCancelMessage("Command param required.") + return true + end + + local split = param:split(",") + local monsterNewName = split[1] + + local spectators, spectator = Game.getSpectators(player:getPosition(), false, false, 4, 4, 4, 4) + for i = 1, #spectators do + spectator = spectators[i] + if spectator:isMonster() then + spectator:setName(monsterNewName) + end + end + + return true +end + +setMonsterName:separator(" ") +setMonsterName:groupType("god") +setMonsterName:register() diff --git a/data/scripts/talkactions/god/reload.lua b/data/scripts/talkactions/god/reload.lua index 20b9431dbba..d0c0cdd45a8 100644 --- a/data/scripts/talkactions/god/reload.lua +++ b/data/scripts/talkactions/god/reload.lua @@ -7,6 +7,7 @@ local reloadTypes = { ["configuration"] = RELOAD_TYPE_CONFIG, ["core"] = RELOAD_TYPE_CORE, ["events"] = RELOAD_TYPE_EVENTS, + ["familiar"] = RELOAD_TYPE_FAMILIARS, ["global"] = RELOAD_TYPE_CORE, ["group"] = RELOAD_TYPE_GROUPS, ["groups"] = RELOAD_TYPE_GROUPS, @@ -20,6 +21,8 @@ local reloadTypes = { ["monsters"] = RELOAD_TYPE_MONSTERS, ["mount"] = RELOAD_TYPE_MOUNTS, ["mounts"] = RELOAD_TYPE_MOUNTS, + ["outfit"] = RELOAD_TYPE_OUTFITS, + ["outfits"] = RELOAD_TYPE_OUTFITS, ["npc"] = RELOAD_TYPE_NPCS, ["npcs"] = RELOAD_TYPE_NPCS, ["raid"] = RELOAD_TYPE_RAIDS, diff --git a/data/scripts/talkactions/player/reward.lua b/data/scripts/talkactions/player/reward.lua index 3f4cc3787de..a05dab3a933 100644 --- a/data/scripts/talkactions/player/reward.lua +++ b/data/scripts/talkactions/player/reward.lua @@ -26,7 +26,7 @@ local function sendExerciseRewardModal(player) local inbox = player:getStoreInbox() local inboxItems = inbox:getItems() - if inbox and #inboxItems <= inbox:getMaxCapacity() and player:getFreeCapacity() >= iType:getWeight() then + if inbox and #inboxItems < inbox:getMaxCapacity() and player:getFreeCapacity() >= iType:getWeight() then local item = inbox:addItem(it.id, it.charges) if item then item:setActionId(IMMOVABLE_ACTION_ID) diff --git a/schema.sql b/schema.sql index 2fbc7dd649b..af245067057 100644 --- a/schema.sql +++ b/schema.sql @@ -850,9 +850,3 @@ INSERT INTO `players` (4, 'Paladin Sample', 1, 1, 8, 3, 185, 185, 4200, 113, 115, 95, 39, 129, 0, 90, 90, 0, 8, '', 470, 1, 10, 0, 10, 0, 10, 0, 10, 0), (5, 'Knight Sample', 1, 1, 8, 4, 185, 185, 4200, 113, 115, 95, 39, 129, 0, 90, 90, 0, 8, '', 470, 1, 10, 0, 10, 0, 10, 0, 10, 0), (6, 'GOD', 6, 1, 2, 0, 155, 155, 100, 113, 115, 95, 39, 75, 0, 60, 60, 0, 8, '', 410, 1, 10, 0, 10, 0, 10, 0, 10, 0); - --- Create vip groups for GOD account -INSERT INTO `account_vipgroups` (`name`, `account_id`, `customizable`) VALUES -('Friends', 1, 0), -('Enemies', 1, 0), -('Trading Partners', 1, 0); diff --git a/src/creatures/appearance/mounts/mounts.cpp b/src/creatures/appearance/mounts/mounts.cpp index 7051d04ddb8..7014d0b026b 100644 --- a/src/creatures/appearance/mounts/mounts.cpp +++ b/src/creatures/appearance/mounts/mounts.cpp @@ -29,7 +29,7 @@ bool Mounts::loadFromXml() { } for (auto mountNode : doc.child("mounts").children()) { - uint16_t lookType = pugi::cast(mountNode.attribute("clientid").value()); + auto lookType = pugi::cast(mountNode.attribute("clientid").value()); if (g_configManager().getBoolean(WARN_UNSAFE_SCRIPTS, __FUNCTION__) && lookType != 0 && !g_game().isLookTypeRegistered(lookType)) { g_logger().warn("{} - An unregistered creature mount with id '{}' was blocked to prevent client crash.", __FUNCTION__, lookType); continue; diff --git a/src/creatures/appearance/outfit/outfit.cpp b/src/creatures/appearance/outfit/outfit.cpp index 9e96c60e756..97120659333 100644 --- a/src/creatures/appearance/outfit/outfit.cpp +++ b/src/creatures/appearance/outfit/outfit.cpp @@ -14,6 +14,10 @@ #include "utils/tools.hpp" #include "game/game.hpp" +Outfits &Outfits::getInstance() { + return inject(); +} + bool Outfits::reload() { for (auto &outfitsVector : outfits) { outfitsVector.clear(); @@ -41,7 +45,7 @@ bool Outfits::loadFromXml() { continue; } - uint16_t type = pugi::cast(attr.value()); + auto type = pugi::cast(attr.value()); if (type > PLAYERSEX_LAST) { g_logger().warn("[Outfits::loadFromXml] - Invalid outfit type {}", type); continue; @@ -53,7 +57,7 @@ bool Outfits::loadFromXml() { continue; } - if (uint16_t lookType = pugi::cast(lookTypeAttribute.value()); + if (auto lookType = pugi::cast(lookTypeAttribute.value()); g_configManager().getBoolean(WARN_UNSAFE_SCRIPTS, __FUNCTION__) && lookType != 0 && !g_game().isLookTypeRegistered(lookType)) { g_logger().warn("[Outfits::loadFromXml] An unregistered creature looktype type with id '{}' was ignored to prevent client crash.", lookType); @@ -74,7 +78,22 @@ bool Outfits::loadFromXml() { return true; } -std::shared_ptr Outfits::getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const { +std::shared_ptr Outfits::getOutfitByLookType(const std::shared_ptr &player, uint16_t lookType, bool isOppositeOutfit) const { + if (!player) { + g_logger().error("[{}] - Player not found", __FUNCTION__); + return nullptr; + } + + auto sex = player->getSex(); + if (sex != PLAYERSEX_FEMALE && sex != PLAYERSEX_MALE) { + g_logger().error("[{}] - Sex invalid or player: {}", __FUNCTION__, player->getName()); + return nullptr; + } + + if (isOppositeOutfit) { + sex = (sex == PLAYERSEX_MALE) ? PLAYERSEX_FEMALE : PLAYERSEX_MALE; + } + auto it = std::ranges::find_if(outfits[sex], [&lookType](const auto &outfit) { return outfit->lookType == lookType; }); @@ -84,15 +103,3 @@ std::shared_ptr Outfits::getOutfitByLookType(PlayerSex_t sex, uint16_t l } return nullptr; } - -/** - * Get the oposite sex equivalent outfit - * @param sex current sex - * @param lookType current looktype - * @return const pointer to the outfit or nullptr if it could not be found. - */ - -std::shared_ptr Outfits::getOpositeSexOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const { - PlayerSex_t searchSex = (sex == PLAYERSEX_MALE) ? PLAYERSEX_FEMALE : PLAYERSEX_MALE; - return getOutfitByLookType(searchSex, lookType); -} diff --git a/src/creatures/appearance/outfit/outfit.hpp b/src/creatures/appearance/outfit/outfit.hpp index 30b84d1aca2..0d89a2c932d 100644 --- a/src/creatures/appearance/outfit/outfit.hpp +++ b/src/creatures/appearance/outfit/outfit.hpp @@ -12,8 +12,10 @@ #include "declarations.hpp" #include "lib/di/container.hpp" +class Player; + struct OutfitEntry { - constexpr OutfitEntry(uint16_t initLookType, uint8_t initAddons) : + constexpr explicit OutfitEntry(uint16_t initLookType, uint8_t initAddons) : lookType(initLookType), addons(initAddons) { } uint16_t lookType; @@ -42,16 +44,12 @@ struct ProtocolOutfit { class Outfits { public: - static Outfits &getInstance() { - return inject(); - } - - std::shared_ptr getOpositeSexOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const; + static Outfits &getInstance(); - bool loadFromXml(); bool reload(); + bool loadFromXml(); - [[nodiscard]] std::shared_ptr getOutfitByLookType(PlayerSex_t sex, uint16_t lookType) const; + [[nodiscard]] std::shared_ptr getOutfitByLookType(const std::shared_ptr &player, uint16_t lookType, bool isOppositeOutfit = false) const; [[nodiscard]] const std::vector> &getOutfits(PlayerSex_t sex) const { return outfits[sex]; } diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp index 2aaadac4d61..175fad49e20 100644 --- a/src/creatures/combat/combat.cpp +++ b/src/creatures/combat/combat.cpp @@ -21,6 +21,8 @@ #include "items/weapons/weapons.hpp" #include "map/spectators.hpp" #include "lib/metrics/metrics.hpp" +#include "lua/callbacks/event_callback.hpp" +#include "lua/callbacks/events_callbacks.hpp" int32_t Combat::getLevelFormula(std::shared_ptr player, const std::shared_ptr wheelSpell, const CombatDamage &damage) const { if (!player) { @@ -273,8 +275,11 @@ ReturnValue Combat::canDoCombat(std::shared_ptr caster, std::shared_pt } } } - - return g_events().eventCreatureOnAreaCombat(caster, tile, aggressive); + ReturnValue ret = g_events().eventCreatureOnAreaCombat(caster, tile, aggressive); + if (ret == RETURNVALUE_NOERROR) { + ret = g_callbacks().checkCallbackWithReturnValue(EventCallback_t::creatureOnTargetCombat, &EventCallback::creatureOnAreaCombat, caster, tile, aggressive); + } + return ret; } bool Combat::isInPvpZone(std::shared_ptr attacker, std::shared_ptr target) { @@ -409,7 +414,11 @@ ReturnValue Combat::canDoCombat(std::shared_ptr attacker, std::shared_ } } } - return g_events().eventCreatureOnTargetCombat(attacker, target); + ReturnValue ret = g_events().eventCreatureOnTargetCombat(attacker, target); + if (ret == RETURNVALUE_NOERROR) { + ret = g_callbacks().checkCallbackWithReturnValue(EventCallback_t::creatureOnTargetCombat, &EventCallback::creatureOnTargetCombat, attacker, target); + } + return ret; } void Combat::setPlayerCombatValues(formulaType_t newFormulaType, double newMina, double newMinb, double newMaxa, double newMaxb) { diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index 7796863f066..d033d59c829 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -259,7 +259,7 @@ void Creature::addEventWalk(bool firstStep) { self->eventWalk = g_dispatcher().scheduleEvent( static_cast(ticks), - [creatureId = self->getID()] { g_game().checkCreatureWalk(creatureId); }, "Creature::checkCreatureWalk" + [creatureId = self->getID()] { g_game().checkCreatureWalk(creatureId); }, "Game::checkCreatureWalk" ); }); } diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index fccc19b05e5..5e0a7d16bb4 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -33,7 +33,7 @@ std::shared_ptr Monster::createMonster(const std::string &name) { Monster::Monster(const std::shared_ptr mType) : Creature(), - strDescription(asLowerCaseString(mType->nameDescription)), + nameDescription(asLowerCaseString(mType->nameDescription)), mType(mType) { defaultOutfit = mType->info.outfit; currentOutfit = mType->info.outfit; @@ -64,6 +64,37 @@ void Monster::removeList() { g_game().removeMonster(static_self_cast()); } +const std::string &Monster::getName() const { + if (name.empty()) { + return mType->name; + } + return name; +} + +void Monster::setName(const std::string &name) { + if (getName() == name) { + return; + } + + this->name = name; + + // NOTE: Due to how client caches known creatures, + // it is not feasible to send creature update to everyone that has ever met it + auto spectators = Spectators().find(position, true); + for (const auto &spectator : spectators) { + if (const auto &tmpPlayer = spectator->getPlayer()) { + tmpPlayer->sendUpdateTileCreature(static_self_cast()); + } + } +} + +const std::string &Monster::getNameDescription() const { + if (nameDescription.empty()) { + return mType->nameDescription; + } + return nameDescription; +} + bool Monster::canWalkOnFieldType(CombatType_t combatType) const { switch (combatType) { case COMBAT_ENERGYDAMAGE: diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index bf9a39e4133..061ad2b9879 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -41,21 +41,22 @@ class Monster final : public Creature { } } - void removeList() override; void addList() override; + void removeList() override; + + const std::string &getName() const override; + void setName(const std::string &name); - const std::string &getName() const override { - return mType->name; - } // Real monster name, set on monster creation "createMonsterType(typeName)" const std::string &getTypeName() const override { return mType->typeName; } - const std::string &getNameDescription() const override { - return mType->nameDescription; - } + const std::string &getNameDescription() const override; + void setNameDescription(const std::string &nameDescription) { + this->nameDescription = nameDescription; + }; std::string getDescription(int32_t) override { - return strDescription + '.'; + return nameDescription + '.'; } CreatureType_t getType() const override { @@ -363,7 +364,8 @@ class Monster final : public Creature { uint16_t forgeStack = 0; ForgeClassifications_t monsterForgeClassification = ForgeClassifications_t::FORGE_NORMAL_MONSTER; - std::string strDescription; + std::string name; + std::string nameDescription; std::shared_ptr mType; SpawnMonster* spawnMonster = nullptr; @@ -453,7 +455,11 @@ class Monster final : public Creature { void dropLoot(std::shared_ptr corpse, std::shared_ptr lastHitCreature) override; void getPathSearchParams(const std::shared_ptr &creature, FindPathParams &fpp) override; bool useCacheMap() const override { - return !randomStepping; + // return !randomStepping; + // As the map cache is done synchronously for each movement that a monster makes, it is better to disable it, + // as the pathfinder, which is one of the resources that uses this cache the most, + // is multithreding and thus the processing cost is divided between the threads. + return false; } friend class MonsterFunctions; diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index e445a58fcca..8f6fa370087 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -101,14 +101,13 @@ void Npc::onRemoveCreature(std::shared_ptr creature, bool isLogout) { } if (auto player = creature->getPlayer()) { + removeShopPlayer(player->getGUID()); onPlayerDisappear(player); } if (spawnNpc) { spawnNpc->startSpawnNpcCheck(); } - - shopPlayerMap.clear(); } void Npc::onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) { @@ -259,7 +258,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 } uint32_t buyPrice = 0; - const std::vector &shopVector = getShopItemVector(player->getGUID()); + const auto &shopVector = getShopItemVector(player->getGUID()); for (const ShopBlock &shopBlock : shopVector) { if (itemType.id == shopBlock.itemId && shopBlock.itemBuyPrice != 0) { buyPrice = shopBlock.itemBuyPrice; @@ -372,7 +371,7 @@ void Npc::onPlayerSellItem(std::shared_ptr player, uint16_t itemId, uint uint32_t sellPrice = 0; const ItemType &itemType = Item::items[itemId]; - const std::vector &shopVector = getShopItemVector(player->getGUID()); + const auto &shopVector = getShopItemVector(player->getGUID()); for (const ShopBlock &shopBlock : shopVector) { if (itemType.id == shopBlock.itemId && shopBlock.itemSellPrice != 0) { sellPrice = shopBlock.itemSellPrice; @@ -586,7 +585,7 @@ void Npc::setPlayerInteraction(uint32_t playerId, uint16_t topicId /*= 0*/) { void Npc::removePlayerInteraction(std::shared_ptr player) { if (playerInteractions.contains(player->getID())) { playerInteractions.erase(player->getID()); - player->closeShopWindow(true); + player->closeShopWindow(); } } @@ -643,30 +642,26 @@ bool Npc::getRandomStep(Direction &moveDirection) { return false; } -void Npc::addShopPlayer(const std::shared_ptr &player, const std::vector &shopItems /* = {}*/) { - if (!player) { - return; - } - - shopPlayerMap.try_emplace(player->getGUID(), shopItems); +bool Npc::isShopPlayer(uint32_t playerGUID) const { + return shopPlayers.find(playerGUID) != shopPlayers.end(); } -void Npc::removeShopPlayer(const std::shared_ptr &player) { - if (!player) { - return; - } +void Npc::addShopPlayer(uint32_t playerGUID, const std::vector &shopItems) { + shopPlayers.try_emplace(playerGUID, shopItems); +} - shopPlayerMap.erase(player->getGUID()); +void Npc::removeShopPlayer(uint32_t playerGUID) { + shopPlayers.erase(playerGUID); } void Npc::closeAllShopWindows() { - for (const auto &[playerGUID, playerPtr] : shopPlayerMap) { - auto shopPlayer = g_game().getPlayerByGUID(playerGUID); - if (shopPlayer) { - shopPlayer->closeShopWindow(); + for (const auto &[playerGUID, shopBlock] : shopPlayers) { + const auto &player = g_game().getPlayerByGUID(playerGUID); + if (player) { + player->closeShopWindow(); } } - shopPlayerMap.clear(); + shopPlayers.clear(); } void Npc::handlePlayerMove(std::shared_ptr player, const Position &newPos) { diff --git a/src/creatures/npcs/npc.hpp b/src/creatures/npcs/npc.hpp index 7e8be84c7f1..c246aa4b68d 100644 --- a/src/creatures/npcs/npc.hpp +++ b/src/creatures/npcs/npc.hpp @@ -95,10 +95,10 @@ class Npc final : public Creature { npcType->info.currencyId = currency; } - std::vector getShopItemVector(uint32_t playerGUID) { + const std::vector &getShopItemVector(uint32_t playerGUID) const { if (playerGUID != 0) { - auto it = shopPlayerMap.find(playerGUID); - if (it != shopPlayerMap.end() && !it->second.empty()) { + auto it = shopPlayers.find(playerGUID); + if (it != shopPlayers.end() && !it->second.empty()) { return it->second; } } @@ -165,8 +165,10 @@ class Npc final : public Creature { internalLight = npcType->info.light; } - void addShopPlayer(const std::shared_ptr &player, const std::vector &shopItems = {}); - void removeShopPlayer(const std::shared_ptr &player); + bool isShopPlayer(uint32_t playerGUID) const; + + void addShopPlayer(uint32_t playerGUID, const std::vector &shopItems); + void removeShopPlayer(uint32_t playerGUID); void closeAllShopWindows(); static uint32_t npcAutoID; @@ -184,7 +186,7 @@ class Npc final : public Creature { std::map playerInteractions; - phmap::flat_hash_map> shopPlayerMap; + std::unordered_map> shopPlayers; std::shared_ptr npcType; std::shared_ptr spawnNpc; diff --git a/src/creatures/players/grouping/familiars.cpp b/src/creatures/players/grouping/familiars.cpp index a922013f19b..6e923b823dc 100644 --- a/src/creatures/players/grouping/familiars.cpp +++ b/src/creatures/players/grouping/familiars.cpp @@ -10,10 +10,15 @@ #include "pch.hpp" #include "creatures/players/grouping/familiars.hpp" +#include "lib/di/container.hpp" #include "config/configmanager.hpp" #include "utils/pugicast.hpp" #include "utils/tools.hpp" +Familiars &Familiars::getInstance() { + return inject(); +} + bool Familiars::reload() { for (auto &familiarsVector : familiars) { familiarsVector.clear(); @@ -42,7 +47,7 @@ bool Familiars::loadFromXml() { continue; } - uint16_t vocation = pugi::cast(attr.value()); + auto vocation = pugi::cast(attr.value()); if (vocation > VOCATION_LAST) { g_logger().warn("[Familiars::loadFromXml] - Invalid familiar vocation {}", vocation); continue; diff --git a/src/creatures/players/grouping/familiars.hpp b/src/creatures/players/grouping/familiars.hpp index 8f553af9fbe..9eda7d95ba2 100644 --- a/src/creatures/players/grouping/familiars.hpp +++ b/src/creatures/players/grouping/familiars.hpp @@ -9,6 +9,7 @@ #pragma once +#include "declarations.hpp" #include "lib/di/container.hpp" struct FamiliarEntry { @@ -32,9 +33,7 @@ struct Familiar { class Familiars { public: - static Familiars &getInstance() { - return inject(); - } + static Familiars &getInstance(); bool loadFromXml(); bool reload(); diff --git a/src/creatures/players/grouping/groups.cpp b/src/creatures/players/grouping/groups.cpp index 27ae68d9fd4..937fa848f3f 100644 --- a/src/creatures/players/grouping/groups.cpp +++ b/src/creatures/players/grouping/groups.cpp @@ -43,7 +43,7 @@ PlayerFlags_t Groups::getFlagFromNumber(uint8_t value) { return magic_enum::enum_value(value); } -bool Groups::reload() const { +bool Groups::reload() { // Clear groups g_game().groups.getGroups().clear(); return g_game().groups.load(); @@ -99,7 +99,7 @@ bool Groups::load() { return true; } -std::shared_ptr Groups::getGroup(uint16_t id) { +std::shared_ptr Groups::getGroup(uint16_t id) const { if (auto it = std::find_if(groups_vector.begin(), groups_vector.end(), [id](auto group_it) { return group_it->id == id; }); diff --git a/src/creatures/players/grouping/groups.hpp b/src/creatures/players/grouping/groups.hpp index d76914e2632..af319e95772 100644 --- a/src/creatures/players/grouping/groups.hpp +++ b/src/creatures/players/grouping/groups.hpp @@ -24,9 +24,9 @@ class Groups { public: static uint8_t getFlagNumber(PlayerFlags_t playerFlags); static PlayerFlags_t getFlagFromNumber(uint8_t value); - bool reload() const; + static bool reload(); bool load(); - std::shared_ptr getGroup(uint16_t id); + [[nodiscard]] std::shared_ptr getGroup(uint16_t id) const; std::vector> &getGroups() { return groups_vector; } diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 56e046ed01a..dc6de3b6a7c 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -1715,13 +1715,7 @@ void Player::onCreatureAppear(std::shared_ptr creature, bool isLogin) Creature::onCreatureAppear(creature, isLogin); if (isLogin && creature == getPlayer()) { - for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { - std::shared_ptr item = inventory[slot]; - if (item) { - item->startDecaying(); - g_moveEvents().onPlayerEquip(getPlayer(), item, static_cast(slot), false); - } - } + onEquipInventory(); // Refresh bosstiary tracker onLogin refreshCyclopediaMonsterTracker(true); @@ -1862,14 +1856,9 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) Creature::onRemoveCreature(creature, isLogout); if (auto player = getPlayer(); player == creature) { - for (uint8_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { - const auto item = inventory[slot]; - if (item) { - g_moveEvents().onPlayerDeEquip(getPlayer(), item, static_cast(slot)); - } - } - if (isLogout) { + onDeEquipInventory(); + if (m_party) { m_party->leaveParty(player); } @@ -1905,32 +1894,39 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout) } } -bool Player::openShopWindow(std::shared_ptr npc) { +bool Player::openShopWindow(std::shared_ptr npc, const std::vector &shopItems) { + Benchmark brenchmark; if (!npc) { g_logger().error("[Player::openShopWindow] - Npc is wrong or nullptr"); return false; } + if (npc->isShopPlayer(getGUID())) { + g_logger().debug("[Player::openShopWindow] - Player {} is already in shop window", getName()); + return false; + } + + npc->addShopPlayer(getGUID(), shopItems); + setShopOwner(npc); sendShop(npc); std::map inventoryMap; sendSaleItemList(getAllSaleItemIdAndCount(inventoryMap)); + + g_logger().debug("[Player::openShopWindow] - Player {} has opened shop window in {} ms", getName(), brenchmark.duration()); return true; } -bool Player::closeShopWindow(bool sendCloseShopWindow /*= true*/) { +bool Player::closeShopWindow() { if (!shopOwner) { return false; } - shopOwner->removeShopPlayer(static_self_cast()); + shopOwner->removeShopPlayer(getGUID()); setShopOwner(nullptr); - if (sendCloseShopWindow) { - sendCloseShop(); - } - + sendCloseShop(); return true; } @@ -2011,6 +2007,25 @@ void Player::onCreatureMove(const std::shared_ptr &creature, const std } } +void Player::onEquipInventory() { + for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { + std::shared_ptr item = inventory[slot]; + if (item) { + item->startDecaying(); + g_moveEvents().onPlayerEquip(getPlayer(), item, static_cast(slot), false); + } + } +} + +void Player::onDeEquipInventory() { + for (int32_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) { + std::shared_ptr item = inventory[slot]; + if (item) { + g_moveEvents().onPlayerDeEquip(getPlayer(), item, static_cast(slot)); + } + } +} + // container void Player::onAddContainerItem(std::shared_ptr item) { checkTradeState(item); @@ -4285,7 +4300,7 @@ bool Player::hasShopItemForSale(uint16_t itemId, uint8_t subType) const { } const ItemType &itemType = Item::items[itemId]; - std::vector shoplist = shopOwner->getShopItemVector(getGUID()); + const auto &shoplist = shopOwner->getShopItemVector(getGUID()); return std::any_of(shoplist.begin(), shoplist.end(), [&](const ShopBlock &shopBlock) { return shopBlock.itemId == itemId && shopBlock.itemBuyPrice != 0 && (!itemType.isFluidContainer() || shopBlock.itemSubType == subType); }); @@ -4914,7 +4929,7 @@ bool Player::canWear(uint16_t lookType, uint8_t addons) const { return true; } - const auto &outfit = Outfits::getInstance().getOutfitByLookType(sex, lookType); + const auto &outfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), lookType); if (!outfit) { return false; } @@ -5001,7 +5016,7 @@ bool Player::removeOutfitAddon(uint16_t lookType, uint8_t addons) { return false; } -bool Player::getOutfitAddons(const std::shared_ptr outfit, uint8_t &addons) const { +bool Player::getOutfitAddons(const std::shared_ptr &outfit, uint8_t &addons) const { if (group->access) { addons = 3; return true; @@ -5830,7 +5845,7 @@ bool Player::toggleMount(bool mount) { return false; } - const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getSex(), defaultOutfit.lookType); + const auto &playerOutfit = Outfits::getInstance().getOutfitByLookType(getPlayer(), defaultOutfit.lookType); if (!playerOutfit) { return false; } diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index c0761704db4..141caafacd9 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -153,8 +153,8 @@ class Player final : public Creature, public Cylinder, public Bankable { const std::string &getName() const override { return name; } - void setName(std::string newName) { - this->name = std::move(newName); + void setName(const std::string &name) { + this->name = name; } const std::string &getTypeName() const override { return name; @@ -476,7 +476,12 @@ class Player final : public Creature, public Cylinder, public Bankable { return blessings[index - 1] != 0; } uint8_t getBlessingCount(uint8_t index) const { - return blessings[index - 1]; + if (index > 0 && index <= blessings.size()) { + return blessings[index - 1]; + } else { + g_logger().error("[{}] - index outside range 0-10.", __FUNCTION__); + return 0; + } } std::string getBlessingsName() const; @@ -845,8 +850,8 @@ class Player final : public Creature, public Cylinder, public Bankable { void onWalkComplete() override; void stopWalk(); - bool openShopWindow(std::shared_ptr npc); - bool closeShopWindow(bool sendCloseShopWindow = true); + bool openShopWindow(std::shared_ptr npc, const std::vector &shopItems = {}); + bool closeShopWindow(); bool updateSaleShopList(std::shared_ptr item); bool hasShopItemForSale(uint16_t itemId, uint8_t subType) const; @@ -1031,7 +1036,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void addOutfit(uint16_t lookType, uint8_t addons); bool removeOutfit(uint16_t lookType); bool removeOutfitAddon(uint16_t lookType, uint8_t addons); - bool getOutfitAddons(const std::shared_ptr outfit, uint8_t &addons) const; + bool getOutfitAddons(const std::shared_ptr &outfit, uint8_t &addons) const; bool canFamiliar(uint16_t lookType) const; void addFamiliar(uint16_t lookType); @@ -1070,6 +1075,11 @@ class Player final : public Creature, public Cylinder, public Bankable { client->sendRemoveTileThing(pos, stackpos); } } + void sendUpdateTileCreature(const std::shared_ptr creature) { + if (client) { + client->sendUpdateTileCreature(creature->getPosition(), creature->getTile()->getClientIndexOfCreature(static_self_cast(), creature), creature); + } + } void sendUpdateTile(std::shared_ptr updateTile, const Position &pos) { if (client) { client->sendUpdateTile(updateTile, pos); @@ -1305,6 +1315,9 @@ class Player final : public Creature, public Cylinder, public Bankable { void onRemoveCreature(std::shared_ptr creature, bool isLogout) override; void onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) override; + void onEquipInventory(); + void onDeEquipInventory(); + void onAttackedCreatureDisappear(bool isLogout) override; void onFollowCreatureDisappear(bool isLogout) override; @@ -2549,8 +2562,7 @@ class Player final : public Creature, public Cylinder, public Bankable { } bool checkAutoLoot(bool isBoss) const { - const bool autoLoot = g_configManager().getBoolean(AUTOLOOT, __FUNCTION__); - if (!autoLoot) { + if (!g_configManager().getBoolean(AUTOLOOT, __FUNCTION__)) { return false; } if (g_configManager().getBoolean(VIP_SYSTEM_ENABLED, __FUNCTION__) && g_configManager().getBoolean(VIP_AUTOLOOT_VIP_ONLY, __FUNCTION__) && !isVip()) { @@ -2558,18 +2570,13 @@ class Player final : public Creature, public Cylinder, public Bankable { } auto featureKV = kv()->scoped("features")->get("autoloot"); - if (featureKV.has_value()) { - auto value = featureKV->getNumber(); - if (value == 2) { - return true; - } else if (value == 1) { - return !isBoss; - } else if (value == 0) { - return false; - } + auto value = featureKV.has_value() ? featureKV->getNumber() : 0; + if (value == 2) { + return true; + } else if (value == 1) { + return !isBoss; } - - return true; + return false; } QuickLootFilter_t getQuickLootFilter() const { @@ -2853,7 +2860,7 @@ class Player final : public Creature, public Cylinder, public Bankable { uint16_t lastStatsTrainingTime = 0; uint16_t staminaMinutes = 2520; - std::vector blessings = { 0, 0, 0, 0, 0, 0, 0, 0 }; + std::vector blessings = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; uint16_t maxWriteLen = 0; uint16_t baseXpGain = 100; uint16_t voucherXpBoost = 0; diff --git a/src/creatures/players/vip/player_vip.cpp b/src/creatures/players/vip/player_vip.cpp index b4b1642ec69..95ebe91ad5f 100644 --- a/src/creatures/players/vip/player_vip.cpp +++ b/src/creatures/players/vip/player_vip.cpp @@ -143,13 +143,13 @@ std::shared_ptr PlayerVIP::getGroupByName(const std::string &name) con void PlayerVIP::addGroupInternal(uint8_t groupId, const std::string &name, bool customizable) { if (getGroupByName(name) != nullptr) { - g_logger().warn("{} - Group name already exists.", __FUNCTION__); + g_logger().debug("{} - Group name already exists.", __FUNCTION__); return; } const auto freeId = getFreeId(); if (freeId == 0) { - g_logger().warn("{} - No id available.", __FUNCTION__); + g_logger().debug("{} - No id available.", __FUNCTION__); return; } diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp index f0637011a19..c6e1b7032c0 100644 --- a/src/enums/player_cyclopedia.hpp +++ b/src/enums/player_cyclopedia.hpp @@ -36,3 +36,17 @@ enum CyclopediaTitle_t : uint8_t { MAP, OTHERS, }; + +enum class CyclopediaMapData_t : uint8_t { + MinimapMarker = 0, + DiscoveryData = 1, + ActiveRaid = 2, + ImminentRaidMainArea = 3, + ImminentRaidSubArea = 4, + SetDiscoveryArea = 5, + Passage = 6, + SubAreaMonsters = 7, + MonsterBestiary = 8, + Donations = 9, + SetCurrentArea = 10, +}; diff --git a/src/game/functions/game_reload.cpp b/src/game/functions/game_reload.cpp index 0fc74bcee71..d9d9eb1f449 100644 --- a/src/game/functions/game_reload.cpp +++ b/src/game/functions/game_reload.cpp @@ -22,7 +22,7 @@ GameReload::GameReload() = default; GameReload::~GameReload() = default; -bool GameReload::init(Reload_t reloadTypes) const { +bool GameReload::init(Reload_t reloadTypes) { switch (reloadTypes) { case Reload_t::RELOAD_TYPE_ALL: return reloadAll(); @@ -32,34 +32,38 @@ bool GameReload::init(Reload_t reloadTypes) const { return reloadConfig(); case Reload_t::RELOAD_TYPE_EVENTS: return reloadEvents(); - case Reload_t::RELOAD_TYPE_CORE: - return reloadCore(); + case Reload_t::RELOAD_TYPE_MODULES: + return reloadModules(); + case Reload_t::RELOAD_TYPE_OUTFITS: + return reloadOutfits(); + case Reload_t::RELOAD_TYPE_MOUNTS: + return reloadMounts(); + case Reload_t::RELOAD_TYPE_FAMILIARS: + return reloadFamiliars(); case Reload_t::RELOAD_TYPE_IMBUEMENTS: return reloadImbuements(); + case Reload_t::RELOAD_TYPE_VOCATIONS: + return reloadVocations(); + case Reload_t::RELOAD_TYPE_CORE: + return reloadCore(); + case Reload_t::RELOAD_TYPE_GROUPS: + return reloadGroups(); + case Reload_t::RELOAD_TYPE_SCRIPTS: + return reloadScripts(); case Reload_t::RELOAD_TYPE_ITEMS: return reloadItems(); - case Reload_t::RELOAD_TYPE_MODULES: - return reloadModules(); case Reload_t::RELOAD_TYPE_MONSTERS: return reloadMonsters(); - case Reload_t::RELOAD_TYPE_MOUNTS: - return reloadMounts(); case Reload_t::RELOAD_TYPE_NPCS: return reloadNpcs(); case Reload_t::RELOAD_TYPE_RAIDS: return reloadRaids(); - case Reload_t::RELOAD_TYPE_SCRIPTS: - return reloadScripts(); - case Reload_t::RELOAD_TYPE_GROUPS: - return reloadGroups(); - case Reload_t::RELOAD_TYPE_VOCATIONS: - return reloadVocations(); default: return false; } } -uint8_t GameReload::getReloadNumber(Reload_t reloadTypes) const { +uint8_t GameReload::getReloadNumber(Reload_t reloadTypes) { return magic_enum::enum_integer(reloadTypes); } @@ -78,7 +82,7 @@ void logReloadStatus(const std::string &name, bool result) { * If it is necessary to call elsewhere, seriously think about creating a function that calls this * Changing this to public may cause some unexpected behavior or bug */ -bool GameReload::reloadAll() const { +bool GameReload::reloadAll() { std::vector reloadResults; reloadResults.reserve(magic_enum::enum_count()); @@ -93,25 +97,62 @@ bool GameReload::reloadAll() const { return std::ranges::any_of(reloadResults, [](bool result) { return result; }); } -bool GameReload::reloadChat() const { +bool GameReload::reloadChat() { const bool result = g_chat().load(); logReloadStatus("Chat", result); return result; } -bool GameReload::reloadConfig() const { +bool GameReload::reloadConfig() { const bool result = g_configManager().reload(); logReloadStatus("Config", result); return result; } -bool GameReload::reloadEvents() const { +bool GameReload::reloadEvents() { const bool result = g_events().loadFromXml(); logReloadStatus("Events", result); return result; } -bool GameReload::reloadCore() const { +bool GameReload::reloadModules() { + const bool result = g_modules().reload(); + logReloadStatus("Modules", result); + return result; +} + +bool GameReload::reloadOutfits() { + const bool result = g_game().outfits.reload(); + logReloadStatus("Outfits", result); + return result; +} + +bool GameReload::reloadMounts() { + const bool result = g_game().mounts.reload(); + logReloadStatus("Mounts", result); + return result; +} + +bool GameReload::reloadFamiliars() { + const bool result = g_game().familiars.reload(); + logReloadStatus("Familiars", result); + return result; +} + +bool GameReload::reloadImbuements() { + const bool result = g_imbuements().reload(); + logReloadStatus("Imbuements", result); + return result; +} + +bool GameReload::reloadVocations() { + const bool result = g_vocations().reload(); + reloadScripts(); + logReloadStatus("Vocations", result); + return result; +} + +bool GameReload::reloadCore() { const auto &coreFolder = g_configManager().getString(CORE_DIRECTORY, __FUNCTION__); const bool coreLoaded = g_luaEnvironment().loadFile(coreFolder + "/core.lua", "core.lua") == 0; @@ -126,25 +167,38 @@ bool GameReload::reloadCore() const { return false; } -bool GameReload::reloadImbuements() const { - const bool result = g_imbuements().reload(); - logReloadStatus("Imbuements", result); +bool GameReload::reloadGroups() { + const bool result = g_game().groups.reload(); + logReloadStatus("Groups", result); return result; } -bool GameReload::reloadItems() const { - const bool result = Item::items.reload(); - logReloadStatus("Items", result); - return result; +bool GameReload::reloadScripts() { + g_scripts().clearAllScripts(); + Zone::clearZones(); + + const auto &datapackFolder = g_configManager().getString(DATA_DIRECTORY, __FUNCTION__); + const auto &coreFolder = g_configManager().getString(CORE_DIRECTORY, __FUNCTION__); + + g_scripts().loadScripts(coreFolder + "/scripts/lib", true, false); + g_scripts().loadScripts(datapackFolder + "/scripts", false, true); + g_scripts().loadScripts(coreFolder + "/scripts", false, true); + + // It should come last, after everything else has been cleaned up. + reloadMonsters(); + reloadNpcs(); + reloadItems(); + logReloadStatus("Scripts", true); + return true; } -bool GameReload::reloadModules() const { - const bool result = g_modules().reload(); - logReloadStatus("Modules", result); +bool GameReload::reloadItems() { + const bool result = Item::items.reload(); + logReloadStatus("Items", result); return result; } -bool GameReload::reloadMonsters() const { +bool GameReload::reloadMonsters() { g_monsters().clear(); const auto &datapackFolder = g_configManager().getString(DATA_DIRECTORY, __FUNCTION__); const auto &coreFolder = g_configManager().getString(CORE_DIRECTORY, __FUNCTION__); @@ -161,50 +215,14 @@ bool GameReload::reloadMonsters() const { } } -bool GameReload::reloadMounts() const { - const bool result = g_game().mounts.reload(); - logReloadStatus("Mounts", result); - return result; -} - -bool GameReload::reloadNpcs() const { +bool GameReload::reloadNpcs() { const bool result = g_npcs().reload(); logReloadStatus("NPCs", result); return result; } -bool GameReload::reloadRaids() const { +bool GameReload::reloadRaids() { const bool result = g_game().raids.reload() && g_game().raids.startup(); logReloadStatus("Raids", result); return result; } - -bool GameReload::reloadScripts() const { - g_scripts().clearAllScripts(); - Zone::clearZones(); - - const auto &datapackFolder = g_configManager().getString(DATA_DIRECTORY, __FUNCTION__); - const auto &coreFolder = g_configManager().getString(CORE_DIRECTORY, __FUNCTION__); - - g_scripts().loadScripts(coreFolder + "/scripts/lib", true, false); - g_scripts().loadScripts(datapackFolder + "/scripts", false, true); - g_scripts().loadScripts(coreFolder + "/scripts", false, true); - - // It should come last, after everything else has been cleaned up. - reloadMonsters(); - reloadNpcs(); - logReloadStatus("Scripts", true); - return true; -} - -bool GameReload::reloadGroups() const { - const bool result = g_game().groups.reload(); - logReloadStatus("Groups", result); - return result; -} - -bool GameReload::reloadVocations() const { - const bool result = g_vocations().reload(); - logReloadStatus("Vocations", result); - return result; -} diff --git a/src/game/functions/game_reload.hpp b/src/game/functions/game_reload.hpp index 45ff35c01a2..0f59046a9d4 100644 --- a/src/game/functions/game_reload.hpp +++ b/src/game/functions/game_reload.hpp @@ -19,17 +19,19 @@ enum class Reload_t : uint8_t { RELOAD_TYPE_CHAT, RELOAD_TYPE_CONFIG, RELOAD_TYPE_EVENTS, - RELOAD_TYPE_CORE, + RELOAD_TYPE_MODULES, + RELOAD_TYPE_OUTFITS, + RELOAD_TYPE_MOUNTS, + RELOAD_TYPE_FAMILIARS, RELOAD_TYPE_IMBUEMENTS, + RELOAD_TYPE_VOCATIONS, + RELOAD_TYPE_CORE, + RELOAD_TYPE_GROUPS, + RELOAD_TYPE_SCRIPTS, RELOAD_TYPE_ITEMS, - RELOAD_TYPE_MODULES, RELOAD_TYPE_MONSTERS, - RELOAD_TYPE_MOUNTS, RELOAD_TYPE_NPCS, RELOAD_TYPE_RAIDS, - RELOAD_TYPE_SCRIPTS, - RELOAD_TYPE_GROUPS, - RELOAD_TYPE_VOCATIONS, // Every is last RELOAD_TYPE_LAST @@ -48,25 +50,27 @@ class GameReload : public Game { return inject(); } - bool init(Reload_t reloadType) const; - uint8_t getReloadNumber(Reload_t reloadTypes) const; + static bool init(Reload_t reloadType); + static uint8_t getReloadNumber(Reload_t reloadTypes); private: - bool reloadAll() const; - bool reloadChat() const; - bool reloadConfig() const; - bool reloadEvents() const; - bool reloadCore() const; - bool reloadImbuements() const; - bool reloadItems() const; - bool reloadModules() const; - bool reloadMonsters() const; - bool reloadMounts() const; - bool reloadNpcs() const; - bool reloadRaids() const; - bool reloadScripts() const; - bool reloadGroups() const; - bool reloadVocations() const; + static bool reloadAll(); + static bool reloadChat(); + static bool reloadConfig(); + static bool reloadEvents(); + static bool reloadModules(); + static bool reloadOutfits(); + static bool reloadMounts(); + static bool reloadFamiliars(); + static bool reloadImbuements(); + static bool reloadVocations(); + static bool reloadCore(); + static bool reloadGroups(); + static bool reloadScripts(); + static bool reloadItems(); + static bool reloadMonsters(); + static bool reloadNpcs(); + static bool reloadRaids(); }; constexpr auto g_gameReload = GameReload::getInstance; diff --git a/src/game/game.cpp b/src/game/game.cpp index 5ad2dfb516f..3fdfa54c35e 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -4261,7 +4261,7 @@ void Game::playerSetShowOffSocket(uint32_t playerId, Outfit_t &outfit, const Pos name << item->getName() << " displaying the "; bool outfited = false; if (outfit.lookType != 0) { - const auto &outfitInfo = Outfits::getInstance().getOutfitByLookType(player->getSex(), outfit.lookType); + const auto &outfitInfo = Outfits::getInstance().getOutfitByLookType(player, outfit.lookType); if (!outfitInfo) { return; } @@ -5932,7 +5932,7 @@ void Game::playerChangeOutfit(uint32_t playerId, Outfit_t outfit, uint8_t isMoun outfit.lookMount = randomMount->clientId; } - const auto playerOutfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), outfit.lookType); + const auto playerOutfit = Outfits::getInstance().getOutfitByLookType(player, outfit.lookType); if (!playerOutfit) { outfit.lookMount = 0; } @@ -6260,7 +6260,6 @@ void Game::checkCreatureWalk(uint32_t creatureId) { const auto &creature = getCreatureByID(creatureId); if (creature && creature->getHealth() > 0) { creature->onCreatureWalk(); - cleanup(); } } @@ -6323,7 +6322,6 @@ void Game::checkCreatures() { --end; } } - cleanup(); index = (index + 1) % EVENT_CREATURECOUNT; } @@ -7972,8 +7970,6 @@ void Game::shutdown() { map.spawnsNpc.clear(); raids.clear(); - cleanup(); - if (serviceManager) { serviceManager->stop(); } @@ -7985,16 +7981,6 @@ void Game::shutdown() { g_logger().info("Done!"); } -void Game::cleanup() { - for (auto it = browseFields.begin(); it != browseFields.end();) { - if (it->second.expired()) { - it = browseFields.erase(it); - } else { - ++it; - } - } -} - void Game::addBestiaryList(uint16_t raceid, std::string name) { auto it = BestiaryList.find(raceid); if (it != BestiaryList.end()) { diff --git a/src/game/game.hpp b/src/game/game.hpp index 3a199254259..2de00410e5e 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -426,7 +426,6 @@ class Game { void updatePlayerHelpers(std::shared_ptr player); - void cleanup(); void shutdown(); void dieSafely(const std::string &errorMsg); void addBestiaryList(uint16_t raceid, std::string name); @@ -582,8 +581,10 @@ class Game { bool hasDistanceEffect(uint16_t effectId); Groups groups; + Familiars familiars; Map map; Mounts mounts; + Outfits outfits; Raids raids; std::unique_ptr m_appearancesPtr; diff --git a/src/game/scheduling/dispatcher.cpp b/src/game/scheduling/dispatcher.cpp index 7cff69d66bf..dbbfc020be5 100644 --- a/src/game/scheduling/dispatcher.cpp +++ b/src/game/scheduling/dispatcher.cpp @@ -56,30 +56,51 @@ void Dispatcher::executeSerialEvents(std::vector &tasks) { } void Dispatcher::executeParallelEvents(std::vector &tasks, const uint8_t groupId) { - std::atomic_uint_fast64_t totalTaskSize = tasks.size(); - std::atomic_bool isTasksCompleted = false; + asyncWait(tasks.size(), [groupId, &tasks](size_t i) { + dispacherContext.type = DispatcherType::AsyncEvent; + dispacherContext.group = static_cast(groupId); + tasks[i].execute(); - for (const auto &task : tasks) { - threadPool.detach_task([groupId, &task, &isTasksCompleted, &totalTaskSize] { - dispacherContext.type = DispatcherType::AsyncEvent; - dispacherContext.group = static_cast(groupId); - dispacherContext.taskName = task.getContext(); + dispacherContext.reset(); + }); - task.execute(); + tasks.clear(); +} - dispacherContext.reset(); +void Dispatcher::asyncWait(size_t requestSize, std::function &&f) { + if (requestSize == 0) { + return; + } - totalTaskSize.fetch_sub(1); - if (totalTaskSize.load() == 0) { - isTasksCompleted.store(true); - isTasksCompleted.notify_one(); - } - }); + // This prevents an async call from running inside another async call. + if (asyncWaitDisabled) { + for (uint_fast64_t i = 0; i < requestSize; ++i) { + f(i); + } + return; } - isTasksCompleted.wait(false); + const auto &partitions = generatePartition(requestSize); + const auto pSize = partitions.size(); - tasks.clear(); + BS::multi_future retFuture; + + if (pSize > 1) { + asyncWaitDisabled = true; + const auto min = partitions[1].first; + const auto max = partitions[partitions.size() - 1].second; + retFuture = threadPool.submit_loop(min, max, [&f](const unsigned int i) { f(i); }); + } + + const auto &[min, max] = partitions[0]; + for (uint_fast64_t i = min; i < max; ++i) { + f(i); + } + + if (pSize > 1) { + retFuture.wait(); + asyncWaitDisabled = false; + } } void Dispatcher::executeEvents(const TaskGroup startGroup) { diff --git a/src/game/scheduling/dispatcher.hpp b/src/game/scheduling/dispatcher.hpp index a6cbc8dd6dd..94b284c9316 100644 --- a/src/game/scheduling/dispatcher.hpp +++ b/src/game/scheduling/dispatcher.hpp @@ -108,6 +108,7 @@ class Dispatcher { } void asyncEvent(std::function &&f, TaskGroup group = TaskGroup::GenericParallel); + void asyncWait(size_t size, std::function &&f); uint64_t asyncCycleEvent(uint32_t delay, std::function &&f, TaskGroup group = TaskGroup::GenericParallel) { return scheduleEvent( @@ -173,6 +174,22 @@ class Dispatcher { } } + std::vector> generatePartition(size_t size) const { + if (size == 0) { + return {}; + } + + std::vector> list; + list.reserve(threadPool.get_thread_count()); + + const auto size_per_block = std::ceil(size / static_cast(threadPool.get_thread_count())); + for (uint_fast64_t i = 0; i < size; i += size_per_block) { + list.emplace_back(i, std::min(size, i + size_per_block)); + } + + return list; + } + uint_fast64_t dispatcherCycle = 0; ThreadPool &threadPool; @@ -200,6 +217,8 @@ class Dispatcher { phmap::btree_multiset, Task::Compare> scheduledTasks; phmap::parallel_flat_hash_map_m> scheduledTasksRef; + bool asyncWaitDisabled = false; + friend class CanaryServer; }; diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index c6591887c8a..7bdc7db8084 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -70,10 +70,10 @@ class Task { bool hasTraceableContext() const { const static auto tasksContext = std::unordered_set({ - "Creature::checkCreatureWalk", "Decay::checkDecay", "Dispatcher::asyncEvent", "Game::checkCreatureAttack", + "Game::checkCreatureWalk", "Game::checkCreatures", "Game::checkImbuements", "Game::checkLight", @@ -94,7 +94,6 @@ class Task { "SpawnNpc::checkSpawnNpc", "Webhook::run", "Protocol::sendRecvMessageCallback", - "sendRecvMessageCallback", }); return tasksContext.contains(context); diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index 6273f8fefbb..8f7cf461fdf 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -66,7 +66,7 @@ bool IOLoginDataLoad::preLoadPlayer(std::shared_ptr player, const std::s } player->setGUID(result->getNumber("id")); - std::shared_ptr group = g_game().groups.getGroup(result->getNumber("group_id")); + const auto &group = g_game().groups.getGroup(result->getNumber("group_id")); if (!group) { g_logger().error("Player {} has group id {} which doesn't exist", player->name, result->getNumber("group_id")); return false; @@ -118,7 +118,7 @@ bool IOLoginDataLoad::loadPlayerFirst(std::shared_ptr player, DBResult_p player->setAccount(result->getNumber("account_id")); } - std::shared_ptr group = g_game().groups.getGroup(result->getNumber("group_id")); + const auto &group = g_game().groups.getGroup(result->getNumber("group_id")); if (!group) { g_logger().error("Player {} has group id {} which doesn't exist", player->name, result->getNumber("group_id")); return false; diff --git a/src/items/containers/container.cpp b/src/items/containers/container.cpp index 63ce657741a..6c526a8500a 100644 --- a/src/items/containers/container.cpp +++ b/src/items/containers/container.cpp @@ -63,6 +63,10 @@ std::shared_ptr Container::create(std::shared_ptr tile) { Container::~Container() { if (getID() == ITEM_BROWSEFIELD) { + if (getParent() && getParent()->getTile()) { + g_game().browseFields.erase(getParent()->getTile()); + } + for (std::shared_ptr item : itemlist) { item->setParent(getParent()); } diff --git a/src/lib/thread/thread_pool.cpp b/src/lib/thread/thread_pool.cpp index 702f0c05504..b2f51ef8f9d 100644 --- a/src/lib/thread/thread_pool.cpp +++ b/src/lib/thread/thread_pool.cpp @@ -26,7 +26,7 @@ #endif ThreadPool::ThreadPool(Logger &logger) : - logger(logger), BS::thread_pool(std::max(getNumberOfCores(), DEFAULT_NUMBER_OF_THREADS)) { + BS::thread_pool(std::max(getNumberOfCores(), DEFAULT_NUMBER_OF_THREADS)), logger(logger) { start(); } @@ -35,6 +35,7 @@ void ThreadPool::start() { } void ThreadPool::shutdown() { - stopped = true; logger.info("Shutting down thread pool..."); + stopped = true; + wait(); } diff --git a/src/lua/callbacks/events_callbacks.hpp b/src/lua/callbacks/events_callbacks.hpp index 53b119f6445..f71103047e6 100644 --- a/src/lua/callbacks/events_callbacks.hpp +++ b/src/lua/callbacks/events_callbacks.hpp @@ -89,6 +89,32 @@ class EventsCallbacks { } } } + /** + * @brief Checks if all registered callbacks of the specified event type succeed. + * @param eventType The type of event to check. + * @param callbackFunc Function pointer to the callback method. + * @param args Variadic arguments to pass to the callback function. + * @return ReturnValue enum. + */ + template + ReturnValue checkCallbackWithReturnValue(EventCallback_t eventType, CallbackFunc callbackFunc, Args &&... args) { + ReturnValue res = RETURNVALUE_NOERROR; + for (const auto &callback : getCallbacksByType(eventType)) { + auto argsCopy = std::make_tuple(args...); + if (callback && callback->isLoadedCallback()) { + ReturnValue callbackResult = std::apply( + [&callback, &callbackFunc](auto &&... args) { + return ((*callback).*callbackFunc)(std::forward(args)...); + }, + argsCopy + ); + if (callbackResult != RETURNVALUE_NOERROR) { + return callbackResult; + } + } + } + return res; + } /** * @brief Checks if all registered callbacks of the specified event type succeed. diff --git a/src/lua/functions/creatures/monster/monster_functions.cpp b/src/lua/functions/creatures/monster/monster_functions.cpp index 1591053c391..273f6af5c99 100644 --- a/src/lua/functions/creatures/monster/monster_functions.cpp +++ b/src/lua/functions/creatures/monster/monster_functions.cpp @@ -77,7 +77,7 @@ int MonsterFunctions::luaMonsterSetType(lua_State* L) { } // Assign new MonsterType monster->mType = mType; - monster->strDescription = asLowerCaseString(mType->nameDescription); + monster->nameDescription = asLowerCaseString(mType->nameDescription); monster->defaultOutfit = mType->info.outfit; monster->currentOutfit = mType->info.outfit; monster->skull = mType->info.skull; @@ -529,6 +529,24 @@ int MonsterFunctions::luaMonsterGetName(lua_State* L) { return 1; } +int MonsterFunctions::luaMonsterSetName(lua_State* L) { + // monster:setName(name[, nameDescription]) + auto monster = getUserdataShared(L, 1); + if (!monster) { + reportErrorFunc(getErrorDesc(LUA_ERROR_MONSTER_NOT_FOUND)); + pushBoolean(L, false); + return 0; + } + + monster->setName(getString(L, 2)); + if (lua_gettop(L) >= 3) { + monster->setNameDescription(getString(L, 3)); + } + + pushBoolean(L, true); + return 1; +} + int MonsterFunctions::luaMonsterHazard(lua_State* L) { // get: monster:hazard() ; set: monster:hazard(hazard) std::shared_ptr monster = getUserdataShared(L, 1); diff --git a/src/lua/functions/creatures/monster/monster_functions.hpp b/src/lua/functions/creatures/monster/monster_functions.hpp index bf2785ae430..dd1c3827344 100644 --- a/src/lua/functions/creatures/monster/monster_functions.hpp +++ b/src/lua/functions/creatures/monster/monster_functions.hpp @@ -56,6 +56,7 @@ class MonsterFunctions final : LuaScriptInterface { registerMethod(L, "Monster", "isForgeable", MonsterFunctions::luaMonsterIsForgeable); registerMethod(L, "Monster", "getName", MonsterFunctions::luaMonsterGetName); + registerMethod(L, "Monster", "setName", MonsterFunctions::luaMonsterSetName); registerMethod(L, "Monster", "hazard", MonsterFunctions::luaMonsterHazard); registerMethod(L, "Monster", "hazardCrit", MonsterFunctions::luaMonsterHazardCrit); @@ -116,6 +117,7 @@ class MonsterFunctions final : LuaScriptInterface { static int luaMonsterIsForgeable(lua_State* L); static int luaMonsterGetName(lua_State* L); + static int luaMonsterSetName(lua_State* L); static int luaMonsterHazard(lua_State* L); static int luaMonsterHazardCrit(lua_State* L); diff --git a/src/lua/functions/creatures/npc/npc_functions.cpp b/src/lua/functions/creatures/npc/npc_functions.cpp index 8c19a4ede0a..13e9b499abc 100644 --- a/src/lua/functions/creatures/npc/npc_functions.cpp +++ b/src/lua/functions/creatures/npc/npc_functions.cpp @@ -354,7 +354,6 @@ int NpcFunctions::luaNpcOpenShopWindow(lua_State* L) { return 1; } - npc->addShopPlayer(player); pushBoolean(L, player->openShopWindow(npc)); return 1; } @@ -405,10 +404,7 @@ int NpcFunctions::luaNpcOpenShopWindowTable(lua_State* L) { } lua_pop(L, 3); - // Close any eventual other shop window currently open. - player->closeShopWindow(true); - npc->addShopPlayer(player, items); - pushBoolean(L, player->openShopWindow(npc)); + pushBoolean(L, player->openShopWindow(npc, items)); return 1; } @@ -429,7 +425,7 @@ int NpcFunctions::luaNpcCloseShopWindow(lua_State* L) { } if (player->getShopOwner() == npc) { - player->closeShopWindow(true); + player->closeShopWindow(); } pushBoolean(L, true); @@ -577,7 +573,7 @@ int NpcFunctions::luaNpcSellItem(lua_State* L) { } uint64_t pricePerUnit = 0; - const std::vector &shopVector = npc->getShopItemVector(player->getGUID()); + const auto &shopVector = npc->getShopItemVector(player->getGUID()); for (ShopBlock shopBlock : shopVector) { if (itemId == shopBlock.itemId && shopBlock.itemBuyPrice != 0) { pricePerUnit = shopBlock.itemBuyPrice; diff --git a/src/lua/functions/creatures/player/group_functions.cpp b/src/lua/functions/creatures/player/group_functions.cpp index 5acc16d72a9..f547eec1590 100644 --- a/src/lua/functions/creatures/player/group_functions.cpp +++ b/src/lua/functions/creatures/player/group_functions.cpp @@ -17,7 +17,7 @@ int GroupFunctions::luaGroupCreate(lua_State* L) { // Group(id) uint32_t id = getNumber(L, 2); - std::shared_ptr group = g_game().groups.getGroup(id); + const auto &group = g_game().groups.getGroup(id); if (group) { pushUserdata(L, group); setMetatable(L, -1, "Group"); @@ -29,7 +29,7 @@ int GroupFunctions::luaGroupCreate(lua_State* L) { int GroupFunctions::luaGroupGetId(lua_State* L) { // group:getId() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { lua_pushnumber(L, group->id); } else { @@ -40,7 +40,7 @@ int GroupFunctions::luaGroupGetId(lua_State* L) { int GroupFunctions::luaGroupGetName(lua_State* L) { // group:getName() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { pushString(L, group->name); } else { @@ -51,7 +51,7 @@ int GroupFunctions::luaGroupGetName(lua_State* L) { int GroupFunctions::luaGroupGetFlags(lua_State* L) { // group:getFlags() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { std::bitset flags; for (uint8_t i = 0; i < magic_enum::enum_integer(PlayerFlags_t::FlagLast); ++i) { @@ -68,7 +68,7 @@ int GroupFunctions::luaGroupGetFlags(lua_State* L) { int GroupFunctions::luaGroupGetAccess(lua_State* L) { // group:getAccess() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { pushBoolean(L, group->access); } else { @@ -79,7 +79,7 @@ int GroupFunctions::luaGroupGetAccess(lua_State* L) { int GroupFunctions::luaGroupGetMaxDepotItems(lua_State* L) { // group:getMaxDepotItems() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { lua_pushnumber(L, group->maxDepotItems); } else { @@ -90,7 +90,7 @@ int GroupFunctions::luaGroupGetMaxDepotItems(lua_State* L) { int GroupFunctions::luaGroupGetMaxVipEntries(lua_State* L) { // group:getMaxVipEntries() - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { lua_pushnumber(L, group->maxVipEntries); } else { @@ -101,7 +101,7 @@ int GroupFunctions::luaGroupGetMaxVipEntries(lua_State* L) { int GroupFunctions::luaGroupHasFlag(lua_State* L) { // group:hasFlag(flag) - std::shared_ptr group = getUserdataShared(L, 1); + const auto &group = getUserdataShared(L, 1); if (group) { auto flag = static_cast(getNumber(L, 2)); pushBoolean(L, group->flags[Groups::getFlagNumber(flag)]); diff --git a/src/map/map.cpp b/src/map/map.cpp index 8b8cebeb539..82629aeae05 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -342,13 +342,35 @@ void Map::moveCreature(const std::shared_ptr &creature, const std::sha bool teleport = forceTeleport || !newTile->getGround() || !Position::areInRange<1, 1, 0>(oldPos, newPos); - auto spectators = Spectators() - .find(oldPos, true) - .find(newPos, true); + Spectators spectators; + if (!teleport && oldPos.z == newPos.z) { + int32_t minRangeX = MAP_MAX_VIEW_PORT_X; + int32_t maxRangeX = MAP_MAX_VIEW_PORT_X; + int32_t minRangeY = MAP_MAX_VIEW_PORT_Y; + int32_t maxRangeY = MAP_MAX_VIEW_PORT_Y; + + if (oldPos.y > newPos.y) { + ++minRangeY; + } else if (oldPos.y < newPos.y) { + ++maxRangeY; + } + + if (oldPos.x < newPos.x) { + ++maxRangeX; + } else if (oldPos.x > newPos.x) { + ++minRangeX; + } + + spectators.find(oldPos, true, minRangeX, maxRangeX, minRangeY, maxRangeY); + } else { + spectators.find(oldPos, true); + spectators.find(newPos, true); + } auto playersSpectators = spectators.filter(); std::vector oldStackPosVector; + oldStackPosVector.reserve(playersSpectators.size()); for (const auto &spec : playersSpectators) { if (spec->canSeeCreature(creature)) { oldStackPosVector.push_back(oldTile->getClientIndexOfCreature(spec->getPlayer(), creature)); diff --git a/src/map/spectators.cpp b/src/map/spectators.cpp index 36cce6d535b..10aaf0faf87 100644 --- a/src/map/spectators.cpp +++ b/src/map/spectators.cpp @@ -18,50 +18,27 @@ void Spectators::clearCache() { spectatorsCache.clear(); } -bool Spectators::contains(const std::shared_ptr &creature) { - return creatures.contains(creature); -} - -bool Spectators::erase(const std::shared_ptr &creature) { - return creatures.erase(creature); -} - Spectators Spectators::insert(const std::shared_ptr &creature) { if (creature) { - creatures.emplace(creature); + creatures.emplace_back(creature); } return *this; } -Spectators Spectators::insertAll(const SpectatorList &list) { +Spectators Spectators::insertAll(const CreatureVector &list) { if (!list.empty()) { - creatures.insertAll(list); - } - return *this; -} + const bool hasValue = !creatures.empty(); -Spectators Spectators::join(Spectators &anotherSpectators) { - return insertAll(anotherSpectators.creatures.data()); -} - -bool Spectators::empty() const noexcept { - return creatures.empty(); -} + creatures.insert(creatures.end(), list.begin(), list.end()); -size_t Spectators::size() noexcept { - return creatures.size(); -} - -CreatureVector::iterator Spectators::begin() noexcept { - return creatures.begin(); -} - -CreatureVector::iterator Spectators::end() noexcept { - return creatures.end(); -} - -const CreatureVector &Spectators::data() noexcept { - return creatures.data(); + // Remove duplicate + if (hasValue) { + std::unordered_set uset(creatures.begin(), creatures.end()); + creatures.clear(); + creatures.insert(creatures.end(), uset.begin(), uset.end()); + } + } + return *this; } bool Spectators::checkCache(const SpectatorsCache::FloorData &specData, bool onlyPlayers, const Position ¢erPos, bool checkDistance, bool multifloor, int32_t minRangeX, int32_t maxRangeX, int32_t minRangeY, int32_t maxRangeY) { @@ -77,7 +54,7 @@ bool Spectators::checkCache(const SpectatorsCache::FloorData &specData, bool onl } if (checkDistance) { - SpectatorList spectators; + CreatureVector spectators; spectators.reserve(creatures.size()); for (const auto &creature : *list) { const auto &specPos = creature->getPosition(); @@ -176,7 +153,7 @@ Spectators Spectators::find(const Position ¢erPos, bool multifloor, bool onl const int32_t endx2 = x2 - (x2 & SECTOR_MASK); const int32_t endy2 = y2 - (y2 & SECTOR_MASK); - SpectatorList spectators; + CreatureVector spectators; spectators.reserve(std::max(MAP_MAX_VIEW_PORT_X, MAP_MAX_VIEW_PORT_Y) * 2); const MapSector* startSector = g_game().map.getMapSector(startx1, starty1); diff --git a/src/map/spectators.hpp b/src/map/spectators.hpp index 93526e05c93..9e998da2bfa 100644 --- a/src/map/spectators.hpp +++ b/src/map/spectators.hpp @@ -16,12 +16,10 @@ class Monster; class Npc; struct Position; -using SpectatorList = std::vector>; - struct SpectatorsCache { struct FloorData { - std::optional floor; - std::optional multiFloor; + std::optional floor; + std::optional multiFloor; }; int32_t minRangeX { 0 }; @@ -48,16 +46,39 @@ class Spectators { requires std::is_base_of_v Spectators filter(); - bool contains(const std::shared_ptr &creature); - bool erase(const std::shared_ptr &creature); Spectators insert(const std::shared_ptr &creature); - Spectators insertAll(const SpectatorList &list); - Spectators join(Spectators &anotherSpectators); - bool empty() const noexcept; - size_t size() noexcept; - CreatureVector::iterator begin() noexcept; - CreatureVector::iterator end() noexcept; - const CreatureVector &data() noexcept; + Spectators insertAll(const CreatureVector &list); + Spectators join(const Spectators &anotherSpectators) { + return insertAll(anotherSpectators.creatures); + } + + bool contains(const std::shared_ptr &creature) const { + return std::ranges::find(creatures, creature) != creatures.end(); + } + + bool erase(const std::shared_ptr &creature) { + return std::erase(creatures, creature) > 0; + } + + bool empty() const noexcept { + return creatures.empty(); + } + + size_t size() const noexcept { + return creatures.size(); + } + + auto begin() const noexcept { + return creatures.begin(); + } + + auto end() const noexcept { + return creatures.end(); + } + + const auto &data() const noexcept { + return creatures; + } private: static phmap::flat_hash_map spectatorsCache; @@ -65,7 +86,7 @@ class Spectators { Spectators find(const Position ¢erPos, bool multifloor = false, bool onlyPlayers = false, int32_t minRangeX = 0, int32_t maxRangeX = 0, int32_t minRangeY = 0, int32_t maxRangeY = 0); bool checkCache(const SpectatorsCache::FloorData &specData, bool onlyPlayers, const Position ¢erPos, bool checkDistance, bool multifloor, int32_t minRangeX, int32_t maxRangeX, int32_t minRangeY, int32_t maxRangeY); - stdext::vector_set> creatures; + CreatureVector creatures; }; template diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index 15963135560..38acc0b484c 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -40,9 +40,9 @@ Position NetworkMessage::getPosition() { return pos; } -void NetworkMessage::addString(const std::string &value, const std::string &function) { +void NetworkMessage::addString(const std::string &value, const std::string &function /* = ""*/) { size_t stringLen = value.length(); - if (value.empty()) { + if (value.empty() && !function.empty()) { g_logger().debug("[NetworkMessage::addString] - Value string is empty, function '{}'", function); } if (!canAdd(stringLen + 2)) { diff --git a/src/server/network/message/networkmessage.hpp b/src/server/network/message/networkmessage.hpp index 72f0e69c3dc..02e19253146 100644 --- a/src/server/network/message/networkmessage.hpp +++ b/src/server/network/message/networkmessage.hpp @@ -90,7 +90,20 @@ class NetworkMessage { void addBytes(const char* bytes, size_t size); void addPaddingBytes(size_t n); - void addString(const std::string &value, const std::string &function); + /** + * Adds a string to the network message buffer. + * + * @param value The string value to be added to the message buffer. + * @param function * @param function An optional parameter that specifies the function name from which `addString` is invoked. + * Including this enhances logging by adding the function name to the debug and error log messages. + * This helps in debugging by indicating the context when issues occur, such as attempting to add an + * empty string or when there are message size errors. + * + * When the function parameter is used, it aids in identifying the context in log messages, + * making it easier to diagnose issues related to network message construction, + * especially in complex systems where the same method might be called from multiple places. + */ + void addString(const std::string &value, const std::string &function = ""); void addDouble(double value, uint8_t precision = 2); diff --git a/src/server/network/protocol/protocol.cpp b/src/server/network/protocol/protocol.cpp index 6f6a1c8228e..78cff72fab4 100644 --- a/src/server/network/protocol/protocol.cpp +++ b/src/server/network/protocol/protocol.cpp @@ -50,7 +50,7 @@ bool Protocol::sendRecvMessageCallback(NetworkMessage &msg) { protocol->parsePacket(msg); protocolConnection->resumeWork(); } - } }, __FUNCTION__); + } }, "Protocol::sendRecvMessageCallback"); return true; } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 8ff8be9ec57..59d2f9285a6 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -3388,7 +3388,7 @@ void ProtocolGame::sendAddMarker(const Position &pos, uint8_t markType, const st msg.addByte(0xDD); if (!oldProtocol) { - msg.addByte(0x00); // unknow + msg.addByte(enumToValue(CyclopediaMapData_t::MinimapMarker)); } msg.addPosition(pos); @@ -4062,7 +4062,7 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { // Outfit description playerDescriptionSize++; msg.addString("Outfit", "ProtocolGame::sendCyclopediaCharacterInspection - Outfit"); - if (const auto outfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), player->getDefaultOutfit().lookType)) { + if (const auto outfit = Outfits::getInstance().getOutfitByLookType(player, player->getDefaultOutfit().lookType)) { msg.addString(outfit->name, "ProtocolGame::sendCyclopediaCharacterInspection - outfit->name"); } else { msg.addString("unknown", "ProtocolGame::sendCyclopediaCharacterInspection - unknown"); @@ -4644,29 +4644,45 @@ void ProtocolGame::sendLootStats(std::shared_ptr item, uint8_t count) { } void ProtocolGame::sendShop(std::shared_ptr npc) { + Benchmark brenchmark; NetworkMessage msg; msg.addByte(0x7A); msg.addString(npc->getName(), "ProtocolGame::sendShop - npc->getName()"); if (!oldProtocol) { msg.add(npc->getCurrency()); - msg.addString(std::string(), "ProtocolGame::sendShop - std::string()"); // Currency name + msg.addString(std::string()); // Currency name } - std::vector shoplist = npc->getShopItemVector(player->getGUID()); + const auto &shoplist = npc->getShopItemVector(player->getGUID()); uint16_t itemsToSend = std::min(shoplist.size(), std::numeric_limits::max()); msg.add(itemsToSend); + // Initialize before the loop to avoid database overload on each iteration + auto talkactionHidden = player->kv()->get("npc-shop-hidden-sell-item"); + // Initialize the inventoryMap outside the loop to avoid creation on each iteration + std::map inventoryMap; + player->getAllSaleItemIdAndCount(inventoryMap); uint16_t i = 0; for (const ShopBlock &shopBlock : shoplist) { if (++i > itemsToSend) { break; } + // Hidden sell items from the shop if they are not in the player's inventory + if (talkactionHidden && talkactionHidden->get()) { + const auto &foundItem = inventoryMap.find(shopBlock.itemId); + if (foundItem == inventoryMap.end() && shopBlock.itemSellPrice > 0 && shopBlock.itemBuyPrice == 0) { + AddHiddenShopItem(msg); + continue; + } + } + AddShopItem(msg, shopBlock); } writeToOutputBuffer(msg); + g_logger().debug("ProtocolGame::sendShop - Time: {} ms, shop items: {}", brenchmark.duration(), shoplist.size()); } void ProtocolGame::sendCloseShop() { @@ -5864,6 +5880,8 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { msg.add(purchaseStatistics.highestPrice); msg.add(purchaseStatistics.lowestPrice); } + } else { + msg.addByte(0x00); } } else { msg.addByte(0x00); // send to old protocol ? @@ -5887,6 +5905,8 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { msg.add(saleStatistics.highestPrice); msg.add(saleStatistics.lowestPrice); } + } else { + msg.addByte(0x00); } } else { msg.addByte(0x00); // send to old protocol ? @@ -5954,7 +5974,7 @@ void ProtocolGame::sendCreatureTurn(std::shared_ptr creature, uint32_t NetworkMessage msg; msg.addByte(0x6B); msg.addPosition(creature->getPosition()); - msg.addByte(stackPos); + msg.addByte(static_cast(stackPos)); msg.add(0x63); msg.add(creature->getID()); msg.addByte(creature->getDirection()); @@ -6392,7 +6412,7 @@ void ProtocolGame::sendAddTileItem(const Position &pos, uint32_t stackpos, std:: NetworkMessage msg; msg.addByte(0x6A); msg.addPosition(pos); - msg.addByte(stackpos); + msg.addByte(static_cast(stackpos)); AddItem(msg, item); writeToOutputBuffer(msg); } @@ -6405,7 +6425,7 @@ void ProtocolGame::sendUpdateTileItem(const Position &pos, uint32_t stackpos, st NetworkMessage msg; msg.addByte(0x6B); msg.addPosition(pos); - msg.addByte(stackpos); + msg.addByte(static_cast(stackpos)); AddItem(msg, item); writeToOutputBuffer(msg); } @@ -6420,6 +6440,23 @@ void ProtocolGame::sendRemoveTileThing(const Position &pos, uint32_t stackpos) { writeToOutputBuffer(msg); } +void ProtocolGame::sendUpdateTileCreature(const Position &pos, uint32_t stackpos, const std::shared_ptr creature) { + if (!canSee(pos)) { + return; + } + + NetworkMessage msg; + msg.addByte(0x6B); + msg.addPosition(pos); + msg.addByte(static_cast(stackpos)); + + bool known; + uint32_t removedKnown; + checkCreatureAsKnown(creature->getID(), known, removedKnown); + AddCreature(msg, creature, false, removedKnown); + writeToOutputBuffer(msg); +} + void ProtocolGame::sendUpdateTile(std::shared_ptr tile, const Position &pos) { if (!canSee(pos)) { return; @@ -6491,7 +6528,7 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos NetworkMessage msg; msg.addByte(0x6A); msg.addPosition(pos); - msg.addByte(stackpos); + msg.addByte(static_cast(stackpos)); bool known; uint32_t removedKnown; @@ -6554,6 +6591,7 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos if (isLogin) { sendMagicEffect(pos, CONST_ME_TELEPORT); + sendHotkeyPreset(); sendDisableLoginMusic(); } @@ -6648,7 +6686,7 @@ void ProtocolGame::sendMoveCreature(std::shared_ptr creature, const Po } else { msg.addByte(0x6D); msg.addPosition(oldPos); - msg.addByte(oldStackPos); + msg.addByte(static_cast(oldStackPos)); msg.addPosition(newPos); } @@ -6683,7 +6721,7 @@ void ProtocolGame::sendMoveCreature(std::shared_ptr creature, const Po NetworkMessage msg; msg.addByte(0x6D); msg.addPosition(oldPos); - msg.addByte(oldStackPos); + msg.addByte(static_cast(oldStackPos)); msg.addPosition(newPos); writeToOutputBuffer(msg); } @@ -7439,7 +7477,7 @@ void ProtocolGame::AddCreature(NetworkMessage &msg, std::shared_ptr cr } if (!oldProtocol && creature->isHealthHidden()) { - msg.addString("", "ProtocolGame::AddCreature - empty"); + msg.addString(std::string()); } else { msg.addString(creature->getName(), "ProtocolGame::AddCreature - creature->getName()"); } @@ -7841,7 +7879,7 @@ void ProtocolGame::RemoveTileThing(NetworkMessage &msg, const Position &pos, uin msg.addByte(0x6C); msg.addPosition(pos); - msg.addByte(stackpos); + msg.addByte(static_cast(stackpos)); } void ProtocolGame::sendKillTrackerUpdate(std::shared_ptr corpse, const std::string &name, const Outfit_t creatureOutfit) { @@ -8085,7 +8123,7 @@ void ProtocolGame::AddHiddenShopItem(NetworkMessage &msg) { // Empty bytes from AddShopItem msg.add(0); msg.addByte(0); - msg.addString(std::string(), "ProtocolGame::AddHiddenShopItem - std::string()"); + msg.addString(std::string()); msg.add(0); msg.add(0); msg.add(0); @@ -8098,18 +8136,6 @@ void ProtocolGame::AddShopItem(NetworkMessage &msg, const ShopBlock &shopBlock) return; } - // Hidden sell items from the shop if they are not in the player's inventory - auto talkactionHidden = player->kv()->get("npc-shop-hidden-sell-item"); - if (talkactionHidden && talkactionHidden->get() == true) { - std::map inventoryMap; - player->getAllSaleItemIdAndCount(inventoryMap); - auto inventoryItems = inventoryMap.find(shopBlock.itemId); - if (inventoryItems == inventoryMap.end() && shopBlock.itemSellPrice > 0 && shopBlock.itemBuyPrice == 0) { - AddHiddenShopItem(msg); - return; - } - } - const ItemType &it = Item::items[shopBlock.itemId]; msg.add(shopBlock.itemId); if (it.isSplash() || it.isFluidContainer()) { @@ -8284,7 +8310,7 @@ void ProtocolGame::reloadCreature(std::shared_ptr creature) { if (knownCreatureSet.contains(creature->getID())) { msg.addByte(0x6B); msg.addPosition(creature->getPosition()); - msg.addByte(stackpos); + msg.addByte(static_cast(stackpos)); AddCreature(msg, creature, false, 0); } else { sendAddCreature(creature, creature->getPosition(), stackpos, false); @@ -9036,3 +9062,17 @@ void ProtocolGame::sendDisableLoginMusic() { msg.addByte(0x00); writeToOutputBuffer(msg); } + +void ProtocolGame::sendHotkeyPreset() { + if (!player || oldProtocol) { + return; + } + + auto vocation = g_vocations().getVocation(player->getVocation()->getBaseId()); + if (vocation) { + NetworkMessage msg; + msg.addByte(0x9D); + msg.add(vocation->getClientId()); + writeToOutputBuffer(msg); + } +} diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 0386093cfc4..24341f15e1e 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -393,6 +393,7 @@ class ProtocolGame final : public Protocol { void sendAddTileItem(const Position &pos, uint32_t stackpos, std::shared_ptr item); void sendUpdateTileItem(const Position &pos, uint32_t stackpos, std::shared_ptr item); void sendRemoveTileThing(const Position &pos, uint32_t stackpos); + void sendUpdateTileCreature(const Position &pos, uint32_t stackpos, const std::shared_ptr creature); void sendUpdateTile(std::shared_ptr tile, const Position &pos); void sendAddCreature(std::shared_ptr creature, const Position &pos, int32_t stackpos, bool isLogin); @@ -513,6 +514,7 @@ class ProtocolGame final : public Protocol { void sendSingleSoundEffect(const Position &pos, SoundEffect_t id, SourceEffect_t source); void sendDoubleSoundEffect(const Position &pos, SoundEffect_t mainSoundId, SourceEffect_t mainSource, SoundEffect_t secondarySoundId, SourceEffect_t secondarySource); + void sendHotkeyPreset(); void sendDisableLoginMusic(); uint8_t m_playerDeathTime = 0;