diff --git a/README.md b/README.md
index 119850ea7ea..a5d3aa20797 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,43 @@
# OpenTibiaBR - Canary
[![Discord Channel](https://img.shields.io/discord/528117503952551936.svg?style=flat-square&logo=discord)](https://discord.gg/gvTj5sh9Mp)
-[![GitHub issues](https://img.shields.io/github/issues/opentibiabr/canary)](https://github.com/opentibiabr/canary/issues)
-[![GitHub pull request](https://img.shields.io/github/issues-pr/opentibiabr/canary)](https://github.com/opentibiabr/canary/pulls)
-[![Contributors](https://img.shields.io/github/contributors/opentibiabr/canary.svg?style=flat-square)](https://github.com/opentibiabr/canary/graphs/contributors)
-[![GitHub](https://img.shields.io/github/license/opentibiabr/canary)](https://github.com/opentibiabr/canary/blob/master/LICENSE)
-
-![GitHub repo size](https://img.shields.io/github/repo-size/opentibiabr/canary)
-
-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=opentibiabr_canary&metric=alert_status)](https://sonarcloud.io/dashboard?id=opentibiabr_canary)
-
-## Builds
-
[![Build - Ubuntu](https://github.com/opentibiabr/canary/actions/workflows/build-ubuntu.yml/badge.svg)](https://github.com/opentibiabr/canary/actions/workflows/build-ubuntu.yml)
[![Build - Windows - CMake](https://github.com/opentibiabr/canary/actions/workflows/build-windows-cmake.yml/badge.svg)](https://github.com/opentibiabr/canary/actions/workflows/build-windows-cmake.yml)
[![Build - Windows - Solution](https://github.com/opentibiabr/canary/actions/workflows/build-windows-solution.yml/badge.svg)](https://github.com/opentibiabr/canary/actions/workflows/build-windows-solution.yml)
+[![Build - Docker](https://github.com/opentibiabr/canary/actions/workflows/build-docker.yml/badge.svg)](https://github.com/opentibiabr/canary/actions/workflows/build-docker.yml)
+[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=opentibiabr_canary&metric=alert_status)](https://sonarcloud.io/dashboard?id=opentibiabr_canary)
+![GitHub repo size](https://img.shields.io/github/repo-size/opentibiabr/canary)
+[![GitHub](https://img.shields.io/github/license/opentibiabr/canary)](https://github.com/opentibiabr/canary/blob/main/LICENSE)
-## Docker
-
-`docker pull opentibiabr/canary:latest`
-[![Automation](https://img.shields.io/docker/cloud/automated/opentibiabr/canary)](https://hub.docker.com/r/opentibiabr/canary)
-[![Image Size](https://img.shields.io/docker/image-size/opentibiabr/canary)](https://hub.docker.com/r/opentibiabr/canary/tags?page=1&ordering=last_updated)
-![Pulls](https://img.shields.io/docker/pulls/opentibiabr/canary)
-[![Build](https://img.shields.io/docker/cloud/build/opentibiabr/canary)](https://hub.docker.com/r/opentibiabr/canary/builds)
-
-## Project
-
-OpenTibiaBR - Canary is a free and open-source MMORPG server emulator written in C++.
-
-It is a fork of the [OTServBR-Global](https://github.com/opentibiabr/otservbr-global) project. You can see the
-repository history in the [releases](https://github.com/opentibiabr/otservbr-global/releases/).
-
-This project was created with the intention of being a base as clean as possible, to work as an MMORPG engine and not
-necessarily linked to Tibia Global, although it will also work. The OpenTibiaBR - Global was adapted to work with the
-source of the Canary, so that it will be the first repository to use this engine.
-
-To connect to the server and to take a stable experience, you can
-use [mehah's otclient](https://github.com/mehah/otclient)
+OpenTibiaBR - Canary is a free and open-source MMORPG server emulator written in C++. It is a fork of the [OTServBR-Global](https://github.com/opentibiabr/otservbr-global) project. To connect to the server and to take a stable experience, you can use [mehah's otclient](https://github.com/mehah/otclient)
or [tibia client](https://github.com/dudantas/tibia-client/releases/latest) and if you want to edit something, check
-our [customized tools](https://docs.opentibiabr.com/opentibiabr/downloads/tools).
-
-If you want edit the map, use the [own remere's map editor](https://github.com/opentibiabr/remeres-map-editor/).
+our [customized tools](https://docs.opentibiabr.com/opentibiabr/downloads/tools). If you want to edit the map, use our own [remere's map editor](https://github.com/opentibiabr/remeres-map-editor/).
-You are subject to our code of conduct, read
-at [this link](https://github.com/opentibiabr/canary/blob/master/CODE_OF_CONDUCT.md).
-
-### Getting **Started**
+## Getting Started
* [Gitbook](https://docs.opentibiabr.com/opentibiabr/projects/canary).
* [Wiki](https://github.com/opentibiabr/canary/wiki).
-### Issues
+## Support
+
+If you need help, please visit our [discord](https://discord.gg/gvTj5sh9Mp). Our issue tracker is not a support forum, and using it as one will result in your issue being closed.
-We use the [issue tracker on GitHub](https://github.com/opentibiabr/canary/issues). Keep in mind that everyone who is
-watching the repository gets notified by e-mail when there is an activity, so be thoughtful and avoid writing comments
-that aren't meant for an issue (e.g. "+1"). If you'd like for an issue to be fixed faster, you should either fix it
-yourself and submit a pull request, or place a bounty on the issue.
+## Contributing
-### Pull requests
+Here are some ways you can contribute:
-Before [creating a pull request](https://github.com/opentibiabr/canary/pulls) please keep in mind:
+* [Issue Tracker](https://github.com/opentibiabr/canary/issues/new/choose).
+* [Pull Request](https://github.com/opentibiabr/canary/pulls).
-* Do not send Pull Request changing the map, as we can't review the changes it's better to use
- our [Discord](https://discord.gg/gvTj5sh9Mp) to talk about or send the map changes to the responsible for updating it.
-* Focus on fixing only one thing, mixing too much things on the same Pull Request make it harder to review, harder to
- test and if we need to revert the change it will remove other things together.
-* Follow the project indentation, if your editor support you can use the [editorconfig](https://editorconfig.org/) to
- automatic configure the indentation.
-* There are people that doesn't play the game on the official server, so explain your changes to help understand what
- are you changing and why.
-* Avoid opening a Pull Request to just update one line of an xml file.
+You are subject to our code of conduct, read at [this link](https://github.com/opentibiabr/canary/blob/main/CODE_OF_CONDUCT.md).
-### Special Thanks
+## Special Thanks
-* our partners
-* our crew (majesty, gpedro, eduardo dantas, foot)
-* [our contributors](https://github.com/opentibiabr/canary/graphs/contributors)
-* [fear lucien](https://github.com/FearLucien)
-* [cjaker](https://github.com/Eternal-Scripts)
-* [slavidodo](https://github.com/slavidodo)
-* [mignari and our awesome tools](https://github.com/ottools)
-* [mattyx14/otxserver](https://github.com/mattyx14/otxserver) and contributors
-* [otland/forgottenserver](https://github.com/otland/forgottenserver) and contributors
-* [saiyansking/optimized_forgottenserver](https://github.com/SaiyansKing/optimized_forgottenserver) and contributors
-* if we forget someone, we apologize by forgot you. but you know, **forgot**tenserver.
+- Our contributors ([Canary](https://github.com/opentibiabr/canary/graphs/contributors) | [OTServBR-Global](https://github.com/opentibiabr/otservbr-global/graphs/contributors)).
-### **Sponsors**
+## Sponsors
-See our [donate page](https://docs.opentibiabr.com/home/donate)
+See our [donate page](https://docs.opentibiabr.com/home/donate).
## Project supported by JetBrains
@@ -98,6 +48,6 @@ other open-source initiatives.
-### Partners
+## Partners
[![Supported by OTServ Brasil](https://raw.githubusercontent.com/otbr/otserv-brasil/main/otbr.png)](https://forums.otserv.com.br)
diff --git a/data-canary/scripts/creaturescripts/player_death.lua b/data-canary/scripts/creaturescripts/player_death.lua
index 5ac2e70ff99..c30e2e98d99 100644
--- a/data-canary/scripts/creaturescripts/player_death.lua
+++ b/data-canary/scripts/creaturescripts/player_death.lua
@@ -42,6 +42,12 @@ function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustifi
mostDamageName = "field item"
end
+ player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE)
+
+ if mostDamageKiller and mostDamageKiller:isPlayer() and killer ~= mostDamageKiller then
+ mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL)
+ end
+
local playerGuid = player:getGuid()
db.query(
"INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES ("
@@ -83,6 +89,7 @@ function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustifi
end
if byPlayer == 1 then
+ killer:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL)
local targetGuild = player:getGuild()
targetGuild = targetGuild and targetGuild:getId() or 0
if targetGuild ~= 0 then
diff --git a/data-otservbr-global/monster/undeads/iks_ahpututu.lua b/data-otservbr-global/monster/undeads/iks_ahpututu.lua
index 06e8d061228..d1f44cd61d3 100644
--- a/data-otservbr-global/monster/undeads/iks_ahpututu.lua
+++ b/data-otservbr-global/monster/undeads/iks_ahpututu.lua
@@ -15,7 +15,7 @@ monster.outfit = {
monster.raceId = 2349
monster.Bestiary = {
- class = "Iks",
+ class = "Undead",
race = BESTY_RACE_UNDEAD,
toKill = 5,
FirstUnlock = 1,
diff --git a/data-otservbr-global/monster/undeads/iks_aucar.lua b/data-otservbr-global/monster/undeads/iks_aucar.lua
index 26bfc867d9b..efd711a01bd 100644
--- a/data-otservbr-global/monster/undeads/iks_aucar.lua
+++ b/data-otservbr-global/monster/undeads/iks_aucar.lua
@@ -15,7 +15,7 @@ monster.outfit = {
monster.raceId = 2344
monster.Bestiary = {
- class = "Iks",
+ class = "Undead",
race = BESTY_RACE_UNDEAD,
toKill = 1000,
FirstUnlock = 50,
diff --git a/data-otservbr-global/monster/undeads/iks_chuka.lua b/data-otservbr-global/monster/undeads/iks_chuka.lua
index 85f0cd77c3f..514011c9bd8 100644
--- a/data-otservbr-global/monster/undeads/iks_chuka.lua
+++ b/data-otservbr-global/monster/undeads/iks_chuka.lua
@@ -15,7 +15,7 @@ monster.outfit = {
monster.raceId = 2345
monster.Bestiary = {
- class = "Iks",
+ class = "Undead",
race = BESTY_RACE_UNDEAD,
toKill = 1000,
FirstUnlock = 50,
diff --git a/data-otservbr-global/monster/undeads/iks_churrascan.lua b/data-otservbr-global/monster/undeads/iks_churrascan.lua
index 704e40a53fa..fd44b36f620 100644
--- a/data-otservbr-global/monster/undeads/iks_churrascan.lua
+++ b/data-otservbr-global/monster/undeads/iks_churrascan.lua
@@ -15,7 +15,7 @@ monster.outfit = {
monster.raceId = 2350
monster.Bestiary = {
- class = "Iks",
+ class = "Undead",
race = BESTY_RACE_UNDEAD,
toKill = 1000,
FirstUnlock = 50,
diff --git a/data-otservbr-global/monster/undeads/iks_pututu.lua b/data-otservbr-global/monster/undeads/iks_pututu.lua
index 460aa7eb511..bbf16a43d16 100644
--- a/data-otservbr-global/monster/undeads/iks_pututu.lua
+++ b/data-otservbr-global/monster/undeads/iks_pututu.lua
@@ -15,7 +15,7 @@ monster.outfit = {
monster.raceId = 2343
monster.Bestiary = {
- class = "Iks",
+ class = "Undead",
race = BESTY_RACE_UNDEAD,
toKill = 1000,
FirstUnlock = 50,
diff --git a/data-otservbr-global/scripts/actions/rookgaard/rapier_quest.lua b/data-otservbr-global/scripts/actions/rookgaard/rapier_quest.lua
index 20e6697d2f7..5d09dcadf86 100644
--- a/data-otservbr-global/scripts/actions/rookgaard/rapier_quest.lua
+++ b/data-otservbr-global/scripts/actions/rookgaard/rapier_quest.lua
@@ -8,6 +8,7 @@ function rapierQuest.onUse(player, item, fromPosition, target, toPosition, isHot
player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have found a rapier.")
player:addItem(rewardId, 1)
player:questKV("rapier"):set("completed", true)
+ player:takeScreenshot(SCREENSHOT_TYPE_TREASUREFOUND)
return true
end
diff --git a/data-otservbr-global/scripts/creaturescripts/others/login_events.lua b/data-otservbr-global/scripts/creaturescripts/others/login_events.lua
index f88095e4756..2223f095f04 100644
--- a/data-otservbr-global/scripts/creaturescripts/others/login_events.lua
+++ b/data-otservbr-global/scripts/creaturescripts/others/login_events.lua
@@ -1,10 +1,7 @@
local loginEvents = CreatureEvent("LoginEvents")
function loginEvents.onLogin(player)
local events = {
- "AdvanceSave",
"RookgaardAdvance",
- "FamiliarLogin",
- "FamiliarAdvance",
--Quests
--Cults Of Tibia Quest
"HealthPillar",
diff --git a/data-otservbr-global/scripts/creaturescripts/others/player_death.lua b/data-otservbr-global/scripts/creaturescripts/others/player_death.lua
index b848235ebae..b4756addfee 100644
--- a/data-otservbr-global/scripts/creaturescripts/others/player_death.lua
+++ b/data-otservbr-global/scripts/creaturescripts/others/player_death.lua
@@ -44,6 +44,12 @@ function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustifi
mostDamageName = "field item"
end
+ player:takeScreenshot(byPlayer and SCREENSHOT_TYPE_DEATHPVP or SCREENSHOT_TYPE_DEATHPVE)
+
+ if mostDamageKiller and mostDamageKiller:isPlayer() then
+ mostDamageKiller:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL)
+ end
+
local playerGuid = player:getGuid()
db.query(
"INSERT INTO `player_deaths` (`player_id`, `time`, `level`, `killed_by`, `is_player`, `mostdamage_by`, `mostdamage_is_player`, `unjustified`, `mostdamage_unjustified`) VALUES ("
@@ -83,6 +89,7 @@ function playerDeath.onDeath(player, corpse, killer, mostDamageKiller, unjustifi
end
if byPlayer == 1 then
+ killer:takeScreenshot(SCREENSHOT_TYPE_PLAYERKILL)
local targetGuild = player:getGuild()
local targetGuildId = targetGuild and targetGuild:getId() or 0
if targetGuildId ~= 0 then
diff --git a/data/libs/functions/revscriptsys.lua b/data/libs/functions/revscriptsys.lua
index 84b8275e3ce..515522d6443 100644
--- a/data/libs/functions/revscriptsys.lua
+++ b/data/libs/functions/revscriptsys.lua
@@ -233,6 +233,10 @@ do
self:type("periodchange")
self:onPeriodChange(value)
return
+ elseif key == "onSave" then
+ self:type("save")
+ self:onSave(value)
+ return
end
rawset(self, key, value)
end
diff --git a/data/modules/scripts/blessings/blessings.lua b/data/modules/scripts/blessings/blessings.lua
index adfa364e7e1..e061501a330 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/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua
index f000c4079af..ed17c456c0d 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 322a7749c93..433abf34492 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,
@@ -399,6 +399,17 @@ function parseRequestStoreOffers(playerId, msg)
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)
local player = Player(playerId)
local id = msg:getU32()
@@ -450,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)
@@ -466,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)
@@ -504,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]" })
@@ -523,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
@@ -1827,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/scripts/creaturescripts/familiar/on_login.lua b/data/scripts/creaturescripts/familiar/on_login.lua
index e264040dbf9..dd4a1928e77 100644
--- a/data/scripts/creaturescripts/familiar/on_login.lua
+++ b/data/scripts/creaturescripts/familiar/on_login.lua
@@ -5,6 +5,7 @@ function familiarOnLogin.onLogin(player)
return false
end
+ player:registerEvent("FamiliarAdvance")
local vocation = FAMILIAR_ID[player:getVocation():getBaseId()]
local familiarName
diff --git a/data/scripts/creaturescripts/monster/boss_lever_death.lua b/data/scripts/creaturescripts/monster/boss_lever_death.lua
index a2c58d1e43b..4e035271623 100644
--- a/data/scripts/creaturescripts/monster/boss_lever_death.lua
+++ b/data/scripts/creaturescripts/monster/boss_lever_death.lua
@@ -30,6 +30,9 @@ function onBossDeath.onDeath(creature)
zn:removePlayers()
end, bossLever.timeAfterKill * 1000, zone)
end
+ onDeathForDamagingPlayers(creature, function(creature, player)
+ player:takeScreenshot(SCREENSHOT_TYPE_BOSSDEFEATED)
+ end)
return true
end
diff --git a/data/scripts/creaturescripts/player/login.lua b/data/scripts/creaturescripts/player/login.lua
index fbe4f9e63b6..41676066975 100644
--- a/data/scripts/creaturescripts/player/login.lua
+++ b/data/scripts/creaturescripts/player/login.lua
@@ -173,6 +173,7 @@ function playerLoginGlobal.onLogin(player)
player:registerEvent("PlayerDeath")
player:registerEvent("DropLoot")
player:registerEvent("BossParticipation")
+ player:registerEvent("UpdatePlayerOnAdvancedLevel")
return true
end
diff --git a/data/scripts/eventcallbacks/player/on_look.lua b/data/scripts/eventcallbacks/player/on_look.lua
index 90d3089052d..022aebbcc36 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 4aeec3dde80..b6d0412d391 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/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt
index af6ba129eac..c87a45a0aa0 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/combat/combat.cpp b/src/creatures/combat/combat.cpp
index 87f7500d0a9..91ff712de4c 100644
--- a/src/creatures/combat/combat.cpp
+++ b/src/creatures/combat/combat.cpp
@@ -108,7 +108,7 @@ CombatDamage Combat::getCombatDamage(std::shared_ptr creature, std::sh
return damage;
}
-void Combat::getCombatArea(const Position ¢erPos, const Position &targetPos, const std::unique_ptr &area, std::forward_list> &list) {
+void Combat::getCombatArea(const Position ¢erPos, const Position &targetPos, const std::unique_ptr &area, std::vector> &list) {
if (targetPos.z >= MAP_MAX_LAYERS) {
return;
}
@@ -116,7 +116,7 @@ void Combat::getCombatArea(const Position ¢erPos, const Position &targetPos,
if (area) {
area->getList(centerPos, targetPos, list);
} else {
- list.push_front(g_game().map.getOrCreateTile(targetPos));
+ list.emplace_back(g_game().map.getOrCreateTile(targetPos));
}
}
@@ -1119,7 +1119,7 @@ bool Combat::doCombat(std::shared_ptr caster, const Position &position
}
void Combat::CombatFunc(std::shared_ptr caster, const Position &origin, const Position &pos, const std::unique_ptr &area, const CombatParams ¶ms, CombatFunction func, CombatDamage* data) {
- std::forward_list> tileList;
+ std::vector> tileList;
if (caster) {
getCombatArea(caster->getPosition(), pos, area, tileList);
@@ -1827,26 +1827,29 @@ AreaCombat::AreaCombat(const AreaCombat &rhs) {
}
}
-void AreaCombat::getList(const Position ¢erPos, const Position &targetPos, std::forward_list> &list) const {
+void AreaCombat::getList(const Position ¢erPos, const Position &targetPos, std::vector> &list) const {
const std::unique_ptr &area = getArea(centerPos, targetPos);
if (!area) {
return;
}
- uint32_t centerY, centerX;
+ uint32_t centerY;
+ uint32_t centerX;
area->getCenter(centerY, centerX);
+ const uint32_t rows = area->getRows();
+ const uint32_t cols = area->getCols();
+ list.reserve(rows * cols);
+
Position tmpPos(targetPos.x - centerX, targetPos.y - centerY, targetPos.z);
- uint32_t cols = area->getCols();
- for (uint32_t y = 0, rows = area->getRows(); y < rows; ++y) {
- for (uint32_t x = 0; x < cols; ++x) {
- if (area->getValue(y, x) != 0 && g_game().isSightClear(targetPos, tmpPos, true)) {
- list.push_front(g_game().map.getOrCreateTile(tmpPos));
+ for (uint32_t y = 0; y < rows; ++y, ++tmpPos.y, tmpPos.x -= cols) {
+ for (uint32_t x = 0; x < cols; ++x, ++tmpPos.x) {
+ if (area->getValue(y, x) != 0) {
+ if (g_game().isSightClear(targetPos, tmpPos, true)) {
+ list.emplace_back(g_game().map.getOrCreateTile(tmpPos));
+ }
}
- tmpPos.x++;
}
- tmpPos.x -= cols;
- tmpPos.y++;
}
}
diff --git a/src/creatures/combat/combat.hpp b/src/creatures/combat/combat.hpp
index 93b02502f25..304a709135d 100644
--- a/src/creatures/combat/combat.hpp
+++ b/src/creatures/combat/combat.hpp
@@ -84,7 +84,7 @@ class ChainPickerCallback final : public CallBack {
};
struct CombatParams {
- std::forward_list> conditionList;
+ std::vector> conditionList;
std::unique_ptr valueCallback;
std::unique_ptr tileCallback;
@@ -218,7 +218,7 @@ class AreaCombat {
// non-assignable
AreaCombat &operator=(const AreaCombat &) = delete;
- void getList(const Position ¢erPos, const Position &targetPos, std::forward_list> &list) const;
+ void getList(const Position ¢erPos, const Position &targetPos, std::vector> &list) const;
void setupArea(const std::list &list, uint32_t rows);
void setupArea(int32_t length, int32_t spread);
@@ -290,7 +290,7 @@ class Combat {
static void doCombatDispel(std::shared_ptr caster, std::shared_ptr target, const CombatParams ¶ms);
static void doCombatDispel(std::shared_ptr caster, const Position &position, const std::unique_ptr &area, const CombatParams ¶ms);
- static void getCombatArea(const Position ¢erPos, const Position &targetPos, const std::unique_ptr &area, std::forward_list> &list);
+ static void getCombatArea(const Position ¢erPos, const Position &targetPos, const std::unique_ptr &area, std::vector> &list);
static bool isInPvpZone(std::shared_ptr attacker, std::shared_ptr target);
static bool isProtected(std::shared_ptr attacker, std::shared_ptr target);
@@ -320,7 +320,7 @@ class Combat {
return area != nullptr;
}
void addCondition(const std::shared_ptr condition) {
- params.conditionList.emplace_front(condition);
+ params.conditionList.emplace_back(condition);
}
void setPlayerCombatValues(formulaType_t formulaType, double mina, double minb, double maxa, double maxb);
void postCombatEffects(std::shared_ptr caster, const Position &origin, const Position &pos) const {
diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp
index c68ab4f351b..e3474ab2436 100644
--- a/src/creatures/creature.cpp
+++ b/src/creatures/creature.cpp
@@ -1050,6 +1050,10 @@ void Creature::goToFollowCreature_async(std::function &&onComplete) {
pathfinderRunning.store(true);
g_dispatcher().asyncEvent([self = getCreature()] {
+ if (!self || self->isRemoved()) {
+ return;
+ }
+
self->goToFollowCreature();
self->pathfinderRunning.store(false);
});
diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp
index 968b90d8cae..ca8f4584775 100644
--- a/src/creatures/monsters/spawns/spawn_monster.cpp
+++ b/src/creatures/monsters/spawns/spawn_monster.cpp
@@ -61,8 +61,7 @@ bool SpawnsMonster::loadFromXML(const std::string &filemonstername) {
continue;
}
- spawnMonsterList.emplace_front(centerPos, radius);
- SpawnMonster &spawnMonster = spawnMonsterList.front();
+ SpawnMonster &spawnMonster = spawnMonsterList.emplace_back(centerPos, radius);
for (auto childMonsterNode : spawnMonsterNode.children()) {
if (strcasecmp(childMonsterNode.name(), "monster") == 0) {
diff --git a/src/creatures/monsters/spawns/spawn_monster.hpp b/src/creatures/monsters/spawns/spawn_monster.hpp
index 35b856d23d4..81a08973cb7 100644
--- a/src/creatures/monsters/spawns/spawn_monster.hpp
+++ b/src/creatures/monsters/spawns/spawn_monster.hpp
@@ -36,6 +36,25 @@ class SpawnMonster {
SpawnMonster(const SpawnMonster &) = delete;
SpawnMonster &operator=(const SpawnMonster &) = delete;
+ // moveable
+ SpawnMonster(SpawnMonster &&rhs) noexcept :
+ spawnMonsterMap(std::move(rhs.spawnMonsterMap)),
+ spawnedMonsterMap(std::move(rhs.spawnedMonsterMap)),
+ checkSpawnMonsterEvent(rhs.checkSpawnMonsterEvent), centerPos(rhs.centerPos), radius(rhs.radius), interval(rhs.interval) { }
+
+ SpawnMonster &operator=(SpawnMonster &&rhs) noexcept {
+ if (this != &rhs) {
+ spawnMonsterMap = std::move(rhs.spawnMonsterMap);
+ spawnedMonsterMap = std::move(rhs.spawnedMonsterMap);
+
+ checkSpawnMonsterEvent = rhs.checkSpawnMonsterEvent;
+ centerPos = rhs.centerPos;
+ radius = rhs.radius;
+ interval = rhs.interval;
+ }
+ return *this;
+ }
+
bool addMonster(const std::string &name, const Position &pos, Direction dir, uint32_t interval, uint32_t weight = 1);
void removeMonster(std::shared_ptr monster);
void removeMonsters();
@@ -83,10 +102,6 @@ class SpawnsMonster {
bool loadFromXML(const std::string &filemonstername);
void startup();
void clear();
- SpawnMonster &addSpawnMonster(const Position &pos, int32_t radius) {
- spawnMonsterList.emplace_front(pos, radius);
- return spawnMonsterList.front();
- }
bool isStarted() const {
return started;
@@ -94,12 +109,12 @@ class SpawnsMonster {
bool isLoaded() const {
return loaded;
}
- std::forward_list &getspawnMonsterList() {
+ std::vector &getspawnMonsterList() {
return spawnMonsterList;
}
private:
- std::forward_list spawnMonsterList;
+ std::vector spawnMonsterList;
std::string filemonstername;
bool loaded = false;
bool started = false;
diff --git a/src/creatures/npcs/spawns/spawn_npc.cpp b/src/creatures/npcs/spawns/spawn_npc.cpp
index a8be411dc6c..694822c2386 100644
--- a/src/creatures/npcs/spawns/spawn_npc.cpp
+++ b/src/creatures/npcs/spawns/spawn_npc.cpp
@@ -57,7 +57,7 @@ bool SpawnsNpc::loadFromXml(const std::string &fileNpcName) {
continue;
}
- const auto &spawnNpc = spawnNpcList.emplace_front(std::make_shared(centerPos, radius));
+ const auto &spawnNpc = spawnNpcList.emplace_back(std::make_shared(centerPos, radius));
for (auto childNode : spawnNode.children()) {
if (strcasecmp(childNode.name(), "npc") == 0) {
diff --git a/src/creatures/npcs/spawns/spawn_npc.hpp b/src/creatures/npcs/spawns/spawn_npc.hpp
index 49eb3bc6f2b..b0d0ab8622a 100644
--- a/src/creatures/npcs/spawns/spawn_npc.hpp
+++ b/src/creatures/npcs/spawns/spawn_npc.hpp
@@ -94,12 +94,12 @@ class SpawnsNpc {
return fileName = std::move(setName);
}
- std::forward_list> &getSpawnNpcList() {
+ std::vector> &getSpawnNpcList() {
return spawnNpcList;
}
private:
- std::forward_list> spawnNpcList;
+ std::vector> spawnNpcList;
std::string fileName;
bool loaded = false;
bool started = false;
diff --git a/src/creatures/players/achievement/player_achievement.cpp b/src/creatures/players/achievement/player_achievement.cpp
index 69d1d7ab1fa..cd0735ab54c 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;
}
@@ -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 d1073a9bf1e..e0c027e5808 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_cyclopedia.cpp b/src/creatures/players/cyclopedia/player_cyclopedia.cpp
new file mode 100644
index 00000000000..abbc920d322
--- /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 00000000000..32c446cc368
--- /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 a6b44f3d3c4..7c348cbf79d 100644
--- a/src/creatures/players/cyclopedia/player_title.cpp
+++ b/src/creatures/players/cyclopedia/player_title.cpp
@@ -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/player.cpp b/src/creatures/players/player.cpp
index a9320ffba5f..2660c039110 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);
}
@@ -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()) {
@@ -708,6 +696,11 @@ void Player::addSkillAdvance(skills_t skill, uint64_t count) {
std::ostringstream ss;
ss << "You advanced to " << getSkillName(skill) << " level " << skills[skill].level << '.';
sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
+ if (skill == SKILL_LEVEL) {
+ sendTakeScreenshot(SCREENSHOT_TYPE_LEVELUP);
+ } else {
+ sendTakeScreenshot(SCREENSHOT_TYPE_SKILLUP);
+ }
g_creatureEvents().playerAdvance(static_self_cast(), skill, (skills[skill].level - 1), skills[skill].level);
@@ -2337,8 +2330,10 @@ void Player::addManaSpent(uint64_t amount) {
std::ostringstream ss;
ss << "You advanced to magic level " << magLevel << '.';
sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
+ sendTakeScreenshot(SCREENSHOT_TYPE_SKILLUP);
g_creatureEvents().playerAdvance(static_self_cast(), SKILL_MAGLEVEL, magLevel - 1, magLevel);
+ sendTakeScreenshot(SCREENSHOT_TYPE_SKILLUP);
sendUpdateStats = true;
currReqMana = nextReqMana;
@@ -2476,6 +2471,7 @@ void Player::addExperience(std::shared_ptr target, uint64_t exp, bool
std::ostringstream ss;
ss << "You advanced from Level " << prevLevel << " to Level " << level << '.';
sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
+ sendTakeScreenshot(SCREENSHOT_TYPE_LEVELUP);
}
if (nextLevelExp > currLevelExp) {
@@ -5293,12 +5289,12 @@ double Player::getLostPercent() const {
void Player::learnInstantSpell(const std::string &spellName) {
if (!hasLearnedInstantSpell(spellName)) {
- learnedInstantSpellList.push_front(spellName);
+ learnedInstantSpellList.emplace_back(spellName);
}
}
void Player::forgetInstantSpell(const std::string &spellName) {
- learnedInstantSpellList.remove(spellName);
+ std::erase(learnedInstantSpellList, spellName);
}
bool Player::hasLearnedInstantSpell(const std::string &spellName) const {
@@ -5704,12 +5700,12 @@ bool Player::addPartyInvitation(std::shared_ptr newParty) {
return false;
}
- invitePartyList.push_front(newParty);
+ invitePartyList.emplace_back(newParty);
return true;
}
void Player::removePartyInvitation(std::shared_ptr remParty) {
- invitePartyList.remove(remParty);
+ std::erase(invitePartyList, remParty);
}
void Player::clearPartyInvitations() {
@@ -6022,6 +6018,7 @@ bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) {
std::ostringstream ss;
ss << "You advanced to magic level " << magLevel << '.';
sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
+ sendTakeScreenshot(SCREENSHOT_TYPE_SKILLUP);
}
uint8_t newPercent;
@@ -6078,6 +6075,11 @@ bool Player::addOfflineTrainingTries(skills_t skill, uint64_t tries) {
std::ostringstream ss;
ss << "You advanced to " << getSkillName(skill) << " level " << skills[skill].level << '.';
sendTextMessage(MESSAGE_EVENT_ADVANCE, ss.str());
+ if (skill == SKILL_LEVEL) {
+ sendTakeScreenshot(SCREENSHOT_TYPE_LEVELUP);
+ } else {
+ sendTakeScreenshot(SCREENSHOT_TYPE_SKILLUP);
+ }
}
uint8_t newPercent;
@@ -6122,7 +6124,7 @@ bool Player::hasModalWindowOpen(uint32_t modalWindowId) const {
}
void Player::onModalWindowHandled(uint32_t modalWindowId) {
- modalWindows.remove(modalWindowId);
+ std::erase(modalWindows, modalWindowId);
}
void Player::sendModalWindow(const ModalWindow &modalWindow) {
@@ -6130,7 +6132,7 @@ void Player::sendModalWindow(const ModalWindow &modalWindow) {
return;
}
- modalWindows.push_front(modalWindow.id);
+ modalWindows.emplace_back(modalWindow.id);
client->sendModalWindow(modalWindow);
}
@@ -6249,8 +6251,10 @@ size_t Player::getMaxDepotItems() const {
return g_configManager().getNumber(FREE_DEPOT_LIMIT, __FUNCTION__);
}
-std::forward_list> Player::getMuteConditions() const {
- std::forward_list> muteConditions;
+std::vector> Player::getMuteConditions() const {
+ std::vector> muteConditions;
+ muteConditions.reserve(conditions.size());
+
for (const std::shared_ptr &condition : conditions) {
if (condition->getTicks() <= 0) {
continue;
@@ -6261,7 +6265,7 @@ std::forward_list> Player::getMuteConditions() const
continue;
}
- muteConditions.push_front(condition);
+ muteConditions.emplace_back(condition);
}
return muteConditions;
}
@@ -6619,7 +6623,7 @@ 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)) {
@@ -8026,6 +8030,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) {
diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp
index 04b6a139a02..bfaad14cf76 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;
@@ -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);
}
@@ -1727,6 +1730,12 @@ class Player final : public Creature, public Cylinder, public Bankable {
}
}
+ void sendTakeScreenshot(Screenshot_t screenshotType) {
+ if (client) {
+ client->sendTakeScreenshot(screenshotType);
+ }
+ }
+
void onThink(uint32_t interval) override;
void postAddNotification(std::shared_ptr thing, std::shared_ptr oldParent, int32_t index, CylinderLink_t link = LINK_OWNER) override;
@@ -2599,9 +2608,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;
@@ -2631,6 +2637,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;
@@ -2654,7 +2664,7 @@ class Player final : public Creature, public Cylinder, public Bankable {
static uint32_t playerFirstID;
static uint32_t playerLastID;
- std::forward_list> getMuteConditions() const;
+ std::vector> getMuteConditions() const;
void checkTradeState(std::shared_ptr- item);
bool hasCapacity(std::shared_ptr
- item, uint32_t count) const;
@@ -2750,11 +2760,11 @@ class Player final : public Creature, public Cylinder, public Bankable {
GuildWarVector guildWarVector;
- std::forward_list> invitePartyList;
- std::forward_list modalWindows;
- std::forward_list learnedInstantSpellList;
+ std::vector> invitePartyList;
+ std::vector modalWindows;
+ std::vector learnedInstantSpellList;
// TODO: This variable is only temporarily used when logging in, get rid of it somehow.
- std::forward_list> storedConditionList;
+ std::vector> storedConditionList;
std::unordered_set> m_bestiaryMonsterTracker;
std::unordered_set> m_bosstiaryMonsterTracker;
@@ -2952,6 +2962,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()));
@@ -3028,12 +3040,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;
@@ -3059,4 +3073,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/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp
index c6e1b7032c0..295e573984f 100644
--- a/src/enums/player_cyclopedia.hpp
+++ b/src/enums/player_cyclopedia.hpp
@@ -37,6 +37,16 @@ enum CyclopediaTitle_t : uint8_t {
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,
diff --git a/src/game/game.cpp b/src/game/game.cpp
index bae7975a295..2348d384569 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;
@@ -553,7 +593,8 @@ void Game::setGameState(GameState_t newState) {
}
case GAME_STATE_SHUTDOWN: {
- g_globalEvents().execute(GLOBALEVENT_SHUTDOWN);
+ g_globalEvents().save();
+ g_globalEvents().shutdown();
// kick all players that are still online
auto it = players.begin();
@@ -571,6 +612,8 @@ void Game::setGameState(GameState_t newState) {
}
case GAME_STATE_CLOSED: {
+ g_globalEvents().save();
+
/* kick all players without the CanAlwaysLogin flag */
auto it = players.begin();
while (it != players.end()) {
@@ -6895,14 +6938,6 @@ int32_t Game::applyHealthChange(CombatDamage &damage, std::shared_ptr
}
}
}
-
- if (damage.primary.value >= targetHealth) {
- damage.primary.value = targetHealth;
- damage.secondary.value = 0;
- } else if (damage.secondary.value) {
- damage.secondary.value = std::min(damage.secondary.value, targetHealth - damage.primary.value);
- }
-
return targetHealth;
}
@@ -7260,19 +7295,12 @@ bool Game::combatChangeHealth(std::shared_ptr attacker, std::shared_pt
}
}
- auto targetHealth = applyHealthChange(damage, target);
- if (damage.primary.value >= targetHealth) {
- damage.primary.value = targetHealth;
- damage.secondary.value = 0;
- } else if (damage.secondary.value) {
- damage.secondary.value = std::min(damage.secondary.value, targetHealth - damage.primary.value);
- }
-
// Apply Custom PvP Damage (must be placed here to avoid recursive calls)
if (attackerPlayer && targetPlayer) {
applyPvPDamage(damage, attackerPlayer, targetPlayer);
}
+ auto targetHealth = target->getHealth();
realDamage = damage.primary.value + damage.secondary.value;
if (realDamage == 0) {
return true;
@@ -7284,6 +7312,14 @@ bool Game::combatChangeHealth(std::shared_ptr attacker, std::shared_pt
}
}
+ targetHealth = applyHealthChange(damage, target);
+ if (damage.primary.value >= targetHealth) {
+ damage.primary.value = targetHealth;
+ damage.secondary.value = 0;
+ } else if (damage.secondary.value) {
+ damage.secondary.value = std::min(damage.secondary.value, targetHealth - damage.primary.value);
+ }
+
target->drainHealth(attacker, realDamage);
if (realDamage > 0 && targetMonster) {
if (attackerPlayer && attackerPlayer->getPlayer()) {
@@ -8300,121 +8336,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;
@@ -10735,3 +10662,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 2de00410e5e..0537ee030a7 100644
--- a/src/game/game.hpp
+++ b/src/game/game.hpp
@@ -732,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;
@@ -742,6 +748,12 @@ class Game {
std::vector m_highscoreCategories;
std::unordered_map m_highscoreCategoriesNames;
+ std::map m_blessingNames;
+
+ std::unordered_map m_summaryCategories;
+ std::unordered_map m_hirelingSkills;
+ std::unordered_map m_hirelingOutfits;
+
std::map forgeMonsterEventIds;
std::unordered_set fiendishMonsters;
std::unordered_set influencedMonsters;
diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp
index a6ce6e7eaa8..8b165bc725d 100644
--- a/src/game/game_definitions.hpp
+++ b/src/game/game_definitions.hpp
@@ -102,6 +102,17 @@ enum class HighscoreCategories_t : uint8_t {
BOSS_POINTS = 14,
};
+enum Blessings_t : uint8_t {
+ TWIST_OF_FATE = 1,
+ WISDOM_OF_SOLITUDE = 2,
+ SPARK_OF_THE_PHOENIX = 3,
+ FIRE_OF_THE_SUNS = 4,
+ SPIRITUAL_SHIELDING = 5,
+ EMBRACE_OF_TIBIA = 6,
+ BLOOD_OF_THE_MOUNTAIN = 7,
+ HEARTH_OF_THE_MOUNTAIN = 8,
+};
+
enum HighscoreType_t : uint8_t {
HIGHSCORE_GETENTRIES = 0,
HIGHSCORE_OURRANK = 1
diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp
index 8f7cf461fdf..764d4641469 100644
--- a/src/io/functions/iologindata_load_player.cpp
+++ b/src/io/functions/iologindata_load_player.cpp
@@ -182,6 +182,8 @@ bool IOLoginDataLoad::loadPlayerFirst(std::shared_ptr player, DBResult_p
player->setManaShield(result->getNumber("manashield"));
player->setMaxManaShield(result->getNumber("max_manashield"));
+
+ player->setMarriageSpouse(result->getNumber("marriage_spouse"));
return true;
}
@@ -215,9 +217,7 @@ void IOLoginDataLoad::loadPlayerBlessings(std::shared_ptr player, DBResu
}
for (int i = 1; i <= 8; i++) {
- std::ostringstream ss;
- ss << "blessings" << i;
- player->addBlessing(static_cast(i), static_cast(result->getNumber(ss.str())));
+ player->addBlessing(static_cast(i), static_cast(result->getNumber(fmt::format("blessings{}", i))));
}
}
@@ -235,7 +235,7 @@ void IOLoginDataLoad::loadPlayerConditions(std::shared_ptr player, DBRes
auto condition = Condition::createCondition(propStream);
while (condition) {
if (condition->unserialize(propStream)) {
- player->storedConditionList.push_front(condition);
+ player->storedConditionList.emplace_back(condition);
}
condition = Condition::createCondition(propStream);
}
@@ -465,7 +465,7 @@ void IOLoginDataLoad::loadPlayerInstantSpellList(std::shared_ptr player,
query << "SELECT `player_id`, `name` FROM `player_spells` WHERE `player_id` = " << player->getGUID();
if ((result = db.storeQuery(query.str()))) {
do {
- player->learnedInstantSpellList.emplace_front(result->getString("name"));
+ player->learnedInstantSpellList.emplace_back(result->getString("name"));
} while (result->next());
}
}
@@ -913,6 +913,7 @@ void IOLoginDataLoad::loadPlayerInitializeSystem(std::shared_ptr player)
player->achiev()->loadUnlockedAchievements();
player->badge()->checkAndUpdateNewBadges();
player->title()->checkAndUpdateNewTitles();
+ player->cyclopedia()->loadSummaryData();
player->initializePrey();
player->initializeTaskHunting();
diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp
index 122ade42c72..132cead6412 100644
--- a/src/io/iologindata.cpp
+++ b/src/io/iologindata.cpp
@@ -349,15 +349,14 @@ bool IOLoginData::hasBiddedOnHouse(uint32_t guid) {
return db.storeQuery(query.str()).get() != nullptr;
}
-std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) {
- std::forward_list entries;
-
+std::vector IOLoginData::getVIPEntries(uint32_t accountId) {
std::string query = fmt::format("SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {}", accountId);
+ std::vector entries;
- DBResult_ptr result = Database::getInstance().storeQuery(query);
- if (result) {
+ if (const auto &result = Database::getInstance().storeQuery(query)) {
+ entries.reserve(result->countResults());
do {
- entries.emplace_front(
+ entries.emplace_back(
result->getNumber("player_id"),
result->getString("name"),
result->getString("description"),
@@ -366,6 +365,7 @@ std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) {
);
} while (result->next());
}
+
return entries;
}
@@ -388,15 +388,16 @@ void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) {
g_database().executeQuery(query);
}
-std::forward_list IOLoginData::getVIPGroupEntries(uint32_t accountId, uint32_t guid) {
- std::forward_list entries;
-
+std::vector IOLoginData::getVIPGroupEntries(uint32_t accountId, uint32_t guid) {
std::string query = fmt::format("SELECT `id`, `name`, `customizable` FROM `account_vipgroups` WHERE `account_id` = {}", accountId);
- DBResult_ptr result = g_database().storeQuery(query);
- if (result) {
+ std::vector entries;
+
+ if (const auto &result = g_database().storeQuery(query)) {
+ entries.reserve(result->countResults());
+
do {
- entries.emplace_front(
+ entries.emplace_back(
result->getNumber("id"),
result->getString("name"),
result->getNumber("customizable") == 0 ? false : true
diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp
index be414739837..1451cf89778 100644
--- a/src/io/iologindata.hpp
+++ b/src/io/iologindata.hpp
@@ -31,12 +31,12 @@ class IOLoginData {
static void increaseBankBalance(uint32_t guid, uint64_t bankBalance);
static bool hasBiddedOnHouse(uint32_t guid);
- static std::forward_list getVIPEntries(uint32_t accountId);
+ static std::vector getVIPEntries(uint32_t accountId);
static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify);
static void editVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify);
static void removeVIPEntry(uint32_t accountId, uint32_t guid);
- static std::forward_list getVIPGroupEntries(uint32_t accountId, uint32_t guid);
+ static std::vector getVIPGroupEntries(uint32_t accountId, uint32_t guid);
static void addVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable);
static void editVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable);
static void removeVIPGroupEntry(uint8_t groupId, uint32_t accountId);
diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp
index 475a210a760..e2a367c4ec4 100644
--- a/src/io/iomapserialize.cpp
+++ b/src/io/iomapserialize.cpp
@@ -241,19 +241,21 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, std::shared_ptr til
return;
}
- std::forward_list> items;
+ std::vector> items;
+ items.reserve(32);
+
uint16_t count = 0;
for (auto &item : *tileItems) {
if (item->getID() == ITEM_BATHTUB_FILLED_NOTMOVABLE) {
std::shared_ptr
- tub = Item::CreateItem(ITEM_BATHTUB_FILLED);
- items.push_front(tub);
+ items.emplace_back(tub);
++count;
continue;
} else if (!item->isSavedToHouses()) {
continue;
}
- items.push_front(item);
+ items.emplace_back(item);
++count;
}
diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp
index ec5c5102794..83ecd091850 100644
--- a/src/lua/functions/core/game/game_functions.cpp
+++ b/src/lua/functions/core/game/game_functions.cpp
@@ -28,6 +28,7 @@
#include "lua/callbacks/events_callbacks.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 "map/spectators.hpp"
diff --git a/src/lua/functions/core/game/global_functions.cpp b/src/lua/functions/core/game/global_functions.cpp
index af89c1854f7..6b5175b06cf 100644
--- a/src/lua/functions/core/game/global_functions.cpp
+++ b/src/lua/functions/core/game/global_functions.cpp
@@ -16,6 +16,7 @@
#include "lua/functions/core/game/global_functions.hpp"
#include "lua/scripts/lua_environment.hpp"
#include "lua/scripts/script_environment.hpp"
+#include "lua/global/globalevent.hpp"
#include "server/network/protocol/protocolstatus.hpp"
#include "creatures/players/wheel/player_wheel.hpp"
#include "lua/global/lua_timer_event_descr.hpp"
@@ -217,7 +218,7 @@ int GlobalFunctions::luaGetWorldLight(lua_State* L) {
int GlobalFunctions::luaGetWorldUpTime(lua_State* L) {
// getWorldUpTime()
- uint64_t uptime = (OTSYS_TIME() - ProtocolStatus::start) / 1000;
+ uint64_t uptime = (OTSYS_TIME(true) - ProtocolStatus::start) / 1000;
lua_pushnumber(L, uptime);
return 1;
}
@@ -443,7 +444,7 @@ int GlobalFunctions::luaDoAreaCombatCondition(lua_State* L) {
if (area || areaId == 0) {
CombatParams params;
params.impactEffect = getNumber(L, 5);
- params.conditionList.emplace_front(condition);
+ params.conditionList.emplace_back(condition);
Combat::doCombatCondition(creature, getPosition(L, 2), area, params);
pushBoolean(L, true);
} else {
@@ -478,7 +479,7 @@ int GlobalFunctions::luaDoTargetCombatCondition(lua_State* L) {
CombatParams params;
params.impactEffect = getNumber(L, 4);
- params.conditionList.emplace_front(condition->clone());
+ params.conditionList.emplace_back(condition->clone());
Combat::doCombatCondition(creature, target, params);
pushBoolean(L, true);
return 1;
@@ -706,6 +707,7 @@ int GlobalFunctions::luaStopEvent(lua_State* L) {
}
int GlobalFunctions::luaSaveServer(lua_State* L) {
+ g_globalEvents().save();
g_saveManager().scheduleAll();
pushBoolean(L, true);
return 1;
diff --git a/src/lua/functions/core/game/lua_enums.cpp b/src/lua/functions/core/game/lua_enums.cpp
index 2e3813e0769..d0be72158d6 100644
--- a/src/lua/functions/core/game/lua_enums.cpp
+++ b/src/lua/functions/core/game/lua_enums.cpp
@@ -214,6 +214,20 @@ void LuaEnums::initOthersEnums(lua_State* L) {
registerEnum(L, WEAPON_WAND);
registerEnum(L, WEAPON_AMMO);
registerEnum(L, WEAPON_MISSILE);
+
+ registerEnum(L, SCREENSHOT_TYPE_NONE);
+ registerEnum(L, SCREENSHOT_TYPE_ACHIEVEMENT);
+ registerEnum(L, SCREENSHOT_TYPE_BESTIARYENTRYCOMPLETED);
+ registerEnum(L, SCREENSHOT_TYPE_BESTIARYENTRYUNLOCKED);
+ registerEnum(L, SCREENSHOT_TYPE_BOSSDEFEATED);
+ registerEnum(L, SCREENSHOT_TYPE_DEATHPVE);
+ registerEnum(L, SCREENSHOT_TYPE_DEATHPVP);
+ registerEnum(L, SCREENSHOT_TYPE_LEVELUP);
+ registerEnum(L, SCREENSHOT_TYPE_PLAYERKILLASSIST);
+ registerEnum(L, SCREENSHOT_TYPE_PLAYERKILL);
+ registerEnum(L, SCREENSHOT_TYPE_PLAYERATTACKING);
+ registerEnum(L, SCREENSHOT_TYPE_TREASUREFOUND);
+ registerEnum(L, SCREENSHOT_TYPE_SKILLUP);
}
void LuaEnums::initAccountEnums(lua_State* L) {
diff --git a/src/lua/functions/creatures/monster/monster_functions.cpp b/src/lua/functions/creatures/monster/monster_functions.cpp
index 273f6af5c99..5578477d6ba 100644
--- a/src/lua/functions/creatures/monster/monster_functions.cpp
+++ b/src/lua/functions/creatures/monster/monster_functions.cpp
@@ -363,8 +363,7 @@ int MonsterFunctions::luaMonsterSetSpawnPosition(lua_State* L) {
const Position &pos = monster->getPosition();
monster->setMasterPos(pos);
- g_game().map.spawnsMonster.getspawnMonsterList().emplace_front(pos, 5);
- SpawnMonster &spawnMonster = g_game().map.spawnsMonster.getspawnMonsterList().front();
+ SpawnMonster &spawnMonster = g_game().map.spawnsMonster.getspawnMonsterList().emplace_back(pos, 5);
uint32_t interval = getNumber(L, 2, 90) * 1000 * 100 / std::max((uint32_t)1, (g_configManager().getNumber(RATE_SPAWN, __FUNCTION__) * eventschedule));
spawnMonster.addMonster(monster->mType->typeName, pos, DIRECTION_NORTH, static_cast(interval));
spawnMonster.startSpawnMonsterCheck();
diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp
index 9cc5434abd5..609ecbe95e0 100644
--- a/src/lua/functions/creatures/player/player_functions.cpp
+++ b/src/lua/functions/creatures/player/player_functions.cpp
@@ -16,6 +16,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 "game/game.hpp"
#include "io/iologindata.hpp"
@@ -2732,7 +2733,7 @@ int PlayerFunctions::luaPlayerRemoveBlessing(lua_State* L) {
}
int PlayerFunctions::luaPlayerGetBlessingCount(lua_State* L) {
- // player:getBlessingCount(index)
+ // player:getBlessingCount(index[, storeCount = false])
std::shared_ptr player = getUserdataShared(L, 1);
uint8_t index = getNumber(L, 2);
if (index == 0) {
@@ -2740,7 +2741,7 @@ int PlayerFunctions::luaPlayerGetBlessingCount(lua_State* L) {
}
if (player) {
- lua_pushnumber(L, player->getBlessingCount(index));
+ lua_pushnumber(L, player->getBlessingCount(index, getBoolean(L, 3, false)));
} else {
lua_pushnil(L);
}
@@ -4222,6 +4223,7 @@ int PlayerFunctions::luaPlayerAddAchievement(lua_State* L) {
achievementId = g_game().getAchievementByName(getString(L, 2)).id;
}
+ player->sendTakeScreenshot(SCREENSHOT_TYPE_ACHIEVEMENT);
pushBoolean(L, player->achiev()->add(achievementId, getBoolean(L, 3, true)));
return 1;
}
@@ -4355,3 +4357,39 @@ int PlayerFunctions::luaPlayerSetCurrentTitle(lua_State* L) {
pushBoolean(L, true);
return 1;
}
+
+int PlayerFunctions::luaPlayerCreateTransactionSummary(lua_State* L) {
+ // player:createTransactionSummary(type, amount[, id = 0])
+ const auto &player = getUserdataShared(L, 1);
+ if (!player) {
+ reportErrorFunc(getErrorDesc(LUA_ERROR_PLAYER_NOT_FOUND));
+ return 1;
+ }
+
+ auto type = getNumber(L, 2, 0);
+ if (type == 0) {
+ reportErrorFunc(getErrorDesc(LUA_ERROR_VARIANT_NOT_FOUND));
+ return 1;
+ }
+
+ auto amount = getNumber(L, 3, 1);
+ auto id = getString(L, 4, "");
+
+ player->cyclopedia()->updateStoreSummary(type, amount, id);
+ pushBoolean(L, true);
+ return 1;
+}
+
+int PlayerFunctions::luaPlayerTakeScreenshot(lua_State* L) {
+ // player:takeScreenshot(screenshotType)
+ const auto &player = getUserdataShared(L, 1);
+ if (!player) {
+ lua_pushnil(L);
+ return 1;
+ }
+
+ auto screenshotType = getNumber(L, 2);
+ player->sendTakeScreenshot(screenshotType);
+ pushBoolean(L, true);
+ return 1;
+}
diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp
index 4d89a86d27f..3912753a1ca 100644
--- a/src/lua/functions/creatures/player/player_functions.hpp
+++ b/src/lua/functions/creatures/player/player_functions.hpp
@@ -372,6 +372,11 @@ class PlayerFunctions final : LuaScriptInterface {
registerMethod(L, "Player", "getTitles", PlayerFunctions::luaPlayerGetTitles);
registerMethod(L, "Player", "setCurrentTitle", PlayerFunctions::luaPlayerSetCurrentTitle);
+ // Store Summary
+ registerMethod(L, "Player", "createTransactionSummary", PlayerFunctions::luaPlayerCreateTransactionSummary);
+
+ registerMethod(L, "Player", "takeScreenshot", PlayerFunctions::luaPlayerTakeScreenshot);
+
GroupFunctions::init(L);
GuildFunctions::init(L);
MountFunctions::init(L);
@@ -732,5 +737,9 @@ class PlayerFunctions final : LuaScriptInterface {
static int luaPlayerGetTitles(lua_State* L);
static int luaPlayerSetCurrentTitle(lua_State* L);
+ static int luaPlayerCreateTransactionSummary(lua_State* L);
+
+ static int luaPlayerTakeScreenshot(lua_State* L);
+
friend class CreatureFunctions;
};
diff --git a/src/lua/functions/events/global_event_functions.cpp b/src/lua/functions/events/global_event_functions.cpp
index ea6a5f7df03..129a466e97c 100644
--- a/src/lua/functions/events/global_event_functions.cpp
+++ b/src/lua/functions/events/global_event_functions.cpp
@@ -40,6 +40,8 @@ int GlobalEventFunctions::luaGlobalEventType(lua_State* L) {
global->setEventType(GLOBALEVENT_PERIODCHANGE);
} else if (tmpStr == "onthink") {
global->setEventType(GLOBALEVENT_ON_THINK);
+ } else if (tmpStr == "save") {
+ global->setEventType(GLOBALEVENT_SAVE);
} else {
g_logger().error("[GlobalEventFunctions::luaGlobalEventType] - "
"Invalid type for global event: {}");
diff --git a/src/lua/functions/events/global_event_functions.hpp b/src/lua/functions/events/global_event_functions.hpp
index 6c988a61ca1..e04072a24e4 100644
--- a/src/lua/functions/events/global_event_functions.hpp
+++ b/src/lua/functions/events/global_event_functions.hpp
@@ -25,6 +25,7 @@ class GlobalEventFunctions final : LuaScriptInterface {
registerMethod(L, "GlobalEvent", "onShutdown", GlobalEventFunctions::luaGlobalEventOnCallback);
registerMethod(L, "GlobalEvent", "onRecord", GlobalEventFunctions::luaGlobalEventOnCallback);
registerMethod(L, "GlobalEvent", "onPeriodChange", GlobalEventFunctions::luaGlobalEventOnCallback);
+ registerMethod(L, "GlobalEvent", "onSave", GlobalEventFunctions::luaGlobalEventOnCallback);
}
private:
diff --git a/src/lua/global/globalevent.cpp b/src/lua/global/globalevent.cpp
index 04420431def..32fa1a01ab1 100644
--- a/src/lua/global/globalevent.cpp
+++ b/src/lua/global/globalevent.cpp
@@ -66,6 +66,14 @@ void GlobalEvents::startup() const {
execute(GLOBALEVENT_STARTUP);
}
+void GlobalEvents::shutdown() const {
+ execute(GLOBALEVENT_SHUTDOWN);
+}
+
+void GlobalEvents::save() const {
+ execute(GLOBALEVENT_SAVE);
+}
+
void GlobalEvents::timer() {
time_t now = time(nullptr);
@@ -165,7 +173,8 @@ GlobalEventMap GlobalEvents::getEventMap(GlobalEvent_t type) {
case GLOBALEVENT_PERIODCHANGE:
case GLOBALEVENT_STARTUP:
case GLOBALEVENT_SHUTDOWN:
- case GLOBALEVENT_RECORD: {
+ case GLOBALEVENT_RECORD:
+ case GLOBALEVENT_SAVE: {
GlobalEventMap retMap;
for (const auto &it : serverMap) {
if (it.second->getEventType() == type) {
@@ -196,6 +205,8 @@ std::string GlobalEvent::getScriptTypeName() const {
return "onPeriodChange";
case GLOBALEVENT_ON_THINK:
return "onThink";
+ case GLOBALEVENT_SAVE:
+ return "onSave";
default:
g_logger().error("[GlobalEvent::getScriptTypeName] - Invalid event type");
return std::string();
diff --git a/src/lua/global/globalevent.hpp b/src/lua/global/globalevent.hpp
index 004c6cb5f8c..128a743b3fb 100644
--- a/src/lua/global/globalevent.hpp
+++ b/src/lua/global/globalevent.hpp
@@ -29,6 +29,8 @@ class GlobalEvents final : public Scripts {
}
void startup() const;
+ void shutdown() const;
+ void save() const;
void timer();
void think();
diff --git a/src/lua/lua_definitions.hpp b/src/lua/lua_definitions.hpp
index 083871e237b..81add18e516 100644
--- a/src/lua/lua_definitions.hpp
+++ b/src/lua/lua_definitions.hpp
@@ -108,6 +108,7 @@ enum GlobalEvent_t {
GLOBALEVENT_RECORD,
GLOBALEVENT_PERIODCHANGE,
GLOBALEVENT_ON_THINK,
+ GLOBALEVENT_SAVE,
};
enum ModuleType_t {
diff --git a/src/map/map.cpp b/src/map/map.cpp
index 82629aeae05..7406ecffbdf 100644
--- a/src/map/map.cpp
+++ b/src/map/map.cpp
@@ -457,62 +457,159 @@ bool Map::canThrowObjectTo(const Position &fromPos, const Position &toPos, bool
return isSightClear(fromPos, toPos, false);
}
-bool Map::checkSightLine(const Position &fromPos, const Position &toPos) {
- if (fromPos == toPos) {
+bool Map::checkSightLine(Position start, Position destination) {
+ if (start.x == destination.x && start.y == destination.y) {
return true;
}
- Position start(fromPos.z > toPos.z ? toPos : fromPos);
- Position destination(fromPos.z > toPos.z ? fromPos : toPos);
+ int32_t distanceX = Position::getDistanceX(start, destination);
+ int32_t distanceY = Position::getDistanceY(start, destination);
- const int8_t mx = start.x < destination.x ? 1 : start.x == destination.x ? 0
- : -1;
- const int8_t my = start.y < destination.y ? 1 : start.y == destination.y ? 0
- : -1;
+ if (start.y == destination.y) {
+ // Horizontal line
+ const uint16_t delta = start.x < destination.x ? 0x0001 : 0xFFFF;
+ while (--distanceX > 0) {
+ start.x += delta;
- int32_t A = Position::getOffsetY(destination, start);
- int32_t B = Position::getOffsetX(start, destination);
- int32_t C = -(A * destination.x + B * destination.y);
+ const auto &tile = getTile(start.x, start.y, start.z);
+ if (tile && tile->hasFlag(TILESTATE_BLOCKPROJECTILE)) {
+ return false;
+ }
+ }
+ } else if (start.x == destination.x) {
+ // Vertical line
+ const uint16_t delta = start.y < destination.y ? 0x0001 : 0xFFFF;
+ while (--distanceY > 0) {
+ start.y += delta;
+
+ const auto &tile = getTile(start.x, start.y, start.z);
+ if (tile && tile->hasFlag(TILESTATE_BLOCKPROJECTILE)) {
+ return false;
+ }
+ }
+ } else {
+ // Xiaolin Wu's line algorithm - https://en.wikipedia.org/wiki/Xiaolin_Wu%27s_line_algorithm
+ // based on Michael Abrash's implementation - https://www.amazon.com/gp/product/1576101746/102-5103244-8168911
+ uint16_t eAdj;
+ uint16_t eAcc = 0;
+ uint16_t deltaX = 0x0001;
+ uint16_t deltaY = 0x0001;
+
+ if (distanceY > distanceX) {
+ eAdj = (static_cast(distanceX) << 16) / static_cast(distanceY);
+
+ if (start.y > destination.y) {
+ std::swap(start.x, destination.x);
+ std::swap(start.y, destination.y);
+ }
+ if (start.x > destination.x) {
+ deltaX = 0xFFFF;
+ eAcc -= eAdj;
+ }
- while (start.x != destination.x || start.y != destination.y) {
- int32_t move_hor = std::abs(A * (start.x + mx) + B * (start.y) + C);
- int32_t move_ver = std::abs(A * (start.x) + B * (start.y + my) + C);
- int32_t move_cross = std::abs(A * (start.x + mx) + B * (start.y + my) + C);
+ while (--distanceY > 0) {
+ uint16_t xIncrease = 0;
+ const uint16_t eAccTemp = eAcc;
+ eAcc += eAdj;
+ if (eAcc <= eAccTemp) {
+ xIncrease = deltaX;
+ }
- if (start.y != destination.y && (start.x == destination.x || move_hor > move_ver || move_hor > move_cross)) {
- start.y += my;
- }
+ const auto &tile = getTile(start.x + xIncrease, start.y + deltaY, start.z);
+ if (tile && tile->hasFlag(TILESTATE_BLOCKPROJECTILE)) {
+ if (Position::areInRange<1, 1>(start, destination)) {
+ return true;
+ }
+ return false;
+ }
- if (start.x != destination.x && (start.y == destination.y || move_ver > move_hor || move_ver > move_cross)) {
- start.x += mx;
- }
+ start.x += xIncrease;
+ start.y += deltaY;
+ }
+ } else {
+ eAdj = (static_cast(distanceY) << 16) / static_cast(distanceX);
- const std::shared_ptr tile = getTile(start.x, start.y, start.z);
- if (tile && tile->hasProperty(CONST_PROP_BLOCKPROJECTILE)) {
- return false;
- }
- }
+ if (start.x > destination.x) {
+ std::swap(start.x, destination.x);
+ std::swap(start.y, destination.y);
+ }
+ if (start.y > destination.y) {
+ deltaY = 0xFFFF;
+ eAcc -= eAdj;
+ }
- // now we need to perform a jump between floors to see if everything is clear (literally)
- while (start.z != destination.z) {
- const std::shared_ptr tile = getTile(start.x, start.y, start.z);
- if (tile && tile->getThingCount() > 0) {
- return false;
- }
+ while (--distanceX > 0) {
+ uint16_t yIncrease = 0;
+ const uint16_t eAccTemp = eAcc;
+ eAcc += eAdj;
+ if (eAcc <= eAccTemp) {
+ yIncrease = deltaY;
+ }
- start.z++;
- }
+ const auto &tile = getTile(start.x + deltaX, start.y + yIncrease, start.z);
+ if (tile && tile->hasFlag(TILESTATE_BLOCKPROJECTILE)) {
+ if (Position::areInRange<1, 1>(start, destination)) {
+ return true;
+ }
+ return false;
+ }
+ start.x += deltaX;
+ start.y += yIncrease;
+ }
+ }
+ }
return true;
}
bool Map::isSightClear(const Position &fromPos, const Position &toPos, bool floorCheck) {
+ // Check if this sight line should be even possible
if (floorCheck && fromPos.z != toPos.z) {
return false;
}
- // Cast two converging rays and see if either yields a result.
- return checkSightLine(fromPos, toPos) || checkSightLine(toPos, fromPos);
+ // Check if we even need to perform line checking
+ if (fromPos.z == toPos.z && (Position::areInRange<1, 1>(fromPos, toPos) || (!floorCheck && fromPos.z == 0))) {
+ return true;
+ }
+
+ // We can only throw one floor up
+ if (fromPos.z > toPos.z && Position::getDistanceZ(fromPos, toPos) > 1) {
+ return false;
+ }
+
+ // Perform check for current floor
+ const bool sightClear = checkSightLine(fromPos, toPos);
+ if (floorCheck || (fromPos.z == toPos.z && sightClear)) {
+ return sightClear;
+ }
+
+ uint8_t startZ;
+ if (sightClear && (fromPos.z < toPos.z || fromPos.z == toPos.z)) {
+ startZ = fromPos.z;
+ } else {
+ // Check if we can throw above obstacle
+ const auto &tile = getTile(fromPos.x, fromPos.y, fromPos.z - 1);
+ if ((tile && (tile->getGround() || tile->hasFlag(TILESTATE_BLOCKPROJECTILE))) || !checkSightLine(Position(fromPos.x, fromPos.y, fromPos.z - 1), Position(toPos.x, toPos.y, toPos.z - 1))) {
+ return false;
+ }
+
+ // We can throw above obstacle
+ if (fromPos.z > toPos.z) {
+ return true;
+ }
+
+ startZ = fromPos.z - 1;
+ }
+
+ // now we need to perform a jump between floors to see if everything is clear (literally)
+ for (; startZ != toPos.z; ++startZ) {
+ const auto &tile = getTile(toPos.x, toPos.y, startZ);
+ if (tile && (tile->getGround() || tile->hasFlag(TILESTATE_BLOCKPROJECTILE))) {
+ return false;
+ }
+ }
+ return true;
}
std::shared_ptr Map::canWalkTo(const std::shared_ptr &creature, const Position &pos) {
diff --git a/src/map/map.hpp b/src/map/map.hpp
index e57328e12b3..bae07445156 100644
--- a/src/map/map.hpp
+++ b/src/map/map.hpp
@@ -119,7 +119,7 @@ class Map : public MapCache {
* \returns The result if there is no obstacles
*/
bool isSightClear(const Position &fromPos, const Position &toPos, bool floorCheck);
- bool checkSightLine(const Position &fromPos, const Position &toPos);
+ bool checkSightLine(Position start, Position destination);
std::shared_ptr canWalkTo(const std::shared_ptr &creature, const Position &pos);
diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp
index 1476f255135..f3a4bcef3ac 100644
--- a/src/server/network/protocol/protocolgame.cpp
+++ b/src/server/network/protocol/protocolgame.cpp
@@ -28,6 +28,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/grouping/familiars.hpp"
#include "server/network/protocol/protocolgame.hpp"
@@ -3456,7 +3457,7 @@ void ProtocolGame::sendCyclopediaCharacterGeneralStats() {
msg.add