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. JetBrains -### 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(player->getOfflineTrainingTime() / 60 / 1000); msg.add(player->getSpeed()); msg.add(player->getBaseSpeed()); - msg.add(player->getBonusCapacity()); + msg.add(player->getCapacity()); msg.add(player->getBaseCapacity()); msg.add(player->hasFlag(PlayerFlags_t::HasInfiniteCapacity) ? 1000000 : player->getFreeCapacity()); msg.addByte(8); @@ -3660,21 +3661,15 @@ void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t p NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTDEATHS); - msg.addByte(0x00); - - uint16_t totalPages = static_cast(std::ceil(static_cast(entries.size()) / pages)); - uint16_t currentPage = std::min(page, totalPages); - uint16_t firstObject = (currentPage - 1) * pages; - uint16_t finalObject = firstObject + pages; - - msg.add(currentPage); - msg.add(totalPages); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(page); msg.add(pages); - for (uint16_t i = firstObject; i < finalObject; i++) { - RecentDeathEntry entry = entries[i]; + msg.add(entries.size()); + for (const RecentDeathEntry &entry : entries) { msg.add(entry.timestamp); msg.addString(entry.cause, "ProtocolGame::sendCyclopediaCharacterRecentDeaths - entry.cause"); } + writeToOutputBuffer(msg); } @@ -3686,22 +3681,16 @@ void ProtocolGame::sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t NetworkMessage msg; msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_RECENTPVPKILLS); - msg.addByte(0x00); - - uint16_t totalPages = static_cast(std::ceil(static_cast(entries.size()) / pages)); - uint16_t currentPage = std::min(page, totalPages); - uint16_t firstObject = (currentPage - 1) * pages; - uint16_t finalObject = firstObject + pages; - - msg.add(currentPage); - msg.add(totalPages); + msg.addByte(0x00); // 0x00 Here means 'no error' + msg.add(page); msg.add(pages); - for (uint16_t i = firstObject; i < finalObject; i++) { - RecentPvPKillEntry entry = entries[i]; + msg.add(entries.size()); + for (const RecentPvPKillEntry &entry : entries) { msg.add(entry.timestamp); msg.addString(entry.description, "ProtocolGame::sendCyclopediaCharacterRecentPvPKills - entry.description"); msg.addByte(entry.status); } + writeToOutputBuffer(msg); } @@ -3959,17 +3948,72 @@ void ProtocolGame::sendCyclopediaCharacterStoreSummary() { msg.addByte(CYCLOPEDIA_CHARACTERINFO_STORESUMMARY); msg.addByte(0x00); // 0x00 Here means 'no error' msg.add(player->getXpBoostTime()); // Remaining Store Xp Boost Time - msg.add(0); // RemainingDailyRewardXpBoostTime + auto remaining = player->kv()->get("daily-reward-xp-boost"); + msg.add(remaining ? static_cast(remaining->getNumber()) : 0); // Remaining Daily Reward Xp Boost Time + + auto cyclopediaSummary = player->cyclopedia()->getSummary(); + + // getBlessingsObtained + auto blessingNames = g_game().getBlessingNames(); + msg.addByte(static_cast(blessingNames.size())); + for (const auto &bless : blessingNames) { + msg.addString(bless.second, "ProtocolGame::sendCyclopediaCharacterStoreSummary - blessing.name"); + uint8_t blessingIndex = bless.first - 1; + msg.addByte((blessingIndex < player->blessings.size()) ? static_cast(player->blessings[blessingIndex]) : 0); + } + + uint8_t preySlotsUnlocked = 0; + // Prey third slot unlocked + if (const auto &slotP = player->getPreySlotById(PreySlot_Three); + slotP && slotP->state != PreyDataState_Locked) { + preySlotsUnlocked++; + } + // Task hunting third slot unlocked + if (const auto &slotH = player->getTaskHuntingSlotById(PreySlot_Three); + slotH && slotH->state != PreyTaskDataState_Locked) { + preySlotsUnlocked++; + } + msg.addByte(preySlotsUnlocked); // getPreySlotById + getTaskHuntingSlotById + + msg.addByte(cyclopediaSummary.m_preyWildcards); // getPreyCardsObtained + msg.addByte(cyclopediaSummary.m_instantRewards); // getRewardCollectionObtained + msg.addByte(player->hasCharmExpansion() ? 0x01 : 0x00); + msg.addByte(cyclopediaSummary.m_hirelings); // getHirelingsObtained + + std::vector m_hSkills; + for (const auto &it : g_game().getHirelingSkills()) { + if (player->kv()->scoped("hireling-skills")->get(it.second)) { + m_hSkills.emplace_back(it.first); + g_logger().debug("skill id: {}, name: {}", it.first, it.second); + } + } + msg.addByte(m_hSkills.size()); + for (const auto &id : m_hSkills) { + msg.addByte(id - 1000); + } + + /*std::vector m_hOutfits; + for (const auto &it : g_game().getHirelingOutfits()) { + if (player->kv()->scoped("hireling-outfits")->get(it.second)) { + m_hOutfits.emplace_back(it.first); + g_logger().debug("outfit id: {}, name: {}", it.first, it.second); + } + } + msg.addByte(m_hOutfits.size()); + for (const auto &id : m_hOutfits) { + msg.addByte(0x01); // TODO need to get the correct id from hireling outfit + }*/ + msg.addByte(0x00); // hireling outfit size + + auto houseItems = player->cyclopedia()->getResult(static_cast(Summary_t::HOUSE_ITEMS)); + msg.add(houseItems.size()); + for (const auto &hItem_it : houseItems) { + const ItemType &it = Item::items[hItem_it.first]; + msg.add(it.id); // Item ID + msg.addString(it.name, "ProtocolGame::sendCyclopediaCharacterStoreSummary - houseItem.name"); + msg.addByte(hItem_it.second); + } - msg.addByte(0x00); // getBlessingsObtained - msg.addByte(0x00); // getTaskHuntingSlotById - msg.addByte(0x00); // getPreyCardsObtained - msg.addByte(0x00); // getRewardCollectionObtained - msg.addByte(0x00); // player->hasCharmExpansion() ? 0x01 : 0x00 - msg.addByte(0x00); // getHirelingsObtained - msg.addByte(0x00); // getHirelinsJobsObtained - msg.addByte(0x00); // getHirelinsOutfitsObtained - msg.add(0); // getHouseItemsObtained writeToOutputBuffer(msg); } @@ -4028,23 +4072,23 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { auto playerDescriptionPosition = msg.getBufferPosition(); msg.skipBytes(1); + // Player title + if (player->title()->getCurrentTitle() != 0) { + playerDescriptionSize++; + msg.addString("Character Title", "ProtocolGame::sendCyclopediaCharacterInspection - Title"); + msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->title()->getCurrentTitleName()"); + } + // Level description playerDescriptionSize++; msg.addString("Level", "ProtocolGame::sendCyclopediaCharacterInspection - Level"); + msg.addString(std::to_string(player->getLevel()), "ProtocolGame::sendCyclopediaCharacterInspection - std::to_string(player->getLevel())"); // Vocation description playerDescriptionSize++; - msg.addString(std::to_string(player->getLevel()), "ProtocolGame::sendCyclopediaCharacterInspection - std::to_string(player->getLevel())"); msg.addString("Vocation", "ProtocolGame::sendCyclopediaCharacterInspection - Vocation"); msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getVocation()->getVocName()"); - // Player title - if (player->title()->getCurrentTitle() != 0) { - playerDescriptionSize++; - msg.addString("Title", "ProtocolGame::sendCyclopediaCharacterInspection - Title"); - msg.addString(player->title()->getCurrentTitleName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->title()->getCurrentTitleName()"); - } - // Loyalty title if (!player->getLoyaltyTitle().empty()) { playerDescriptionSize++; @@ -4052,6 +4096,47 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getLoyaltyTitle()"); } + // Marriage description + if (const auto spouseId = player->getMarriageSpouse(); spouseId > 0) { + if (const auto &spouse = g_game().getPlayerByID(spouseId, true); spouse) { + playerDescriptionSize++; + msg.addString("Married to", "ProtocolGame::sendCyclopediaCharacterInspection - Married to"); + msg.addString(spouse->getName(), "ProtocolGame::sendCyclopediaCharacterInspection - spouse->getName()"); + } + } + + // Prey description + for (uint8_t slotId = PreySlot_First; slotId <= PreySlot_Last; slotId++) { + if (const auto &slot = player->getPreySlotById(static_cast(slotId)); + slot && slot->isOccupied()) { + playerDescriptionSize++; + std::string activePrey = fmt::format("Active Prey {}", slotId + 1); + msg.addString(activePrey, "ProtocolGame::sendCyclopediaCharacterInspection - active prey"); + + std::string desc; + if (auto mtype = g_monsters().getMonsterTypeByRaceId(slot->selectedRaceId)) { + desc.append(mtype->name); + } else { + desc.append("Unknown creature"); + } + + if (slot->bonus == PreyBonus_Damage) { + desc.append(" (Improved Damage +"); + } else if (slot->bonus == PreyBonus_Defense) { + desc.append(" (Improved Defense +"); + } else if (slot->bonus == PreyBonus_Experience) { + desc.append(" (Improved Experience +"); + } else if (slot->bonus == PreyBonus_Loot) { + desc.append(" (Improved Loot +"); + } + desc.append(fmt::format("{}%, remaining", slot->bonusPercentage)); + uint8_t hours = slot->bonusTimeLeft / 3600; + uint8_t minutes = (slot->bonusTimeLeft - (hours * 3600)) / 60; + desc.append(fmt::format("{}:{}{}h", hours, (minutes < 10 ? "0" : ""), minutes)); + msg.addString(desc, "ProtocolGame::sendCyclopediaCharacterInspection - prey description"); + } + } + // Outfit description playerDescriptionSize++; msg.addString("Outfit", "ProtocolGame::sendCyclopediaCharacterInspection - Outfit"); @@ -6610,7 +6695,7 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos sendVIPGroups(); - const std::forward_list &vipEntries = IOLoginData::getVIPEntries(player->getAccountId()); + const auto &vipEntries = IOLoginData::getVIPEntries(player->getAccountId()); if (player->isAccessPlayer()) { for (const VIPEntry &entry : vipEntries) { @@ -9071,3 +9156,14 @@ void ProtocolGame::sendHotkeyPreset() { writeToOutputBuffer(msg); } } + +void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType) { + if (screenshotType == SCREENSHOT_TYPE_NONE || oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0x75); + msg.addByte(screenshotType); + writeToOutputBuffer(msg); +} diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 24341f15e1e..8e99aa68a54 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -14,6 +14,7 @@ #include "creatures/creature.hpp" #include "enums/forge_conversion.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" +#include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" class NetworkMessage; @@ -515,6 +516,7 @@ class ProtocolGame final : public Protocol { void sendDoubleSoundEffect(const Position &pos, SoundEffect_t mainSoundId, SourceEffect_t mainSource, SoundEffect_t secondarySoundId, SourceEffect_t secondarySource); void sendHotkeyPreset(); + void sendTakeScreenshot(Screenshot_t screenshotType); void sendDisableLoginMusic(); uint8_t m_playerDeathTime = 0; diff --git a/src/server/network/protocol/protocolstatus.cpp b/src/server/network/protocol/protocolstatus.cpp index 1674b80a2b5..15a3f74be07 100644 --- a/src/server/network/protocol/protocolstatus.cpp +++ b/src/server/network/protocol/protocolstatus.cpp @@ -23,7 +23,7 @@ std::string ProtocolStatus::SERVER_VERSION = "3.0"; std::string ProtocolStatus::SERVER_DEVELOPERS = "OpenTibiaBR Organization"; std::map ProtocolStatus::ipConnectMap; -const uint64_t ProtocolStatus::start = OTSYS_TIME(); +const uint64_t ProtocolStatus::start = OTSYS_TIME(true); void ProtocolStatus::onRecvFirstMessage(NetworkMessage &msg) { uint32_t ip = getIP(); diff --git a/src/server/signals.cpp b/src/server/signals.cpp index c85b21312eb..626a918f677 100644 --- a/src/server/signals.cpp +++ b/src/server/signals.cpp @@ -15,6 +15,7 @@ #include "lib/thread/thread_pool.hpp" #include "lua/creature/events.hpp" #include "lua/scripts/lua_environment.hpp" +#include "lua/global/globalevent.hpp" #include "server/signals.hpp" Signals::Signals(asio::io_service &service) : @@ -92,6 +93,7 @@ void Signals::sigtermHandler() { void Signals::sigusr1Handler() { // Dispatcher thread g_logger().info("SIGUSR1 received, saving the game state..."); + g_globalEvents().save(); g_saveManager().scheduleAll(); } diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index 584d4a6b5b6..f89b2c2f217 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1515,7 +1515,10 @@ void UPDATE_OTSYS_TIME() { OTSYSTIME = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } -int64_t OTSYS_TIME() { +int64_t OTSYS_TIME(bool useTime) { + if (useTime) { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } return OTSYSTIME; } @@ -1834,6 +1837,31 @@ std::string getVerbForPronoun(PlayerPronoun_t pronoun, bool pastTense) { return pastTense ? "was" : "is"; } +std::string formatWithArticle(const std::string &value, bool withSpace) { + if (value.empty()) { + return ""; + } + + auto removeArticle = [](const std::string &str) -> std::string { + const std::string articles[] = { "a ", "an " }; + for (const auto &article : articles) { + if (str.size() > article.size() && std::equal(article.begin(), article.end(), str.begin(), [](char a, char b) { return std::tolower(a) == std::tolower(b); })) { + return str.substr(article.size()); + } + } + return str; + }; + + std::string modifiedValue = removeArticle(value); + if (modifiedValue.empty()) { + return ""; + } + + const char &character = std::tolower(modifiedValue.front()); + auto article = character == 'a' || character == 'e' || character == 'i' || character == 'o' || character == 'u' ? "an" : "a"; + return fmt::format("{}{} {}.", withSpace ? " " : "", article, modifiedValue); +} + std::vector split(const std::string &str, char delimiter /* = ','*/) { std::vector tokens; std::string token; diff --git a/src/utils/tools.hpp b/src/utils/tools.hpp index 25683bf22bb..a4426a066a8 100644 --- a/src/utils/tools.hpp +++ b/src/utils/tools.hpp @@ -147,7 +147,7 @@ bool isCaskItem(uint16_t itemId); std::string getObjectCategoryName(ObjectCategory_t category); bool isValidObjectCategory(ObjectCategory_t category); -int64_t OTSYS_TIME(); +int64_t OTSYS_TIME(bool useTime = false); void UPDATE_OTSYS_TIME(); SpellGroup_t stringToSpellGroup(const std::string &value); @@ -201,6 +201,8 @@ std::string getPlayerPossessivePronoun(PlayerPronoun_t pronoun, PlayerSex_t sex, std::string getPlayerReflexivePronoun(PlayerPronoun_t pronoun, PlayerSex_t sex, const std::string &name); std::string getVerbForPronoun(PlayerPronoun_t pronoun, bool pastTense = false); +std::string formatWithArticle(const std::string &value, bool withSpace = true); + std::string toKey(const std::string &str); static inline double quadraticPoly(double a, double b, double c, double x) { diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index af2c40ba533..be50fd35b9d 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -709,17 +709,6 @@ enum class PlayerFlags_t : uint8_t { FlagLast }; -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 BedItemPart_t : uint8_t { BED_NONE_PART, BED_PILLOW_PART, @@ -762,3 +751,19 @@ enum Concoction_t : uint16_t { DeathAmplification = 36741, PhysicalAmplification = 36742, }; + +enum Screenshot_t : uint8_t { + SCREENSHOT_TYPE_NONE = 0, + SCREENSHOT_TYPE_ACHIEVEMENT = 1, + SCREENSHOT_TYPE_BESTIARYENTRYCOMPLETED = 2, + SCREENSHOT_TYPE_BESTIARYENTRYUNLOCKED = 3, + SCREENSHOT_TYPE_BOSSDEFEATED = 4, + SCREENSHOT_TYPE_DEATHPVE = 5, + SCREENSHOT_TYPE_DEATHPVP = 6, + SCREENSHOT_TYPE_LEVELUP = 7, + SCREENSHOT_TYPE_PLAYERKILLASSIST = 8, + SCREENSHOT_TYPE_PLAYERKILL = 9, + SCREENSHOT_TYPE_PLAYERATTACKING = 10, + SCREENSHOT_TYPE_TREASUREFOUND = 11, + SCREENSHOT_TYPE_SKILLUP = 12 +}; diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 4cb91d1cb94..77ece957935 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -46,6 +46,7 @@ + @@ -261,6 +262,7 @@ +