diff --git a/data-otxserver/lib/core/storages.lua b/data-otxserver/lib/core/storages.lua
index fce8059f7..2b8d9a778 100644
--- a/data-otxserver/lib/core/storages.lua
+++ b/data-otxserver/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-otxserver/monster/quests/grave_danger/bosses/lord_azaram.lua b/data-otxserver/monster/quests/grave_danger/bosses/lord_azaram.lua
index 701a0b6f9..8507c3a0c 100644
--- a/data-otxserver/monster/quests/grave_danger/bosses/lord_azaram.lua
+++ b/data-otxserver/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-otxserver/monster/quests/the_dream_courts/bosses/faceless_bane.lua b/data-otxserver/monster/quests/the_dream_courts/bosses/faceless_bane.lua
index e4d8553c4..868fe08e7 100644
--- a/data-otxserver/monster/quests/the_dream_courts/bosses/faceless_bane.lua
+++ b/data-otxserver/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-otxserver/scripts/creaturescripts/monster/faceless_bane_immunity.lua b/data-otxserver/scripts/creaturescripts/monster/faceless_bane_immunity.lua
new file mode 100644
index 000000000..36e1ecd11
--- /dev/null
+++ b/data-otxserver/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-otxserver/scripts/creaturescripts/quests/grimvale/feroxa_transform.lua b/data-otxserver/scripts/creaturescripts/quests/grimvale/feroxa_transform.lua
new file mode 100644
index 000000000..ac7cd6f2b
--- /dev/null
+++ b/data-otxserver/scripts/creaturescripts/quests/grimvale/feroxa_transform.lua
@@ -0,0 +1,39 @@
+local feroxaTransform = CreatureEvent("FeroxaTransform")
+function feroxaTransform.onThink(creature)
+ if creature:getName():lower() ~= "feroxa" then
+ return true
+ end
+ if creature:getMaxHealth() == 100000 then
+ if creature:getHealth() <= 50000 then
+ creature:getPosition():sendMagicEffect(CONST_ME_POFF)
+ Game.createMonster("feroxa2", creature:getPosition(), true, true)
+ creature:remove()
+ end
+ end
+ if creature:getMaxHealth() == 50000 then
+ if creature:getHealth() <= 25000 then
+ creature:getPosition():sendMagicEffect(CONST_ME_POFF)
+ local feroxas = {
+ [1] = { name = "feroxa3" },
+ [2] = { name = "feroxa4" },
+ }
+ Game.createMonster(feroxas[math.random(#feroxas)].name, creature:getPosition(), true, true)
+ creature:remove()
+ end
+ end
+end
+
+feroxaTransform:register()
+
+local feroxaDeath = CreatureEvent("FeroxaDeath")
+function feroxaDeath.onDeath(creature, corpse, deathList)
+ if creature and creature:getMonster() then
+ local pool = Tile(creature:getPosition()):getItemById(2886)
+ if pool then
+ pool:remove()
+ end
+ Game.createMonster("Feroxa5", creature:getPosition(), true, true)
+ end
+end
+
+feroxaDeath:register()
diff --git a/data/libs/functions/boss_lever.lua b/data/libs/functions/boss_lever.lua
index 3792cdf62..b95bf7211 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 8b5784759..30134cd7c 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/blessings/blessings.lua b/data/modules/scripts/blessings/blessings.lua
index adfa364e7..e061501a3 100644
--- a/data/modules/scripts/blessings/blessings.lua
+++ b/data/modules/scripts/blessings/blessings.lua
@@ -21,7 +21,7 @@ Blessings.Credits = {
Blessings.Config = {
AdventurerBlessingLevel = configManager.getNumber(configKeys.ADVENTURERSBLESSING_LEVEL), -- Free full bless until level
- HasToF = false, -- Enables/disables twist of fate
+ HasToF = not configManager.getBoolean(configKeys.TOGGLE_SERVER_IS_RETRO), -- Enables/disables twist of fate
InquisitonBlessPriceMultiplier = 1.1, -- Bless price multiplied by henricus
SkulledDeathLoseStoreItem = configManager.getBoolean(configKeys.SKULLED_DEATH_LOSE_STORE_ITEM), -- Destroy all items on store when dying with red/blackskull
InventoryGlowOnFiveBless = configManager.getBoolean(configKeys.INVENTORY_GLOW), -- Glow in yellow inventory items when the player has 5 or more bless,
@@ -142,7 +142,7 @@ Blessings.sendBlessDialog = function(player)
msg:addU16(Blessings.BitWiseTable[v.id])
msg:addByte(player:getBlessingCount(v.id))
if player:getClient().version > 1200 then
- msg:addByte(0) -- Store Blessings Count
+ msg:addByte(player:getBlessingCount(v.id, true)) -- Store Blessings Count
end
end
end
diff --git a/data/modules/scripts/daily_reward/daily_reward.lua b/data/modules/scripts/daily_reward/daily_reward.lua
index 09b927ced..b6a7c1699 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/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua
index f000c4079..ed17c456c 100644
--- a/data/modules/scripts/gamestore/gamestore.lua
+++ b/data/modules/scripts/gamestore/gamestore.lua
@@ -135,7 +135,7 @@ GameStore.Categories = {
icons = { "Blood_of_the_Mountain.png" },
name = "Blood of the Mountain",
price = 25,
- blessid = 8,
+ blessid = 7,
count = 1,
id = GameStore.SubActions.BLESSING_BLOOD,
description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death",
@@ -154,7 +154,7 @@ GameStore.Categories = {
icons = { "Heart_of_the_Mountain.png" },
name = "Heart of the Mountain",
price = 25,
- blessid = 7,
+ blessid = 8,
count = 1,
id = GameStore.SubActions.BLESSING_HEART,
description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death",
diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua
index ba7398d9d..433abf344 100644
--- a/data/modules/scripts/gamestore/init.lua
+++ b/data/modules/scripts/gamestore/init.lua
@@ -47,8 +47,8 @@ GameStore.SubActions = {
BLESSING_SUNS = 6,
BLESSING_SPIRITUAL = 7,
BLESSING_EMBRACE = 8,
- BLESSING_HEART = 9,
- BLESSING_BLOOD = 10,
+ BLESSING_BLOOD = 9,
+ BLESSING_HEART = 10,
BLESSING_ALL_PVE = 11,
BLESSING_ALL_PVP = 12,
CHARM_EXPANSION = 13,
@@ -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,18 @@ function parseRequestStoreOffers(playerId, msg)
addPlayerEvent(sendShowStoreOffers, 250, playerId, searchResultsCategory)
end
+ player:updateUIExhausted()
+end
+
+-- Used on cyclopedia store summary
+local function insertPlayerTransactionSummary(player, offer)
+ local id = offer.id
+ if offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then
+ id = offer.itemtype
+ elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then
+ id = offer.blessid
+ end
+ player:createTransactionSummary(offer.type, math.max(1, offer.count or 1), id)
end
function parseBuyStoreOffer(playerId, msg)
@@ -449,9 +461,7 @@ function parseBuyStoreOffer(playerId, msg)
-- Handled errors are thrown to indicate that the purchase has failed;
-- Handled errors have a code index and unhandled errors do not
local pcallOk, pcallError = pcall(function()
- if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM then
- GameStore.processItemPurchase(player, offer.itemtype, offer.count or 1, offer.movable, offer.setOwner)
- elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then
+ if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then
GameStore.processItemPurchase(player, offer.itemtype, offer.count or 1, offer.movable, offer.setOwner)
elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then
GameStore.processInstantRewardAccess(player, offer.count)
@@ -465,11 +475,9 @@ function parseBuyStoreOffer(playerId, msg)
GameStore.processPremiumPurchase(player, offer.id)
elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_STACKABLE then
GameStore.processStackablePurchase(player, offer.itemtype, offer.count, offer.name, offer.movable, offer.setOwner)
- elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then
+ elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_BED then
GameStore.processHouseRelatedPurchase(player, offer)
- elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT then
- GameStore.processOutfitPurchase(player, offer.sexId, offer.addon)
- elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then
+ elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then
GameStore.processOutfitPurchase(player, offer.sexId, offer.addon)
elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then
GameStore.processMountPurchase(player, offer.id)
@@ -503,8 +511,6 @@ function parseBuyStoreOffer(playerId, msg)
GameStore.processHirelingSkillPurchase(player, offer)
elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then
GameStore.processHirelingOutfitPurchase(player, offer)
- elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_BED then
- GameStore.processHouseRelatedPurchase(player, offer)
else
-- This should never happen by our convention, but just in case the guarding condition is messed up...
error({ code = 0, message = "This offer is unavailable [2]" })
@@ -522,6 +528,9 @@ function parseBuyStoreOffer(playerId, msg)
return queueSendStoreAlertToUser(alertMessage, 500, playerId)
end
+ if table.contains({ GameStore.OfferTypes.OFFER_TYPE_HOUSE, GameStore.OfferTypes.OFFER_TYPE_EXPBOOST, GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS }, offer.type) then
+ insertPlayerTransactionSummary(player, offer)
+ end
local configure = useOfferConfigure(offer.type)
if configure ~= GameStore.ConfigureOffers.SHOW_CONFIGURE then
if not player:makeCoinTransaction(offer) then
@@ -532,19 +541,33 @@ function parseBuyStoreOffer(playerId, msg)
sendUpdatedStoreBalances(playerId)
return addPlayerEvent(sendStorePurchaseSuccessful, 650, playerId, message)
end
+
+ player:updateUIExhausted()
return true
end
-- Both functions use same formula!
function parseOpenTransactionHistory(playerId, msg)
+ local player = Player(playerId)
+ if not player then
+ return
+ end
+
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 player = Player(playerId)
+ if not player then
+ return
+ end
+
local page = msg:getU32()
sendStoreTransactionHistory(playerId, page + 1, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE)
+ player:updateUIExhausted()
end
local function getCategoriesRook()
@@ -1812,6 +1835,7 @@ function GameStore.processHirelingPurchase(player, offer, productType, hirelingN
player:makeCoinTransaction(offer, hirelingName)
local message = "You have successfully bought " .. hirelingName
+ player:createTransactionSummary(offer.type, 1)
return addPlayerEvent(sendStorePurchaseSuccessful, 650, player:getId(), message)
-- If not, we ask him to do!
else
diff --git a/data/npclib/npc_system/npc_handler.lua b/data/npclib/npc_system/npc_handler.lua
index 8f4d5f2ca..31aaa88fa 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/creaturescripts/player/offline_training.lua b/data/scripts/creaturescripts/player/offline_training.lua
index 4466c6ee0..7d08f07ef 100644
--- a/data/scripts/creaturescripts/player/offline_training.lua
+++ b/data/scripts/creaturescripts/player/offline_training.lua
@@ -55,18 +55,23 @@ function offlineTraining.onLogin(player)
local vocation = player:getVocation()
local promotion = vocation:getPromotion()
local topVocation = not promotion and vocation or promotion
- local updateSkills = false
+ local tries = nil
if table.contains({ SKILL_CLUB, SKILL_SWORD, SKILL_AXE, SKILL_DISTANCE }, offlineTrainingSkill) then
- local modifier = topVocation:getBaseAttackSpeed() / 1000 / configManager.getFloat(configKeys.RATE_OFFLINE_TRAINING_SPEED)
- updateSkills = player:addOfflineTrainingTries(offlineTrainingSkill, (trainingTime / modifier) / (offlineTrainingSkill == SKILL_DISTANCE and 4 or 2))
+ local modifier = topVocation:getBaseAttackSpeed() / 1000
+ tries = (trainingTime / modifier) / (offlineTrainingSkill == SKILL_DISTANCE and 4 or 2)
elseif offlineTrainingSkill == SKILL_MAGLEVEL then
- local gainTicks = topVocation:getManaGainTicks() * 2
+ local gainTicks = topVocation:getManaGainTicks() / 1000
if gainTicks == 0 then
gainTicks = 1
end
- updateSkills = player:addOfflineTrainingTries(SKILL_MAGLEVEL, trainingTime * (vocation:getManaGainAmount() / gainTicks))
+ tries = trainingTime * (vocation:getManaGainAmount() / gainTicks)
+ end
+
+ local updateSkills = false
+ if tries then
+ updateSkills = player:addOfflineTrainingTries(offlineTrainingSkill, tries * configManager.getFloat(configKeys.RATE_OFFLINE_TRAINING_SPEED))
end
if updateSkills then
diff --git a/data/scripts/eventcallbacks/README.md b/data/scripts/eventcallbacks/README.md
index 601653574..ae5de046b 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 a6295074d..f68cc95cc 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/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua
index 90d308905..022aebbcc 100644
--- a/data/scripts/eventcallbacks/player/on_look.lua
+++ b/data/scripts/eventcallbacks/player/on_look.lua
@@ -60,11 +60,12 @@ function callback.playerOnLook(player, thing, position, distance)
description = string.format("%s\nDecays to: %d", description, decayId)
end
elseif thing:isCreature() then
- local str = "%s\nHealth: %d / %d"
+ local str, pId = "%s\n%s\nHealth: %d / %d"
if thing:isPlayer() and thing:getMaxMana() > 0 then
+ pId = string.format("Player ID: %i", thing:getGuid())
str = string.format("%s, Mana: %d / %d", str, thing:getMana(), thing:getMaxMana())
end
- description = string.format(str, description, thing:getHealth(), thing:getMaxHealth()) .. "."
+ description = string.format(str, description, pId, thing:getHealth(), thing:getMaxHealth())
end
description = string.format("%s\nPosition: (%d, %d, %d)", description, position.x, position.y, position.z)
@@ -76,7 +77,7 @@ function callback.playerOnLook(player, thing, position, distance)
description = string.format("%s\nSpeed: %d", description, speed)
if thing:isPlayer() then
- description = string.format("%s\nIP: %s.", description, Game.convertIpToString(thing:getIp()))
+ description = string.format("%s\nIP: %s", description, Game.convertIpToString(thing:getIp()))
end
end
end
diff --git a/data/scripts/talkactions/god/create_npc.lua b/data/scripts/talkactions/god/create_npc.lua
index 4aeec3dde..b6d0412d3 100644
--- a/data/scripts/talkactions/god/create_npc.lua
+++ b/data/scripts/talkactions/god/create_npc.lua
@@ -1,3 +1,6 @@
+-- To summon a temporary npc use /n npcname
+-- To summon a permanent npc use /n npcname,true
+
local createNpc = TalkAction("/n")
function createNpc.onSay(player, words, param)
@@ -9,11 +12,44 @@ function createNpc.onSay(player, words, param)
return true
end
+ local split = param:split(",")
+ local name = split[1]
+ local permanentStr = split[2]
+
local position = player:getPosition()
- local npc = Game.createNpc(param, position)
+ local npc = Game.createNpc(name, position)
if npc then
npc:setMasterPos(position)
position:sendMagicEffect(CONST_ME_MAGIC_RED)
+
+ if permanentStr and permanentStr == "true" then
+ local mapName = configManager.getString(configKeys.MAP_NAME)
+ local mapNpcsPath = mapName .. "-npc.xml"
+ local filePath = string.format("%s/world/%s", DATA_DIRECTORY, mapNpcsPath)
+ local npcsFile = io.open(filePath, "r")
+ if not npcsFile then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to add permanent NPC. NPC File not found.")
+ return true
+ end
+ local fileContent = npcsFile:read("*all")
+ npcsFile:close()
+ local endTag = ""
+ if not fileContent:find(endTag, 1, true) then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to add permanent NPC. The NPC file format is incorrect. Missing end tag " .. endTag .. ".")
+ return true
+ end
+ local textToAdd = string.format('\t\n\t\t\n\t', position.x, position.y, position.z, name, position.z)
+ local newFileContent = fileContent:gsub(endTag, textToAdd .. "\n" .. endTag)
+ npcsFile = io.open(filePath, "w")
+ if not npcsFile then
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "There was an error when trying to write to the NPC file.")
+ return true
+ end
+ npcsFile:write(newFileContent)
+ npcsFile:close()
+
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Permanent NPC added successfully.")
+ end
else
player:sendCancelMessage("There is not enough room.")
position:sendMagicEffect(CONST_ME_POFF)
diff --git a/data/scripts/talkactions/player/reward.lua b/data/scripts/talkactions/player/reward.lua
index d198f60ef..687a1df91 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 2fbc7dd64..af2450670 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/canary_server.cpp b/src/canary_server.cpp
index 17cd75c6a..28a2053b4 100644
--- a/src/canary_server.cpp
+++ b/src/canary_server.cpp
@@ -93,8 +93,8 @@ int CanaryServer::run() {
#ifndef _WIN32
if (getuid() == 0 || geteuid() == 0) {
logger.warn("{} has been executed as root user, "
- "please consider running it as a normal user",
- ProtocolStatus::SERVER_NAME);
+ "please consider running it as a normal user",
+ ProtocolStatus::SERVER_NAME);
}
#endif
@@ -235,7 +235,7 @@ void CanaryServer::toggleForceCloseButton() {
void CanaryServer::badAllocationHandler() {
// Use functions that only use stack allocation
g_logger().error("Allocation failed, server out of memory, "
- "decrease the size of your map or compile in 64 bits mode");
+ "decrease the size of your map or compile in 64 bits mode");
if (isatty(STDIN_FILENO)) {
getchar();
@@ -319,7 +319,7 @@ void CanaryServer::initializeDatabase() {
DatabaseManager::updateDatabase();
if (g_configManager().getBoolean(OPTIMIZE_DATABASE, __FUNCTION__)
- && !DatabaseManager::optimizeTables()) {
+ && !DatabaseManager::optimizeTables()) {
logger.debug("No tables were optimized");
}
}
diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt
index af6ba129e..c87a45a0a 100644
--- a/src/creatures/CMakeLists.txt
+++ b/src/creatures/CMakeLists.txt
@@ -23,6 +23,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE
players/player.cpp
players/achievement/player_achievement.cpp
players/cyclopedia/player_badge.cpp
+ players/cyclopedia/player_cyclopedia.cpp
players/cyclopedia/player_title.cpp
players/wheel/player_wheel.cpp
players/wheel/wheel_gems.cpp
diff --git a/src/creatures/appearance/outfit/outfit.cpp b/src/creatures/appearance/outfit/outfit.cpp
index 971206593..251bf7bde 100644
--- a/src/creatures/appearance/outfit/outfit.cpp
+++ b/src/creatures/appearance/outfit/outfit.cpp
@@ -58,8 +58,8 @@ bool Outfits::loadFromXml() {
}
if (auto lookType = pugi::cast(lookTypeAttribute.value());
- g_configManager().getBoolean(WARN_UNSAFE_SCRIPTS, __FUNCTION__) && lookType != 0
- && !g_game().isLookTypeRegistered(lookType)) {
+ 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);
continue;
}
diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp
index 2aaadac4d..87f7500d0 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) {
@@ -648,8 +657,8 @@ CombatDamage Combat::applyImbuementElementalDamage(std::shared_ptr attac
}
if (imbuementInfo.imbuement->combatType == COMBAT_NONE
- || damage.primary.type == COMBAT_HEALING
- || damage.secondary.type == COMBAT_HEALING) {
+ || damage.primary.type == COMBAT_HEALING
+ || damage.secondary.type == COMBAT_HEALING) {
continue;
}
@@ -1247,8 +1256,8 @@ void Combat::doCombatHealth(std::shared_ptr caster, std::shared_ptr caster, std::shared_ptr target, const Position &origin, CombatDamage &damage, const CombatParams ¶ms) {
bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target, params.aggressive) == RETURNVALUE_NOERROR);
if ((caster && target)
- && (caster == target || canCombat)
- && (params.impactEffect != CONST_ME_NONE)) {
+ && (caster == target || canCombat)
+ && (params.impactEffect != CONST_ME_NONE)) {
g_game().addMagicEffect(target->getPosition(), params.impactEffect);
}
@@ -1291,8 +1300,8 @@ void Combat::doCombatMana(std::shared_ptr caster, std::shared_ptr caster, std::shared_ptr target, const Position &origin, CombatDamage &damage, const CombatParams ¶ms) {
bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target, params.aggressive) == RETURNVALUE_NOERROR);
if ((caster && target)
- && (caster == target || canCombat)
- && (params.impactEffect != CONST_ME_NONE)) {
+ && (caster == target || canCombat)
+ && (params.impactEffect != CONST_ME_NONE)) {
g_game().addMagicEffect(target->getPosition(), params.impactEffect);
}
@@ -1359,8 +1368,8 @@ void Combat::doCombatDispel(std::shared_ptr caster, const Position &po
void Combat::doCombatDispel(std::shared_ptr caster, std::shared_ptr target, const CombatParams ¶ms) {
bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target, params.aggressive) == RETURNVALUE_NOERROR);
if ((caster && target)
- && (caster == target || canCombat)
- && (params.impactEffect != CONST_ME_NONE)) {
+ && (caster == target || canCombat)
+ && (params.impactEffect != CONST_ME_NONE)) {
g_game().addMagicEffect(target->getPosition(), params.impactEffect);
}
@@ -1399,7 +1408,7 @@ void Combat::doCombatDefault(std::shared_ptr caster, std::shared_ptrgetPosition(), params.impactEffect);
+ g_game().addMagicEffect(target->getPosition(), params.impactEffect);
}
*/
@@ -1531,8 +1540,8 @@ void ValueCallback::getMinMaxValues(std::shared_ptr player, CombatDamage
// onGetPlayerMinMaxValues(...)
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[ValueCallback::getMinMaxValues - Player {} formula {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- player->getName(), fmt::underlying(type));
+ "Call stack overflow. Too many lua script calls being nested.",
+ player->getName(), fmt::underlying(type));
return;
}
@@ -1624,8 +1633,8 @@ void TileCallback::onTileCombat(std::shared_ptr creature, std::shared_
// onTileCombat(creature, pos)
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[TileCallback::onTileCombat - Creature {} type {} on tile x: {} y: {} z: {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName(), fmt::underlying(type), (tile->getPosition()).getX(), (tile->getPosition()).getY(), (tile->getPosition()).getZ());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName(), fmt::underlying(type), (tile->getPosition()).getX(), (tile->getPosition()).getY(), (tile->getPosition()).getZ());
return;
}
@@ -1655,8 +1664,8 @@ void TargetCallback::onTargetCombat(std::shared_ptr creature, std::sha
// onTargetCombat(creature, target)
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[TargetCallback::onTargetCombat - Creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName());
return;
}
@@ -1715,8 +1724,8 @@ void ChainCallback::onChainCombat(std::shared_ptr creature, uint8_t &m
// onChainCombat(creature)
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[ChainCallback::onTargetCombat - Creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName());
return;
}
@@ -1757,8 +1766,8 @@ bool ChainPickerCallback::onChainCombat(std::shared_ptr creature, std:
// onChainCombat(creature, target)
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[ChainPickerCallback::onTargetCombat - Creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName());
return true;
}
@@ -2169,7 +2178,7 @@ void Combat::applyExtensions(std::shared_ptr caster, std::shared_ptrgetInventoryItem(CONST_SLOT_LEFT);
- playerWeapon != nullptr && playerWeapon->getTier() > 0) {
+ playerWeapon != nullptr && playerWeapon->getTier() > 0) {
double_t fatalChance = playerWeapon->getFatalChance();
double_t randomChance = uniform_random(0, 10000) / 100;
if (fatalChance > 0 && randomChance < fatalChance) {
diff --git a/src/creatures/combat/condition.cpp b/src/creatures/combat/condition.cpp
index 7f477f1d8..b9603d010 100644
--- a/src/creatures/combat/condition.cpp
+++ b/src/creatures/combat/condition.cpp
@@ -2042,7 +2042,7 @@ bool ConditionFeared::executeCondition(std::shared_ptr creature, int32
g_dispatcher().addEvent([id = creature->getID(), listDir = listDir.data()] {
g_game().forcePlayerAutoWalk(id, listDir);
},
- "ConditionFeared::executeCondition");
+ "ConditionFeared::executeCondition");
g_logger().debug("[ConditionFeared::executeCondition] Walking Scheduled");
}
diff --git a/src/creatures/combat/spells.cpp b/src/creatures/combat/spells.cpp
index dd4305de1..f8852c5e5 100644
--- a/src/creatures/combat/spells.cpp
+++ b/src/creatures/combat/spells.cpp
@@ -108,7 +108,7 @@ void Spells::clear() {
bool Spells::hasInstantSpell(const std::string &word) const {
if (auto iterate = instants.find(word);
- iterate != instants.end()) {
+ iterate != instants.end()) {
return true;
}
return false;
@@ -127,8 +127,8 @@ bool Spells::registerInstantLuaEvent(const std::shared_ptr instant
// Checks if there is any spell registered with the same name
if (hasInstantSpell(words)) {
g_logger().warn("[Spells::registerInstantLuaEvent] - "
- "Duplicate registered instant spell with words: {}, on spell with name: {}",
- words, instantName);
+ "Duplicate registered instant spell with words: {}, on spell with name: {}",
+ words, instantName);
return false;
}
// Register spell word in the map
@@ -166,7 +166,7 @@ std::list Spells::getSpellsByVocation(uint16_t vocationId) {
vocSpellsIt = vocSpells.find(vocationId);
if (vocSpellsIt != vocSpells.end()
- && vocSpellsIt->second) {
+ && vocSpellsIt->second) {
spellsList.push_back(it.second->getSpellId());
}
}
@@ -361,8 +361,8 @@ bool CombatSpell::executeCastSpell(std::shared_ptr creature, const Lua
// onCastSpell(creature, var)
if (!getScriptInterface()->reserveScriptEnv()) {
g_logger().error("[CombatSpell::executeCastSpell - Creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName());
return false;
}
@@ -950,8 +950,8 @@ bool InstantSpell::executeCastSpell(std::shared_ptr creature, const Lu
// onCastSpell(creature, var)
if (!getScriptInterface()->reserveScriptEnv()) {
g_logger().error("[InstantSpell::executeCastSpell - Creature {} words {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName(), getWords());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName(), getWords());
return false;
}
@@ -1095,8 +1095,8 @@ bool RuneSpell::executeCastSpell(std::shared_ptr creature, const LuaVa
// onCastSpell(creature, var, isHotkey)
if (!getScriptInterface()->reserveScriptEnv()) {
g_logger().error("[RuneSpell::executeCastSpell - Creature {} runeId {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- creature->getName(), getRuneItemId());
+ "Call stack overflow. Too many lua script calls being nested.",
+ creature->getName(), getRuneItemId());
return false;
}
diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp
index 7796863f0..c68ab4f35 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"
);
});
}
@@ -828,7 +828,7 @@ bool Creature::dropCorpse(std::shared_ptr lastHitCreature, std::shared
g_dispatcher().addEvent([player, corpseContainer, corpsePosition = corpse->getPosition()] {
g_game().playerQuickLootCorpse(player, corpseContainer, corpsePosition);
},
- "Game::playerQuickLootCorpse");
+ "Game::playerQuickLootCorpse");
}
}
}
diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp
index 33f62dbd2..74ec6c5f2 100644
--- a/src/creatures/creatures_definitions.hpp
+++ b/src/creatures/creatures_definitions.hpp
@@ -1524,12 +1524,12 @@ using StashItemList = std::map;
using ItemsTierCountList = std::map>;
/*
- > ItemsTierCountList structure:
- |- [itemID]
- |- [itemTier]
- |- Count
- | ...
- | ...
+ > ItemsTierCountList structure:
+ |- [itemID]
+ |- [itemTier]
+ |- Count
+ | ...
+ | ...
*/
struct ProtocolFamiliars {
diff --git a/src/creatures/interactions/chat.cpp b/src/creatures/interactions/chat.cpp
index e16af670f..900247e45 100644
--- a/src/creatures/interactions/chat.cpp
+++ b/src/creatures/interactions/chat.cpp
@@ -145,8 +145,8 @@ bool ChatChannel::executeCanJoinEvent(const std::shared_ptr &player) {
LuaScriptInterface* scriptInterface = g_chat().getScriptInterface();
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[CanJoinChannelEvent::execute - Player {}, on channel {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- player->getName(), getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ player->getName(), getName());
return false;
}
@@ -171,8 +171,8 @@ bool ChatChannel::executeOnJoinEvent(const std::shared_ptr &player) {
LuaScriptInterface* scriptInterface = g_chat().getScriptInterface();
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[OnJoinChannelEvent::execute - Player {}, on channel {}] "
- "Call stack overflow. Too many lua script calls being nested",
- player->getName(), getName());
+ "Call stack overflow. Too many lua script calls being nested",
+ player->getName(), getName());
return false;
}
@@ -197,8 +197,8 @@ bool ChatChannel::executeOnLeaveEvent(const std::shared_ptr &player) {
LuaScriptInterface* scriptInterface = g_chat().getScriptInterface();
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[OnLeaveChannelEvent::execute - Player {}, on channel {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- player->getName(), getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ player->getName(), getName());
return false;
}
@@ -223,8 +223,8 @@ bool ChatChannel::executeOnSpeakEvent(const std::shared_ptr &player, Spe
LuaScriptInterface* scriptInterface = g_chat().getScriptInterface();
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[OnSpeakChannelEvent::execute - Player {}, type {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- player->getName(), fmt::underlying(type));
+ "Call stack overflow. Too many lua script calls being nested.",
+ player->getName(), fmt::underlying(type));
return false;
}
diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp
index 5e0a7d16b..4b993cbab 100644
--- a/src/creatures/monsters/monster.cpp
+++ b/src/creatures/monsters/monster.cpp
@@ -50,8 +50,8 @@ Monster::Monster(const std::shared_ptr mType) :
for (const std::string &scriptName : mType->info.scripts) {
if (!registerCreatureEvent(scriptName)) {
g_logger().warn("[Monster::Monster] - "
- "Unknown event name: {}",
- scriptName);
+ "Unknown event name: {}",
+ scriptName);
}
}
}
@@ -138,8 +138,8 @@ void Monster::onCreatureAppear(std::shared_ptr creature, bool isLogin)
LuaScriptInterface* scriptInterface = mType->info.scriptInterface;
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[Monster::onCreatureAppear - Monster {} creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- getName(), creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ getName(), creature->getName());
return;
}
@@ -176,8 +176,8 @@ void Monster::onRemoveCreature(std::shared_ptr creature, bool isLogout
LuaScriptInterface* scriptInterface = mType->info.scriptInterface;
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[Monster::onCreatureDisappear - Monster {} creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- getName(), creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ getName(), creature->getName());
return;
}
@@ -217,8 +217,8 @@ void Monster::onCreatureMove(const std::shared_ptr &creature, const st
LuaScriptInterface* scriptInterface = mType->info.scriptInterface;
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("[Monster::onCreatureMove - Monster {} creature {}] "
- "Call stack overflow. Too many lua script calls being nested.",
- getName(), creature->getName());
+ "Call stack overflow. Too many lua script calls being nested.",
+ getName(), creature->getName());
return;
}
@@ -291,8 +291,8 @@ void Monster::onCreatureSay(std::shared_ptr creature, SpeakClasses typ
LuaScriptInterface* scriptInterface = mType->info.scriptInterface;
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("Monster {} creature {}] Call stack overflow. Too many lua "
- "script calls being nested.",
- getName(), creature->getName());
+ "script calls being nested.",
+ getName(), creature->getName());
return;
}
@@ -771,8 +771,8 @@ void Monster::onThink(uint32_t interval) {
LuaScriptInterface* scriptInterface = mType->info.scriptInterface;
if (!scriptInterface->reserveScriptEnv()) {
g_logger().error("Monster {} Call stack overflow. Too many lua script calls "
- "being nested.",
- getName());
+ "being nested.",
+ getName());
return;
}
@@ -2041,8 +2041,8 @@ void Monster::dropLoot(std::shared_ptr corpse, 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/monsters/monsters.cpp b/src/creatures/monsters/monsters.cpp
index 3d457aa4d..78358f69d 100644
--- a/src/creatures/monsters/monsters.cpp
+++ b/src/creatures/monsters/monsters.cpp
@@ -97,7 +97,7 @@ bool Monsters::deserializeSpell(const std::shared_ptr spell, spell
}
if (std::string spellName = asLowerCaseString(spell->name);
- spellName == "melee") {
+ spellName == "melee") {
sb.isMelee = true;
if (spell->attack > 0 && spell->skill > 0) {
@@ -164,8 +164,8 @@ bool Monsters::deserializeSpell(const std::shared_ptr spell, spell
condition->setOutfit(outfit);
} else {
g_logger().error("[Monsters::deserializeSpell] - "
- "Missing outfit monster or item in outfit spell for: {}",
- description);
+ "Missing outfit monster or item in outfit spell for: {}",
+ description);
return false;
}
@@ -208,8 +208,8 @@ bool Monsters::deserializeSpell(const std::shared_ptr spell, spell
} else if (spellName == "condition") {
if (spell->conditionType == CONDITION_NONE) {
g_logger().error("[Monsters::deserializeSpell] - "
- "{} condition is not set for: {}",
- description, spell->name);
+ "{} condition is not set for: {}",
+ description, spell->name);
}
} else if (spellName == "strength") {
//
@@ -217,8 +217,8 @@ bool Monsters::deserializeSpell(const std::shared_ptr spell, spell
//
} else {
g_logger().error("[Monsters::deserializeSpell] - "
- "{} unknown or missing parameter on spell with name: {}",
- description, spell->name);
+ "{} unknown or missing parameter on spell with name: {}",
+ description, spell->name);
}
if (spell->shoot != CONST_ANI_NONE) {
@@ -295,9 +295,9 @@ bool MonsterType::loadCallback(LuaScriptInterface* scriptInterface) {
std::shared_ptr Monsters::getMonsterType(const std::string &name, bool silent /* = false*/) const {
std::string lowerCaseName = asLowerCaseString(name);
if (auto it = monsters.find(lowerCaseName);
- it != monsters.end()
- // We will only return the MonsterType if it match the exact name of the monster
- && it->first.find(lowerCaseName) != it->first.npos) {
+ it != monsters.end()
+ // We will only return the MonsterType if it match the exact name of the monster
+ && it->first.find(lowerCaseName) != it->first.npos) {
return it->second;
}
if (!silent) {
diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp
index e445a58fc..fdbf58853 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;
@@ -522,7 +521,7 @@ void Npc::onThinkWalk(uint32_t interval) {
}
if (Direction newDirection;
- getRandomStep(newDirection)) {
+ getRandomStep(newDirection)) {
listWalkDir.push_front(newDirection);
addEventWalk();
}
@@ -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();
}
}
@@ -634,7 +633,7 @@ bool Npc::getRandomStep(Direction &moveDirection) {
std::ranges::shuffle(directionvector, getRandomGenerator());
for (const Position &creaturePos = getPosition();
- Direction direction : directionvector) {
+ Direction direction : directionvector) {
if (canWalkTo(creaturePos, direction)) {
moveDirection = direction;
return true;
@@ -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 7e8be84c7..c246aa4b6 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/achievement/player_achievement.cpp b/src/creatures/players/achievement/player_achievement.cpp
index 2db53dbe7..cd0735ab5 100644
--- a/src/creatures/players/achievement/player_achievement.cpp
+++ b/src/creatures/players/achievement/player_achievement.cpp
@@ -35,7 +35,7 @@ bool PlayerAchievement::add(uint16_t id, bool message /* = true*/, uint32_t time
addPoints(achievement.points);
int toSaveTimeStamp = timestamp != 0 ? timestamp : (OTSYS_TIME() / 1000);
getUnlockedKV()->set(achievement.name, toSaveTimeStamp);
- m_achievementsUnlocked.push_back({ achievement.id, toSaveTimeStamp });
+ m_achievementsUnlocked.emplace_back(achievement.id, toSaveTimeStamp);
m_achievementsUnlocked.shrink_to_fit();
return true;
}
@@ -53,7 +53,7 @@ bool PlayerAchievement::remove(uint16_t id) {
if (auto it = std::find_if(m_achievementsUnlocked.begin(), m_achievementsUnlocked.end(), [id](auto achievement_it) {
return achievement_it.first == id;
});
- it != m_achievementsUnlocked.end()) {
+ it != m_achievementsUnlocked.end()) {
getUnlockedKV()->remove(achievement.name);
m_achievementsUnlocked.erase(it);
removePoints(achievement.points);
@@ -72,7 +72,7 @@ bool PlayerAchievement::isUnlocked(uint16_t id) const {
if (auto it = std::find_if(m_achievementsUnlocked.begin(), m_achievementsUnlocked.end(), [id](auto achievement_it) {
return achievement_it.first == id;
});
- it != m_achievementsUnlocked.end()) {
+ it != m_achievementsUnlocked.end()) {
return true;
}
@@ -80,7 +80,8 @@ bool PlayerAchievement::isUnlocked(uint16_t id) const {
}
uint16_t PlayerAchievement::getPoints() const {
- return m_player.kv()->scoped("achievements")->get("points")->getNumber();
+ auto kvScoped = m_player.kv()->scoped("achievements")->get("points");
+ return kvScoped ? static_cast(kvScoped->getNumber()) : 0;
}
void PlayerAchievement::addPoints(uint16_t toAddPoints) {
@@ -109,12 +110,12 @@ void PlayerAchievement::loadUnlockedAchievements() {
g_logger().debug("[{}] - Achievement {} found for player {}.", __FUNCTION__, achievementName, m_player.getName());
- m_achievementsUnlocked.push_back({ achievement.id, getUnlockedKV()->get(achievementName)->getNumber() });
+ m_achievementsUnlocked.emplace_back(achievement.id, getUnlockedKV()->get(achievementName)->getNumber());
}
}
void PlayerAchievement::sendUnlockedSecretAchievements() {
- std::vector> m_achievementsUnlocked;
+ std::vector> achievementsUnlocked;
uint16_t unlockedSecret = 0;
for (const auto &[achievId, achievCreatedTime] : getUnlockedAchievements()) {
Achievement achievement = g_game().getAchievementById(achievId);
@@ -126,10 +127,10 @@ void PlayerAchievement::sendUnlockedSecretAchievements() {
unlockedSecret++;
}
- m_achievementsUnlocked.push_back({ achievement, achievCreatedTime });
+ achievementsUnlocked.emplace_back(achievement, achievCreatedTime);
}
- m_player.sendCyclopediaCharacterAchievements(unlockedSecret, m_achievementsUnlocked);
+ m_player.sendCyclopediaCharacterAchievements(unlockedSecret, achievementsUnlocked);
}
const std::shared_ptr &PlayerAchievement::getUnlockedKV() {
diff --git a/src/creatures/players/achievement/player_achievement.hpp b/src/creatures/players/achievement/player_achievement.hpp
index d1073a9bf..e0c027e58 100644
--- a/src/creatures/players/achievement/player_achievement.hpp
+++ b/src/creatures/players/achievement/player_achievement.hpp
@@ -31,11 +31,11 @@ class PlayerAchievement {
explicit PlayerAchievement(Player &player);
bool add(uint16_t id, bool message = true, uint32_t timestamp = 0);
bool remove(uint16_t id);
- bool isUnlocked(uint16_t id) const;
- uint16_t getPoints() const;
+ [[nodiscard]] bool isUnlocked(uint16_t id) const;
+ [[nodiscard]] uint16_t getPoints() const;
void addPoints(uint16_t toAddPoints);
void removePoints(uint16_t toRemovePoints);
- std::vector> getUnlockedAchievements() const;
+ [[nodiscard]] std::vector> getUnlockedAchievements() const;
void loadUnlockedAchievements();
void sendUnlockedSecretAchievements();
const std::shared_ptr &getUnlockedKV();
diff --git a/src/creatures/players/cyclopedia/player_badge.cpp b/src/creatures/players/cyclopedia/player_badge.cpp
index 9b892a616..639640b2f 100644
--- a/src/creatures/players/cyclopedia/player_badge.cpp
+++ b/src/creatures/players/cyclopedia/player_badge.cpp
@@ -26,7 +26,7 @@ bool PlayerBadge::hasBadge(uint8_t id) const {
if (auto it = std::find_if(m_badgesUnlocked.begin(), m_badgesUnlocked.end(), [id](auto badge_it) {
return badge_it.first.m_id == id;
});
- it != m_badgesUnlocked.end()) {
+ it != m_badgesUnlocked.end()) {
return true;
}
diff --git a/src/creatures/players/cyclopedia/player_cyclopedia.cpp b/src/creatures/players/cyclopedia/player_cyclopedia.cpp
new file mode 100644
index 000000000..abbc920d3
--- /dev/null
+++ b/src/creatures/players/cyclopedia/player_cyclopedia.cpp
@@ -0,0 +1,185 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#include "pch.hpp"
+
+#include "database/databasetasks.hpp"
+#include "creatures/players/player.hpp"
+#include "player_cyclopedia.hpp"
+#include "game/game.hpp"
+#include "kv/kv.hpp"
+
+PlayerCyclopedia::PlayerCyclopedia(Player &player) :
+ m_player(player) { }
+
+Summary PlayerCyclopedia::getSummary() {
+ return { getAmount(Summary_t::PREY_CARDS),
+ getAmount(Summary_t::INSTANT_REWARDS),
+ getAmount(Summary_t::HIRELINGS) };
+}
+
+void PlayerCyclopedia::loadSummaryData() {
+ DBResult_ptr result = g_database().storeQuery(fmt::format("SELECT COUNT(*) as `count` FROM `player_hirelings` WHERE `player_id` = {}", m_player.getGUID()));
+ auto kvScoped = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(static_cast(Summary_t::HIRELINGS)));
+ if (result && !kvScoped->get("amount").has_value()) {
+ kvScoped->set("amount", result->getNumber("count"));
+ }
+}
+
+void PlayerCyclopedia::loadDeathHistory(uint16_t page, uint16_t entriesPerPage) {
+ Benchmark bm_check;
+ uint32_t offset = static_cast(page - 1) * entriesPerPage;
+ auto query = fmt::format("SELECT `time`, `level`, `killed_by`, `mostdamage_by`, (select count(*) FROM `player_deaths` WHERE `player_id` = {}) as `entries` FROM `player_deaths` WHERE `player_id` = {} AND `time` >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY)) ORDER BY `time` DESC LIMIT {}, {}", m_player.getGUID(), m_player.getGUID(), offset, entriesPerPage);
+
+ uint32_t playerID = m_player.getID();
+ std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) {
+ std::shared_ptr player = g_game().getPlayerByID(playerID);
+ if (!player) {
+ return;
+ }
+
+ player->resetAsyncOngoingTask(PlayerAsyncTask_RecentDeaths);
+ if (!result) {
+ player->sendCyclopediaCharacterRecentDeaths(0, 0, {});
+ return;
+ }
+
+ auto pages = result->getNumber("entries");
+ pages += entriesPerPage - 1;
+ pages /= entriesPerPage;
+
+ std::vector entries;
+ entries.reserve(result->countResults());
+ do {
+ std::string killed_by = result->getString("killed_by");
+ std::string mostdamage_by = result->getString("mostdamage_by");
+
+ std::string cause = fmt::format("Died at Level {}", result->getNumber("level"));
+
+ if (!killed_by.empty()) {
+ cause.append(fmt::format(" by{}", formatWithArticle(killed_by)));
+ }
+
+ if (!mostdamage_by.empty()) {
+ cause.append(fmt::format("{}{}", !killed_by.empty() ? " and" : "", formatWithArticle(mostdamage_by)));
+ }
+
+ entries.emplace_back(cause, result->getNumber("time"));
+ } while (result->next());
+ player->sendCyclopediaCharacterRecentDeaths(page, static_cast(pages), entries);
+ };
+ g_databaseTasks().store(query, callback);
+ m_player.addAsyncOngoingTask(PlayerAsyncTask_RecentDeaths);
+
+ g_logger().debug("Loading death history from the player {} took {} milliseconds.", m_player.getName(), bm_check.duration());
+}
+
+void PlayerCyclopedia::loadRecentKills(uint16_t page, uint16_t entriesPerPage) {
+ Benchmark bm_check;
+
+ const std::string &escapedName = g_database().escapeString(m_player.getName());
+ uint32_t offset = static_cast(page - 1) * entriesPerPage;
+ auto query = fmt::format("SELECT `d`.`time`, `d`.`killed_by`, `d`.`mostdamage_by`, `d`.`unjustified`, `d`.`mostdamage_unjustified`, `p`.`name`, (select count(*) FROM `player_deaths` WHERE ((`killed_by` = {} AND `is_player` = 1) OR (`mostdamage_by` = {} AND `mostdamage_is_player` = 1))) as `entries` FROM `player_deaths` AS `d` INNER JOIN `players` AS `p` ON `d`.`player_id` = `p`.`id` WHERE ((`d`.`killed_by` = {} AND `d`.`is_player` = 1) OR (`d`.`mostdamage_by` = {} AND `d`.`mostdamage_is_player` = 1)) AND `time` >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 70 DAY)) ORDER BY `time` DESC LIMIT {}, {}", escapedName, escapedName, escapedName, escapedName, offset, entriesPerPage);
+
+ uint32_t playerID = m_player.getID();
+ std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) {
+ std::shared_ptr player = g_game().getPlayerByID(playerID);
+ if (!player) {
+ return;
+ }
+
+ player->resetAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills);
+ if (!result) {
+ player->sendCyclopediaCharacterRecentPvPKills(0, 0, {});
+ return;
+ }
+
+ auto pages = result->getNumber("entries");
+ pages += entriesPerPage - 1;
+ pages /= entriesPerPage;
+
+ std::vector entries;
+ entries.reserve(result->countResults());
+ do {
+ std::string cause1 = result->getString("killed_by");
+ std::string cause2 = result->getString("mostdamage_by");
+ std::string name = result->getString("name");
+
+ uint8_t status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_JUSTIFIED;
+ if (player->getName() == cause1) {
+ if (result->getNumber("unjustified") == 1) {
+ status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED;
+ }
+ } else if (player->getName() == cause2) {
+ if (result->getNumber("mostdamage_unjustified") == 1) {
+ status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED;
+ }
+ }
+
+ entries.emplace_back(fmt::format("Killed {}.", name), result->getNumber("time"), status);
+ } while (result->next());
+ player->sendCyclopediaCharacterRecentPvPKills(page, static_cast(pages), entries);
+ };
+ g_databaseTasks().store(query, callback);
+ m_player.addAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills);
+
+ g_logger().debug("Loading recent kills from the player {} took {} milliseconds.", m_player.getName(), bm_check.duration());
+}
+
+void PlayerCyclopedia::updateStoreSummary(uint8_t type, uint16_t amount, const std::string &id) {
+ switch (type) {
+ case Summary_t::HOUSE_ITEMS:
+ case Summary_t::BLESSINGS:
+ insertValue(type, amount, id);
+ break;
+ case Summary_t::ALL_BLESSINGS:
+ for (int i = 1; i < 8; ++i) {
+ insertValue(static_cast(Summary_t::BLESSINGS), amount, fmt::format("{}", i));
+ }
+ break;
+ default:
+ updateAmount(type, amount);
+ break;
+ }
+}
+
+uint16_t PlayerCyclopedia::getAmount(uint8_t type) {
+ auto kvScope = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->get("amount");
+ return static_cast(kvScope ? kvScope->getNumber() : 0);
+}
+
+void PlayerCyclopedia::updateAmount(uint8_t type, uint16_t amount) {
+ auto oldAmount = getAmount(type);
+ m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->set("amount", oldAmount + amount);
+}
+
+std::map PlayerCyclopedia::getResult(uint8_t type) const {
+ auto kvScope = m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type));
+ std::map result; // ID, amount
+ for (const auto &scope : kvScope->keys()) {
+ size_t pos = scope.find('.');
+ if (pos == std::string::npos) {
+ g_logger().error("[{}] Invalid key format: {}", __FUNCTION__, scope);
+ continue;
+ }
+ std::string id = scope.substr(0, pos);
+ auto amount = kvScope->scoped(id)->get("amount");
+ result.emplace(std::stoll(id), static_cast(amount ? amount->getNumber() : 0));
+ }
+ return result;
+}
+
+void PlayerCyclopedia::insertValue(uint8_t type, uint16_t amount, const std::string &id) {
+ auto result = getResult(type);
+ auto it = result.find(std::stoll(id));
+ auto oldAmount = (it != result.end() ? it->second : 0);
+ auto newAmount = oldAmount + amount;
+ m_player.kv()->scoped("summary")->scoped(g_game().getSummaryKeyByType(type))->scoped(id)->set("amount", newAmount);
+ g_logger().debug("[{}] type: {}, id: {}, old amount: {}, added amount: {}, new amount: {}", __FUNCTION__, type, id, oldAmount, amount, newAmount);
+}
diff --git a/src/creatures/players/cyclopedia/player_cyclopedia.hpp b/src/creatures/players/cyclopedia/player_cyclopedia.hpp
new file mode 100644
index 000000000..32c446cc3
--- /dev/null
+++ b/src/creatures/players/cyclopedia/player_cyclopedia.hpp
@@ -0,0 +1,46 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#pragma once
+
+#include "creatures/creatures_definitions.hpp"
+#include "enums/player_cyclopedia.hpp"
+
+class Player;
+class KV;
+
+struct Summary {
+ uint16_t m_preyWildcards = 0;
+ uint16_t m_instantRewards = 0;
+ uint16_t m_hirelings = 0;
+
+ [[maybe_unused]] Summary(uint16_t mPreyWildcards, uint16_t mInstantRewards, uint16_t mHirelings) :
+ m_preyWildcards(mPreyWildcards), m_instantRewards(mInstantRewards), m_hirelings(mHirelings) { }
+};
+
+class PlayerCyclopedia {
+public:
+ explicit PlayerCyclopedia(Player &player);
+
+ Summary getSummary();
+
+ void loadSummaryData();
+ void loadDeathHistory(uint16_t page, uint16_t entriesPerPage);
+ void loadRecentKills(uint16_t page, uint16_t entriesPerPage);
+
+ void updateStoreSummary(uint8_t type, uint16_t amount = 1, const std::string &id = "");
+ uint16_t getAmount(uint8_t type);
+ void updateAmount(uint8_t type, uint16_t amount = 1);
+
+ [[nodiscard]] std::map getResult(uint8_t type) const;
+ void insertValue(uint8_t type, uint16_t amount = 1, const std::string &id = "");
+
+private:
+ Player &m_player;
+};
diff --git a/src/creatures/players/cyclopedia/player_title.cpp b/src/creatures/players/cyclopedia/player_title.cpp
index 624b03134..7c348cbf7 100644
--- a/src/creatures/players/cyclopedia/player_title.cpp
+++ b/src/creatures/players/cyclopedia/player_title.cpp
@@ -26,7 +26,7 @@ bool PlayerTitle::isTitleUnlocked(uint8_t id) const {
if (auto it = std::find_if(m_titlesUnlocked.begin(), m_titlesUnlocked.end(), [id](auto title_it) {
return title_it.first.m_id == id;
});
- it != m_titlesUnlocked.end()) {
+ it != m_titlesUnlocked.end()) {
return true;
}
@@ -84,7 +84,8 @@ const std::vector> &PlayerTitle::getUnlockedTitles()
}
uint8_t PlayerTitle::getCurrentTitle() const {
- return static_cast(m_player.kv()->scoped("titles")->get("current-title")->getNumber());
+ auto title = m_player.kv()->scoped("titles")->get("current-title");
+ return title ? static_cast(title->getNumber()) : 0;
}
void PlayerTitle::setCurrentTitle(uint8_t id) {
diff --git a/src/creatures/players/grouping/familiars.cpp b/src/creatures/players/grouping/familiars.cpp
index 6e923b823..6312aaa28 100644
--- a/src/creatures/players/grouping/familiars.cpp
+++ b/src/creatures/players/grouping/familiars.cpp
@@ -77,7 +77,7 @@ std::shared_ptr Familiars::getFamiliarByLookType(uint16_t vocation, ui
if (auto it = std::find_if(familiars[vocation].begin(), familiars[vocation].end(), [lookType](auto familiar_it) {
return familiar_it->lookType == lookType;
});
- it != familiars[vocation].end()) {
+ it != familiars[vocation].end()) {
return *it;
}
return nullptr;
diff --git a/src/creatures/players/grouping/groups.cpp b/src/creatures/players/grouping/groups.cpp
index 937fa848f..c4dab4a90 100644
--- a/src/creatures/players/grouping/groups.cpp
+++ b/src/creatures/players/grouping/groups.cpp
@@ -103,7 +103,7 @@ 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;
});
- it != groups_vector.end()) {
+ it != groups_vector.end()) {
return *it;
}
return nullptr;
diff --git a/src/creatures/players/grouping/party.hpp b/src/creatures/players/grouping/party.hpp
index 10663a278..6aaecc561 100644
--- a/src/creatures/players/grouping/party.hpp
+++ b/src/creatures/players/grouping/party.hpp
@@ -108,7 +108,7 @@ class Party : public SharedObject {
if (auto it = std::find_if(membersData.begin(), membersData.end(), [playerId](const std::shared_ptr &preyIt) {
return preyIt->id == playerId;
});
- it != membersData.end()) {
+ it != membersData.end()) {
return *it;
}
diff --git a/src/creatures/players/imbuements/imbuements.cpp b/src/creatures/players/imbuements/imbuements.cpp
index ed312dbf8..4bfb0de83 100644
--- a/src/creatures/players/imbuements/imbuements.cpp
+++ b/src/creatures/players/imbuements/imbuements.cpp
@@ -347,9 +347,9 @@ std::vector Imbuements::getImbuements(std::shared_ptr player
// Parse the storages for each imbuement in imbuements.xml and config.lua (enable/disable storage)
if (g_configManager().getBoolean(TOGGLE_IMBUEMENT_SHRINE_STORAGE, __FUNCTION__)
- && imbuement->getStorage() != 0
- && player->getStorageValue(imbuement->getStorage() == -1)
- && imbuement->getBaseID() >= 1 && imbuement->getBaseID() <= 3) {
+ && imbuement->getStorage() != 0
+ && player->getStorageValue(imbuement->getStorage() == -1)
+ && imbuement->getBaseID() >= 1 && imbuement->getBaseID() <= 3) {
continue;
}
diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp
index 2c92f6941..eddffa20a 100644
--- a/src/creatures/players/player.cpp
+++ b/src/creatures/players/player.cpp
@@ -17,6 +17,7 @@
#include "creatures/players/wheel/player_wheel.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
+#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_title.hpp"
#include "creatures/players/storages/storages.hpp"
#include "game/game.hpp"
@@ -53,6 +54,7 @@ Player::Player(ProtocolGame_ptr p) :
m_wheelPlayer = std::make_unique(*this);
m_playerAchievement = std::make_unique(*this);
m_playerBadge = std::make_unique(*this);
+ m_playerCyclopedia = std::make_unique(*this);
m_playerTitle = std::make_unique(*this);
}
@@ -284,7 +286,7 @@ std::shared_ptr- Player::getQuiverAmmoOfType(const ItemType &it) const {
std::shared_ptr
- quiver = inventory[CONST_SLOT_RIGHT];
for (std::shared_ptr container = quiver->getContainer();
- auto ammoItem : container->getItemList()) {
+ auto ammoItem : container->getItemList()) {
if (ammoItem->getAmmoType() == it.ammoType) {
if (level >= Item::items[ammoItem->getID()].minReqLevel) {
return ammoItem;
@@ -633,20 +635,6 @@ phmap::flat_hash_map> Player::getAllSlotItems() c
return itemMap;
}
-phmap::flat_hash_map Player::getBlessingNames() const {
- static phmap::flat_hash_map blessingNames = {
- { TWIST_OF_FATE, "Twist of Fate" },
- { WISDOM_OF_SOLITUDE, "The Wisdom of Solitude" },
- { SPARK_OF_THE_PHOENIX, "The Spark of the Phoenix" },
- { FIRE_OF_THE_SUNS, "The Fire of the Suns" },
- { SPIRITUAL_SHIELDING, "The Spiritual Shielding" },
- { EMBRACE_OF_TIBIA, "The Embrace of Tibia" },
- { BLOOD_OF_THE_MOUNTAIN, "Blood of the Mountain" },
- { HEARTH_OF_THE_MOUNTAIN, "Heart of the Mountain" },
- };
- return blessingNames;
-}
-
void Player::setTraining(bool value) {
for (const auto &[key, player] : g_game().getPlayers()) {
if (!this->isInGhostMode() || player->isAccessPlayer()) {
@@ -1894,32 +1882,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;
}
@@ -3828,7 +3823,7 @@ bool Player::hasItemCountById(uint16_t itemId, uint32_t itemAmount, bool checkSt
// Check items from stash
for (StashItemList stashToSend = getStashItems();
- auto [stashItemId, itemCount] : stashToSend) {
+ auto [stashItemId, itemCount] : stashToSend) {
if (!checkStash) {
break;
}
@@ -4293,7 +4288,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);
});
@@ -5385,7 +5380,7 @@ uint16_t Player::getSkillLevel(skills_t skill) const {
skillLevel = std::max(0, skillLevel + varSkills[skill]);
if (auto it = maxValuePerSkill.find(skill);
- it != maxValuePerSkill.end()) {
+ it != maxValuePerSkill.end()) {
skillLevel = std::min(it->second, skillLevel);
}
@@ -6221,7 +6216,7 @@ std::pair Player::getForgeSliversAndCores() const {
// Check items from stash
for (StashItemList stashToSend = getStashItems();
- auto [itemId, itemCount] : stashToSend) {
+ auto [itemId, itemCount] : stashToSend) {
if (itemId == ITEM_FORGE_SLIVER) {
sliverCount += itemCount;
}
@@ -6612,12 +6607,12 @@ std::string Player::getBlessingsName() const {
}
});
- auto BlessingNames = getBlessingNames();
+ auto BlessingNames = g_game().getBlessingNames();
std::ostringstream os;
for (uint8_t i = 1; i <= 8; i++) {
if (hasBlessing(i)) {
if (auto blessName = BlessingNames.find(static_cast(i));
- blessName != BlessingNames.end()) {
+ blessName != BlessingNames.end()) {
os << (*blessName).second;
} else {
continue;
@@ -6810,7 +6805,7 @@ void Player::requestDepotSearchItem(uint16_t itemId, uint8_t tier) {
uint32_t stashCount = 0;
if (const ItemType &iType = Item::items[itemId];
- iType.stackable && iType.wareId > 0) {
+ iType.stackable && iType.wareId > 0) {
stashCount = getStashItemCount(itemId);
}
@@ -6859,10 +6854,10 @@ void Player::retrieveAllItemsFromDepotSearch(uint16_t itemId, uint8_t tier, bool
for (const std::shared_ptr
- &locker : depotLocker->getItemList()) {
std::shared_ptr c = locker->getContainer();
if (!c || c->empty() ||
- // Retrieve from inbox.
- (c->isInbox() && isDepot) ||
- // Retrieve from depot.
- (!c->isInbox() && !isDepot)) {
+ // Retrieve from inbox.
+ (c->isInbox() && isDepot) ||
+ // Retrieve from depot.
+ (!c->isInbox() && !isDepot)) {
continue;
}
@@ -6928,7 +6923,7 @@ std::shared_ptr
- Player::getItemFromDepotSearch(uint16_t itemId, const Posi
for (const std::shared_ptr
- &locker : depotLocker->getItemList()) {
std::shared_ptr c = locker->getContainer();
if (!c || c->empty() || (c->isInbox() && pos.y != 0x21) || // From inbox.
- (!c->isInbox() && pos.y != 0x20)) { // From depot.
+ (!c->isInbox() && pos.y != 0x20)) { // From depot.
continue;
}
@@ -7112,7 +7107,7 @@ void Player::forgeFuseItems(ForgeAction_t actionType, uint16_t firstItemId, uint
return;
}
if (returnValue = g_game().internalRemoveItem(secondForgingItem, 1);
- returnValue != RETURNVALUE_NOERROR) {
+ returnValue != RETURNVALUE_NOERROR) {
g_logger().error("[Log 2] Failed to remove forge item {} from player with name {}", secondItemId, getName());
sendCancelMessage(getReturnMessage(returnValue));
sendForgeError(RETURNVALUE_CONTACTADMINISTRATOR);
@@ -7355,7 +7350,7 @@ void Player::forgeTransferItemTier(ForgeAction_t actionType, uint16_t donorItemI
return;
}
if (returnValue = g_game().internalRemoveItem(receiveItem, 1);
- returnValue != RETURNVALUE_NOERROR) {
+ returnValue != RETURNVALUE_NOERROR) {
g_logger().error("[Log 2] Failed to remove transfer item {} from player with name {}", receiveItemId, getName());
sendCancelMessage(getReturnMessage(returnValue));
sendForgeError(RETURNVALUE_CONTACTADMINISTRATOR);
@@ -7495,7 +7490,7 @@ void Player::forgeResourceConversion(ForgeAction_t actionType) {
}
if (std::shared_ptr
- item = Item::CreateItem(ITEM_FORGE_CORE, 1);
- item) {
+ item) {
returnValue = g_game().internalPlayerAddItem(static_self_cast(), item);
}
if (returnValue != RETURNVALUE_NOERROR) {
@@ -7517,7 +7512,7 @@ void Player::forgeResourceConversion(ForgeAction_t actionType) {
auto upgradeCost = dustLevel - 75;
if (auto dusts = getForgeDusts();
- upgradeCost > dusts) {
+ upgradeCost > dusts) {
g_logger().error("[{}] Not enough dust", __FUNCTION__);
sendForgeError(RETURNVALUE_CONTACTADMINISTRATOR);
return;
@@ -8019,6 +8014,15 @@ const std::unique_ptr &Player::vip() const {
return m_playerVIP;
}
+// Cyclopedia
+std::unique_ptr &Player::cyclopedia() {
+ return m_playerCyclopedia;
+}
+
+const std::unique_ptr &Player::cyclopedia() const {
+ return m_playerCyclopedia;
+}
+
void Player::sendLootMessage(const std::string &message) const {
auto party = getParty();
if (!party) {
@@ -8093,7 +8097,7 @@ bool Player::hasPermittedConditionInPZ() const {
uint16_t Player::getDodgeChance() const {
uint16_t chance = 0;
if (auto playerArmor = getInventoryItem(CONST_SLOT_ARMOR);
- playerArmor != nullptr && playerArmor->getTier()) {
+ playerArmor != nullptr && playerArmor->getTier()) {
chance += static_cast(playerArmor->getDodgeChance() * 100);
}
diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp
index d374bd599..627b640ad 100644
--- a/src/creatures/players/player.hpp
+++ b/src/creatures/players/player.hpp
@@ -36,6 +36,7 @@
#include "enums/object_category.hpp"
#include "enums/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
+#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_title.hpp"
#include "creatures/players/vip/player_vip.hpp"
@@ -54,6 +55,7 @@ class Spell;
class PlayerWheel;
class PlayerAchievement;
class PlayerBadge;
+class PlayerCyclopedia;
class PlayerTitle;
class PlayerVIP;
class Spectators;
@@ -475,13 +477,18 @@ class Player final : public Creature, public Cylinder, public Bankable {
bool hasBlessing(uint8_t index) const {
return blessings[index - 1] != 0;
}
- uint8_t getBlessingCount(uint8_t index) const {
- if (index > 0 && index <= blessings.size()) {
- return blessings[index - 1];
- } else {
- g_logger().error("[{}] - index outside range 0-10.", __FUNCTION__);
- return 0;
+
+ uint8_t getBlessingCount(uint8_t index, bool storeCount = false) const {
+ if (!storeCount) {
+ if (index > 0 && index <= blessings.size()) {
+ return blessings[index - 1];
+ } else {
+ g_logger().error("[{}] - index outside range 0-10.", __FUNCTION__);
+ return 0;
+ }
}
+ auto amount = kv()->scoped("summary")->scoped("blessings")->scoped(fmt::format("{}", index))->get("amount");
+ return amount ? static_cast(amount->getNumber()) : 0;
}
std::string getBlessingsName() const;
@@ -850,8 +857,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;
@@ -1642,11 +1649,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
client->sendCyclopediaCharacterRecentDeaths(page, pages, entries);
}
}
- void sendCyclopediaCharacterRecentPvPKills(
- uint16_t page, uint16_t pages,
- const std::vector<
- RecentPvPKillEntry> &entries
- ) {
+ void sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t pages, const std::vector &entries) {
if (client) {
client->sendCyclopediaCharacterRecentPvPKills(page, pages, entries);
}
@@ -1956,7 +1959,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
bool isImmuneCleanse(ConditionType_t conditiontype) {
uint64_t timenow = OTSYS_TIME();
if ((cleanseCondition.first == conditiontype)
- && (timenow <= cleanseCondition.second)) {
+ && (timenow <= cleanseCondition.second)) {
return true;
}
return false;
@@ -2154,7 +2157,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
if (auto it = std::find_if(preys.begin(), preys.end(), [slotid](const std::unique_ptr &preyIt) {
return preyIt->id == slotid;
});
- it != preys.end()) {
+ it != preys.end()) {
return *it;
}
@@ -2221,7 +2224,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
if (auto it = std::find_if(preys.begin(), preys.end(), [raceId](const std::unique_ptr &it) {
return it->selectedRaceId == raceId;
});
- it != preys.end()) {
+ it != preys.end()) {
return *it;
}
@@ -2252,7 +2255,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
if (auto it = std::find_if(taskHunting.begin(), taskHunting.end(), [slotid](const std::unique_ptr &itTask) {
return itTask->id == slotid;
});
- it != taskHunting.end()) {
+ it != taskHunting.end()) {
return *it;
}
@@ -2321,7 +2324,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
if (auto it = std::find_if(taskHunting.begin(), taskHunting.end(), [raceId](const std::unique_ptr &itTask) {
return itTask->selectedRaceId == raceId;
});
- it != taskHunting.end()) {
+ it != taskHunting.end()) {
return *it;
}
@@ -2562,8 +2565,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()) {
@@ -2571,18 +2573,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 {
@@ -2605,9 +2602,6 @@ class Player final : public Creature, public Cylinder, public Bankable {
// This get all players slot items
phmap::flat_hash_map> getAllSlotItems() const;
- // This get all blessings
- phmap::flat_hash_map getBlessingNames() const;
-
// Gets the equipped items with augment by type
std::vector> getEquippedAugmentItemsByType(Augment_t augmentType) const;
@@ -2637,6 +2631,10 @@ class Player final : public Creature, public Cylinder, public Bankable {
std::unique_ptr &title();
const std::unique_ptr &title() const;
+ // Player summary interface
+ std::unique_ptr &cyclopedia();
+ const std::unique_ptr &cyclopedia() const;
+
// Player vip interface
std::unique_ptr &vip();
const std::unique_ptr &vip() const;
@@ -2958,6 +2956,8 @@ class Player final : public Creature, public Cylinder, public Bankable {
int32_t magicShieldCapacityFlat = 0;
int32_t magicShieldCapacityPercent = 0;
+ int32_t marriageSpouse = -1;
+
void updateItemsLight(bool internal = false);
uint16_t getStepSpeed() const override {
return std::max(PLAYER_MIN_SPEED, std::min(PLAYER_MAX_SPEED, getSpeed()));
@@ -3034,12 +3034,14 @@ class Player final : public Creature, public Cylinder, public Bankable {
friend class IOLoginDataSave;
friend class PlayerAchievement;
friend class PlayerBadge;
+ friend class PlayerCyclopedia;
friend class PlayerTitle;
friend class PlayerVIP;
std::unique_ptr m_wheelPlayer;
std::unique_ptr m_playerAchievement;
std::unique_ptr m_playerBadge;
+ std::unique_ptr m_playerCyclopedia;
std::unique_ptr m_playerTitle;
std::unique_ptr m_playerVIP;
@@ -3065,4 +3067,11 @@ class Player final : public Creature, public Cylinder, public Bankable {
bool hasOtherRewardContainerOpen(const std::shared_ptr container) const;
void checkAndShowBlessingMessage();
+
+ void setMarriageSpouse(const int32_t spouseId) {
+ marriageSpouse = spouseId;
+ }
+ int32_t getMarriageSpouse() const {
+ return marriageSpouse;
+ }
};
diff --git a/src/creatures/players/vip/player_vip.cpp b/src/creatures/players/vip/player_vip.cpp
index b4b1642ec..95ebe91ad 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/creatures/players/vocations/vocation.cpp b/src/creatures/players/vocations/vocation.cpp
index 98dbaeb1b..6fec27251 100644
--- a/src/creatures/players/vocations/vocation.cpp
+++ b/src/creatures/players/vocations/vocation.cpp
@@ -129,13 +129,13 @@ bool Vocations::loadFromXml() {
voc->skillMultipliers[skill_id] = pugi::cast(childNode.attribute("multiplier").value());
} else {
g_logger().warn("[Vocations::loadFromXml] - "
- "No valid skill id: {} for vocation: {}",
- skill_id, voc->id);
+ "No valid skill id: {} for vocation: {}",
+ skill_id, voc->id);
}
} else {
g_logger().warn("[Vocations::loadFromXml] - "
- "Missing skill id for vocation: {}",
- voc->id);
+ "Missing skill id for vocation: {}",
+ voc->id);
}
} else if (strcasecmp(childNode.name(), "mitigation") == 0) {
pugi::xml_attribute factorAttribute = childNode.attribute("multiplier");
@@ -198,8 +198,8 @@ std::shared_ptr Vocations::getVocation(uint16_t id) {
auto it = vocationsMap.find(id);
if (it == vocationsMap.end()) {
g_logger().warn("[Vocations::getVocation] - "
- "Vocation {} not found",
- id);
+ "Vocation {} not found",
+ id);
return nullptr;
}
return it->second;
diff --git a/src/database/databasemanager.cpp b/src/database/databasemanager.cpp
index cbcc116dd..dfbb7d9a6 100644
--- a/src/database/databasemanager.cpp
+++ b/src/database/databasemanager.cpp
@@ -89,8 +89,8 @@ void DatabaseManager::updateDatabase() {
ss << g_configManager().getString(DATA_DIRECTORY, __FUNCTION__) + "/migrations/" << version << ".lua";
if (luaL_dofile(L, ss.str().c_str()) != 0) {
g_logger().error("DatabaseManager::updateDatabase - Version: {}"
- "] {}",
- version, lua_tostring(L, -1));
+ "] {}",
+ version, lua_tostring(L, -1));
break;
}
diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp
index f0637011a..295e57398 100644
--- a/src/enums/player_cyclopedia.hpp
+++ b/src/enums/player_cyclopedia.hpp
@@ -36,3 +36,27 @@ enum CyclopediaTitle_t : uint8_t {
MAP,
OTHERS,
};
+
+enum Summary_t : uint8_t {
+ HOUSE_ITEMS = 9,
+ BOOSTS = 10,
+ PREY_CARDS = 12,
+ BLESSINGS = 14,
+ ALL_BLESSINGS = 17,
+ INSTANT_REWARDS = 18,
+ HIRELINGS = 20,
+};
+
+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/game.cpp b/src/game/game.cpp
index 57daddded..7ef6b7a90 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -38,6 +38,7 @@
#include "creatures/players/wheel/player_wheel.hpp"
#include "creatures/players/achievement/player_achievement.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
+#include "creatures/players/cyclopedia/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_title.hpp"
#include "creatures/npcs/npc.hpp"
#include "server/network/webhook/webhook.hpp"
@@ -362,6 +363,45 @@ Game::Game() {
HighscoreCategory("Fishing", static_cast(HighscoreCategories_t::FISHING)),
HighscoreCategory("Magic Level", static_cast(HighscoreCategories_t::MAGIC_LEVEL))
};
+
+ m_blessingNames = {
+ { static_cast(TWIST_OF_FATE), "Twist of Fate" },
+ { static_cast(WISDOM_OF_SOLITUDE), "The Wisdom of Solitude" },
+ { static_cast(SPARK_OF_THE_PHOENIX), "The Spark of the Phoenix" },
+ { static_cast(FIRE_OF_THE_SUNS), "The Fire of the Suns" },
+ { static_cast(SPIRITUAL_SHIELDING), "The Spiritual Shielding" },
+ { static_cast(EMBRACE_OF_TIBIA), "The Embrace of Tibia" },
+ { static_cast(BLOOD_OF_THE_MOUNTAIN), "Blood of the Mountain" },
+ { static_cast(HEARTH_OF_THE_MOUNTAIN), "Heart of the Mountain" },
+ };
+
+ m_summaryCategories = {
+ { static_cast(Summary_t::HOUSE_ITEMS), "house-items" },
+ { static_cast(Summary_t::BOOSTS), "xp-boosts" },
+ { static_cast(Summary_t::PREY_CARDS), "prey-cards" },
+ { static_cast(Summary_t::BLESSINGS), "blessings" },
+ { static_cast(Summary_t::INSTANT_REWARDS), "instant-rewards" },
+ { static_cast(Summary_t::HIRELINGS), "hirelings" },
+ };
+
+ m_hirelingSkills = {
+ { 1001, "banker" },
+ { 1002, "cooker" },
+ { 1003, "steward" },
+ { 1004, "trader" }
+ };
+
+ m_hirelingOutfits = {
+ { 2001, "banker" },
+ { 2002, "cooker" },
+ { 2003, "steward" },
+ { 2004, "trader" },
+ { 2005, "servant" },
+ { 2006, "hydra" },
+ { 2007, "ferumbras" },
+ { 2008, "bonelord" },
+ { 2009, "dragon" },
+ };
}
Game::~Game() = default;
@@ -386,7 +426,7 @@ void Game::loadBoostedCreature() {
const auto result = db.storeQuery("SELECT * FROM `boosted_creature`");
if (!result) {
g_logger().warn("[Game::loadBoostedCreature] - "
- "Failed to detect boosted creature database. (CODE 01)");
+ "Failed to detect boosted creature database. (CODE 01)");
return;
}
@@ -423,15 +463,15 @@ void Game::loadBoostedCreature() {
if (selectedMonster.raceId == 0) {
g_logger().warn("[Game::loadBoostedCreature] - "
- "It was not possible to generate a new boosted creature->");
+ "It was not possible to generate a new boosted creature->");
return;
}
const auto monsterType = g_monsters().getMonsterType(selectedMonster.name);
if (!monsterType) {
g_logger().warn("[Game::loadBoostedCreature] - "
- "It was not possible to generate a new boosted creature-> Monster '{}' not found.",
- selectedMonster.name);
+ "It was not possible to generate a new boosted creature-> Monster '{}' not found.",
+ selectedMonster.name);
return;
}
@@ -451,7 +491,7 @@ void Game::loadBoostedCreature() {
if (!db.executeQuery(query)) {
g_logger().warn("[Game::loadBoostedCreature] - "
- "Failed to detect boosted creature database. (CODE 02)");
+ "Failed to detect boosted creature database. (CODE 02)");
}
}
@@ -1703,7 +1743,7 @@ void Game::playerMoveItem(std::shared_ptr player, const Position &fromPo
uint8_t itemStackPos = fromStackPos;
if (fromPos.x != 0xFFFF && Position::areInRange<1, 1>(mapFromPos, playerPos)
- && !Position::areInRange<1, 1, 0>(mapFromPos, walkPos)) {
+ && !Position::areInRange<1, 1, 0>(mapFromPos, walkPos)) {
// need to pickup the item first
std::shared_ptr
- moveItem = nullptr;
@@ -2093,20 +2133,20 @@ ReturnValue Game::internalMoveItem(std::shared_ptr fromCylinder, std::
std::shared_ptr
- quiver = toCylinder->getItem();
if (quiver && quiver->isQuiver()
- && quiver->getHoldingPlayer()
- && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
+ && quiver->getHoldingPlayer()
+ && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
quiver->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, quiver);
} else {
quiver = fromCylinder->getItem();
if (quiver && quiver->isQuiver()
- && quiver->getHoldingPlayer()
- && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
+ && quiver->getHoldingPlayer()
+ && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
quiver->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, quiver);
}
}
if (SoundEffect_t soundEffect = item->getMovementSound(toCylinder);
- toCylinder && soundEffect != SoundEffect_t::SILENCE) {
+ toCylinder && soundEffect != SoundEffect_t::SILENCE) {
if (toCylinder->getContainer() && actor && actor->getPlayer() && (toCylinder->getContainer()->isInsideDepot(true) || toCylinder->getContainer()->getHoldingPlayer())) {
actor->getPlayer()->sendSingleSoundEffect(toCylinder->getPosition(), soundEffect, SourceEffect_t::OWN);
} else {
@@ -2239,8 +2279,8 @@ ReturnValue Game::internalAddItem(std::shared_ptr toCylinder, std::sha
}
if (addedItem && addedItem->isQuiver()
- && addedItem->getHoldingPlayer()
- && addedItem->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == addedItem) {
+ && addedItem->getHoldingPlayer()
+ && addedItem->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == addedItem) {
addedItem->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, addedItem);
}
@@ -2300,8 +2340,8 @@ ReturnValue Game::internalRemoveItem(std::shared_ptr
- item, int32_t count /
std::shared_ptr
- quiver = cylinder->getItem();
if (quiver && quiver->isQuiver()
- && quiver->getHoldingPlayer()
- && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
+ && quiver->getHoldingPlayer()
+ && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
quiver->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, quiver);
}
@@ -2760,8 +2800,8 @@ std::shared_ptr
- Game::transformItem(std::shared_ptr
- item, uint16_t n
std::shared_ptr
- quiver = cylinder->getItem();
if (quiver && quiver->isQuiver()
- && quiver->getHoldingPlayer()
- && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
+ && quiver->getHoldingPlayer()
+ && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
quiver->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, quiver);
}
item->startDecaying();
@@ -2772,8 +2812,8 @@ std::shared_ptr
- Game::transformItem(std::shared_ptr
- item, uint16_t n
std::shared_ptr
- quiver = cylinder->getItem();
if (quiver && quiver->isQuiver()
- && quiver->getHoldingPlayer()
- && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
+ && quiver->getHoldingPlayer()
+ && quiver->getHoldingPlayer()->getThing(CONST_SLOT_RIGHT) == quiver) {
quiver->getHoldingPlayer()->sendInventoryItem(CONST_SLOT_RIGHT, quiver);
}
@@ -3699,7 +3739,7 @@ void Game::playerUseItemEx(uint32_t playerId, const Position &fromPos, uint8_t f
mustReloadDepotSearch = true;
} else {
if (auto targetThing = internalGetThing(player, toPos, toStackPos, toItemId, STACKPOS_FIND_THING);
- targetThing && targetThing->getItem() && targetThing->getItem()->isInsideDepot(true)) {
+ targetThing && targetThing->getItem() && targetThing->getItem()->isInsideDepot(true)) {
mustReloadDepotSearch = true;
}
}
@@ -4235,7 +4275,7 @@ void Game::playerSetShowOffSocket(uint32_t playerId, Outfit_t &outfit, const Pos
item->setCustomAttribute("LookFeet", static_cast(outfit.lookFeet));
item->setCustomAttribute("LookAddons", static_cast(outfit.lookAddons));
} else if (auto pastLookType = item->getCustomAttribute("PastLookType");
- pastLookType && pastLookType->getInteger() > 0) {
+ pastLookType && pastLookType->getInteger() > 0) {
item->removeCustomAttribute("LookType");
item->removeCustomAttribute("PastLookType");
}
@@ -4247,7 +4287,7 @@ void Game::playerSetShowOffSocket(uint32_t playerId, Outfit_t &outfit, const Pos
item->setCustomAttribute("LookMountLegs", static_cast(outfit.lookMountLegs));
item->setCustomAttribute("LookMountFeet", static_cast(outfit.lookMountFeet));
} else if (auto pastLookMount = item->getCustomAttribute("PastLookMount");
- pastLookMount && pastLookMount->getInteger() > 0) {
+ pastLookMount && pastLookMount->getInteger() > 0) {
item->removeCustomAttribute("LookMount");
item->removeCustomAttribute("PastLookMount");
}
@@ -5496,8 +5536,8 @@ void Game::playerLootAllCorpses(std::shared_ptr player, const Position &
}
if (!tileCorpse->isRewardCorpse()
- && tileCorpse->getCorpseOwner() != 0
- && !player->canOpenCorpse(tileCorpse->getCorpseOwner())) {
+ && tileCorpse->getCorpseOwner() != 0
+ && !player->canOpenCorpse(tileCorpse->getCorpseOwner())) {
player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE);
g_logger().debug("Player {} cannot loot corpse from id {} in position {}", player->getName(), tileItem->getID(), tileItem->getPosition().toString());
continue;
@@ -6255,7 +6295,6 @@ void Game::checkCreatureWalk(uint32_t creatureId) {
const auto &creature = getCreatureByID(creatureId);
if (creature && creature->getHealth() > 0) {
creature->onCreatureWalk();
- cleanup();
}
}
@@ -6318,7 +6357,6 @@ void Game::checkCreatures() {
--end;
}
}
- cleanup();
index = (index + 1) % EVENT_CREATURECOUNT;
}
@@ -7122,9 +7160,9 @@ bool Game::combatChangeHealth(std::shared_ptr attacker, std::shared_pt
if (!damage.extension && attackerMonster && targetPlayer) {
// Charm rune (target as player)
if (charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(targetPlayer, g_monsters().getMonsterTypeByRaceId(attackerMonster->getRaceId()));
- activeCharm != CHARM_NONE && activeCharm != CHARM_CLEANSE) {
+ activeCharm != CHARM_NONE && activeCharm != CHARM_CLEANSE) {
if (const auto charm = g_iobestiary().getBestiaryCharm(activeCharm);
- charm->type == CHARM_DEFENSIVE && charm->chance > normal_random(0, 100) && g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value))) {
+ charm->type == CHARM_DEFENSIVE && charm->chance > normal_random(0, 100) && g_iobestiary().parseCharmCombat(charm, targetPlayer, attacker, (damage.primary.value + damage.secondary.value))) {
return false; // Dodge charm
}
}
@@ -7510,7 +7548,7 @@ void Game::applyCharmRune(
return;
}
if (charmRune_t activeCharm = g_iobestiary().getCharmFromTarget(attackerPlayer, g_monsters().getMonsterTypeByRaceId(targetMonster->getRaceId()));
- activeCharm != CHARM_NONE) {
+ activeCharm != CHARM_NONE) {
const auto charm = g_iobestiary().getBestiaryCharm(activeCharm);
int8_t chance = charm->id == CHARM_CRIPPLE ? charm->chance : charm->chance + attackerPlayer->getCharmChanceModifier();
g_logger().debug("charm chance: {}, base: {}, bonus: {}", chance, charm->chance, attackerPlayer->getCharmChanceModifier());
@@ -7536,7 +7574,7 @@ void Game::applyManaLeech(
// Void charm rune
if (targetMonster) {
if (uint16_t playerCharmRaceidVoid = attackerPlayer->parseRacebyCharm(CHARM_VOID, false, 0);
- playerCharmRaceidVoid != 0 && playerCharmRaceidVoid == targetMonster->getRace()) {
+ playerCharmRaceidVoid != 0 && playerCharmRaceidVoid == targetMonster->getRace()) {
if (const auto charm = g_iobestiary().getBestiaryCharm(CHARM_VOID)) {
manaSkill += charm->percent;
}
@@ -7567,7 +7605,7 @@ void Game::applyLifeLeech(
}
if (targetMonster) {
if (uint16_t playerCharmRaceidVamp = attackerPlayer->parseRacebyCharm(CHARM_VAMP, false, 0);
- playerCharmRaceidVamp != 0 && playerCharmRaceidVamp == targetMonster->getRaceId()) {
+ playerCharmRaceidVamp != 0 && playerCharmRaceidVamp == targetMonster->getRaceId()) {
if (const auto lifec = g_iobestiary().getBestiaryCharm(CHARM_VAMP)) {
lifeSkill += lifec->percent;
}
@@ -7967,8 +8005,6 @@ void Game::shutdown() {
map.spawnsNpc.clear();
raids.clear();
- cleanup();
-
if (serviceManager) {
serviceManager->stop();
}
@@ -7980,16 +8016,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()) {
@@ -8314,121 +8340,12 @@ void Game::playerCyclopediaCharacterInfo(std::shared_ptr player, uint32_
case CYCLOPEDIA_CHARACTERINFO_COMBATSTATS:
player->sendCyclopediaCharacterCombatStats();
break;
- case CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS: {
- std::ostringstream query;
- uint32_t offset = static_cast(page - 1) * entriesPerPage;
- query << "SELECT `time`, `level`, `killed_by`, `mostdamage_by`, (select count(*) FROM `player_deaths` WHERE `player_id` = " << playerGUID << ") as `entries` FROM `player_deaths` WHERE `player_id` = " << playerGUID << " ORDER BY `time` DESC LIMIT " << offset << ", " << entriesPerPage;
-
- uint32_t playerID = player->getID();
- std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) {
- std::shared_ptr player = g_game().getPlayerByID(playerID);
- if (!player) {
- return;
- }
-
- player->resetAsyncOngoingTask(PlayerAsyncTask_RecentDeaths);
- if (!result) {
- player->sendCyclopediaCharacterRecentDeaths(0, 0, {});
- return;
- }
-
- uint32_t pages = result->getNumber("entries");
- pages += entriesPerPage - 1;
- pages /= entriesPerPage;
-
- std::vector entries;
- entries.reserve(result->countResults());
- do {
- std::string cause1 = result->getString("killed_by");
- std::string cause2 = result->getString("mostdamage_by");
-
- std::ostringstream cause;
- cause << "Died at Level " << result->getNumber("level") << " by";
- if (!cause1.empty()) {
- const char &character = cause1.front();
- if (character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u') {
- cause << " an ";
- } else {
- cause << " a ";
- }
- cause << cause1;
- }
-
- if (!cause2.empty()) {
- if (!cause1.empty()) {
- cause << " and ";
- }
-
- const char &character = cause2.front();
- if (character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u') {
- cause << " an ";
- } else {
- cause << " a ";
- }
- cause << cause2;
- }
- cause << '.';
- entries.emplace_back(std::move(cause.str()), result->getNumber("time"));
- } while (result->next());
- player->sendCyclopediaCharacterRecentDeaths(page, static_cast(pages), entries);
- };
- g_databaseTasks().store(query.str(), callback);
- player->addAsyncOngoingTask(PlayerAsyncTask_RecentDeaths);
+ case CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS:
+ player->cyclopedia()->loadDeathHistory(page, entriesPerPage);
break;
- }
- case CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS: {
- // TODO: add guildwar, assists and arena kills
- Database &db = Database::getInstance();
- const std::string &escapedName = db.escapeString(player->getName());
- std::ostringstream query;
- uint32_t offset = static_cast(page - 1) * entriesPerPage;
- query << "SELECT `d`.`time`, `d`.`killed_by`, `d`.`mostdamage_by`, `d`.`unjustified`, `d`.`mostdamage_unjustified`, `p`.`name`, (select count(*) FROM `player_deaths` WHERE ((`killed_by` = " << escapedName << " AND `is_player` = 1) OR (`mostdamage_by` = " << escapedName << " AND `mostdamage_is_player` = 1))) as `entries` FROM `player_deaths` AS `d` INNER JOIN `players` AS `p` ON `d`.`player_id` = `p`.`id` WHERE ((`d`.`killed_by` = " << escapedName << " AND `d`.`is_player` = 1) OR (`d`.`mostdamage_by` = " << escapedName << " AND `d`.`mostdamage_is_player` = 1)) ORDER BY `time` DESC LIMIT " << offset << ", " << entriesPerPage;
-
- uint32_t playerID = player->getID();
- std::function callback = [playerID, page, entriesPerPage](const DBResult_ptr &result, bool) {
- std::shared_ptr player = g_game().getPlayerByID(playerID);
- if (!player) {
- return;
- }
-
- player->resetAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills);
- if (!result) {
- player->sendCyclopediaCharacterRecentPvPKills(0, 0, {});
- return;
- }
-
- uint32_t pages = result->getNumber("entries");
- pages += entriesPerPage - 1;
- pages /= entriesPerPage;
-
- std::vector entries;
- entries.reserve(result->countResults());
- do {
- std::string cause1 = result->getString("killed_by");
- std::string cause2 = result->getString("mostdamage_by");
- std::string name = result->getString("name");
-
- uint8_t status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_JUSTIFIED;
- if (player->getName() == cause1) {
- if (result->getNumber("unjustified") == 1) {
- status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED;
- }
- } else if (player->getName() == cause2) {
- if (result->getNumber("mostdamage_unjustified") == 1) {
- status = CYCLOPEDIA_CHARACTERINFO_RECENTKILLSTATUS_UNJUSTIFIED;
- }
- }
-
- std::ostringstream description;
- description << "Killed " << name << '.';
- entries.emplace_back(std::move(description.str()), result->getNumber("time"), status);
- } while (result->next());
- player->sendCyclopediaCharacterRecentPvPKills(page, static_cast(pages), entries);
- };
- g_databaseTasks().store(query.str(), callback);
- player->addAsyncOngoingTask(PlayerAsyncTask_RecentPvPKills);
+ case CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS:
+ player->cyclopedia()->loadRecentKills(page, entriesPerPage);
break;
- }
case CYCLOPEDIA_CHARACTERINFO_ACHIEVEMENTS:
player->achiev()->sendUnlockedSecretAchievements();
break;
@@ -9537,7 +9454,7 @@ void Game::playerBosstiarySlot(uint32_t playerId, uint8_t slotId, uint32_t selec
uint32_t bossIdSlot = player->getSlotBossId(slotId);
if (uint32_t boostedBossId = g_ioBosstiary().getBoostedBossId();
- selectedBossId == 0 && bossIdSlot != boostedBossId) {
+ selectedBossId == 0 && bossIdSlot != boostedBossId) {
uint8_t removeTimes = player->getRemoveTimes();
uint32_t removePrice = g_ioBosstiary().calculteRemoveBoss(removeTimes);
g_game().removeMoney(player, removePrice, 0, true);
@@ -9573,7 +9490,7 @@ void Game::playerSetMonsterPodium(uint32_t playerId, uint32_t monsterRaceId, con
if (!Position::areInRange<1, 1, 0>(pos, player->getPosition())) {
if (stdext::arraylist listDir(128);
- player->getPathTo(pos, listDir, 0, 1, true, false)) {
+ player->getPathTo(pos, listDir, 0, 1, true, false)) {
g_dispatcher().addEvent([this, playerId = player->getID(), listDir = listDir.data()] { playerAutoWalk(playerId, listDir); }, "Game::playerAutoWalk");
std::shared_ptr task = createPlayerTask(
400, [this, playerId, pos] { playerBrowseField(playerId, pos); }, "Game::playerBrowseField"
@@ -9611,7 +9528,7 @@ void Game::playerSetMonsterPodium(uint32_t playerId, uint32_t monsterRaceId, con
const auto [podiumVisible, monsterVisible] = podiumAndMonsterVisible;
bool changeTentuglyName = false;
if (auto monsterOutfit = mType->info.outfit;
- (monsterOutfit.lookType != 0 || monsterOutfit.lookTypeEx != 0) && monsterVisible) {
+ (monsterOutfit.lookType != 0 || monsterOutfit.lookTypeEx != 0) && monsterVisible) {
// "Tantugly's Head" boss have to send other looktype to the podium
if (monsterOutfit.lookTypeEx == 35105) {
monsterOutfit.lookTypeEx = 39003;
@@ -9673,7 +9590,7 @@ void Game::playerRotatePodium(uint32_t playerId, const Position &pos, uint8_t st
if (pos.x != 0xFFFF && !Position::areInRange<1, 1, 0>(pos, player->getPosition())) {
if (stdext::arraylist listDir(128);
- player->getPathTo(pos, listDir, 0, 1, true, true)) {
+ player->getPathTo(pos, listDir, 0, 1, true, true)) {
g_dispatcher().addEvent([this, playerId = player->getID(), listDir = listDir.data()] { playerAutoWalk(playerId, listDir); }, "Game::playerAutoWalk");
std::shared_ptr task = createPlayerTask(
400, [this, playerId, pos, stackPos, itemId] {
@@ -10045,8 +9962,8 @@ void Game::sendUpdateCreature(std::shared_ptr creature) {
uint32_t Game::makeInfluencedMonster() {
if (auto influencedLimit = g_configManager().getNumber(FORGE_INFLUENCED_CREATURES_LIMIT, __FUNCTION__);
- // Condition
- forgeableMonsters.empty() || influencedMonsters.size() >= influencedLimit) {
+ // Condition
+ forgeableMonsters.empty() || influencedMonsters.size() >= influencedLimit) {
return 0;
}
@@ -10126,8 +10043,8 @@ uint32_t Game::makeFiendishMonster(uint32_t forgeableMonsterId /* = 0*/, bool cr
}
if (auto fiendishLimit = g_configManager().getNumber(FORGE_FIENDISH_CREATURES_LIMIT, __FUNCTION__);
- // Condition
- forgeableMonsters.empty() || fiendishMonsters.size() >= fiendishLimit) {
+ // Condition
+ forgeableMonsters.empty() || fiendishMonsters.size() >= fiendishLimit) {
return 0;
}
@@ -10231,8 +10148,8 @@ bool Game::removeForgeMonster(uint32_t id, ForgeClassifications_t monsterForgeCl
bool Game::removeInfluencedMonster(uint32_t id, bool create /* = false*/) {
if (auto find = influencedMonsters.find(id);
- // Condition
- find != influencedMonsters.end()) {
+ // Condition
+ find != influencedMonsters.end()) {
influencedMonsters.erase(find);
if (create) {
@@ -10248,8 +10165,8 @@ bool Game::removeInfluencedMonster(uint32_t id, bool create /* = false*/) {
bool Game::removeFiendishMonster(uint32_t id, bool create /* = true*/) {
if (auto find = fiendishMonsters.find(id);
- // Condition
- find != fiendishMonsters.end()) {
+ // Condition
+ find != fiendishMonsters.end()) {
fiendishMonsters.erase(find);
checkForgeEventId(id);
@@ -10300,8 +10217,8 @@ void Game::createFiendishMonsters() {
}
if (auto ret = makeFiendishMonster();
- // Condition
- ret == 0) {
+ // Condition
+ ret == 0) {
return;
}
@@ -10319,8 +10236,8 @@ void Game::createInfluencedMonsters() {
}
if (auto ret = makeInfluencedMonster();
- // If condition
- ret == 0) {
+ // If condition
+ ret == 0) {
return;
}
@@ -10339,8 +10256,8 @@ void Game::checkForgeEventId(uint32_t monsterId) {
bool Game::addInfluencedMonster(std::shared_ptr monster) {
if (monster && monster->canBeForgeMonster()) {
if (auto maxInfluencedMonsters = static_cast(g_configManager().getNumber(FORGE_INFLUENCED_CREATURES_LIMIT, __FUNCTION__));
- // If condition
- (influencedMonsters.size() + 1) > maxInfluencedMonsters) {
+ // If condition
+ (influencedMonsters.size() + 1) > maxInfluencedMonsters) {
return false;
}
@@ -10749,3 +10666,19 @@ Title Game::getTitleByName(const std::string &name) {
}
return {};
}
+
+const std::string &Game::getSummaryKeyByType(uint8_t type) {
+ return m_summaryCategories[type];
+}
+
+const std::map &Game::getBlessingNames() {
+ return m_blessingNames;
+}
+
+const std::unordered_map &Game::getHirelingSkills() {
+ return m_hirelingSkills;
+}
+
+const std::unordered_map &Game::getHirelingOutfits() {
+ return m_hirelingOutfits;
+}
diff --git a/src/game/game.hpp b/src/game/game.hpp
index b4be8facd..0537ee030 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);
@@ -733,6 +732,12 @@ class Game {
Title getTitleById(uint8_t id);
Title getTitleByName(const std::string &name);
+ const std::string &getSummaryKeyByType(uint8_t type);
+
+ const std::map &getBlessingNames();
+ const std::unordered_map &getHirelingSkills();
+ const std::unordered_map &getHirelingOutfits();
+
private:
std::map m_achievements;
std::map m_achievementsNameToId;
@@ -743,6 +748,12 @@ class Game {
std::vector