From d0f6634f6ffe527bf46435c27a32e3c81d0ea930 Mon Sep 17 00:00:00 2001 From: dudantas Date: Mon, 9 Aug 2021 19:48:12 -0300 Subject: [PATCH 01/30] Gamestore in cpp --- data/XML/gamestore.xml | 503 ++ data/modules/modules.xml | 10 - data/modules/scripts/gamestore/gamestore.lua | 4318 ----------------- data/modules/scripts/gamestore/init.lua | 1846 ------- data/modules/scripts/gamestore/readme.md | 119 - src/config/config_definitions.hpp | 1 + src/config/configmanager.cpp | 1 + src/creatures/creatures_definitions.hpp | 12 + src/creatures/players/account/account.cpp | 96 +- src/creatures/players/account/account.hpp | 10 +- src/creatures/players/player.cpp | 82 +- src/creatures/players/player.h | 75 +- src/game/game.cpp | 590 +-- src/game/game.h | 16 +- src/game/game_definitions.hpp | 61 +- src/game/gamestore.cpp | 921 +++- src/game/gamestore.h | 63 - src/game/gamestore.hpp | 353 ++ src/io/iologindata.cpp | 2 +- .../functions/core/game/config_functions.hpp | 1 + .../creatures/player/player_functions.cpp | 40 +- src/otserv.cpp | 4 + src/server/network/protocol/protocolgame.cpp | 307 +- src/server/network/protocol/protocolgame.h | 17 - src/utils/tools.cpp | 13 +- src/utils/tools.h | 3 +- vcpkg.json | 22 - 27 files changed, 1847 insertions(+), 7639 deletions(-) create mode 100644 data/XML/gamestore.xml delete mode 100644 data/modules/scripts/gamestore/gamestore.lua delete mode 100644 data/modules/scripts/gamestore/init.lua delete mode 100644 data/modules/scripts/gamestore/readme.md delete mode 100644 src/game/gamestore.h create mode 100644 src/game/gamestore.hpp delete mode 100644 vcpkg.json diff --git a/data/XML/gamestore.xml b/data/XML/gamestore.xml new file mode 100644 index 00000000000..8999d9ea562 --- /dev/null +++ b/data/XML/gamestore.xml @@ -0,0 +1,503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/modules/modules.xml b/data/modules/modules.xml index ca01b803338..1152e0ec4b8 100644 --- a/data/modules/modules.xml +++ b/data/modules/modules.xml @@ -1,15 +1,5 @@ - - - - - - - - - - diff --git a/data/modules/scripts/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua deleted file mode 100644 index 91193cfc618..00000000000 --- a/data/modules/scripts/gamestore/gamestore.lua +++ /dev/null @@ -1,4318 +0,0 @@ ---[[ -Items have been updated so that if the offer type is not one of the types: OFFER_TYPE_OUTFIT, OFFER_TYPE_OUTFIT_ADDON, -OFFER_TYPE_MOUNT, OFFER_TYPE_NAMECHANGE, OFFER_TYPE_SEXCHANGE, OFFER_TYPE_PROMOTION, OFFER_TYPE_EXPBOOST, -OFFER_TYPE_PREYSLOT, OFFER_TYPE_PREYBONUS, OFFER_TYPE_TEMPLE, OFFER_TYPE_BLESSINGS, OFFER_TYPE_PREMIUM, -OFFER_TYPE_ALLBLESSINGS -]] - --- Parser -dofile('data/modules/scripts/gamestore/init.lua') --- Config - -HomeBanners = { - images = { "home/banner_riftwatcher.png" , "home/banner_runemaster.png" , "home/banner_podiumofrenown.png" }, - delay = 10 -} - -GameStore.Categories = { - { - --Premium Time - icons = { "Category_PremiumTime.png" }, - name = "Premium Time", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Premium_Time_30.png" }, - name = "30 Days of Premium Time", - price = 250, - id = 3030, - validUntil = 30, - description = "Enhance your gaming experience by gaining additional abilities and advantages:\n\n• access to Premium areas\n• use Tibia's transport system (ships, carpet)\n• more spells\n• rent houses\n• found guilds\n• offline training\n• larger depots\n• and many more\n\n{usablebyallicon} valid for all characters on this account\n{activated}", - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_90.png" }, - name = "90 Days of Premium Time", - price = 750, - id = 3090, - validUntil = 90, - description = "Enhance your gaming experience by gaining additional abilities and advantages:\n\n• access to Premium areas\n• use Tibia's transport system (ships, carpet)\n• more spells\n• rent houses\n• found guilds\n• offline training\n• larger depots\n• and many more\n\n{usablebyallicon} valid for all characters on this account\n{activated}", - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_180.png" }, - name = "180 Days of Premium Time", - price = 1500, - id = 3180, - validUntil = 180, - description = "Enhance your gaming experience by gaining additional abilities and advantages:\n\n• access to Premium areas\n• use Tibia's transport system (ships, carpet)\n• more spells\n• rent houses\n• found guilds\n• offline training\n• larger depots\n• and many more\n\n{usablebyallicon} valid for all characters on this account\n{activated}", - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_360.png" }, - name = "360 Days of Premium Time", - price = 3000, - id = 3360, - validUntil = 360, - description = "Enhance your gaming experience by gaining additional abilities and advantages:\n\n• access to Premium areas\n• use Tibia's transport system (ships, carpet)\n• more spells\n• rent houses\n• found guilds\n• offline training\n• larger depots\n• and many more\n\n{usablebyallicon} valid for all characters on this account\n{activated}", - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - }, - }, - { - icons = { "Category_Consumables.png" }, - name = "Consumables", - rookgaard = true, - subclasses = {"Blessings", "Casks", "Exercise Weapons", "Kegs", "Potions", "Runes"}, - }, - -- Blessings - { - icons = { "Category_Blessings.png" }, - name = "Blessings", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "All_PvE_Blessings.png" }, - name = "All Regular Blessings", - price = 130, - id = 11, - count = 1, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, - }, - { - icons = { "All_PvE_Blessings.png" }, - name = "All Regular Blessings", - price = 650, - id = 12, - count = 5, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, - }, - { - icons = { "Twist_of_Fate.png" }, - name = "Twist of Fate", - price = 8, - blessid = 1, - count = 1, - id = 3, - description = "Protects your character's regular blessings or an Amulet of Loss if you are unfortunate enough to die in a PvP fight.\n\n{character}\n{limit|5}\n{info} added directly to the Record of Blessings\n{info} does not work for characters with a red or black skull", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Twist_of_Fate.png" }, - name = "Twist of Fate", - price = 40, - blessid = 1, - count = 5, - description = "Protects your character's regular blessings or an Amulet of Loss if you are unfortunate enough to die in a PvP fight.\n\n{character}\n{limit|5}\n{info} added directly to the Record of Blessings\n{info} does not work for characters with a red or black skull", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Wisdom_of_Solitude.png" }, - name = "The Wisdom of Solitude", - price = 15, - blessid = 2, - count = 1, - id = 4, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Spark_of_the_Phoenix.png" }, - name = "The Spark of the Phoenix", - price = 20, - blessid = 3, - count = 1, - id = 5, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Fire_of_the_Suns.png" }, - name = "The Fire of the Suns", - price = 15, - blessid = 4, - count = 1, - id = 6, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Spiritual_Shielding.png" }, - name = "The Spiritual Shielding", - price = 15, - blessid = 5, - count = 1, - id = 7, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Embrace_of_Tibia.png" }, - name = "The Embrace of Tibia", - price = 15, - blessid = 6, - count = 1, - id = 8, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Heart_of_the_Mountain.png" }, - name = "Heart of the Mountain", - price = 25, - blessid = 7, - count = 1, - id = 9, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Blood_of_the_Mountain.png" }, - name = "Blood of the Mountain", - price = 25, - blessid = 8, - count = 1, - id = 10, - 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", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Death_Redemption.png" }, - name = "Death Redemption", - price = 260, - blessid = 10, - count = 1, - description = "Reduces the penalty of your character's most recent death.\n\n{character}\n{info} can only be used for the most recent death and only within 24 hours after this death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - }, - }, - -- Casks - { - icons = { "Category_Casks.png" }, - name = "Casks", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Health_Cask.png" }, - name = "Health Cask", - price = 5, - itemtype = 28555, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Strong_Health_Cask.png" }, - name = "Strong Health Cask", - price = 11, - itemtype = 28556, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Great_Health_Cask.png" }, - name = "Great Health Cask", - price = 22, - itemtype = 28557, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Health_Cask.png" }, - name = "Ultimate Health Cask", - price = 36, - itemtype = 28558, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Supreme_Health_Cask.png" }, - name = "Supreme Health Cask", - price = 59, - itemtype = 28559, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mana_Cask.png" }, - name = "Mana Cask", - price = 5, - itemtype = 28565, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Strong_Mana_Cask.png" }, - name = "Strong Mana Cask", - price = 9, - itemtype = 28566, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Great_Mana_Cask.png" }, - name = "Great Mana Cask", - price = 14, - itemtype = 28567, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Mana_Cask.png" }, - name = "Ultimate Mana Cask", - price = 42, - itemtype = 28568, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Great_Spirit_Cask.png" }, - name = "Great Spirit Cask", - price = 22, - itemtype = 28575, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points and mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Spirit_Cask.png" }, - name = "Ultimate Spirit Cask", - price = 42, - itemtype = 28576, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points and mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- Exercise Weapons - { - icons = { "Category_ExerciseWeapons.png" }, - name = "Exercise Weapons", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Exercise_Axe.png" }, - name = "Exercise Axe", - price = 25, - itemtype = 32385, - charges = 500, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Bow.png" }, - name = "Exercise Bow", - price = 25, - itemtype = 32387, - charges = 500, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Club.png" }, - name = "Exercise Club", - price = 25, - itemtype = 32386, - charges = 500, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Rod.png" }, - name = "Exercise Rod", - price = 25, - itemtype = 32388, - charges = 500, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Sword.png" }, - name = "Exercise Sword", - price = 25, - itemtype = 32384, - charges = 500, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Wand.png" }, - name = "Exercise Wand", - price = 25, - itemtype = 32389, - charges = 500, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Axe.png" }, - name = "Durable Exercise Axe", - price = 90, - itemtype = 40115, - charges = 1800, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Bow.png" }, - name = "Durable Exercise Bow", - price = 90, - itemtype = 40117, - charges = 1800, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Club.png" }, - name = "Durable Exercise Club", - price = 90, - itemtype = 40116, - charges = 1800, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Rod.png" }, - name = "Durable Exercise Rod", - price = 90, - itemtype = 40118, - charges = 1800, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Sword.png" }, - name = "Durable Exercise Sword", - price = 90, - itemtype = 40114, - charges = 1800, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Wand.png" }, - name = "Durable Exercise Wand", - price = 90, - itemtype = 40119, - charges = 1800, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Axe.png" }, - name = "Lasting Exercise Axe", - price = 720, - itemtype = 40121, - charges = 14400, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Bow.png" }, - name = "Lasting Exercise Bow", - price = 720, - itemtype = 40123, - charges = 14400, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Club.png" }, - name = "Lasting Exercise Club", - price = 720, - itemtype = 40122, - charges = 14400, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Rod.png" }, - name = "Lasting Exercise Rod", - price = 720, - itemtype = 40124, - charges = 14400, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Sword.png" }, - name = "Lasting Exercise Sword", - price = 720, - itemtype = 40120, - charges = 14400, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Wand.png" }, - name = "Lasting Exercise Wand", - price = 720, - itemtype = 40125, - charges = 14400, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - }, - }, - -- Kegs - { - icons = { "Category_Kegs.png" }, - name = "Kegs", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Health_Keg.png" }, - name = "Health Keg", - price = 26, - itemtype = 28579, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Health_Keg.png" }, - name = "Strong Health Keg", - price = 53, - itemtype = 28580, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Health_Keg.png" }, - name = "Great Health Keg", - price = 103, - itemtype = 28581, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Health_Keg.png" }, - name = "Ultimate Health Keg", - price = 175, - itemtype = 28582, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Supreme_Health_Keg.png" }, - name = "Supreme Health Keg", - price = 288, - itemtype = 28583, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Mana_Keg.png" }, - name = "Mana Keg", - price = 26, - itemtype = 28584, - count = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Mana_Keg.png" }, - name = "Strong Mana Keg", - price = 43, - itemtype = 28585, - count = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Mana_Keg.png" }, - name = "Great Mana Keg", - price = 66, - itemtype = 28586, - count = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Mana_Keg.png" }, - name = "Ultimate Mana Keg", - price = 202, - itemtype = 28587, - count = 500, - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Spirit_Keg.png" }, - name = "Great Spirit Keg", - price = 105, - itemtype = 28589, - count = 500, - description = "Fill up potions to restore your hit points and mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = {"Ultimate_Spirit_Keg.png"}, - name = "Ultimate Spirit Keg", - price = 202, - itemtype = 28590, - count = 500, - description = "Fill up potions to restore your hit points and mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - }, - }, - -- Potions - { - icons = { "Category_Potions.png" }, - name = "Potions", - parent = "Consumables", - offers = { - { - icons = { "Health_Potion.png" }, - name = "Health Potion", - price = 6, - itemtype = 7618, - count = 125, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Health_Potion.png" }, - name = "Health Potion", - price = 11, - itemtype = 7618, - count = 300, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Health_Potion.png" }, - name = "Strong Health Potion", - price = 10, - itemtype = 7588, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Health_Potion.png" }, - name = "Strong Health Potion", - price = 21, - itemtype = 7588, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Health_Potion.png" }, - name = "Great Health Potion", - price = 18, - itemtype = 7591, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Health_Potion.png" }, - name = "Great Health Potion", - price = 41, - itemtype = 7591, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Health_Potion.png" }, - name = "Ultimate Health Potion", - price = 29, - itemtype = 8473, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Health_Potion.png" }, - name = "Ultimate Health Potion", - price = 68, - itemtype = 8473, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Supreme_Health_Potion.png" }, - name = "Supreme Health Potion", - price = 47, - itemtype = 26031, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Supreme_Health_Potion.png" }, - name = "Supreme Health Potion", - price = 113, - itemtype = 26031, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Mana_Potion.png" }, - name = "Mana Potion", - price = 6, - itemtype = 7620, - count = 125, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Mana_Potion.png" }, - name = "Mana Potion", - price = 12, - itemtype = 7620, - count = 300, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Mana_Potion.png" }, - name = "Strong Mana Potion", - price = 7, - itemtype = 7589, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Mana_Potion.png" }, - name = "Strong Mana Potion", - price = 17, - itemtype = 7589, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Mana_Potion.png" }, - name = "Great Mana Potion", - price = 11, - itemtype = 7590, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Mana_Potion.png" }, - name = "Great Mana Potion", - price = 26, - itemtype = 7590, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n-{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Mana_Potion.png" }, - name = "Ultimate Mana Potion", - price = 33, - itemtype = 26029, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Mana_Potion.png" }, - name = "Ultimate Mana Potion", - price = 79, - itemtype = 26029, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Spirit_Potion.png" }, - name = "Great Spirit Potion", - price = 18, - itemtype = 8472, - count = 100, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Spirit_Potion.png" }, - name = "Great Spirit Potion", - price = 41, - itemtype = 8472, - count = 250, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Spirit_Potion.png" }, - name = "Ultimate Spirit Potion", - price = 33, - itemtype = 26030, - count = 100, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Spirit_Potion.png" }, - name = "Ultimate Spirit Potion", - price = 79, - itemtype = 26030, - count = 250, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - -- Runes - { - icons = { "Category_Runes.png" }, - name = "Runes", - parent = "Consumables", - offers = { - { - icons = { "Animate_Dead_Rune.png" }, - name = "Animate Dead Rune", - price = 75, - itemtype = 2316, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck} only buyable if fitting vocation and level of purchasing character\n{battlesign}\n{capacity}\n\nAfter a long time of research, the magicians of Edron succeeded in storing some life energy in a rune. When this energy was unleashed onto a body it was found that an undead creature arose that could be mentally controlled by the user of the rune. This rune is useful to create allies in combat.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Avalanche_Rune.png" }, - name = "Avalanche Rune", - price = 12, - itemtype = 2274, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe ice damage which arises from this rune is a useful weapon in every battle but it comes in particularly handy if you fight against a horde of creatures dominated by the element fire.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Chameleon_Rune.png" }, - name = "Chameleon Rune", - price = 42, - itemtype = 2291, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe metamorphosis caused by this rune is only superficial, and while casters who are using the rune can take on the exterior form of nearly any inanimate object, they will always retain their original smell and mental abilities. So there is no real practical use for this rune, making this largely a fun rune.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Convince_Creature_Rune.png" }, - name = "Convince Creature Rune", - price = 16, - itemtype = 2290, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nUsing this rune together with some mana, you can convince certain creatures. The needed amount of mana is determined by the power of the creature one wishes to convince, so the amount of mana to convince a rat is lower than that which is needed for an orc.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Cure_Poison_Rune_(Item).png" }, - name = "Cure Poison Rune", - price = 13, - itemtype = 2266, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nIn the old days, many adventurers fell prey to poisonous creatures that were roaming the caves and forests. After many years of research druids finally succeeded in altering the cure poison spell so it could be bound to a rune. By using this rune it is possible to stop the effect of any known poison.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Disintegrate_Rune.png" }, - name = "Disintegrate Rune", - price = 5, - itemtype = 2310, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nNothing is worse than being cornered when fleeing from an enemy you just cannot beat, especially if the obstacles in your way are items you could easily remove if only you had the time! However, there is one reliable remedy: The Disintegrate rune will instantly destroy up to 500 movable items that are in your way, making room for a quick escape.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Bomb_Rune.png" }, - name = "Energy Bomb Rune", - price = 40, - itemtype = 2262, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nUsing the Energy Bomb rune will create a field of deadly energy that deals damage to all who carelessly step into it. Its area of effect is covering a full 9 square metres! Creatures that are caught in the middle of an Energy Bomb are frequently confused by the unexpected effect, and some may even stay in the field of deadly sparks for a while.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Field_Rune.png" }, - name = "Energy Field Rune", - price = 8, - itemtype = 2277, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis spell creates a limited barrier made up of crackling energy that will cause electrical damage to all those passing through. Since there are few creatures that are immune to the harmful effects of energy this spell is not to be underestimated.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Wall_Rune.png" }, - name = "Energy Wall Rune", - price = 17, - itemtype = 2279, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nCasting this spell generates a solid wall made up of magical energy. Walls made this way surpass any other magically created obstacle in width, so it is always a good idea to have an Energy Wall rune or two in one's pocket when travelling through the wilderness.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Explosion_Rune.png" }, - name = "Explosion Rune", - price = 6, - itemtype = 2313, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune must be aimed at areas rather than at specific creatures, so it is possible for explosions to be unleashed even if no targets are close at all. These explosions cause a considerable physical damage within a substantial blast radius.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fireball_Rune.png" }, - name = "Fireball Rune", - price = 6, - itemtype = 2302, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a massive fiery ball is released which hits the aimed foe with immense power. It is especially effective against opponents of the element earth.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Bomb_Rune.png" }, - name = "Fire Bomb Rune", - price = 29, - itemtype = 2305, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune is a deadly weapon in the hands of the skilled user. On releasing it an area of 9 square metres is covered by searing flames that will scorch all those that are unfortunate enough to be caught in them. Worse, many monsters are confused by the unexpected blaze, and with a bit of luck a caster will even manage to trap his opponents by using the spell.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Field_Rune.png" }, - name = "Fire Field Rune", - price = 6, - itemtype = 2301, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a field of one square metre is covered by searing fire that will last for some minutes, gradually diminishing as the blaze wears down. As with all field spells, Fire Field is quite useful to block narrow passageways or to create large, connected barriers.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Wall_Rune.png" }, - name = "Fire Wall Rune", - price = 12, - itemtype = 2303, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune offers reliable protection against all creatures that are afraid of fire. The exceptionally long duration of the spell as well as the possibility to form massive barriers or even protective circles out of fire walls make this a versatile, practical spell.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Fireball_Rune.png" }, - name = "Great Fireball Rune", - price = 12, - itemtype = 2304, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nA shot of this rune affects a huge area - up to 37 square metres! It stands to reason that the Great Fireball is a favourite of most Tibians, as it is well suited both to hit whole crowds of monsters and individual targets that are difficult to hit because they are fast or hard to spot.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Icicle_Rune.png" }, - name = "Icicle Rune", - price = 6, - itemtype = 2271, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nParticularly creatures determined by the element fire are vulnerable against this ice-cold rune. Being hit by the magic stored in this rune, an ice arrow seems to pierce the heart of the struck victim. The damage done by this rune is quite impressive which makes this a quite popular rune among Tibian mages.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Intense_Healing_Rune.png" }, - name = "Intense Healing Rune", - price = 19, - itemtype = 2265, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune is commonly used by young adventurers who are not skilled enough to use the rune's stronger version. Also, since the rune's effectiveness is determined by the user's magic skill, it is still popular among experienced spell casters who use it to get effective healing magic at a cheap price.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Magic_Wall_Rune.png" }, - name = "Magic Wall Rune", - price = 23, - itemtype = 2293, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis spell causes all particles that are contained in the surrounding air to quickly gather and contract until a solid wall is formed that covers one full square metre. The wall that is formed that way is impenetrable to any missiles or to light and no creature or character can walk through it. However, the wall will only last for a couple of seconds.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Poison_Bomb_Rune.png" }, - name = "Poison Bomb Rune", - price = 17, - itemtype = 2286, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune causes an area of 9 square metres to be contaminated with toxic gas that will poison anybody who is caught within it. Conceivable applications include the blocking of areas or the combat against fast-moving or invisible targets. Keep in mind, however, that there are a number of creatures that are immune to poison.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Poison_Wall_Rune.png" }, - name = "Poison Wall Rune", - price = 10, - itemtype = 2289, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a wall of concentrated toxic fumes is created which inflicts a moderate poison on all those who are foolish enough to enter it. The effect is usually impressive enough to discourage monsters from doing so, although few of the stronger ones will hesitate if there is nothing but a poison wall between them and their dinner.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Soulfire_Rune.png" }, - name = "Soulfire Rune", - price = 9, - itemtype = 2308, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nSoulfire is an immensely evil spell as it directly targets a creature's very life essence. When the rune is used on a victim, its soul is temporarily moved out of its body, casting it down into the blazing fires of hell itself! Note that the experience and the mental strength of the caster influence the damage that is caused.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Stone_Shower_Rune.png" }, - name = "Stone Shower Rune", - price = 7, - itemtype = 2288, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nParticularly creatures with an affection to energy will suffer greatly from this rune filled with powerful earth damage. As the name already says, a shower of stones drums on the opponents of the rune user in an area up to 37 squares.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Sudden_Death_Rune.png" }, - name = "Sudden Death Rune", - price = 28, - itemtype = 2268, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nNearly no other spell can compare to Sudden Death when it comes to sheer damage. For this reason it is immensely popular despite the fact that only a single target is affected. However, since the damage caused by the rune is of deadly nature, it is less useful against most undead creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Thunderstorm_Rune.png" }, - name = "Thunderstorm Rune", - price = 9, - itemtype = 2315, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nFlashes filled with dangerous energy hit the rune user's opponent when this rune is being used. It is especially effective against ice dominated creatures. Covering up an area up to 37 squares, this rune is particularly useful when you meet a whole mob of opponents.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Healing_Rune.png" }, - name = "Ultimate Healing Rune", - price = 35, - itemtype = 2273, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe coveted Ultimate Healing rune is an all-time favourite among all vocations. No other healing enchantments that are bound into runes can compare to its salutary effect.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Wild_Growth_Rune.png" }, - name = "Wild Growth Rune", - price = 32, - itemtype = 2269, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nBy unleashing this spell, all seeds that are lying dormant in the surrounding quickly sprout and grow into full-sized plants, thus forming an impenetrable thicket. Unfortunately, plant life created this way is short-lived and will collapse within minutes, so the magically created obstacle will not last long.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - -- Cosmetics - { - icons = { "Category_Cosmetics.png" }, - name = "Cosmetics", - rookgaard = true, - subclasses = {"Mounts", "Outfits"}, - }, - -- Mounts - { - icons = { "Category_Mounts.png" }, - name = "Mounts", - parent = "Cosmetics", - rookgaard = true, - offers = { - { - icons = { "Arctic_Unicorn.png" }, - name = "Artic Unicorn", - price = 870, - id = 114, - description = "{character}\n{speedboost}\n\nThe Arctic Unicorn lives in a deep rivalry with its cousin the Blazing Unicorn. Even though they were born in completely different areas, they somehow share the same bloodline. The eternal battle between fire and ice continues. Who will win? Tangerine vs.crystal blue! The choice is yours!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Armoured_War_Horse.png" }, - name = "Armoured War Horse", - price = 870, - id = 23, - description = "{character}\n{speedboost}\n\nThe Armoured War Horse is a dangerous black beauty! When you see its threatening, blood-red eyes coming towards you, you'll know trouble is on its way. Protected by its heavy armour plates, the warhorse is the perfect partner for dangerous hunting sessions and excessive enemy slaughtering.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Batcat.png" }, - name = "Batcat", - price = 870, - id = 77, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Batcat to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Battle_Badger.png" }, - name = "Battle Badger", - price = 690, - id = 147, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Black_Stag.png" }, - name = "Black Stag", - price = 660, - id = 73, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Black Stag will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Blackpelt.png" }, - name = "Blackpelt", - price = 690, - id = 58, - description = "{character}\n{speedboost}\n\nThe Blackpelt is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Blazing_Unicorn.png" }, - name = "Blazing Unicorn", - price = 870, - id = 113, - description = "{character}\n{speedboost}\n\nThe Blazing Unicorn lives in a deep rivalry with its cousin the Arctic Unicorn. Even though they were born in completely different areas, they somehow share the same bloodline. The eternal battle between fire and ice continues. Who will win? Crystal blue vs. tangerine! The choice is yours!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Bloodcurl.png" }, - name = "Bloodcurl", - price = 750, - id = 92, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Bloodcurl will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Boreal_Owl.png" }, - name = "Boreal Owl", - price = 870, - id = 129, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Bunny_Dray.png" }, - name = "Bunny Dray", - price = 870, - id = 139, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Bunny Dray comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Caped_Snowman.png" }, - name = "Caped Snowman", - price = 870, - id = 137, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Caped Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cave_Tarantula.png" }, - name = "Cave Tarantula", - price = 690, - id = 117, - description = "{character}\n{speedboost}\n\nIt is said that the Cave Tarantula was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cinderhoof.png" }, - name = "Cinderhoof", - price = 870, - id = 90, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Cinderhoof to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cony_Cart.png" }, - name = "Cony Cart", - price = 870, - id = 140, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Cony Cart comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Copper_Fly.png" }, - name = "Copper Fly", - price = 870, - id = 61, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Copper Fly, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Coralripper.png" }, - name = "Coralripper", - price = 570, - id = 79, - description = "{character}\n{speedboost}\n\nIf the Coralripper moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cranium_Spider.png" }, - name = "Cranium Spider", - price = 690, - id = 116, - description = "{character}\n{speedboost}\n\nIt is said that the Cranium Spider was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Crimson_Ray.png" }, - name = "Crimson Ray", - price = 870, - id = 33, - description = "{character}\n{speedboost}\n\nHave you ever dreamed of gliding through the air on the back of a winged creature? With its deep red wings, the majestic Crimson Ray is a worthy mount for courageous heroes. Feel like a king on its back as you ride into your next adventure.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Death_Crawler.png" }, - name = "Death Crawler", - price = 600, - id = 46, - description = "{character}\n{speedboost}\n\nThe Death Crawler is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Desert_King.png" }, - name = "Desert King", - price = 450, - id = 41, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Desert King is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Doombringer.png" }, - name = "Doombringer", - price = 780, - id = 53, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Doombringer is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dreadhare.png" }, - name = "Dreadhare", - price = 870, - id = 104, - description = "{character}\n{speedboost}\n\nDo you like fluffy bunnies but think they are too small? Do you admire the majesty of stags and their antlers but are afraid of their untameable wilderness? Do not worry, the mystic creature Dreadhare consolidates the best qualities of both animals. Hop on its backs and enjoy the ride.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ebony_Tiger.png" }, - name = "Ebony Tiger", - price = 750, - id = 123, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Ebony Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ember_Saurian.png" }, - name = "Ember Saurian", - price = 750, - id = 111, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Ember Saurian has been spotted in a sea of flames and fire deep down in the depths of Kazordoon.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emerald_Sphinx.png" }, - name = "Emerald Sphinx", - price = 750, - id = 108, - description = "{character}\n{speedboost}\n\nRide an Emerald Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emerald_Waccoon.png" }, - name = "Emerald Waccoon", - price = 750, - id = 70, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Emerald Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emperor_Deer.png" }, - name = "Emperor Deer", - price = 660, - id = 74, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Emperor Deer will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ether_Badger.png" }, - name = "Ether Badger", - price = 690, - id = 148, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Feral_Tiger.png" }, - name = "Feral Tiger", - price = 750, - id = 124, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Feral Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Festive_Snowman.png" }, - name = "Festive Snowman", - price = 900, - id = 135, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Festive Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gloom_Widow.png" }, - name = "Gloom Widow", - price = 690, - id = 118, - description = "{character}\n{speedboost}\n\nIt is said that the Gloom Widow was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Prismatic_Unicorn.png" }, - name = "Prismatic Unicorn", - price = 870, - id = 115, - description = "{character}\n{speedboost}\n\nLegend has it that a mare and a stallion once reached the end of a rainbow and decided to stay there. Influenced by the mystical power of the rainbow, the mare gave birth to an exceptional foal: Not only the big, strong horn on its forehead but the unusual colouring of its hair makes the Prismatic Unicorn a unique mount in every respect.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Rabbit_Rickshaw.png" }, - name = "Rabbit Rickshaw", - price = 870, - id = 138, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Rabbit Rickshaw comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Draptor.png" }, - name = "Shadow Draptor", - price = 870, - id = 24, - description = "{character}\n{speedboost}\n\nA wild, ancient creature, which had been hiding in the depths of the shadows for a very long time, has been spotted in Tibia again! The almighty Shadow Draptor has returned and only the bravest Tibians can control such a beast!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Steelbeak.png" }, - name = "Steelbeak", - price = 870, - id = 34, - description = "{character}\n{speedboost}\n\nForged by only the highest skilled blacksmiths in the depths of Kazordoon's furnaces, a wild animal made out of the finest steel arose from glowing embers and blazing heat. Protected by its impenetrable armour, the Steelbeak is ready to accompany its master on every battleground.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jungle_Saurian.png" }, - name = "Jungle Saurian", - price = 750, - id = 110, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Jungle Saurian likes to hide in dense wood and overturned trees.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jungle_Tiger.png" }, - name = "Jungle Tiger", - price = 750, - id = 125, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Jungle Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Lagoon_Saurian.png" }, - name = "Lagoon Saurian", - price = 750, - id = 112, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Lagoon Saurian feels most comfortable in torrential rivers and behind dangerous waterfalls.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gold_Sphinx.png" }, - name = "Gold Sphinx", - price = 750, - id = 107, - description = "{character}\n{speedboost}\n\nRide a Gold Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Sphinx.png" }, - name = "Shadow Sphinx", - price = 750, - id = 109, - description = "{character}\n{speedboost}\n\nRide a Shadow Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jackalope.png" }, - name = "Jackalope", - price = 870, - id = 103, - description = "{character}\n{speedboost}\n\nDo you like fluffy bunnies but think they are too small? Do you admire the majesty of stags and their antlers but are afraid of their untameable wilderness? Do not worry, the mystic creature Jackalope consolidates the best qualities of both animals. Hop on its backs and enjoy the ride.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ivory_Fang.png" }, - name = "Ivory Fang", - price = 750, - id = 100, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Ivory Fang, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Claw.png" }, - name = "Shadow Claw", - price = 750, - id = 101, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Shadow Claw, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snow_Pelt.png" }, - name = "Snow Pelt", - price = 750, - id = 102, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Snow Pelt, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snowy_Owl.png" }, - name = "Snowy Owl", - price = 870, - id = 128, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tawny_Owl.png" }, - name = "Tawny Owl", - price = 870, - id = 127, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Swamp_Crocovile.png" }, - name = "Swamp Crocovile", - price = 750, - id = 142, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Swamp_Snapper.png" }, - name = "Swamp Snapper", - price = 690, - id = 95, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Swamp Snapper might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mould_Shell.png" }, - name = "Mould Shell", - price = 690, - id = 96, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Mould Shell might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Reed_Lurker.png" }, - name = "Reed Lurker", - price = 690, - id = 97, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Reed Lurker might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Leafscuttler.png" }, - name = "Leafscuttler", - price = 750, - id = 93, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Leafscuttler will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mouldpincer.png" }, - name = "Mouldpincer", - price = 750, - id = 91, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Mouldpincer will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Muffled_Snowman.png" }, - name = "Muffled Snowman", - price = 900, - id = 136, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Muffled Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightdweller.png" }, - name = "Nightdweller", - price = 870, - id = 88, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Nightdweller to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightmarish_Crocovile.png" }, - name = "Nightmarish Crocovile", - price = 750, - id = 143, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Frostflare.png" }, - name = "Frostflare", - price = 870, - id = 89, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Frostflare to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Slagsnare.png" }, - name = "Slagsnare", - price = 780, - id = 84, - description = "{character}\n{speedboost}\n\nThe Slagsnare has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightstinger.png" }, - name = "Nightstinger", - price = 780, - id = 85, - description = "{character}\n{speedboost}\n\nThe Nightstinger has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Razorcreep.png" }, - name = "Razorcreep", - price = 780, - id = 86, - description = "{character}\n{speedboost}\n\nThe Razorcreep has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gorongra.png" }, - name = "Gorongra", - price = 720, - id = 81, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Gorongra is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Noctungra.png" }, - name = "Noctungra", - price = 720, - id = 82, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Noctungra is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Silverneck.png" }, - name = "Silverneck", - price = 720, - id = 83, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Silverneck is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Sea_Devil.png" }, - name = "Sea Devil", - price = 570, - id = 78, - description = "{character}\n{speedboost}\n\nIf the Sea Devil moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Plumfish.png" }, - name = "Plumfish", - price = 570, - id = 80, - description = "{character}\n{speedboost}\n\nIf the Plumfish moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flitterkatzen.png" }, - name = "Flitterkatzen", - price = 870, - id = 75, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Flitterkatzen to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ringtail_Waccoon.png" }, - name = "Ringtail Waccoon", - price = 750, - id = 68, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Ringtail Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "River_Crocovile.png" }, - name = "River Crocovile", - price = 750, - id = 141, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Sanguine_Frog.png" }, - name = "Sanguine Frog", - price = 690, - id = 121, - description = "{character}\n{speedboost}\n\nFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Sanguine Frog a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Night_Waccoon.png" }, - name = "Night Waccoon", - price = 750, - id = 69, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Night Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flying_Divan.png" }, - name = "Flying Divan", - price = 900, - id = 65, - description = "{character}\n{speedboost}\n\nThe Flying Divan is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Magic_Carpet.png" }, - name = "Magic Carpet", - price = 900, - id = 66, - description = "{character}\n{speedboost}\n\nThe Magic Carpet is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Marsh_Toad.png" }, - name = "Marsh Toad", - price = 690, - id = 120, - description = "{character}\n{speedboost}\n\nThe Magic Carpet is the perfect mount for those who are too busy to take cFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Marsh Toad a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Kashmir.png" }, - name = "Floating Kashmir", - price = 900, - id = 67, - description = "{character}\n{speedboost}\n\nThe Floating Kashmir is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Hart.png" }, - name = "Shadow Hart", - price = 660, - id = 72, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Shadow Hart will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tundra_Rambler.png" }, - name = "Tundra Rambler", - price = 750, - id = 62, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Tundra Rambler will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Highland_Yak.png" }, - name = "Highland Yak", - price = 750, - id = 63, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Highland Yak will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Glacier_Vagabond.png" }, - name = "Glacier Vagabond", - price = 750, - id = 64, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Glacier Vagabond will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Golden_Dragonfly.png" }, - name = "Golden Dragonfly", - price = 600, - id = 59, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Golden Dragonfly, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Steel_Bee.png" }, - name = "Steel Bee", - price = 600, - id = 60, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Steel Bee, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Hailstorm_Fury.png" }, - name = "Hailtorm Fury", - price = 780, - id = 55, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Hailstorm Fury is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Siegebreaker.png" }, - name = "Siegebreaker", - price = 690, - id = 56, - description = "{character}\n{speedboost}\n\nThe Siegebreaker is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Poisonbane.png" }, - name = "Poisonbane", - price = 690, - id = 57, - description = "{character}\n{speedboost}\n\nThe Poisonbane is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nethersteed.png" }, - name = "Nethersteed", - price = 900, - id = 50, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Nethersteed has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tempest.png" }, - name = "Tempest", - price = 900, - id = 51, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Tempest has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flamesteed.png" }, - name = "Flamesteed", - price = 900, - id = 47, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Flamesteed has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tombstinger.png" }, - name = "Tombstinger", - price = 600, - id = 36, - description = "{character}\n{speedboost}\n\nThe Tombstinger is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Toxic_Toad.png" }, - name = "Toxic Toad", - price = 690, - id = 122, - description = "{character}\n{speedboost}\n\nFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Toxic Toad a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jade_Pincer.png" }, - name = "Jade Pincer", - price = 600, - id = 49, - description = "{character}\n{speedboost}\n\nThe Jade Pincer is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jade_Lion.png" }, - name = "Jade Lion", - price = 450, - id = 48, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Jade Lion is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Venompaw.png" }, - name = "Venompaw", - price = 870, - id = 76, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Venompaw to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Winter_King.png" }, - name = "Winter King", - price = 450, - id = 52, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Winter King is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Wolpertinger.png" }, - name = "Wolpertinger", - price = 870, - id = 105, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Woodland Prince is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Woodland_Prince.png" }, - name = "Woodland Prince", - price = 780, - id = 54, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Woodland Prince is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Zaoan_Badger.png" }, - name = "Zaoan Badger", - price = 690, - id = 149, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Augur.png" }, - name = "Floating Augur", - price = 870, - id = 155, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Sage.png" }, - name = "Floating Sage", - price = 870, - id = 153, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Scholar.png" }, - name = "Floating Scholar", - price = 870, - id = 154, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snow_Strider.png" }, - name = "Snow Strider", - price = 870, - id = 164, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dusk_Pryer.png" }, - name = "Dusk Pryer", - price = 870, - id = 165, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dawn_Strayer.png" }, - name = "Dawn Strayer", - price = 870, - id = 166, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Savanna_Ostrich.png" }, - name = "Savanna Ostrich", - price = 500, - id = 168, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Coral_Rhea.png" }, - name = "Coral Rhea", - price = 500, - id = 169, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Eventide_Nandu.png" }, - name = "Eventide Nandu", - price = 500, - id = 170, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Voracious_Hyaena.png" }, - name = "Voracious Hyaena", - price = 750, - id = 171, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cunning_Hyaena.png" }, - name = "Cunning Hyaena", - price = 750, - id = 172, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Scruffy_Hyaena.png" }, - name = "Scruffy Hyaena", - price = 750, - id = 173, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Void_Watcher.png" }, - name = "Void Watcher", - price = 870, - id = 179, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Void Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - home = true, - }, - { - icons = { "Rune_Watcher.png" }, - name = "Rune Watcher", - price = 870, - id = 180, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Rune Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - home = true, - }, - { - icons = { "Rift_Watcher.png" }, - name = "Rift Watcher", - price = 870, - id = 181, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Rift Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - home = true, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - -- Base outfit has addon = 0 or no defined addon. By default addon is set to 0. - { - icons = { "Category_Outfits.png" }, - name = "Outfits", - parent = "Cosmetics", - offers = { - { - icons = { "Outfit_Arena_Champion_Male_Addon_3.png", "Outfit_Arena_Champion_Female_Addon_3.png" }, - name = "Full Arena Champion Outfit", - price = 870, - sexId = {female = 885, male = 884}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFight your bloody battles in the arena and become a darling of the crowd. Once you have made it to the top and everyone is cheering your name, the fashionable outfit of an Arena Champion will show the world what you are made of.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Beastmaster_Male_Addon_3.png", "Outfit_Beastmaster_Female_Addon_3.png" }, - name = "Full Beastmaster Outfit", - price = 870, - sexId = {female = 636, male = 637}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you have enough authority to make wild animals subservient to you? Become a Beastmaster and surround yourself with fearsome companions. When your beasts bare their teeth, your enemies will turn tails and run.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Breezy_Garb_Male_Addon_3.png", "Outfit_Breezy_Garb_Female_Addon_3.png" }, - name = "Full Breezy Garb Outfit", - price = 600, - sexId = {female = 1246, male = 1245}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nEven the most eager adventurers and toughest warriors need some time to rest and recharge. Enjoy tranquility and peace as you picnic in good company at one of your favourite places in Tibia. Put on your Breezy Garb outfit, grab your walking stick, a basket filled with tasty snacks and then head out into nature!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Ceremonial_Garb_Male_Addon_3.png", "Outfit_Ceremonial_Garb_Female_Addon_3.png" }, - name = "Full Ceremonial Garb Outfit", - price = 750, - sexId = {female = 694, male = 695}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nIf you want to make a great entrance at a Tibian costume party, the Ceremonial Garb is certainly a good choice. With a drum over your shoulder and adorned with feathers you are perfectly dressed to lead a carnival parade through the streets of Thais.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Champion_Male_Addon_3.png", "Outfit_Champion_Female_Addon_3.png" }, - name = "Full Champion Outfit", - price = 570, - sexId = {female = 632, male = 633}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nProtect your body with heavy armour plates and spiky bones to teach your enemies the meaning of fear! The Champion outfit perfectly suits battle-hardened warriors who rely on their trusty sword and shield.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Chaos_Acolyte_Male_Addon_3.png", "Outfit_Chaos_Acolyte_Female_Addon_3.png" }, - name = "Full Chaos Acolyte Outfit", - price = 900, - sexId = {female = 664, male = 665}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou have always felt like the cat among the pigeons and have a fable for dark magic? The Chaos Acolyte outfit is a perfect way to express your inner nature. Show your commitment for the higher cause and wreak havoc on your enemies in this unique outfit.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Conjurer_Male_Addon_3.png", "Outfit_Conjurer_Female_Addon_3.png" }, - name = "Full Conjurer Outfit", - price = 750, - sexId = {female = 635, male = 634}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou recently graduated from the Magic Academy and want to bring your knowledge to good use? Congratulations, you are now an honourable disciple of magic! Open up a bottle of well-aged mana and treat yourself with the fashionable Conjurer outfit.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Death_Herald_Male_Addon_3.png", "Outfit_Death_Herald_Female_Addon_3.png" }, - name = "Full Death Herald Outfit", - price = 600, - sexId = {female = 666, male = 667}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDeath and decay are your ever-present companions? Your enemies are dropping like flies and your path is covered with their bodies? However, as decency demands, you want to at least give them a proper funeral? Then the Death Herald is just the right outfit for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Entrepreneur_Male_Addon_3.png", "Outfit_Entrepreneur_Female_Addon_3.png" }, - name = "Full Entrepreneur Outfit", - price = 750, - sexId = {female = 471, male = 472}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nSlaughter through hordes of monsters during your early morning hunt and kiss the hand of Queen Eloise later on at the evening reception in her historical residence. With the Entrepreneur outfit you will cut a fine figure on every occasion.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Evoker_Male_Addon_3.png", "Outfit_Evoker_Female_Addon_3.png" }, - name = "Full Evoker Outfit", - price = 840, - sexId = {female = 724, male = 725}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDance around flickering fires in the Evoker outfit while singing unholy chants to praise witchcraft and wizardry. Your faithful bat will always be by your side.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Grove_Keeper_Male_Addon_3.png", "Outfit_Grove_Keeper_Female_Addon_3.png" }, - name = "Full Groove Keeper Outfit", - price = 870, - sexId = {female = 909, male = 908}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFeeling the springy grass under your feet and inhaling the spicy air of the forest is pure satisfaction for your soul? Every animal is your friend and you caringly look after trees and plants all the time? Then it is time to become one with nature: Become a Grove Keeper!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Guidon_Bearer_Male_Addon_3.png", "Outfit_Guidon_Bearer_Female_Addon_3.png" }, - name = "Full Guidon Bearer Outfit", - price = 870, - sexId = {female = 1187, male = 1186}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nCarrying the guidon of a unit, always marching in front, is not only an honour but also comes with great responsibility. Guidon bearers wield great power, they lead where others follow and keep the spirits of the troops up as they wave their flag against the golden suns of Tibia.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Herbalist_Male_Addon_3.png", "Outfit_Herbalist_Female_Addon_3.png" }, - name = "Full Herbalist Outfit", - price = 750, - sexId = {female = 1020, male = 1021}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Herbalist outfit is the perfect outfit for all herbs collectors. Those of you who are aware that you do not necessarily have to reach into the mouth of a hydra to get a hydra tongue and those who know exactly where to get blood- and shadow-herbs will find a matching outfit for their daily hobby. Show the world your affinity for herbs and impress your friends with your knowledge of medicine and potions.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Herder_Male_Addon_3.png", "Outfit_Herder_Female_Addon_3.png" }, - name = "Full Herder Outfit", - price = 750, - sexId = {female = 1280, male = 1279}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Herder is one with nature, being outside all day, watching carefully over his flock. If you like to spend time on picturesque meadows and are always looking for greener pastures, then this outfit is for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Lupine_Warden_Male_Addon_3.png", "Outfit_Lupine_Warden_Female_Addon_3.png" }, - name = "Full Lupine Warden Outfit", - price = 840, - sexId = {female = 900,male = 899}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you feel the adrenaline rushing through your veins when the sun goes down and a full moon lightens the night? Do you have the urge to hunt down your target no matter what? Unleash the beast inside of you and lead your friends to battle with the Lupine Warden outfit!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Mercenary_Male_Addon_3.png", "Outfit_Mercenary_Female_Addon_3.png" }, - name = "Full Mercenary Outfit", - price = 870, - sexId = {female = 1057, male = 1056}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Mercenary carries a powerful, razor-sharp axe on his shoulders that effortlessly cuts through any armour and bone. You should better tell your friends to keep a safe distance, since heads will roll over the blood-soaked battleground after a powerful swing of yours.\nConsidering the sheer size of this axe, it might even be possible to chop onions without shedding a tear.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Owl_Keeper_Male_Addon_3.png", "Outfit_Owl_Keeper_Female_Addon_3.png" }, - name = "Full Owl Keeper Outfit", - price = 600, - sexId = {female = 1174,male = 1173}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nOwl Keepers are often referred to as spirits walking through the forest at night, mere shadows during the day. They are also said to be shamans, protecting the flora and fauna of the Tibian lands. You often see them wearing a stag's antlers on their head and in the company of an owl, for they are as wise and mysterious as these intriguing creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Pharaoh_Male_Addon_3.png", "Outfit_Pharaoh_Female_Addon_3.png" }, - name = "Full Pharaoh Outfit", - price = 750, - sexId = {female = 956,male = 955}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou know how to read hieroglyphs? You admire the exceptional architectural abilities and the unsolved mysteries of an ancient high culture? Next time you pay a visit to your friends, tell them to prepare a bathtub full of milk and honey for you because a Pharaoh is now walking through the streets of Ankrahmun!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Philosopher_Male_Addon_3.png", "Outfit_Philosopher_Female_Addon_3.png" }, - name = "Full Philosopher Outfit", - price = 750, - sexId = {female = 874,male = 873}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you feel the urge to tell people what is really going on in the world? Do you know all answers to the important questions of life? Are you a true philosopher? Then dress like one to showcase the latest fashion for all wise theorists.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Pumpkin_Mummy_Male_Addon_3.png", "Outfit_Pumpkin_Mummy_Female_Addon_3.png" }, - name = "Full Pumpkin Mummy Outfit", - price = 870, - sexId = {female = 1128,male = 1127}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nIf you cannot decide whether to wrap yourself up as a mummy or flaunt an enormous pumpkin head for your next hunting party, why not combine both? The Pumpkin Mummy outfit is the perfect costume for scary nights and spooky days.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Puppeteer_Male_Addon_3.png", "Outfit_Puppeteer_Female_Addon_3.png" }, - name = "Full Puppeteer Outfit", - price = 870, - sexId = {female = 696, male = 697}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAre you a fan of puppetry? You like to travel the world together with one or two little acting fellows? Or are you simply the one who likes to pull the strings? Then the Puppeteer outfit is the right choice for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Ranger_Male_Addon_3.png", "Outfit_Ranger_Female_Addon_3.png" }, - name = "Full Ranger Outfit", - price = 750, - sexId = {female = 683,male = 684}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nMost of the day, the Ranger is looking over his forest. He is taking care of all animals and plants and tries to keep everything in balance. Intruders are greeted by a warning shot from his deadly longbow. It is the perfect outfit for Paladins who live in close touch with nature.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Royal_Pumpkin_Male_Addon_3.png", "Outfit_Royal_Pumpkin_Female_Addon_3.png" }, - name = "Full Royal Pumpkin Outfit", - price = 840, - sexId = {male= 760, female= 759}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe mutated pumpkin is too weak for your mighty weapons? Time to show that evil vegetable how to scare the living daylight out of people! Put on a scary looking pumpkin on your head and spread terror and fear amongst the Tibian population.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sea_Dog_Male_Addon_3.png", "Outfit_Sea_Dog_Female_Addon_3.png" }, - name = "Full Sea Dog Outfit", - price = 600, - sexId = {female = 749,male = 750}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAhoy mateys! Flaunt the swashbuckling Sea Dog outfit and strike a pose with your hook to impress both landlubbers and fellow pirates. Board your next ship in style!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Seaweaver_Male_Addon_3.png", "Outfit_Seaweaver_Female_Addon_3.png" }, - name = "Full Seaweaver Outfit", - price = 570, - sexId = {female = 732,male = 733}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Seaweaver outfit is the perfect choice if you want to show the world that you are indeed a son or a daughter of the submarine kingdom. You can almost feel the salty taste and the rough wind of the sea when wearing it.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Siege_Master_Male_Addon_3.png", "Outfit_Siege_Master_Female_Addon_3.png" }, - name = "Full Siege Master Outfit", - price = 600, - sexId = {female = 1050,male = 1051}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nNeither thick stone walls nor heavily armoured gates can stop the Siege Master, who brings down hostile fortifications in the blink of an eye. Whenever he tenses his muscular arms to lift the powerful battering ram, his enemies' knees begin to buckle. It is the perfect outfit for those who also stand for brute strength and immense destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sinister_Archer_Male_Addon_3.png", "Outfit_Sinister_Archer_Female_Addon_3.png" }, - name = "Full Sinister Archer Outfit", - price = 600, - sexId = {female = 1103,male = 1102}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFrom an early age, the Sinister Archer has been fascinated by people's dark machinations and perversions. Sinister Archers claim that they advocate the good and that they only use their arrows to pierce the hearts of those who have committed many crimes and misdeeds. However, they are still viewed by the public with much suspicion due to their dubious appearance. To keep their identity secret, they often hide themselves behind a skull-like face guard that can easily withstand even axe and club blows.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Spirit_Caller_Male_Addon_3.png", "Outfit_Spirit_Caller_Female_Addon_3.png" }, - name = "Full Spirit Caller Outfit", - price = 600, - sexId = {female = 698, male = 699}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou are in love with the deep soul of Mother Earth and prefer to walk in the shadows of her wooden children? Choose the Spirit Caller outfit to live in harmony with nature.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sun_Priest_Male_Addon_3.png", "Outfit_Sun_Priest_Female_Addon_3.png" }, - name = "Full Sun Priest Outfit", - price = 750, - sexId = {female = 1024, male = 1023}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you worship warm temperatures and are opposed to the thought of long and dark winter nights? Do you refuse to spend countless evenings in front of your chimney while ice-cold wind whistles through the cracks and niches of your house? It is time to stop freezing and to become an honourable Sun Priest! With this stylish outfit, you can finally show the world your unconditional dedication and commitment to the sun!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Trophy_Hunter_Male_Addon_3.png", "Outfit_Trophy_Hunter_Female_Addon_3.png" }, - name = "Full Trophy Hunter Outfit", - price = 870, - sexId = {female = 900, male = 899}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou spend hours in the woods in search of wild and rare animals? Countless stuffed skulls of deer, wolves and other creatures are decorating your walls? Now you have the chance to present your trophies in public. Become a Trophy Hunter and cover your shoulders with the finest bear skulls!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Winter_Warden_Male_Addon_3.png", "Outfit_Winter_Warden_Female_Addon_3.png" }, - name = "Full Winter Warden Outfit", - price = 870, - sexId = {female = 852,male = 853}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe warm and cosy cloak of the Winter Warden outfit will keep you warm in every situation. Best thing, it is not only comfortable but fashionable as well. You will be the envy of any snow queen or king, guaranteed!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Citizen_Male.png", "Outfit_Retro_Citizen_Female.png" }, - name = "Retro Citizen", - price = 870, - sexId = {female = 975,male = 974}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nDo you still remember your first stroll through the streets of Thais? For old times' sake, walk the paths of Nostalgia as a Retro Citizen!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Hunter_Male.png", "Outfit_Retro_Hunter_Female.png" }, - name = "Retro Hunter", - price = 870, - sexId = {female = 973,male = 972}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWhenever you pick up your bow and spears, you walk down memory lane and think of your early days? Treat yourself with the fashionable Retro Hunter outfit and hunt some good old monsters from your childhood.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Knight_Male.png", "Outfit_Retro_Knight_Female.png" }, - name = "Retro Knight", - price = 870, - sexId = {female = 971,male = 970}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWho needs a fancy looking sword with bling-bling and ornaments? Back in the days, we survived without such unnecessary accessories! Time to show those younkers what a Retro Knight is made of.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Mage_Male.png", "Outfit_Retro_Mage_Female.png" }, - name = "Retro Wizzard", - price = 870, - sexId = {female = 969, male = 968}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nDress up as a Retro Mage and you will always cut a fine figure on the battleground while eliminating your enemies with your magical powers the old-fashioned way.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Nobleman_Male.png", "Outfit_Retro_Nobleman_Female.png" }, - name = "Retro Noblewoman", - price = 870, - sexId = { female = 967, male = 966}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nKing Tibianus has invited you to a summer ball and you have nothing to wear for this special event? Do not worry, the Retro Noble(wo)man outfit makes you a real eye catcher on every festive occasion.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Summoner_Male.png", "Outfit_Retro_Summoner_Female.png" }, - name = "Retro Summoner", - price = 870, - sexId = {female = 965, male = 964}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWhile the Retro Mage usually throws runes and mighty spells directly at the enemies, the Retro Summoner outfit might be the better choice for Tibians that prefer to send mighty summons to the battlefield to keep their enemies at distance.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Warrior_Male.png", "Outfit_Retro_Warrior_Female.png" }, - name = "Retro Warrior", - price = 870, - sexId = {female = 963, male = 962}, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nYou are fearless and strong as a behemoth but have problems finding the right outfit for your adventures? The Retro Warrior outfit is a must-have for all fashion-conscious old-school Tibians out there.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Trailblazer_Male_Addon_3.png", "Outfit_Trailblazer_Female_Addon_3.png" }, - name = "Full Trailblazer Outfit", - price = 600, - sexId = {female = 1293, male = 1292}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nThe Trailblazer is on a mission of enlightenment and carries the flame of wisdom near and far. The everlasting shine brightens the hearts and minds of all creatures its rays touch, bringing light even to the darkest corners of the world as a beacon of insight and knowledge.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Jouster_Male_Addon_3.png", "Outfit_Jouster_Female_Addon_3.png" }, - name = "Full Jouster Outfit", - price = 870, - sexId = {female = 1332, male = 1331}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nThe Jouster is all geared up for a tournament, ready to partake in festive activities involving friendly competition to prove their chivalry. However, being well-armoured, they are also a force to be reckoned with on the battlefield, especially with a trusty steed at their service.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Rune_Master_Male_Addon_3.png", "Outfit_Rune_Master_Female_Addon_3.png" }, - name = "Full Rune Master Outfit", - price = 870, - sexId = {female = 1385, male = 1384}, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Rune Master has dedicated their whole life to the study and mastery of runes. They are intrigued by the ancient symbols, shrouded in mystery, and how their magic works. Rune Masters have a deep understanding of the awesome power they are wielding and can make use of the full potential of runes.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - home = true, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - --House - { - icons = { "Category_HouseTools.png" }, - name = "Houses", - rookgaard = true, - subclasses = {"Decorations", "Furniture", "Upgrades", "Hirelings", "Hirelings Dresses"}, - }, - { - icons = { "Category_HouseDecorations.png" }, - name = "Decorations", - parent = "Houses", - offers = { - -- Decorations - { - icons = { "Alchemistic_Bookstand.png" }, - name = "Alchemistic Bookstand", - price = 100, - itemtype = 32028, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Cupboard.png" }, - name = "Alchemistic Cupboard", - price = 50, - itemtype = 32038, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Scales.png" }, - name = "Alchemistic Scales", - price = 120, - itemtype = 32032, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "All-Seeing_Tapestry.png" }, - name = "All-Seeing Tapestry", - price = 60, - itemtype = 26106, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Anglerfish_Lamp.png" }, - name = "Anglerfish Lamp", - price = 120, - itemtype = 32259, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Arrival_at_Thais_Painting.png" }, - name = "Arrival The Thais Paint", - price = 50, - itemtype = 32046, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Azure_Carpet.png" }, - name = "Azure Carpet", - price = 35, - itemtype = 26366, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Dragon.png" }, - name = "Baby Dragon", - price = 250, - itemtype = 26098, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Polar_Bear.png" }, - name = "Baby Polar Bear", - price = 250, - itemtype = 37625, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Rotworm.png" }, - name = "Baby Rotworm", - price = 150, - itemtype = 32390, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Unicorn.png" }, - name = "Baby Unicorn", - price = 250, - itemtype = 36538, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Bamboo_Mat.png" }, - name = "Bamboo Mat", - price = 25, - itemtype = 26089, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Barrel_&_Anchor_Lamp.png" }, - name = "Barrel & Anchor Lamp", - price = 80, - itemtype = 36772, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bath_Tub.png" }, - name = "Bath Tub", - price = 250, - itemtype = 29312, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bellflower.png" }, - name = "Bellflower", - price = 50, - itemtype = 32396, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bitter-Smack_Leaf.png" }, - name = "Bitter-Smack Leaf", - price = 50, - itemtype = 27893, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blooming_Cactus.png" }, - name = "Blooming Cactus", - price = 50, - itemtype = 27892, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blue_Round_Cushion.png" }, - name = "Blue Round Cushion", - price = 50, - itemtype = 36057, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blue_Square_Cushion.png" }, - name = "Blue Square Cushion", - price = 50, - itemtype = 36054, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Brocade_Tapestry.png" }, - name = "Brocade Tapestry", - price = 50, - itemtype = 26381, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carnivorous_Plant.png" }, - name = "Carnivorous Plant", - price = 50, - itemtype = 33417, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cat_in_a_Basket.png" }, - name = "Cat in a Basket", - price = 150, - itemtype = 26107, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Chameleon.png" }, - name = "Chamaleon", - price = 250, - itemtype = 27889, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Chest_of_Abundance.png" }, - name = "Chest of Abundance", - price = 120, - itemtype = 33516, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Colourful_Carpet.png" }, - name = "Colourful Carpet", - price = 35, - itemtype = 27085, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Crested_Carpet.png" }, - name = "Crested Carpet", - price = 25, - itemtype = 29388, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Crimson_Carpet.png" }, - name = "Crimson Carpet", - price = 35, - itemtype = 26363, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Crystal_Lamp.png" }, - name = "Crystal Lamp", - price = 80, - itemtype = 36031, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Curly_Hortensis_Lamp.png" }, - name = "Curly Hortensis Lamp", - price = 120, - itemtype = 36530, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dark_Parquet.png" }, - name = "Dark Parquet", - price = 30, - itemtype = 26369, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Decorated_Carpet.png" }, - name = "Decorated Carpet", - price = 35, - itemtype = 29390, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Pet.png" }, - name = "Demon Pet", - price = 250, - itemtype = 29409, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Skull.png" }, - name = "Demon Skull", - price = 50, - itemtype = 36047, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Diamond_Carpet.png" }, - name = "Diamond Carpet", - price = 25, - itemtype = 27088, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dog_House.png" }, - name = "Dog House", - price = 150, - itemtype = 26353, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dungeon_Scene_Painting.png" }, - name = "Dungeon Scene Painting", - price = 100, - itemtype = 32045, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Emerald_Carpet.png" }, - name = "Emerald Carpet", - price = 35, - itemtype = 26367, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fennec.png" }, - name = "Fennec", - price = 150, - itemtype = 32394, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} to trigger an animation feed it with meat, ham, dragon ham, haunch of a boar, roasted meat or bug meat\n{info} can be fed once every 65 seconds\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Bust.png" }, - name = "Ferumbras Dust", - price = 70, - itemtype = 32040, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}\n{useicon} house owner can use it to display a duplicate of an owned Ferumbras' Hat on this bust - also works if the character has already turned Ferumbras' hat in to earn the outfit addon", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Portrait.png" }, - name = "Ferumbras Portrait", - price = 100, - itemtype = 32048, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Filled_Shoes.png" }, - name = "Festive Filled Shoes", - price = 50, - itemtype = 35021, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Fireplace.png" }, - name = "Festive Fireplace", - price = 180, - itemtype = 35027, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Pile_of_Presents.png" }, - name = "Festive Pile of Presents", - price = 50, - itemtype = 35039, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Pyramid.png" }, - name = "Festive Pyramid", - price = 120, - itemtype = 35042, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Rocking_Chair.png" }, - name = "Festive Rocking Chair", - price = 50, - itemtype = 35035, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Sack_of_Presents.png" }, - name = "Festive Sack of Presents", - price = 50, - itemtype = 35041, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Sleigh.png" }, - name = "Festive Sleigh", - price = 50, - itemtype = 35038, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Table.png" }, - name = "Festive Table", - price = 100, - itemtype = 35023, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Tree.png" }, - name = "Festive Tree", - price = 180, - itemtype = 35031, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fish_in_a_Tank.png" }, - name = "Fish Tank", - price = 180, - itemtype = 26347, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Flowery_Carpet.png" }, - name = "Flowery Carpet", - price = 35, - itemtype = 27084, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fluorescent_Fungi.png" }, - name = "Fluorescent Fungi", - price = 60, - itemtype = 33491, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Forget-Me-Not.png" }, - name = "Forget-Me-Not", - price = 50, - itemtype = 32397, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Fur_Carpet.png" }, - name = "Fur Carpet", - price = 30, - itemtype = 27087, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Gloomy_Poisonous_Fungi.png" }, - name = "Gloomy Poisonous Fungi", - price = 60, - itemtype = 33497, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Glowing_Sulphur_Fungi.png" }, - name = "Glowing Sulphur Fungi", - price = 60, - itemtype = 33495, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Golden_Demon_Skull.png" }, - name = "Golden Demon Skull", - price = 80, - itemtype = 36046, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - description = "Buy an incredible Golden Dragon Tapestry to decorate your home.", - icons = { "Golden_Dragon_Tapestry.png" }, - name = "Golden Dragon Tapestry", - price = 70, - itemtype = 26379, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{info} drag the unwrapped tapestry to a wall to hang it up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Golden_Minotaur_Skull.png" }, - name = "Golden Minotaur Skull", - price = 100, - itemtype = 36044, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Green_Round_Cushion.png" }, - name = "Green Round Cushion", - price = 50, - itemtype = 36056, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Green_Square_Cushion.png" }, - name = "Green Square Cushion", - price = 50, - itemtype = 36053, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hamster_in_a_Wheel.png" }, - name = "Hamster in a Wheel", - price = 180, - itemtype = 26100, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Hedgehog.png" }, - name = "Hedgehog", - price = 150, - itemtype = 36515, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Weapons_Rack.png" }, - name = "Hrodmir Weapons Rack", - price = 90, - itemtype = 29317, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Idol_Lamp.png" }, - name = "Idol Lamp", - price = 80, - itemtype = 36049, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Incomprehensible_Riches.png" }, - name = "Incomprehensible Riches", - price = 90, - itemtype = 33515, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "King_Tibianus_Bust.png" }, - name = "King Tibianus Bust", - price = 50, - itemtype = 32049, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Light_of_Change.png" }, - name = "Light of Change", - price = 180, - itemtype = 32023, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Light_Parquet.png" }, - name = "Light Parquet", - price = 30, - itemtype = 26368, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Predator_Lamp.png" }, - name = "Lit Predator Lamp", - price = 60, - itemtype = 26092, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Protectress_Lamp.png" }, - name = "Lit Protectress Lamp", - price = 90, - itemtype = 26096, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Skull_Lamp.png" }, - name = "Lit Skull Lamp", - price = 90, - itemtype = 27103, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Little_Big_Flower_Lamp.png" }, - name = "Little Big Flower Lamp", - price = 80, - itemtype = 36532, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lordly_Tapestry.png" }, - name = "Lordly Tapestry", - price = 50, - itemtype = 26104, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Luminescent_Fungi.png" }, - name = "Luminescent Fungi", - price = 60, - itemtype = 33493, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Marble_Floor.png" }, - name = "Marble Floor", - price = 30, - itemtype = 26376, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Menacing_Tapestry.png" }, - name = "Menacing Tapestry", - price = 70, - itemtype = 26105, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mermaid_Figure_Head.png" }, - name = "Mermaid Figure Head", - price = 120, - itemtype = 26105, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Minotaur_Skull.png" }, - name = "Minotaur Skull", - price = 70, - itemtype = 36045, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Model_Ship_Lamp.png" }, - name = "Model Ship Lamp", - price = 80, - itemtype = 36777, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Monkey.png" }, - name = "Monkey", - price = 180, - itemtype = 36790, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Mystic_Carpet.png" }, - name = "Mystic Carpet", - price = 35, - itemtype = 29354, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Night_Sky_Carpet.png" }, - name = "Night Sky Carpet", - price = 25, - itemtype = 27090, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Painting_of_Tibiasula.png" }, - name = "Painting of Tibiasula", - price = 250, - itemtype = 33518, - itemtype = 33519, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Parrot.png" }, - name = "Parrot", - price = 180, - itemtype = 27100, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Patterned_Carpet.png" }, - name = "Patterned Carpet", - price = 30, - itemtype = 27089, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pile_of_Alchemistic_Books.png" }, - name = "Pile of Alchemistic Books", - price = 120, - itemtype = 32036, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pink_Roses.png" }, - name = "Pink Roses", - price = 50, - itemtype = 27894, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Flag.png" }, - name = "Pirate Flag", - price = 50, - itemtype = 36780, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Ship_Ballista.png" }, - name = "Pirate Ship Ballista", - price = 120, - itemtype = 36768, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Skeleton_Cage.png" }, - name = "Pirate Skeleton Cage", - price = 120, - itemtype = 36782, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Treasure_Chest.png" }, - name = "Pirate Treasure Chest", - price = 120, - itemtype = 36771, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Treasure_Map.png" }, - name = "Pirate Treasure Map", - price = 50, - itemtype = 36781, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Queen_Eloise_Bust.png" }, - name = "Queen Eloise Bust", - price = 50, - itemtype = 32043, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Red_Geranium.png" }, - name = "Red Geranium", - price = 50, - itemtype = 32398, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Red_Roses.png" }, - name = "Red Roses", - price = 50, - itemtype = 27895, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Exalted_Sarcophagus.png" }, - name = "Sarcophagus", - price = 120, - itemtype = 36518, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Shaggy_Carpet.png" }, - name = "Shaggy Carpet", - price = 30, - itemtype = 29352, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ship's_Wheel.png" }, - name = "Ship's Wheel", - price = 50, - itemtype = 36783, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - description = "Buy an incredible Star Carpet to decorate your home.", - icons = { "Rolled-up_Star_Carpet.png" }, - name = "Star Carpet", - price = 25, - itemtype = 27091, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Piled-up_Stone_Tiles.png" }, - name = "Stone Tiles", - price = 25, - itemtype = 29357, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Striped_Carpet .png" }, - name = "Striped Carpet", - price = 30, - itemtype = 27086, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Stuffed_Bear_Display.png" }, - name = "Stuffed Bear Display", - price = 90, - itemtype = 33499, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Stuffed_Teddy_Display.png" }, - name = "Stuffed Teddy Display", - price = 50, - itemtype = 33501, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sulphur_Blossom_Lamp.png" }, - name = "Sulphur Blossom Lamp", - price = 80, - itemtype = 36558, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sword_Tapestry.png" }, - name = "Sword Tapestry", - price = 60, - itemtype = 26380, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Terrarium_Snake.png" }, - name = "Terrarium Snake", - price = 180, - itemtype = 29407, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Spider_in_a_Terrarium.png" }, - name = "Terrarium Spider", - price = 180, - itemtype = 29314, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tibia_Streets_Painting.png" }, - name = "Tibia Streets Painting", - price = 100, - itemtype = 32047, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Torch_of_Change.png" }, - name = "Torch of Change", - price = 120, - itemtype = 33175, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Lamp.png" }, - name = "Vengothic Lamp", - price = 180, - itemtype = 27886, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Venorean_Table_Clock.png" }, - name = "Venorean Table Clock", - price = 120, - itemtype = 29348, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Verdant_Carpet.png" }, - name = "Verdant Carpet", - price = 30, - itemtype = 29350, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Violet_Round_Cushion.png" }, - name = "Violet Round Cushion", - price = 50, - itemtype = 36055, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Violet_Square_Cushion.png" }, - name = "Violet Square Cushion", - price = 50, - itemtype = 36052, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Wheat_Carpet.png" }, - name = "Wheat Carpet", - price = 30, - itemtype = 29387, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_White_Fur_Carpet.png" }, - name = "White Fur Carpet", - price = 30, - itemtype = 26088, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "White_Shark_Trophy.png" }, - name = "White Shark Trophy", - price = 80, - itemtype = 36786, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Wooden_Planks.png" }, - name = "Wooden Planks", - price = 25, - itemtype = 29359, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Yalaharian_Carpet.png" }, - name = "Yalaharian Carpet", - price = 35, - itemtype = 26087, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Yellow_Roses.png" }, - name = "Yellow Roses", - price = 50, - itemtype = 27896, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - --Furniture - { - icons = { "Category_HouseFurniture.png" }, - name = "Furniture", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Alchemistic_Cabinet.png" }, - name = "Alchemistic Cabinet", - price = 100, - itemtype = 32020, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Chair.png" }, - name = "Alchemistic Chair", - price = 50, - itemtype = 32018, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Table.png" }, - name = "Alchemistic Table", - price = 80, - itemtype = 32021, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Cabinet.png" }, - name = "Comfy Cabinet", - price = 100, - itemtype = 33513, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Chair.png" }, - name = "Comfy Chair", - price = 70, - itemtype = 33505, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Chest.png" }, - name = "Comfy Chest", - price = 60, - itemtype = 33509, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Table.png" }, - name = "Comfy Table", - price = 60, - itemtype = 33507, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Cabinet.png" }, - name = "Dwarven Stone Cabinet", - price = 100, - itemtype = 36027, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Chair.png" }, - name = "Dwarven Stone Chair", - price = 50, - itemtype = 36020, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Chest.png" }, - name = "Dwarven Stone Chest", - price = 80, - itemtype = 36022, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Table.png" }, - name = "Dwarven Stone Table", - price = 50, - itemtype = 36026, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Cabinet.png" }, - name = "Ferocious Cabinet", - price = 110, - itemtype = 26077, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Chair.png" }, - name = "Ferocious Chair", - price = 50, - itemtype = 26065, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Table.png" }, - name = "Ferocious Table", - price = 50, - itemtype = 26070, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Trunk.png" }, - name = "Ferocious Trunk", - price = 80, - itemtype = 26079, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Chair.png" }, - name = "Hrodmir Chair", - price = 50, - itemtype = 36528, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Chest.png" }, - name = "Hrodmir Chest", - price = 80, - itemtype = 36522, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Cupboard.png" }, - name = "Hrodmir Cupboard", - price = 100, - itemtype = 36540, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Table.png" }, - name = "Hrodmir Table", - price = 50, - itemtype = 36514, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Cabinet.png" }, - name = "Magnificent Cabinet", - price = 100, - itemtype = 26075, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Chair.png" }, - name = "Magnificent Chair", - price = 60, - itemtype = 26061, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Table.png" }, - name = "Magnificent Table", - price = 60, - itemtype = 26074, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Trunk.png" }, - name = "Magnificent Trunk", - price = 70, - itemtype = 26083, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Cabinet.png" }, - name = "Ornate Cabinet", - price = 100, - itemtype = 29398, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Chair.png" }, - name = "Ornate Chair", - price = 50, - itemtype = 29394, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Chest.png" }, - name = "Ornate Chest", - price = 80, - itemtype = 29401, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Table.png" }, - name = "Ornate Table", - price = 50, - itemtype = 29397, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Round_Side_Table.png" }, - name = "Round Side Table", - price = 50, - itemtype = 36043, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Cabinet.png" }, - name = "Rustic Cabinet", - price = 100, - itemtype = 26356, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Chair.png" }, - name = "Rustic Chair", - price = 50, - itemtype = 26351, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Table.png" }, - name = "Rustic Table", - price = 50, - itemtype = 26354, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Trunk.png" }, - name = "Rustic Trunk", - price = 80, - itemtype = 26358, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Cabinet.png" }, - name = "Skeletal Cabinet", - price = 100, - itemtype = 33415, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Chair.png" }, - name = "Skeletal Chair", - price = 50, - itemtype = 32260, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Chest.png" }, - name = "Skeletal Chest", - price = 80, - itemtype = 32266, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Table.png" }, - name = "Skeletal Table", - price = 50, - itemtype = 32264, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Square_Side_Table.png" }, - name = "Square Side Table", - price = 50, - itemtype = 29397, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Cabinet.png" }, - name = "Vengothic Cabinet", - price = 100, - itemtype = 27903, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Chair.png" }, - name = "Vengothic Chair", - price = 50, - itemtype = 27899, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Chest.png" }, - name = "Vengothic Chest", - price = 80, - itemtype = 27905, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Table.png" }, - name = "Vengothic Table", - price = 50, - itemtype = 27901, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Cabinet.png" }, - name = "Verdant Cabinet", - price = 100, - itemtype = 29341, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Chair.png" }, - name = "Verdant Chair", - price = 50, - itemtype = 29339, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Table.png" }, - name = "Verdant Table", - price = 80, - itemtype = 29347, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Trunk.png" }, - name = "Verdant Trunk", - price = 50, - itemtype = 29343, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wooden_Bookcase.png" }, - name = "Wooden Bookcase", - price = 50, - itemtype = 36029, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- Upgrades - { - icons = { "Category_HouseUpgrades.png" }, - name = "Upgrades", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Reward_Shrine.png" }, - name = "Daily Reward Shrine", - price = 150, - itemtype = 29022, - count = 1, - description = "Pick up your daily reward comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it to open the reward wall\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Exercise_Dummy.png" }, - name = "Demon Exercise Dummy", - price = 900, - itemtype = 32145, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n{useicon} use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Exercise_Dummy.png" }, - name = "Ferumbras Exercise Dummy", - price = 900, - itemtype = 32143, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n{useicon} use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Gilded_Imbuing_Shrine.png" }, - name = "Gilded Imbuing Shrine", - price = 200, - itemtype = 27851, - count = 1, - description = "Enhance your equipment comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it with an imbuable item to open the imbuing dialog\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Imbuing_Shrine.png" }, - name = "Imbuing Shrine", - price = 150, - itemtype = 27843, - count = 1, - description = "Enhance your equipment comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it with an imbuable item to open the imbuing dialog\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mailbox.png" }, - name = "Mailbox", - price = 150, - itemtype = 26055, - count = 1, - description = "Send your letters and parcels right from your own home!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Monk_Exercise_Dummy.png" }, - name = "Monk Exercise Dummy", - price = 900, - itemtype = 32147, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n- use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Mailbox.png" }, - name = "Ornate Mailbox", - price = 200, - itemtype = 26057, - count = 1, - description = "Send your letters and parcels right from your own home!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Shiny_Reward_Shrine.png" }, - name = "Shiny Daily Reward Shrine", - price = 200, - itemtype = 29024, - count = 1, - description = "Pick up your daily reward comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it to open the reward wall\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- Boost - { - icons = { "Category_Boosts.png" }, - name = "Boosts", - offers = { - { - icons = { "XP_Boost.png" }, - name = "XP Boost", - price = 30, - id = 65010, - description = "Purchase a boost that increases the experience points your character gains from hunting by 50%!\n\n{character}\n{info} lasts for 1 hour hunting time\n{info} paused if stamina falls under 14 hours\n{info} can be purchased up to 5 times between 2 server saves\n{info} price increases with every purchase\n{info} cannot be purchased if an XP boost is already active", - type = GameStore.OfferTypes.OFFER_TYPE_EXPBOOST, - }, - }, - rookgaard = true, - state = GameStore.States.STATE_NONE, - }, - -- Extras - { - icons = { "Category_Extras.png" }, - name = "Extras", - rookgaard = true, - subclasses = {"Extra Services", "Useful Things"}, - }, - -- Extras Services - { - icons = { "Category_ExtraServices.png" }, - name = "Extra Services", - parent = "Extras", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Name_Change.png" }, - name = "Character Name Change", - price = 250, - id = 65002, - description = "Tired of your current character name? Purchase a new one!\n\n{character}\n{info} relog required after purchase to finalise the name change", - type = GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE, - }, - { - icons = { "Sex_Change.png" }, - name = "Character Sex Change", - price = 120, - id = 65003, - description = "Turns your female character into a male one - or vice versa.\n\n{character}\n{activated}\n{info} you will keep all outfits you have purchased or earned in quest", - type = GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE, - }, - }, - }, - -- Usefull Things - { - icons = { "Category_UsefulThings.png" }, - name = "Useful Things", - parent = "Extras", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Gold_Converter.png" }, - name = "Gold Converter", - price = 5, - itemtype = 26378, - charges = 500, - description = "Changes either a stack of 100 gold pieces into 1 platinum coin, or a stack of 100 platinum coins into 1 crystal coin!\n\n{character}\n{storeinbox}\n{useicon} use it on a stack of 100 to change it to the superior currency\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Gold_Pouch.png" }, - name = "Gold Pouch", - price = 900, - itemtype = 26377, - count = 1, - description = "Carries as many gold, platinum or crystal coins as your capacity allows, however, no other items.\n\n{character}\n{storeinbox}\n{once}\n{useicon} use it to open it\n{info} always placed on the first position of your Store inbox", - type = GameStore.OfferTypes.OFFER_TYPE_POUNCH, - }, - { - icons = { "Instant_Reward_Access.png" }, - name = "Instant Reward Access", - price = 100, - id = 2, - count = 1, - description = "No matter where you are in Tibia, claim your daily reward on the spot!\n\n{character}\n{info} added to your reward wall\n{info} maximum amount that can be owned by character: 90", - type = GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS, - }, - { - icons = { "Charm_Expansion_Offer.png" }, - name = "Charm Expansion", - price = 450, - id = 65005, - description = "Assign as many of your unlocked Charms as you like and get a 25% discount whenever you are removing a Charm from a creature!\n\n{character}\n{once}", - type = GameStore.OfferTypes.OFFER_TYPE_CHARMS, - }, - { - icons = { "Magic_Gold_Converter.png" }, - name = "Magic Gold Converter", - price = 15, - itemtype = 32109, - charges = 500, - description = "Changes automatically either a stack of 100 gold pieces into 1 platinum coin, or a stack of 100 platinum coins into 1 crystal coin!\n\n{character}\n{storeinbox}\n{useicon} use it to activate or deactivate the automatic conversion\n{info} converts all stacks of 100 gold or platinum in the inventory whenever it is activated\n{info} deactivated upon purchase\n{info} usable for 500 conversions a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Permanent_Prey_Slot.png" }, - name = "Permanent Prey Slot", - price = 900, - id = 65008, - description = "Get an additional prey slot to activate additional prey!\n\n{character}\n{info} maximum amount that can be owned by character: 3\n{info} added directly to Prey dialog", - type = GameStore.OfferTypes.OFFER_TYPE_PREYSLOT, - }, - { - icons = { "Prey_Bonus_Reroll.png" }, - name = "Prey Wildcard", - price = 50, - id = 1, - count = 5, - description = "Use Prey Wildcards to reroll the bonus of an active prey, to lock your active prey or to select a prey of your choice.\n\n{character}\n{info} added directly to Prey dialog\n{info} maximum amount that can be owned by character: 50", - type = GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, - }, - { - icons = { "Prey_Bonus_Reroll.png" }, - name = "Prey Wildcard", - price = 200, - count = 20, - description = "Use Prey Wildcards to reroll the bonus of an active prey, to lock your active prey or to select a prey of your choice.\n\n{character}\n{info} added directly to Prey dialog\n{info} maximum amount that can be owned by character: 50", - type = GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, - }, - { - icons = { "Temple_Teleport.png" }, - name = "Temple Teleport", - price = 15, - description = "Teleports you instantly to your home temple.\n\n{character}\n{useicon} use it to teleport you to your home temple\n{battlesign}\n{info} does not work in no-logout zones or close to a character's home temple", - type = GameStore.OfferTypes.OFFER_TYPE_TEMPLE, - }, - }, - }, - --Tournament - { - icons = { "Category_Tournament.png" }, - name = "Tournament", - rookgaard = true, - subclasses = {"Tickets", "Exclusive Offers"}, - }, - -- Tickets - { - icons = { "Category_Tickets.png" }, - parent = "Tournament", - name = "Tickets", - rookgaard = true, - offers = { - { - icons = { "Tournament_Restricted.png" }, - name = "Restricted Tournament Ticket", - price = 500, - }, - }, - -- Exclusive Offers - }, { - icons = { "Category_ExclusiveOffers.png" }, - name = "Exclusive Offers", - parent = "Tournament", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Cerberus_Champion.png" }, - name = "Cerberus Champion", - price = 1250, - id = 146, - description = "{info} usable by all characters of the account\n{speedboost}\n\nA fierce and grim guardian of the underworld has risen to fight side by side with the bravest warriors in order to send evil creatures into the realm of the dead. The three headed Cerberus Champion is constantly baying for blood and using its sharp fangs it easily rips apart even the strongest armour and shield.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Jousting_Eagle.png" }, - name = "Jousting Eagle", - price = 1250, - id = 145, - description = "{info} usable by all characters of the account\n{speedboost}\n\nHigh above the clouds far away from dry land, the training of giant eagles takes place. Only the cream of the crop is able to survive in such harsh environment long enough to call themselves Jousting Eagles while the weaklings find themselves at the bottom of the sea. The tough ones become noble and graceful mounts that are well known for their agility and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Outfit_Lion_of_War_Male_Addon_3.png", "Outfit_Lion_of_War_Female_Addon_3.png" }, - name = "Full Lion of War Outfit", - price = 1750, - sexId = {female = 1207, male = 1206}, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Lion of War has fought on countless battlefields and never lost once. Enemies tremble with fear when he batters his sword against his almighty shield. Realising that a Lion of War knows no mercy, his opponents often surrender before the battle even begins.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Outfit_Veteran_Paladin_Male_Addon_3.png", "Outfit_Veteran_Paladin_Female_Addon_3.png" }, - name = "Full Veteran Paladin Outfit", - price = 1750, - sexId = {female = 1205, male = 1204}, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Veteran Paladin has mastered the art of distance fighting. No matter how far away his prey may be, a marksman like the Veteran Paladin will always hit with extraordinary precision. No one can escape his keen hawk-eyed vision and even small stones become deadly weapons in his hands.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Outfit_Void_Master_Male_Addon_3.png", "Outfit_Void_Master_Female_Addon_3.png" }, - name = "Full Void Master Outfit", - price = 1750, - sexId = {female = 1203, male = 1202}, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAccording to ancient rumours, the pulsating orb that the Void Master balances skilfully on the tip of his staff consists of powerful cosmic spheres. If you gaze too long into the infinite emptiness inside the orb, its powers will absorb your mind.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Cerberus_Champion_Puppy.png" }, - name = "Cerberus Champion Puppy", - price = 800, - itemtype = 36299, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Jousting_Eagle_Baby.png" }, - name = "Jousting Eagle Baby", - price = 800, - itemtype = 36297, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Sublime_Tournament_Accolade.png" }, - name = "Sublime Tournament Accolade", - price = 500, - itemtype = 36307, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Sublime_Tournament_Carpet.png" }, - name = "Sublime Tournament Carpet", - price = 70, - itemtype = 36302, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use an unwrapped carpet to roll it out or up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Tournament_Accolade.png" }, - name = "Tournament Accolade", - price = 500, - itemtype = 36305, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Tournament_Carpet.png" }, - name = "Tournament Carpet", - price = 70, - itemtype = 36301, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use an unwrapped carpet to roll it out or up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - coinType = GameStore.CointType.Tournament, - }, - { - icons = { "Baby_Vulcongra_Accolade.png" }, - name = "Baby Vulcongra", - price = 800, - itemtype = 37743, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Guzzlemaw_Grub.png" }, - name = "Guzzlemaw Grub", - price = 800, - itemtype = 37742, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch.png" }, - name = "Cozy Couch", - price = 100, - itemtype = 37783, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Left_End.png" }, - name = "Cozy Couch Left End", - price = 100, - itemtype = 37787, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Right_End.png" }, - name = "Cozy Couch Right End", - price = 100, - itemtype = 37791, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Corner.png" }, - name = "Cozy Couch Corner", - price = 100, - itemtype = 37799, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Brain_Squid.png" }, - name = "Baby Brain Squid", - price = 800, - itemtype = 37744, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table.png" }, - name = "Carved Table", - price = 100, - itemtype = 37807, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table_Centre.png" }, - name = "Carved Table Centre", - price = 100, - itemtype = 37809, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table_Corner.png" }, - name = "Carved Table Corner", - price = 100, - itemtype = 37804, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, -} - - --- Each outfit must be uniquely identified to distinguish between addons. --- Here we dynamically assign ids for outfits. These ids must be unique. -local runningId = 45000 -for k, category in ipairs(GameStore.Categories) do - if category.offers then - for m, offer in ipairs(category.offers) do - if not offer.id then - if type(offer.count) == "table" then - for i = 1, #offer.price do - offer.id[i] = runningId - runningId = runningId + 1 - end - else - offer.id = runningId - runningId = runningId + 1 - end - end - if not offer.type then - offer.type = GameStore.OfferTypes.OFFER_TYPE_NONE - end - if not offer.coinType then - offer.coinType = GameStore.CointType.Coin - end - end - end -end diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua deleted file mode 100644 index 7ec33cec0ef..00000000000 --- a/data/modules/scripts/gamestore/init.lua +++ /dev/null @@ -1,1846 +0,0 @@ -GameStore = { - ModuleName = "GameStore", - Developers = { "Cjaker", "metabob", "Rick" }, - Version = "1.1", - LastUpdated = "25-07-2020 11:52AM" -} - ---== Enums ==-- -GameStore.OfferTypes = { - OFFER_TYPE_NONE = 0, - OFFER_TYPE_ITEM = 1, - OFFER_TYPE_STACKABLE = 2, - OFFER_TYPE_CHARGES = 3, - OFFER_TYPE_OUTFIT = 4, - OFFER_TYPE_OUTFIT_ADDON = 5, - OFFER_TYPE_MOUNT = 6, - OFFER_TYPE_NAMECHANGE = 7, - OFFER_TYPE_SEXCHANGE = 8, - OFFER_TYPE_HOUSE = 9, - OFFER_TYPE_EXPBOOST = 10, - OFFER_TYPE_PREYSLOT = 11, - OFFER_TYPE_PREYBONUS = 12, - OFFER_TYPE_TEMPLE = 13, - OFFER_TYPE_BLESSINGS = 14, - OFFER_TYPE_PREMIUM = 15, - OFFER_TYPE_POUNCH = 16, - OFFER_TYPE_ALLBLESSINGS = 17, - OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, - OFFER_TYPE_CHARMS = 19, - OFFER_TYPE_HIRELING = 20, - OFFER_TYPE_HIRELING_NAMECHANGE = 21, - OFFER_TYPE_HIRELING_SEXCHANGE = 22, - OFFER_TYPE_HIRELING_SKILL = 23, - OFFER_TYPE_HIRELING_OUTFIT = 24 -} - -GameStore.ActionType = { - OPEN_HOME = 0, - OPEN_PREMIUM_BOOST = 1, - OPEN_CATEGORY = 2, - OPEN_USEFUL_THINGS = 3, - OPEN_OFFER = 4, -} - -GameStore.CointType = { - Coin = 0, - Transferable = 1, - Tournament = 2, -} - -GameStore.Storages = { - expBoostCount = 51052 -} - -GameStore.ConverType = { - SHOW_NONE = 0, - SHOW_MOUNT = 1, - SHOW_OUTFIT = 2, - SHOW_ITEM = 3, - SHOW_HIRELING = 4 -} - -GameStore.ConfigureOffers = { - SHOW_NORMAL = 0, - SHOW_CONFIGURE = 1 -} - -function convertType(type) - local types = { - [GameStore.OfferTypes.OFFER_TYPE_OUTFIT] = GameStore.ConverType.SHOW_OUTFIT, - [GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON] = GameStore.ConverType.SHOW_OUTFIT, - [GameStore.OfferTypes.OFFER_TYPE_MOUNT] = GameStore.ConverType.SHOW_MOUNT, - [GameStore.OfferTypes.OFFER_TYPE_ITEM] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_STACKABLE] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_HOUSE] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_CHARGES] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING] = GameStore.ConverType.SHOW_HIRELING, - } - - if not types[type] then - return GameStore.ConverType.SHOW_NONE - end - - return types[type] -end - -function useOfferConfigure(type) - local types = { - [GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE] = GameStore.ConfigureOffers.SHOW_CONFIGURE - } - - if not types[type] then - return GameStore.ConfigureOffers.SHOW_NORMAL - end - - return types[type] -end - -GameStore.ClientOfferTypes = { - CLIENT_STORE_OFFER_OTHER = 0, - CLIENT_STORE_OFFER_NAMECHANGE = 1, - CLIENT_STORE_OFFER_HIRELING = 10, -} - -GameStore.HistoryTypes = { - HISTORY_TYPE_NONE = 0, - HISTORY_TYPE_GIFT = 1, - HISTORY_TYPE_REFUND = 2 -} - -GameStore.States = { - STATE_NONE = 0, - STATE_NEW = 1, - STATE_SALE = 2, - STATE_TIMED = 3 -} - -GameStore.StoreErrors = { - STORE_ERROR_PURCHASE = 0, - STORE_ERROR_NETWORK = 1, - STORE_ERROR_HISTORY = 2, - STORE_ERROR_TRANSFER = 3, - STORE_ERROR_INFORMATION = 4 -} - -GameStore.ServiceTypes = { - SERVICE_STANDERD = 0, - SERVICE_OUTFITS = 3, - SERVICE_MOUNTS = 4, - SERVICE_BLESSINGS = 5 -} - -GameStore.SendingPackets = { - S_CoinBalance = 0xDF, -- 223 - S_StoreError = 0xE0, -- 224 - S_RequestPurchaseData = 0xE1, -- 225 - S_CoinBalanceUpdating = 0xF2, -- 242 - S_OpenStore = 0xFB, -- 251 - S_StoreOffers = 0xFC, -- 252 - S_OpenTransactionHistory = 0xFD, -- 253 - S_CompletePurchase = 0xFE -- 254 -} - -GameStore.RecivedPackets = { - C_StoreEvent = 0xE9, -- 233 - C_TransferCoins = 0xEF, -- 239 - C_ParseHirelingName = 0xEC, -- 236 - C_OpenStore = 0xFA, -- 250 - C_RequestStoreOffers = 0xFB, -- 251 - C_BuyStoreOffer = 0xFC, -- 252 - C_OpenTransactionHistory = 0xFD, -- 253 - C_RequestTransactionHistory = 0xFE, -- 254 -} - -GameStore.ExpBoostValues = { - [1] = 30, - [2] = 45, - [3] = 90, - [4] = 180, - [5] = 360 -} - -GameStore.DefaultValues = { - DEFAULT_VALUE_ENTRIES_PER_PAGE = 26 -} - -GameStore.DefaultDescriptions = { - OUTFIT = { "This outfit looks nice. Only high-class people are able to wear it!", - "An outfit that was created to suit you. We are sure you'll like it.", - "Legend says only smart people should wear it, otherwise you will burn!" }, - MOUNT = { "This is a fantastic mount that helps to become faster, try it!", - "The first rider of this mount became the leader of his country! legends say that." }, - NAMECHANGE = { "Are you hunted? Tired of that? Get a new name, a new life!", - "A new name to suit your needs!" }, - SEXCHANGE = { "Bored of your character's sex? Get a new sex for him now!!" }, - EXPBOOST = { "Are you tired of leveling slow? try it!" }, - PREYSLOT = { "It's hunting season! Activate a prey to gain a bonus when hunting a certain monster. Every character can purchase one Permanent Prey Slot, which enables the activation of an additional prey. \nIf you activate a prey, you can select one monster out of nine. The bonus for your prey will be selected randomly from one of the following: damage boost, damage reduction, bonus XP, improved loot. The bonus value may range from 5% to 50%. Your prey will be active for 2 hours hunting time: the duration of an active prey will only be reduced while you are hunting." }, - PREYBONUS = { "You activated a prey but do not like the randomly selected bonus? Roll for a new one! Here you can purchase five Prey Bonus Rerolls! \nA Bonus Reroll allows you to get a bonus with a higher value (max. 50%). The bonus for your prey will be selected randomly from one of the following: damage boost, damage reduction, bonus XP, improved loot. The 2 hours hunting time will start anew once you have rolled for a new bonus. Your prey monster will stay the same." }, - TEMPLE = { "Need a quick way home? Buy this transportation service to get instantly teleported to your home temple. \n\nNote, you cannot use this service while having a battle sign or a protection zone block. Further, the service will not work in no-logout zones or close to your home temple." } -} - ---==Parsing==-- -GameStore.isItsPacket = function(byte) - for k, v in pairs(GameStore.RecivedPackets) do - if v == byte then - return true - end - end - return false -end - -local function queueSendStoreAlertToUser(message, delay, playerId, storeErrorCode) - storeErrorCode = storeErrorCode and storeErrorCode or GameStore.StoreErrors.STORE_ERROR_NETWORK - addPlayerEvent(sendStoreError, delay, playerId, storeErrorCode, message) -end - -function onRecvbyte(player, msg, byte) - if not configManager.getBoolean(STOREMODULES) then return true end - if player:getVocation():getId() == 0 and not GameStore.haveCategoryRook() then - return player:sendCancelMessage("Store don't have offers for rookgaard citizen.") - end - - local exaust = player:getStorageValue(Storage.StoreExaust) - local currentTime = os.time() - - if byte == GameStore.RecivedPackets.C_StoreEvent then - elseif byte == GameStore.RecivedPackets.C_TransferCoins then - parseTransferCoins(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_OpenStore then - if exaust > currentTime then - player:sendCancelMessage("You are exhausted") - return false - end - local num = currentTime + 1 - player:setStorageValue(Storage.StoreExaust, num) - - parseOpenStore(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_RequestStoreOffers then - parseRequestStoreOffers(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_BuyStoreOffer then - parseBuyStoreOffer(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_OpenTransactionHistory then - parseOpenTransactionHistory(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_RequestTransactionHistory then - parseRequestTransactionHistory(player:getId(), msg) - end - return true -end - -function parseTransferCoins(playerId, msg) - local player = Player(playerId) - if not player then - return false - end - - local reciver = msg:getString() - local amount = msg:getU32() - - if (player:getCoinsBalance() < amount) then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You don't have this amount of coins.") - end - - if reciver:lower() == player:getName():lower() then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You can't transfer coins to yourself.") - end - - local resultId = db.storeQuery("SELECT `account_id` FROM `players` WHERE `name` = " .. db.escapeString(reciver:lower()) .. "") - if not resultId then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "We couldn't find that player.") - end - - local accountId = result.getNumber(resultId, "account_id") - if accountId == player:getAccountId() then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You cannot transfer coin to a character in the same account.") - end - - db.query("UPDATE `accounts` SET `coins` = `coins` + " .. amount .. " WHERE `id` = " .. accountId) - player:removeCoinsBalance(amount) - addPlayerEvent(sendStorePurchaseSuccessful, 550, playerId, "You have transfered " .. amount .. " coins to " .. reciver .. " successfully") - - -- Adding history for both reciver/sender - GameStore.insertHistory(accountId, GameStore.HistoryTypes.HISTORY_TYPE_NONE, player:getName() .. " transfered you this amount.", amount) - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, "You transfered this amount to " .. reciver, -1 * amount) -- negative -end - -function parseOpenStore(playerId, msg) - openStore(playerId) - - local category = GameStore.Categories and GameStore.Categories[1] or nil - if category then - addPlayerEvent(parseRequestStoreOffers, 50, playerId) - end -end - -function parseRequestStoreOffers(playerId, msg) - local player = Player(playerId) - if not player then - return false - end - - local actionType = msg:getByte() - - if actionType == GameStore.ActionType.OPEN_CATEGORY then - local categoryName = msg:getString() - local category = GameStore.getCategoryByName(categoryName) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end - elseif actionType == GameStore.ActionType.OPEN_HOME then - sendHomePage(player:getId()) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, "Home Offers") - end - elseif actionType == GameStore.ActionType.OPEN_PREMIUM_BOOST then - local subAction = msg:getByte() - local category = nil - - if subAction == 0 then - category = GameStore.getCategoryByName("Premium Time") - else - category = GameStore.getCategoryByName("Boosts") - end - - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end - elseif actionType == GameStore.ActionType.OPEN_USEFUL_THINGS then - local subAction = msg:getByte() - local redirectId = subAction - local category = nil - if subAction >= 4 then - category = GameStore.getCategoryByName("Blessings") - else - category = GameStore.getCategoryByName("Useful Things") - end - - -- Third prey slot offerId - -- We can't use offerId 0 - if subAction == 0 then - redirectId = 65008 - end - - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category, redirectId) - end - - elseif actionType == GameStore.ActionType.OPEN_OFFER then - local offerId = msg:getU32() - local category = GameStore.getCategoryByOffer(offerId) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category, offerId) - end - end -end - -function parseBuyStoreOffer(playerId, msg) - local player = Player(playerId) - local id = msg:getU32() - local offer = GameStore.getOfferById(id) - local productType = msg:getByte() - - -- All guarding conditions under which the offer should not be processed must be included here - if (table.contains(GameStore.OfferTypes, offer.type) == false) -- we've got an invalid offer type - or (not player) -- player not found - or (player:getVocation():getId() == 0) and (not GameStore.haveOfferRook(id)) -- we don't have such offer - or (not offer) -- we could not find the offer - or (offer.type == GameStore.OfferTypes.OFFER_TYPE_NONE) -- offer is disabled - or (offer.type ~= GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYBONUS and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYSLOT and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_TEMPLE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_POUNCH and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT and - not offer.id) then - return queueSendStoreAlertToUser("This offer is unavailable [1]", 350, playerId, GameStore.StoreErrors.STORE_ERROR_INFORMATION) - end - - -- At this point the purchase is assumed to be formatted correctly - local offerPrice = offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] or offer.price - - if not player:canRemoveCoins(offerPrice) then - return queueSendStoreAlertToUser("You don't have enough coins. Your purchase has been cancelled.", 250, playerId) - end - - -- Use pcall to catch unhandled errors and send an alert to the user because the client expects it at all times; (OTClient will unlock UI) - -- 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) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_POUNCH then GameStore.processItemPurchase(player, offer.itemtype, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then GameStore.processInstantRewardAccess(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARMS then GameStore.processCharmsPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then GameStore.processSignleBlessingPurchase(player, offer.blessid, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS then GameStore.processAllBlessingsPurchase(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREMIUM then GameStore.processPremiumPurchase(player, offer.id) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_STACKABLE then GameStore.processStackablePurchase(player, offer.itemtype, offer.count, offer.name) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then GameStore.processHouseRelatedPurchase(player, offer.itemtype, offer.count) - 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 GameStore.processOutfitPurchase(player, offer.sexId, offer.addon) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then GameStore.processMountPurchase(player, offer.id) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE then local newName = msg:getString(); GameStore.processNameChangePurchase(player, offer.id, productType, newName, offer.name, offerPrice) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE then GameStore.processSexChangePurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then GameStore.processExpBoostPuchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYSLOT then GameStore.processPreySlotPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT then GameStore.processPreyHuntingSlotPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYBONUS then GameStore.processPreyBonusReroll(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_TEMPLE then GameStore.processTempleTeleportPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARGES then GameStore.processChargesPurchase(player, offer.itemtype, offer.name, offer.charges) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING then local hirelingName = msg:getString(); local sex = msg:getByte(); GameStore.processHirelingPurchase(player, offer, productType, hirelingName, sex) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE then local hirelingName = msg:getString(); GameStore.processHirelingChangeNamePurchase(player, offer, productType, hirelingName) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE then GameStore.processHirelingChangeSexPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL then GameStore.processHirelingSkillPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then GameStore.processHirelingOutfitPurchase(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]"}) - end - end) - - if not pcallOk then - local alertMessage = pcallError.code and pcallError.message or "Something went wrong. Your purchase has been cancelled." - - if not pcallError.code then -- unhandled error - -- log some debugging info - Spdlog.warn("[parseBuyStoreOffer] - Purchase failed due to an unhandled script error. Stacktrace: ".. pcallError) - end - - return queueSendStoreAlertToUser(alertMessage, 500, playerId) - end - - local configure = useOfferConfigure(offer.type) - if configure ~= GameStore.ConfigureOffers.SHOW_CONFIGURE then - player:removeCoinsBalance(offerPrice) - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, offer.name, (offerPrice) * -1) - local message = string.format("You have purchased %s for %d coins.", offer.name, offerPrice) - sendUpdateCoinBalance(playerId) - return addPlayerEvent(sendStorePurchaseSuccessful, 650, playerId, message) - end - return true -end - --- Both functions use same formula! -function parseOpenTransactionHistory(playerId, msg) - local page = 1 - GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE = msg:getByte() - sendStoreTransactionHistory(playerId, page, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) -end - -function parseRequestTransactionHistory(playerId, msg) - local page = msg:getU32() - sendStoreTransactionHistory(playerId, page + 1, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) -end - -local function getCategoriesRook() - local tmpTable, count = {}, 0 - for i, v in pairs(GameStore.Categories) do - if (v.rookgaard) then - tmpTable[#tmpTable + 1] = v - count = count + 1 - end - end - - return tmpTable, count -end - ---==Sending==-- -function openStore(playerId) - local player = Player(playerId) - if not player then - return false - end - - if not GameStore.Categories then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_OpenStore) - - local GameStoreCategories, GameStoreCount = nil, 0 - if (player:getVocation():getId() == 0) then - GameStoreCategories, GameStoreCount = getCategoriesRook() - else - GameStoreCategories, GameStoreCount = GameStore.Categories, #GameStore.Categories - end - - if (GameStoreCategories) then - msg:addU16(GameStoreCount) - for k, category in ipairs(GameStoreCategories) do - msg:addString(category.name) - msg:addByte(category.state or GameStore.States.STATE_NONE) - msg:addByte(#category.icons) - for m, icon in ipairs(category.icons) do - msg:addString(icon) - end - - if category.parent then - msg:addString(category.parent) - else - msg:addU16(0) - end - end - - msg:sendToPlayer(player) - sendCoinBalanceUpdating(playerId, true) - end -end - -function sendOfferDescription(player, offerId, description) - local msg = NetworkMessage() - msg:addByte(0xEA) - msg:addU32(offerId) - msg:addString(description) - msg:sendToPlayer(player) -end - -function Player.canBuyOffer(self, offer) - local playerId = self:getId() - local disabled, disabledReason = 0, "" - if offer.disabled == true or not offer.type then - disabled = 1 - end - - if offer.type ~= GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYSLOT and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYBONUS and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_TEMPLE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_POUNCH and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL and - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT and - not offer.id then - disabled = 1 - end - - if disabled == 1 and offer.disabledReason then - -- dynamic disable - disabledReason = offer.disabledReason - end - - if disabled ~= 1 then - if offer.type == GameStore.OfferTypes.OFFER_TYPE_POUNCH then - local pounch = self:getItemById(26377, true) - if pounch then - disabled = 1 - disabledReason = "You already have Loot Pouch." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then - if self:getBlessingCount(offer.blessid) >= 5 then - disabled = 1 - disabledReason = "You reached the maximum amount for this blessing." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS then - for i = 1, 8 do - if self:getBlessingCount(i) >= 5 then - disabled = 1 - disabledReason = "You already have all Blessings." - break - end - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then - local outfitLookType - if self:getSex() == PLAYERSEX_MALE then - outfitLookType = offer.sexId.male - else - outfitLookType = offer.sexId.female - end - - if outfitLookType then - if offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT and self:hasOutfit(outfitLookType) then - disabled = 1 - disabledReason = "You already have this outfit." - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then - if self:hasOutfit(outfitLookType) then - if self:hasOutfit(outfitLookType, offer.addon) then - disabled = 1 - disabledReason = "You already have this addon." - end - else - disabled = 1 - disabledReason = "You don't have the outfit, you can't buy the addon." - end - end - else - disabled = 1 - disabledReason = "The offer is fake." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then - local hasMount = self:hasMount(offer.id) - if hasMount == true then - disabled = 1 - disabledReason = "You already have this mount." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then - if self:getCollectionTokens() >= 90 then - disabled = 1 - disabledReason = "You already have maximum of reward tokens." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYBONUS then - if self:getPreyBonusRerolls() >= 50 then - disabled = 1 - disabledReason = "You already have maximum of prey wildcards." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARMS then - if self:charmExpansion() then - disabled = 1 - disabledReason = "You already have charm expansion." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYSLOT then - if self:getStorageValue(Prey.Config.StoreSlotStorage) == 1 then - disabled = 1 - disabledReason = "You already have 3 slots released." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then - local remainingBoost = self:getExpBoostStamina() - if self:getStorageValue(GameStore.Storages.expBoostCount) == 6 then - disabled = 1 - disabledReason = "You can't buy XP Boost for today." - end - if (remainingBoost > 0) then - disabled = 1 - disabledReason = "You already have an active XP boost." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING then - if self:getHirelingsCount() >= 10 then - disabled = 1 - disabledReason = "You already have bought the maximum number of allowed hirelings." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL then - local skill = (HIRELING_STORAGE.SKILL + offer.id) - if self:hasHirelingSkill(skill) then - disabled = 1 - disabledReason = "This skill is already unlocked." - end - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then - local outfit = offer.id - HIRELING_STORAGE.OUTFIT - if self:hasHirelingOutfit(outfit) then - disabled = 1 - disabledReason = "This hireling outfit is already unlocked." - end - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE then - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE then - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - end - end - - return {disabled = disabled, disabledReason = disabledReason} -end - -function sendShowStoreOffers(playerId, category, redirectId) - local player = Player(playerId) - if not player then - return false - end - - local version = player:getClient().version - local msg = NetworkMessage() - local haveSaleOffer = 0 - msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString(category.name) - - msg:addU32(redirectId or 0) - - msg:addByte(0) -- Window Type - msg:addByte(0) -- Collections Size - msg:addU16(0) -- Collection Name - - if not category.offers then - msg:addU16(0) - msg:sendToPlayer(player) - return - end - - local offers = {} - local count = 0 - for k, offer in ipairs(category.offers) do - local name = offer.name or "Something Special" - if not offers[name] then - offers[name] = {} - count = count + 1 - offers[name].offers = {} - offers[name].state = offer.state - offers[name].id = offer.id - offers[name].type = offer.type - offers[name].icons = offer.icons - offers[name].basePrice = offer.basePrice - offers[name].description = offer.description - if offer.sexId then - offers[name].sexId = offer.sexId - end - if offer.itemtype then - offers[name].itemtype = offer.itemtype - end - end - table.insert(offers[name].offers, offer) - end - - -- If player doesn't have hireling - if category.name == "Hirelings" then - if player:getHirelingsCount() < 1 then - offers["Hireling Name Change"] = nil - offers["Hireling Sex Change"] = nil - offers["Hireling Trader"] = nil - offers["Hireling Steward"] = nil - offers["Hireling Banker"] = nil - offers["Hireling Cook"] = nil - count = count - 6 - end - end - - msg:addU16(count) - - if count > 0 then - for name, offer in pairs(offers) do - msg:addString(name) - msg:addByte(#offer.offers) - sendOfferDescription(player, offer.id and offer.id or 0xFFFF, offer.description) - for _, off in ipairs(offer.offers) do - xpBoostPrice = nil - if offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then - xpBoostPrice = GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] - end - - msg:addU32(off.id) - msg:addU16(off.count) - msg:addU32(xpBoostPrice or off.price) - msg:addByte(off.coinType or 0x00) - - local disabled, disabledReason = player:canBuyOffer(off).disabled, player:canBuyOffer(off).disabledReason - msg:addByte(disabled) - if disabled == 1 then - msg:addByte(0x01); - msg:addString(disabledReason) - end - - if (off.state) then - if (off.state == GameStore.States.STATE_SALE) then - local daySub = off.validUntil - os.sdate("*t").day - if (daySub >= 0) then - msg:addByte(off.state) - msg:addU32(os.time() + daySub * 86400) - msg:addU32(off.basePrice) - haveSaleOffer = 1 - else - msg:addByte(GameStore.States.STATE_NONE) - end - else - msg:addByte(off.state) - end - else - msg:addByte(GameStore.States.STATE_NONE) - end - end - - local tryOnType = 0 - local type = convertType(offer.type) - - msg:addByte(type); - if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1]) - elseif type == GameStore.ConverType.SHOW_MOUNT then - local mount = Mount(offer.id) - msg:addU16(mount:getClientId()) - - tryOnType = 1 - elseif type == GameStore.ConverType.SHOW_ITEM then - msg:addU16(ItemType(offer.itemtype):getClientId()) - elseif type == GameStore.ConverType.SHOW_OUTFIT then - msg:addU16(player:getSex() == PLAYERSEX_FEMALE and offer.sexId.female or offer.sexId.male) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - - tryOnType = 1 - elseif type == GameStore.ConverType.SHOW_HIRELING then - if player:getSex() == PLAYERSEX_MALE then - msg:addByte(1) - else - msg:addByte(2) - end - msg:addU16(offer.sexId.male) - msg:addU16(offer.sexId.female) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - end - - msg:addByte(tryOnType) -- TryOn Type - msg:addU16(0) -- Collection (to-do) - msg:addU16(0) -- Popularity Score (to-do) - msg:addU32(0) -- State New Until (timestamp) - - local configure = useOfferConfigure(offer.type) - if configure == GameStore.ConfigureOffers.SHOW_CONFIGURE then - msg:addByte(1) - else - msg:addByte(0) - end - - msg:addU16(0) -- Products Capacity (unnused) - end - end - - player:sendButtonIndication(haveSaleOffer, 1) - msg:sendToPlayer(player) - msg:delete() -end - -function sendStoreTransactionHistory(playerId, page, entriesPerPage) - local player = Player(playerId) - if not player then - return false - end - local version = player:getClient().version - local totalEntries = GameStore.retrieveHistoryTotalPages(player:getAccountId()) - local totalPages = math.ceil(totalEntries / entriesPerPage) - local entries = GameStore.retrieveHistoryEntries(player:getAccountId(), page, entriesPerPage) -- this makes everything easy! - if #entries == 0 then - return addPlayerEvent(sendStoreError, 250, playerId, GameStore.StoreErrors.STORE_ERROR_HISTORY, "You don't have any entries yet.") - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_OpenTransactionHistory) - msg:addU32(totalPages > 0 and page - 1 or 0x0) -- current page - msg:addU32(totalPages > 0 and totalPages or 0x0) -- total page - msg:addByte(#entries) - - for k, entry in ipairs(entries) do - if version >= 1220 then - msg:addU32(0) - end - msg:addU32(entry.time) - msg:addByte(entry.mode) - msg:addU32(entry.amount) - msg:addByte(0x0) -- 0 = transferable tibia coin, 1 = normal tibia coin - msg:addString(entry.description) - if version >= 1220 then - msg:addByte(0) -- details - end - end - msg:sendToPlayer(player) -end - -function sendStorePurchaseSuccessful(playerId, message) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CompletePurchase) - msg:addByte(0x00) - msg:addString(message) - - msg:sendToPlayer(player) -end - -function sendStoreError(playerId, errorType, message) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_StoreError) - - msg:addByte(errorType) - msg:addString(message) - - msg:sendToPlayer(player) -end - -function sendCoinBalanceUpdating(playerId, updating) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CoinBalanceUpdating) - msg:addByte(0x00) - msg:sendToPlayer(player) - - if updating == true then - sendUpdateCoinBalance(playerId) - end -end - -function sendUpdateCoinBalance(playerId) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CoinBalanceUpdating) - msg:addByte(0x01) - - msg:addByte(GameStore.SendingPackets.S_CoinBalance) - msg:addByte(0x01) - - msg:addU32(player:getCoinsBalance()) - msg:addU32(player:getCoinsBalance()) - msg:addU32(player:getCoinsBalance()) - msg:addU32(0) -- Tournament Coins - - msg:sendToPlayer(player) -end - -function sendRequestPurchaseData(playerId, offerId, type) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_RequestPurchaseData) - msg:addU32(offerId) - msg:addByte(type) - msg:sendToPlayer(player) -end - ---==GameStoreFunctions==-- -GameStore.getCategoryByName = function(name) - for k, category in ipairs(GameStore.Categories) do - if category.name:lower() == name:lower() then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - end - return nil -end - -GameStore.getCategoryByOffer = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if type(offer.id) == "number" then - if offer.id == id then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - elseif type(offer.id) == "table" then - for m, offerId in pairs(offer.id) do - -- in case of outfits we have offer.id = {male = ..., female = ...} - if offerId == id then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - end - end - - end - end - end - return nil -end - -GameStore.getOfferById = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if type(offer.id) == "number" then - if offer.id == id then - return offer - end - elseif type(offer.id) == "table" and (offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON) then - for m, offerId in pairs(offer.id) do - -- in case of outfits we have offer.id = {male = ..., female = ...} - if offerId == id then - return offer - end - end - - -- case multi offer - elseif type(offer.id) == "table" then - local newoffer = offer - for i = 1, #offer.id do - local offerId = offer.id[i] - if offerId == id then - newoffer.id = offerId - newoffer.price = offer.price[i] - return newoffer - end - end - end - - end - end - end - return nil -end - --- Using for multi offer -function GameStore.getOffersByName(name) - local offers = {} - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if offer.name:lower() == name:lower() then - table.insert(offers, offer) - end - end - end - end - return offers -end - -GameStore.haveCategoryRook = function() - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers and category.rookgaard then - return true - end - end - - return false -end - -GameStore.haveOfferRook = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers and category.rookgaard then - for Off_k, offer in ipairs(category.offers) do - if offer.id == id then - return true - end - end - end - end - return nil -end - -GameStore.insertHistory = function(accountId, mode, description, amount) - return db.query(string.format("INSERT INTO `store_history`(`account_id`, `mode`, `description`, `coin_amount`, `time`) VALUES (%s, %s, %s, %s, %s)", accountId, mode, db.escapeString(description), amount, os.time())) -end - -GameStore.retrieveHistoryTotalPages = function (accountId) - local resultId = db.storeQuery("SELECT count(id) as total FROM store_history WHERE account_id = " .. accountId) - if resultId == false then - return 0 - end - - local totalPages = result.getNumber(resultId, "total") - result.free(resultId) - return totalPages -end - -GameStore.retrieveHistoryEntries = function(accountId, currentPage, entriesPerPage) - local entries = {} - local offset = currentPage > 1 and entriesPerPage * (currentPage - 1) or 0 - - local resultId = db.storeQuery("SELECT * FROM `store_history` WHERE `account_id` = " .. accountId .. " ORDER BY `time` DESC LIMIT " .. offset .. ", " .. entriesPerPage .. ";") - if resultId ~= false then - repeat - local entry = { - mode = result.getNumber(resultId, "mode"), - description = result.getString(resultId, "description"), - amount = result.getNumber(resultId, "coin_amount"), - time = result.getNumber(resultId, "time"), - } - table.insert(entries, entry) - until not result.next(resultId) - result.free(resultId) - end - return entries -end - -GameStore.getDefaultDescription = function(offerType, count) - local t, descList = GameStore.OfferTypes - if offerType == t.OFFER_TYPE_OUTFIT or offerType == t.OFFER_TYPE_OUTFIT_ADDON then - descList = GameStore.DefaultDescriptions.OUTFIT - elseif offerType == t.OFFER_TYPE_MOUNT then - descList = GameStore.DefaultDescriptions.MOUNT - elseif offerType == t.OFFER_TYPE_NAMECHANGE then - descList = GameStore.DefaultDescriptions.NAMECHANGE - elseif offerType == t.OFFER_TYPE_SEXCHANGE then - descList = GameStore.DefaultDescriptions.SEXCHANGE - elseif offerType == t.OFFER_TYPE_EXPBOOST then - descList = GameStore.DefaultDescriptions.EXPBOOST - elseif offerType == t.OFFER_TYPE_PREYSLOT then - descList = GameStore.DefaultDescriptions.PREYSLOT - elseif offerType == t.OFFER_TYPE_PREYBONUS then - descList = GameStore.DefaultDescriptions.PREYBONUS - elseif offerType == t.OFFER_TYPE_TEMPLE then - descList = GameStore.DefaultDescriptions.TEMPLE - end - - return descList[math.floor(math.random(1, #descList))] or "" -end - -GameStore.canUseHirelingName = function(name) - local result = { - ability = false - } - if name:len() < 3 or name:len() > 14 then - result.reason = "The length of the hireling name must be between 3 and 14 characters." - return result - end - - local match = name:gmatch("%s+") - local count = 0 - for v in match do - count = count + 1 - end - - local matchtwo = name:match("^%s+") - if (matchtwo) then - result.reason = "The hireling name can't have whitespace at begin." - return result - end - - local matchthree = name:match("[^a-zA-Z ]") - if (matchthree) then - result.reason = "The hireling name has invalid characters" - return result - end - - if (count > 1) then - result.reason = "The hireling name have more than 1 whitespace." - return result - end - - -- just copied from znote aac. - local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } - local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do - if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. "\" in your hireling name." - return result - end - end - end - - local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if (tmpName:lower():find(words[i])) then - result.reason = "You can't use word \"" .. words[i] .. "\" with whitespace in your hireling name." - return result - end - end - - result.ability = true - return result -end - -GameStore.canChangeToName = function(name) - local result = { - ability = false - } - if name:len() < 3 or name:len() > 14 then - result.reason = "The length of your new name must be between 3 and 14 characters." - return result - end - - local match = name:gmatch("%s+") - local count = 0 - for v in match do - count = count + 1 - end - - local matchtwo = name:match("^%s+") - if (matchtwo) then - result.reason = "Your new name can't have whitespace at begin." - return result - end - - if (count > 1) then - result.reason = "Your new name have more than 1 whitespace." - return result - end - - -- just copied from znote aac. - local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } - local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do - if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. "\" in your new name." - return result - end - end - end - - local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if (tmpName:lower():find(words[i])) then - result.reason = "You can't use word \"" .. words[i] .. "\" with whitespace in your new name." - return result - end - end - - if MonsterType(name) then - result.reason = "Your new name \"" .. name .. "\" can't be a monster's name." - return result - elseif Npc(name) then - result.reason = "Your new name \"" .. name .. "\" can't be a npc's name." - return result - end - - local letters = "{}|_*+-=<>0123456789@#%^&()/*'\\.,:;~!\"$" - for i = 1, letters:len() do - local c = letters:sub(i, i) - for i = 1, name:len() do - local m = name:sub(i, i) - if m == c then - result.reason = "You can't use this letter \"" .. c .. "\" in your new name." - return result - end - end - end - result.ability = true - return result -end - --- --- PURCHASE PROCESSOR FUNCTIONS --- Must throw an error when the purchase has not been made. The error must of --- take a table {code = ..., message = ...} if the error is handled. When no code --- index is present the error is assumed to be unhandled. - -function GameStore.processItemPurchase(player, offerId, offerCount) - if player:getFreeCapacity() < ItemType(offerId):getWeight(offerCount) then - return error({ code = 0, message = "Please make sure you have free capacity to hold this item."}) - end - - local inbox = player:getSlotItem(CONST_SLOT_STORE_INBOX) - if inbox and inbox:getEmptySlots() > offerCount then - for t = 1, offerCount do - inbox:addItem(offerId, offerCount or 1) - end - else - return error({ code = 0, message = "Please make sure you have free slots in your store inbox."}) - end -end -function GameStore.processChargesPurchase(player, itemtype, name, charges) - if player:getFreeCapacity() < ItemType(itemtype):getWeight(1) then - return error({ code = 0, message = "Please make sure you have free capacity to hold this item."}) - end - - local inbox = player:getSlotItem(CONST_SLOT_STORE_INBOX) - if inbox and inbox:getEmptySlots() > 1 then - inbox:addItem(itemtype, charges) - else - return error({ code = 0, message = "Please make sure you have free slots in your store inbox."}) - end -end - -function GameStore.processSignleBlessingPurchase(player, blessId, count) - player:addBlessing(blessId, count) -end - -function GameStore.processAllBlessingsPurchase(player, count) - player:addBlessing(1, count) - player:addBlessing(2, count) - player:addBlessing(3, count) - player:addBlessing(4, count) - player:addBlessing(5, count) - player:addBlessing(6, count) - player:addBlessing(7, count) - player:addBlessing(8, count) -end - -function GameStore.processInstantRewardAccess(player, offerCount) - if player:getCollectionTokens() + offerCount >= 91 then - return error({code = 1, message = "You cannot own more than 90 reward tokens."}) - end - player:setCollectionTokens(player:getCollectionTokens() + offerCount) -end - -function GameStore.processCharmsPurchase(player) - player:charmExpansion(true) -end - -function GameStore.processPremiumPurchase(player, offerId) - player:addPremiumDays(offerId - 3000) -end - -function GameStore.processStackablePurchase(player, offerId, offerCount, offerName) - local function isKegItem(itemId) - return itemId >= ITEM_KEG_START and itemId <= ITEM_KEG_END - end - - if isKegItem(offerId) then - if player:getFreeCapacity() < ItemType(offerId):getWeight(1) + ItemType(2596):getWeight() then - return error({code = 0, message = "Please make sure you have free capacity to hold this item."}) - end - elseif player:getFreeCapacity() < ItemType(offerId):getWeight(offerCount) + ItemType(2596):getWeight() then - return error({code = 0, message = "Please make sure you have free capacity to hold this item."}) - end - - local inbox = player:getSlotItem(CONST_SLOT_STORE_INBOX) - if inbox and inbox:getEmptySlots() > 0 then - if (isKegItem(offerId)) then - if (offerCount >= 500) then - local parcel = Item(inbox:addItem(2596, 1):getUniqueId()) - local function changeParcel(parcel) - local packagename = '' .. offerCount .. 'x ' .. offerName .. ' package.' - if parcel then - parcel:setAttribute(ITEM_ATTRIBUTE_NAME, packagename) - local pendingCount = offerCount - while (pendingCount > 0) do - local pack - if (pendingCount > 500) then - pack = 500 - else - pack = pendingCount - end - local kegItem = parcel:addItem(offerId, 1) - kegItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, pack) - pendingCount = pendingCount - pack - end - end - end - addEvent(function() changeParcel(parcel) end, 250) - else - local kegItem = inbox:addItem(offerId, 1) - kegItem:setAttribute(ITEM_ATTRIBUTE_CHARGES, offerCount) - end - elseif (offerCount > 100) then - local parcel = Item(inbox:addItem(2596, 1):getUniqueId()) - local function changeParcel(parcel) - local packagename = '' .. offerCount .. 'x ' .. offerName .. ' package.' - if parcel then - parcel:setAttribute(ITEM_ATTRIBUTE_NAME, packagename) - local pendingCount = offerCount - while (pendingCount > 0) do - local pack - if (pendingCount > 100) then - pack = 100 - else - pack = pendingCount - end - parcel:addItem(offerId, pack) - pendingCount = pendingCount - pack - end - end - end - addEvent(function() changeParcel(parcel) end, 250) - else - inbox:addItem(offerId, offerCount) - end - else - return error({code = 0, message = "Please make sure you have free slots in your store inbox."}) - end -end - -function GameStore.processHouseRelatedPurchase(player, offerId, offerCount) - local function isCaskItem(itemId) - return (itemId >= ITEM_HEALTH_CASK_START and itemId <= ITEM_HEALTH_CASK_END) or - (itemId >= ITEM_MANA_CASK_START and itemId <= ITEM_MANA_CASK_END) or - (itemId >= ITEM_SPIRIT_CASK_START and itemId <= ITEM_SPIRIT_CASK_END) - end - - local inbox = player:getSlotItem(CONST_SLOT_STORE_INBOX) - if inbox and inbox:getEmptySlots() > 0 then - local decoKit = inbox:addItem(26054, 1) - local function changeKit(kit) - local decoItemName = ItemType(offerId):getName() - if kit then - kit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item in the Store.\nUnwrap it in your own house to create a <" .. decoItemName .. ">.") - kit:setCustomAttribute("unWrapId", offerId) - - if isCaskItem(offerId) then - kit:setAttribute(ITEM_ATTRIBUTE_DATE, offerCount) - end - end - end - addEvent(function() changeKit(decoKit) end, 250) - else - return error({code = 0, message = "Please make sure you have free slots in your store inbox."}) - end -end - -function GameStore.processOutfitPurchase(player, offerSexIdTable, addon) - local looktype - local _addon = addon and addon or 0 - - if player:getSex() == PLAYERSEX_MALE then - looktype = offerSexIdTable.male - elseif player:getSex() == PLAYERSEX_FEMALE then - looktype = offerSexIdTable.female - end - - if not looktype then - return error({code = 0, message = "This outfit seems not to suit your sex, we are sorry for that!"}) - elseif (not player:hasOutfit(looktype, 0)) and (_addon == 1 or _addon == 2) then - return error({code = 0, message = "You must own the outfit before you can buy its addon."}) - elseif player:hasOutfit(looktype, _addon) then - return error({code = 0, message = "You already own this outfit."}) - else - if not (player:addOutfitAddon(looktype, _addon)) -- TFS call failed - or (not player:hasOutfit(looktype, _addon)) -- Additional check; if the looktype doesn't match player sex for example, - -- then the TFS check will still pass... bug? (TODO) - then - error({ code = 0, message = "There has been an issue with your outfit purchase. Your purchase has been cancelled."}) - else - player:addOutfitAddon(offerSexIdTable.male, _addon) - player:addOutfitAddon(offerSexIdTable.female, _addon) - end - end -end - -function GameStore.processMountPurchase(player, offerId) - player:addMount(offerId) -end - -function GameStore.processNameChangePurchase(player, offerId, productType, newName, offerName, offerPrice) - local playerId = player:getId() - - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE then - local tile = Tile(player:getPosition()) - if (tile) then - if (not tile:hasFlag(TILESTATE_PROTECTIONZONE)) then - return error({code = 1, message = "You can change name only in Protection Zone."}) - end - end - - local resultId = db.storeQuery("SELECT * FROM `players` WHERE `name` = " .. db.escapeString(newName) .. "") - if resultId ~= false then - return error({code = 1, message = "This name is already used, please try again!"}) - end - - local result = GameStore.canChangeToName(newName) - if not result.ability then - return error({code = 1, message = result.reason}) - end - - player:removeCoinsBalance(offerPrice) - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, offerName, (offerPrice) * -1) - - local message = string.format("You have purchased %s for %d coins.", offerName, offerPrice) - addPlayerEvent(sendStorePurchaseSuccessful, 500, playerId, message) - - newName = newName:lower():gsub("(%l)(%w*)", function(a, b) return string.upper(a) .. b end) - db.query("UPDATE `players` SET `name` = " .. db.escapeString(newName) .. " WHERE `id` = " .. player:getGuid()) - message = "You have successfully changed you name, relogin!" - addEvent(function() - local player = Player(playerId) - if not player then - return false - end - - player:remove() - end, 1000) - -- If not, we ask him to do! - else - return addPlayerEvent(sendRequestPurchaseData, 250, playerId, offerId, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE) - end -end - -function GameStore.processSexChangePurchase(player) - player:toggleSex() -end - - -function GameStore.processExpBoostPuchase(player) - local currentExpBoostTime = player:getExpBoostStamina() - local expBoostCount = player:getStorageValue(GameStore.Storages.expBoostCount) - - player:setStoreXpBoost(50) - player:setExpBoostStamina(currentExpBoostTime + 3600) - - if (player:getStorageValue(GameStore.Storages.expBoostCount) == -1 or expBoostCount == 6) then - player:setStorageValue(GameStore.Storages.expBoostCount, 1) - end - - player:setStorageValue(GameStore.Storages.expBoostCount, expBoostCount + 1) -end - -function GameStore.processPreySlotPurchase(player) - if player:getStorageValue(Prey.Config.StoreSlotStorage) < 1 then - player:setStorageValue(Prey.Config.StoreSlotStorage, 1) - player:setPreyUnlocked(CONST_PREY_SLOT_THIRD, 2) - player:setPreyState(CONST_PREY_SLOT_THIRD, 1) - - -- Update Prey Data - for slot = CONST_PREY_SLOT_FIRST, CONST_PREY_SLOT_THIRD do - player:sendPreyData(slot) - end - end -end - -function GameStore.processPreyHuntingSlotPurchase(player) - if player:getStorageValue(CONST_HUNTING_STORAGE) < 1 then - player:setStorageValue(CONST_HUNTING_STORAGE, 1) - - -- Update Prey Data - player:sendPreyHuntingData(CONST_PREY_SLOT_THIRD) - end -end - -function GameStore.processPreyBonusReroll(player, offerCount) - if player:getPreyBonusRerolls() + offerCount >= 51 then - return error({code = 1, message = "You cannot own more than 50 prey wildcards."}) - end - player:setPreyBonusRerolls(player:getPreyBonusRerolls() + offerCount) -end - -function GameStore.processTempleTeleportPurchase(player) - if player:getCondition(CONDITION_INFIGHT, CONDITIONID_DEFAULT) or player:isPzLocked() then - return error({code = 0, message = "You can't use temple teleport in fight!"}) - end - - player:teleportTo(player:getTown():getTemplePosition()) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'You have been teleported to your hometown.') -end - -function GameStore.processHirelingPurchase(player, offer, productType, hirelingName, chosenSex) - local playerId = player:getId() - local offerId = offer.id - - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_HIRELING then - - local result = GameStore.canUseHirelingName(hirelingName) - if not result.ability then - return error({code = 1, message = result.reason}) - end - - hirelingName = hirelingName:lower():gsub("(%l)(%w*)", function(a, b) return string.upper(a) .. b end) - - local hireling = player:addNewHireling(hirelingName, chosenSex) - if not hireling then - return error({code = 1, message = "Error delivering your hireling lamp, try again later."}) - end - - player:removeCoinsBalance(offer.price) - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, offer.name .. ' ('.. hirelingName ..')', (offer.price) * -1) - local message = "You have successfully bought " .. hirelingName - return addPlayerEvent(sendStorePurchaseSuccessful, 650, playerId, message) - -- If not, we ask him to do! - else - if player:getHirelingsCount() >= 10 then - return error({code = 1, message = "You cannot have more than 10 hirelings."}) - end - -- TODO: Use the correct dialog (byte 0xDB) on client 1205+ - -- for compatibility, request name using the change name dialog - return addPlayerEvent(sendRequestPurchaseData, 250, playerId, offerId, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_HIRELING) - end -end - -function GameStore.processHirelingChangeNamePurchase(player, offer, productType, newHirelingName) - local playerId = player:getId() - local offerId = offer.id - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE then - local result = GameStore.canUseHirelingName(newHirelingName) - if not result.ability then - return error({code = 1, message = result.reason}) - end - - newHirelingName = newHirelingName:lower():gsub("(%l)(%w*)", function(a, b) return string.upper(a) .. b end) - - local message = 'Close the store window to select which hireling should be renamed to '.. newHirelingName - addPlayerEvent(sendStorePurchaseSuccessful, 200, playerId, message) - - addPlayerEvent(HandleHirelingNameChange,550, playerId, offer, newHirelingName) - - else - return addPlayerEvent(sendRequestPurchaseData, 250, playerId, offerId, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE) - end -end - -function GameStore.processHirelingChangeSexPurchase(player, offer) - local playerId = player:getId() - - local message = 'Close the store window to select which hireling should have the sex changed.' - addPlayerEvent(sendStorePurchaseSuccessful, 200, playerId, message) - - addPlayerEvent(HandleHirelingSexChange, 550, playerId, offer) -end - -function GameStore.processHirelingSkillPurchase(player, offer) - player:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - local skill = offer.id - HIRELING_STORAGE.SKILL - player:enableHirelingSkill(skill) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'A new hireling skill has been added to all your hirelings') -end - -function GameStore.processHirelingOutfitPurchase(player, offer) - player:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) - local outfit = offer.id - HIRELING_STORAGE.OUTFIT - player:enableHirelingOutfit(outfit) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, 'A new hireling outfit has been added to all your hirelings') -end - ---==Player==-- -function Player.getCoinsBalance(self) - resultId = db.storeQuery("SELECT `coins` FROM `accounts` WHERE `id` = " .. self:getAccountId()) - if not resultId then return 0 end - return result.getNumber(resultId, "coins") -end - -function Player.setCoinsBalance(self, coins) - db.query("UPDATE `accounts` SET `coins` = " .. coins .. " WHERE `id` = " .. self:getAccountId()) - return true -end - -function Player.canRemoveCoins(self, coins) - if self:getCoinsBalance() < coins then - return false - end - return true -end - -function Player.removeCoinsBalance(self, coins) - if self:canRemoveCoins(coins) then - return self:setCoinsBalance(self:getCoinsBalance() - coins) - end - - return false -end - -function Player.addCoinsBalance(self, coins, update) - self:setCoinsBalance(self:getCoinsBalance() + coins) - if update then sendCoinBalanceUpdating(self, true) end - return true -end - -function Player.sendButtonIndication(self, value1, value2) - local msg = NetworkMessage() - msg:addByte(0x19) - msg:addByte(value1) -- Sale - msg:addByte(value2) -- New Item - msg:sendToPlayer(self) -end - -function Player.toggleSex(self) - local currentSex = self:getSex() - local playerOutfit = self:getOutfit() - - playerOutfit.lookAddons = 0 - if currentSex == PLAYERSEX_FEMALE then - self:setSex(PLAYERSEX_MALE) - playerOutfit.lookType = 128 - else - self:setSex(PLAYERSEX_FEMALE) - playerOutfit.lookType = 136 - end - self:setOutfit(playerOutfit) -end - -local function getHomeOffers(playerId) - local player = Player(playerId) - if not player then return {} end - - local GameStoreCategories = GameStore.Categories - - local offers = {} - if (GameStoreCategories) then - for k, category in ipairs(GameStoreCategories) do - if category.offers then - for _, offer in ipairs(category.offers) do - if offer.home then - table.insert(offers, offer) - end - end - end - end - end - - return offers -end - -function sendHomePage(playerId) - local player = Player(playerId) - if not player then - return - end - - local version = player:getClient().version - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_StoreOffers) - - msg:addString("Home") - msg:addU32(0x0) -- Redirect ID (not used here) - msg:addByte(0x0) -- Window Type - msg:addByte(0x0) -- Collections Size - msg:addU16(0x00) -- Collection Name - - local homeOffers = getHomeOffers(player:getId()) - msg:addU16(#homeOffers) -- offers - - for p, offer in pairs(homeOffers)do - msg:addString(offer.name) - msg:addByte(0x1) -- ? - msg:addU32(offer.id or 0) -- id - msg:addU16(0x1) - msg:addU32(offer.price) - msg:addByte(offer.coinType or 0x00) - local disabled, disabledReason = player:canBuyOffer(offer).disabled, player:canBuyOffer(offer).disabledReason - msg:addByte(disabled) - if disabled == 1 then - msg:addByte(0x01); - msg:addString(disabledReason) - end - - msg:addByte(0x00) - - local type = convertType(offer.type) - - msg:addByte(type); - if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1]) - elseif type == GameStore.ConverType.SHOW_MOUNT then - local mount = Mount(offer.id) - msg:addU16(mount:getClientId()) - elseif type == GameStore.ConverType.SHOW_ITEM then - msg:addU16(ItemType(offer.itemtype):getClientId()) - elseif type == GameStore.ConverType.SHOW_OUTFIT then - msg:addU16(player:getSex() == PLAYERSEX_FEMALE and offer.sexId.female or offer.sexId.male) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - end - - msg:addByte(0) -- TryOn Type - msg:addU16(0) -- Collection - msg:addU16(0) -- Popularity Score - msg:addU32(0) -- State New Until - msg:addByte(0) -- User Configuration - msg:addU16(0) -- Products Capacity - end - - local banner = HomeBanners - msg:addByte(#banner.images) - for m, image in ipairs(banner.images) do - msg:addString(image) - msg:addByte(0x04) -- Banner Type (offer) - msg:addU32(0x00) -- Offer Id - msg:addByte(0) - msg:addByte(0) - end - - msg:addByte(banner.delay) -- Delay to swtich images - - msg:sendToPlayer(player) - -end - -function Player:openStore(serviceName) --exporting the method so other scripts can use to open store - openStore(self:getId()) - - --local serviceType = msg:getByte() - local category = GameStore.Categories and GameStore.Categories[1] or nil - - if serviceName and serviceName:lower() == "home" then - return sendHomePage(self:getId()) - end - - if serviceName and GameStore.getCategoryByName(serviceName) then - category = GameStore.getCategoryByName(serviceName) - end - - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end -end - --- Hireling Helpers -function HandleHirelingNameChange(playerId, offer, newHirelingName) - local player = Player(playerId); - - local cb = function(playerId, data, hireling) - local offer = data.offer - local newHirelingName = data.newHirelingName - local player = Player(playerId); - if not hireling then - return player:showInfoModal("Error","Your must select a hireling.") - end - - if hireling.active > 0 then - return player:showInfoModal("Error", "Your hireling must be inside his/her lamp.") - end - - if not player:removeCoinsBalance(offer.price) then - return player:showInfoModal("Error", "Transaction error") - end - local oldName = hireling.name - hireling.name = newHirelingName - local lamp = player:findHirelingLamp(hireling:getId()) - if lamp then - lamp:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "This mysterious lamp summons your very own personal hireling.\nThis item cannot be traded.\nThis magic lamp is the home of " .. hireling:getName() .. ".") - end - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, offer.name .. ' ('.. oldName .. '->' .. newHirelingName ..')', (offer.price) * -1) - - player:showInfoModal('Info',string.format('%s has been renamed to %s', oldName, newHirelingName)) - end - - player:sendHirelingSelectionModal('Choose a Hireling', 'Select a hireling below', cb, {offer=offer, newHirelingName=newHirelingName}) -end - -function HandleHirelingSexChange(playerId, offer) - local player = Player(playerId); - - local cb = function(playerId, data, hireling) - local player = Player(playerId); - if not hireling then - return player:showInfoModal("Error","Your must select a hireling.") - end - - if hireling.active > 0 then - return player:showInfoModal("Error", "Your hireling must be inside his/her lamp.") - end - - if not player:removeCoinsBalance(data.offer.price) then - return player:showInfoModal("Error", "Transaction error") - end - - local changeTo,sexString,lookType - if hireling.sex == HIRELING_SEX.FEMALE then - changeTo = HIRELING_SEX.MALE - sexString = 'male' - lookType = HIRELING_OUTFIT_DEFAULT.male - else - changeTo = HIRELING_SEX.FEMALE - sexString = 'female' - lookType = HIRELING_OUTFIT_DEFAULT.female - end - - hireling.sex = changeTo - hireling.looktype = lookType - - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, offer.name .. ' ('.. hireling:getName() ..')', (offer.price) * -1) - - player:showInfoModal('Info',string.format('%s sex was changed to %s', hireling:getName(), sexString)) - end - - player:sendHirelingSelectionModal('Choose a Hireling', 'Select a hireling below', cb, {offer=offer}) -end diff --git a/data/modules/scripts/gamestore/readme.md b/data/modules/scripts/gamestore/readme.md deleted file mode 100644 index e179a1b8172..00000000000 --- a/data/modules/scripts/gamestore/readme.md +++ /dev/null @@ -1,119 +0,0 @@ -## Module : Premium Shop - Game Store ---- - -> Notify! Please put the images in folder called "64". - -### Faq - -##### 1. How to make a category? -```lua ---- Method 1 --- -GameStore.Categories = { - { name = "mounts", - ... - }, - { name = "outfits", - ... - } -} ---- Method 2 --- -GameStore.Categories = { - mounts = { - name = "mounts" - }, - outfits = {....} -} ---- Method 3 --- -mounts = {....} -GameStore.Categories = { - mounts, - ... -} -``` - -##### 2. How to add offers to category -```lua ---- Method 1 --- -GameStore.Categories = { - mounts = { - offers = { - - } - } -} ---- Method 2 --- -mounts = { name = "mounts", icons = {"Category_Mounts.png"}} -mounts.offers = { - {name = "fafa", thingId = ....} -} -GameStore.Categories = { - mounts = { name = "mounts", - offers = { - {name = "fafa", thingId = ....} - } - } -} -``` - -### Category Options -| Method | Type | Usage | Default | -|-------------|-----------------------|------------------------------|-----------------------------| -| name* | string | the category name | nil | -| description | string | the category description | "" | -| state | GameStore.States(int) | the category highlight state | GameStore.States.STATE_NONE | -| icons* | table[string(s)] | the icons for the category | nil | -| offers(*) | table[offer(s)] | the category offers | nil | - -#### Example : -```lua -mounts = { - name = "Mounts", - description = "Have a mount and become an important-look person!", - state = GameStore.States.STATE_NEW, - icons = {"Category_Mounts.png"}, - offers = {....} -} -``` - -### Offer Options -| Method | Type | Usage | Default | -|----------------|---------------------------|------------------------------------------------------------------------|--------------------------------------| -| name* | string | the offer name | nil | -| description | string | the offer descrioption | "" | -| thingId* | int | the id of the choosed type ( itemId or mountId or outfitLookType, ....)| nil | -| type* | GameStore.OfferTypes(int) | the type of the offer, item or mount or outfit or ... | GameStore.OfferTypes.OFFER_TYPE_NONE | -| price* | int | the offer price | nil | -| state | GameStore.States(int) | the offer highlight state | GameStore.States.STATE_NONE | -| icons* | table[string(s)] | the icons for the category | nil | -| disabled | bool | dynamically disable the offer | false | -| disabledReason | string | reason for being disabled ( use when disabled is true ) | nil | - -#### Example : -```lua -mounts.offers = { - { name = "Titanica", - description = "Looking for nice mount? Titanica is the one you are looking for, she is beautiful, smart and running quickly.", - thingId = 4, -- here we use mount id. - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - price = 500, - state = GameStore.States.STATE_SALE, - icons = {"Product_Titanica1", "Product_Titanica2"} - --[[Dynamicly used variables - disabled = (true|false), - disbabledReason = (string) - ]] - } -} -``` - -### Offer types and States. - -| OfferTypes | States | -|-------------------------|-------------| -| `OFFER_TYPE_ITEM` | `STATE_NEW` | -| `OFFER_TYPE_OUTFIT` | `STATE_SALE` | -| `OFFER_TYPE_OUTFIT_ADDON` | `STATE_TIMED` | -| `OFFER_TYPE_MOUNT` | | -| `OFFER_TYPE_NAMECHANGE` | | -| `OFFER_TYPE_SEXCHANGE` | | -| `OFFER_TYPE_PROMOTION` | | diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 7c15ee6b99a..50857bc35b8 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -148,6 +148,7 @@ enum integerConfig_t { STAMINA_TRAINER_DELAY, STAMINA_PZ_GAIN, STAMINA_TRAINER_GAIN, + TIME_GMT, LAST_INTEGER_CONFIG }; diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index af662dd8b8f..98d30c7e15c 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -229,6 +229,7 @@ bool ConfigManager::load() integer[BLACK_SKULL_DURATION] = getGlobalNumber(L, "blackSkullDuration", 45); integer[ORANGE_SKULL_DURATION] = getGlobalNumber(L, "orangeSkullDuration", 7); integer[SERVER_SAVE_NOTIFY_DURATION] = getGlobalNumber(L, "serverSaveNotifyDuration", 5); + integer[TIME_GMT] = getGlobalNumber(L, "timeGMT", -3 * 60 * 60); integer[PARTY_LIST_MAX_DISTANCE] = getGlobalNumber(L, "partyListMaxDistance", 0); diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index c9d3a5f2ed6..832e000d08b 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -21,6 +21,18 @@ #define SRC_CREATURES_CREATURES_DEFINITIONS_HPP_ // Enum +struct StoreHistory { + StoreHistory(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust) : + time(time), mode(mode), amount(amount), coinMode(coinMode), description(std::move(description)), cust(cust) {} + + uint32_t time; + uint8_t mode; + uint32_t amount; + uint8_t coinMode; + std::string description; + int32_t cust; + +}; enum SkillsId_t { SKILLVALUE_LEVEL = 0, diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index a320c8ad1ba..25a3ca135d7 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -21,6 +21,7 @@ #include "creatures/players/account/account.hpp" #include "database/databasetasks.h" +#include "game/game.h" #include #include @@ -87,13 +88,19 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { * Coins Methods ******************************************************************************/ -error_t Account::GetCoins(uint32_t *coins) { - - if (db_ == nullptr || coins == nullptr || id_ == 0) { +error_t Account::GetCoins(CoinType_t coinType) { + if (db_ == nullptr || id_ == 0) { return ERROR_NOT_INITIALIZED; } std::ostringstream query; + std::string coins = "coins"; + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coins = "coins"; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + coins = "tournamentBalance"; + } + query << "SELECT `coins` FROM `accounts` WHERE `id` = " << id_; DBResult_ptr result = db_->storeQuery(query.str()); @@ -101,34 +108,27 @@ error_t Account::GetCoins(uint32_t *coins) { return ERROR_DB; } - *coins = result->getNumber("coins"); + result->getNumber("coins"); return ERROR_NO; } -error_t Account::AddCoins(uint32_t amount) { - - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } - if (amount == 0) { - return ERROR_NO; - } - - uint32_t current_coins = 0; - this->GetCoins(¤t_coins); - if ((current_coins + amount) < current_coins) { - return ERROR_VALUE_OVERFLOW; - } - - std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = " << (current_coins + amount) - << " WHERE `id` = " << id_; +error_t Account::AddCoins(int32_t amount) +{ + std::string coins = "`coins`"; + CoinType_t coinType; + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coins = "`coins`"; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + coins = "`tournamentBalance`"; + } + std::ostringstream query; + query << "UPDATE `accounts` SET " << coins << " = " << coins << " + " << amount << " WHERE `id` = " << id_; - db_tasks_->addTask(query.str()); + g_databaseTasks.addTask(query.str()); return ERROR_NO; } -error_t Account::RemoveCoins(uint32_t amount) { +error_t Account::RemoveCoins(int32_t amount) { if (db_tasks_ == nullptr) { return ERROR_NULLPTR; @@ -138,43 +138,35 @@ error_t Account::RemoveCoins(uint32_t amount) { return ERROR_NO; } - uint32_t current_coins = 0; - this->GetCoins(¤t_coins); - - if ((current_coins - amount) > current_coins) { - return ERROR_VALUE_NOT_ENOUGH_COINS; - } - - std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = "<< (current_coins - amount) - << " WHERE `id` = " << id_; + CoinType_t coinType; + std::string coins = "`coins`"; + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coins = "`coins`"; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + coins = "`tournamentBalance`"; + } + std::ostringstream query; + query << "UPDATE `accounts` SET " << coins << " = " << coins << " - " << amount << " WHERE `id` = " << id_; - db_tasks_->addTask(query.str()); + g_databaseTasks.addTask(query.str()); return ERROR_NO; } -error_t Account::RegisterCoinsTransaction(CoinTransactionType type, - uint32_t coins, - const std::string& description) { - - if (db_ == nullptr) { - return ERROR_NULLPTR; - } - - std::ostringstream query; - query << "INSERT INTO `coins_transactions` (`account_id`, `type`, `amount`," - " `description`) VALUES (" << id_ << ", " << static_cast(type) << ", "<< coins - << ", " << db_->escapeString(description) << ")"; +error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust) +{ + Database& db = Database::getInstance(); + std::ostringstream query; + query << "INSERT INTO `store_history` (`accountid`, `time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" << + id_ << "," << time << "," << static_cast(mode) << "," << amount << "," << static_cast(coinMode) << "," << + db.escapeString(description) << "," << cust << ")"; - if (!db_->executeQuery(query.str())) { - return ERROR_DB; - } + StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); + g_game.addAccountHistory(id_, historyOffer); - return ERROR_NO; + db.executeQuery(query.str()); } - /******************************************************************************* * Database ******************************************************************************/ diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index e2fa9ff422a..5dddaca4822 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -132,7 +132,7 @@ class Account { * @param coins Pointer to return the number of coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t GetCoins(uint32_t *coins); + error_t GetCoins(CoinType_t coinType); /** * @brief Add coins to the account and update database. @@ -140,7 +140,7 @@ class Account { * @param amount Amount of coins to be added * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t AddCoins(uint32_t amount); + error_t AddCoins(int32_t amount); /** * @brief Removes coins from the account and update database. @@ -148,7 +148,7 @@ class Account { * @param amount Amount of coins to be removed * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t RemoveCoins(uint32_t amount); + error_t RemoveCoins(int32_t amount); /** * @brief Register account coins transactions in database. @@ -158,9 +158,7 @@ class Account { * @param description Description of the transaction * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t RegisterCoinsTransaction(CoinTransactionType type, uint32_t coins, - const std::string &description); - + error_t RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust); /*************************************************************************** * Database diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 73dbe9dfaa3..8d5100d0be8 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4619,9 +4619,63 @@ void Player::setPremiumDays(int32_t v) sendBasicData(); } -void Player::setTibiaCoins(int32_t v) +void Player::setTibiaCoins(int32_t v, CoinType_t coinType) { - coinBalance = v; + switch (coinType) { + case COIN_TYPE_DEFAULT: + case COIN_TYPE_TRANSFERABLE: { + coinBalance = v; + break; + } + + case COIN_TYPE_TOURNAMENT: { + tournamentCoinBalance = v; + break; + } + + default: { + coinBalance = v; + break; + } + } +} + +bool Player::canRemoveCoins(int32_t v, CoinType_t coinType) +{ + if (lastUpdateCoin - OTSYS_TIME() < 2000) { + // a cada 2 segundos atualizar, na diferença que for chamada + lastUpdateCoin = OTSYS_TIME() + 2000; + + account::Account account(getAccount()); + account.LoadAccountDB(); + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coinBalance = account.GetCoins(coinType); + } else if (coinType == COIN_TYPE_TOURNAMENT) { + tournamentCoinBalance = account.GetCoins(coinType); + } + } + + + int32_t coins; + switch (coinType) { + case COIN_TYPE_DEFAULT: + case COIN_TYPE_TRANSFERABLE: { + coins = coinBalance; + break; + } + + case COIN_TYPE_TOURNAMENT: { + coins = tournamentCoinBalance; + break; + } + + default: { + coins = coinBalance; + break; + } + } + + return (coins - v) >= 0; } PartyShields_t Player::getPartyShield(const Player* player) const @@ -5423,6 +5477,29 @@ void Player::stowItem(Item* item, uint32_t count, bool allItems) { stashContainer(itemDict); } +void Player::addAccountStorageValue(const uint32_t key, const int32_t value) +{ + if (value != -1) { + int32_t oldValue; + getAccountStorageValue(key, oldValue); + accountStorageMap[key] = value; + } else { + accountStorageMap.erase(key); + } +} + +bool Player::getAccountStorageValue(const uint32_t key, int32_t& value) const +{ + auto it = accountStorageMap.find(key); + if (it == accountStorageMap.end()) { + value = -1; + return false; + } + + value = it->second; + return true; +} + /******************************************************************************* * Interfaces ******************************************************************************/ @@ -5440,4 +5517,3 @@ error_t Player::GetAccountInterface(account::Account* account) { account = account_; return account::ERROR_NO; } - diff --git a/src/creatures/players/player.h b/src/creatures/players/player.h index 9c092119d0f..1ed36d3154b 100644 --- a/src/creatures/players/player.h +++ b/src/creatures/players/player.h @@ -28,7 +28,6 @@ #include "items/containers/depot/depotchest.h" #include "items/containers/depot/depotlocker.h" #include "grouping/familiars.h" -#include "game/gamestore.h" #include "grouping/groups.h" #include "grouping/guild.h" #include "imbuements/imbuements.h" @@ -525,7 +524,17 @@ class Player final : public Creature, public Cylinder bool isPremium() const; void setPremiumDays(int32_t v); - void setTibiaCoins(int32_t v); + void setTibiaCoins(int32_t v, CoinType_t coinType = COIN_TYPE_DEFAULT); + bool canRemoveCoins(int32_t v, CoinType_t coinType = COIN_TYPE_DEFAULT); + int32_t getCoinBalance(CoinType_t coinType = COIN_TYPE_DEFAULT) { + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + return coinBalance; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + return tournamentCoinBalance; + } else { + return 0; + } + } uint16_t getHelpers() const; @@ -1055,44 +1064,6 @@ class Player final : public Creature, public Cylinder } } - //store - void sendOpenStore(uint8_t serviceType) { - if(client) { - client->sendOpenStore(serviceType); - } - } - - void sendShowStoreCategoryOffers(StoreCategory* category) { - if(client) { - client->sendStoreCategoryOffers(category); - } - } - - void sendStoreError(GameStoreError_t error, const std::string& errorMessage) { - if(client) { - client->sendStoreError(error, errorMessage); - } - } - - void sendStorePurchaseSuccessful(const std::string& message, const uint32_t newCoinBalance) { - if(client) - { - client->sendStorePurchaseSuccessful(message, newCoinBalance); - } - } - - void sendStoreRequestAdditionalInfo(uint32_t offerId, ClientOffer_t clientOfferType) { - if(client) { - client->sendStoreRequestAdditionalInfo(offerId, clientOfferType); - } - } - - void sendStoreTrasactionHistory(HistoryStoreOfferList& list, uint32_t page, uint8_t entriesPerPage) { - if(client) { - client->sendStoreTrasactionHistory(list, page, entriesPerPage); - } - } - // Quickloot void sendLootContainers() { if (client) { @@ -1523,12 +1494,6 @@ class Player final : public Creature, public Cylinder } } - void sendStoreOpen(uint8_t serviceType) { - if (client) { - client->sendOpenStore(serviceType); - } - } - void receivePing() { lastPong = OTSYS_TIME(); } @@ -1850,10 +1815,15 @@ class Player final : public Creature, public Cylinder uint16_t getFreeBackpackSlots() const; + void addAccountStorageValue(const uint32_t key, const int32_t value); + bool getAccountStorageValue(const uint32_t key, int32_t& value) const; + // Interfaces error_t SetAccountInterface(account::Account *account); error_t GetAccountInterface(account::Account *account); + std::vector unjustifiedKills; + private: std::forward_list getMuteConditions() const; @@ -1923,6 +1893,7 @@ class Player final : public Creature, public Cylinder std::map depotChests; std::map moduleDelayMap; std::map storageMap; + std::map accountStorageMap; std::map rewardMap; @@ -1970,9 +1941,10 @@ class Player final : public Creature, public Cylinder int64_t nextPotionAction = 0; int64_t lastQuickLootNotification = 0; int64_t lastWalking = 0; - uint64_t asyncOngoingTasks = 0; - std::vector unjustifiedKills; + uint32_t lastUpdateCoin = OTSYS_TIME(); + + uint64_t asyncOngoingTasks = 0; BedItem* bedItem = nullptr; Guild* guild = nullptr; @@ -2018,15 +1990,18 @@ class Player final : public Creature, public Cylinder int32_t varStats[STAT_LAST + 1] = {}; int32_t shopCallback = -1; int32_t MessageBufferCount = 0; - uint32_t premiumDays = 0; int32_t bloodHitCount = 0; int32_t shieldBlockCount = 0; int32_t offlineTrainingSkill = -1; int32_t offlineTrainingTime = 0; int32_t idleTime = 0; - uint32_t coinBalance = 0; + int32_t tournamentCoinBalance = 0; + uint16_t expBoostStamina = 0; + uint32_t coinBalance = 0; + uint32_t premiumDays = 0; + uint16_t lastStatsTrainingTime = 0; uint16_t staminaMinutes = 2520; std::vector preyStaminaMinutes = {7200, 7200, 7200}; diff --git a/src/game/game.cpp b/src/game/game.cpp index b6dd41b8e29..870e2b06f91 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -29,6 +29,7 @@ #include "database/databasetasks.h" #include "lua/creature/events.h" #include "game/game.h" +#include "game/gamestore.hpp" #include "lua/global/globalevent.h" #include "io/iologindata.h" #include "io/iomarket.h" @@ -64,6 +65,7 @@ extern Weapons* g_weapons; extern Scripts* g_scripts; extern Modules* g_modules; extern Imbuements* g_imbuements; +extern GameStore g_gameStore; Game::Game() { @@ -290,11 +292,6 @@ void Game::setGameState(GameState_t newState) mounts.loadFromXml(); - if (!g_config.getBoolean(STOREMODULES)) { - gameStore.loadFromXml(); - gameStore.startup(); - } - loadMotdNum(); loadPlayersRecord(); @@ -7564,6 +7561,10 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr uint32_t minFee = std::min(100000, calcFee); uint32_t fee = std::max(20, minFee); + account::Account account(player->getAccount()); + account.LoadAccountDB(); + uint32_t coins; + if (type == MARKETACTION_SELL) { if (fee > (player->getBankBalance() + player->getMoney())) { @@ -7576,42 +7577,37 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr } if (it.id == ITEM_STORE_COIN) { - account::Account account(player->getAccount()); - account.LoadAccountDB(); - uint32_t coins; - account.GetCoins(&coins); - - if (amount > coins) { - return; - } - account.RemoveCoins(static_cast(amount)); - } else { - uint16_t stashmath = amount; - uint16_t stashminus = player->getStashItemCount(it.wareId); - if (stashminus > 0) { - stashmath = (amount - (amount > stashminus ? stashminus : amount)); - player->withdrawItem(it.wareId, (amount > stashminus ? stashminus : amount)); - } - - std::forward_list itemList = getMarketItemList(it.wareId, stashmath, depotLocker); - - if (!itemList.empty()) { - if (it.stackable) { - uint16_t tmpAmount = stashmath; - for (Item *item : itemList) { - uint16_t removeCount = std::min(tmpAmount, item->getItemCount()); - tmpAmount -= removeCount; - internalRemoveItem(item, removeCount); + if (amount > player->getCoinBalance()) { + return; + } - } - } else { - for (Item *item : itemList) { - internalRemoveItem(item); + account.AddCoins(static_cast(amount)); + } else { + uint16_t stashmath = amount; + uint16_t stashminus = player->getStashItemCount(it.wareId); + if (stashminus > 0) { + stashmath = (amount - (amount > stashminus ? stashminus : amount)); + player->withdrawItem(it.wareId, (amount > stashminus ? stashminus : amount)); + } + + std::forward_list itemList = getMarketItemList(it.wareId, stashmath, depotLocker); + + if (!itemList.empty()) { + if (it.stackable) { + uint16_t tmpAmount = stashmath; + for (Item *item : itemList) { + uint16_t removeCount = std::min(tmpAmount, item->getItemCount()); + tmpAmount -= removeCount; + internalRemoveItem(item, removeCount); + } + } else { + for (Item *item : itemList) { + internalRemoveItem(item); + } } } } - } - g_game.removeMoney(player, fee, 0, true); + removeMoney(player, fee, 0, true); } else { uint64_t totalPrice = price * amount; @@ -7620,7 +7616,7 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr return; } - g_game.removeMoney(player, totalPrice, 0, true); + removeMoney(player, totalPrice, 0, true); } IOMarket::createOffer(player->getGUID(), static_cast(type), it.id, amount, price, anonymous); @@ -7777,20 +7773,15 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } } - if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(player->getAccount()); - uint32_t coins; - account.GetCoins(&coins); - if (amount > coins) - { - return; - } + account::Account account; + account.LoadAccountDB(player->getAccount()); + uint32_t coins; + account.GetCoins(COIN_TYPE_DEFAULT); - account.RemoveCoins(amount); - account.RegisterCoinsTransaction(account::COIN_REMOVE, amount, - "Sold on Market"); - } else { + if (it.id == ITEM_STORE_COIN) { + account.AddCoins(static_cast(amount)); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + } else { std::forward_list itemList = getMarketItemList(it.wareId, amount, depotLocker); if (itemList.empty()) { return; @@ -7816,15 +7807,13 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->setBankBalance(player->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(buyerPlayer->getAccount()); - account.AddCoins(amount); - account.RegisterCoinsTransaction(account::COIN_ADD, amount, - "Purchased on Market"); - } - else if (it.stackable) - { - uint16_t tmpAmount = amount; + account::Account account; + account.LoadAccountDB(buyerPlayer->getAccount()); + account.AddCoins(static_cast(amount)); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); + } + else if (it.stackable) { + uint16_t tmpAmount = amount; while (tmpAmount > 0) { uint16_t stackCount = std::min(100, tmpAmount); Item* item = Item::CreateItem(it.id, stackCount); @@ -7835,10 +7824,8 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 tmpAmount -= stackCount; } - } - else - { - int32_t subType; + } else { + int32_t subType; if (it.charges != 0) { subType = it.charges; } else { @@ -7852,9 +7839,9 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 break; } } - } + } - if (buyerPlayer->isOffline()) { + if (buyerPlayer->isOffline()) { IOLoginData::savePlayer(buyerPlayer); delete buyerPlayer; } @@ -7883,11 +7870,10 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(player->getAccount()); - account.AddCoins(amount); - account.RegisterCoinsTransaction(account::COIN_ADD, amount, - "Purchased on Market"); + account::Account account; + account.LoadAccountDB(player->getAccount()); + account.AddCoins(static_cast(amount)); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); } else if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { @@ -7920,28 +7906,27 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (sellerPlayer) { sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(sellerPlayer->getAccount()); - account.RegisterCoinsTransaction(account::COIN_REMOVE, amount, - "Sold on Market"); - } + account::Account account; + account.LoadAccountDB(sellerPlayer->getAccount()); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + } } else { IOLoginData::increaseBankBalance(offer.playerId, totalPrice); if (it.id == ITEM_STORE_COIN) { sellerPlayer = new Player(nullptr); if (IOLoginData::loadPlayerById(sellerPlayer, offer.playerId)) { - account::Account account; - account.LoadAccountDB(sellerPlayer->getAccount()); - account.RegisterCoinsTransaction(account::COIN_REMOVE, amount, - "Sold on Market"); - } + account::Account account; + account.LoadAccountDB(sellerPlayer->getAccount()); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + } delete sellerPlayer; } } + if (it.id != ITEM_STORE_COIN) { - player->onReceiveMail(); + player->onReceiveMail(); } } @@ -7966,424 +7951,6 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->updateMarketExhausted(); // Exhausted for accept offer in the market } -void Game::playerStoreOpen(uint32_t playerId, uint8_t serviceType) -{ - Player* player = getPlayerByID(playerId); - if (player) { - player->sendOpenStore(serviceType); - } -} - -void Game::playerShowStoreCategoryOffers(uint32_t playerId, StoreCategory* category) -{ - Player* player = getPlayerByID(playerId); - if (player) { - player->sendShowStoreCategoryOffers(category); - } -} - -void Game::playerBuyStoreOffer(uint32_t playerId, uint32_t offerId, - uint8_t productType, - const std::string & additionalInfo /* ="" */ ) { - Player * player = getPlayerByID(playerId); - if (player) { - const BaseOffer * offer = gameStore.getOfferByOfferId(offerId); - - if (offer == nullptr || offer -> type == DISABLED) { - player -> sendStoreError(STORE_ERROR_NETWORK, "The offer is either fake or corrupt."); - return; - } - - account::Account account; - account.LoadAccountDB(player -> getAccount()); - uint32_t coins; - account.GetCoins( & coins); - if (coins < offer -> price) // player doesnt have enough coins - { - player -> sendStoreError(STORE_ERROR_PURCHASE, "You don't have enough coins"); - return; - } - - std::stringstream message; - if (offer -> type == ITEM || offer -> type == STACKABLE_ITEM || offer -> type == WRAP_ITEM) { - const ItemOffer * tmp = (ItemOffer * ) offer; - - message << "You have purchased " << tmp -> count << "x " << offer -> name << " for " << offer -> price << " coins."; - - Thing * thing = player -> getThing(CONST_SLOT_STORE_INBOX); - if (thing == nullptr) { - player -> sendStoreError(STORE_ERROR_NETWORK, "We cannot locate your store inbox, try again after relog and if this error persists, contact the system administrator."); - return; - } - - Container * inbox = thing -> getItem() -> getContainer(); // TODO: Not the right way to get the storeInbox - if (!inbox) { - player -> sendStoreError(STORE_ERROR_NETWORK, "We cannot locate your store inbox, try again after relog and if this error persists, contact the system administrator."); - return; - } - - uint32_t freeSlots = inbox -> capacity() - inbox -> size(); - uint32_t requiredSlots = (tmp -> type == ITEM || tmp -> type == WRAP_ITEM) ? tmp -> count : (tmp -> count % 100) ? (uint32_t)(tmp -> count / 100) + 1 : (uint32_t) tmp -> count / 100; - uint32_t capNeeded = (tmp -> type == WRAP_ITEM) ? 0 : Item::items[tmp -> productId].weight * tmp -> count; - if (freeSlots < requiredSlots) { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Insuficient free slots in your store inbox."); - return; - } else if (player -> getFreeCapacity() < capNeeded) { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Not enough cap to carry."); - return; - } else { - uint16_t pendingCount = tmp -> count; - uint8_t packSize = (offer -> type == STACKABLE_ITEM) ? 100 : 1; - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - while (pendingCount > 0) { - Item * item; - - if (offer -> type == WRAP_ITEM) { - item = Item::CreateItem(TRANSFORM_BOX_ID, std::min < uint16_t > (packSize, pendingCount)); - item -> setActionId(tmp -> productId); - item -> setSpecialDescription("Unwrap it in your own house to create a <" + Item::items[tmp -> productId].name + ">."); - } else { - item = Item::CreateItem(tmp -> productId, std::min < uint16_t > (packSize, pendingCount)); - } - - if (internalAddItem(inbox, item, INDEX_WHEREEVER, FLAG_NOLIMIT) != RETURNVALUE_NOERROR) { - delete item; - player -> sendStoreError(STORE_ERROR_PURCHASE, "We couldn't deliver all the items.\nOnly the delivered ones were charged from you account"); - account.AddCoins((offer -> price * (tmp -> count - pendingCount) / tmp -> count)); - account.RegisterCoinsTransaction(account::COIN_REMOVE, - offer -> price + (offer -> price * (tmp -> count - pendingCount)) / tmp -> count, - offer -> name); - return; - } - pendingCount -= std::min < uint16_t > (pendingCount, packSize); - } - - account.GetCoins( & coins); - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } - } else if (offer -> type == OUTFIT || offer -> type == OUTFIT_ADDON) { - const OutfitOffer * outfitOffer = (OutfitOffer * ) offer; - - uint16_t looktype = (player -> getSex() == PLAYERSEX_MALE) ? outfitOffer -> maleLookType : outfitOffer -> femaleLookType; - uint8_t addons = outfitOffer -> addonNumber; - - if (!player -> canWear(looktype, addons)) { - player -> addOutfit(looktype, addons); - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - message << "You've successfully bought the " << outfitOffer -> name << "."; - account.GetCoins( & coins); - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } else { - player -> sendStoreError(STORE_ERROR_NETWORK, "This outfit seems not to suit you well, we are sorry for that!"); - return; - } - } else if (offer -> type == MOUNT) { - const MountOffer * mntOffer = (MountOffer * ) offer; - const Mount * mount = mounts.getMountByID(mntOffer -> mountId); - if (player -> hasMount(mount)) { - player -> sendStoreError(STORE_ERROR_PURCHASE, "You arealdy own this mount."); - return; - } else { - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(mntOffer -> price); - if (!player -> tameMount(mount -> id)) { - account.AddCoins(mntOffer -> price); - player -> sendStoreError(STORE_ERROR_PURCHASE, "An error ocurred processing your purchase. Try again later."); - return; - } else { - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - message << "You've successfully bought the " << mount -> name << " Mount."; - account.GetCoins( & coins); - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } - } - } else if (offer -> type == NAMECHANGE) { - if (productType == SIMPLE) { // client didn't sent the new name yet, request additionalInfo - player -> sendStoreRequestAdditionalInfo(offer -> id, ADDITIONALINFO); - return; - } else { - Database & db = Database::getInstance(); - std::ostringstream query; - std::string newName = additionalInfo; - trimString(newName); - - query << "SELECT `id` FROM `players` WHERE `name`=" << db.escapeString(newName); - if (db.storeQuery(query.str())) { // name already in use - message << "This name is already in use."; - player -> sendStoreError(STORE_ERROR_PURCHASE, message.str()); - return; - } else { - query.str(""); - toLowerCaseString(newName); - - std::string responseMessage; - NameEval_t nameValidation = validateName(newName); - - switch (nameValidation) { - case INVALID_LENGTH: - responseMessage = "Your new name must be more than 3 and less than 14 characters long."; - break; - case INVALID_TOKEN_LENGTH: - responseMessage = "Every words of your new name must be at least 2 characters long."; - break; - case INVALID_FORBIDDEN: - responseMessage = "You're using forbidden words in your new name."; - break; - case INVALID_CHARACTER: - responseMessage = "Your new name contains invalid characters."; - break; - case INVALID: - responseMessage = "Your new name is invalid."; - break; - case VALID: - responseMessage = "You have successfully changed you name, you must relog to see changes."; - break; - } - - if (nameValidation != VALID) { // invalid name typed - player -> sendStoreError(STORE_ERROR_PURCHASE, responseMessage); - return; - } else { // valid name so far - - // check if it's an NPC or Monster name. - - if (g_monsters.getMonsterType(newName)) { - responseMessage = "Your new name cannot be a monster's name."; - player -> sendStoreError(STORE_ERROR_PURCHASE, responseMessage); - return; - } else if (g_npcs.getNpcType(newName)) { - responseMessage = "Your new name cannot be an NPC's name."; - player -> sendStoreError(STORE_ERROR_PURCHASE, responseMessage); - return; - } else { - capitalizeWords(newName); - - query << "UPDATE `players` SET `name` = " << db.escapeString(newName) << " WHERE `id` = " << - player -> getGUID(); - if (db.executeQuery(query.str())) { - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, - offer -> price, offer -> name); - account.GetCoins( & coins); - message << "You have successfully changed you name, you must relog to see the changes."; - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } else { - message << "An error ocurred processing your request, no changes were made."; - player -> sendStoreError(STORE_ERROR_PURCHASE, message.str()); - return; - } - } - } - } - } - } else if (offer -> type == SEXCHANGE) { - PlayerSex_t playerSex = player -> getSex(); - Outfit_t playerOutfit = player -> getCurrentOutfit(); - - message << "Your character is now "; - - for (auto outfit: player -> outfits) { // adding all outfits of the oposite sex. - const Outfit * opositeSexOutfit = Outfits::getInstance().getOpositeSexOutfitByLookType(playerSex, outfit.lookType); - - if (opositeSexOutfit) { - player -> addOutfit(opositeSexOutfit -> lookType, 0); // since addons could have different recipes, we can't add automatically - } - } - - if (playerSex == PLAYERSEX_FEMALE) { - player -> setSex(PLAYERSEX_MALE); - playerOutfit.lookType = 128; // default citizen - playerOutfit.lookAddons = 0; - - message << "male."; - } else { //player is male - player -> setSex(PLAYERSEX_FEMALE); - playerOutfit.lookType = 136; // default citizen - playerOutfit.lookAddons = 0; - message << "female."; - } - playerChangeOutfit(player -> getID(), playerOutfit); - // TODO: add the other sex equivalent outfits player already have in the current sex. - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - account.GetCoins( & coins); - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } else if (offer -> type == PROMOTION) { - if (player -> isPremium() && !player -> isPromoted()) { - uint16_t promotedId = g_vocations.getPromotedVocation(player -> getVocation() -> getId()); - - if (promotedId == VOCATION_NONE || promotedId == player -> getVocation() -> getId()) { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Your character cannot be promoted."); - return; - } else { - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, - offer -> price, offer -> name); - account.GetCoins( & coins); - player -> setVocation(promotedId); - player -> addStorageValue(STORAGEVALUE_PROMOTION, 1); - message << "You've been promoted! Relog to see the changes."; - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } - } else { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Your character cannot be promoted."); - return; - } - } else if (offer -> type == PREMIUM_TIME) { - PremiumTimeOffer * premiumTimeOffer = (PremiumTimeOffer * ) offer; - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - account.GetCoins( & coins); - player -> setPremiumDays(player -> premiumDays + premiumTimeOffer -> days); - IOLoginData::addPremiumDays(player -> getAccount(), premiumTimeOffer -> days); - message << "You've successfully bought " << premiumTimeOffer -> days << " days of premium time."; - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } else if (offer -> type == TELEPORT) { - TeleportOffer * tpOffer = (TeleportOffer * ) offer; - if (player -> canLogout()) { - Position toPosition; - Position fromPosition = player -> getPosition(); - if (tpOffer -> position.x == 0 || tpOffer -> position.y == 0 || tpOffer -> position.z == 0) { //temple teleport - toPosition = player -> getTemplePosition(); - } else { - toPosition = tpOffer -> position; - } - - ReturnValue returnValue = internalTeleport(player, toPosition, false); - if (returnValue != RETURNVALUE_NOERROR) { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Your character cannot be teleported there at the moment."); - return; - } else { - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - account.GetCoins( & coins); - addMagicEffect(fromPosition, CONST_ME_POFF); - addMagicEffect(toPosition, CONST_ME_TELEPORT); - player -> sendStorePurchaseSuccessful("You've successfully been teleported.", coins); - return; - } - } else { - player -> sendStoreError(STORE_ERROR_PURCHASE, "Your character has some teleportation block at the moment and cannot be teleported."); - return; - } - } else if (offer -> type == BLESSING) { - BlessingOffer * blessingOffer = (BlessingOffer * ) offer; - - uint8_t blessingsToAdd = 0; - for (uint8_t bless: blessingOffer -> blessings) { - if (player -> hasBlessing(bless)) { // player already has this bless - message << "Your character already has "; - message << ((blessingOffer -> blessings.size() > 1) ? "one or more of these blessings." : "this bless."); - - player -> sendStoreError(STORE_ERROR_PURCHASE, message.str()); - return; - } - blessingsToAdd = bless; - } - account.LoadAccountDB(player -> getAccount()); - account.RemoveCoins(offer -> price); - account.RegisterCoinsTransaction(account::COIN_REMOVE, offer -> price, - offer -> name); - account.GetCoins( & coins); - player -> addBlessing(blessingsToAdd, 1); - message << "You've successfully bought the " << offer -> name << "."; - player -> sendStorePurchaseSuccessful(message.str(), coins); - return; - } else { - // TODO: BOOST_XP and BOOST_STAMINA (the support systems are not yet implemented) - player -> sendStoreError(STORE_ERROR_INFORMATION, "JLCVP: NOT YET IMPLEMENTED!"); - return; - } - } -} - -void Game::playerCoinTransfer(uint32_t playerId, - const std::string & receiverName, uint32_t amount) { - Player * sender = getPlayerByID(playerId); - Player * receiver = getPlayerByName(receiverName); - std::stringstream message; - if (!sender) { - return; - } else if (!receiver) { - message << "Player \"" << receiverName << "\" doesn't exist."; - sender -> sendStoreError(STORE_ERROR_TRANSFER, message.str()); - return; - } else { - - account::Account sender_account; - sender_account.LoadAccountDB(sender -> getAccount()); - account::Account receiver_account; - receiver_account.LoadAccountDB(receiver -> getAccount()); - uint32_t sender_coins; - sender_account.GetCoins( & sender_coins); - - if (sender -> getAccount() == receiver -> getAccount()) { // sender and receiver are the same - message << "You cannot send coins to your own account."; - sender -> sendStoreError(STORE_ERROR_TRANSFER, message.str()); - return; - } else if (sender_coins < amount) { - message << "You don't have enough funds to transfer these coins."; - sender -> sendStoreError(STORE_ERROR_TRANSFER, message.str()); - return; - } else { - - sender_account.RemoveCoins(amount); - receiver_account.AddCoins(amount); - message << "Transfered to " << receiverName; - sender_account.RegisterCoinsTransaction(account::COIN_REMOVE, amount, - message.str()); - - message.str(""); - message << "Received from" << sender -> name; - receiver_account.RegisterCoinsTransaction(account::COIN_REMOVE, - amount, message.str()); - - sender_account.GetCoins( & sender_coins); - message.str(""); - message << "You have successfully transfered " << amount << " coins to " << receiverName << "."; - sender -> sendStorePurchaseSuccessful(message.str(), sender_coins); - if (receiver && !receiver -> isOffline()) { - receiver -> sendCoinBalance(); - } - } - } -} - -void Game::playerStoreTransactionHistory(uint32_t playerId, uint32_t page) -{ - Player* player = getPlayerByID(playerId); - if (player) { - HistoryStoreOfferList list = IOGameStore::getHistoryEntries(player->getAccount(),page); - if (!list.empty()) { - player->sendStoreTrasactionHistory(list, page, GameStore::HISTORY_ENTRIES_PER_PAGE); - } else { - player->sendStoreError(STORE_ERROR_HISTORY, "You don't have any entries yet."); - } - } -} - void Game::parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer) { Player* player = getPlayerByID(playerId); @@ -8741,3 +8308,26 @@ bool Game::hasDistanceEffect(uint8_t effectId) { } return false; } + +bool Game::addAccountHistory(uint32_t accountId, StoreHistory history) +{ + storeHistory[accountId].emplace_back(history); + + return true; +} + +void Game::loadAccountStoreHistory(uint32_t account, std::vector history) +{ + storeHistory[account] = history; +} + +bool Game::getAccountHistory(const uint32_t accountId, std::vector& history) const +{ + auto it = storeHistory.find(accountId); + if (it == storeHistory.end()) { + return false; + } + + history = it->second; + return true; +} diff --git a/src/game/game.h b/src/game/game.h index a9e97f69d5a..ad5a19cc4c9 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -25,8 +25,9 @@ #include "creatures/players/account/account.hpp" #include "creatures/combat/combat.h" #include "items/containers/container.h" -#include "game/gamestore.h" +#include "game/gamestore.hpp" #include "creatures/players/grouping/groups.h" +#include "game/gamestore.hpp" #include "io/iobestiary.h" #include "items/item.h" #include "map/map.h" @@ -325,11 +326,6 @@ class Game void playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spriteId, uint16_t amount, uint32_t price, bool anonymous); void playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter); void playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16_t counter, uint16_t amount); - void playerStoreOpen(uint32_t playerId, uint8_t serviceType); - void playerShowStoreCategoryOffers(uint32_t playerId, StoreCategory* category); - void playerBuyStoreOffer(uint32_t playerId, uint32_t offerId, uint8_t productType, const std::string& additionalInfo=""); - void playerCoinTransfer(uint32_t playerId, const std::string& receiverName, uint32_t amount); - void playerStoreTransactionHistory(uint32_t playerId, uint32_t page); void parsePlayerExtendedOpcode(uint32_t playerId, uint8_t opcode, const std::string& buffer); @@ -453,11 +449,14 @@ class Game bool hasEffect(uint8_t effectId); bool hasDistanceEffect(uint8_t effectId); + bool addAccountHistory(uint32_t accountId, StoreHistory history); + void loadAccountStoreHistory(uint32_t account, std::vector history); + bool getAccountHistory(const uint32_t accountId, std::vector& history) const; + Groups groups; Map map; Mounts mounts; Raids raids; - GameStore gameStore; std::forward_list toDecayItems; @@ -529,6 +528,9 @@ class Game void checkDecay(); void internalDecayItem(Item* item); + // Account id and history + std::unordered_map> storeHistory; + std::unordered_map players; std::unordered_map mappedPlayerNames; std::unordered_map guilds; diff --git a/src/game/game_definitions.hpp b/src/game/game_definitions.hpp index f7e0000484c..3dcb253e03e 100644 --- a/src/game/game_definitions.hpp +++ b/src/game/game_definitions.hpp @@ -46,27 +46,6 @@ enum ClientOffer_t{ ADDITIONALINFO=1 }; -enum StoreState_t { - NORMAL=0, - NEW, - SALE, - LIMITED_TIME -}; - -enum GameStoreError_t{ - STORE_ERROR_PURCHASE=0, - STORE_ERROR_NETWORK, - STORE_ERROR_HISTORY, - STORE_ERROR_TRANSFER, - STORE_ERROR_INFORMATION -}; - -enum StoreService_t { - SERVICE_STANDARD = 0, - SERVICE_OUTFIT = 3, - SERVICE_MOUNT = 4 -}; - enum StackPosType_t { STACKPOS_MOVE, STACKPOS_LOOK, @@ -112,6 +91,24 @@ enum LightState_t { LIGHT_STATE_SUNRISE, }; +enum BlessType_t : uint8_t +{ + ADVENTURE_BLESS = 0, + TWIST_OF_FATE = 1, // PVP Bless + WISDOM_OF_SOLITUDE = 2, + SPARK_OF_THE_PHOENIX = 3, + FIRE_OF_THE_SUNS = 4, + SPIRITUAL_SHIELDING = 5, + EMBRACE_OF_TIBIA = 6, + HEART_OF_THE_MOUNTAIN = 7, + BLOOD_OF_THE_MOUNTAIN = 8, + + BLESS_ADV_FIRST = ADVENTURE_BLESS, + BLESS_FIRST = TWIST_OF_FATE, + BLESS_PVE_FIRST = WISDOM_OF_SOLITUDE, + BLESS_LAST = BLOOD_OF_THE_MOUNTAIN +}; + enum CyclopediaCharacterInfoType_t : uint8_t { CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION = 0, CYCLOPEDIA_CHARACTERINFO_GENERALSTATS = 1, @@ -159,16 +156,13 @@ enum Webhook_Colors_t : uint32_t { WEBHOOK_COLOR_RAID = 0x0000FF }; -// Structs -struct HistoryStoreOffer { - uint32_t time; - uint8_t mode; - uint32_t amount; - std::string description; +enum CoinType_t : uint8_t { + COIN_TYPE_DEFAULT = 0, + COIN_TYPE_TRANSFERABLE = 1, + COIN_TYPE_TOURNAMENT = 2, }; -using HistoryStoreOfferList = std::vector; - +// Structs struct ModalWindow { std::list> buttons, choices; std::string title, message; @@ -191,7 +185,6 @@ struct BaseOffer{ std::string description; uint32_t price; Offer_t type; - StoreState_t state; std::vector icons; }; @@ -222,12 +215,4 @@ struct BlessingOffer : BaseOffer{ std::vector blessings; }; -struct StoreCategory{ - std::string name; - std::string description; - StoreState_t state; - std::vector icons; - std::vector offers; -}; - #endif // SRC_GAME_GAME_DEFINITIONS_HPP_ diff --git a/src/game/gamestore.cpp b/src/game/gamestore.cpp index 9f2f20380b2..d4775c74a4b 100644 --- a/src/game/gamestore.cpp +++ b/src/game/gamestore.cpp @@ -19,335 +19,728 @@ #include "otpch.h" -#include - -#include "database/database.h" -#include "game/gamestore.h" +#include "config/configmanager.h" +#include "game/game.h" +#include "game/gamestore.hpp" #include "utils/pugicast.h" -#include "utils/tools.h" - -uint16_t GameStore::HISTORY_ENTRIES_PER_PAGE=16; -std::vector getIconsVector(std::string rawString) +extern Game g_game; +extern ConfigManager g_config; + +const std::unordered_map CoinTypeMap = { + {"coin", COIN_TYPE_DEFAULT}, + {"transferable", COIN_TYPE_TRANSFERABLE}, + {"tournament", COIN_TYPE_TOURNAMENT} +}; +const std::unordered_map OfferStatesMap = { + {"none", OFFER_STATE_NONE}, + {"new", OFFER_STATE_NEW}, + {"sale", OFFER_STATE_SALE}, + {"timed", OFFER_STATE_TIMED} +}; +const std::unordered_map OfferTypesMap = { + {"none", OFFER_TYPE_NONE}, + {"item", OFFER_TYPE_ITEM}, + {"stackeable", OFFER_TYPE_STACKABLE}, + {"outfit", OFFER_TYPE_OUTFIT}, + {"outfit addon", OFFER_TYPE_OUTFIT_ADDON}, + {"mount", OFFER_TYPE_MOUNT}, + {"namechange", OFFER_TYPE_NAMECHANGE}, + {"sexchange", OFFER_TYPE_SEXCHANGE}, + {"promotion", OFFER_TYPE_PROMOTION}, + {"house", OFFER_TYPE_HOUSE}, + {"expboost", OFFER_TYPE_EXPBOOST}, + {"preyslot", OFFER_TYPE_PREYSLOT}, + {"preybonus", OFFER_TYPE_PREYBONUS}, + {"temple", OFFER_TYPE_TEMPLE}, + {"blessing", OFFER_TYPE_BLESSINGS}, + {"premium", OFFER_TYPE_PREMIUM}, + {"pouch", OFFER_TYPE_POUCH}, + {"allblessing", OFFER_TYPE_ALLBLESSINGS}, + {"reward", OFFER_TYPE_INSTANT_REWARD_ACCESS}, + {"training", OFFER_TYPE_TRAINING}, + {"charmexpansion", OFFER_TYPE_CHARM_EXPANSION}, + {"charmpoints", OFFER_TYPE_CHARM_POINTS}, + {"multiitems", OFFER_TYPE_MULTI_ITEMS}, + {"vip", OFFER_TYPE_VIP}, + {"fragremove", OFFER_TYPE_FRAG_REMOVE}, + {"skullremove", OFFER_TYPE_SKULL_REMOVE}, + {"recoverykey", OFFER_TYPE_RECOVERYKEY}, +}; + +const std::unordered_map OfferBuyTypesMap = { + {"none", OFFER_BUY_TYPE_OTHERS}, + {"offername", OFFER_BUY_TYPE_NAMECHANGE}, + {"teste", OFFER_BUY_TYPE_TESTE} +}; + +const std::unordered_map OfferSkullMap = { + {"none", SKULL_NONE}, + {"red", SKULL_RED}, + {"black", SKULL_BLACK} +}; + + +bool GameStore::isValidType(OfferTypes_t type) { - std::vector icons; - boost::split(icons, rawString, boost::is_any_of("|")); //converting the |-separated string to a vector of tokens - icons.shrink_to_fit(); - return icons; + auto it = std::find_if(OfferTypesMap.begin(), OfferTypesMap.end(), [type](std::pair const& pair) { + return pair.second == type; + }); + + return it != OfferTypesMap.end(); } -std::vector getIntVector(std::string rawString) -{ - std::vector ints; - std::vector rawInts; - boost::split(rawInts, rawString, boost::is_any_of("|")); +bool GameStore::loadFromXml(bool /* reloading */) { + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/gamestore.xml"); + if (!result) { + printXMLError("Error - GameStore::loadFromXml", "data/XML/gamestore.xml", result); + return false; + } + + loaded = true; + for (auto baseNode : doc.child("store").children()) { + pugi::xml_attribute attr; + // StoreCategory + if (strcasecmp(baseNode.name(), "category") == 0) { + pugi::xml_attribute name = baseNode.attribute("name"); + if (!name) { + std::cout << "[Warning - GameStore::loadFromXml] Missing name for Category entry" << std::endl; + continue; + } - for(std::string numStr : rawInts) { - uint8_t i = (uint8_t)std::stoi(numStr) ; - ints.push_back(i); - } - ints.shrink_to_fit(); - return ints; -} + std::vector offersName; + for (auto childNode : baseNode.children()) { + if (strcasecmp(childNode.name(), "subcategory") == 0) { + offersName.push_back( childNode.attribute("name").as_string() ); + } + } -bool GameStore::reload() -{ - for (auto category:storeCategoryOffers) { - for (auto offer:category->offers) { - offer->icons.clear(); - if (offer->type == BLESSING) { - ((BlessingOffer *) offer)->blessings.clear(); + categories.emplace_back( + name.value(), + offersName, + baseNode.attribute("icon").as_string(), + baseNode.attribute("rookgaard").as_bool(true) + ); + + offercount++; + + categories.shrink_to_fit(); + // Home + } else if (strcasecmp(baseNode.name(), "home") == 0) { + for (auto childNode : baseNode.children()) { + if (strcasecmp(childNode.name(), "offer") == 0) { + home.offers.push_back( childNode.attribute("name").as_string() ); + } else if (strcasecmp(childNode.name(), "banner") == 0) { + home.banners.push_back( childNode.attribute("image").as_string() ); + } } - free(offer); //offer is a pointer, so it needs to be released manually - } - category->offers.clear(); - category->icons.clear(); - free(category); //category is also a pointer - } - storeCategoryOffers.clear(); - loaded = false; - return loadFromXml(); -} -bool GameStore::loadFromXml() -{ - if (isLoaded()) { - return true; - } else { - offerCount = 0; - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file("data/XML/gamestore.xml"); - if (!result) { - printXMLError("Error - GameStore::loadFromXml", "data/XML/gamestore.xml", result); - return false; - } + // Offers + } else if (strcasecmp(baseNode.name(), "offers") == 0) { + pugi::xml_attribute attributeName = baseNode.attribute("name"); + if (!attributeName) { + std::cout << "[Warning - GameStore::loadFromXml] Missing name for Offers entry" << std::endl; + continue; + } + + // make StoreOffers + std::string name = attributeName.value(); - for (auto categoryNode : doc.child("gamestore").children()) { //category iterator - StoreCategory *cat = new StoreCategory(); - cat->name = categoryNode.attribute("name").as_string(); - cat->description = categoryNode.attribute("description").as_string(""); - if (!cat->name.length()) { - printXMLError("Error parsing XML category name - GameStore::loadFromXml", "data/XML/gamestore.xml", result); - return false; + auto res = storeOffers.emplace(std::piecewise_construct, + std::forward_as_tuple(name), + std::forward_as_tuple(name) + ); + + if (!res.second) { + std::cout << "[Warning - GameStore::loadFromXml] Duplicate Category Offer by name: '" << name << "' [ignored]" << std::endl; + continue; } - std::string state = categoryNode.attribute("state").as_string("normal"); - if (boost::iequals(state, "normal")) { //reading state (defaults to normal) - cat->state = StoreState_t::NORMAL; - } else if (boost::iequals(state, "new")) { - cat->state = StoreState_t::NEW; - } else if (boost::iequals(state, "sale")) { - cat->state = StoreState_t::SALE; - } else if (boost::iequals(state, "limitedtime")) { - cat->state = StoreState_t::LIMITED_TIME; + offercount++; + + // editando StoreOffers na memoria + StoreOffers& offers = res.first->second; + // reaproveitando variavel + attributeName = baseNode.attribute("description"); + if (attributeName) { + offers.description = attributeName.value(); } - cat->icons = getIconsVector(categoryNode.attribute("icons").as_string("default.png")); - - for (auto offerNode : categoryNode.children()) { - std::string type = offerNode.attribute("type").as_string(); - BaseOffer *offer = nullptr; - if (boost::iequals(type, "namechange")) { - offer = new BaseOffer(); - offer->type = NAMECHANGE; - } else if (boost::iequals(type, "sexchange")) { - offer = new BaseOffer(); - offer->type = SEXCHANGE; - } else if (boost::iequals(type, "promotion")) { - offer = new BaseOffer(); - offer->type = PROMOTION; - } else if (boost::iequals(type, "outfit")) { - OutfitOffer *tmp = new OutfitOffer(); - tmp->type = OUTFIT; - tmp->maleLookType = (uint16_t) offerNode.attribute("malelooktype").as_uint(); - tmp->femaleLookType = (uint16_t) offerNode.attribute("femalelooktype").as_uint(); - tmp->addonNumber = (uint8_t) offerNode.attribute("addon").as_uint(0); - if (!tmp->femaleLookType || !tmp->maleLookType || tmp->addonNumber > 3) { - printXMLError("Error parsing XML outfit offer - GameStore::loadFromXml", - "data/XML/gamestore.xml", - result); - return false; - } else { - offer = tmp; + + attributeName = baseNode.attribute("icon"); + if (attributeName) { + offers.icon = attributeName.value(); + } + + attributeName = baseNode.attribute("rookgaard"); + if (attributeName) { + offers.rookgaard = attributeName.as_bool(); + } + + attributeName = baseNode.attribute("state"); + if (attributeName) { + auto parseState = OfferStatesMap.find(attributeName.value()); + if (parseState != OfferStatesMap.end()) { + offers.state = parseState->second; + } + } + + if (offers.state == OFFER_STATE_SALE) { + saleoffer = true; + } else if (offers.state == OFFER_STATE_NEW) { + newoffer = true; + } + + attributeName = baseNode.attribute("parent"); + if (attributeName) { + offers.parent = attributeName.value(); + } + + // pegando as ofertas + for (auto childNode : baseNode.children()) { + if (strcasecmp(childNode.name(), "offer") == 0) { + if (!(attr = childNode.attribute("name"))) { + std::cout << "[Warning - GameStore::loadFromXml] Missing key attribute in GameStore::StoreOffers::StoreOffer name" << std::endl; + continue; } - } else if (boost::iequals(type, "addon")) { - OutfitOffer *tmp = new OutfitOffer(); - tmp->type = OUTFIT_ADDON; - tmp->maleLookType = (uint16_t) offerNode.attribute("malelooktype").as_uint(); - tmp->femaleLookType = (uint16_t) offerNode.attribute("femalelooktype").as_uint(); - tmp->addonNumber = (uint8_t) offerNode.attribute("addon").as_uint(0); - if (!tmp->femaleLookType || !tmp->maleLookType || !tmp->addonNumber || tmp->addonNumber > 3) { - printXMLError("Error parsing XML addon offer - GameStore::loadFromXml", "data/XML/gamestore.xml", result); - return false; + + name = attr.value(); + uint32_t id = 0; + pugi::xml_attribute childNodeName = childNode.attribute("id"); + if (childNodeName) { + id = pugi::cast(childNodeName.value()); } else { - offer = tmp; + runningid++; + id = runningid; } - } else if (boost::iequals(type, "mount")) { - MountOffer *tmp = new MountOffer(); - tmp->type = MOUNT; - tmp->mountId = (uint8_t) offerNode.attribute("mountid").as_uint(); - if (!tmp->mountId) { - printXMLError( - "Error parsing XML mountID number not specified for an mount offer - GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; - } else { - offer = tmp; + + auto resultOffer = offers.offers.emplace(std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple(id, name )); + + if (!resultOffer.second) { + std::cout << "[Warning - GameStore::loadFromXml] Duplicate Store Offer by name: '" << attr.value() << "'" << std::endl; + continue; } - } else if (boost::iequals(type, "item")) { - ItemOffer *tmp = new ItemOffer(); - tmp->type = ITEM; - tmp->productId = (uint16_t) offerNode.attribute("productid").as_uint(); - tmp->count = (uint16_t) offerNode.attribute("count").as_uint(); - - if (!tmp->productId || !tmp->count) { - printXMLError("Error parsing XML Item Offer - GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; - } else { - offer = tmp; + + StoreOffer& offer = resultOffer.first->second; + childNodeName = childNode.attribute("price"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need price" << std::endl; + continue; } - } else if (boost::iequals(type, "stackableitem")) { - ItemOffer *tmp = new ItemOffer(); - tmp->type = STACKABLE_ITEM; - tmp->productId = (uint16_t) offerNode.attribute("productid").as_uint(); - tmp->count = (uint16_t) offerNode.attribute("count").as_uint(); - - if (!tmp->productId || !tmp->count) { - printXMLError("Error parsing XML Stackable Item Offer - GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; - } else { - offer = tmp; + offer.price = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("count"); + if (childNodeName) { + offer.count = pugi::cast(childNodeName.value()); } - } else if (boost::iequals(type, "wrapitem")) { - ItemOffer *tmp = new ItemOffer(); - tmp->type = WRAP_ITEM; - tmp->productId = (uint16_t) offerNode.attribute("productid").as_uint(); - tmp->count = (uint16_t) offerNode.attribute("count").as_uint(); - if (!tmp->productId || !tmp->count) { - printXMLError("Error parsing XML Wrappable Item Offer - GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; - } else { - offer = tmp; + + childNodeName = childNode.attribute("icon"); + if (childNodeName) { + offer.icon = childNodeName.value(); } - } else if (boost::iequals(type, "bless")) { - BlessingOffer* tmp = new BlessingOffer(); - tmp->blessings = getIntVector(offerNode.attribute("blessnumber").as_string()); - tmp->type = BLESSING; - if (!tmp->blessings.size()) { - //no number was found - printXMLError("Error Parsing XML bless offer - " - "no blessnumber specified - " - "GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; + + childNodeName = childNode.attribute("description"); + if (childNodeName) { + offer.description = childNodeName.value(); } - offer = tmp; - } else if (boost::iequals(type, "teleport")) { - TeleportOffer* tmp = new TeleportOffer(); - tmp->type = TELEPORT; - - uint16_t posX, posY; - uint8_t posZ; - posX = (uint16_t)offerNode.attribute("x").as_uint(); - posY = (uint16_t)offerNode.attribute("y").as_uint(); - posZ = (uint8_t)offerNode.attribute("z").as_uint(); - - tmp->position = Position(posX,posY,posZ); - - offer = tmp; - } else if (boost::iequals(type, "premiumtime")) { - PremiumTimeOffer* tmp = new PremiumTimeOffer(); - tmp->type = PREMIUM_TIME; - - tmp->days = (uint16_t)offerNode.attribute("days").as_uint(); - if (tmp->days == 0) { - printXMLError("Error parsing XML premiumtime offer type " - "- required 'days' attribute not found - " - "GameStore::loadFromXml", - "data/XML/gamestore.xml", - result); - return false; + childNodeName = childNode.attribute("description12"); + if (childNodeName) { + offer.description12 = childNodeName.value(); + replaceString(offer.description12, "
  • ", "•"); } - offer = tmp; - } + childNodeName = childNode.attribute("type"); + if (childNodeName) { + auto parseType = OfferTypesMap.find(childNodeName.value()); + if (parseType != OfferTypesMap.end()) { + offer.type = parseType->second; + } + } + + + childNodeName = childNode.attribute("disabled"); + if (childNodeName) { + offer.disabled = childNodeName.as_bool(); + } + + if (offer.type == OFFER_TYPE_OUTFIT || offer.type == OFFER_TYPE_OUTFIT_ADDON) { + childNodeName = childNode.attribute("female"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need female outfit" << std::endl; + continue; + } + offer.female = pugi::cast(childNodeName.value()); + childNodeName = childNode.attribute("male"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need male outfit" << std::endl; + continue; + } + offer.male = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("addon"); + if (!childNodeName) { + offer.addon = 0; + } else { + offer.addon = pugi::cast(childNodeName.value()); + } + } else if (offer.type == OFFER_TYPE_BLESSINGS) { + childNodeName = childNode.attribute("blessid"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need bless id" << std::endl; + continue; + } + offer.blessid = pugi::cast(childNodeName.value()); + } else if (offer.type == OFFER_TYPE_ITEM || offer.type == OFFER_TYPE_STACKABLE || + offer.type == OFFER_TYPE_HOUSE || offer.type == OFFER_TYPE_TRAINING || + offer.type == OFFER_TYPE_POUCH) { + + childNodeName = childNode.attribute("itemtype"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need itemtype" << std::endl; + continue; + } + offer.itemtype = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("charges"); + if (childNodeName) { + offer.charges = pugi::cast(childNodeName.value()); + } + + childNodeName = childNode.attribute("actionid"); + if (childNodeName) { + offer.actionid = pugi::cast(childNodeName.value()); + } + + if (offer.count == 0) { + offer.count = 1; + } + } else if (offer.type == OFFER_TYPE_MULTI_ITEMS) { + childNodeName = childNode.attribute("items"); + if (!childNodeName) { + std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need items" << std::endl; + continue; + } + + StringVector itemsList = explodeString(childNodeName.value(), ";"); + for (const std::string& itemsInfo : itemsList) { + StringVector info = explodeString(itemsInfo, ","); + if (info.size() == 2) { + uint16_t itemid = std::stoi(info[0]); + uint16_t item_count = std::stoi(info[1]); + offer.itemList[itemid] = item_count; + } + } + + } else if (offer.type == OFFER_TYPE_SKULL_REMOVE) { + childNodeName = childNode.attribute("skull"); + if (childNodeName) { + auto parseSkull = OfferSkullMap.find(childNodeName.value()); + if (parseSkull != OfferSkullMap.end()) { + offer.skull = parseSkull->second; + } + } + } + + childNodeName = childNode.attribute("state"); + if (childNodeName) { + auto parseState = OfferStatesMap.find(childNodeName.value()); + if (parseState != OfferStatesMap.end()) { + offer.state = parseState->second; + } + } + + if (offer.state == OFFER_STATE_SALE) { + saleoffer = true; + childNodeName = childNode.attribute("validUntil"); + if (childNodeName) { + offer.validUntil = pugi::cast(childNodeName.value()); + } + childNodeName = childNode.attribute("basePrice"); + if (childNodeName) { + offer.basePrice = pugi::cast(childNodeName.value()); + } + } else if (offer.state == OFFER_STATE_NEW) { + newoffer = true; + } - if (!offer) { - printXMLError("Error parsing XML invalid offer type - GameStore::loadFromXml", "data/XML/gamestore.xml", result); - return false; - } else { - offer->name = offerNode.attribute("name").as_string(); - offer->price = offerNode.attribute("price").as_uint(); - offer->description = offerNode.attribute("description").as_string(""); - offer->icons = getIconsVector(offerNode.attribute("icons").as_string("default.png")); - - std::string offerstate = categoryNode.attribute("state").as_string("normal"); - - if (boost::iequals(offerstate, "normal")) { //reading state (defaults to normal) - offer->state = StoreState_t::NORMAL; - } else if (boost::iequals(offerstate, "new")) { - offer->state = StoreState_t::NEW; - } else if (boost::iequals(offerstate, "sale")) { - //offer->state = StoreState_t::SALE; - // TODO: Solve the client crash with sale offers. Probably we need to add the previous price to show the strikethrough text indicating a sale. - offer->state = StoreState_t::NORMAL; - } else if (boost::iequals(offerstate, "limitedtime")) { - offer->state = StoreState_t::LIMITED_TIME; + childNodeName = childNode.attribute("coinType"); + if (childNodeName) { + auto parseCoin = CoinTypeMap.find(childNodeName.value()); + if (parseCoin != CoinTypeMap.end()) { + offer.coinType = parseCoin->second; + } } - if (!offer->name.length() || !offer->price) { - printXMLError( - "Error parsing XML - " - "One or more required offer params are missing - " - "GameStore::loadFromXml", - "data/XML/gamestore.xml", result); - return false; + childNodeName = childNode.attribute("buyType"); + if (childNodeName) { + auto parsebtpe = OfferBuyTypesMap.find(childNodeName.value()); + if (parsebtpe != OfferBuyTypesMap.end()) { + offer.buyType = parsebtpe->second; + } } - offerCount++; - offer->id = offerCount; - cat->offers.push_back(offer); + + offer.rookgaard = offers.rookgaard; + childNodeName = childNode.attribute("rookgaard"); + if (childNodeName) { + offer.rookgaard = childNodeName.as_bool(); + } + } + } + } + } + + return true; +} + +bool GameStore::reload() { + categories.clear(); + storeOffers.clear(); + home.offers.clear(); + home.banners.clear(); + runningid = beginid; + loaded = false; + offercount = 0; + + newoffer = false; + saleoffer = false; + + return loadFromXml(true); +} + +std::vector GameStore::getStoreOffers() +{ + std::vector filter; + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + filter.push_back(offers); + } + + return filter; +} + +std::vector GameStore::getStoreOffer(StoreOffers* offers) +{ + std::vector filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + filter.push_back(offer); + } + } + return filter; +} + +StoreOffer* GameStore::getStoreOfferByName(std::string name) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + for (auto& info2 : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info2.first); + if (offer && strcasecmp(offer->getName().c_str(), name.c_str()) == 0) { + return offer; + } + } + } + return nullptr; +} + +std::vector GameStore::getHomeOffers() +{ + std::vector filter; + + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { + StoreOffer* oferta = getStoreOfferByName((*off)); + if (oferta) { + bool hasDec = false; + for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; ++off2) { + if ((*off2)->getName() == oferta->getName()){ + hasDec = true; + break; } } - cat->offers.shrink_to_fit(); - storeCategoryOffers.push_back(cat); + + if (!hasDec) { + filter.emplace_back(oferta); + } } - storeCategoryOffers.shrink_to_fit(); - loaded = true; - return true; } + + return filter; } -int8_t GameStore::getCategoryIndexByName(std::string categoryName) + +std::map> GameStore::getHomeOffersOrganized() { - for (uint16_t i = 0; i < storeCategoryOffers.size(); i++) { - if (boost::iequals(storeCategoryOffers.at(i)->name, categoryName)) { - return i; + std::map> filter; + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { + StoreOffer* oferta = getStoreOfferByName((*off)); + if (oferta) { + std::string name = oferta->getName(); + filter[name].emplace_back(oferta); } } - return -1; + + return filter; } -bool GameStore::haveCategoryByState(StoreState_t state) +std::map> GameStore::getStoreOrganizedByName(StoreOffers* offers) { - for (auto category : storeCategoryOffers) { - if (category->state == state) { - return true; + std::map> filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + std::string name = offer->getName(); + filter[name].emplace_back(offer); } } - return false; + + return filter; } -uint16_t GameStore::getOffersCount() +StoreOffer* StoreOffers::getOfferByID(uint32_t id) { - uint16_t count = 0; - for(auto category:storeCategoryOffers) { - count+= category->offers.size(); + auto it = offers.find(id); + if (it == offers.end()) { + return nullptr; } - return count; + return &it->second; } -const BaseOffer *GameStore::getOfferByOfferId(uint32_t offerId) +std::string StoreOffer::getDisabledReason(Player* player) { - for(StoreCategory* category : storeCategoryOffers) { - for (BaseOffer *offer : category->offers) { - if (offer->id == offerId) { - return offer; + + uint16_t outfitLookType = player->getSex() == PLAYERSEX_FEMALE ? female : male; + + std::string disabledReason; + if (disabled) { + disabledReason = "This offer is disabled."; + } else if (type == OFFER_TYPE_POUCH) { + Item* item = g_game.findItemOfType(player, 26377, true, -1); + if (item) + disabledReason = "You already have Loot Pouch."; + } else if (type == OFFER_TYPE_BLESSINGS) { + if (player->hasBlessing(blessid)) + disabledReason = "You already have this Bless."; + } else if (type == OFFER_TYPE_ALLBLESSINGS) { + uint8_t count = 0; + uint8_t limitBless = 0; + uint8_t minBless = (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : BLESS_FIRST); + uint8_t maxBless = BLESS_LAST; + for (int i = minBless; i <= maxBless; ++i) { + limitBless++; + if (player->hasBlessing(i)) { + count++; + } + } + + if (count >= limitBless) + disabledReason = "You already have all Blessings."; + + } else if (type == OFFER_TYPE_OUTFIT && player->canWear(outfitLookType, addon)) { + disabledReason = "You already have this outfit."; + } else if (type == OFFER_TYPE_OUTFIT_ADDON) { + if (player->canWear(outfitLookType, 0)) { + if (player->canWear(outfitLookType, addon)) { + disabledReason = "You already have this addon."; } } + } else if (type == OFFER_TYPE_MOUNT) { + Mount* mount = g_game.mounts.getMountByID(id); + if (!mount) { + disabledReason = "Mount not found"; + } else if (player->hasMount(mount)) { + disabledReason = "You already have this mount."; + } + + } else if (type == OFFER_TYPE_PROMOTION) { + disabledReason = "This offer has disabled."; + } else if (type == OFFER_TYPE_PREYSLOT) { + //if (player->isUnlockedPrey(2)) { + disabledReason = "You already have 3 slots released."; + //} + + } else if (type == OFFER_TYPE_EXPBOOST) { + int32_t value1; + player->getStorageValue(51052, value1); + int32_t value2; + player->getStorageValue(51053, value2); + if (value1 >= 6) { + disabledReason = "Can be purchased up to 5 times between 2 server saves."; + } else if ((OS_TIME(nullptr) - value2) < (1*60*60)) { + disabledReason = "You still have active boost."; + } + } else if (type == OFFER_TYPE_CHARM_EXPANSION) { + if (player->hasCharmExpansion()) { + disabledReason = "You have charm expansion"; + } + } else if (type == OFFER_TYPE_SKULL_REMOVE) { + if (player->getSkull() != skull) { + disabledReason = "This offer is disabled for you"; + } + } else if (type == OFFER_TYPE_FRAG_REMOVE) { + if (player->unjustifiedKills.empty()) { + disabledReason = "You have no frag to remove."; + } + } else if (type == OFFER_TYPE_RECOVERYKEY) { + int32_t value; + player->getAccountStorageValue(1, value); + if (value > OS_TIME(nullptr)) { + disabledReason = "You recently generated an RK."; + } + } + + if (player->getVocation()->getId() == 0 && !rookgaard) { + disabledReason = "This offer is deactivated."; + } + + if (player->getCoinBalance(coinType) - getPrice(player) < 0) { + if (coinType == COIN_TYPE_TOURNAMENT) { + disabledReason = "You don't have tournament coins."; + } else { + disabledReason = "You don't have coins."; + } + } + + return disabledReason; +} + +uint8_t GameStore::convertType(OfferTypes_t type) +{ + uint8_t offertype = 0; + if (type == OFFER_TYPE_POUCH || type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE || type == OFFER_TYPE_HOUSE || type == OFFER_TYPE_TRAINING) { + offertype = 3; + } else if (type == OFFER_TYPE_OUTFIT || type == OFFER_TYPE_OUTFIT_ADDON) { + offertype = 2; + } else if (type == OFFER_TYPE_MOUNT) { + offertype = 1; + } + + return offertype; +} + +StoreOffers* GameStore::getOfferByName(std::string name) +{ + // checa primeiro as categorias + for (auto offer = categories.begin(), end = categories.end(); offer != end; ++offer) { + if (strcasecmp((*offer).name.c_str(), name.c_str()) == 0) { + return getOfferByName((*offer).subcategory[0]); + } + } + + // pula para ofertas + auto it = storeOffers.find(name); + if (it == storeOffers.end()) { + // Clicando no banner tbm chama uma oferta + // std::cout << "[Warning - GameStore::getOfferByName] Offer " << name << " not found" << std::endl; + return nullptr; + } + return &it->second; +} + +StoreOffers* GameStore::getOffersByOfferId(uint32_t id) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr){ + continue; + } + + StoreOffer* offer = offers->getOfferByID(id); + if (offer != nullptr && offer->getId() == id) { + return offers; + } + } + + return nullptr; +} + +StoreOffer* GameStore::getOfferById(uint32_t id) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr){ + continue; + } + + StoreOffer* offer = offers->getOfferByID(id); + if (offer != nullptr && offer->getId() == id) { + return offer; + } } return nullptr; } -HistoryStoreOfferList IOGameStore::getHistoryEntries(uint32_t account_id, uint32_t page) +uint32_t StoreOffer::getPrice(Player* player) { - HistoryStoreOfferList historyStoreOfferList; + uint32_t newPrice = 0; + if (player && type == OFFER_TYPE_EXPBOOST) { + int32_t value1; + player->getStorageValue(51052, value1); + uint32_t xpBoostPrice = getExpBoostPrice(value1); + if (xpBoostPrice > 0) { + if (player->isPremium()) { + xpBoostPrice *= 0.90; + } + + } + + return xpBoostPrice; + } + + if (state == OFFER_STATE_SALE) { + time_t mytime; + mytime = time(NULL); + struct tm tm = *localtime(&mytime); + int32_t daySub = validUntil - tm.tm_mday; + if (daySub < 0) { + newPrice = basePrice; + } + } + + uint32_t p_prince = price; + if (player && player->isPremium()) { + newPrice *= 0.90; + p_prince *= 0.90; + } - std::ostringstream query; + return newPrice > 0 ? newPrice : p_prince; +} - query << "SELECT `description`,`mode`,`coin_amount`,`time` FROM `store_history` WHERE `account_id` = " <((page-1), 0)*GameStore::HISTORY_ENTRIES_PER_PAGE) - << "," << GameStore::HISTORY_ENTRIES_PER_PAGE <<";"; - DBResult_ptr result = Database::getInstance().storeQuery(query.str()); +uint16_t StoreOffer::getCount(bool inBuy) +{ + if (!inBuy && type == OFFER_TYPE_PREMIUM) { + return 1; + } + + return count; +} - if (result) { - do { - HistoryStoreOffer entry; +std::string StoreOffer::getDescription(Player* player /*= nullptr */) +{ + if (!player) { + return description; + } - entry.description = result->getString("description"); - entry.mode = result->getNumber("mode"); - entry.amount = result->getNumber("coin_amount"); - entry.time = result->getNumber("time"); + uint16_t version = player->getProtocolVersion(); + std::string showDesc = version < 1200 ? description : description12; + if (showDesc.empty() && version >= 1200 ) { + showDesc = description; + } - historyStoreOfferList.push_back(entry); - } while (result->next()); + if ((type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE) && showDesc.empty()) { + Item* virtualItem = Item::CreateItem(itemtype, count); + if (virtualItem) { + showDesc = "You see " + virtualItem->getDescription(version < 1200 ? -1 : -2); + delete virtualItem; + } } - return historyStoreOfferList; + return showDesc; } diff --git a/src/game/gamestore.h b/src/game/gamestore.h deleted file mode 100644 index f5c56f012c1..00000000000 --- a/src/game/gamestore.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Canary - A free and open-source MMORPG server emulator - * Copyright (C) 2021 OpenTibiaBR - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef SRC_GAME_GAMESTORE_H_ -#define SRC_GAME_GAMESTORE_H_ - -#include "declarations.hpp" - -class GameStore { - public: - static uint16_t HISTORY_ENTRIES_PER_PAGE; - static void startup() { - HISTORY_ENTRIES_PER_PAGE=16; - } - - bool isLoaded() { - return loaded; - } - - bool reload(); - bool loadFromXml(); - uint16_t getOffersCount(); - - uint16_t getCategoryCount() { - return (uint16_t) storeCategoryOffers.size(); - } - - std::vector getCategoryOffers() { - return storeCategoryOffers; - }; - - int8_t getCategoryIndexByName(std::string categoryName); - bool haveCategoryByState(StoreState_t state); - const BaseOffer* getOfferByOfferId(uint32_t offerId); - - private: - uint32_t offerCount=0; - bool loaded=false; - std::vector storeCategoryOffers; -}; - -class IOGameStore { - public: - static HistoryStoreOfferList getHistoryEntries(uint32_t account_id, uint32_t page); -}; - -#endif // SRC_GAME_GAMESTORE_H_ diff --git a/src/game/gamestore.hpp b/src/game/gamestore.hpp new file mode 100644 index 00000000000..fdee94499f2 --- /dev/null +++ b/src/game/gamestore.hpp @@ -0,0 +1,353 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (C) 2021 OpenTibiaBR + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef C40D8054_B234_4A7F_9ACA_13C27DBFBB09 +#define C40D8054_B234_4A7F_9ACA_13C27DBFBB09 + +#ifndef SRC_GAME_GAMESTORE_H_ +#define SRC_GAME_GAMESTORE_H_ + +#include + +#include "declarations.hpp" + +#include "creatures/players/player.h" +#include "utils/tools.h" + +class Player; +class Item; +class Mounts; + +class StoreOffers; +class StoreOffer; + +enum OfferTypes_t : uint8_t { + OFFER_TYPE_NONE = 0, // (this will disable offer) + OFFER_TYPE_ITEM = 1, + OFFER_TYPE_STACKABLE = 2, + OFFER_TYPE_OUTFIT = 3, + OFFER_TYPE_OUTFIT_ADDON = 4, + OFFER_TYPE_MOUNT = 5, + OFFER_TYPE_NAMECHANGE = 6, + OFFER_TYPE_SEXCHANGE = 7, + OFFER_TYPE_PROMOTION = 8, + OFFER_TYPE_HOUSE = 9, + OFFER_TYPE_EXPBOOST = 10, + OFFER_TYPE_PREYSLOT = 11, + OFFER_TYPE_PREYBONUS = 12, + OFFER_TYPE_TEMPLE = 13, + OFFER_TYPE_BLESSINGS = 14, + OFFER_TYPE_PREMIUM = 15, + OFFER_TYPE_POUCH = 16, + OFFER_TYPE_ALLBLESSINGS = 17, + OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, + OFFER_TYPE_TRAINING = 19, + OFFER_TYPE_CHARM_EXPANSION = 20, + OFFER_TYPE_CHARM_POINTS = 21, + OFFER_TYPE_MULTI_ITEMS = 22, + OFFER_TYPE_VIP = 24, + OFFER_TYPE_FRAG_REMOVE = 25, + OFFER_TYPE_SKULL_REMOVE = 26, + OFFER_TYPE_RECOVERYKEY = 27, +}; + +enum OfferBuyTypes_t : uint8_t { + OFFER_BUY_TYPE_OTHERS = 0, + OFFER_BUY_TYPE_NAMECHANGE = 1, + OFFER_BUY_TYPE_TESTE = 3, +}; + +enum ClientOfferTypes_t { + CLIENT_STORE_OFFER_OTHER = 0, + CLIENT_STORE_OFFER_NAMECHANGE = 1 +}; + +enum OfferStates_t { + OFFER_STATE_NONE = 0, + OFFER_STATE_NEW = 1, + OFFER_STATE_SALE = 2, + OFFER_STATE_TIMED = 3 +}; + +enum StoreErrors_t { + STORE_ERROR_PURCHASE = 0, + STORE_ERROR_NETWORK = 1, + STORE_ERROR_HISTORY = 2, + STORE_ERROR_TRANSFER = 3, + STORE_ERROR_INFORMATION = 4 +}; + +enum StoreServiceTypes_t { + STORE_SERVICE_STANDERD = 0, + STORE_SERVICE_OUTFITS = 3, + STORE_SERVICE_MOUNTS = 4, + STORE_SERVICE_BLESSINGS = 5 +}; + +enum StoreHistoryTypes_t { + HISTORY_TYPE_NONE = 0, + HISTORY_TYPE_GIFT = 1, + HISTORY_TYPE_REFUND = 2 +}; + +struct StoreCategory { + StoreCategory(std::string name, std::vector subcategory, std::string icon, bool rookgaard) : + name(std::move(name)), subcategory(std::move(subcategory)), icon(std::move(icon)), rookgaard(rookgaard) {} + + std::string name; + std::vector subcategory; + std::string icon; + bool rookgaard; +}; + +struct StoreHome { + std::vector offers; + std::vector banners; +}; + +class GameStore { + public: + bool loadFromXml(bool reloading = false); + bool reload(); + + bool hasNewOffer() { + return newoffer; + } + + bool hasSaleOffer() { + return saleoffer; + } + + uint16_t getOfferCount() { + return offercount; + } + + bool isValidType(OfferTypes_t type); + + std::vector getStoreOffers(); + std::vector getStoreCategories() { + return categories; + } + StoreHome getStoreHome() { + return home; + } + + std::map> getStoreOrganizedByName(StoreOffers* offer); + std::map> getHomeOffersOrganized(); + std::vector getStoreOffer(StoreOffers* offer); + + std::vector getHomeOffers(); + const std::vector& getHomeBanners() const { + return home.banners; + } + + uint8_t convertType(OfferTypes_t type); + StoreOffers* getOfferByName(std::string name); + StoreOffers* getOffersByOfferId(uint32_t id); + StoreOffer* getStoreOfferByName(std::string name); + StoreOffer* getOfferById(uint32_t id); + protected: + friend class StoreOffers; + friend class StoreOffer; + + std::vector categories; + std::map storeOffers; + StoreHome home; + + bool loaded = false; + private: + // Como mount usa como base uint16, podemos setar os ids das ofertas (que nao foram identificada) como o valor maximo do uint16 + 1 + uint16_t beginid = std::numeric_limits::max(); + uint32_t runningid = beginid; + uint16_t offercount = 0; + + bool newoffer = false; + bool saleoffer = false; + +}; + +class StoreOffers { + public: + StoreOffers(std::string name) : + name(std::move(name)) {} + + std::string getName() { + return name; + } + std::string getDescription() { + return description; + } + std::string getIcon() { + return icon; + } + std::string getParent() { + return parent; + } + + bool canUseRookgaard() { + return rookgaard; + } + + OfferStates_t getOfferState() { + return state; + } + + StoreOffer* getOfferByID(uint32_t id); + protected: + friend class GameStore; + friend class StoreOffer; + + std::map offers; + + private: + std::string name; + std::string icon = ""; + std::string description = ""; + std::string parent; + bool rookgaard = false; + OfferStates_t state = OFFER_STATE_NONE; + +}; + +class StoreOffer { + public: + StoreOffer(uint32_t _id, std::string _name) : + id(_id), name(std::move( _name)) {} + + std::string getDisabledReason(Player* player); + + std::string getName() { + return name; + } + std::string getDescription(Player* player = nullptr); + std::string getIcon() { + return icon; + } + + uint32_t getId() { + return id; + } + uint32_t getPrice(Player* player = nullptr); + uint32_t getBasePrice() { + return basePrice; + } + uint32_t getValidUntil() { + return validUntil; + } + uint16_t getCount(bool inBuy = false); + + uint16_t getBlessid() { + return blessid; + } + uint16_t getItemType() { + return itemtype; + } + uint16_t getCharges() { + return charges; + } + uint16_t getActionID() { + return actionid; + } + uint8_t getAddon() { + return addon; + } + uint16_t getOutfitMale() { + return male; + } + uint16_t getOutfitFemale() { + return female; + } + + const std::map& getItems() const { + return itemList; + } + + OfferStates_t getOfferState() { + return state; + } + CoinType_t getCoinType() { + return coinType; + } + OfferTypes_t getOfferType() { + return type; + } + OfferBuyTypes_t getOfferBuyType() { + return buyType; + } + + Skulls_t getSkull() { + return skull; + } + + uint32_t getExpBoostPrice(int32_t value) { + if(value == 1) + return 30; + else if (value == 2) + return 45; + else if (value == 3) + return 90; + else if (value == 4) + return 180; + else if (value == 5) + return 360; + else + return 30; + + } + + bool haveOfferRookgaard() { + return rookgaard; + } + + protected: + friend class GameStore; + friend class StoreOffers; + + private: + uint32_t id = 0; + std::string name = ""; + + std::map itemList; + std::string description = ""; + std::string description12; + std::string icon = ""; + OfferStates_t state = OFFER_STATE_NONE; + CoinType_t coinType = COIN_TYPE_DEFAULT; + OfferBuyTypes_t buyType = OFFER_BUY_TYPE_OTHERS; + uint16_t count = 1; + uint32_t price = 150; // default price -- evitando que entre oferta sem valor + uint32_t basePrice = 0; // default price -- evitando que entre oferta sem valor + uint32_t validUntil = 0; + uint16_t blessid = 0; + uint16_t itemtype = 0; + uint16_t charges = 1; + uint8_t addon = 0; + uint16_t male; + uint16_t female; + uint16_t actionid = 0; + Skulls_t skull = SKULL_NONE; + + bool disabled = false; + bool rookgaard = true; + OfferTypes_t type = OFFER_TYPE_NONE; +}; + +#endif + +#endif diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 876696b3466..7ea9ac9e3bc 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -281,7 +281,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) acc.GetPremiumRemaningDays(&(player->premiumDays)); } - acc.GetCoins(&(player->coinBalance)); + acc.GetCoins(COIN_TYPE_DEFAULT); player->preyBonusRerolls = result->getNumber("bonus_rerolls"); diff --git a/src/lua/functions/core/game/config_functions.hpp b/src/lua/functions/core/game/config_functions.hpp index b2ada4e78a0..409fc200e7a 100644 --- a/src/lua/functions/core/game/config_functions.hpp +++ b/src/lua/functions/core/game/config_functions.hpp @@ -146,6 +146,7 @@ class ConfigFunctions final : LuaScriptInterface { registerEnumIn(L, "configKeys", PUSH_WHEN_ATTACKING) + registerEnumIn(L, "configKeys", TIME_GMT) #undef registerEnumIn } diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index bf950cf2333..f82b8279f60 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2296,9 +2296,9 @@ int PlayerFunctions::luaPlayerGetTibiaCoins(lua_State* L) { account::Account account(player->getAccount()); account.LoadAccountDB(); uint32_t coins; - account.GetCoins(&coins); + account.GetCoins(COIN_TYPE_DEFAULT); lua_pushnumber(L, coins); - } else { + } else { lua_pushnil(L); } return 1; @@ -2312,16 +2312,16 @@ int PlayerFunctions::luaPlayerAddTibiaCoins(lua_State* L) { return 1; } - uint32_t coins = getNumber(L, 2); + uint32_t coins = getNumber(L, 2); - account::Account account(player->getAccount()); - account.LoadAccountDB(); - if(account.AddCoins(coins)) { - account.GetCoins(&(player->coinBalance)); - pushBoolean(L, true); - } else { - lua_pushnil(L); - } + account::Account account(player->getAccount()); + account.LoadAccountDB(); + if(account.AddCoins(coins)) { + account.GetCoins(COIN_TYPE_DEFAULT); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } return 1; } @@ -2334,16 +2334,16 @@ int PlayerFunctions::luaPlayerRemoveTibiaCoins(lua_State* L) { return 1; } - uint32_t coins = getNumber(L, 2); + uint32_t coins = getNumber(L, 2); - account::Account account(player->getAccount()); - account.LoadAccountDB(); - if (account.RemoveCoins(coins)) { - account.GetCoins(&(player->coinBalance)); - pushBoolean(L, true); - } else { - lua_pushnil(L); - } + account::Account account(player->getAccount()); + account.LoadAccountDB(); + if (account.RemoveCoins(coins)) { + account.GetCoins(COIN_TYPE_DEFAULT); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } return 1; } diff --git a/src/otserv.cpp b/src/otserv.cpp index d9cff5dff07..80b4f9154c4 100644 --- a/src/otserv.cpp +++ b/src/otserv.cpp @@ -31,6 +31,7 @@ #include "database/databasemanager.h" #include "database/databasetasks.h" #include "game/game.h" +#include "game/gamestore.hpp" #include "game/scheduling/scheduler.h" #include "io/iomarket.h" #include "lua/creature/events.h" @@ -54,6 +55,7 @@ Scheduler g_scheduler; Game g_game; ConfigManager g_config; extern Events* g_events; +GameStore g_gameStore; extern Imbuements* g_imbuements; extern LuaEnvironment g_luaEnvironment; extern Modules* g_modules; @@ -178,6 +180,8 @@ void loadModules() { "data/XML/outfits.xml"); modulesLoadHelper(Familiars::getInstance().loadFromXml(), "data/XML/familiars.xml"); + modulesLoadHelper(g_gameStore.loadFromXml(), + "data/XML/gamestore.xml"); modulesLoadHelper(g_imbuements->loadFromXml(), "data/XML/imbuements.xml"); modulesLoadHelper(g_modules->loadFromXml(), diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 65d301975be..add42529c5b 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -639,7 +639,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt } if (!player || player->isRemoved() || player->getHealth() <= 0) { - return; + return; } switch (recvbyte) { @@ -738,7 +738,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xE7: /* thank you */ break; case 0xE8: parseDebugAssert(msg); break; case 0xEE: parseGreet(msg); break; - case 0xEF: if (!g_config.getBoolean(STOREMODULES)) { parseCoinTransfer(msg); } break; /* premium coins transfer */ + case 0xEF: parseCoinTransfer(msg); break; case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; case 0xF1: parseQuestLine(msg); break; // case 0xF2: parseRuleViolationReport(msg); break; @@ -749,11 +749,6 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xF7: parseMarketCancelOffer(msg); break; case 0xF8: parseMarketAcceptOffer(msg); break; case 0xF9: parseModalWindowAnswer(msg); break; - case 0xFA: if (!g_config.getBoolean(STOREMODULES)) { parseStoreOpen(msg); } break; - case 0xFB: if (!g_config.getBoolean(STOREMODULES)) { parseStoreRequestOffers(msg); } break; - case 0xFC: if (!g_config.getBoolean(STOREMODULES)) { parseStoreBuyOffer(msg); } break; -// case 0xFD: parseStoreOpenTransactionHistory(msg); break; -// case 0xFE: parseStoreRequestTransactionHistory(msg); break; //case 0xDF, 0xE0, 0xE1, 0xFB, 0xFC, 0xFD, 0xFE Premium Shop. @@ -2494,77 +2489,14 @@ void ProtocolGame::parseMarketBrowse(NetworkMessage &msg) } else { - player->sendMarketEnter(player->getLastDepotId()); + player->sendMarketEnter(player->getLastDepotId()); addGameTask(&Game::playerBrowseMarket, player->getID(), browseId); } } -void ProtocolGame::parseStoreOpen(NetworkMessage &msg) -{ - uint8_t serviceType = msg.getByte(); - addGameTaskTimed(600, &Game::playerStoreOpen, player->getID(), serviceType); -} - -void ProtocolGame::parseStoreRequestOffers(NetworkMessage &message) -{ - //StoreService_t serviceType = SERVICE_STANDARD; - message.getByte(); // discard service type byte // version >= 1092 - - std::string categoryName = message.getString(); - const int16_t index = g_game.gameStore.getCategoryIndexByName(categoryName); - - if (index >= 0) - { - addGameTaskTimed(350, &Game::playerShowStoreCategoryOffers, player->getID(), - g_game.gameStore.getCategoryOffers().at(index)); - } - else - { - SPDLOG_WARN("[ProtocolGame::parseStoreRequestOffers] - " - "Requested category: {} doesn't exists", categoryName); - } -} - -void ProtocolGame::parseStoreBuyOffer(NetworkMessage &message) -{ - uint32_t offerId = message.get(); - uint8_t productType = message.getByte(); //used only in return of a namechange offer request - std::string additionalInfo; - if (productType == ADDITIONALINFO) - { - additionalInfo = message.getString(); - } - addGameTaskTimed(350, &Game::playerBuyStoreOffer, player->getID(), offerId, productType, additionalInfo); -} - -void ProtocolGame::parseStoreOpenTransactionHistory(NetworkMessage &msg) -{ - uint8_t entriesPerPage = msg.getByte(); - if (entriesPerPage > 0 && entriesPerPage != GameStore::HISTORY_ENTRIES_PER_PAGE) - { - GameStore::HISTORY_ENTRIES_PER_PAGE = entriesPerPage; - } - - addGameTaskTimed(2000, &Game::playerStoreTransactionHistory, player->getID(), 1); -} - -void ProtocolGame::parseStoreRequestTransactionHistory(NetworkMessage &msg) -{ - uint32_t pageNumber = msg.get(); - addGameTaskTimed(2000, &Game::playerStoreTransactionHistory, player->getID(), pageNumber); -} - -void ProtocolGame::parseCoinTransfer(NetworkMessage &msg) -{ - std::string receiverName = msg.getString(); - uint32_t amount = msg.get(); - - if (amount > 0) - { - addGameTaskTimed(350, &Game::playerCoinTransfer, player->getID(), receiverName, amount); - } - - updateCoinBalance(); +void ProtocolGame::parseCoinTransfer(NetworkMessage& msg) { + std::string recipient = msg.getString(); + uint16_t amount = msg.get(); } void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) @@ -3430,17 +3362,6 @@ void ProtocolGame::sendBlessStatus() writeToOutputBuffer(msg); } -void ProtocolGame::sendStoreHighlight() -{ - NetworkMessage msg; - bool haveSale = g_game.gameStore.haveCategoryByState(StoreState_t::SALE); - bool haveNewItem = g_game.gameStore.haveCategoryByState(StoreState_t::NEW); - msg.addByte(0x19); - msg.addByte((haveSale) ? 1 : 0); - msg.addByte((haveNewItem) ? 1 : 0); - writeToOutputBuffer(msg); -} - void ProtocolGame::sendPremiumTrigger() { if (!g_config.getBoolean(FREE_PREMIUM)) @@ -4024,7 +3945,7 @@ void ProtocolGame::updateCoinBalance() if (!player) { return; } - + g_dispatcher.addTask( createTask(std::bind([](uint32_t playerId) { Player* threadPlayer = g_game.getPlayerByID(playerId); @@ -5193,7 +5114,6 @@ void ProtocolGame::sendAddCreature(const Creature *creature, const Position &pos sendBlessStatus(); sendPremiumTrigger(); - sendStoreHighlight(); sendItemsPrice(); @@ -5791,219 +5711,6 @@ void ProtocolGame::sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time) writeToOutputBuffer(msg); } -void ProtocolGame::sendOpenStore(uint8_t) -{ - NetworkMessage msg; - - msg.addByte(0xFB); //open store - msg.addByte(0x00); - - //add categories - uint16_t categoriesCount = g_game.gameStore.getCategoryOffers().size(); - - msg.add(categoriesCount); - - for (StoreCategory *category : g_game.gameStore.getCategoryOffers()) - { - msg.addString(category->name); - msg.addString(category->description); - - uint8_t stateByte; - switch (category->state) - { - case NORMAL: - stateByte = 0; - break; - case NEW: - stateByte = 1; - break; - case SALE: - stateByte = 2; - break; - case LIMITED_TIME: - stateByte = 3; - break; - default: - stateByte = 0; - break; - } - msg.addByte(stateByte); - - msg.addByte((uint8_t)category->icons.size()); - for (std::string iconStr : category->icons) - { - msg.addString(iconStr); - } - msg.addString(""); //TODO: parentCategory - } - - writeToOutputBuffer(msg); -} - -void ProtocolGame::sendStoreCategoryOffers(StoreCategory *category) -{ - NetworkMessage msg; - msg.addByte(0xFC); //StoreOffers - msg.addString(category->name); - msg.add(category->offers.size()); - - for (BaseOffer *offer : category->offers) - { - msg.add(offer->id); - std::stringstream offername; - if (offer->type == Offer_t::ITEM || offer->type == Offer_t::STACKABLE_ITEM) - { - if (((ItemOffer *)offer)->count > 1) - { - offername << ((ItemOffer *)offer)->count << "x "; - } - } - offername << offer->name; - - msg.addString(offername.str()); - msg.addString(offer->description); - - msg.add(offer->price); - msg.addByte((uint8_t)offer->state); - - //outfits - uint8_t disabled = 0; - std::stringstream disabledReason; - - disabledReason << ""; - - if (offer->type == OUTFIT || offer->type == OUTFIT_ADDON) - { - OutfitOffer *outfitOffer = (OutfitOffer *)offer; - - uint16_t looktype = (player->getSex() == PLAYERSEX_MALE) ? outfitOffer->maleLookType : outfitOffer->femaleLookType; - uint8_t addons = outfitOffer->addonNumber; - - if (player->canWear(looktype, addons)) - { //player can wear the offer already - disabled = 1; - if (addons == 0) - { //addons == 0 //oufit-only offer and player already has it - disabledReason << "You already have this outfit."; - } - else - { - disabledReason << "You already have this outfit/addon."; - } - } - else - { - if (outfitOffer->type == OUTFIT_ADDON && !player->canWear(looktype, 0)) - { //addon offer and player doesnt have the base outfit - disabled = 1; - disabledReason << "You don't have the outfit, you can't buy the addon."; - } - } - } - else if (offer->type == MOUNT) - { - MountOffer *mountOffer = (MountOffer *)offer; - Mount *m = g_game.mounts.getMountByID(mountOffer->mountId); - if (player->hasMount(m)) - { - disabled = 1; - disabledReason << "You already have this mount."; - } - } - else if (offer->type == PROMOTION) - { - if (player->isPromoted() || !player->isPremium()) - { //TODO: add support to multiple promotion levels - disabled = 1; - disabledReason << "You can't get this promotion"; - } - } - - msg.addByte(disabled); - - if (disabled) - { - msg.addString(disabledReason.str()); - } - - //add icons - msg.addByte((uint8_t)offer->icons.size()); - - for (std::string iconName : offer->icons) - { - msg.addString(iconName); - } - - msg.add(0); - //TODO: add support to suboffers - } - - writeToOutputBuffer(msg); -} - -void ProtocolGame::sendStoreError(GameStoreError_t error, const std::string &message) -{ - NetworkMessage msg; - - msg.addByte(0xE0); //storeError - msg.addByte(error); - msg.addString(message); - - writeToOutputBuffer(msg); -} - -void ProtocolGame::sendStorePurchaseSuccessful(const std::string &message, const uint32_t coinBalance) -{ - NetworkMessage msg; - - msg.addByte(0xFE); //CompletePurchase - msg.addByte(0x00); - - msg.addString(message); - msg.add(coinBalance); //dont know why the client needs it duplicated. But ok... - msg.add(coinBalance); - - writeToOutputBuffer(msg); -} - -void ProtocolGame::sendStoreRequestAdditionalInfo(uint32_t offerId, ClientOffer_t clientOfferType) -{ - NetworkMessage msg; - - msg.addByte(0xE1); //RequestPurchaseData - msg.add(offerId); - msg.addByte(clientOfferType); - - writeToOutputBuffer(msg); -} - -void ProtocolGame::sendStoreTrasactionHistory(HistoryStoreOfferList &list, uint32_t page, uint8_t entriesPerPage) -{ - NetworkMessage msg; - uint32_t isLastPage = (list.size() <= entriesPerPage) ? 0x01 : 0x00; - - //TODO: Support multiple pages - isLastPage = 0x01; //FIXME - page = 0x00; - //////////////////////// - - msg.addByte(0xFD); //BrowseTransactionHistory - msg.add(page); //which page - msg.add(isLastPage); //is the last page? / - msg.addByte((uint8_t)list.size()); //how many elements follows - - for (HistoryStoreOffer offer : list) - { - msg.add(offer.time); - msg.addByte(offer.mode); - msg.add(offer.amount); //FIXME: investigate why it doesn't send the price properly - msg.addByte(0x00); // 0 = transferable tibia coin, 1 = normal tibia coin - msg.addString(offer.description); - } - - writeToOutputBuffer(msg); -} - void ProtocolGame::sendModalWindow(const ModalWindow &modalWindow) { NetworkMessage msg; diff --git a/src/server/network/protocol/protocolgame.h b/src/server/network/protocol/protocolgame.h index da2c7196699..99d0577f259 100644 --- a/src/server/network/protocol/protocolgame.h +++ b/src/server/network/protocol/protocolgame.h @@ -27,9 +27,6 @@ #include "config/configmanager.h" #include "creatures/creature.h" #include "game/scheduling/tasks.h" -#include "game/gamestore.h" - - class NetworkMessage; class Player; @@ -222,10 +219,6 @@ class ProtocolGame final : public Protocol void parseOpenPrivateChannel(NetworkMessage &msg); void parseCloseChannel(NetworkMessage &msg); - //Store methods - void parseStoreOpen(NetworkMessage &message); - void parseStoreRequestOffers(NetworkMessage &message); - void parseStoreBuyOffer(NetworkMessage &message); void parseCoinTransfer(NetworkMessage &msg); // imbue info @@ -280,7 +273,6 @@ class ProtocolGame final : public Protocol void sendCreatureOutfit(const Creature *creature, const Outfit_t &outfit); void sendStats(); void sendBasicData(); - void sendStoreHighlight(); void sendTextMessage(const TextMessage &message); void sendReLoginWindow(uint8_t unfairFightReduction); @@ -353,15 +345,6 @@ class ProtocolGame final : public Protocol void sendCoinBalance(); - void sendOpenStore(uint8_t serviceType); - void sendStoreCategoryOffers(StoreCategory *category); - void sendStoreError(GameStoreError_t error, const std::string &message); - void sendStorePurchaseSuccessful(const std::string &message, const uint32_t coinBalance); - void sendStoreRequestAdditionalInfo(uint32_t offerId, ClientOffer_t clientOfferType); - void sendStoreTrasactionHistory(HistoryStoreOfferList &list, uint32_t page, uint8_t entriesPerPage); - void parseStoreOpenTransactionHistory(NetworkMessage &msg); - void parseStoreRequestTransactionHistory(NetworkMessage &msg); - //tiles void sendMapDescription(const Position &pos); diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index 7614e3b591e..eff126b2dc9 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1324,9 +1324,18 @@ const char* getReturnMessage(ReturnValue value) } } -int64_t OTSYS_TIME() +int64_t OTSYS_TIME(bool useTime) { - return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + if (useTime) { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + int64_t time = (std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + (g_config.getNumber(TIME_GMT) * 1000); + return time; +} + +int32_t OS_TIME(time_t* timer) +{ + return (time(timer) + g_config.getNumber(TIME_GMT)); } SpellGroup_t stringToSpellGroup(const std::string &value) diff --git a/src/utils/tools.h b/src/utils/tools.h index 0be46a0febd..dfff22cd5fb 100644 --- a/src/utils/tools.h +++ b/src/utils/tools.h @@ -110,7 +110,8 @@ bool isCaskItem(uint16_t itemId); std::string getObjectCategoryName(ObjectCategory_t category); -int64_t OTSYS_TIME(); +int64_t OTSYS_TIME(bool useTime = false); +int32_t OS_TIME(time_t* timer); SpellGroup_t stringToSpellGroup(const std::string &value); diff --git a/vcpkg.json b/vcpkg.json deleted file mode 100644 index 39e57de5d9e..00000000000 --- a/vcpkg.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "canary", - "version-string": "1.0.0", - "dependencies": [ - "boost-asio", - "boost-lockfree", - "boost-variant", - "boost-filesystem", - "boost-iostreams", - "boost-system", - "libmariadb", - "pugixml", - "spdlog", - "curl", - "jsoncpp", - "cryptopp", - { - "name": "luajit", - "platform": "windows" - } - ] -} From 5dc0d5ddf5e8420da4a1a9d9a64c985f9d88b2f3 Mon Sep 17 00:00:00 2001 From: dudantas Date: Tue, 10 Aug 2021 00:22:13 -0300 Subject: [PATCH 02/30] Gamestore functions and bytes --- client_assertions.txt | 5 + src/config/config_definitions.hpp | 1 + src/config/configmanager.cpp | 3 +- src/creatures/players/player.cpp | 24 + src/creatures/players/player.h | 59 +- src/game/game.cpp | 658 +++++++++++++++++++ src/game/game.h | 6 + src/game/gamestore.cpp | 6 +- src/game/gamestore.hpp | 4 +- src/items/item.cpp | 15 + src/items/item.h | 2 +- src/items/items_definitions.hpp | 3 +- src/lua/functions/core/game/lua_enums.hpp | 1 + src/server/network/protocol/protocolgame.cpp | 343 ++++++++++ src/server/network/protocol/protocolgame.h | 22 + src/utils/tools.cpp | 14 + src/utils/tools.h | 2 + vcpkg.json | 22 + 18 files changed, 1183 insertions(+), 7 deletions(-) create mode 100644 client_assertions.txt create mode 100644 vcpkg.json diff --git a/client_assertions.txt b/client_assertions.txt new file mode 100644 index 00000000000..98600b05d15 --- /dev/null +++ b/client_assertions.txt @@ -0,0 +1,5 @@ +----- 10/08/2021 00:21:43 - Druid Sample (127.0.0.1) ----- + + + + diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 50857bc35b8..908cbff7250 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -91,6 +91,7 @@ enum stringConfig_t { MAP_CUSTOM_SPAWN, MAP_CUSTOM_AUTHOR, DISCORD_WEBHOOK_URL, + DEFAULT_OFFER, LAST_STRING_CONFIG }; diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 98d30c7e15c..d4f3b2e5da9 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -193,7 +193,8 @@ bool ConfigManager::load() string[MOTD] = getGlobalString(L, "motd", ""); string[WORLD_TYPE] = getGlobalString(L, "worldType", "pvp"); string[STORE_IMAGES_URL] = getGlobalString(L, "coinImagesURL", ""); - string[DISCORD_WEBHOOK_URL] = getGlobalString(L, "discordWebhookURL", ""); + string[DISCORD_WEBHOOK_URL] = getGlobalString(L, "discordWebhookURL", ""); + string[DEFAULT_OFFER] = getGlobalString(L, "defaultStoreOffer", "Blessings"); integer[MAX_PLAYERS] = getGlobalNumber(L, "maxPlayers"); integer[PZ_LOCKED] = getGlobalNumber(L, "pzLocked", 60000); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 8d5100d0be8..78d03e74ae2 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -5477,6 +5477,30 @@ void Player::stowItem(Item* item, uint32_t count, bool allItems) { stashContainer(itemDict); } +bool Player::removeFrags(uint8_t count) +{ + if (unjustifiedKills.empty()) { + return false; + } + + uint8_t passed = 0; + std::vector v_unjustifiedKills = unjustifiedKills; + for (const auto& kill : v_unjustifiedKills) { + if (passed >= count) { + break; + } + + if (kill.time > 0) { + unjustifiedKills.erase(unjustifiedKills.begin() + passed); + passed++; + } + + } + + sendUnjustifiedPoints(); + return true; +} + void Player::addAccountStorageValue(const uint32_t key, const int32_t value) { if (value != -1) { diff --git a/src/creatures/players/player.h b/src/creatures/players/player.h index 1ed36d3154b..896945302ab 100644 --- a/src/creatures/players/player.h +++ b/src/creatures/players/player.h @@ -52,6 +52,7 @@ class SchedulerTask; class Bed; class Guild; class Imbuement; +class StoreOffers; struct OpenContainer { Container* container; @@ -1076,6 +1077,58 @@ class Player final : public Creature, public Cylinder } } + // GameStore + void openStore() { + if (client) { + client->openStore(); + } + } + void updateCoinBalance() { + if (client) { + client->updateCoinBalance(); + } + } + + void sendStoreHome() { + if (client) { + client->sendStoreHome(); + } + } + void sendStoreHistory(uint32_t totalPages, uint32_t pages, std::vector filter) { + if (client) { + client->sendStoreHistory(totalPages, pages, filter); + } + } + void sendStorePurchaseSuccessful(const std::string& message) { + if (client) { + client->sendStorePurchaseSuccessful(message); + } + } + void sendStoreError(uint8_t errorType, std::string message) { + if (client) { + client->sendStoreError(errorType, message); + } + } + void sendOfferDescription(uint32_t id, std::string desc) { + if (client) { + client->sendOfferDescription(id, desc); + } + } + void sendShowStoreOffers(StoreOffers* offers) { + if (client) { + client->sendShowStoreOffers(offers); + } + } + + uint16_t getEntriesPerPage() { + return entriesPerPage; + } + void setEntriesPerPage(uint16_t entriesPage) { + entriesPerPage = entriesPage; + } + + bool removeFrags(uint8_t count = 1); + //event methods void onUpdateTileItem(const Tile* tile, const Position& pos, const Item* oldItem, const ItemType& oldType, const Item* newItem, @@ -1929,6 +1982,8 @@ class Player final : public Creature, public Cylinder uint64_t lastAttack = 0; uint64_t bankBalance = 0; uint64_t lastQuestlogUpdate = 0; + uint64_t asyncOngoingTasks = 0; + int64_t lastFailedFollow = 0; int64_t skullTicks = 0; int64_t lastWalkthroughAttempt = 0; @@ -1944,8 +1999,6 @@ class Player final : public Creature, public Cylinder uint32_t lastUpdateCoin = OTSYS_TIME(); - uint64_t asyncOngoingTasks = 0; - BedItem* bedItem = nullptr; Guild* guild = nullptr; GuildRank_ptr guildRank; @@ -1986,6 +2039,7 @@ class Player final : public Creature, public Cylinder uint32_t windowTextId = 0; uint32_t editListId = 0; uint32_t manaMax = 0; + int32_t varSkills[SKILL_LAST + 1] = {}; int32_t varStats[STAT_LAST + 1] = {}; int32_t shopCallback = -1; @@ -1998,6 +2052,7 @@ class Player final : public Creature, public Cylinder int32_t tournamentCoinBalance = 0; uint16_t expBoostStamina = 0; + uint16_t entriesPerPage = 26; uint32_t coinBalance = 0; uint32_t premiumDays = 0; diff --git a/src/game/game.cpp b/src/game/game.cpp index 870e2b06f91..e243f393891 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8331,3 +8331,661 @@ bool Game::getAccountHistory(const uint32_t accountId, std::vector history = it->second; return true; } + +void Game::playerOpenStore(uint32_t playerId, bool openStore, StoreOffers* offers) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + // Update coins + player->updateCoinBalance(); + + if (openStore) { + player->openStore(); + } else if (offers == nullptr) { + player->sendStoreHome(); + } else if (offers != nullptr) { + player->sendShowStoreOffers(offers); + } + + return; +} + +void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std::string& param) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + StoreOffer* thisOffer = const_cast(&offer); + if (!thisOffer || thisOffer == nullptr) { + player->sendStoreError(STORE_ERROR_NETWORK, "The offer is fake, please report it!"); + return; + } + + + OfferTypes_t offerType = thisOffer->getOfferType(); + if (!g_gameStore.isValidType(offerType)) { + player->sendStoreError(STORE_ERROR_INFORMATION, "This offer is unavailable."); + return; + } + + if (!player->canRemoveCoins(thisOffer->getPrice(player)) ) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have coins."); + return; + } + + player->updateCoinBalance(); + std::string message = thisOffer->getDisabledReason(player); + if (!message.empty()) { + player->sendStoreError(STORE_ERROR_PURCHASE, message); + return; + } + + if (player->isPzLocked()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't buy this offer in pz locked!"); + return; + } + + bool successfully = false; + + Tile* playerTile = player->getTile(); + + int32_t offerPrice = thisOffer->getPrice(player) * -1; + std::stringstream returnmessage; + if (offerType == OFFER_TYPE_NAMECHANGE) { + std::ostringstream query; + std::string newName = param; + trimString(newName); + + Database& db = Database::getInstance(); + query << "SELECT `id` FROM `players` WHERE `name`=" << db.escapeString(newName); + if (db.storeQuery(query.str())) { + // Name already in use + returnmessage << "This name is already in use."; + player->sendStoreError(STORE_ERROR_PURCHASE, returnmessage.str()); + return; + } else { + query.str(""); + toLowerCaseString(newName); + + std::string responseMessage; + NameEval_t nameValidation = validateName(newName); + + switch (nameValidation) { + case INVALID_LENGTH: + responseMessage = "Your new name must be more than 3 and less than 14 characters long."; + break; + case INVALID_TOKEN_LENGTH: + responseMessage = "Every words of your new name must be at least 2 characters long."; + break; + case INVALID_FORBIDDEN: + responseMessage = "You're using forbidden words in your new name."; + break; + case INVALID_CHARACTER: + responseMessage = "Your new name contains invalid characters."; + break; + case INVALID: + responseMessage = "Your new name is invalid."; + break; + case VALID: + responseMessage = "You have successfully changed you name, you must relog to see changes."; + break; + } + + if (nameValidation != VALID) { // Invalid name typed + player->sendStoreError(STORE_ERROR_PURCHASE, responseMessage); + return; + } else { + // Valid name so far + + // Check if it's an NPC or Monster name. + if (g_monsters.getMonsterType(newName)) { + responseMessage = "Your new name cannot be a monster's name."; + player->sendStoreError(STORE_ERROR_PURCHASE, responseMessage); + return; + } else if (getNpcByName(newName)) { + responseMessage = "Your new name cannot be an NPC's name."; + player->sendStoreError(STORE_ERROR_PURCHASE, responseMessage); + return; + } else { + capitalizeWords(newName); + + query << "UPDATE `players` SET `name` = " << db.escapeString(newName) << " WHERE `id` = " + << player->getGUID(); + if (db.executeQuery(query.str())) { + returnmessage << "You have successfully changed you name, you must relog to see the changes."; + successfully = true; + } else { + returnmessage << "An error ocurred processing your request, no changes were made."; + player->sendStoreError(STORE_ERROR_PURCHASE, returnmessage.str()); + return; + } + } + } + } + } else if (offerType == OFFER_TYPE_ITEM || offerType == OFFER_TYPE_TRAINING || offerType == OFFER_TYPE_POUCH || offerType == OFFER_TYPE_HOUSE || offerType == OFFER_TYPE_STACKABLE) { + // Create itemType + const ItemType& itemType = Item::items[thisOffer->getItemType()]; + uint16_t itemId = itemType.id; + + if (itemId == 0) { + player->sendStoreError(STORE_ERROR_NETWORK, "There was an error with the offer, report to Gamemaster."); + return; + } + + bool isKeg = (itemId >= ITEM_KEG_START && itemId <= ITEM_KEG_END); + + bool isCaskItem = ((itemId >= ITEM_HEALTH_CASK_START && itemId <= ITEM_HEALTH_CASK_END) || + (itemId >= ITEM_MANA_CASK_START && itemId <= ITEM_MANA_CASK_END) || + (itemId >= ITEM_SPIRIT_CASK_START && itemId <= ITEM_SPIRIT_CASK_END)); + uint64_t weight = static_cast(itemType.weight) * std::max(1, (isKeg ? 1 : thisOffer->getCount())); + if (isCaskItem) { + const ItemType& itemType2 = Item::items[TRANSFORM_BOX_ID]; + weight = static_cast(itemType2.weight); + } + if (player->getFreeCapacity() < weight) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free capacity to hold this item."); + return; + } + + Thing* thing = player->getThing(CONST_SLOT_STORE_INBOX); + if (!thing) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + Item* inboxItem = thing->getItem(); + if (!inboxItem) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + Container* inbox = inboxItem->getContainer(); + if (!inbox || (inbox->capacity() - inbox->size() <= 0) ) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + int32_t itemCount = thisOffer->getCount(); + int32_t pendingCount = thisOffer->getCount(); + + bool isHouseOffer = (offerType == OFFER_TYPE_HOUSE); + bool isTraining = (offerType == OFFER_TYPE_TRAINING); + + std::vector itemList; + int32_t rcreateitem = ((isKeg || isHouseOffer || isTraining) ? 1 : 100); + while (pendingCount > 0) { + uint16_t n = static_cast(std::min(pendingCount, rcreateitem)); + Item* tmpItem = Item::CreateItem((isHouseOffer ? TRANSFORM_BOX_ID : itemId), n); + if (!tmpItem) { + break; + } + + // Set player owner + tmpItem->setOwner(player->getGUID()); + + uint32_t removecount = n; + if (isKeg) { + int32_t pack; + if (pendingCount > 500) + pack = 500; + else + pack = pendingCount; + + tmpItem->setCharges(pack); + tmpItem->setDate(pack); + removecount = pack; + } else if (isTraining) { + int32_t pack = thisOffer->getCharges(); + tmpItem->setIntAttr(ITEM_ATTRIBUTE_CHARGES, pack); + + removecount = pack; + } else if (isHouseOffer) { + std::ostringstream packagename; + packagename << "You bought this item in the Store.\nUnwrap it in your own house to create a <" << itemType.name << ">."; + tmpItem->setStrAttr(ITEM_ATTRIBUTE_DESCRIPTION, packagename.str()); + tmpItem->setIntAttr(ITEM_ATTRIBUTE_WRAPID, itemId); + + if (isCaskItem) { + tmpItem->setCharges(itemCount); + tmpItem->setDate(itemCount); + removecount = pendingCount + 1; + } + } + + pendingCount -= removecount; + itemList.push_back(tmpItem); + } + + if (itemList.empty()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "There was an error in your purchase, report to ADM"); + return; + } + + if (itemCount > 100 && !isHouseOffer) { + Item* parcel = Item::CreateItem(2596, 1); + if (!parcel) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + std::ostringstream packagename; + packagename << itemCount << "x " << thisOffer->getName() << " package."; + parcel->setStrAttr(ITEM_ATTRIBUTE_NAME, packagename.str()); + + for (Item* item : itemList) { + if (g_game.internalAddItem(parcel->getContainer(), item) != RETURNVALUE_NOERROR) { + parcel->getContainer()->internalAddThing(item); + } + } + + if (g_game.internalAddItem(inbox, parcel) != RETURNVALUE_NOERROR) { + inbox->internalAddThing(parcel); + } + } else { + for (Item* item : itemList) { + if (g_game.internalAddItem(inbox, item) != RETURNVALUE_NOERROR) { + inbox->internalAddThing(item); + } + } + } + + successfully = true; + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_MULTI_ITEMS) { + std::map itemMap = thisOffer->getItems(); + + if (itemMap.empty()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "There was an error in your purchase, report to ADM"); + return; + } + + std::vector itemList; + Thing* thing = player->getThing(CONST_SLOT_STORE_INBOX); + if (!thing) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + Item* inboxItem = thing->getItem(); + if (!inboxItem) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + Container* inbox = inboxItem->getContainer(); + if (!inbox || (inbox->capacity() - inbox->size() <= 0) ) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + uint32_t capacity = 0; + for (const auto& it : itemMap) { + int32_t pendingCount = it.second; + while (pendingCount > 0) { + const ItemType& itemType = Item::items[it.first]; + uint16_t n = static_cast(std::min((itemType.stackable ? pendingCount : 1), 100)); + Item* tmpItem = Item::CreateItem(it.first, n); + if (!tmpItem) { + break; + } + + if (thisOffer->getActionID() > 0 ) { + tmpItem->setActionId(thisOffer->getActionID()); + } + + // Set player owner + tmpItem->setOwner(player->getGUID()); + uint32_t removecount = n; + pendingCount -= removecount; + capacity += static_cast(itemType.weight) * std::max(1, n); + + itemList.push_back(tmpItem); + } + } + + if (player->getFreeCapacity() < capacity) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free capacity to hold this item."); + return; + } + + if (itemList.empty()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "There was an error in your purchase, report to ADM"); + return; + } + + Item* parcel = Item::CreateItem(2596, 1); + if (!parcel) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + std::ostringstream packagename; + packagename << "1x " << thisOffer->getName() << " package."; + parcel->setStrAttr(ITEM_ATTRIBUTE_NAME, packagename.str()); + + for (Item* item : itemList) { + if (g_game.internalAddItem(parcel->getContainer(), item) != RETURNVALUE_NOERROR) { + parcel->getContainer()->internalAddThing(item); + } + } + + if (g_game.internalAddItem(inbox, parcel) != RETURNVALUE_NOERROR) { + inbox->internalAddThing(parcel); + } + + successfully = true; + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_INSTANT_REWARD_ACCESS) { + //player->setInstantRewardTokens(player->getInstantRewardTokens() + thisOffer->getCount()); + //player->sendResourceData(RESOURCETYPE_REWARD, player->getInstantRewardTokens()); + successfully = true; + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_BLESSINGS) { + if (thisOffer->getBlessid() < 1 || thisOffer->getBlessid() > 8) { + player->sendStoreError(STORE_ERROR_PURCHASE, "This purchase is impossible. Report to ADM [ERRBID" + std::to_string(thisOffer->getBlessid()) +"]"); + return; + } + + player->addBlessing(thisOffer->getBlessid(), thisOffer->getCount()); + player->sendBlessStatus(); + + successfully = true; + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_ALLBLESSINGS) { + + uint8_t count = 0; + uint8_t limitBless = 0; + uint8_t minBless = (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : BLESS_FIRST); + uint8_t maxBless = BLESS_LAST; + for (int i = minBless; i <= maxBless; ++i) { + limitBless++; + if (player->hasBlessing(i)) { + count++; + } + } + + if (count >= limitBless) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You already have all blessings."); + return; + } + + for (int i = minBless; i <= maxBless; ++i) + { + player->addBlessing(i, thisOffer->getCount()); + } + + player->sendBlessStatus(); + successfully = true; + returnmessage << "You have purchased " << std::to_string(thisOffer->getCount()) << "x " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_PREMIUM) { + if (player->premiumDays != std::numeric_limits::max()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You have reached the maximum premium limit"); + return; + } + + int32_t addDays = std::min(0xFFFE - player->premiumDays, thisOffer->getCount(true)); + player->setPremiumDays(player->premiumDays + addDays); + IOLoginData::addPremiumDays(player->getAccount(), addDays); + successfully = true; + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + } else if (offerType == OFFER_TYPE_OUTFIT || offerType == OFFER_TYPE_OUTFIT_ADDON) { + uint8_t addons = thisOffer->getAddon(); + uint16_t lookType = (player->getSex() == PLAYERSEX_FEMALE ? thisOffer->getOutfitFemale() : thisOffer->getOutfitMale()); + + if ((addons == 1 || addons == 2) && !player->canWear(lookType, 0)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You must own the outfit before you can buy its addon."); + return; + } + + if (player->canWear(lookType, addons)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You already own this outfit."); + return; + } + + successfully = true; + returnmessage << "You've successfully bought the "<< thisOffer->getName() << "."; + } else if (offerType == OFFER_TYPE_MOUNT) { + Mount* mount = thisOffer->getMount(); + if (!mount || mount == nullptr) { + player->sendStoreError(STORE_ERROR_PURCHASE, "This purchase is impossible. Report to ADM [ERRMID" + std::to_string(thisOffer->getId()) +"]"); + return; + } + + if (player->hasMount(mount)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You arealdy own this mount."); + return; + } + + if (!player->tameMount( mount->id )) { + player->sendStoreError(STORE_ERROR_PURCHASE, "An error ocurred processing your purchase. Try again later."); + return; + } + + returnmessage << "You've successfully bought the " << mount->name <<" Mount."; + successfully = true; + } else if (offerType == OFFER_TYPE_SEXCHANGE) { + Outfit_t outfit = player->getCurrentOutfit(); + if (player->getSex() == PLAYERSEX_FEMALE) { + player->setSex(PLAYERSEX_MALE); + outfit.lookType = 128; + outfit.lookAddons = 0; + player->setCurrentOutfit(outfit); + } else { + player->setSex(PLAYERSEX_MALE); + outfit.lookType = 136; + outfit.lookAddons = 0; + player->setCurrentOutfit(outfit); + } + + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + successfully = true; + + } else if (offerType == OFFER_TYPE_EXPBOOST) { + uint16_t currentExpBoostTime = player->getExpBoostStamina(); + + player->setStoreXpBoost(50); + player->setExpBoostStamina(currentExpBoostTime + 3600); + + int32_t value1; + player->getStorageValue(51052, value1); + if (value1 == -1) { + value1 = 1; + player->addStorageValue(51052, 1); + } + + // update + player->getStorageValue(51052, value1); + + returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; + + player->addStorageValue(51052, value1 + 1); + player->addStorageValue(51053, OS_TIME(nullptr)); // last bought + player->sendStats(); + successfully = true; + + } else if (offerType == OFFER_TYPE_TEMPLE) { + if (player->hasCondition(CONDITION_INFIGHT)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use temple teleport in fight!"); + return; + } + const Position& position = player->getTemplePosition(); + const Position oldPosition = player->getPosition(); + if (internalTeleport(player, position, false) != RETURNVALUE_NOERROR) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use temple teleport in fight!"); + return; + } + + if (oldPosition.x == position.x) { + if (oldPosition.y < position.y) { + internalCreatureTurn(player, DIRECTION_SOUTH); + } else { + internalCreatureTurn(player, DIRECTION_NORTH); + } + } else if (oldPosition.x > position.x) { + internalCreatureTurn(player, DIRECTION_WEST); + } else if (oldPosition.x < position.x) { + internalCreatureTurn(player, DIRECTION_EAST); + } + + + addMagicEffect(position, CONST_ME_TELEPORT); + player->sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have been teleported to your hometown."); + successfully = true; + + } else if (offerType == OFFER_TYPE_PROMOTION) { + player->sendStoreError(STORE_ERROR_PURCHASE, "This offer has disable."); + return; + } else if (offerType == OFFER_TYPE_CHARM_EXPANSION) { + if (player->hasCharmExpansion()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You have charm expansion"); + return; + } + + player->setCharmExpansion(true); + returnmessage << "You've successfully bought the " << thisOffer->getName() <<"."; + successfully = true; + } else if (offerType == OFFER_TYPE_CHARM_POINTS) { + player->setCharmPoints( player->getCharmPoints() + thisOffer->getCount()); + successfully = true; + } else if (offerType == OFFER_TYPE_FRAG_REMOVE) { + if (playerTile && !playerTile->hasFlag(TILESTATE_PROTECTIONZONE)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use this offer in fight!"); + return; + } + if (player->hasCondition(CONDITION_INFIGHT)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use this offer in fight!"); + return; + } + + if (player->unjustifiedKills.empty()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You have no frag to remove."); + return; + } + + successfully = player->removeFrags(1); + } else if (offerType == OFFER_TYPE_SKULL_REMOVE) { + if (playerTile && !playerTile->hasFlag(TILESTATE_PROTECTIONZONE)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use this offer in fight!"); + return; + } + if (player->hasCondition(CONDITION_INFIGHT)) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You can't use this offer in fight!"); + return; + } + + if (player->getSkull() != thisOffer->getSkull()) { + player->sendStoreError(STORE_ERROR_PURCHASE, "This offer is disabled for you!"); + return; + } + + player->removeFrags(); + player->setSkull(SKULL_NONE); + + successfully = true; + } else if (offerType == OFFER_TYPE_RECOVERYKEY) { + std::ostringstream newkey; + newkey << generateRK(4) << "-" << generateRK(4) << "-" << generateRK(4) << "-" << generateRK(4); + + Item* letter = Item::CreateItem(2597, 1); + if (!letter) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + std::ostringstream text; + text << "Recovery key succesfully renewed!\n\nYour new recovery key is: " << newkey.str() << "\nSave this in a safe place."; + letter->setStrAttr(ITEM_ATTRIBUTE_TEXT, text.str()); + + Thing* thing = player->getThing(CONST_SLOT_STORE_INBOX); + if (!thing) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + Item* inboxItem = thing->getItem(); + if (!inboxItem) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + Container* inbox = inboxItem->getContainer(); + if (!inbox || (inbox->capacity() - inbox->size() <= 0) ) { + player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free slots in your store inbox."); + return; + } + + if (g_game.internalAddItem(inbox, letter) != RETURNVALUE_NOERROR) { + inbox->internalAddThing(letter); + } + + std::ostringstream query; + query << "UPDATE accounts SET `key` = " << Database::getInstance().escapeString(newkey.str()) << " WHERE `id` = " << player->getAccount(); + Database::getInstance().executeQuery(query.str()); + + uint32_t newtime = OS_TIME(nullptr) + (7*24*60*60); + player->addAccountStorageValue(1, newtime); + successfully = true; + } + + if (successfully) { + account::Account account(player->getAccount()); + account.LoadAccountDB(); + account.RemoveCoins(offerPrice*-1); + player->setTibiaCoins(account.GetCoins(thisOffer->getCoinType()), thisOffer->getCoinType()); + if (returnmessage.str().empty()) { + returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" coins"; + } + + player->updateCoinBalance(); + + player->sendStorePurchaseSuccessful(returnmessage.str()); + account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), thisOffer->getCount(true), static_cast(thisOffer->getCoinType()), std::move(thisOffer->getName()), offerPrice); + } else { + player->sendStoreError(STORE_ERROR_PURCHASE, "Something went wrong with your purchase."); + } + + return; +} + +void Game::playerStoreTransactionHistory(uint32_t playerId, uint32_t pages, uint8_t entryPages) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + uint32_t accountId = player->getAccount(); + std::vector filter; + uint32_t totalEntries = static_cast(storeHistory[accountId].size()); + uint32_t getPages = totalEntries / entryPages; + + uint32_t totalPages = getPages + ((totalEntries / entryPages > 0 ? 1 : 0)); + auto begin = storeHistory[accountId].begin() + (pages > 1 ? entryPages * (pages - 1) : 0); + uint8_t count = 0; + for (auto currentHistory = begin, end = storeHistory[accountId].end(); currentHistory != end; ++currentHistory) { + if (count == entryPages) { + break; + } + count++; + filter.emplace_back(*currentHistory); + } + if (filter.empty()) { + player->sendStoreError(STORE_ERROR_HISTORY, "You don't have any entries yet."); + return; + } + + player->sendStoreHistory(totalPages, pages, filter); + +} + +void Game::queueSendStoreAlertToUser(uint32_t playerId, std::string message, StoreErrors_t storeErrorCode) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->sendStoreError(storeErrorCode, message); +} diff --git a/src/game/game.h b/src/game/game.h index ad5a19cc4c9..13082112b93 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -517,6 +517,12 @@ class Game return CharmList; } + // Store + void playerOpenStore(uint32_t playerId, bool openStore, StoreOffers* offers = nullptr); + void playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std::string& param); + void playerStoreTransactionHistory(uint32_t playerId, uint32_t pages, uint8_t entryPages); + void queueSendStoreAlertToUser(uint32_t playerId, std::string message, StoreErrors_t storeErrorCode = STORE_ERROR_NETWORK); + private: void checkImbuements(); bool playerSaySpell(Player* player, SpeakClasses type, const std::string& text); diff --git a/src/game/gamestore.cpp b/src/game/gamestore.cpp index d4775c74a4b..ce9fffcd855 100644 --- a/src/game/gamestore.cpp +++ b/src/game/gamestore.cpp @@ -62,7 +62,6 @@ const std::unordered_map OfferTypesMap = { {"charmexpansion", OFFER_TYPE_CHARM_EXPANSION}, {"charmpoints", OFFER_TYPE_CHARM_POINTS}, {"multiitems", OFFER_TYPE_MULTI_ITEMS}, - {"vip", OFFER_TYPE_VIP}, {"fragremove", OFFER_TYPE_FRAG_REMOVE}, {"skullremove", OFFER_TYPE_SKULL_REMOVE}, {"recoverykey", OFFER_TYPE_RECOVERYKEY}, @@ -610,6 +609,11 @@ std::string StoreOffer::getDisabledReason(Player* player) return disabledReason; } +Mount* StoreOffer::getMount() +{ + return g_game.mounts.getMountByID(id); +} + uint8_t GameStore::convertType(OfferTypes_t type) { uint8_t offertype = 0; diff --git a/src/game/gamestore.hpp b/src/game/gamestore.hpp index fdee94499f2..eb3fc1647b6 100644 --- a/src/game/gamestore.hpp +++ b/src/game/gamestore.hpp @@ -182,7 +182,7 @@ class GameStore { }; -class StoreOffers { +class StoreOffers { public: StoreOffers(std::string name) : name(std::move(name)) {} @@ -315,6 +315,8 @@ class StoreOffer { return rookgaard; } + Mount* getMount(); + protected: friend class GameStore; friend class StoreOffers; diff --git a/src/items/item.cpp b/src/items/item.cpp index 6a8a38b3376..12b0d6ad19d 100644 --- a/src/items/item.cpp +++ b/src/items/item.cpp @@ -648,6 +648,16 @@ Attr_ReadValue Item::readAttr(AttrTypes_t attr, PropStream& propStream) break; } + case ATTR_WRAPID: { + uint16_t wrapId; + if (!propStream.read(wrapId)) { + return ATTR_READ_ERROR; + } + + setIntAttr(ITEM_ATTRIBUTE_WRAPID, wrapId); + break; + } + //these should be handled through derived classes //If these are called then something has changed in the items.xml since the map was saved //just read the values @@ -885,6 +895,11 @@ void Item::serializeAttr(PropWriteStream& propWriteStream) const propWriteStream.write(ATTR_QUICKLOOTCONTAINER); propWriteStream.write(getQuicklootAttr()); } + + if (hasAttribute(ITEM_ATTRIBUTE_WRAPID)) { + propWriteStream.write(ATTR_WRAPID); + propWriteStream.write(getIntAttr(ITEM_ATTRIBUTE_WRAPID)); + } } bool Item::hasProperty(ItemProperty prop) const diff --git a/src/items/item.h b/src/items/item.h index 0be9cc7416d..ced52601730 100644 --- a/src/items/item.h +++ b/src/items/item.h @@ -433,7 +433,7 @@ class ItemAttributes | ITEM_ATTRIBUTE_ARMOR | ITEM_ATTRIBUTE_HITCHANCE | ITEM_ATTRIBUTE_SHOOTRANGE | ITEM_ATTRIBUTE_OWNER | ITEM_ATTRIBUTE_DURATION | ITEM_ATTRIBUTE_DECAYSTATE | ITEM_ATTRIBUTE_CORPSEOWNER | ITEM_ATTRIBUTE_CHARGES | ITEM_ATTRIBUTE_FLUIDTYPE | ITEM_ATTRIBUTE_DOORID | ITEM_ATTRIBUTE_IMBUINGSLOTS - | ITEM_ATTRIBUTE_OPENCONTAINER | ITEM_ATTRIBUTE_QUICKLOOTCONTAINER; + | ITEM_ATTRIBUTE_OPENCONTAINER | ITEM_ATTRIBUTE_QUICKLOOTCONTAINER | ITEM_ATTRIBUTE_WRAPID; const static uint32_t stringAttributeTypes = ITEM_ATTRIBUTE_DESCRIPTION | ITEM_ATTRIBUTE_TEXT | ITEM_ATTRIBUTE_WRITER | ITEM_ATTRIBUTE_NAME | ITEM_ATTRIBUTE_ARTICLE | ITEM_ATTRIBUTE_PLURALNAME | ITEM_ATTRIBUTE_SPECIAL; diff --git a/src/items/items_definitions.hpp b/src/items/items_definitions.hpp index 6a032cba906..b065b1a2776 100644 --- a/src/items/items_definitions.hpp +++ b/src/items/items_definitions.hpp @@ -346,7 +346,7 @@ enum ItemDecayState_t : uint8_t { }; enum AttrTypes_t { - //ATTR_DESCRIPTION = 1, + ATTR_WRAPID = 1, //ATTR_EXT_FILE = 2, ATTR_TILE_FLAGS = 3, ATTR_ACTION_ID = 4, @@ -434,6 +434,7 @@ enum ItemAttrTypes : uint32_t { ITEM_ATTRIBUTE_IMBUINGSLOTS = 1 << 24, ITEM_ATTRIBUTE_OPENCONTAINER = 1 << 25, ITEM_ATTRIBUTE_QUICKLOOTCONTAINER = 1 << 26, + ITEM_ATTRIBUTE_WRAPID = 1 << 27, ITEM_ATTRIBUTE_CUSTOM = 1U << 31 }; diff --git a/src/lua/functions/core/game/lua_enums.hpp b/src/lua/functions/core/game/lua_enums.hpp index b329e4589ff..329977e2eed 100644 --- a/src/lua/functions/core/game/lua_enums.hpp +++ b/src/lua/functions/core/game/lua_enums.hpp @@ -506,6 +506,7 @@ class LuaEnums final : LuaScriptInterface { registerEnum(L, ITEM_ATTRIBUTE_SPECIAL) registerEnum(L, ITEM_ATTRIBUTE_OPENCONTAINER) registerEnum(L, ITEM_ATTRIBUTE_QUICKLOOTCONTAINER) + registerEnum(L, ITEM_ATTRIBUTE_WRAPID) registerEnum(L, ITEM_TYPE_DEPOT) registerEnum(L, ITEM_TYPE_REWARDCHEST) diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index add42529c5b..69dfd50d402 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -26,6 +26,7 @@ #include "config/configmanager.h" #include "declarations.hpp" #include "game/game.h" +#include "game/gamestore.hpp" #include "creatures/players/imbuements/imbuements.h" #include "io/iobestiary.h" #include "io/iologindata.h" @@ -50,6 +51,7 @@ extern Modules *g_modules; extern Spells *g_spells; extern Imbuements *g_imbuements; extern Monsters g_monsters; +extern GameStore g_gameStore; void ProtocolGame::AddItem(NetworkMessage &msg, uint16_t id, uint8_t count) { @@ -749,6 +751,12 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xF7: parseMarketCancelOffer(msg); break; case 0xF8: parseMarketAcceptOffer(msg); break; case 0xF9: parseModalWindowAnswer(msg); break; + // Gamestore + case 0xFA: parseOpenStore(); break; + case 0xFB: parseRequestStoreOffers(msg); break; + case 0xFC: parseBuyStoreOffer(msg); break; + case 0xFD: parseOpenTransactionHistory(msg); break; + case 0xFE: parseRequestTransactionHistory(msg); break; //case 0xDF, 0xE0, 0xE1, 0xFB, 0xFC, 0xFD, 0xFE Premium Shop. @@ -2558,6 +2566,87 @@ void ProtocolGame::parseSeekInContainer(NetworkMessage &msg) addGameTask(&Game::playerSeekInContainer, player->getID(), containerId, index); } +void ProtocolGame::parseOpenStore() +{ + addGameTask(&Game::playerOpenStore, player->getID(), true, nullptr); +} + +void ProtocolGame::parseRequestStoreOffers(NetworkMessage& msg) +{ + uint8_t actionType = msg.getByte(); + if (actionType == 0) { + player->sendStoreHome(); + return; + } + + StoreOffers* offers = nullptr; + if (actionType == 0) { + offers = g_gameStore.getOfferByName(g_config.getString(DEFAULT_OFFER)); + } else if (actionType == 2) { + std::string categoryName = msg.getString(); + offers = g_gameStore.getOfferByName(categoryName); + } else if (actionType == 4) { + uint32_t id = msg.get(); + offers = g_gameStore.getOffersByOfferId(id); + } else { + // SPDLOG_INFO("Test"); + // std::string categoryName = msg.getString(); + // offers = g_gameStore.getOfferByName(categoryName); + } + + if (offers != nullptr) { + addGameTask(&Game::playerOpenStore, player->getID(), false, offers); + } else { + addGameTask(&Game::playerOpenStore, player->getID(), false, nullptr); + } +} + +void ProtocolGame::parseBuyStoreOffer(NetworkMessage& msg) +{ + uint32_t id = msg.get(); + OfferBuyTypes_t productType = static_cast(msg.getByte()); + std::string param; + + StoreOffer* offer = g_gameStore.getOfferById(id); + if (offer == nullptr) { + return; + } + + if (offer->getOfferType() == OFFER_TYPE_NAMECHANGE && productType != OFFER_BUY_TYPE_NAMECHANGE) { + requestPurchaseData(id, OFFER_BUY_TYPE_NAMECHANGE); + return; + } + + if (offer->getOfferType() == OFFER_TYPE_NAMECHANGE) { + param = msg.getString(); + } + + addGameTask(&Game::playerBuyStoreOffer, player->getID(), *offer, std::move(param)); +} + +void ProtocolGame::parseSendDescription(NetworkMessage& msg) +{ + uint32_t offerId = msg.get(); + StoreOffer* storeOffer = g_gameStore.getOfferById(offerId); + if (storeOffer == nullptr) { + return; + } + player->sendOfferDescription(offerId, storeOffer->getDescription(player)); +} + +void ProtocolGame::parseOpenTransactionHistory(NetworkMessage& msg) +{ + uint8_t entryPages = msg.getByte(); + player->setEntriesPerPage(entryPages); + addGameTask(&Game::playerStoreTransactionHistory, player->getID(), 1, entryPages); +} + +void ProtocolGame::parseRequestTransactionHistory(NetworkMessage& msg) +{ + uint32_t pages = msg.get(); + addGameTask(&Game::playerStoreTransactionHistory, player->getID(), pages + 1, player->getEntriesPerPage()); +} + // Send methods void ProtocolGame::sendOpenPrivateChannel(const std::string &receiver) { @@ -6483,3 +6572,257 @@ void ProtocolGame::sendLockerItems(std::map itemMap, uint16_ writeToOutputBuffer(msg); } + +void ProtocolGame::sendStoreHistory(uint32_t totalPages, uint32_t pages, std::vector filter) +{ + NetworkMessage msg; + msg.addByte(0xFD); + msg.add(totalPages > 0 ? pages - 1 : 0x0); //-- current page + msg.add(totalPages > 0 ? totalPages : 0x0); //-- total page + msg.addByte(filter.size()); + + for (auto currentHistory = filter.begin(), end = filter.end(); currentHistory != end; ++currentHistory) { + // Version 12.20+ + msg.add(0); + + msg.add((*currentHistory).time); + msg.addByte((*currentHistory).mode); + msg.add((*currentHistory).cust); + + // Version 1200+ + msg.addByte((*currentHistory).coinMode); //0 = transferable tibia coin, 1 = normal tibia coin + msg.addString((*currentHistory).description); + msg.addByte(0); //-- details + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendShowStoreOffers(StoreOffers* offers) +{ + if (offers == nullptr) { + player->sendStoreHome(); + return; + } + + NetworkMessage msg; + msg.addByte(0xFC); + msg.addString(offers->getName()); + + // Version 1180+ + msg.add(0); + // Version 1185+) + msg.add(0); + + uint16_t count = 0; + std::map> organized = g_gameStore.getStoreOrganizedByName(offers); + for (const auto& it : organized) { + if (!it.first.empty()) + count++; + } + + msg.add(count); + + if (count > 0) { + for (const auto& it : organized) { + msg.addString(it.first); + msg.addByte(it.second.size()); + addStoreOffer(msg, it.second); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendOfferDescription(uint32_t id, std::string desc) +{ + NetworkMessage msg; + msg.addByte(0xEA); + msg.add(id); + msg.addString(desc); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendStoreHome() +{ + NetworkMessage msg; + msg.addByte(0xFC); + msg.addString("Home"); + + msg.add(0x0); + msg.addByte(0x0); + msg.addByte(0x0); + msg.add(0x00); + + uint16_t count = 0; + std::map> organized = g_gameStore.getHomeOffersOrganized(); + for (const auto& it : organized) { + if (!it.first.empty()) + count++; + } + + msg.add(count); + if (count > 0) { + for (const auto& it : organized) { + msg.addString(it.first); + msg.addByte(it.second.size()); + addStoreOffer(msg, it.second); + } + + } + + std::vector banners = g_gameStore.getHomeBanners(); + for (auto banner = banners.begin(), end = banners.end(); banner != end; ++banner) { + msg.addByte(banners.size()); + msg.addString((*banner)); + msg.addByte(banners.size()+1); + msg.add(0x0); + msg.addByte(0x0); + } + + msg.addByte(banners.size()); + msg.addByte(banners.size()+1); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendStoreError(uint8_t errorType, std::string message) +{ + NetworkMessage msg; + msg.addByte(0xE0); + msg.addByte(errorType); + msg.addString(message); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendStorePurchaseSuccessful(const std::string& message) +{ + NetworkMessage msg; + + msg.addByte(0xFE); + msg.addByte(0x00); + + msg.addString(message); + writeToOutputBuffer(msg); +} + +void ProtocolGame::requestPurchaseData(uint32_t offerId, uint8_t offerType) +{ + NetworkMessage msg; + msg.addByte(0xE1); + msg.add(offerId); + msg.addByte(offerType); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::openStore() +{ + NetworkMessage msg; + msg.addByte(0xFB); + + msg.add(g_gameStore.getOfferCount()); + // enviando primeiro as categorias sem subcategorias + std::vector categories = g_gameStore.getStoreCategories(); + for (auto it = categories.begin(), end = categories.end(); it != end; ++it) { + msg.addString((*it).name); + + msg.addByte(OFFER_STATE_NONE); + + msg.addByte(1); + msg.addString((*it).icon); + + msg.add(0x00); + } + + std::vector offers = g_gameStore.getStoreOffers(); + for (auto it = offers.begin(), end = offers.end(); it != end; ++it) { + msg.addString((*it)->getName()); + + msg.addByte((*it)->getOfferState()); + + msg.addByte(1); + msg.addString((*it)->getIcon()); + + msg.addString((*it)->getParent()); + } + + writeToOutputBuffer(msg); + player->updateCoinBalance(); + sendStoreHome(); +} + +void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector it) +{ + std::string lasticon; + Mount* lastmount = nullptr; + OfferTypes_t lasttype = OFFER_TYPE_NONE; + uint32_t lastid = 0; + uint16_t lastitemid = 0; + uint16_t lastoutfit = 0; + for (auto offer = it.begin(), end = it.end(); offer != end; ++offer) { + lasttype = (*offer)->getOfferType(); + lasticon = (*offer)->getIcon(); + lastitemid = (*offer)->getItemType(); + lastoutfit = (player->getSex() == PLAYERSEX_FEMALE ? (*offer)->getOutfitFemale() : (*offer)->getOutfitMale()); + lastmount = (*offer)->getMount(); + if (lastid == 0) + lastid = (*offer)->getId(); + + msg.add((*offer)->getId()); + msg.add((*offer)->getCount()); + msg.add((*offer)->getPrice(player)); + msg.addByte((*offer)->getCoinType()); + + std::string disabled = (*offer)->getDisabledReason(player); + msg.addByte(!disabled.empty()); + if (!disabled.empty()) { + msg.addByte(0x01); + msg.addString(disabled); + } + + if ((*offer)->getOfferState() == OFFER_STATE_SALE) { + time_t mytime; + mytime = time(NULL); + struct tm tm = *localtime(&mytime); + int32_t daySub = (*offer)->getValidUntil() - tm.tm_mday; + if (daySub >= 0) { + msg.addByte((*offer)->getOfferState()); + msg.add(mytime + daySub * 86400); + msg.add((*offer)->getBasePrice()); + } else { + msg.addByte(OFFER_STATE_NONE); + } + } else { + msg.addByte((*offer)->getOfferState()); + } + + } + + uint8_t oftp = g_gameStore.convertType(lasttype); + msg.addByte(oftp); + if (oftp == 0) { + msg.addString(lasticon); + } else if (oftp == 1) { + msg.add(lastmount->clientId); + } else if (oftp == 2) { + msg.add(lastoutfit); + msg.addByte(player->getCurrentOutfit().lookHead); + msg.addByte(player->getCurrentOutfit().lookBody); + msg.addByte(player->getCurrentOutfit().lookLegs); + msg.addByte(player->getCurrentOutfit().lookFeet); + } else if (oftp == 3) { + msg.addItemId(lastitemid); + } + + // Version 1220+ + msg.addByte(0x00); + + msg.add(0x00); // category + + msg.add(298); + msg.add(lasttype == OFFER_TYPE_NAMECHANGE ? lastid : 0x00); + msg.addByte(lasttype == OFFER_TYPE_NAMECHANGE); + msg.add(0x00); + +} diff --git a/src/server/network/protocol/protocolgame.h b/src/server/network/protocol/protocolgame.h index 99d0577f259..9825d2aa956 100644 --- a/src/server/network/protocol/protocolgame.h +++ b/src/server/network/protocol/protocolgame.h @@ -37,6 +37,9 @@ class Tile; class Connection; class Quest; class ProtocolGame; +class StoreOffers; +class StoreOffer; + using ProtocolGame_ptr = std::shared_ptr; extern ConfigManager g_config; @@ -224,6 +227,25 @@ class ProtocolGame final : public Protocol // imbue info void addImbuementInfo(NetworkMessage &msg, uint32_t imbuid); + // Store + void parseOpenStore(); + void parseRequestStoreOffers(NetworkMessage& msg); + void parseBuyStoreOffer(NetworkMessage& msg); + void parseSendDescription(NetworkMessage& msg); + void parseOpenTransactionHistory(NetworkMessage& msg); + void parseRequestTransactionHistory(NetworkMessage& msg); + + void sendStoreHistory(uint32_t totalPages, uint32_t pages, std::vector filter); + void sendShowStoreOffers(StoreOffers* offers); + void sendOfferDescription(uint32_t id, std::string desc); + void sendStoreHome(); + void sendStoreError(uint8_t errorType, std::string message); + void sendStorePurchaseSuccessful(const std::string& message); + + void requestPurchaseData(uint32_t offerId, uint8_t offerType); + void openStore(); + void addStoreOffer(NetworkMessage& msg, std::vector it); + //Send functions void sendChannelMessage(const std::string &author, const std::string &text, SpeakClasses type, uint16_t channel); void sendChannelEvent(uint16_t channelId, const std::string &playerName, ChannelEvent_t channelEvent); diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index eff126b2dc9..b87b4fa2329 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1067,6 +1067,8 @@ ItemAttrTypes stringToItemAttribute(const std::string& str) return ITEM_ATTRIBUTE_FLUIDTYPE; } else if (str == "doorid") { return ITEM_ATTRIBUTE_DOORID; + } else if (str == "wrapid") { + return ITEM_ATTRIBUTE_WRAPID; } return ITEM_ATTRIBUTE_NONE; } @@ -1445,3 +1447,15 @@ std::string getObjectCategoryName(ObjectCategory_t category) default: return std::string(); } } + +std::string generateRK(size_t length) +{ + std::ostringstream newkey; + std::string consonants = "ABCDEFGHIJLMNOPQRSTUVWXYZ0123456789"; + while (newkey.str().length() < length) + { + uint8_t pos = uniform_random(1, consonants.length()); + newkey << consonants.substr(pos, 1); + } + return newkey.str(); +} diff --git a/src/utils/tools.h b/src/utils/tools.h index dfff22cd5fb..5f9be81e30f 100644 --- a/src/utils/tools.h +++ b/src/utils/tools.h @@ -110,6 +110,8 @@ bool isCaskItem(uint16_t itemId); std::string getObjectCategoryName(ObjectCategory_t category); +std::string generateRK(size_t length); + int64_t OTSYS_TIME(bool useTime = false); int32_t OS_TIME(time_t* timer); diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000000..39e57de5d9e --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,22 @@ +{ + "name": "canary", + "version-string": "1.0.0", + "dependencies": [ + "boost-asio", + "boost-lockfree", + "boost-variant", + "boost-filesystem", + "boost-iostreams", + "boost-system", + "libmariadb", + "pugixml", + "spdlog", + "curl", + "jsoncpp", + "cryptopp", + { + "name": "luajit", + "platform": "windows" + } + ] +} From a8f99b9ca17f6f83a090c2b400eb532824a1b254 Mon Sep 17 00:00:00 2001 From: dudantas Date: Tue, 10 Aug 2021 00:23:40 -0300 Subject: [PATCH 03/30] Delete client_assertions.txt --- client_assertions.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 client_assertions.txt diff --git a/client_assertions.txt b/client_assertions.txt deleted file mode 100644 index 98600b05d15..00000000000 --- a/client_assertions.txt +++ /dev/null @@ -1,5 +0,0 @@ ------ 10/08/2021 00:21:43 - Druid Sample (127.0.0.1) ----- - - - - From c16c630f8e8e70d874ca596a84e2aa36e509b288 Mon Sep 17 00:00:00 2001 From: dudantas Date: Tue, 10 Aug 2021 13:39:26 -0300 Subject: [PATCH 04/30] Fix indent --- data/lib/core/functions/tables.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/lib/core/functions/tables.lua b/data/lib/core/functions/tables.lua index 449ecedb10c..ed0237ff5ba 100644 --- a/data/lib/core/functions/tables.lua +++ b/data/lib/core/functions/tables.lua @@ -114,8 +114,8 @@ function pairsByKeys(t, f) local a = {} for n in pairs(t) do table.insert(a, n) end table.sort(a, f) - local i = 0 -- iterator variable - local iter = function () -- iterator function + local i = 0 -- iterator variable + local iter = function () -- iterator function i = i + 1 if a[i] == nil then return nil else return a[i], t[a[i]] From 14c827623865359e84ee06d4da6dcdbe0b26a6d9 Mon Sep 17 00:00:00 2001 From: dudantas Date: Thu, 12 Aug 2021 22:04:05 -0300 Subject: [PATCH 05/30] Store loading informations done --- config.lua.dist | 3 - data/XML/gamestore.xml | 503 ------------ data/XML/store.xml | 504 ++++++++++++ data/modules/scripts/blessings/blessings.lua | 2 +- src/CMakeLists.txt | 2 +- src/config/config_definitions.hpp | 1 - src/config/configmanager.cpp | 1 - src/creatures/players/account/account.cpp | 2 +- src/creatures/players/store/store.cpp | 760 ++++++++++++++++++ .../players/store/store.hpp} | 51 +- src/game/game.cpp | 16 +- src/game/game.h | 3 +- src/game/gamestore.cpp | 750 ----------------- .../functions/core/game/config_functions.hpp | 1 - src/otserv.cpp | 12 +- src/server/network/protocol/protocolgame.cpp | 40 +- 16 files changed, 1326 insertions(+), 1325 deletions(-) delete mode 100644 data/XML/gamestore.xml create mode 100644 data/XML/store.xml create mode 100644 src/creatures/players/store/store.cpp rename src/{game/gamestore.hpp => creatures/players/store/store.hpp} (86%) delete mode 100644 src/game/gamestore.cpp diff --git a/config.lua.dist b/config.lua.dist index 5e799cd61e7..a3684a2c797 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -51,9 +51,6 @@ freeDepotLimit = 2000 premiumDepotLimit = 10000 depotBoxes = 18 --- GameStore -gamestoreByModules = true - -- NOTE: Access only for Premium Account onlyPremiumAccount = false diff --git a/data/XML/gamestore.xml b/data/XML/gamestore.xml deleted file mode 100644 index 8999d9ea562..00000000000 --- a/data/XML/gamestore.xml +++ /dev/null @@ -1,503 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/XML/store.xml b/data/XML/store.xml new file mode 100644 index 00000000000..1e95a9c43b3 --- /dev/null +++ b/data/XML/store.xml @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/modules/scripts/blessings/blessings.lua b/data/modules/scripts/blessings/blessings.lua index faa4ec2d315..3abdde1604a 100644 --- a/data/modules/scripts/blessings/blessings.lua +++ b/data/modules/scripts/blessings/blessings.lua @@ -7,7 +7,7 @@ Blessings.Credits = { todo = { "Insert & Select query in blessings_history", "Add unfair fight reductio (convert the get killer is pvp fight with getDamageMap of dead player)", - "Gamestore buy blessing", + "Store buy blessing", "Test ank print text", "Test all functions", "Test henricus prices/blessings", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d8560b45ea4..85355c578b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -124,6 +124,7 @@ target_sources(${PROJECT_NAME} creatures/players/grouping/party.cpp creatures/players/imbuements/imbuements.cpp creatures/players/management/ban.cpp + creatures/players/store/store.cpp creatures/players/management/waitlist.cpp creatures/players/player.cpp creatures/players/vocations/vocation.cpp @@ -131,7 +132,6 @@ target_sources(${PROJECT_NAME} database/databasemanager.cpp database/databasetasks.cpp game/game.cpp - game/gamestore.cpp game/movement/position.cpp game/movement/teleport.cpp game/scheduling/scheduler.cpp diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 908cbff7250..4cebd3865c1 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -50,7 +50,6 @@ enum booleanConfig_t { FORCE_MONSTERTYPE_LOAD, HOUSE_OWNED_BY_ACCOUNT, CLEAN_PROTECTION_ZONES, - STOREMODULES, ALLOW_BLOCK_SPAWN, ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS, WEATHER_RAIN, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index d4f3b2e5da9..1a9e319f3fb 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -171,7 +171,6 @@ bool ConfigManager::load() boolean[HOUSE_OWNED_BY_ACCOUNT] = getGlobalBoolean(L, "houseOwnedByAccount", false); boolean[CLEAN_PROTECTION_ZONES] = getGlobalBoolean(L, "cleanProtectionZones", false); boolean[SERVER_SAVE_SHUTDOWN] = getGlobalBoolean(L, "serverSaveShutdown", true); - boolean[STOREMODULES] = getGlobalBoolean(L, "gamestoreByModules", true); boolean[ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS] = getGlobalBoolean(L, "onlyInvitedCanMoveHouseItems", true); boolean[PUSH_WHEN_ATTACKING] = getGlobalBoolean(L, "pushWhenAttacking", false); diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index 25a3ca135d7..a3147277390 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -164,7 +164,7 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); g_game.addAccountHistory(id_, historyOffer); - db.executeQuery(query.str()); + return db.executeQuery(query.str()); } /******************************************************************************* diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp new file mode 100644 index 00000000000..08ea02faf03 --- /dev/null +++ b/src/creatures/players/store/store.cpp @@ -0,0 +1,760 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (C) 2021 OpenTibiaBR + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "otpch.h" + +#include "game/game.h" +#include "utils/pugicast.h" +#include "creatures/players/store/store.hpp" + +extern Game g_game; + +const std::unordered_map CoinTypeMap = { + {"coin", COIN_TYPE_DEFAULT}, + {"transferable", COIN_TYPE_TRANSFERABLE}, + {"tournament", COIN_TYPE_TOURNAMENT} +}; +const std::unordered_map OfferStatesMap = { + {"none", OFFER_STATE_NONE}, + {"new", OFFER_STATE_NEW}, + {"sale", OFFER_STATE_SALE}, + {"timed", OFFER_STATE_TIMED} +}; +const std::unordered_map OfferTypesMap = { + {"none", OFFER_TYPE_NONE}, + {"item", OFFER_TYPE_ITEM}, + {"stackeable", OFFER_TYPE_STACKABLE}, + {"outfit", OFFER_TYPE_OUTFIT}, + {"outfitaddon", OFFER_TYPE_OUTFIT_ADDON}, + {"mount", OFFER_TYPE_MOUNT}, + {"namechange", OFFER_TYPE_NAME_CHANGE}, + {"sexchange", OFFER_TYPE_SEX_CHANGE}, + {"promotion", OFFER_TYPE_PROMOTION}, + {"house", OFFER_TYPE_HOUSE}, + {"expboost", OFFER_TYPE_EXP_BOOST}, + {"preyslot", OFFER_TYPE_PREY_SLOT}, + {"preybonus", OFFER_TYPE_PREY_BONUS}, + {"temple", OFFER_TYPE_TEMPLE}, + {"blessing", OFFER_TYPE_BLESSINGS}, + {"premium", OFFER_TYPE_PREMIUM}, + {"pouch", OFFER_TYPE_POUCH}, + {"allblessing", OFFER_TYPE_ALL_BLESSINGS}, + {"reward", OFFER_TYPE_INSTANT_REWARD_ACCESS}, + {"training", OFFER_TYPE_TRAINING}, + {"charmexpansion", OFFER_TYPE_CHARM_EXPANSION}, + {"charmpoints", OFFER_TYPE_CHARM_POINTS}, + {"multiitems", OFFER_TYPE_MULTI_ITEMS}, + {"fragremove", OFFER_TYPE_FRAG_REMOVE}, + {"skullremove", OFFER_TYPE_SKULL_REMOVE}, + {"recoverykey", OFFER_TYPE_RECOVERY_KEY}, +}; + +const std::unordered_map OfferBuyTypesMap = { + {"none", OFFER_BUY_TYPE_OTHERS}, + {"offername", OFFER_BUY_TYPE_NAMECHANGE}, + {"teste", OFFER_BUY_TYPE_TESTE} +}; + +const std::unordered_map OfferSkullMap = { + {"none", SKULL_NONE}, + {"red", SKULL_RED}, + {"black", SKULL_BLACK} +}; + + +bool Store::isValidType(OfferTypes_t type) { + auto it = std::find_if(OfferTypesMap.begin(), OfferTypesMap.end(), [type](std::pair const& pair) { + return pair.second == type; + }); + + return it != OfferTypesMap.end(); +} + +/*bool Store::loadStore(const FileName& storeFileName) { + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(storeFileName.GetFullPath().mb_str()); + if(!result) { + printXMLError("[Store::loadStore] - ", storeFileName.GetFullName(), result); + return false; + } + + pugi::xml_node storeNode = doc.child("store"); + if(!storeNode) { + printXMLError("[Store::loadStore] - ", storeFileName.GetFullName(), result); + return false; + } + + loadFromXML(false); + return true; +}*/ + +bool Store::loadFromXML(bool /* reloading */) { + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/store.xml"); + if (!result) { + printXMLError("[Store::loadFromXML] - ", "data/XML/store.xml", result); + return false; + } + + loaded = true; + for (auto baseNode : doc.child("store").children()) { + pugi::xml_attribute storeAttribute; + // Load store category + if (strcasecmp(baseNode.name(), "category") == 0) { + // Check if the tag 'name' exist "" + pugi::xml_attribute categoryName = baseNode.attribute("name"); + if (!categoryName) { + SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for 'category' entry"); + continue; + } + + loadCategory(baseNode, categoryName); + // Load store home + } else if (strcasecmp(baseNode.name(), "home") == 0) { + loadHome(baseNode); + // Load store offers + } else if (strcasecmp(baseNode.name(), "offer") == 0) { + // Check if the tag 'name' exist "" + pugi::xml_attribute offerName = baseNode.attribute("name"); + if (!offerName) { + SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for 'offer' entry"); + continue; + } + + loadOffer(baseNode, storeAttribute, offerName); + } + } + return true; +} + +bool Store::loadCategory(pugi::xml_node node, pugi::xml_attribute name) { + std::vector offersName; + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "subcategory") == 0) { + offersName.push_back(childNode.attribute("name").as_string()); + } + } + + categories.emplace_back( + name.value(), + offersName, + node.attribute("icon").as_string(), + node.attribute("rookgaard").as_bool(true) + ); + + offercount++; + + categories.shrink_to_fit(); + return true; +} + +bool Store::loadHome(pugi::xml_node node) { + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "offer") == 0) { + home.offers.push_back(childNode.attribute("name").as_string()); + } else if (strcasecmp(childNode.name(), "banner") == 0) { + home.banners.push_back(childNode.attribute("image").as_string()); + } + } + return true; +} + +bool Store::loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, pugi::xml_attribute attributeName) { + // Make store offers + std::string name = attributeName.value(); + + auto result = storeOffers.emplace( + std::piecewise_construct, + std::forward_as_tuple(name), + std::forward_as_tuple(name) + ); + + if (!result.second) { + SPDLOG_WARN("[Store::loadStore] - Duplicate category offer by name: '{}' ignored'", name); + return false; + } + + offercount++; + + // Editing store offers + StoreOffers& offers = result.first->second; + attributeName = node.attribute("description"); + if (attributeName) { + offers.description = attributeName.value(); + } + + attributeName = node.attribute("icon"); + if (attributeName) { + offers.icon = attributeName.value(); + } + + attributeName = node.attribute("rookgaard"); + if (attributeName) { + offers.rookgaard = attributeName.as_bool(); + } + + attributeName = node.attribute("state"); + if (attributeName) { + auto parseState = OfferStatesMap.find(attributeName.value()); + if (parseState != OfferStatesMap.end()) { + offers.state = parseState->second; + } + } + + if (offers.state == OFFER_STATE_SALE) { + saleoffer = true; + } else if (offers.state == OFFER_STATE_NEW) { + newoffer = true; + } + + attributeName = node.attribute("parent"); + if (attributeName) { + offers.parent = attributeName.value(); + } + + // Getting the offers + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "offers") == 0) { + if (!(storeAttribute = childNode.attribute("name"))) { + SPDLOG_WARN("[Store::loadStore] - Missing 'name' attribute in 'offers' entry"); + continue; + } + + name = storeAttribute.value(); + uint32_t id = 0; + pugi::xml_attribute childNodeName = childNode.attribute("id"); + if (childNodeName) { + id = pugi::cast(childNodeName.value()); + } else { + runningid++; + id = runningid; + } + + auto resultOffer = offers.offers.emplace( + std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple(id, name) + ); + + if (!resultOffer.second) { + SPDLOG_WARN("[Store::loadStore] - Duplicate offer by name: '{}'", node.value()); + continue; + } + + StoreOffer& offer = resultOffer.first->second; + childNodeName = childNode.attribute("price"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need price", offer.name); + continue; + } + offer.price = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("count"); + if (childNodeName) { + offer.count = pugi::cast(childNodeName.value()); + } + + childNodeName = childNode.attribute("icon"); + if (childNodeName) { + offer.icon = childNodeName.value(); + } + + childNodeName = childNode.attribute("description"); + if (childNodeName) { + offer.description = childNodeName.value(); + replaceString(offer.description, "
  • ", "•"); + } + + childNodeName = childNode.attribute("type"); + if (childNodeName) { + auto parseType = OfferTypesMap.find(childNodeName.value()); + if (parseType != OfferTypesMap.end()) { + offer.type = parseType->second; + } + } + + childNodeName = childNode.attribute("disabled"); + if (childNodeName) { + offer.disabled = childNodeName.as_bool(); + } + + if (offer.type == OFFER_TYPE_OUTFIT || offer.type == OFFER_TYPE_OUTFIT_ADDON) { + childNodeName = childNode.attribute("female"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need female outfit", offer.name); + continue; + } + offer.female = pugi::cast(childNodeName.value()); + childNodeName = childNode.attribute("male"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need male outfit", offer.name); + continue; + } + offer.male = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("addon"); + if (!childNodeName) { + offer.addon = 0; + } else { + offer.addon = pugi::cast(childNodeName.value()); + } + } else if (offer.type == OFFER_TYPE_BLESSINGS) { + childNodeName = childNode.attribute("blessid"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need bless id", offer.name); + continue; + } + offer.blessid = pugi::cast(childNodeName.value()); + } else if (offer.type == OFFER_TYPE_ITEM || offer.type == OFFER_TYPE_STACKABLE || + offer.type == OFFER_TYPE_HOUSE || offer.type == OFFER_TYPE_TRAINING || + offer.type == OFFER_TYPE_POUCH) { + + childNodeName = childNode.attribute("itemid"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need itemid", offer.name); + continue; + } + offer.itemId = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("charges"); + if (childNodeName) { + offer.charges = pugi::cast(childNodeName.value()); + } + + childNodeName = childNode.attribute("actionid"); + if (childNodeName) { + offer.actionid = pugi::cast(childNodeName.value()); + } + + if (offer.count == 0) { + offer.count = 1; + } + } else if (offer.type == OFFER_TYPE_MULTI_ITEMS) { + childNodeName = childNode.attribute("items"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need items", offer.name); + continue; + } + + StringVector itemsList = explodeString(childNodeName.value(), ";"); + for (const std::string& itemsInfo : itemsList) { + StringVector info = explodeString(itemsInfo, ","); + if (info.size() == 2) { + uint16_t itemid = std::stoi(info[0]); + uint16_t item_count = std::stoi(info[1]); + offer.itemList[itemid] = item_count; + } + } + + } else if (offer.type == OFFER_TYPE_SKULL_REMOVE) { + childNodeName = childNode.attribute("skull"); + if (childNodeName) { + auto parseSkull = OfferSkullMap.find(childNodeName.value()); + if (parseSkull != OfferSkullMap.end()) { + offer.skull = parseSkull->second; + } + } + } + + childNodeName = childNode.attribute("state"); + if (childNodeName) { + auto parseState = OfferStatesMap.find(childNodeName.value()); + if (parseState != OfferStatesMap.end()) { + offer.state = parseState->second; + } + } + + if (offer.state == OFFER_STATE_SALE) { + saleoffer = true; + childNodeName = childNode.attribute("validUntil"); + if (childNodeName) { + offer.validUntil = pugi::cast(childNodeName.value()); + } + childNodeName = childNode.attribute("basePrice"); + if (childNodeName) { + offer.basePrice = pugi::cast(childNodeName.value()); + } + } else if (offer.state == OFFER_STATE_NEW) { + newoffer = true; + } + + childNodeName = childNode.attribute("coinType"); + if (childNodeName) { + auto parseCoin = CoinTypeMap.find(childNodeName.value()); + if (parseCoin != CoinTypeMap.end()) { + offer.coinType = parseCoin->second; + } + } + + childNodeName = childNode.attribute("buyType"); + if (childNodeName) { + auto parsebtpe = OfferBuyTypesMap.find(childNodeName.value()); + if (parsebtpe != OfferBuyTypesMap.end()) { + offer.buyType = parsebtpe->second; + } + } + + offer.rookgaard = offers.rookgaard; + childNodeName = childNode.attribute("rookgaard"); + if (childNodeName) { + offer.rookgaard = childNodeName.as_bool(); + } + + } + } + return true; +} + +bool Store::reload() { + categories.clear(); + storeOffers.clear(); + home.offers.clear(); + home.banners.clear(); + runningid = beginid; + loaded = false; + offercount = 0; + + newoffer = false; + saleoffer = false; + + return loadFromXML(true); +} + +std::vector Store::getStoreOffers() { + std::vector filter; + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + filter.push_back(offers); + } + + return filter; +} + +std::vector Store::getStoreOffer(StoreOffers* offers) { + std::vector filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + filter.push_back(offer); + } + } + return filter; +} + +StoreOffer* Store::getStoreOfferByName(std::string name) { + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + for (auto& info2 : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info2.first); + if (offer && strcasecmp(offer->getName().c_str(), name.c_str()) == 0) { + return offer; + } + } + } + return nullptr; +} + +std::vector Store::getHomeOffers() { + std::vector filter; + + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { + StoreOffer* oferta = getStoreOfferByName((*off)); + if (oferta) { + bool hasDec = false; + for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; ++off2) { + if ((*off2)->getName() == oferta->getName()){ + hasDec = true; + break; + } + } + + if (!hasDec) { + filter.emplace_back(oferta); + } + } + } + + return filter; +} + + +std::map> Store::getHomeOffersOrganized() { + std::map> filter; + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { + StoreOffer* oferta = getStoreOfferByName((*off)); + if (oferta) { + std::string name = oferta->getName(); + filter[name].emplace_back(oferta); + } + } + + + return filter; +} + +std::map> Store::getStoreOrganizedByName(StoreOffers* offers) { + std::map> filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + std::string name = offer->getName(); + filter[name].emplace_back(offer); + } + } + + + return filter; +} + +StoreOffer* StoreOffers::getOfferByID(uint32_t id) { + auto it = offers.find(id); + if (it == offers.end()) { + return nullptr; + } + + return &it->second; +} + +std::string StoreOffer::getDisabledReason(Player* player) { + + uint16_t outfitLookType = player->getSex() == PLAYERSEX_FEMALE ? female : male; + + std::string disabledReason; + if (disabled) { + disabledReason = "This offer is disabled."; + } else if (type == OFFER_TYPE_POUCH) { + Item* item = g_game.findItemOfType(player, 26377, true, -1); + if (item) + disabledReason = "You already have Loot Pouch."; + } else if (type == OFFER_TYPE_BLESSINGS) { + if (player->hasBlessing(blessid)) + disabledReason = "You already have this Bless."; + } else if (type == OFFER_TYPE_ALL_BLESSINGS) { + uint8_t count = 0; + uint8_t limitBless = 0; + uint8_t minBless = (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : BLESS_FIRST); + uint8_t maxBless = BLESS_LAST; + for (int i = minBless; i <= maxBless; ++i) { + limitBless++; + if (player->hasBlessing(i)) { + count++; + } + } + + if (count >= limitBless) + disabledReason = "You already have all Blessings."; + + } else if (type == OFFER_TYPE_OUTFIT && player->canWear(outfitLookType, addon)) { + disabledReason = "You already have this outfit."; + } else if (type == OFFER_TYPE_OUTFIT_ADDON) { + if (player->canWear(outfitLookType, 0)) { + if (player->canWear(outfitLookType, addon)) { + disabledReason = "You already have this addon."; + } + } + } else if (type == OFFER_TYPE_MOUNT) { + Mount* mount = g_game.mounts.getMountByID(id); + if (!mount) { + disabledReason = "Mount not found"; + } else if (player->hasMount(mount)) { + disabledReason = "You already have this mount."; + } + + } else if (type == OFFER_TYPE_PROMOTION) { + disabledReason = "This offer has disabled."; + } else if (type == OFFER_TYPE_PREY_SLOT) { + //if (player->isUnlockedPrey(2)) { + disabledReason = "You already have 3 slots released."; + //} + + } else if (type == OFFER_TYPE_EXP_BOOST) { + int32_t value1; + player->getStorageValue(51052, value1); + int32_t value2; + player->getStorageValue(51053, value2); + if (value1 >= 6) { + disabledReason = "Can be purchased up to 5 times between 2 server saves."; + } else if ((OS_TIME(nullptr) - value2) < (1*60*60)) { + disabledReason = "You still have active boost."; + } + } else if (type == OFFER_TYPE_CHARM_EXPANSION) { + if (player->hasCharmExpansion()) { + disabledReason = "You have charm expansion"; + } + } else if (type == OFFER_TYPE_SKULL_REMOVE) { + if (player->getSkull() != skull) { + disabledReason = "This offer is disabled for you"; + } + } else if (type == OFFER_TYPE_FRAG_REMOVE) { + if (player->unjustifiedKills.empty()) { + disabledReason = "You have no frag to remove."; + } + } else if (type == OFFER_TYPE_RECOVERY_KEY) { + int32_t value; + player->getAccountStorageValue(1, value); + if (value > OS_TIME(nullptr)) { + disabledReason = "You recently generated an RK."; + } + } + + if (player->getVocation()->getId() == 0 && !rookgaard) { + disabledReason = "This offer is deactivated."; + } + + if (player->getCoinBalance(coinType) - getPrice(player) < 0) { + if (coinType == COIN_TYPE_TOURNAMENT) { + disabledReason = "You don't have tournament coins."; + } else { + disabledReason = "You don't have coins."; + } + } + + return disabledReason; +} + +Mount* StoreOffer::getMount() { + return g_game.mounts.getMountByID(id); +} + +uint8_t Store::convertType(OfferTypes_t type) { + uint8_t offertype = 0; + if (type == OFFER_TYPE_POUCH || type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE || type == OFFER_TYPE_HOUSE || type == OFFER_TYPE_TRAINING) { + offertype = 3; + } else if (type == OFFER_TYPE_OUTFIT || type == OFFER_TYPE_OUTFIT_ADDON) { + offertype = 2; + } else if (type == OFFER_TYPE_MOUNT) { + offertype = 1; + } + + return offertype; +} + +StoreOffers* Store::getOfferByName(std::string name) { + // Check the categories first + for (auto offer = categories.begin(), end = categories.end(); offer != end; ++offer) { + if (strcasecmp((*offer).name.c_str(), name.c_str()) == 0) { + return getOfferByName((*offer).subcategory[0]); + } + } + + // Go to offers + auto it = storeOffers.find(name); + if (it == storeOffers.end()) { + // Clicking on the banner too calls an offer + // SPDLOG_WARN("[Store::getOfferByName] Offer '{}' not found", name); + return nullptr; + } + return &it->second; +} + +StoreOffers* Store::getOffersByOfferId(uint32_t id) { + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr){ + continue; + } + + StoreOffer* offer = offers->getOfferByID(id); + if (offer != nullptr && offer->getId() == id) { + return offers; + } + } + + return nullptr; +} + +StoreOffer* Store::getOfferById(uint32_t id) { + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr){ + continue; + } + + StoreOffer* offer = offers->getOfferByID(id); + if (offer != nullptr && offer->getId() == id) { + return offer; + } + } + + return nullptr; +} + +uint32_t StoreOffer::getPrice(Player* player) { + uint32_t newPrice = 0; + if (player && type == OFFER_TYPE_EXP_BOOST) { + int32_t value1; + player->getStorageValue(51052, value1); + uint32_t xpBoostPrice = getExpBoostPrice(value1); + if (xpBoostPrice > 0) { + if (player->isPremium()) { + xpBoostPrice *= 0.90; + } + + } + + return xpBoostPrice; + } + + if (state == OFFER_STATE_SALE) { + time_t mytime; + mytime = time(NULL); + struct tm tm = *localtime(&mytime); + int32_t daySub = validUntil - tm.tm_mday; + if (daySub < 0) { + newPrice = basePrice; + } + } + + uint32_t p_prince = price; + if (player && player->isPremium()) { + newPrice *= 0.90; + p_prince *= 0.90; + } + + return newPrice > 0 ? newPrice : p_prince; +} + +uint16_t StoreOffer::getCount(bool inBuy) { + if (!inBuy && type == OFFER_TYPE_PREMIUM) { + return 1; + } + + return count; +} + +std::string StoreOffer::getDescription(Player* player /*= nullptr */) { + if (!player) { + return description; + } + + std::string showDesc = description; + if (showDesc.empty()) { + showDesc = description; + } + + if ((type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE) && showDesc.empty()) { + Item* virtualItem = Item::CreateItem(itemId, count); + if (virtualItem) { + showDesc = "You see " + virtualItem->getDescription(-2); + delete virtualItem; + } + } + + return showDesc; +} diff --git a/src/game/gamestore.hpp b/src/creatures/players/store/store.hpp similarity index 86% rename from src/game/gamestore.hpp rename to src/creatures/players/store/store.hpp index eb3fc1647b6..f1ff535536c 100644 --- a/src/game/gamestore.hpp +++ b/src/creatures/players/store/store.hpp @@ -15,13 +15,10 @@ * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ +*/ -#ifndef C40D8054_B234_4A7F_9ACA_13C27DBFBB09 -#define C40D8054_B234_4A7F_9ACA_13C27DBFBB09 - -#ifndef SRC_GAME_GAMESTORE_H_ -#define SRC_GAME_GAMESTORE_H_ +#ifndef SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ +#define SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ #include @@ -38,24 +35,24 @@ class StoreOffers; class StoreOffer; enum OfferTypes_t : uint8_t { - OFFER_TYPE_NONE = 0, // (this will disable offer) + OFFER_TYPE_NONE = 0, // This will disable offer OFFER_TYPE_ITEM = 1, OFFER_TYPE_STACKABLE = 2, OFFER_TYPE_OUTFIT = 3, OFFER_TYPE_OUTFIT_ADDON = 4, OFFER_TYPE_MOUNT = 5, - OFFER_TYPE_NAMECHANGE = 6, - OFFER_TYPE_SEXCHANGE = 7, + OFFER_TYPE_NAME_CHANGE = 6, + OFFER_TYPE_SEX_CHANGE = 7, OFFER_TYPE_PROMOTION = 8, OFFER_TYPE_HOUSE = 9, - OFFER_TYPE_EXPBOOST = 10, - OFFER_TYPE_PREYSLOT = 11, - OFFER_TYPE_PREYBONUS = 12, + OFFER_TYPE_EXP_BOOST = 10, + OFFER_TYPE_PREY_SLOT = 11, + OFFER_TYPE_PREY_BONUS = 12, OFFER_TYPE_TEMPLE = 13, OFFER_TYPE_BLESSINGS = 14, OFFER_TYPE_PREMIUM = 15, OFFER_TYPE_POUCH = 16, - OFFER_TYPE_ALLBLESSINGS = 17, + OFFER_TYPE_ALL_BLESSINGS = 17, OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, OFFER_TYPE_TRAINING = 19, OFFER_TYPE_CHARM_EXPANSION = 20, @@ -64,7 +61,7 @@ enum OfferTypes_t : uint8_t { OFFER_TYPE_VIP = 24, OFFER_TYPE_FRAG_REMOVE = 25, OFFER_TYPE_SKULL_REMOVE = 26, - OFFER_TYPE_RECOVERYKEY = 27, + OFFER_TYPE_RECOVERY_KEY = 27, }; enum OfferBuyTypes_t : uint8_t { @@ -121,9 +118,13 @@ struct StoreHome { std::vector banners; }; -class GameStore { +class Store { public: - bool loadFromXml(bool reloading = false); + //bool loadStore(const FileName& identifier); + bool loadFromXML(bool reloading = false); + bool loadCategory(pugi::xml_node node, pugi::xml_attribute name); + bool loadHome(pugi::xml_node node); + bool loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, pugi::xml_attribute attributeName); bool reload(); bool hasNewOffer() { @@ -172,7 +173,8 @@ class GameStore { bool loaded = false; private: - // Como mount usa como base uint16, podemos setar os ids das ofertas (que nao foram identificada) como o valor maximo do uint16 + 1 + // As mount uses uint16 as base, we can set the ids of the offers (which were not identified) + // As the maximum value of uint16 + 1 uint16_t beginid = std::numeric_limits::max(); uint32_t runningid = beginid; uint16_t offercount = 0; @@ -210,7 +212,7 @@ class StoreOffers { StoreOffer* getOfferByID(uint32_t id); protected: - friend class GameStore; + friend class Store; friend class StoreOffer; std::map offers; @@ -256,7 +258,7 @@ class StoreOffer { return blessid; } uint16_t getItemType() { - return itemtype; + return itemId; } uint16_t getCharges() { return charges; @@ -318,7 +320,7 @@ class StoreOffer { Mount* getMount(); protected: - friend class GameStore; + friend class Store; friend class StoreOffers; private: @@ -327,17 +329,16 @@ class StoreOffer { std::map itemList; std::string description = ""; - std::string description12; std::string icon = ""; OfferStates_t state = OFFER_STATE_NONE; CoinType_t coinType = COIN_TYPE_DEFAULT; OfferBuyTypes_t buyType = OFFER_BUY_TYPE_OTHERS; uint16_t count = 1; - uint32_t price = 150; // default price -- evitando que entre oferta sem valor - uint32_t basePrice = 0; // default price -- evitando que entre oferta sem valor + uint32_t price = 0; // Default price (This preventing valueless offers from entering) + uint32_t basePrice = 0; // Default price (This preventing valueless offers from entering) uint32_t validUntil = 0; uint16_t blessid = 0; - uint16_t itemtype = 0; + uint16_t itemId = 0; uint16_t charges = 1; uint8_t addon = 0; uint16_t male; @@ -351,5 +352,3 @@ class StoreOffer { }; #endif - -#endif diff --git a/src/game/game.cpp b/src/game/game.cpp index e243f393891..3e89f20b6c5 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -29,7 +29,6 @@ #include "database/databasetasks.h" #include "lua/creature/events.h" #include "game/game.h" -#include "game/gamestore.hpp" #include "lua/global/globalevent.h" #include "io/iologindata.h" #include "io/iomarket.h" @@ -42,6 +41,7 @@ #include "lua/creature/talkaction.h" #include "items/weapons/weapons.h" #include "lua/scripts/scripts.h" +#include "creatures/players/store/store.hpp" #include "lua/modules/modules.h" #include "creatures/players/imbuements/imbuements.h" #include "creatures/players/account/account.hpp" @@ -65,7 +65,7 @@ extern Weapons* g_weapons; extern Scripts* g_scripts; extern Modules* g_modules; extern Imbuements* g_imbuements; -extern GameStore g_gameStore; +extern Store g_store; Game::Game() { @@ -8368,7 +8368,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: OfferTypes_t offerType = thisOffer->getOfferType(); - if (!g_gameStore.isValidType(offerType)) { + if (!g_store.isValidType(offerType)) { player->sendStoreError(STORE_ERROR_INFORMATION, "This offer is unavailable."); return; } @@ -8396,7 +8396,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: int32_t offerPrice = thisOffer->getPrice(player) * -1; std::stringstream returnmessage; - if (offerType == OFFER_TYPE_NAMECHANGE) { + if (offerType == OFFER_TYPE_NAME_CHANGE) { std::ostringstream query; std::string newName = param; trimString(newName); @@ -8694,7 +8694,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: successfully = true; returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; - } else if (offerType == OFFER_TYPE_ALLBLESSINGS) { + } else if (offerType == OFFER_TYPE_ALL_BLESSINGS) { uint8_t count = 0; uint8_t limitBless = 0; @@ -8766,7 +8766,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: returnmessage << "You've successfully bought the " << mount->name <<" Mount."; successfully = true; - } else if (offerType == OFFER_TYPE_SEXCHANGE) { + } else if (offerType == OFFER_TYPE_SEX_CHANGE) { Outfit_t outfit = player->getCurrentOutfit(); if (player->getSex() == PLAYERSEX_FEMALE) { player->setSex(PLAYERSEX_MALE); @@ -8783,7 +8783,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: returnmessage << "You have purchased " << thisOffer->getName() << " for " << thisOffer->getPrice(player) <<" coins"; successfully = true; - } else if (offerType == OFFER_TYPE_EXPBOOST) { + } else if (offerType == OFFER_TYPE_EXP_BOOST) { uint16_t currentExpBoostTime = player->getExpBoostStamina(); player->setStoreXpBoost(50); @@ -8885,7 +8885,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: player->setSkull(SKULL_NONE); successfully = true; - } else if (offerType == OFFER_TYPE_RECOVERYKEY) { + } else if (offerType == OFFER_TYPE_RECOVERY_KEY) { std::ostringstream newkey; newkey << generateRK(4) << "-" << generateRK(4) << "-" << generateRK(4) << "-" << generateRK(4); diff --git a/src/game/game.h b/src/game/game.h index 13082112b93..fd26d815ff7 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -25,9 +25,7 @@ #include "creatures/players/account/account.hpp" #include "creatures/combat/combat.h" #include "items/containers/container.h" -#include "game/gamestore.hpp" #include "creatures/players/grouping/groups.h" -#include "game/gamestore.hpp" #include "io/iobestiary.h" #include "items/item.h" #include "map/map.h" @@ -35,6 +33,7 @@ #include "movement/position.h" #include "creatures/players/player.h" #include "lua/creature/raids.h" +#include "creatures/players/store/store.hpp" #include "creatures/players/grouping/team_finder.hpp" #include "utils/wildcardtree.h" diff --git a/src/game/gamestore.cpp b/src/game/gamestore.cpp deleted file mode 100644 index ce9fffcd855..00000000000 --- a/src/game/gamestore.cpp +++ /dev/null @@ -1,750 +0,0 @@ -/** - * Canary - A free and open-source MMORPG server emulator - * Copyright (C) 2021 OpenTibiaBR - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "otpch.h" - -#include "config/configmanager.h" -#include "game/game.h" -#include "game/gamestore.hpp" -#include "utils/pugicast.h" - -extern Game g_game; -extern ConfigManager g_config; - -const std::unordered_map CoinTypeMap = { - {"coin", COIN_TYPE_DEFAULT}, - {"transferable", COIN_TYPE_TRANSFERABLE}, - {"tournament", COIN_TYPE_TOURNAMENT} -}; -const std::unordered_map OfferStatesMap = { - {"none", OFFER_STATE_NONE}, - {"new", OFFER_STATE_NEW}, - {"sale", OFFER_STATE_SALE}, - {"timed", OFFER_STATE_TIMED} -}; -const std::unordered_map OfferTypesMap = { - {"none", OFFER_TYPE_NONE}, - {"item", OFFER_TYPE_ITEM}, - {"stackeable", OFFER_TYPE_STACKABLE}, - {"outfit", OFFER_TYPE_OUTFIT}, - {"outfit addon", OFFER_TYPE_OUTFIT_ADDON}, - {"mount", OFFER_TYPE_MOUNT}, - {"namechange", OFFER_TYPE_NAMECHANGE}, - {"sexchange", OFFER_TYPE_SEXCHANGE}, - {"promotion", OFFER_TYPE_PROMOTION}, - {"house", OFFER_TYPE_HOUSE}, - {"expboost", OFFER_TYPE_EXPBOOST}, - {"preyslot", OFFER_TYPE_PREYSLOT}, - {"preybonus", OFFER_TYPE_PREYBONUS}, - {"temple", OFFER_TYPE_TEMPLE}, - {"blessing", OFFER_TYPE_BLESSINGS}, - {"premium", OFFER_TYPE_PREMIUM}, - {"pouch", OFFER_TYPE_POUCH}, - {"allblessing", OFFER_TYPE_ALLBLESSINGS}, - {"reward", OFFER_TYPE_INSTANT_REWARD_ACCESS}, - {"training", OFFER_TYPE_TRAINING}, - {"charmexpansion", OFFER_TYPE_CHARM_EXPANSION}, - {"charmpoints", OFFER_TYPE_CHARM_POINTS}, - {"multiitems", OFFER_TYPE_MULTI_ITEMS}, - {"fragremove", OFFER_TYPE_FRAG_REMOVE}, - {"skullremove", OFFER_TYPE_SKULL_REMOVE}, - {"recoverykey", OFFER_TYPE_RECOVERYKEY}, -}; - -const std::unordered_map OfferBuyTypesMap = { - {"none", OFFER_BUY_TYPE_OTHERS}, - {"offername", OFFER_BUY_TYPE_NAMECHANGE}, - {"teste", OFFER_BUY_TYPE_TESTE} -}; - -const std::unordered_map OfferSkullMap = { - {"none", SKULL_NONE}, - {"red", SKULL_RED}, - {"black", SKULL_BLACK} -}; - - -bool GameStore::isValidType(OfferTypes_t type) -{ - auto it = std::find_if(OfferTypesMap.begin(), OfferTypesMap.end(), [type](std::pair const& pair) { - return pair.second == type; - }); - - return it != OfferTypesMap.end(); -} - -bool GameStore::loadFromXml(bool /* reloading */) { - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file("data/XML/gamestore.xml"); - if (!result) { - printXMLError("Error - GameStore::loadFromXml", "data/XML/gamestore.xml", result); - return false; - } - - loaded = true; - for (auto baseNode : doc.child("store").children()) { - pugi::xml_attribute attr; - // StoreCategory - if (strcasecmp(baseNode.name(), "category") == 0) { - pugi::xml_attribute name = baseNode.attribute("name"); - if (!name) { - std::cout << "[Warning - GameStore::loadFromXml] Missing name for Category entry" << std::endl; - continue; - } - - std::vector offersName; - for (auto childNode : baseNode.children()) { - if (strcasecmp(childNode.name(), "subcategory") == 0) { - offersName.push_back( childNode.attribute("name").as_string() ); - } - } - - categories.emplace_back( - name.value(), - offersName, - baseNode.attribute("icon").as_string(), - baseNode.attribute("rookgaard").as_bool(true) - ); - - offercount++; - - categories.shrink_to_fit(); - // Home - } else if (strcasecmp(baseNode.name(), "home") == 0) { - for (auto childNode : baseNode.children()) { - if (strcasecmp(childNode.name(), "offer") == 0) { - home.offers.push_back( childNode.attribute("name").as_string() ); - } else if (strcasecmp(childNode.name(), "banner") == 0) { - home.banners.push_back( childNode.attribute("image").as_string() ); - } - } - - // Offers - } else if (strcasecmp(baseNode.name(), "offers") == 0) { - pugi::xml_attribute attributeName = baseNode.attribute("name"); - if (!attributeName) { - std::cout << "[Warning - GameStore::loadFromXml] Missing name for Offers entry" << std::endl; - continue; - } - - // make StoreOffers - std::string name = attributeName.value(); - - auto res = storeOffers.emplace(std::piecewise_construct, - std::forward_as_tuple(name), - std::forward_as_tuple(name) - ); - - if (!res.second) { - std::cout << "[Warning - GameStore::loadFromXml] Duplicate Category Offer by name: '" << name << "' [ignored]" << std::endl; - continue; - } - - offercount++; - - // editando StoreOffers na memoria - StoreOffers& offers = res.first->second; - // reaproveitando variavel - attributeName = baseNode.attribute("description"); - if (attributeName) { - offers.description = attributeName.value(); - } - - attributeName = baseNode.attribute("icon"); - if (attributeName) { - offers.icon = attributeName.value(); - } - - attributeName = baseNode.attribute("rookgaard"); - if (attributeName) { - offers.rookgaard = attributeName.as_bool(); - } - - attributeName = baseNode.attribute("state"); - if (attributeName) { - auto parseState = OfferStatesMap.find(attributeName.value()); - if (parseState != OfferStatesMap.end()) { - offers.state = parseState->second; - } - } - - if (offers.state == OFFER_STATE_SALE) { - saleoffer = true; - } else if (offers.state == OFFER_STATE_NEW) { - newoffer = true; - } - - attributeName = baseNode.attribute("parent"); - if (attributeName) { - offers.parent = attributeName.value(); - } - - // pegando as ofertas - for (auto childNode : baseNode.children()) { - if (strcasecmp(childNode.name(), "offer") == 0) { - if (!(attr = childNode.attribute("name"))) { - std::cout << "[Warning - GameStore::loadFromXml] Missing key attribute in GameStore::StoreOffers::StoreOffer name" << std::endl; - continue; - } - - name = attr.value(); - uint32_t id = 0; - pugi::xml_attribute childNodeName = childNode.attribute("id"); - if (childNodeName) { - id = pugi::cast(childNodeName.value()); - } else { - runningid++; - id = runningid; - } - - auto resultOffer = offers.offers.emplace(std::piecewise_construct, - std::forward_as_tuple(id), - std::forward_as_tuple(id, name )); - - if (!resultOffer.second) { - std::cout << "[Warning - GameStore::loadFromXml] Duplicate Store Offer by name: '" << attr.value() << "'" << std::endl; - continue; - } - - StoreOffer& offer = resultOffer.first->second; - childNodeName = childNode.attribute("price"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need price" << std::endl; - continue; - } - offer.price = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("count"); - if (childNodeName) { - offer.count = pugi::cast(childNodeName.value()); - } - - childNodeName = childNode.attribute("icon"); - if (childNodeName) { - offer.icon = childNodeName.value(); - } - - childNodeName = childNode.attribute("description"); - if (childNodeName) { - offer.description = childNodeName.value(); - } - - childNodeName = childNode.attribute("description12"); - if (childNodeName) { - offer.description12 = childNodeName.value(); - replaceString(offer.description12, "
  • ", "•"); - } - - childNodeName = childNode.attribute("type"); - if (childNodeName) { - auto parseType = OfferTypesMap.find(childNodeName.value()); - if (parseType != OfferTypesMap.end()) { - offer.type = parseType->second; - } - } - - - childNodeName = childNode.attribute("disabled"); - if (childNodeName) { - offer.disabled = childNodeName.as_bool(); - } - - if (offer.type == OFFER_TYPE_OUTFIT || offer.type == OFFER_TYPE_OUTFIT_ADDON) { - childNodeName = childNode.attribute("female"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need female outfit" << std::endl; - continue; - } - offer.female = pugi::cast(childNodeName.value()); - childNodeName = childNode.attribute("male"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need male outfit" << std::endl; - continue; - } - offer.male = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("addon"); - if (!childNodeName) { - offer.addon = 0; - } else { - offer.addon = pugi::cast(childNodeName.value()); - } - } else if (offer.type == OFFER_TYPE_BLESSINGS) { - childNodeName = childNode.attribute("blessid"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need bless id" << std::endl; - continue; - } - offer.blessid = pugi::cast(childNodeName.value()); - } else if (offer.type == OFFER_TYPE_ITEM || offer.type == OFFER_TYPE_STACKABLE || - offer.type == OFFER_TYPE_HOUSE || offer.type == OFFER_TYPE_TRAINING || - offer.type == OFFER_TYPE_POUCH) { - - childNodeName = childNode.attribute("itemtype"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need itemtype" << std::endl; - continue; - } - offer.itemtype = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("charges"); - if (childNodeName) { - offer.charges = pugi::cast(childNodeName.value()); - } - - childNodeName = childNode.attribute("actionid"); - if (childNodeName) { - offer.actionid = pugi::cast(childNodeName.value()); - } - - if (offer.count == 0) { - offer.count = 1; - } - } else if (offer.type == OFFER_TYPE_MULTI_ITEMS) { - childNodeName = childNode.attribute("items"); - if (!childNodeName) { - std::cout << "[Warning - GameStore::loadFromXml] Store Offer by name: '" << offer.name << "' need items" << std::endl; - continue; - } - - StringVector itemsList = explodeString(childNodeName.value(), ";"); - for (const std::string& itemsInfo : itemsList) { - StringVector info = explodeString(itemsInfo, ","); - if (info.size() == 2) { - uint16_t itemid = std::stoi(info[0]); - uint16_t item_count = std::stoi(info[1]); - offer.itemList[itemid] = item_count; - } - } - - } else if (offer.type == OFFER_TYPE_SKULL_REMOVE) { - childNodeName = childNode.attribute("skull"); - if (childNodeName) { - auto parseSkull = OfferSkullMap.find(childNodeName.value()); - if (parseSkull != OfferSkullMap.end()) { - offer.skull = parseSkull->second; - } - } - } - - childNodeName = childNode.attribute("state"); - if (childNodeName) { - auto parseState = OfferStatesMap.find(childNodeName.value()); - if (parseState != OfferStatesMap.end()) { - offer.state = parseState->second; - } - } - - if (offer.state == OFFER_STATE_SALE) { - saleoffer = true; - childNodeName = childNode.attribute("validUntil"); - if (childNodeName) { - offer.validUntil = pugi::cast(childNodeName.value()); - } - childNodeName = childNode.attribute("basePrice"); - if (childNodeName) { - offer.basePrice = pugi::cast(childNodeName.value()); - } - } else if (offer.state == OFFER_STATE_NEW) { - newoffer = true; - } - - childNodeName = childNode.attribute("coinType"); - if (childNodeName) { - auto parseCoin = CoinTypeMap.find(childNodeName.value()); - if (parseCoin != CoinTypeMap.end()) { - offer.coinType = parseCoin->second; - } - } - - childNodeName = childNode.attribute("buyType"); - if (childNodeName) { - auto parsebtpe = OfferBuyTypesMap.find(childNodeName.value()); - if (parsebtpe != OfferBuyTypesMap.end()) { - offer.buyType = parsebtpe->second; - } - } - - offer.rookgaard = offers.rookgaard; - childNodeName = childNode.attribute("rookgaard"); - if (childNodeName) { - offer.rookgaard = childNodeName.as_bool(); - } - } - } - } - } - - return true; -} - -bool GameStore::reload() { - categories.clear(); - storeOffers.clear(); - home.offers.clear(); - home.banners.clear(); - runningid = beginid; - loaded = false; - offercount = 0; - - newoffer = false; - saleoffer = false; - - return loadFromXml(true); -} - -std::vector GameStore::getStoreOffers() -{ - std::vector filter; - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - filter.push_back(offers); - } - - return filter; -} - -std::vector GameStore::getStoreOffer(StoreOffers* offers) -{ - std::vector filter; - for (auto& info : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info.first); - if (offer) { - filter.push_back(offer); - } - } - return filter; -} - -StoreOffer* GameStore::getStoreOfferByName(std::string name) -{ - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - for (auto& info2 : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info2.first); - if (offer && strcasecmp(offer->getName().c_str(), name.c_str()) == 0) { - return offer; - } - } - } - return nullptr; -} - -std::vector GameStore::getHomeOffers() -{ - std::vector filter; - - for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* oferta = getStoreOfferByName((*off)); - if (oferta) { - bool hasDec = false; - for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; ++off2) { - if ((*off2)->getName() == oferta->getName()){ - hasDec = true; - break; - } - } - - if (!hasDec) { - filter.emplace_back(oferta); - } - } - } - - return filter; -} - - -std::map> GameStore::getHomeOffersOrganized() -{ - std::map> filter; - for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* oferta = getStoreOfferByName((*off)); - if (oferta) { - std::string name = oferta->getName(); - filter[name].emplace_back(oferta); - } - } - - - return filter; -} - -std::map> GameStore::getStoreOrganizedByName(StoreOffers* offers) -{ - std::map> filter; - for (auto& info : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info.first); - if (offer) { - std::string name = offer->getName(); - filter[name].emplace_back(offer); - } - } - - - return filter; -} - -StoreOffer* StoreOffers::getOfferByID(uint32_t id) -{ - auto it = offers.find(id); - if (it == offers.end()) { - return nullptr; - } - - return &it->second; -} - -std::string StoreOffer::getDisabledReason(Player* player) -{ - - uint16_t outfitLookType = player->getSex() == PLAYERSEX_FEMALE ? female : male; - - std::string disabledReason; - if (disabled) { - disabledReason = "This offer is disabled."; - } else if (type == OFFER_TYPE_POUCH) { - Item* item = g_game.findItemOfType(player, 26377, true, -1); - if (item) - disabledReason = "You already have Loot Pouch."; - } else if (type == OFFER_TYPE_BLESSINGS) { - if (player->hasBlessing(blessid)) - disabledReason = "You already have this Bless."; - } else if (type == OFFER_TYPE_ALLBLESSINGS) { - uint8_t count = 0; - uint8_t limitBless = 0; - uint8_t minBless = (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : BLESS_FIRST); - uint8_t maxBless = BLESS_LAST; - for (int i = minBless; i <= maxBless; ++i) { - limitBless++; - if (player->hasBlessing(i)) { - count++; - } - } - - if (count >= limitBless) - disabledReason = "You already have all Blessings."; - - } else if (type == OFFER_TYPE_OUTFIT && player->canWear(outfitLookType, addon)) { - disabledReason = "You already have this outfit."; - } else if (type == OFFER_TYPE_OUTFIT_ADDON) { - if (player->canWear(outfitLookType, 0)) { - if (player->canWear(outfitLookType, addon)) { - disabledReason = "You already have this addon."; - } - } - } else if (type == OFFER_TYPE_MOUNT) { - Mount* mount = g_game.mounts.getMountByID(id); - if (!mount) { - disabledReason = "Mount not found"; - } else if (player->hasMount(mount)) { - disabledReason = "You already have this mount."; - } - - } else if (type == OFFER_TYPE_PROMOTION) { - disabledReason = "This offer has disabled."; - } else if (type == OFFER_TYPE_PREYSLOT) { - //if (player->isUnlockedPrey(2)) { - disabledReason = "You already have 3 slots released."; - //} - - } else if (type == OFFER_TYPE_EXPBOOST) { - int32_t value1; - player->getStorageValue(51052, value1); - int32_t value2; - player->getStorageValue(51053, value2); - if (value1 >= 6) { - disabledReason = "Can be purchased up to 5 times between 2 server saves."; - } else if ((OS_TIME(nullptr) - value2) < (1*60*60)) { - disabledReason = "You still have active boost."; - } - } else if (type == OFFER_TYPE_CHARM_EXPANSION) { - if (player->hasCharmExpansion()) { - disabledReason = "You have charm expansion"; - } - } else if (type == OFFER_TYPE_SKULL_REMOVE) { - if (player->getSkull() != skull) { - disabledReason = "This offer is disabled for you"; - } - } else if (type == OFFER_TYPE_FRAG_REMOVE) { - if (player->unjustifiedKills.empty()) { - disabledReason = "You have no frag to remove."; - } - } else if (type == OFFER_TYPE_RECOVERYKEY) { - int32_t value; - player->getAccountStorageValue(1, value); - if (value > OS_TIME(nullptr)) { - disabledReason = "You recently generated an RK."; - } - } - - if (player->getVocation()->getId() == 0 && !rookgaard) { - disabledReason = "This offer is deactivated."; - } - - if (player->getCoinBalance(coinType) - getPrice(player) < 0) { - if (coinType == COIN_TYPE_TOURNAMENT) { - disabledReason = "You don't have tournament coins."; - } else { - disabledReason = "You don't have coins."; - } - } - - return disabledReason; -} - -Mount* StoreOffer::getMount() -{ - return g_game.mounts.getMountByID(id); -} - -uint8_t GameStore::convertType(OfferTypes_t type) -{ - uint8_t offertype = 0; - if (type == OFFER_TYPE_POUCH || type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE || type == OFFER_TYPE_HOUSE || type == OFFER_TYPE_TRAINING) { - offertype = 3; - } else if (type == OFFER_TYPE_OUTFIT || type == OFFER_TYPE_OUTFIT_ADDON) { - offertype = 2; - } else if (type == OFFER_TYPE_MOUNT) { - offertype = 1; - } - - return offertype; -} - -StoreOffers* GameStore::getOfferByName(std::string name) -{ - // checa primeiro as categorias - for (auto offer = categories.begin(), end = categories.end(); offer != end; ++offer) { - if (strcasecmp((*offer).name.c_str(), name.c_str()) == 0) { - return getOfferByName((*offer).subcategory[0]); - } - } - - // pula para ofertas - auto it = storeOffers.find(name); - if (it == storeOffers.end()) { - // Clicando no banner tbm chama uma oferta - // std::cout << "[Warning - GameStore::getOfferByName] Offer " << name << " not found" << std::endl; - return nullptr; - } - return &it->second; -} - -StoreOffers* GameStore::getOffersByOfferId(uint32_t id) -{ - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - if (offers == nullptr){ - continue; - } - - StoreOffer* offer = offers->getOfferByID(id); - if (offer != nullptr && offer->getId() == id) { - return offers; - } - } - - return nullptr; -} - -StoreOffer* GameStore::getOfferById(uint32_t id) -{ - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - if (offers == nullptr){ - continue; - } - - StoreOffer* offer = offers->getOfferByID(id); - if (offer != nullptr && offer->getId() == id) { - return offer; - } - } - - return nullptr; -} - -uint32_t StoreOffer::getPrice(Player* player) -{ - uint32_t newPrice = 0; - if (player && type == OFFER_TYPE_EXPBOOST) { - int32_t value1; - player->getStorageValue(51052, value1); - uint32_t xpBoostPrice = getExpBoostPrice(value1); - if (xpBoostPrice > 0) { - if (player->isPremium()) { - xpBoostPrice *= 0.90; - } - - } - - return xpBoostPrice; - } - - if (state == OFFER_STATE_SALE) { - time_t mytime; - mytime = time(NULL); - struct tm tm = *localtime(&mytime); - int32_t daySub = validUntil - tm.tm_mday; - if (daySub < 0) { - newPrice = basePrice; - } - } - - uint32_t p_prince = price; - if (player && player->isPremium()) { - newPrice *= 0.90; - p_prince *= 0.90; - } - - return newPrice > 0 ? newPrice : p_prince; -} - -uint16_t StoreOffer::getCount(bool inBuy) -{ - if (!inBuy && type == OFFER_TYPE_PREMIUM) { - return 1; - } - - return count; -} - -std::string StoreOffer::getDescription(Player* player /*= nullptr */) -{ - if (!player) { - return description; - } - - uint16_t version = player->getProtocolVersion(); - std::string showDesc = version < 1200 ? description : description12; - if (showDesc.empty() && version >= 1200 ) { - showDesc = description; - } - - if ((type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE) && showDesc.empty()) { - Item* virtualItem = Item::CreateItem(itemtype, count); - if (virtualItem) { - showDesc = "You see " + virtualItem->getDescription(version < 1200 ? -1 : -2); - delete virtualItem; - } - } - - return showDesc; -} diff --git a/src/lua/functions/core/game/config_functions.hpp b/src/lua/functions/core/game/config_functions.hpp index 409fc200e7a..46defe0db7b 100644 --- a/src/lua/functions/core/game/config_functions.hpp +++ b/src/lua/functions/core/game/config_functions.hpp @@ -58,7 +58,6 @@ class ConfigFunctions final : LuaScriptInterface { registerEnumIn(L, "configKeys", REMOVE_WEAPON_AMMO) registerEnumIn(L, "configKeys", REMOVE_WEAPON_CHARGES) registerEnumIn(L, "configKeys", REMOVE_POTION_CHARGES) - registerEnumIn(L, "configKeys", STOREMODULES) registerEnumIn(L, "configKeys", WEATHER_RAIN) registerEnumIn(L, "configKeys", WEATHER_THUNDER) registerEnumIn(L, "configKeys", FREE_QUESTS) diff --git a/src/otserv.cpp b/src/otserv.cpp index 80b4f9154c4..336913f26bc 100644 --- a/src/otserv.cpp +++ b/src/otserv.cpp @@ -31,7 +31,6 @@ #include "database/databasemanager.h" #include "database/databasetasks.h" #include "game/game.h" -#include "game/gamestore.hpp" #include "game/scheduling/scheduler.h" #include "io/iomarket.h" #include "lua/creature/events.h" @@ -41,6 +40,7 @@ #include "security/rsa.h" #include "server/network/protocol/protocollogin.h" #include "server/network/protocol/protocolstatus.h" +#include "creatures/players/store/store.hpp" #include "server/network/webhook/webhook.h" #include "server/server.h" @@ -55,7 +55,7 @@ Scheduler g_scheduler; Game g_game; ConfigManager g_config; extern Events* g_events; -GameStore g_gameStore; +Store g_store; extern Imbuements* g_imbuements; extern LuaEnvironment g_luaEnvironment; extern Modules* g_modules; @@ -180,8 +180,9 @@ void loadModules() { "data/XML/outfits.xml"); modulesLoadHelper(Familiars::getInstance().loadFromXml(), "data/XML/familiars.xml"); - modulesLoadHelper(g_gameStore.loadFromXml(), - "data/XML/gamestore.xml"); + + modulesLoadHelper(g_store.loadFromXML(), + "data/store/store.xml"); modulesLoadHelper(g_imbuements->loadFromXml(), "data/XML/imbuements.xml"); modulesLoadHelper(g_modules->loadFromXml(), @@ -245,9 +246,6 @@ void mainLoader(int, char*[], ServiceManager* services) { g_game.setGameState(GAME_STATE_STARTUP); srand(static_cast(OTSYS_TIME())); -#ifdef _WIN32 - SetConsoleTitle(STATUS_SERVER_NAME); -#endif #if defined(GIT_RETRIEVED_STATE) && GIT_RETRIEVED_STATE SPDLOG_INFO("{} - Based on [{}] dated [{}]", STATUS_SERVER_NAME, STATUS_SERVER_VERSION, GIT_COMMIT_DATE_ISO8601); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 69dfd50d402..48497f97866 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -26,7 +26,6 @@ #include "config/configmanager.h" #include "declarations.hpp" #include "game/game.h" -#include "game/gamestore.hpp" #include "creatures/players/imbuements/imbuements.h" #include "io/iobestiary.h" #include "io/iologindata.h" @@ -38,6 +37,7 @@ #include "server/network/protocol/protocolgame.h" #include "game/scheduling/scheduler.h" #include "creatures/combat/spells.h" +#include "creatures/players/store/store.hpp" #include "creatures/players/management/waitlist.h" #include "items/weapons/weapons.h" @@ -51,7 +51,7 @@ extern Modules *g_modules; extern Spells *g_spells; extern Imbuements *g_imbuements; extern Monsters g_monsters; -extern GameStore g_gameStore; +extern Store g_store; void ProtocolGame::AddItem(NetworkMessage &msg, uint16_t id, uint8_t count) { @@ -751,7 +751,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xF7: parseMarketCancelOffer(msg); break; case 0xF8: parseMarketAcceptOffer(msg); break; case 0xF9: parseModalWindowAnswer(msg); break; - // Gamestore + // Store case 0xFA: parseOpenStore(); break; case 0xFB: parseRequestStoreOffers(msg); break; case 0xFC: parseBuyStoreOffer(msg); break; @@ -2581,17 +2581,17 @@ void ProtocolGame::parseRequestStoreOffers(NetworkMessage& msg) StoreOffers* offers = nullptr; if (actionType == 0) { - offers = g_gameStore.getOfferByName(g_config.getString(DEFAULT_OFFER)); + offers = g_store.getOfferByName(g_config.getString(DEFAULT_OFFER)); } else if (actionType == 2) { std::string categoryName = msg.getString(); - offers = g_gameStore.getOfferByName(categoryName); + offers = g_store.getOfferByName(categoryName); } else if (actionType == 4) { uint32_t id = msg.get(); - offers = g_gameStore.getOffersByOfferId(id); + offers = g_store.getOffersByOfferId(id); } else { // SPDLOG_INFO("Test"); // std::string categoryName = msg.getString(); - // offers = g_gameStore.getOfferByName(categoryName); + // offers = g_store.getOfferByName(categoryName); } if (offers != nullptr) { @@ -2607,17 +2607,17 @@ void ProtocolGame::parseBuyStoreOffer(NetworkMessage& msg) OfferBuyTypes_t productType = static_cast(msg.getByte()); std::string param; - StoreOffer* offer = g_gameStore.getOfferById(id); + StoreOffer* offer = g_store.getOfferById(id); if (offer == nullptr) { return; } - if (offer->getOfferType() == OFFER_TYPE_NAMECHANGE && productType != OFFER_BUY_TYPE_NAMECHANGE) { + if (offer->getOfferType() == OFFER_TYPE_NAME_CHANGE && productType != OFFER_BUY_TYPE_NAMECHANGE) { requestPurchaseData(id, OFFER_BUY_TYPE_NAMECHANGE); return; } - if (offer->getOfferType() == OFFER_TYPE_NAMECHANGE) { + if (offer->getOfferType() == OFFER_TYPE_NAME_CHANGE) { param = msg.getString(); } @@ -2627,7 +2627,7 @@ void ProtocolGame::parseBuyStoreOffer(NetworkMessage& msg) void ProtocolGame::parseSendDescription(NetworkMessage& msg) { uint32_t offerId = msg.get(); - StoreOffer* storeOffer = g_gameStore.getOfferById(offerId); + StoreOffer* storeOffer = g_store.getOfferById(offerId); if (storeOffer == nullptr) { return; } @@ -6615,7 +6615,7 @@ void ProtocolGame::sendShowStoreOffers(StoreOffers* offers) msg.add(0); uint16_t count = 0; - std::map> organized = g_gameStore.getStoreOrganizedByName(offers); + std::map> organized = g_store.getStoreOrganizedByName(offers); for (const auto& it : organized) { if (!it.first.empty()) count++; @@ -6655,7 +6655,7 @@ void ProtocolGame::sendStoreHome() msg.add(0x00); uint16_t count = 0; - std::map> organized = g_gameStore.getHomeOffersOrganized(); + std::map> organized = g_store.getHomeOffersOrganized(); for (const auto& it : organized) { if (!it.first.empty()) count++; @@ -6671,7 +6671,7 @@ void ProtocolGame::sendStoreHome() } - std::vector banners = g_gameStore.getHomeBanners(); + std::vector banners = g_store.getHomeBanners(); for (auto banner = banners.begin(), end = banners.end(); banner != end; ++banner) { msg.addByte(banners.size()); msg.addString((*banner)); @@ -6721,9 +6721,9 @@ void ProtocolGame::openStore() NetworkMessage msg; msg.addByte(0xFB); - msg.add(g_gameStore.getOfferCount()); + msg.add(g_store.getOfferCount()); // enviando primeiro as categorias sem subcategorias - std::vector categories = g_gameStore.getStoreCategories(); + std::vector categories = g_store.getStoreCategories(); for (auto it = categories.begin(), end = categories.end(); it != end; ++it) { msg.addString((*it).name); @@ -6735,7 +6735,7 @@ void ProtocolGame::openStore() msg.add(0x00); } - std::vector offers = g_gameStore.getStoreOffers(); + std::vector offers = g_store.getStoreOffers(); for (auto it = offers.begin(), end = offers.end(); it != end; ++it) { msg.addString((*it)->getName()); @@ -6799,7 +6799,7 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i } - uint8_t oftp = g_gameStore.convertType(lasttype); + uint8_t oftp = g_store.convertType(lasttype); msg.addByte(oftp); if (oftp == 0) { msg.addString(lasticon); @@ -6821,8 +6821,8 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i msg.add(0x00); // category msg.add(298); - msg.add(lasttype == OFFER_TYPE_NAMECHANGE ? lastid : 0x00); - msg.addByte(lasttype == OFFER_TYPE_NAMECHANGE); + msg.add(lasttype == OFFER_TYPE_NAME_CHANGE ? lastid : 0x00); + msg.addByte(lasttype == OFFER_TYPE_NAME_CHANGE); msg.add(0x00); } From 300fefebdfcc6df967b357c786c0922da5cc268c Mon Sep 17 00:00:00 2001 From: dudantas Date: Fri, 13 Aug 2021 22:55:39 -0300 Subject: [PATCH 06/30] Store working 100% --- data/scripts/talkactions/god/store_coins.lua | 147 ++++++++++++++++++ src/creatures/players/account/account.cpp | 69 ++++++-- src/creatures/players/account/account.hpp | 10 +- src/creatures/players/player.cpp | 28 ++-- src/creatures/players/player.h | 16 +- src/creatures/players/store/store.cpp | 2 +- src/game/game.cpp | 49 +++--- src/io/iologindata.cpp | 21 ++- .../creatures/player/player_functions.cpp | 47 +++--- .../creatures/player/player_functions.hpp | 12 +- src/server/network/protocol/protocolgame.cpp | 58 +++---- src/server/network/protocol/protocolgame.h | 4 +- 12 files changed, 334 insertions(+), 129 deletions(-) create mode 100644 data/scripts/talkactions/god/store_coins.lua diff --git a/data/scripts/talkactions/god/store_coins.lua b/data/scripts/talkactions/god/store_coins.lua new file mode 100644 index 00000000000..4eb788a6d84 --- /dev/null +++ b/data/scripts/talkactions/god/store_coins.lua @@ -0,0 +1,147 @@ +-- /getcoins playername +-- /addcoins playername, coinscount, example: "/addcoins god, 100" +-- /removecoins playername, coinscount, example: "/addcoins god, 100" + +local getCoins = TalkAction("/getcoins") + +function getCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return true + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[getCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[getCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins() .." store coins.") + return true +end + +getCoins:separator(" ") +getCoins:register() + +local addCoins = TalkAction("/addcoins") + +function addCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return true + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[addCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if have all parameters (god and coinscount) + if not split[2] then + player:sendCancelMessage("Insufficient parameters") + Spdlog.error("[addCoins.onSay] - Insufficient parameters") + return false + end + + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[addCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + -- Trim left + split[2] = split[2]:gsub("^%s*(.-)$", "%1") + + -- Keep the coinscount in storage "coins" + local coins = 0 + if split[2] then + coins = tonumber(split[2]) + end + + -- Check if the coins is valid + if coins <= 0 or coins == nil then + player:sendCancelMessage("Invalid coins count.") + return false + end + + targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + player:addStoreCoins(coins) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store coins to your account.") + -- Distro log + Spdlog.info("".. player:getName() .." added ".. coins .." store coins to ".. targetPlayer:getName() .." account.") + return true +end + +addCoins:separator(" ") +addCoins:register() + +local removeCoins = TalkAction("/removecoins") + +function removeCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return true + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[removeCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if have all parameters (god and coinscount) + if not split[2] then + player:sendCancelMessage("Insufficient parameters") + Spdlog.error("[removeCoins.onSay] - Insufficient parameters") + return false + end + + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[removeCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + -- Trim left + split[2] = split[2]:gsub("^%s*(.-)$", "%1") + + -- Keep the coinscount in storage "coins" + local coins = 0 + if split[2] then + coins = tonumber(split[2]) + end + + -- Check if the coins is valid + if coins <= 0 or coins == nil then + player:sendCancelMessage("Invalid coins count.") + return false + end + + targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + player:removeStoreCoins(coins) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store coins to your account.") + -- Distro log + Spdlog.info("".. player:getName() .." removed ".. coins .." store coins to ".. targetPlayer:getName() .." account.") + return true +end + +removeCoins:separator(" ") +removeCoins:register() diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index a3147277390..bb14a7807b6 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -34,6 +34,8 @@ Account::Account() { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -45,6 +47,8 @@ Account::Account(uint32_t id) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -55,6 +59,8 @@ Account::Account(const std::string &email) : email_(email) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -90,26 +96,25 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { error_t Account::GetCoins(CoinType_t coinType) { if (db_ == nullptr || id_ == 0) { - return ERROR_NOT_INITIALIZED; + return ERROR_NOT_INITIALIZED; } std::ostringstream query; std::string coins = "coins"; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "coins"; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournamentBalance"; - } + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coins = "coins"; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + coins = "tournamentBalance"; + } - query << "SELECT `coins` FROM `accounts` WHERE `id` = " << id_; + query << "SELECT `" << coins << "` FROM `accounts` WHERE `id` = " << id_; DBResult_ptr result = db_->storeQuery(query.str()); if (!result) { - return ERROR_DB; + return ERROR_DB; } - result->getNumber("coins"); - return ERROR_NO; + return result->getNumber(coins); } error_t Account::AddCoins(int32_t amount) @@ -158,8 +163,8 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t Database& db = Database::getInstance(); std::ostringstream query; query << "INSERT INTO `store_history` (`accountid`, `time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" << - id_ << "," << time << "," << static_cast(mode) << "," << amount << "," << static_cast(coinMode) << "," << - db.escapeString(description) << "," << cust << ")"; + id_ << "," << time << "," << static_cast(mode) << "," << amount << "," << static_cast(coinMode) << "," << + db.escapeString(description) << "," << cust << ")"; StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); g_game.addAccountHistory(id_, historyOffer); @@ -210,6 +215,8 @@ error_t Account::LoadAccountDB(std::ostringstream &query) { this->SetPassword(result->getString("password")); this->SetPremiumRemaningDays(result->getNumber("premdays")); this->SetPremiumLastDay(result->getNumber("lastday")); + this->SetStoreCoinBalance(result->getNumber("coins")); + this->SetTournamentCoinBalance(result->getNumber("tournamentBalance")); return ERROR_NO; } @@ -265,6 +272,8 @@ error_t Account::SaveAccountDB() { << "`email` = " << db_->escapeString(email_) << " , " << "`type` = " << account_type_ << " , " << "`password` = " << db_->escapeString(password_) << " , " + << "`coins` = " << coin_balance << " , " + << "`tournamentBalance` = " << tournament_coin_balance << " , " << "`premdays` = " << premium_remaining_days_ << " , " << "`lastday` = " << premium_last_day_; @@ -367,6 +376,42 @@ error_t Account::GetPremiumLastDay(time_t *last_day) { return ERROR_NO; } +error_t Account::SetStoreCoinBalance(uint32_t coins) { + if (coins == 0) { + return ERROR_INVALID_ID; + } + + coin_balance = coins; + return ERROR_NO; +} + +error_t Account::GetStoreCoinBalance(uint32_t *coins) { + if (coins == nullptr) { + return ERROR_NULLPTR; + } + + *coins = coin_balance; + return ERROR_NO; +} + +error_t Account::SetTournamentCoinBalance(uint32_t tournamentCoins) { + if (tournamentCoins == 0) { + return ERROR_INVALID_ID; + } + + tournament_coin_balance = tournamentCoins; + return ERROR_NO; +} + +error_t Account::GetTournamentCoinBalance(uint32_t *tournamentCoins) { + if (tournamentCoins == nullptr) { + return ERROR_NULLPTR; + } + + *tournamentCoins = tournament_coin_balance; + return ERROR_NO; +} + error_t Account::SetAccountType(AccountType account_type) { if (account_type > 5) { return ERROR_INVALID_ACC_TYPE; diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index 5dddaca4822..c18b35d96dd 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -132,7 +132,7 @@ class Account { * @param coins Pointer to return the number of coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t GetCoins(CoinType_t coinType); + error_t GetCoins(CoinType_t coinType = COIN_TYPE_DEFAULT); /** * @brief Add coins to the account and update database. @@ -211,6 +211,12 @@ class Account { error_t SetPremiumRemaningDays(uint32_t days); error_t GetPremiumRemaningDays(uint32_t *days); + error_t SetStoreCoinBalance(uint32_t coins); + error_t GetStoreCoinBalance(uint32_t *coins); + + error_t SetTournamentCoinBalance(uint32_t tournamentCoins); + error_t GetTournamentCoinBalance(uint32_t *tournamentCoins); + error_t SetPremiumLastDay(time_t last_day); error_t GetPremiumLastDay(time_t *last_day); @@ -234,6 +240,8 @@ class Account { std::string password_; uint32_t premium_remaining_days_; time_t premium_last_day_; + uint32_t coin_balance; + uint32_t tournament_coin_balance; AccountType account_type_; }; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 78d03e74ae2..07252f2c35b 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4619,63 +4619,63 @@ void Player::setPremiumDays(int32_t v) sendBasicData(); } -void Player::setTibiaCoins(int32_t v, CoinType_t coinType) +void Player::setStoreCoins(int32_t coins, CoinType_t coinType) { switch (coinType) { case COIN_TYPE_DEFAULT: case COIN_TYPE_TRANSFERABLE: { - coinBalance = v; + coinBalance = coins; break; } case COIN_TYPE_TOURNAMENT: { - tournamentCoinBalance = v; + tournamentCoinBalance = coins; break; } default: { - coinBalance = v; + coinBalance = coins; break; } } } -bool Player::canRemoveCoins(int32_t v, CoinType_t coinType) +bool Player::canRemoveStoreCoins(int32_t coins, CoinType_t coinType) { if (lastUpdateCoin - OTSYS_TIME() < 2000) { - // a cada 2 segundos atualizar, na diferença que for chamada + // Update every 2 seconds lastUpdateCoin = OTSYS_TIME() + 2000; - account::Account account(getAccount()); + account::Account account(this->getAccount()); account.LoadAccountDB(); if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coinBalance = account.GetCoins(coinType); + account.GetStoreCoinBalance(&(coinBalance)); + this->coinBalance = account.GetCoins(); } else if (coinType == COIN_TYPE_TOURNAMENT) { tournamentCoinBalance = account.GetCoins(coinType); } } - - int32_t coins; + int32_t removeCoins; switch (coinType) { case COIN_TYPE_DEFAULT: case COIN_TYPE_TRANSFERABLE: { - coins = coinBalance; + removeCoins = coinBalance; break; } case COIN_TYPE_TOURNAMENT: { - coins = tournamentCoinBalance; + removeCoins = tournamentCoinBalance; break; } default: { - coins = coinBalance; + removeCoins = coinBalance; break; } } - return (coins - v) >= 0; + return (removeCoins - coins) >= 0; } PartyShields_t Player::getPartyShield(const Player* player) const diff --git a/src/creatures/players/player.h b/src/creatures/players/player.h index 896945302ab..877d493a2d3 100644 --- a/src/creatures/players/player.h +++ b/src/creatures/players/player.h @@ -525,9 +525,9 @@ class Player final : public Creature, public Cylinder bool isPremium() const; void setPremiumDays(int32_t v); - void setTibiaCoins(int32_t v, CoinType_t coinType = COIN_TYPE_DEFAULT); - bool canRemoveCoins(int32_t v, CoinType_t coinType = COIN_TYPE_DEFAULT); - int32_t getCoinBalance(CoinType_t coinType = COIN_TYPE_DEFAULT) { + void setStoreCoins(int32_t coins, CoinType_t coinType = COIN_TYPE_DEFAULT); + bool canRemoveStoreCoins(int32_t coins, CoinType_t coinType = COIN_TYPE_DEFAULT); + int32_t getStoreCoinBalance(CoinType_t coinType = COIN_TYPE_DEFAULT) { if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { return coinBalance; } else if (coinType == COIN_TYPE_TOURNAMENT) { @@ -1049,9 +1049,9 @@ class Player final : public Creature, public Cylinder client->sendLockerItems(itemMap, count); } } - void sendCoinBalance() { + void sendStoreCoinBalance() { if (client) { - client->sendCoinBalance(); + client->sendStoreCoinBalance(); } } void sendInventoryItem(Slots_t slot, const Item* item) { @@ -1077,15 +1077,15 @@ class Player final : public Creature, public Cylinder } } - // GameStore + // Store void openStore() { if (client) { client->openStore(); } } - void updateCoinBalance() { + void updateStoreCoinBalance() { if (client) { - client->updateCoinBalance(); + client->updateStoreCoinBalance(); } } diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index 08ea02faf03..f801c047bb5 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -617,7 +617,7 @@ std::string StoreOffer::getDisabledReason(Player* player) { disabledReason = "This offer is deactivated."; } - if (player->getCoinBalance(coinType) - getPrice(player) < 0) { + if (player->getStoreCoinBalance(coinType) - getPrice(player) < 0) { if (coinType == COIN_TYPE_TOURNAMENT) { disabledReason = "You don't have tournament coins."; } else { diff --git a/src/game/game.cpp b/src/game/game.cpp index 3e89f20b6c5..2bbf276a5a5 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -7561,10 +7561,6 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr uint32_t minFee = std::min(100000, calcFee); uint32_t fee = std::max(20, minFee); - account::Account account(player->getAccount()); - account.LoadAccountDB(); - uint32_t coins; - if (type == MARKETACTION_SELL) { if (fee > (player->getBankBalance() + player->getMoney())) { @@ -7577,11 +7573,13 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr } if (it.id == ITEM_STORE_COIN) { - if (amount > player->getCoinBalance()) { + if (amount > player->getStoreCoinBalance()) { return; } - account.AddCoins(static_cast(amount)); + account::Account account(player->getAccount()); + account.LoadAccountDB(); + account.AddCoins(amount); } else { uint16_t stashmath = amount; uint16_t stashminus = player->getStashItemCount(it.wareId); @@ -7671,9 +7669,9 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(player->getAccount()); - account.AddCoins(offer.amount); + account::Account account; + account.LoadAccountDB(player->getAccount()); + account.AddCoins(offer.amount); } else if (it.stackable) { uint16_t tmpAmount = offer.amount; @@ -7773,13 +7771,14 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } } - account::Account account; - account.LoadAccountDB(player->getAccount()); - uint32_t coins; - account.GetCoins(COIN_TYPE_DEFAULT); - if (it.id == ITEM_STORE_COIN) { - account.AddCoins(static_cast(amount)); + account::Account account; + account.LoadAccountDB(player->getAccount()); + if (amount > account.GetCoins()) { + return; + } + + account.AddCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); } else { std::forward_list itemList = getMarketItemList(it.wareId, amount, depotLocker); @@ -7809,7 +7808,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; account.LoadAccountDB(buyerPlayer->getAccount()); - account.AddCoins(static_cast(amount)); + account.AddCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); } else if (it.stackable) { @@ -7872,7 +7871,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; account.LoadAccountDB(player->getAccount()); - account.AddCoins(static_cast(amount)); + account.AddCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); } else if (it.stackable) { uint16_t tmpAmount = amount; @@ -7906,9 +7905,9 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (sellerPlayer) { sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.LoadAccountDB(sellerPlayer->getAccount()); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + account::Account account; + account.LoadAccountDB(sellerPlayer->getAccount()); + account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); } } else { IOLoginData::increaseBankBalance(offer.playerId, totalPrice); @@ -8340,7 +8339,7 @@ void Game::playerOpenStore(uint32_t playerId, bool openStore, StoreOffers* offer } // Update coins - player->updateCoinBalance(); + player->updateStoreCoinBalance(); if (openStore) { player->openStore(); @@ -8373,12 +8372,12 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: return; } - if (!player->canRemoveCoins(thisOffer->getPrice(player)) ) { + if (!player->canRemoveStoreCoins(thisOffer->getPrice(player)) ) { player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have coins."); return; } - player->updateCoinBalance(); + player->updateStoreCoinBalance(); std::string message = thisOffer->getDisabledReason(player); if (!message.empty()) { player->sendStoreError(STORE_ERROR_PURCHASE, message); @@ -8933,12 +8932,12 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: account::Account account(player->getAccount()); account.LoadAccountDB(); account.RemoveCoins(offerPrice*-1); - player->setTibiaCoins(account.GetCoins(thisOffer->getCoinType()), thisOffer->getCoinType()); + player->setStoreCoins(account.GetCoins(thisOffer->getCoinType()), thisOffer->getCoinType()); if (returnmessage.str().empty()) { returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" coins"; } - player->updateCoinBalance(); + player->updateStoreCoinBalance(); player->sendStorePurchaseSuccessful(returnmessage.str()); account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), thisOffer->getCount(true), static_cast(thisOffer->getCoinType()), std::move(thisOffer->getName()), offerPrice); diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 7ea9ac9e3bc..800ba7de781 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -104,7 +104,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) Database& db = Database::getInstance(); std::ostringstream query; - query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type`"; + query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type`, (SELECT `coins` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `coinbalance`, (SELECT `tournamentBalance` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `tournamentBalance`"; if (!g_config.getBoolean(FREE_PREMIUM)) { query << ", (SELECT `premdays` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `premium_days`"; } @@ -128,6 +128,8 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) player->setGroup(group); player->accountNumber = result->getNumber("account_id"); player->accountType = static_cast(result->getNumber("account_type")); + player->coinBalance = result->getNumber("coinbalance"); + player->tournamentCoinBalance = result->getNumber("tournamentBalance"); if (!g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = result->getNumber("premium_days"); } else { @@ -266,22 +268,25 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) Database& db = Database::getInstance(); uint32_t accountId = result->getNumber("account_id"); - account::Account acc; - acc.SetDatabaseInterface(&db); - acc.LoadAccountDB(accountId); + account::Account account; + account.SetDatabaseInterface(&db); + account.LoadAccountDB(accountId); player->setGUID(result->getNumber("id")); player->name = result->getString("name"); - acc.GetID(&(player->accountNumber)); - acc.GetAccountType(&(player->accountType)); + account.GetID(&(player->accountNumber)); + account.GetAccountType(&(player->accountType)); + account.GetStoreCoinBalance(&(player->coinBalance)); + account.GetTournamentCoinBalance(&(player->coinBalance)); if (g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = std::numeric_limits::max(); } else { - acc.GetPremiumRemaningDays(&(player->premiumDays)); + account.GetPremiumRemaningDays(&(player->premiumDays)); } - acc.GetCoins(COIN_TYPE_DEFAULT); + player->coinBalance = account.GetCoins(); + player->tournamentCoinBalance = account.GetCoins(COIN_TYPE_TOURNAMENT); player->preyBonusRerolls = result->getNumber("bonus_rerolls"); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index f82b8279f60..a4fa4cce3ab 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2289,45 +2289,43 @@ int PlayerFunctions::luaPlayerRemovePremiumDays(lua_State* L) { return 1; } -int PlayerFunctions::luaPlayerGetTibiaCoins(lua_State* L) { - // player:getTibiaCoins() +int PlayerFunctions::luaPlayerGetStoreCoins(lua_State* L) { + // player:getStoreCoins([coinType = COIN_TYPE_DEFAULT]) Player* player = getUserdata(L, 1); - if (player) { - account::Account account(player->getAccount()); - account.LoadAccountDB(); - uint32_t coins; - account.GetCoins(COIN_TYPE_DEFAULT); - lua_pushnumber(L, coins); - } else { + if (!player) { lua_pushnil(L); + return 1; } + + lua_pushnumber(L, player->getStoreCoinBalance(getNumber(L, 2, COIN_TYPE_DEFAULT))); return 1; } -int PlayerFunctions::luaPlayerAddTibiaCoins(lua_State* L) { - // player:addTibiaCoins(coins) +int PlayerFunctions::luaPlayerAddStoreCoins(lua_State* L) { + // player:addStoreCoins(coins, [coinType = COIN_TYPE_DEFAULT]) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - uint32_t coins = getNumber(L, 2); - - account::Account account(player->getAccount()); - account.LoadAccountDB(); - if(account.AddCoins(coins)) { - account.GetCoins(COIN_TYPE_DEFAULT); - pushBoolean(L, true); - } else { - lua_pushnil(L); + CoinType_t coinType = getNumber(L, 3, COIN_TYPE_DEFAULT); + if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { + int32_t coins = getNumber(L, 2); + int32_t addCoins = std::min(std::numeric_limits::max() - player->getStoreCoinBalance(coinType), coins); + if (addCoins > 0) { + account::Account account(player->getAccount()); + account.LoadAccountDB(); + player->setStoreCoins(player->getStoreCoinBalance(coinType) + addCoins, coinType); + account.AddCoins(addCoins); + lua_pushnumber(L, addCoins); + } } - return 1; } -int PlayerFunctions::luaPlayerRemoveTibiaCoins(lua_State* L) { - // player:removeTibiaCoins(coins) +int PlayerFunctions::luaPlayerRemoveStoreCoins(lua_State* L) { + // player:removeStoreCoins(coins) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); @@ -2339,8 +2337,7 @@ int PlayerFunctions::luaPlayerRemoveTibiaCoins(lua_State* L) { account::Account account(player->getAccount()); account.LoadAccountDB(); if (account.RemoveCoins(coins)) { - account.GetCoins(COIN_TYPE_DEFAULT); - pushBoolean(L, true); + lua_pushnumber(L, account.GetCoins()); } else { lua_pushnil(L); } diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 5152c090571..686e5831d45 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -213,9 +213,9 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "addPremiumDays", PlayerFunctions::luaPlayerAddPremiumDays); registerMethod(L, "Player", "removePremiumDays", PlayerFunctions::luaPlayerRemovePremiumDays); - registerMethod(L, "Player", "getTibiaCoins", PlayerFunctions::luaPlayerGetTibiaCoins); - registerMethod(L, "Player", "addTibiaCoins", PlayerFunctions::luaPlayerAddTibiaCoins); - registerMethod(L, "Player", "removeTibiaCoins", PlayerFunctions::luaPlayerRemoveTibiaCoins); + registerMethod(L, "Player", "getStoreCoins", PlayerFunctions::luaPlayerGetStoreCoins); + registerMethod(L, "Player", "addStoreCoins", PlayerFunctions::luaPlayerAddStoreCoins); + registerMethod(L, "Player", "removeStoreCoins", PlayerFunctions::luaPlayerRemoveStoreCoins); registerMethod(L, "Player", "hasBlessing", PlayerFunctions::luaPlayerHasBlessing); registerMethod(L, "Player", "addBlessing", PlayerFunctions::luaPlayerAddBlessing); @@ -492,9 +492,9 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerAddPremiumDays(lua_State* L); static int luaPlayerRemovePremiumDays(lua_State* L); - static int luaPlayerGetTibiaCoins(lua_State* L); - static int luaPlayerAddTibiaCoins(lua_State* L); - static int luaPlayerRemoveTibiaCoins(lua_State* L); + static int luaPlayerGetStoreCoins(lua_State* L); + static int luaPlayerAddStoreCoins(lua_State* L); + static int luaPlayerRemoveStoreCoins(lua_State* L); static int luaPlayerHasBlessing(lua_State* L); static int luaPlayerAddBlessing(lua_State* L); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 48497f97866..01eebe30272 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -2529,7 +2529,7 @@ void ProtocolGame::parseMarketCancelOffer(NetworkMessage &msg) addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter); } - updateCoinBalance(); + updateStoreCoinBalance(); } void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) @@ -2542,7 +2542,7 @@ void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) addGameTask(&Game::playerAcceptMarketOffer, player->getID(), timestamp, counter, amount); } - updateCoinBalance(); + updateStoreCoinBalance(); } void ProtocolGame::parseModalWindowAnswer(NetworkMessage &msg) @@ -3997,39 +3997,44 @@ void ProtocolGame::sendMarketEnter(uint32_t depotId) writeToOutputBuffer(msg); - updateCoinBalance(); + updateStoreCoinBalance(); sendResourcesBalance(player->getMoney(), player->getBankBalance()); } -void ProtocolGame::sendCoinBalance() +void ProtocolGame::sendStoreCoinBalance() { if (!player) { return; } - // send is updating - // TODO: export this to it own function NetworkMessage msg; + // Updating balance + // TODO: export this to it own function msg.addByte(0xF2); msg.addByte(0x01); writeToOutputBuffer(msg); msg.reset(); - // send update + // Coins balance msg.addByte(0xDF); msg.addByte(0x01); - msg.add(player->coinBalance); // Normal Coins - msg.add(player->coinBalance); // Transferable Coins - msg.add(player->coinBalance); // Reserved Auction Coins - msg.add(0); // Tournament Coins + // Total coins + msg.add(player->getStoreCoinBalance(COIN_TYPE_DEFAULT)); + // Transferable coins + msg.add(player->getStoreCoinBalance(COIN_TYPE_TRANSFERABLE)); + // Reserved Auction Coins + // Version 1220+ + msg.add(0); + // Tournament Coins + msg.add(player->getStoreCoinBalance(COIN_TYPE_TOURNAMENT)); writeToOutputBuffer(msg); } -void ProtocolGame::updateCoinBalance() +void ProtocolGame::updateStoreCoinBalance() { if (!player) { return; @@ -4037,17 +4042,16 @@ void ProtocolGame::updateCoinBalance() g_dispatcher.addTask( createTask(std::bind([](uint32_t playerId) { - Player* threadPlayer = g_game.getPlayerByID(playerId); - if (threadPlayer) { - account::Account account; - account.LoadAccountDB(threadPlayer->getAccount()); - uint32_t coins; - account.GetCoins(&coins); - threadPlayer->coinBalance = coins; - threadPlayer->sendCoinBalance(); + Player* player = g_game.getPlayerByID(playerId); + if (player != nullptr) { + account::Account account(player->getAccount()); + account.LoadAccountDB(); + player->coinBalance = account.GetCoins(); + player->tournamentCoinBalance = account.GetCoins(COIN_TYPE_TOURNAMENT); + player->sendStoreCoinBalance(); } - }, - player->getID()))); + }, player->getID())) + ); } void ProtocolGame::sendMarketLeave() @@ -4084,7 +4088,7 @@ void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList & msg.addString(offer.playerName); } - updateCoinBalance(); + updateStoreCoinBalance(); writeToOutputBuffer(msg); } @@ -6748,7 +6752,7 @@ void ProtocolGame::openStore() } writeToOutputBuffer(msg); - player->updateCoinBalance(); + player->updateStoreCoinBalance(); sendStoreHome(); } @@ -6773,14 +6777,14 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i msg.add((*offer)->getCount()); msg.add((*offer)->getPrice(player)); msg.addByte((*offer)->getCoinType()); - + std::string disabled = (*offer)->getDisabledReason(player); msg.addByte(!disabled.empty()); if (!disabled.empty()) { msg.addByte(0x01); msg.addString(disabled); } - + if ((*offer)->getOfferState() == OFFER_STATE_SALE) { time_t mytime; mytime = time(NULL); @@ -6792,7 +6796,7 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i msg.add((*offer)->getBasePrice()); } else { msg.addByte(OFFER_STATE_NONE); - } + } } else { msg.addByte((*offer)->getOfferState()); } diff --git a/src/server/network/protocol/protocolgame.h b/src/server/network/protocol/protocolgame.h index 9825d2aa956..348fe0de576 100644 --- a/src/server/network/protocol/protocolgame.h +++ b/src/server/network/protocol/protocolgame.h @@ -330,7 +330,7 @@ class ProtocolGame final : public Protocol void sendResourceBalance(Resource_t resourceType, uint64_t value); void sendSaleItemList(const ShopInfoMap &shop, const std::map &inventoryMap); void sendMarketEnter(uint32_t depotId); - void updateCoinBalance(); + void updateStoreCoinBalance(); void sendMarketLeave(); void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList &buyOffers, const MarketOfferList &sellOffers); void sendMarketAcceptOffer(const MarketOfferEx &offer); @@ -365,7 +365,7 @@ class ProtocolGame final : public Protocol void sendSpellCooldown(uint8_t spellId, uint32_t time); void sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time); - void sendCoinBalance(); + void sendStoreCoinBalance(); //tiles void sendMapDescription(const Position &pos); From 08b053ccfe37682448ad9970d2ab04fc00eee830 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sat, 14 Aug 2021 05:33:37 -0300 Subject: [PATCH 07/30] Fix bug in the "Account::AddCoins" and "Account::RemoveCoins" functions and add new functions --- data/scripts/talkactions/god/store_coins.lua | 8 +- .../god/store_tournament_coins.lua | 147 ++++++ schema.sql | 19 +- src/creatures/players/account/account.cpp | 470 +++++++++--------- src/creatures/players/account/account.hpp | 386 +++++++------- src/creatures/players/player.cpp | 5 +- src/game/game.cpp | 49 ++ src/game/game.h | 1 + src/lua/functions/core/game/lua_enums.hpp | 4 + .../creatures/player/player_functions.cpp | 39 +- src/server/network/protocol/protocolgame.cpp | 14 +- src/server/network/protocol/protocolgame.h | 3 +- 12 files changed, 682 insertions(+), 463 deletions(-) create mode 100644 data/scripts/talkactions/god/store_tournament_coins.lua diff --git a/data/scripts/talkactions/god/store_coins.lua b/data/scripts/talkactions/god/store_coins.lua index 4eb788a6d84..0bf69ea5e57 100644 --- a/data/scripts/talkactions/god/store_coins.lua +++ b/data/scripts/talkactions/god/store_coins.lua @@ -1,6 +1,6 @@ -- /getcoins playername -- /addcoins playername, coinscount, example: "/addcoins god, 100" --- /removecoins playername, coinscount, example: "/addcoins god, 100" +-- /removecoins playername, coinscount, example: "/removecoins god, 100" local getCoins = TalkAction("/getcoins") @@ -25,7 +25,7 @@ function getCoins.onSay(player, words, param) return false end - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins() .." store coins.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") return true end @@ -78,7 +78,7 @@ function addCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:addStoreCoins(coins) + player:addStoreCoins(coins, COIN_TYPE_DEFAULT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store coins to your account.") -- Distro log @@ -135,7 +135,7 @@ function removeCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:removeStoreCoins(coins) + player:removeStoreCoins(coins, COIN_TYPE_DEFAULT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store coins to your account.") -- Distro log diff --git a/data/scripts/talkactions/god/store_tournament_coins.lua b/data/scripts/talkactions/god/store_tournament_coins.lua new file mode 100644 index 00000000000..eda88e1c769 --- /dev/null +++ b/data/scripts/talkactions/god/store_tournament_coins.lua @@ -0,0 +1,147 @@ +-- /gettournamentcoins playername +-- /addtournamentcoins playername, coinscount, example: "/addtournamentcoins god, 100" +-- /removetournamentcoins playername, coinscount, example: "/removetournamentcoins god, 100" + +local getTournamentCoins = TalkAction("/gettournamentcoins") + +function getTournamentCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return true + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[getTournamentCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[getTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") + return true +end + +getTournamentCoins:separator(" ") +getTournamentCoins:register() + +local addTournamentCoins = TalkAction("/addTournamentCoins") + +function addTournamentCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return true + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[addTournamentCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if have all parameters (god and coinscount) + if not split[2] then + player:sendCancelMessage("Insufficient parameters") + Spdlog.error("[addTournamentCoins.onSay] - Insufficient parameters") + return false + end + + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[addTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + -- Trim left + split[2] = split[2]:gsub("^%s*(.-)$", "%1") + + -- Keep the coinscount in variable "coins" + local coins = 0 + if split[2] then + coins = tonumber(split[2]) + end + + -- Check if the coins is valid + if coins <= 0 or coins == nil then + player:sendCancelMessage("Invalid coins count.") + return false + end + + targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + player:addStoreCoins(coins, COIN_TYPE_TOURNAMENT) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store tournament coins to your account.") + -- Distro log + Spdlog.info("".. player:getName() .." added ".. coins .." store tournament coins to ".. targetPlayer:getName() .." account.") + return true +end + +addTournamentCoins:separator(" ") +addTournamentCoins:register() + +local removeTournamentCoins = TalkAction("/removeTournamentCoins") + +function removeTournamentCoins.onSay(player, words, param) + if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then + return false + end + + -- Check the first param (player name) exists + if param == "" then + player:sendCancelMessage("Player name param required") + Spdlog.error("[removeTournamentCoins.onSay] - Player name param not found") + return false + end + + local split = param:split(",") + -- Check if have all parameters (god and coinscount) + if not split[2] then + player:sendCancelMessage("Insufficient parameters") + Spdlog.error("[removeTournamentCoins.onSay] - Insufficient parameters") + return false + end + + -- Check if player is online + local targetPlayer = Player(split[1]) + if not targetPlayer then + player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + Spdlog.error("[removeTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") + return false + end + + -- Trim left + split[2] = split[2]:gsub("^%s*(.-)$", "%1") + + -- Keep the coinscount in variable "coins" + local coins = 0 + if split[2] then + coins = tonumber(split[2]) + end + + -- Check if the coins is valid + if coins <= 0 or coins == nil then + player:sendCancelMessage("Invalid coins count.") + return false + end + + targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) + player:removeStoreCoins(coins, COIN_TYPE_TOURNAMENT) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store tournament coins to your account.") + -- Distro log + Spdlog.info("".. player:getName() .." removed ".. coins .." store tournament coins to ".. targetPlayer:getName() .." account.") + return true +end + +removeTournamentCoins:separator(" ") +removeTournamentCoins:register() diff --git a/schema.sql b/schema.sql index f7bcb875b58..8dc420d8e89 100644 --- a/schema.sql +++ b/schema.sql @@ -27,17 +27,14 @@ CREATE TABLE IF NOT EXISTS `accounts` ( -- Table structure `coins_transactions` CREATE TABLE IF NOT EXISTS `coins_transactions` ( - `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, - `account_id` int(11) UNSIGNED NOT NULL, - `type` tinyint(1) UNSIGNED NOT NULL, - `amount` int(12) UNSIGNED NOT NULL, - `description` varchar(3500) NOT NULL, - `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP, - INDEX `account_id` (`account_id`), - CONSTRAINT `coins_transactions_pk` PRIMARY KEY (`id`), - CONSTRAINT `coins_transactions_account_fk` - FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) - ON DELETE CASCADE + `account_id` int(11) NOT NULL, + `mode` tinyint(1) NOT NULL DEFAULT 0, + `amount` int(11) NOT NULL, + `coinMode` tinyint(2) NOT NULL DEFAULT 0, + `description` varchar(255) DEFAULT NULL, + `cust` int(11) NOT NULL, + `time` bigint(20) DEFAULT NULL, + INDEX `account_id` (`account_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `players` diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index bb14a7807b6..7d033304108 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -9,7 +9,7 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along @@ -29,41 +29,41 @@ namespace account { Account::Account() { - id_ = 0; - email_.clear(); - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; + id_ = 0; + email_.clear(); + password_.clear(); + premium_remaining_days_ = 0; + premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; + account_type_ = ACCOUNT_TYPE_NORMAL; + db_ = &Database::getInstance(); + db_tasks_ = &g_databaseTasks; } Account::Account(uint32_t id) { - id_ = id; - email_.clear(); - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; + id_ = id; + email_.clear(); + password_.clear(); + premium_remaining_days_ = 0; + premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; + account_type_ = ACCOUNT_TYPE_NORMAL; + db_ = &Database::getInstance(); + db_tasks_ = &g_databaseTasks; } Account::Account(const std::string &email) : email_(email) { - id_ = 0; - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; + id_ = 0; + password_.clear(); + premium_remaining_days_ = 0; + premium_last_day_ = 0; + coin_balance = 0; + tournament_coin_balance = 0; + account_type_ = ACCOUNT_TYPE_NORMAL; + db_ = &Database::getInstance(); + db_tasks_ = &g_databaseTasks; } @@ -72,21 +72,21 @@ Account::Account(const std::string &email) : email_(email) { ******************************************************************************/ error_t Account::SetDatabaseInterface(Database *database) { - if (database == nullptr) { - return ERROR_NULLPTR; - } + if (database == nullptr) { + return ERROR_NULLPTR; + } - db_ = database; - return ERROR_NO; + db_ = database; + return ERROR_NO; } error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { - if (database_tasks == nullptr) { - return ERROR_NULLPTR; - } + if (database_tasks == nullptr) { + return ERROR_NULLPTR; + } - db_tasks_ = database_tasks; - return ERROR_NO; + db_tasks_ = database_tasks; + return ERROR_NO; } @@ -95,33 +95,32 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { ******************************************************************************/ error_t Account::GetCoins(CoinType_t coinType) { - if (db_ == nullptr || id_ == 0) { - return ERROR_NOT_INITIALIZED; - } + if (db_ == nullptr || id_ == 0) { + return ERROR_NOT_INITIALIZED; + } - std::ostringstream query; - std::string coins = "coins"; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "coins"; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournamentBalance"; - } + std::ostringstream query; + std::string coins = "coins"; + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + coins = "coins"; + } else if (coinType == COIN_TYPE_TOURNAMENT) { + coins = "tournamentBalance"; + } - query << "SELECT `" << coins << "` FROM `accounts` WHERE `id` = " << id_; + query << "SELECT `" << coins << "` FROM `accounts` WHERE `id` = " << id_; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return ERROR_DB; - } + DBResult_ptr result = db_->storeQuery(query.str()); + if (!result) { + return ERROR_DB; + } - return result->getNumber(coins); + return result->getNumber(coins); } -error_t Account::AddCoins(int32_t amount) +error_t Account::AddCoins(int32_t amount, CoinType_t coinType) { std::string coins = "`coins`"; - CoinType_t coinType; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { + if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { coins = "`coins`"; } else if (coinType == COIN_TYPE_TOURNAMENT) { coins = "`tournamentBalance`"; @@ -129,22 +128,21 @@ error_t Account::AddCoins(int32_t amount) std::ostringstream query; query << "UPDATE `accounts` SET " << coins << " = " << coins << " + " << amount << " WHERE `id` = " << id_; - g_databaseTasks.addTask(query.str()); - return ERROR_NO; + db_tasks_->addTask(query.str()); + return ERROR_NO; } -error_t Account::RemoveCoins(int32_t amount) { +error_t Account::RemoveCoins(int32_t amount, CoinType_t coinType) { - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } + if (db_tasks_ == nullptr) { + return ERROR_NULLPTR; + } - if (amount == 0) { - return ERROR_NO; - } + if (amount == 0) { + return ERROR_NO; + } - CoinType_t coinType; - std::string coins = "`coins`"; + std::string coins = "`coins`"; if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { coins = "`coins`"; } else if (coinType == COIN_TYPE_TOURNAMENT) { @@ -153,23 +151,33 @@ error_t Account::RemoveCoins(int32_t amount) { std::ostringstream query; query << "UPDATE `accounts` SET " << coins << " = " << coins << " - " << amount << " WHERE `id` = " << id_; - g_databaseTasks.addTask(query.str()); - - return ERROR_NO; + db_tasks_->addTask(query.str()); + return ERROR_NO; } error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust) { - Database& db = Database::getInstance(); + if (db_ == nullptr) { + return ERROR_NULLPTR; + } + std::ostringstream query; - query << "INSERT INTO `store_history` (`accountid`, `time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" << - id_ << "," << time << "," << static_cast(mode) << "," << amount << "," << static_cast(coinMode) << "," << - db.escapeString(description) << "," << cust << ")"; + query << "INSERT INTO `coins_transactions` (`account_id`,`time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" + << id_ << "," + << time << "," + << static_cast(mode) << "," + << amount << "," << static_cast(coinMode) << "," + << db_->escapeString(description) << "," + << cust << ")"; + + if (!db_->executeQuery(query.str())) { + return ERROR_DB; + } StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); g_game.addAccountHistory(id_, historyOffer); - return db.executeQuery(query.str()); + return db_->executeQuery(query.str()); } /******************************************************************************* @@ -177,48 +185,48 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t ******************************************************************************/ error_t Account::LoadAccountDB() { - if (id_ != 0) { - return this->LoadAccountDB(id_); - } else if (!email_.empty()) { - return this->LoadAccountDB(email_); - } + if (id_ != 0) { + return this->LoadAccountDB(id_); + } else if (!email_.empty()) { + return this->LoadAccountDB(email_); + } - return ERROR_NOT_INITIALIZED; + return ERROR_NOT_INITIALIZED; } error_t Account::LoadAccountDB(std::string email) { - std::ostringstream query; - query << "SELECT * FROM `accounts` WHERE `email` = " - << db_->escapeString(email); - return this->LoadAccountDB(query); + std::ostringstream query; + query << "SELECT * FROM `accounts` WHERE `email` = " + << db_->escapeString(email); + return this->LoadAccountDB(query); } error_t Account::LoadAccountDB(uint32_t id) { - std::ostringstream query; - query << "SELECT * FROM `accounts` WHERE `id` = " << id; - return this->LoadAccountDB(query); + std::ostringstream query; + query << "SELECT * FROM `accounts` WHERE `id` = " << id; + return this->LoadAccountDB(query); } error_t Account::LoadAccountDB(std::ostringstream &query) { - if (db_ == nullptr) { - return ERROR_NULLPTR; - } + if (db_ == nullptr) { + return ERROR_NULLPTR; + } - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return false; - } + DBResult_ptr result = db_->storeQuery(query.str()); + if (!result) { + return false; + } - this->SetID(result->getNumber("id")); - this->SetEmail(result->getString("email")); - this->SetAccountType(static_cast(result->getNumber("type"))); - this->SetPassword(result->getString("password")); - this->SetPremiumRemaningDays(result->getNumber("premdays")); - this->SetPremiumLastDay(result->getNumber("lastday")); - this->SetStoreCoinBalance(result->getNumber("coins")); - this->SetTournamentCoinBalance(result->getNumber("tournamentBalance")); + this->SetID(result->getNumber("id")); + this->SetEmail(result->getString("email")); + this->SetAccountType(static_cast(result->getNumber("type"))); + this->SetPassword(result->getString("password")); + this->SetPremiumRemaningDays(result->getNumber("premdays")); + this->SetPremiumLastDay(result->getNumber("lastday")); + this->SetStoreCoinBalance(result->getNumber("coins")); + this->SetTournamentCoinBalance(result->getNumber("tournamentBalance")); - return ERROR_NO; + return ERROR_NO; } error_t Account::LoadAccountPlayerDB(Player *player, std::string& characterName) { @@ -228,7 +236,8 @@ error_t Account::LoadAccountPlayerDB(Player *player, std::string& characterName) std::ostringstream query; query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " - << id_ << " AND `name` = " << db_->escapeString(characterName) << " ORDER BY `name` ASC"; + << id_ << " AND `name` = " << db_->escapeString(characterName) + << " ORDER BY `name` ASC"; DBResult_ptr result = db_->storeQuery(query.str()); if (!result || result->getNumber("deletion") != 0) { @@ -242,52 +251,53 @@ error_t Account::LoadAccountPlayerDB(Player *player, std::string& characterName) } error_t Account::LoadAccountPlayersDB(std::vector *players) { - if (id_ == 0) { - return ERROR_NOT_INITIALIZED; - } - - std::ostringstream query; - query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " - << id_ << " ORDER BY `name` ASC"; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return ERROR_DB; - } - - do { - if (result->getNumber("deletion") == 0) { - Player new_player; - new_player.name = result->getString("name"); - new_player.deletion = result->getNumber("deletion"); - players->push_back(new_player); - } - } while (result->next()); - return ERROR_NO; + if (id_ == 0) { + return ERROR_NOT_INITIALIZED; + } + + std::ostringstream query; + query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " + << id_ << " ORDER BY `name` ASC"; + + DBResult_ptr result = db_->storeQuery(query.str()); + if (!result) { + return ERROR_DB; + } + + do { + if (result->getNumber("deletion") == 0) { + Player new_player; + new_player.name = result->getString("name"); + new_player.deletion = result->getNumber("deletion"); + players->push_back(new_player); + } + } while (result->next()); + return ERROR_NO; } error_t Account::SaveAccountDB() { - std::ostringstream query; - - query << "UPDATE `accounts` SET " - << "`email` = " << db_->escapeString(email_) << " , " - << "`type` = " << account_type_ << " , " - << "`password` = " << db_->escapeString(password_) << " , " - << "`coins` = " << coin_balance << " , " - << "`tournamentBalance` = " << tournament_coin_balance << " , " - << "`premdays` = " << premium_remaining_days_ << " , " - << "`lastday` = " << premium_last_day_; + std::ostringstream query; - if (id_ != 0) { - query << " WHERE `id` = " << id_; - } else if (!email_.empty()) { - query << " WHERE `email` = " << email_; - } + query << "UPDATE `accounts` SET " + << "`email` = " << db_->escapeString(email_) << " , " + << "`type` = " << account_type_ << " , " + << "`password` = " << db_->escapeString(password_) << " , " + << "`coins` = " << coin_balance << " , " + << "`tournamentBalance` = " << tournament_coin_balance << " , " + << "`premdays` = " << premium_remaining_days_ << " , " + << "`lastday` = " << premium_last_day_; + + if (id_ != 0) { + query << " WHERE `id` = " << id_; + } else if (!email_.empty()) { + query << " WHERE `email` = " << email_; + } - if (!db_->executeQuery(query.str())) { - return ERROR_DB; - } + if (!db_->executeQuery(query.str())) { + return ERROR_DB; + } - return ERROR_NO; + return ERROR_NO; } /******************************************************************************* @@ -295,138 +305,138 @@ error_t Account::SaveAccountDB() { ******************************************************************************/ error_t Account::SetID(uint32_t id) { - if (id == 0) { - return ERROR_INVALID_ID; - } - id_ = id; - return ERROR_NO; + if (id == 0) { + return ERROR_INVALID_ID; + } + id_ = id; + return ERROR_NO; } error_t Account::GetID(uint32_t *id) { - if (id == nullptr) { - return ERROR_NULLPTR; - } + if (id == nullptr) { + return ERROR_NULLPTR; + } - *id = id_; - return ERROR_NO; + *id = id_; + return ERROR_NO; } error_t Account::SetEmail(std::string email) { - if (email.empty()) { - return ERROR_INVALID_ACCOUNT_EMAIL; - } - email_ = email; - return ERROR_NO; + if (email.empty()) { + return ERROR_INVALID_ACCOUNT_EMAIL; + } + email_ = email; + return ERROR_NO; } error_t Account::GetEmail(std::string *email) { - if (email == nullptr) { - return ERROR_NULLPTR; - } + if (email == nullptr) { + return ERROR_NULLPTR; + } - *email = email_; - return ERROR_NO; + *email = email_; + return ERROR_NO; } error_t Account::SetPassword(std::string password) { - if (password.empty()) { - return ERROR_INVALID_ACC_PASSWORD; - } - password_ = password; - return ERROR_NO; + if (password.empty()) { + return ERROR_INVALID_ACC_PASSWORD; + } + password_ = password; + return ERROR_NO; } error_t Account::GetPassword(std::string *password) { - if (password == nullptr) { - return ERROR_NULLPTR; - } + if (password == nullptr) { + return ERROR_NULLPTR; + } - *password = password_; - return ERROR_NO; + *password = password_; + return ERROR_NO; } error_t Account::SetPremiumRemaningDays(uint32_t days) { - premium_remaining_days_ = days; - return ERROR_NO; + premium_remaining_days_ = days; + return ERROR_NO; } error_t Account::GetPremiumRemaningDays(uint32_t *days) { - if (days == nullptr) { - return ERROR_NULLPTR; - } + if (days == nullptr) { + return ERROR_NULLPTR; + } - *days = premium_remaining_days_; - return ERROR_NO; + *days = premium_remaining_days_; + return ERROR_NO; } error_t Account::SetPremiumLastDay(time_t last_day) { - if (last_day < 0) { - return ERROR_INVALID_LAST_DAY; - } - premium_last_day_ = last_day; - return ERROR_NO; + if (last_day < 0) { + return ERROR_INVALID_LAST_DAY; + } + premium_last_day_ = last_day; + return ERROR_NO; } error_t Account::GetPremiumLastDay(time_t *last_day) { - if (last_day == nullptr) { - return ERROR_NULLPTR; - } + if (last_day == nullptr) { + return ERROR_NULLPTR; + } - *last_day = premium_last_day_; - return ERROR_NO; + *last_day = premium_last_day_; + return ERROR_NO; } error_t Account::SetStoreCoinBalance(uint32_t coins) { - if (coins == 0) { - return ERROR_INVALID_ID; - } + if (coins == 0) { + return ERROR_INVALID_ID; + } - coin_balance = coins; - return ERROR_NO; + coin_balance = coins; + return ERROR_NO; } error_t Account::GetStoreCoinBalance(uint32_t *coins) { - if (coins == nullptr) { - return ERROR_NULLPTR; - } + if (coins == nullptr) { + return ERROR_NULLPTR; + } - *coins = coin_balance; - return ERROR_NO; + *coins = coin_balance; + return ERROR_NO; } error_t Account::SetTournamentCoinBalance(uint32_t tournamentCoins) { - if (tournamentCoins == 0) { - return ERROR_INVALID_ID; - } + if (tournamentCoins == 0) { + return ERROR_INVALID_ID; + } - tournament_coin_balance = tournamentCoins; - return ERROR_NO; + tournament_coin_balance = tournamentCoins; + return ERROR_NO; } error_t Account::GetTournamentCoinBalance(uint32_t *tournamentCoins) { - if (tournamentCoins == nullptr) { - return ERROR_NULLPTR; - } + if (tournamentCoins == nullptr) { + return ERROR_NULLPTR; + } - *tournamentCoins = tournament_coin_balance; - return ERROR_NO; + *tournamentCoins = tournament_coin_balance; + return ERROR_NO; } error_t Account::SetAccountType(AccountType account_type) { - if (account_type > 5) { - return ERROR_INVALID_ACC_TYPE; - } - account_type_ = account_type; - return ERROR_NO; + if (account_type > 5) { + return ERROR_INVALID_ACC_TYPE; + } + account_type_ = account_type; + return ERROR_NO; } error_t Account::GetAccountType(AccountType *account_type) { - if (account_type == nullptr) { - return ERROR_NULLPTR; - } + if (account_type == nullptr) { + return ERROR_NULLPTR; + } - *account_type = account_type_; - return ERROR_NO; + *account_type = account_type_; + return ERROR_NO; } error_t Account::GetAccountPlayer(Player *player, std::string& characterName) { @@ -438,11 +448,11 @@ error_t Account::GetAccountPlayer(Player *player, std::string& characterName) { } error_t Account::GetAccountPlayers(std::vector *players) { - if (players == nullptr) { - return ERROR_NULLPTR; - } + if (players == nullptr) { + return ERROR_NULLPTR; + } - return this->LoadAccountPlayersDB(players); + return this->LoadAccountPlayersDB(players); } -} // namespace account +} // namespace account diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index c18b35d96dd..66a865a8934 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -9,7 +9,7 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along @@ -31,46 +31,46 @@ namespace account { enum Errors : uint8_t { - ERROR_NO = 0, - ERROR_DB, - ERROR_INVALID_ACCOUNT_EMAIL, - ERROR_INVALID_ACC_PASSWORD, - ERROR_INVALID_ACC_TYPE, - ERROR_INVALID_ID, - ERROR_INVALID_LAST_DAY, - ERROR_LOADING_ACCOUNT_PLAYERS, - ERROR_NOT_INITIALIZED, - ERROR_NULLPTR, - ERROR_VALUE_NOT_ENOUGH_COINS, - ERROR_VALUE_OVERFLOW, - ERROR_PLAYER_NOT_FOUND + ERROR_NO = 0, + ERROR_DB, + ERROR_INVALID_ACCOUNT_EMAIL, + ERROR_INVALID_ACC_PASSWORD, + ERROR_INVALID_ACC_TYPE, + ERROR_INVALID_ID, + ERROR_INVALID_LAST_DAY, + ERROR_LOADING_ACCOUNT_PLAYERS, + ERROR_NOT_INITIALIZED, + ERROR_NULLPTR, + ERROR_VALUE_NOT_ENOUGH_COINS, + ERROR_VALUE_OVERFLOW, + ERROR_PLAYER_NOT_FOUND }; enum AccountType : uint8_t { - ACCOUNT_TYPE_NORMAL = 1, - ACCOUNT_TYPE_TUTOR = 2, - ACCOUNT_TYPE_SENIORTUTOR = 3, - ACCOUNT_TYPE_GAMEMASTER = 4, - ACCOUNT_TYPE_GOD = 5 + ACCOUNT_TYPE_NORMAL = 1, + ACCOUNT_TYPE_TUTOR = 2, + ACCOUNT_TYPE_SENIORTUTOR = 3, + ACCOUNT_TYPE_GAMEMASTER = 4, + ACCOUNT_TYPE_GOD = 5 }; enum GroupType : uint8_t { - GROUP_TYPE_NORMAL = 1, - GROUP_TYPE_TUTOR = 2, - GROUP_TYPE_SENIORTUTOR = 3, - GROUP_TYPE_GAMEMASTER = 4, - GROUP_TYPE_COMMUNITYMANAGER = 5, - GROUP_TYPE_GOD = 6 + GROUP_TYPE_NORMAL = 1, + GROUP_TYPE_TUTOR = 2, + GROUP_TYPE_SENIORTUTOR = 3, + GROUP_TYPE_GAMEMASTER = 4, + GROUP_TYPE_COMMUNITYMANAGER = 5, + GROUP_TYPE_GOD = 6 }; enum CoinTransactionType : uint8_t { - COIN_ADD = 1, - COIN_REMOVE = 2 + COIN_ADD = 1, + COIN_REMOVE = 2 }; typedef struct { - std::string name; - uint64_t deletion; + std::string name; + uint64_t deletion; } Player; /** @@ -79,172 +79,172 @@ typedef struct { */ class Account { public: - /** - * @brief Construct a new Account object - * - */ - Account(); - - /** - * @brief Construct a new Account object - * - * @param id Set Account ID to be used by LoadAccountDB - */ - explicit Account(uint32_t id); - - /** - * @brief Construct a new Account object - * - * @param name Set Account Name to be used by LoadAccountDB - */ - explicit Account(const std::string &name); - - /*************************************************************************** - * Interfaces - **************************************************************************/ - - /** - * @brief Set the Database Interface used to get and set information from - * the database - * - * @param database Database Interface pointer to be used - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SetDatabaseInterface(Database *database); - - /** - * @brief Set the Database Tasks Interface used to schedule db update - * - * @param database Database Interface pointer to be used - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SetDatabaseTasksInterface(DatabaseTasks *db_tasks); - - - /*************************************************************************** - * Coins Methods - **************************************************************************/ - - /** - * @brief Get the amount of coins that the account has from database. - * - * @param accountId Account ID to get the coins. - * @param coins Pointer to return the number of coins - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t GetCoins(CoinType_t coinType = COIN_TYPE_DEFAULT); - - /** - * @brief Add coins to the account and update database. - * - * @param amount Amount of coins to be added - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t AddCoins(int32_t amount); - - /** - * @brief Removes coins from the account and update database. - * - * @param amount Amount of coins to be removed - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RemoveCoins(int32_t amount); - - /** - * @brief Register account coins transactions in database. - * - * @param type Type of the transaction(Add/Remove). - * @param coins Amount of coins - * @param description Description of the transaction - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust); - - /*************************************************************************** - * Database - **************************************************************************/ - - /** - * @brief Try to load account from DB using Account ID or Name if they were - * initialized. - * - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(); - - /** - * @brief Try to - * - * @param name - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(std::string name); - - /** - * @brief - * - * @param id - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(uint32_t id); - - /** - * @brief - * - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SaveAccountDB(); - - - /*************************************************************************** - * Setters and Getters - **************************************************************************/ - - error_t GetID(uint32_t *id); - - error_t SetEmail(std::string name); - error_t GetEmail(std::string *name); - - error_t SetPassword(std::string password); - error_t GetPassword(std::string *password); - - error_t SetPremiumRemaningDays(uint32_t days); - error_t GetPremiumRemaningDays(uint32_t *days); - - error_t SetStoreCoinBalance(uint32_t coins); - error_t GetStoreCoinBalance(uint32_t *coins); - - error_t SetTournamentCoinBalance(uint32_t tournamentCoins); - error_t GetTournamentCoinBalance(uint32_t *tournamentCoins); - - error_t SetPremiumLastDay(time_t last_day); - error_t GetPremiumLastDay(time_t *last_day); - - error_t SetAccountType(AccountType account_type); - error_t GetAccountType(AccountType *account_type); - - error_t GetAccountPlayer(Player *player, std::string& characterName); - error_t GetAccountPlayers(std::vector *players); + /** + * @brief Construct a new Account object + * + */ + Account(); + + /** + * @brief Construct a new Account object + * + * @param id Set Account ID to be used by LoadAccountDB + */ + explicit Account(uint32_t id); + + /** + * @brief Construct a new Account object + * + * @param name Set Account Name to be used by LoadAccountDB + */ + explicit Account(const std::string &name); + + /*************************************************************************** + * Interfaces + **************************************************************************/ + + /** + * @brief Set the Database Interface used to get and set information from + * the database + * + * @param database Database Interface pointer to be used + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t SetDatabaseInterface(Database *database); + + /** + * @brief Set the Database Tasks Interface used to schedule db update + * + * @param database Database Interface pointer to be used + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t SetDatabaseTasksInterface(DatabaseTasks *db_tasks); + + + /*************************************************************************** + * Coins Methods + **************************************************************************/ + + /** + * @brief Get the amount of coins that the account has from database. + * + * @param accountId Account ID to get the coins. + * @param coins Pointer to return the number of coins + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t GetCoins(CoinType_t coinType = COIN_TYPE_DEFAULT); + + /** + * @brief Add coins to the account and update database. + * + * @param amount Amount of coins to be added + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t AddCoins(int32_t amount, CoinType_t coinType = COIN_TYPE_DEFAULT); + + /** + * @brief Removes coins from the account and update database. + * + * @param amount Amount of coins to be removed + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t RemoveCoins(int32_t amount, CoinType_t coinType = COIN_TYPE_DEFAULT); + + /** + * @brief Register account coins transactions in database. + * + * @param type Type of the transaction(Add/Remove). + * @param coins Amount of coins + * @param description Description of the transaction + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust); + + /*************************************************************************** + * Database + **************************************************************************/ + + /** + * @brief Try to load account from DB using Account ID or Name if they were + * initialized. + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t LoadAccountDB(); + + /** + * @brief Try to + * + * @param name + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t LoadAccountDB(std::string name); + + /** + * @brief + * + * @param id + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t LoadAccountDB(uint32_t id); + + /** + * @brief + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t SaveAccountDB(); + + + /*************************************************************************** + * Setters and Getters + **************************************************************************/ + + error_t GetID(uint32_t *id); + + error_t SetEmail(std::string name); + error_t GetEmail(std::string *name); + + error_t SetPassword(std::string password); + error_t GetPassword(std::string *password); + + error_t SetPremiumRemaningDays(uint32_t days); + error_t GetPremiumRemaningDays(uint32_t *days); + + error_t SetStoreCoinBalance(uint32_t coins); + error_t GetStoreCoinBalance(uint32_t *coins); + + error_t SetTournamentCoinBalance(uint32_t tournamentCoins); + error_t GetTournamentCoinBalance(uint32_t *tournamentCoins); + + error_t SetPremiumLastDay(time_t last_day); + error_t GetPremiumLastDay(time_t *last_day); + + error_t SetAccountType(AccountType account_type); + error_t GetAccountType(AccountType *account_type); + + error_t GetAccountPlayer(Player *player, std::string& characterName); + error_t GetAccountPlayers(std::vector *players); private: - error_t SetID(uint32_t id); - error_t LoadAccountDB(std::ostringstream &query); - error_t LoadAccountPlayersDB(std::vector *players); - error_t LoadAccountPlayerDB(Player *player, std::string& characterName); - - Database *db_; - DatabaseTasks *db_tasks_; - - uint32_t id_; - std::string email_; - std::string password_; - uint32_t premium_remaining_days_; - time_t premium_last_day_; - uint32_t coin_balance; - uint32_t tournament_coin_balance; - AccountType account_type_; + error_t SetID(uint32_t id); + error_t LoadAccountDB(std::ostringstream &query); + error_t LoadAccountPlayersDB(std::vector *players); + error_t LoadAccountPlayerDB(Player *player, std::string& characterName); + + Database *db_; + DatabaseTasks *db_tasks_; + + uint32_t id_; + std::string email_; + std::string password_; + uint32_t premium_remaining_days_; + time_t premium_last_day_; + uint32_t coin_balance; + uint32_t tournament_coin_balance; + AccountType account_type_; }; -} // namespace account +} // namespace account -#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ +#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 07252f2c35b..5c4e38a1f21 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4649,10 +4649,9 @@ bool Player::canRemoveStoreCoins(int32_t coins, CoinType_t coinType) account::Account account(this->getAccount()); account.LoadAccountDB(); if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - account.GetStoreCoinBalance(&(coinBalance)); - this->coinBalance = account.GetCoins(); + this->coinBalance = account.GetCoins(coinType); } else if (coinType == COIN_TYPE_TOURNAMENT) { - tournamentCoinBalance = account.GetCoins(coinType); + this->tournamentCoinBalance = account.GetCoins(coinType); } } diff --git a/src/game/game.cpp b/src/game/game.cpp index 2bbf276a5a5..5cce3f6ad58 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8988,3 +8988,52 @@ void Game::queueSendStoreAlertToUser(uint32_t playerId, std::string message, Sto player->sendStoreError(storeErrorCode, message); } + +void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipient, uint16_t amount) { + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (player->getStoreCoinBalance() < amount) { + return; + } + + if (amount % g_config.getNumber(STORE_COIN_PACKET) != 0) { + return; + } + + Player* recipientPlayer = getPlayerByName(recipient); + if (!recipientPlayer) { + recipientPlayer = new Player(nullptr); + if (!IOLoginData::loadPlayerByName(recipientPlayer, recipient)) { + delete recipientPlayer; + return; + } + } + + if (recipientPlayer->getAccount() == player->getAccount()) { + return; + } + + std::string description(player->getName() + " transferred to " + recipient); + + account::Account account(player->getAccount()); + account.LoadAccountDB(); + account.AddCoins(-static_cast(amount)); + player->coinBalance -= amount; + account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, -static_cast(amount)); + + account.AddCoins(-static_cast(amount)); + account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, amount); + recipientPlayer->coinBalance += amount; + + if (recipientPlayer->isOffline()) { + IOLoginData::savePlayer(recipientPlayer); + delete recipientPlayer; + } else { + recipientPlayer->sendStoreCoinBalance(); + } + + player->sendStoreCoinBalance(); +} diff --git a/src/game/game.h b/src/game/game.h index fd26d815ff7..2b7a81c9ebc 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -521,6 +521,7 @@ class Game void playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std::string& param); void playerStoreTransactionHistory(uint32_t playerId, uint32_t pages, uint8_t entryPages); void queueSendStoreAlertToUser(uint32_t playerId, std::string message, StoreErrors_t storeErrorCode = STORE_ERROR_NETWORK); + void playerStoreCoinTransfer(uint32_t playerId, const std::string &recipient, uint16_t amount); private: void checkImbuements(); diff --git a/src/lua/functions/core/game/lua_enums.hpp b/src/lua/functions/core/game/lua_enums.hpp index 329977e2eed..87eeebe7d28 100644 --- a/src/lua/functions/core/game/lua_enums.hpp +++ b/src/lua/functions/core/game/lua_enums.hpp @@ -1018,6 +1018,10 @@ class LuaEnums final : LuaScriptInterface { registerEnum(L, WEBHOOK_COLOR_OFFLINE); registerEnum(L, WEBHOOK_COLOR_WARNING); registerEnum(L, WEBHOOK_COLOR_RAID); + + registerEnum(L, COIN_TYPE_DEFAULT); + registerEnum(L, COIN_TYPE_TRANSFERABLE); + registerEnum(L, COIN_TYPE_TOURNAMENT); #undef registerEnum } diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index a4fa4cce3ab..fc053e6d0d8 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2290,26 +2290,31 @@ int PlayerFunctions::luaPlayerRemovePremiumDays(lua_State* L) { } int PlayerFunctions::luaPlayerGetStoreCoins(lua_State* L) { - // player:getStoreCoins([coinType = COIN_TYPE_DEFAULT]) + // player:getStoreCoins(coinType) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - lua_pushnumber(L, player->getStoreCoinBalance(getNumber(L, 2, COIN_TYPE_DEFAULT))); + CoinType_t coinType = getNumber(L, 2); + if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { + lua_pushnumber(L, player->getStoreCoinBalance(coinType)); + } else { + lua_pushnil(L); + } return 1; } int PlayerFunctions::luaPlayerAddStoreCoins(lua_State* L) { - // player:addStoreCoins(coins, [coinType = COIN_TYPE_DEFAULT]) + // player:addStoreCoins(coins, coinType) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - CoinType_t coinType = getNumber(L, 3, COIN_TYPE_DEFAULT); + CoinType_t coinType = getNumber(L, 3); if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { int32_t coins = getNumber(L, 2); int32_t addCoins = std::min(std::numeric_limits::max() - player->getStoreCoinBalance(coinType), coins); @@ -2317,7 +2322,7 @@ int PlayerFunctions::luaPlayerAddStoreCoins(lua_State* L) { account::Account account(player->getAccount()); account.LoadAccountDB(); player->setStoreCoins(player->getStoreCoinBalance(coinType) + addCoins, coinType); - account.AddCoins(addCoins); + account.AddCoins(addCoins, coinType); lua_pushnumber(L, addCoins); } } @@ -2325,23 +2330,29 @@ int PlayerFunctions::luaPlayerAddStoreCoins(lua_State* L) { } int PlayerFunctions::luaPlayerRemoveStoreCoins(lua_State* L) { - // player:removeStoreCoins(coins) + // player:removeStoreCoins(coins, coinType) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - uint32_t coins = getNumber(L, 2); - - account::Account account(player->getAccount()); - account.LoadAccountDB(); - if (account.RemoveCoins(coins)) { - lua_pushnumber(L, account.GetCoins()); - } else { - lua_pushnil(L); + int32_t coins = getNumber(L, 2); + CoinType_t coinType = getNumber(L, 3); + if (!player->canRemoveStoreCoins(coins, coinType)) { + pushBoolean(L, false); + return 1; } + int32_t removeCoins = std::min(player->getStoreCoinBalance(coinType), coins); + if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { + if (removeCoins > 0) { + account::Account account(player->getAccount()); + account.LoadAccountDB(); + player->setStoreCoins(player->getStoreCoinBalance(coinType) - removeCoins, coinType); + account.RemoveCoins(removeCoins, coinType); + } + } return 1; } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 01eebe30272..bfd488dd937 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -740,7 +740,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xE7: /* thank you */ break; case 0xE8: parseDebugAssert(msg); break; case 0xEE: parseGreet(msg); break; - case 0xEF: parseCoinTransfer(msg); break; + case 0xEF: parseStoreCoinTransfer(msg); break; case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; case 0xF1: parseQuestLine(msg); break; // case 0xF2: parseRuleViolationReport(msg); break; @@ -2426,6 +2426,13 @@ void ProtocolGame::parseGreet(NetworkMessage &msg) addGameTask(&Game::playerNpcGreet, player->getID(), npcId); } +void ProtocolGame::parseStoreCoinTransfer(NetworkMessage &msg) { + std::string recipient = msg.getString(); + uint16_t amount = msg.get(); + + addGameTask(&Game::playerStoreCoinTransfer, player->getID(), recipient, amount); +} + void ProtocolGame::parseDebugAssert(NetworkMessage &msg) { if (debugAssertSent) @@ -2502,11 +2509,6 @@ void ProtocolGame::parseMarketBrowse(NetworkMessage &msg) } } -void ProtocolGame::parseCoinTransfer(NetworkMessage& msg) { - std::string recipient = msg.getString(); - uint16_t amount = msg.get(); -} - void ProtocolGame::parseMarketCreateOffer(NetworkMessage &msg) { uint8_t type = msg.getByte(); diff --git a/src/server/network/protocol/protocolgame.h b/src/server/network/protocol/protocolgame.h index 348fe0de576..c8140e52cdb 100644 --- a/src/server/network/protocol/protocolgame.h +++ b/src/server/network/protocol/protocolgame.h @@ -142,6 +142,7 @@ class ProtocolGame final : public Protocol void parseTournamentLeaderboard(NetworkMessage &msg); void parseGreet(NetworkMessage &msg); + void parseStoreCoinTransfer(NetworkMessage &msg); void parseBugReport(NetworkMessage &msg); void parseDebugAssert(NetworkMessage &msg); void parseRuleViolationReport(NetworkMessage &msg); @@ -222,8 +223,6 @@ class ProtocolGame final : public Protocol void parseOpenPrivateChannel(NetworkMessage &msg); void parseCloseChannel(NetworkMessage &msg); - void parseCoinTransfer(NetworkMessage &msg); - // imbue info void addImbuementInfo(NetworkMessage &msg, uint32_t imbuid); From 0d5ff6317dee740d821859978b031980641807a5 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 15 Aug 2021 02:05:09 -0300 Subject: [PATCH 08/30] Fix bug add/remove//get store coins and tournament store coins script --- data/scripts/talkactions/god/store_coins.lua | 6 +++--- data/scripts/talkactions/god/store_tournament_coins.lua | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/scripts/talkactions/god/store_coins.lua b/data/scripts/talkactions/god/store_coins.lua index 0bf69ea5e57..4a62336b3dd 100644 --- a/data/scripts/talkactions/god/store_coins.lua +++ b/data/scripts/talkactions/god/store_coins.lua @@ -25,7 +25,7 @@ function getCoins.onSay(player, words, param) return false end - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. targetPlayer:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") return true end @@ -78,7 +78,7 @@ function addCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:addStoreCoins(coins, COIN_TYPE_DEFAULT) + targetPlayer:addStoreCoins(coins, COIN_TYPE_DEFAULT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store coins to your account.") -- Distro log @@ -135,7 +135,7 @@ function removeCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:removeStoreCoins(coins, COIN_TYPE_DEFAULT) + targetPlayer:removeStoreCoins(coins, COIN_TYPE_DEFAULT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store coins to your account.") -- Distro log diff --git a/data/scripts/talkactions/god/store_tournament_coins.lua b/data/scripts/talkactions/god/store_tournament_coins.lua index eda88e1c769..fd9c9f4dffc 100644 --- a/data/scripts/talkactions/god/store_tournament_coins.lua +++ b/data/scripts/talkactions/god/store_tournament_coins.lua @@ -25,7 +25,7 @@ function getTournamentCoins.onSay(player, words, param) return false end - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. player:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. targetPlayer:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") return true end @@ -78,7 +78,7 @@ function addTournamentCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:addStoreCoins(coins, COIN_TYPE_TOURNAMENT) + targetPlayer:addStoreCoins(coins, COIN_TYPE_TOURNAMENT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store tournament coins to your account.") -- Distro log @@ -135,7 +135,7 @@ function removeTournamentCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:removeStoreCoins(coins, COIN_TYPE_TOURNAMENT) + targetPlayer:removeStoreCoins(coins, COIN_TYPE_TOURNAMENT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store tournament coins to your account.") -- Distro log From 58204e2e698a119f2d3fe0287de8fc54b3a35b00 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 15 Aug 2021 03:04:06 -0300 Subject: [PATCH 09/30] Fix bug in store coins transfer --- schema.sql | 33 ++--- src/creatures/players/account/account.cpp | 5 +- src/game/game.cpp | 10 +- src/io/iologindata.cpp | 160 +++++++++++++--------- src/io/iologindata.h | 3 + 5 files changed, 118 insertions(+), 93 deletions(-) diff --git a/schema.sql b/schema.sql index 8dc420d8e89..9b785e585ae 100644 --- a/schema.sql +++ b/schema.sql @@ -25,18 +25,6 @@ CREATE TABLE IF NOT EXISTS `accounts` ( CONSTRAINT `accounts_unique` UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --- Table structure `coins_transactions` -CREATE TABLE IF NOT EXISTS `coins_transactions` ( - `account_id` int(11) NOT NULL, - `mode` tinyint(1) NOT NULL DEFAULT 0, - `amount` int(11) NOT NULL, - `coinMode` tinyint(2) NOT NULL DEFAULT 0, - `description` varchar(255) DEFAULT NULL, - `cust` int(11) NOT NULL, - `time` bigint(20) DEFAULT NULL, - INDEX `account_id` (`account_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -- Table structure `players` CREATE TABLE IF NOT EXISTS `players` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -669,19 +657,14 @@ CREATE TABLE IF NOT EXISTS `player_storage` ( -- Table structure `store_history` CREATE TABLE IF NOT EXISTS `store_history` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `account_id` int(11) UNSIGNED NOT NULL, - `mode` smallint(2) NOT NULL DEFAULT '0', - `description` varchar(3500) NOT NULL, - `coin_amount` int(12) NOT NULL, - `time` bigint(20) UNSIGNED NOT NULL, - `timestamp` int(11) NOT NULL DEFAULT '0', - `coins` int(11) NOT NULL DEFAULT '0', - INDEX `account_id` (`account_id`), - CONSTRAINT `store_history_pk` PRIMARY KEY (`id`), - CONSTRAINT `store_history_account_fk` - FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) - ON DELETE CASCADE + `account_id` int(11) NOT NULL, + `mode` tinyint(1) NOT NULL DEFAULT 0, + `amount` int(11) NOT NULL, + `coinMode` tinyint(2) NOT NULL DEFAULT 0, + `description` varchar(255) DEFAULT NULL, + `cust` int(11) NOT NULL, + `time` bigint(20) DEFAULT NULL, + INDEX `account_id` (`account_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `tile_store` diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index 7d033304108..299f44ad31d 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -162,7 +162,7 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t } std::ostringstream query; - query << "INSERT INTO `coins_transactions` (`account_id`,`time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" + query << "INSERT INTO `store_history` (`account_id`, `time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" << id_ << "," << time << "," << static_cast(mode) << "," @@ -177,7 +177,8 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); g_game.addAccountHistory(id_, historyOffer); - return db_->executeQuery(query.str()); + db_->executeQuery(query.str()); + return ERROR_NO; } /******************************************************************************* diff --git a/src/game/game.cpp b/src/game/game.cpp index 5cce3f6ad58..c953a9a8258 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8992,10 +8992,12 @@ void Game::queueSendStoreAlertToUser(uint32_t playerId, std::string message, Sto void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipient, uint16_t amount) { Player* player = getPlayerByID(playerId); if (!player) { + SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player with id '{}' not exist", player->getName()); return; } if (player->getStoreCoinBalance() < amount) { + SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player {} not have coins for transfer", player->getName()); return; } @@ -9007,6 +9009,7 @@ void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipie if (!recipientPlayer) { recipientPlayer = new Player(nullptr); if (!IOLoginData::loadPlayerByName(recipientPlayer, recipient)) { + SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player with name '{}' not exist", player->getName()); delete recipientPlayer; return; } @@ -9018,13 +9021,14 @@ void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipie std::string description(player->getName() + " transferred to " + recipient); - account::Account account(player->getAccount()); - account.LoadAccountDB(); + account::Account account; + account.LoadAccountDB(player->getAccount()); account.AddCoins(-static_cast(amount)); player->coinBalance -= amount; account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, -static_cast(amount)); - account.AddCoins(-static_cast(amount)); + account.LoadAccountDB(recipientPlayer->getAccount()); + account.AddCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, amount); recipientPlayer->coinBalance += amount; diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 800ba7de781..3010bc29860 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -33,37 +33,37 @@ extern Game g_game; extern Monsters g_monsters; bool IOLoginData::authenticateAccountPassword(const std::string& email, const std::string& password, account::Account *account) { - if (account::ERROR_NO != account->LoadAccountDB(email)) { - SPDLOG_ERROR("Email {} doesn't match any account.", email); - return false; - } - - std::string accountPassword; - account->GetPassword(&accountPassword); - if (transformToSHA1(password) != accountPassword) { - SPDLOG_ERROR("Password '{}' doesn't match any account", transformToSHA1(password)); - return false; - } - - return true; + if (account::ERROR_NO != account->LoadAccountDB(email)) { + SPDLOG_ERROR("Email {} doesn't match any account.", email); + return false; + } + + std::string accountPassword; + account->GetPassword(&accountPassword); + if (transformToSHA1(password) != accountPassword) { + SPDLOG_ERROR("Password '{}' doesn't match any account", transformToSHA1(password)); + return false; + } + + return true; } bool IOLoginData::gameWorldAuthentication(const std::string& email, const std::string& password, std::string& characterName, uint32_t *accountId) { - account::Account account; - if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { - return false; - } + account::Account account; + if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { + return false; + } - account::Player player; - if (account::ERROR_NO != account.GetAccountPlayer(&player, characterName)) { - SPDLOG_ERROR("Player not found or deleted for account."); - return false; - } + account::Player player; + if (account::ERROR_NO != account.GetAccountPlayer(&player, characterName)) { + SPDLOG_ERROR("Player not found or deleted for account."); + return false; + } - account.GetID(accountId); + account.GetID(accountId); - return true; + return true; } account::AccountType IOLoginData::getAccountType(uint32_t accountId) @@ -84,6 +84,37 @@ void IOLoginData::setAccountType(uint32_t accountId, account::AccountType accoun Database::getInstance().executeQuery(query.str()); } +bool IOLoginData::loadAccountStoreHistory(uint32_t accountId) +{ + std::vector history; + g_game.getAccountHistory(accountId, history); + if (!history.empty()) { + return false; + } + + // Load from database + std::ostringstream query; + query << "SELECT `time`, `mode`, `amount`, `coinMode`, `description`, `cust` FROM `store_history` WHERE `account_id` = " << accountId; + DBResult_ptr result = Database::getInstance().storeQuery(query.str()); + if (result) { + do { + history.emplace_back( + result->getNumber("time"), + static_cast(result->getNumber("mode")), + result->getNumber("amount"), + static_cast(result->getNumber("coinMode")), + result->getString("description"), + result->getNumber("cust") + ); + history.shrink_to_fit(); + + } while (result->next()); + } + + g_game.loadAccountStoreHistory(accountId, history); + return true; +} + void IOLoginData::updateOnlineStatus(uint32_t guid, bool login) { if (g_config.getBoolean(ALLOW_CLONES)) { @@ -122,7 +153,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) Group* group = g_game.groups.getGroup(result->getNumber("group_id")); if (!group) { SPDLOG_ERROR("Player {} has group id {} whitch doesn't exist", player->name, - result->getNumber("group_id")); + result->getNumber("group_id")); return false; } player->setGroup(group); @@ -345,7 +376,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) if (!player->setVocation(result->getNumber("vocation"))) { SPDLOG_ERROR("Player {} has vocation id {} whitch doesn't exist", - player->name, result->getNumber("vocation")); + player->name, result->getNumber("vocation")); return false; } @@ -407,7 +438,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) Town* town = g_game.map.towns.getTown(result->getNumber("town_id")); if (!town) { SPDLOG_ERROR("Player {} has town id {} whitch doesn't exist", player->name, - result->getNumber("town_id")); + result->getNumber("town_id")); return false; } @@ -500,36 +531,36 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) query.str(std::string()); query << "SELECT * FROM `player_charms` WHERE `player_guid` = " << player->getGUID(); if ((result = db.storeQuery(query.str()))) { - player->charmPoints = result->getNumber("charm_points"); - player->charmExpansion = result->getNumber("charm_expansion"); - player->charmRuneWound = result->getNumber("rune_wound"); - player->charmRuneEnflame = result->getNumber("rune_enflame"); - player->charmRunePoison = result->getNumber("rune_poison"); - player->charmRuneFreeze = result->getNumber("rune_freeze"); - player->charmRuneZap = result->getNumber("rune_zap"); - player->charmRuneCurse = result->getNumber("rune_curse"); - player->charmRuneCripple = result->getNumber("rune_cripple"); - player->charmRuneParry = result->getNumber("rune_parry"); - player->charmRuneDodge = result->getNumber("rune_dodge"); - player->charmRuneAdrenaline = result->getNumber("rune_adrenaline"); - player->charmRuneNumb = result->getNumber("rune_numb"); - player->charmRuneCleanse = result->getNumber("rune_cleanse"); - player->charmRuneBless = result->getNumber("rune_bless"); - player->charmRuneScavenge = result->getNumber("rune_scavenge"); - player->charmRuneGut = result->getNumber("rune_gut"); - player->charmRuneLowBlow = result->getNumber("rune_low_blow"); - player->charmRuneDivine = result->getNumber("rune_divine"); - player->charmRuneVamp = result->getNumber("rune_vamp"); - player->charmRuneVoid = result->getNumber("rune_void"); - player->UsedRunesBit = result->getNumber("UsedRunesBit"); - player->UnlockedRunesBit = result->getNumber("UnlockedRunesBit"); - - unsigned long attrBestSize; - const char* Bestattr = result->getStream("tracker list", attrBestSize); - PropStream propBestStream; - propBestStream.init(Bestattr, attrBestSize); - - for (int i = 0; i <= propBestStream.size(); i++) { + player->charmPoints = result->getNumber("charm_points"); + player->charmExpansion = result->getNumber("charm_expansion"); + player->charmRuneWound = result->getNumber("rune_wound"); + player->charmRuneEnflame = result->getNumber("rune_enflame"); + player->charmRunePoison = result->getNumber("rune_poison"); + player->charmRuneFreeze = result->getNumber("rune_freeze"); + player->charmRuneZap = result->getNumber("rune_zap"); + player->charmRuneCurse = result->getNumber("rune_curse"); + player->charmRuneCripple = result->getNumber("rune_cripple"); + player->charmRuneParry = result->getNumber("rune_parry"); + player->charmRuneDodge = result->getNumber("rune_dodge"); + player->charmRuneAdrenaline = result->getNumber("rune_adrenaline"); + player->charmRuneNumb = result->getNumber("rune_numb"); + player->charmRuneCleanse = result->getNumber("rune_cleanse"); + player->charmRuneBless = result->getNumber("rune_bless"); + player->charmRuneScavenge = result->getNumber("rune_scavenge"); + player->charmRuneGut = result->getNumber("rune_gut"); + player->charmRuneLowBlow = result->getNumber("rune_low_blow"); + player->charmRuneDivine = result->getNumber("rune_divine"); + player->charmRuneVamp = result->getNumber("rune_vamp"); + player->charmRuneVoid = result->getNumber("rune_void"); + player->UsedRunesBit = result->getNumber("UsedRunesBit"); + player->UnlockedRunesBit = result->getNumber("UnlockedRunesBit"); + + unsigned long attrBestSize; + const char* Bestattr = result->getStream("tracker list", attrBestSize); + PropStream propBestStream; + propBestStream.init(Bestattr, attrBestSize); + + for (int i = 0; i <= propBestStream.size(); i++) { uint16_t raceid_t; if (propBestStream.read(raceid_t)) { MonsterType* tmp_tt = g_monsters.getMonsterTypeByRaceId(raceid_t); @@ -537,12 +568,12 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->addBestiaryTrackerList(tmp_tt); } } - } + } } else { - query.str(std::string()); - query << "INSERT INTO `player_charms` (`player_guid`) VALUES (" << player->getGUID() << ')'; - Database::getInstance().executeQuery(query.str()); + query.str(std::string()); + query << "INSERT INTO `player_charms` (`player_guid`) VALUES (" << player->getGUID() << ')'; + Database::getInstance().executeQuery(query.str()); } query.str(std::string()); @@ -758,6 +789,9 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) player->updateBaseSpeed(); player->updateInventoryWeight(); player->updateItemsLight(true); + + // Load account history + loadAccountStoreHistory(player->getAccount()); return true; } @@ -1017,12 +1051,12 @@ bool IOLoginData::savePlayer(Player* player) query << "DELETE FROM `player_stash` WHERE `player_id` = " << player->getGUID(); db.executeQuery(query.str()); for (auto it : player->getStashItems()) { - query.str(std::string()); + query.str(std::string()); query << "INSERT INTO `player_stash` (`player_id`,`item_id`,`item_count`) VALUES ("; query << player->getGUID() << ", "; query << it.first << ", "; query << it.second << ")"; - db.executeQuery(query.str()); + db.executeQuery(query.str()); } // learned spells diff --git a/src/io/iologindata.h b/src/io/iologindata.h index 00ac24d032b..bee74ad64e2 100644 --- a/src/io/iologindata.h +++ b/src/io/iologindata.h @@ -38,6 +38,9 @@ class IOLoginData uint32_t *accountId); static account::AccountType getAccountType(uint32_t accountId); static void setAccountType(uint32_t accountId, account::AccountType accountType); + + static bool loadAccountStoreHistory(uint32_t accountId); + static void updateOnlineStatus(uint32_t guid, bool login); static bool preloadPlayer(Player* player, const std::string& name); From 84b9adfa8a39d6b88a2fa3a798a9a9a51b753c87 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 15 Aug 2021 03:34:12 -0300 Subject: [PATCH 10/30] Move store coins talkactions to store folder --- data/scripts/talkactions/god/{ => store}/store_coins.lua | 0 .../talkactions/god/{ => store}/store_tournament_coins.lua | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename data/scripts/talkactions/god/{ => store}/store_coins.lua (100%) rename data/scripts/talkactions/god/{ => store}/store_tournament_coins.lua (100%) diff --git a/data/scripts/talkactions/god/store_coins.lua b/data/scripts/talkactions/god/store/store_coins.lua similarity index 100% rename from data/scripts/talkactions/god/store_coins.lua rename to data/scripts/talkactions/god/store/store_coins.lua diff --git a/data/scripts/talkactions/god/store_tournament_coins.lua b/data/scripts/talkactions/god/store/store_tournament_coins.lua similarity index 100% rename from data/scripts/talkactions/god/store_tournament_coins.lua rename to data/scripts/talkactions/god/store/store_tournament_coins.lua From a6d9307682115c668894bef47239751a4363fdee Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 15 Aug 2021 03:36:25 -0300 Subject: [PATCH 11/30] Fix not receiving outfit bug --- src/game/game.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/game/game.cpp b/src/game/game.cpp index c953a9a8258..1e26dc43ff2 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8746,6 +8746,8 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: successfully = true; returnmessage << "You've successfully bought the "<< thisOffer->getName() << "."; + player->addOutfit(thisOffer->getOutfitMale(), addons); + player->addOutfit(thisOffer->getOutfitFemale(), addons); } else if (offerType == OFFER_TYPE_MOUNT) { Mount* mount = thisOffer->getMount(); if (!mount || mount == nullptr) { From b8077c5c091d1178730bc658d239cb361f266aca Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 15 Aug 2021 09:34:18 -0300 Subject: [PATCH 12/30] Comment to explain the function --- src/io/iologindata.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/io/iologindata.h b/src/io/iologindata.h index bee74ad64e2..8d9cf0403e8 100644 --- a/src/io/iologindata.h +++ b/src/io/iologindata.h @@ -39,6 +39,7 @@ class IOLoginData static account::AccountType getAccountType(uint32_t accountId); static void setAccountType(uint32_t accountId, account::AccountType accountType); + // This function load the "store_history" database table static bool loadAccountStoreHistory(uint32_t accountId); static void updateOnlineStatus(uint32_t guid, bool login); From 9c3cb424025f8777c15c27d87b99f09bbc3087d0 Mon Sep 17 00:00:00 2001 From: dudantas Date: Fri, 20 Aug 2021 00:00:05 -0300 Subject: [PATCH 13/30] Somes small fixes and adjustments --- data/XML/store.xml | 18 ++++++------ .../talkactions/god/store/store_coins.lua | 20 ++++++++++--- .../god/store/store_tournament_coins.lua | 29 ++++++++++++++----- src/creatures/players/account/account.cpp | 8 ++--- src/creatures/players/vocations/vocation.h | 2 +- 5 files changed, 52 insertions(+), 25 deletions(-) diff --git a/data/XML/store.xml b/data/XML/store.xml index 1e95a9c43b3..26051ed938f 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -47,10 +47,10 @@ - - - - + + + + @@ -163,7 +163,7 @@ - + @@ -486,12 +486,12 @@ - + - - - + + + diff --git a/data/scripts/talkactions/god/store/store_coins.lua b/data/scripts/talkactions/god/store/store_coins.lua index 4a62336b3dd..ff1f181e8c0 100644 --- a/data/scripts/talkactions/god/store/store_coins.lua +++ b/data/scripts/talkactions/god/store/store_coins.lua @@ -12,6 +12,7 @@ function getCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[getCoins.onSay] - Player name param not found") return false end @@ -21,11 +22,13 @@ function getCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[getCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. targetPlayer:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have \z + ".. targetPlayer:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") return true end @@ -42,6 +45,7 @@ function addCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[addCoins.onSay] - Player name param not found") return false end @@ -50,6 +54,7 @@ function addCoins.onSay(player, words, param) -- Check if have all parameters (god and coinscount) if not split[2] then player:sendCancelMessage("Insufficient parameters") + -- Distro log Spdlog.error("[addCoins.onSay] - Insufficient parameters") return false end @@ -58,6 +63,7 @@ function addCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[addCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end @@ -79,8 +85,10 @@ function addCoins.onSay(player, words, param) targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) targetPlayer:addStoreCoins(coins, COIN_TYPE_DEFAULT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") - targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store coins to your account.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." \z + store coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added \z + ".. coins .." store coins to your account.") -- Distro log Spdlog.info("".. player:getName() .." added ".. coins .." store coins to ".. targetPlayer:getName() .." account.") return true @@ -99,6 +107,7 @@ function removeCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[removeCoins.onSay] - Player name param not found") return false end @@ -107,6 +116,7 @@ function removeCoins.onSay(player, words, param) -- Check if have all parameters (god and coinscount) if not split[2] then player:sendCancelMessage("Insufficient parameters") + -- Distro log Spdlog.error("[removeCoins.onSay] - Insufficient parameters") return false end @@ -115,6 +125,7 @@ function removeCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[removeCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end @@ -136,7 +147,8 @@ function removeCoins.onSay(player, words, param) targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) targetPlayer:removeStoreCoins(coins, COIN_TYPE_DEFAULT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store coins for the ".. targetPlayer:getName() .." account.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." \z + store coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store coins to your account.") -- Distro log Spdlog.info("".. player:getName() .." removed ".. coins .." store coins to ".. targetPlayer:getName() .." account.") diff --git a/data/scripts/talkactions/god/store/store_tournament_coins.lua b/data/scripts/talkactions/god/store/store_tournament_coins.lua index fd9c9f4dffc..e81e8c32631 100644 --- a/data/scripts/talkactions/god/store/store_tournament_coins.lua +++ b/data/scripts/talkactions/god/store/store_tournament_coins.lua @@ -12,6 +12,7 @@ function getTournamentCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[getTournamentCoins.onSay] - Player name param not found") return false end @@ -21,11 +22,13 @@ function getTournamentCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[getTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have ".. targetPlayer:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." \z + have ".. targetPlayer:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") return true end @@ -42,6 +45,7 @@ function addTournamentCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[addTournamentCoins.onSay] - Player name param not found") return false end @@ -50,6 +54,7 @@ function addTournamentCoins.onSay(player, words, param) -- Check if have all parameters (god and coinscount) if not split[2] then player:sendCancelMessage("Insufficient parameters") + -- Distro log Spdlog.error("[addTournamentCoins.onSay] - Insufficient parameters") return false end @@ -58,6 +63,7 @@ function addTournamentCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[addTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end @@ -79,10 +85,13 @@ function addTournamentCoins.onSay(player, words, param) targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) targetPlayer:addStoreCoins(coins, COIN_TYPE_TOURNAMENT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") - targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added ".. coins .." store tournament coins to your account.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." \z + store tournament coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z + added ".. coins .." store tournament coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." added ".. coins .." store tournament coins to ".. targetPlayer:getName() .." account.") + Spdlog.info("".. player:getName() .." added ".. coins .." \z + store tournament coins to ".. targetPlayer:getName() .." account.") return true end @@ -99,6 +108,7 @@ function removeTournamentCoins.onSay(player, words, param) -- Check the first param (player name) exists if param == "" then player:sendCancelMessage("Player name param required") + -- Distro log Spdlog.error("[removeTournamentCoins.onSay] - Player name param not found") return false end @@ -107,6 +117,7 @@ function removeTournamentCoins.onSay(player, words, param) -- Check if have all parameters (god and coinscount) if not split[2] then player:sendCancelMessage("Insufficient parameters") + -- Distro log Spdlog.error("[removeTournamentCoins.onSay] - Insufficient parameters") return false end @@ -115,6 +126,7 @@ function removeTournamentCoins.onSay(player, words, param) local targetPlayer = Player(split[1]) if not targetPlayer then player:sendCancelMessage("Player ".. string.titleCase(split[1]) .." is not online") + -- Distro log Spdlog.error("[removeTournamentCoins.onSay] - Player ".. string.titleCase(split[1]) .." is not online") return false end @@ -136,10 +148,13 @@ function removeTournamentCoins.onSay(player, words, param) targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) targetPlayer:removeStoreCoins(coins, COIN_TYPE_TOURNAMENT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." store tournament coins for the ".. targetPlayer:getName() .." account.") - targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store tournament coins to your account.") + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." \z + store tournament coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z + removed ".. coins .." store tournament coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." removed ".. coins .." store tournament coins to ".. targetPlayer:getName() .." account.") + Spdlog.info("".. player:getName() .." removed ".. coins .." store tournament \z + coins to ".. targetPlayer:getName() .." account.") return true end diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index 299f44ad31d..1ebd8eb9efc 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -119,14 +119,14 @@ error_t Account::GetCoins(CoinType_t coinType) { error_t Account::AddCoins(int32_t amount, CoinType_t coinType) { - std::string coins = "`coins`"; + std::string coins = "coins"; if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "`coins`"; + coins = "coins"; } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "`tournamentBalance`"; + coins = "tournamentBalance"; } std::ostringstream query; - query << "UPDATE `accounts` SET " << coins << " = " << coins << " + " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `" << coins << "` = `" << coins << "` + " << amount << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); return ERROR_NO; diff --git a/src/creatures/players/vocations/vocation.h b/src/creatures/players/vocations/vocation.h index 723641f0f6a..5c1c63d6c4a 100644 --- a/src/creatures/players/vocations/vocation.h +++ b/src/creatures/players/vocations/vocation.h @@ -140,7 +140,7 @@ class Vocations bool loadFromXml(); Vocation* getVocation(uint16_t id); - const std::map& getVocations() const {return vocationsMap;} + const std::map& getVocations() const {return vocationsMap;} int32_t getVocationId(const std::string& name) const; uint16_t getPromotedVocation(uint16_t vocationId) const; From 1fbbc96d56370c4600c9d4361aea860b84d0cbd0 Mon Sep 17 00:00:00 2001 From: dudantas Date: Fri, 20 Aug 2021 00:32:31 -0300 Subject: [PATCH 14/30] More fixes --- .../talkactions/god/store/store_coins.lua | 7 ++++--- .../god/store/store_tournament_coins.lua | 4 ++-- src/creatures/players/account/account.cpp | 2 +- src/creatures/players/store/store.cpp | 16 ++++++++-------- src/creatures/players/store/store.hpp | 4 ++-- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/data/scripts/talkactions/god/store/store_coins.lua b/data/scripts/talkactions/god/store/store_coins.lua index ff1f181e8c0..13274b62229 100644 --- a/data/scripts/talkactions/god/store/store_coins.lua +++ b/data/scripts/talkactions/god/store/store_coins.lua @@ -90,7 +90,7 @@ function addCoins.onSay(player, words, param) targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added \z ".. coins .." store coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." added ".. coins .." store coins to ".. targetPlayer:getName() .." account.") + Spdlog.info("".. player:getName() .." added ".. coins .." store coins to ".. targetPlayer:getName() .." account") return true end @@ -149,9 +149,10 @@ function removeCoins.onSay(player, words, param) targetPlayer:removeStoreCoins(coins, COIN_TYPE_DEFAULT) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." \z store coins for the ".. targetPlayer:getName() .." account.") - targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." removed ".. coins .." store coins to your account.") + targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z + removed ".. coins .." store coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." removed ".. coins .." store coins to ".. targetPlayer:getName() .." account.") + Spdlog.info("".. player:getName() .." removed ".. coins .." store coins to ".. targetPlayer:getName() .." account") return true end diff --git a/data/scripts/talkactions/god/store/store_tournament_coins.lua b/data/scripts/talkactions/god/store/store_tournament_coins.lua index e81e8c32631..69308c58bb7 100644 --- a/data/scripts/talkactions/god/store/store_tournament_coins.lua +++ b/data/scripts/talkactions/god/store/store_tournament_coins.lua @@ -91,7 +91,7 @@ function addTournamentCoins.onSay(player, words, param) added ".. coins .." store tournament coins to your account.") -- Distro log Spdlog.info("".. player:getName() .." added ".. coins .." \z - store tournament coins to ".. targetPlayer:getName() .." account.") + store tournament coins to ".. targetPlayer:getName() .." account") return true end @@ -154,7 +154,7 @@ function removeTournamentCoins.onSay(player, words, param) removed ".. coins .." store tournament coins to your account.") -- Distro log Spdlog.info("".. player:getName() .." removed ".. coins .." store tournament \z - coins to ".. targetPlayer:getName() .." account.") + coins to ".. targetPlayer:getName() .." account") return true end diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index 1ebd8eb9efc..be5222dbcef 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -99,7 +99,6 @@ error_t Account::GetCoins(CoinType_t coinType) { return ERROR_NOT_INITIALIZED; } - std::ostringstream query; std::string coins = "coins"; if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { coins = "coins"; @@ -107,6 +106,7 @@ error_t Account::GetCoins(CoinType_t coinType) { coins = "tournamentBalance"; } + std::ostringstream query; query << "SELECT `" << coins << "` FROM `accounts` WHERE `id` = " << id_; DBResult_ptr result = db_->storeQuery(query.str()); diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index f801c047bb5..68600f85b1d 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -474,18 +474,18 @@ std::vector Store::getHomeOffers() { std::vector filter; for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* oferta = getStoreOfferByName((*off)); - if (oferta) { + StoreOffer* storeOffer = getStoreOfferByName((*off)); + if (storeOffer) { bool hasDec = false; for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; ++off2) { - if ((*off2)->getName() == oferta->getName()){ + if ((*off2)->getName() == storeOffer->getName()){ hasDec = true; break; } } if (!hasDec) { - filter.emplace_back(oferta); + filter.emplace_back(storeOffer); } } } @@ -497,10 +497,10 @@ std::vector Store::getHomeOffers() { std::map> Store::getHomeOffersOrganized() { std::map> filter; for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* oferta = getStoreOfferByName((*off)); - if (oferta) { - std::string name = oferta->getName(); - filter[name].emplace_back(oferta); + StoreOffer* storeOffer = getStoreOfferByName((*off)); + if (storeOffer) { + std::string name = storeOffer->getName(); + filter[name].emplace_back(storeOffer); } } diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index f1ff535536c..7eddfcd44c5 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -333,7 +333,7 @@ class StoreOffer { OfferStates_t state = OFFER_STATE_NONE; CoinType_t coinType = COIN_TYPE_DEFAULT; OfferBuyTypes_t buyType = OFFER_BUY_TYPE_OTHERS; - uint16_t count = 1; + uint16_t count = 150; uint32_t price = 0; // Default price (This preventing valueless offers from entering) uint32_t basePrice = 0; // Default price (This preventing valueless offers from entering) uint32_t validUntil = 0; @@ -351,4 +351,4 @@ class StoreOffer { OfferTypes_t type = OFFER_TYPE_NONE; }; -#endif +##endif // SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ From cb64bd07b836058e10a6f5f2358e23614e629397 Mon Sep 17 00:00:00 2001 From: dudantas Date: Fri, 20 Aug 2021 00:36:10 -0300 Subject: [PATCH 15/30] Fix compilation error --- src/creatures/players/store/store.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index 7eddfcd44c5..c00c29699df 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -351,4 +351,4 @@ class StoreOffer { OfferTypes_t type = OFFER_TYPE_NONE; }; -##endif // SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ +#endif // SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ From 798e22072e63ee63da181567e0cba2238d0dda64 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 21 Aug 2021 11:05:52 -0300 Subject: [PATCH 16/30] Update src/creatures/creatures_definitions.hpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renato Foot Guimarães Costallat --- src/creatures/creatures_definitions.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index 832e000d08b..8bce17fe0b8 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -20,7 +20,6 @@ #ifndef SRC_CREATURES_CREATURES_DEFINITIONS_HPP_ #define SRC_CREATURES_CREATURES_DEFINITIONS_HPP_ -// Enum struct StoreHistory { StoreHistory(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust) : time(time), mode(mode), amount(amount), coinMode(coinMode), description(std::move(description)), cust(cust) {} @@ -34,6 +33,7 @@ struct StoreHistory { }; +// Enum enum SkillsId_t { SKILLVALUE_LEVEL = 0, SKILLVALUE_TRIES = 1, From 61a0ca4f31f946f2c2cd12eea8d828e5ae25ff04 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Sat, 21 Aug 2021 11:32:56 -0300 Subject: [PATCH 17/30] Update src/creatures/players/store/store.hpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Renato Foot Guimarães Costallat --- src/creatures/players/store/store.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index c00c29699df..bb5ecdd50f7 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -186,7 +186,7 @@ class Store { class StoreOffers { public: - StoreOffers(std::string name) : + explicit StoreOffers(std::string name) : name(std::move(name)) {} std::string getName() { From 075180042aa379163b19b2865c1e8c2e866652ce Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 22 Aug 2021 00:34:49 -0300 Subject: [PATCH 18/30] Moved the premium discount rate in the store to config.lua, and other fixes --- config.lua.dist | 4 +++ data/migrations/0.lua | 8 +++-- data/migrations/1.lua | 5 +++ schema.sql | 3 +- src/config/config_definitions.hpp | 2 ++ src/config/configmanager.cpp | 4 +++ src/creatures/players/account/account.cpp | 6 ++-- src/creatures/players/store/store.cpp | 38 +++++++++-------------- src/creatures/players/store/store.hpp | 20 ++---------- src/io/iologindata.cpp | 2 +- vcpkg.json | 22 ------------- 11 files changed, 41 insertions(+), 73 deletions(-) create mode 100644 data/migrations/1.lua delete mode 100644 vcpkg.json diff --git a/config.lua.dist b/config.lua.dist index a3684a2c797..33fc415e551 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -119,6 +119,10 @@ premiumToCreateMarketOffer = true checkExpiredMarketOffersEachMinutes = 60 maxMarketOffersAtATimePerPlayer = 100 +-- Store +storePremiumDiscount = true +rateStorePremiumDiscount = 1.0 + -- MySQL mysqlHost = "127.0.0.1" mysqlUser = "root" diff --git a/data/migrations/0.lua b/data/migrations/0.lua index c931d4cffc9..50d8834d222 100644 --- a/data/migrations/0.lua +++ b/data/migrations/0.lua @@ -1,5 +1,7 @@ --- return true = There are others migrations file --- return false = This is the last migration file function onUpdateDatabase() - return false + Spdlog.info("Updating database to version 1 (store c++)") + db.query("ALTER TABLE `accounts` ADD `tournament_coins` INT(11) NOT NULL DEFAULT 0") + db.query("DROP TABLE `coins_transactions`") + db.query([[CREATE TABLE IF NOT EXISTS `store_history` (`account_id` int(11) NOT NULL, `mode` smallint(2) NOT NULL DEFAULT '0', `description` VARCHAR(3500) NOT NULL, `coin_amount` int(12) NOT NULL, `time` bigint(20) unsigned NOT NULL, KEY `account_id` (`account_id`), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE) ENGINE=InnoDB;]]) + return true end diff --git a/data/migrations/1.lua b/data/migrations/1.lua new file mode 100644 index 00000000000..c931d4cffc9 --- /dev/null +++ b/data/migrations/1.lua @@ -0,0 +1,5 @@ +-- return true = There are others migrations file +-- return false = This is the last migration file +function onUpdateDatabase() + return false +end diff --git a/schema.sql b/schema.sql index 9b785e585ae..79477d04a8b 100644 --- a/schema.sql +++ b/schema.sql @@ -19,10 +19,9 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `lastday` int(10) UNSIGNED NOT NULL DEFAULT '0', `type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1', `coins` int(12) UNSIGNED NOT NULL DEFAULT '0', + `tournament_coins` INT(11) NOT NULL DEFAULT '0', `creation` int(11) UNSIGNED NOT NULL DEFAULT '0', `recruiter` INT(6) DEFAULT 0, - CONSTRAINT `accounts_pk` PRIMARY KEY (`id`), - CONSTRAINT `accounts_unique` UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `players` diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 4cebd3865c1..0978e96489e 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -56,6 +56,7 @@ enum booleanConfig_t { WEATHER_THUNDER, FREE_QUESTS, ONLY_PREMIUM_ACCOUNT, + STORE_PREMIUM_DISCOUNT, MAP_CUSTOM_ENABLED, ALL_CONSOLE_LOG, STAMINA_TRAINER, @@ -161,6 +162,7 @@ enum floatingConfig_t { RATE_NPC_HEALTH, RATE_NPC_ATTACK, RATE_NPC_DEFENSE, + RATE_STORE_PREMIUM_DISCOUNT, LAST_FLOATING_CONFIG }; diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1a9e319f3fb..7210d3353ab 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -183,6 +183,8 @@ bool ConfigManager::load() boolean[ONLY_PREMIUM_ACCOUNT] = getGlobalBoolean(L, "onlyPremiumAccount", false); + boolean[STORE_PREMIUM_DISCOUNT] = getGlobalBoolean(L, "storePremiumDiscount", false); + string[DEFAULT_PRIORITY] = getGlobalString(L, "defaultPriority", "high"); string[SERVER_NAME] = getGlobalString(L, "serverName", ""); string[OWNER_NAME] = getGlobalString(L, "ownerName", ""); @@ -250,6 +252,8 @@ bool ConfigManager::load() floating[RATE_NPC_ATTACK] = getGlobalFloat(L, "rateNpcAttack", 1.0); floating[RATE_NPC_DEFENSE] = getGlobalFloat(L, "rateNpcDefense", 1.0); + floating[RATE_STORE_PREMIUM_DISCOUNT] = getGlobalFloat(L, "rateStorePremiumDiscount", 0.90); + loaded = true; lua_close(L); return true; diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index be5222dbcef..cfc6805d751 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -103,7 +103,7 @@ error_t Account::GetCoins(CoinType_t coinType) { if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { coins = "coins"; } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournamentBalance"; + coins = "tournament_coins"; } std::ostringstream query; @@ -123,7 +123,7 @@ error_t Account::AddCoins(int32_t amount, CoinType_t coinType) if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { coins = "coins"; } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournamentBalance"; + coins = "tournament_coins"; } std::ostringstream query; query << "UPDATE `accounts` SET `" << coins << "` = `" << coins << "` + " << amount << " WHERE `id` = " << id_; @@ -225,7 +225,7 @@ error_t Account::LoadAccountDB(std::ostringstream &query) { this->SetPremiumRemaningDays(result->getNumber("premdays")); this->SetPremiumLastDay(result->getNumber("lastday")); this->SetStoreCoinBalance(result->getNumber("coins")); - this->SetTournamentCoinBalance(result->getNumber("tournamentBalance")); + this->SetTournamentCoinBalance(result->getNumber("tournament_coins")); return ERROR_NO; } diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index 68600f85b1d..ed5ba04615b 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -19,10 +19,12 @@ #include "otpch.h" +#include "config/configmanager.h" #include "game/game.h" #include "utils/pugicast.h" #include "creatures/players/store/store.hpp" +extern ConfigManager g_config; extern Game g_game; const std::unordered_map CoinTypeMap = { @@ -278,7 +280,6 @@ bool Store::loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, p childNodeName = childNode.attribute("description"); if (childNodeName) { offer.description = childNodeName.value(); - replaceString(offer.description, "
  • ", "•"); } childNodeName = childNode.attribute("type"); @@ -504,7 +505,6 @@ std::map> Store::getHomeOffersOrganized() } } - return filter; } @@ -514,7 +514,7 @@ std::map> Store::getStoreOrganizedByName(S StoreOffer* offer = offers->getOfferByID(info.first); if (offer) { std::string name = offer->getName(); - filter[name].emplace_back(offer); + filter[name].emplace_back(offer); } } @@ -696,38 +696,28 @@ StoreOffer* Store::getOfferById(uint32_t id) { } uint32_t StoreOffer::getPrice(Player* player) { - uint32_t newPrice = 0; - if (player && type == OFFER_TYPE_EXP_BOOST) { - int32_t value1; - player->getStorageValue(51052, value1); - uint32_t xpBoostPrice = getExpBoostPrice(value1); - if (xpBoostPrice > 0) { - if (player->isPremium()) { - xpBoostPrice *= 0.90; - } - - } - - return xpBoostPrice; - } - + uint32_t offerBasePrice = 0; if (state == OFFER_STATE_SALE) { time_t mytime; mytime = time(NULL); struct tm tm = *localtime(&mytime); int32_t daySub = validUntil - tm.tm_mday; if (daySub < 0) { - newPrice = basePrice; + offerBasePrice = basePrice; } } - uint32_t p_prince = price; - if (player && player->isPremium()) { - newPrice *= 0.90; - p_prince *= 0.90; + uint32_t discountPrice = price; + // Check if discount for premium is activated + if (g_config.getBoolean(STORE_PREMIUM_DISCOUNT)) { + if (player && player->isPremium()) { + offerBasePrice *= 1.0; + // Default premium discount rate (0.90 = 10% of discount) + discountPrice *= g_config.getFloat(RATE_STORE_PREMIUM_DISCOUNT); + } } - return newPrice > 0 ? newPrice : p_prince; + return offerBasePrice > 0 ? offerBasePrice : discountPrice; } uint16_t StoreOffer::getCount(bool inBuy) { diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index bb5ecdd50f7..60d0e2be08a 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -297,22 +297,6 @@ class StoreOffer { return skull; } - uint32_t getExpBoostPrice(int32_t value) { - if(value == 1) - return 30; - else if (value == 2) - return 45; - else if (value == 3) - return 90; - else if (value == 4) - return 180; - else if (value == 5) - return 360; - else - return 30; - - } - bool haveOfferRookgaard() { return rookgaard; } @@ -333,8 +317,8 @@ class StoreOffer { OfferStates_t state = OFFER_STATE_NONE; CoinType_t coinType = COIN_TYPE_DEFAULT; OfferBuyTypes_t buyType = OFFER_BUY_TYPE_OTHERS; - uint16_t count = 150; - uint32_t price = 0; // Default price (This preventing valueless offers from entering) + uint16_t count = 1; + uint32_t price = 150; // Default price (This preventing valueless offers from entering) uint32_t basePrice = 0; // Default price (This preventing valueless offers from entering) uint32_t validUntil = 0; uint16_t blessid = 0; diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 3010bc29860..d940796130f 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -160,7 +160,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) player->accountNumber = result->getNumber("account_id"); player->accountType = static_cast(result->getNumber("account_type")); player->coinBalance = result->getNumber("coinbalance"); - player->tournamentCoinBalance = result->getNumber("tournamentBalance"); + player->tournamentCoinBalance = result->getNumber("tournament_coins"); if (!g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = result->getNumber("premium_days"); } else { diff --git a/vcpkg.json b/vcpkg.json deleted file mode 100644 index 39e57de5d9e..00000000000 --- a/vcpkg.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "canary", - "version-string": "1.0.0", - "dependencies": [ - "boost-asio", - "boost-lockfree", - "boost-variant", - "boost-filesystem", - "boost-iostreams", - "boost-system", - "libmariadb", - "pugixml", - "spdlog", - "curl", - "jsoncpp", - "cryptopp", - { - "name": "luajit", - "platform": "windows" - } - ] -} From f1c4587277362c96bf20a5c92c300382d9ffa801 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 22 Aug 2021 00:36:58 -0300 Subject: [PATCH 19/30] Set discount store to false --- config.lua.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.lua.dist b/config.lua.dist index 33fc415e551..9f88b90ef64 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -120,7 +120,7 @@ checkExpiredMarketOffersEachMinutes = 60 maxMarketOffersAtATimePerPlayer = 100 -- Store -storePremiumDiscount = true +storePremiumDiscount = false rateStorePremiumDiscount = 1.0 -- MySQL From e4576f93eef493691c802ce1aea948fa50ae612d Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 22 Aug 2021 00:55:00 -0300 Subject: [PATCH 20/30] Add vcpkg manifest file --- vcpkg.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 vcpkg.json diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000000..39e57de5d9e --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,22 @@ +{ + "name": "canary", + "version-string": "1.0.0", + "dependencies": [ + "boost-asio", + "boost-lockfree", + "boost-variant", + "boost-filesystem", + "boost-iostreams", + "boost-system", + "libmariadb", + "pugixml", + "spdlog", + "curl", + "jsoncpp", + "cryptopp", + { + "name": "luajit", + "platform": "windows" + } + ] +} From a608d80a4f83a68b9ea5021c96b6448d74807ff6 Mon Sep 17 00:00:00 2001 From: dudantas Date: Sun, 22 Aug 2021 18:23:47 -0300 Subject: [PATCH 21/30] Fix: issue not displaying description in store offers --- src/creatures/players/store/store.cpp | 2 +- src/creatures/players/store/store.hpp | 2 +- src/server/network/protocol/protocolgame.cpp | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index ed5ba04615b..d3cdb4ce0c5 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -452,7 +452,7 @@ std::vector Store::getStoreOffer(StoreOffers* offers) { for (auto& info : offers->offers) { StoreOffer* offer = offers->getOfferByID(info.first); if (offer) { - filter.push_back(offer); + filter.push_back(offer); } } return filter; diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index 60d0e2be08a..2a9d7a94560 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -312,7 +312,7 @@ class StoreOffer { std::string name = ""; std::map itemList; - std::string description = ""; + std::string description; std::string icon = ""; OfferStates_t state = OFFER_STATE_NONE; CoinType_t coinType = COIN_TYPE_DEFAULT; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index bfd488dd937..51c45a87e6f 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -738,7 +738,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xE5: parseCyclopediaCharacterInfo(msg); break; case 0xE6: parseBugReport(msg); break; case 0xE7: /* thank you */ break; - case 0xE8: parseDebugAssert(msg); break; + case 0xE8: parseSendDescription(msg); break; case 0xEE: parseGreet(msg); break; case 0xEF: parseStoreCoinTransfer(msg); break; case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; @@ -2633,6 +2633,7 @@ void ProtocolGame::parseSendDescription(NetworkMessage& msg) if (storeOffer == nullptr) { return; } + player->sendOfferDescription(offerId, storeOffer->getDescription(player)); } @@ -6728,7 +6729,7 @@ void ProtocolGame::openStore() msg.addByte(0xFB); msg.add(g_store.getOfferCount()); - // enviando primeiro as categorias sem subcategorias + // First sending categories without subcategories std::vector categories = g_store.getStoreCategories(); for (auto it = categories.begin(), end = categories.end(); it != end; ++it) { msg.addString((*it).name); From 3bf81e09560c6925aa7bfea158f89c5f9a065682 Mon Sep 17 00:00:00 2001 From: dudantas Date: Mon, 23 Aug 2021 02:21:41 -0300 Subject: [PATCH 22/30] Separation of coins and tournament coins The coin and the tournament coin were uncoupled, just as @costallat asked in the review. Thanks! Fixed wrong ids from store.xml --- data/XML/store.xml | 811 +++++++++--------- .../god/store/{store_coins.lua => coins.lua} | 33 +- ...rnament_coins.lua => tournament_coins.lua} | 47 +- src/creatures/players/account/account.cpp | 123 +-- src/creatures/players/account/account.hpp | 66 +- src/creatures/players/player.cpp | 94 +- src/creatures/players/player.h | 30 +- src/creatures/players/store/store.cpp | 10 +- src/creatures/players/store/store.hpp | 2 +- src/game/game.cpp | 92 +- src/game/game.h | 2 +- src/io/iologindata.cpp | 16 +- src/lua/functions/core/game/lua_enums.hpp | 3 - .../creatures/player/player_functions.cpp | 105 ++- .../creatures/player/player_functions.hpp | 20 +- src/server/network/protocol/protocolgame.cpp | 37 +- src/server/network/protocol/protocolgame.h | 6 +- tests/account_test.cpp | 52 +- 18 files changed, 828 insertions(+), 721 deletions(-) rename data/scripts/talkactions/god/store/{store_coins.lua => coins.lua} (77%) rename data/scripts/talkactions/god/store/{store_tournament_coins.lua => tournament_coins.lua} (71%) diff --git a/data/XML/store.xml b/data/XML/store.xml index 26051ed938f..8cb61ce8c8b 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -47,458 +47,457 @@ - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - + + - - + + - - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - + - - - - - - - - - + + + + + + + + + - + - - - - - - - - - + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/data/scripts/talkactions/god/store/store_coins.lua b/data/scripts/talkactions/god/store/coins.lua similarity index 77% rename from data/scripts/talkactions/god/store/store_coins.lua rename to data/scripts/talkactions/god/store/coins.lua index 13274b62229..da526faad63 100644 --- a/data/scripts/talkactions/god/store/store_coins.lua +++ b/data/scripts/talkactions/god/store/coins.lua @@ -28,7 +28,7 @@ function getCoins.onSay(player, words, param) end player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." have \z - ".. targetPlayer:getStoreCoins(COIN_TYPE_DEFAULT) .." store coins.") + ".. targetPlayer:getCoins() .." coins.") return true end @@ -84,13 +84,13 @@ function addCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - targetPlayer:addStoreCoins(coins, COIN_TYPE_DEFAULT) + targetPlayer:addCoins(coins) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." \z - store coins for the ".. targetPlayer:getName() .." account.") + coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." added \z - ".. coins .." store coins to your account.") + ".. coins .." coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." added ".. coins .." store coins to ".. targetPlayer:getName() .." account") + Spdlog.info("".. player:getName() .." added ".. coins .." coins to ".. targetPlayer:getName() .." account") return true end @@ -139,20 +139,29 @@ function removeCoins.onSay(player, words, param) coins = tonumber(split[2]) end - -- Check if the coins is valid + -- Check if the coins to remove is valid if coins <= 0 or coins == nil then - player:sendCancelMessage("Invalid coins count.") - return false + return player:sendCancelMessage("Invalid coins count.") + end + + -- Check if target player have coins + if targetPlayer:getCoins() <= 0 then + return player:sendCancelMessage("The player ".. targetPlayer:getName() .." has no coins to remove.") + end + + -- Check target player have the count of coins to remove + if targetPlayer:getCoins() < coins then + return player:sendCancelMessage("The player does not have this amount of coins to remove, the player balance is ".. targetPlayer:getCoins() .." coins.") end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - targetPlayer:removeStoreCoins(coins, COIN_TYPE_DEFAULT) + targetPlayer:removeCoins(coins) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." \z - store coins for the ".. targetPlayer:getName() .." account.") + coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z - removed ".. coins .." store coins to your account.") + removed ".. coins .." coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." removed ".. coins .." store coins to ".. targetPlayer:getName() .." account") + Spdlog.info("".. player:getName() .." removed ".. coins .." coins to ".. targetPlayer:getName() .." account") return true end diff --git a/data/scripts/talkactions/god/store/store_tournament_coins.lua b/data/scripts/talkactions/god/store/tournament_coins.lua similarity index 71% rename from data/scripts/talkactions/god/store/store_tournament_coins.lua rename to data/scripts/talkactions/god/store/tournament_coins.lua index 69308c58bb7..4de036995d5 100644 --- a/data/scripts/talkactions/god/store/store_tournament_coins.lua +++ b/data/scripts/talkactions/god/store/tournament_coins.lua @@ -28,14 +28,14 @@ function getTournamentCoins.onSay(player, words, param) end player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Player ".. targetPlayer:getName() .." \z - have ".. targetPlayer:getStoreCoins(COIN_TYPE_TOURNAMENT) .." store tournament coins.") + have ".. targetPlayer:getTournamentCoins() .." tournament coins.") return true end getTournamentCoins:separator(" ") getTournamentCoins:register() -local addTournamentCoins = TalkAction("/addTournamentCoins") +local addTournamentCoins = TalkAction("/addtournamentcoins") function addTournamentCoins.onSay(player, words, param) if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then @@ -84,21 +84,21 @@ function addTournamentCoins.onSay(player, words, param) end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - targetPlayer:addStoreCoins(coins, COIN_TYPE_TOURNAMENT) + targetPlayer:addTournamentCoins(coins) player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull added ".. coins .." \z - store tournament coins for the ".. targetPlayer:getName() .." account.") + tournament coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z - added ".. coins .." store tournament coins to your account.") + added ".. coins .." tournament coins to your account.") -- Distro log Spdlog.info("".. player:getName() .." added ".. coins .." \z - store tournament coins to ".. targetPlayer:getName() .." account") + tournament coins to ".. targetPlayer:getName() .." account") return true end addTournamentCoins:separator(" ") addTournamentCoins:register() -local removeTournamentCoins = TalkAction("/removeTournamentCoins") +local removeTournamentCoins = TalkAction("/removetournamentcoins") function removeTournamentCoins.onSay(player, words, param) if not player:getGroup():getAccess() or player:getAccountType() < ACCOUNT_TYPE_GOD then @@ -134,26 +134,35 @@ function removeTournamentCoins.onSay(player, words, param) -- Trim left split[2] = split[2]:gsub("^%s*(.-)$", "%1") - -- Keep the coinscount in variable "coins" - local coins = 0 + -- Keep the tournament coins count in variable "tournamentCoins" + local tournamentCoins = 0 if split[2] then - coins = tonumber(split[2]) + tournamentCoins = tonumber(split[2]) end - -- Check if the coins is valid - if coins <= 0 or coins == nil then - player:sendCancelMessage("Invalid coins count.") - return false + -- Check if the tournament coins to remove is valid + if tournamentCoins <= 0 or tournamentCoins == nil then + return player:sendCancelMessage("Invalid tournament coins count.") + end + + -- Check if target player have tournament coins + if targetPlayer:getTournamentCoins() <= 0 then + return player:sendCancelMessage("The player ".. targetPlayer:getName() .." has no tournament coins to remove.") + end + + -- Check target player have the count of tournament coins to remove + if targetPlayer:getTournamentCoins() < tournamentCoins then + return player:sendCancelMessage("The player does not have this amount of tournament coins to remove, the player balance is ".. targetPlayer:getTournamentCoins() .." tournament coins.") end targetPlayer:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - targetPlayer:removeStoreCoins(coins, COIN_TYPE_TOURNAMENT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. coins .." \z - store tournament coins for the ".. targetPlayer:getName() .." account.") + targetPlayer:removeTournamentCoins(tournamentCoins) + player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "Successfull removed ".. tournamentCoins .." \z + tournament coins for the ".. targetPlayer:getName() .." account.") targetPlayer:sendTextMessage(MESSAGE_EVENT_ADVANCE, "".. player:getName() .." \z - removed ".. coins .." store tournament coins to your account.") + removed ".. tournamentCoins .." tournament coins to your account.") -- Distro log - Spdlog.info("".. player:getName() .." removed ".. coins .." store tournament \z + Spdlog.info("".. player:getName() .." removed ".. tournamentCoins .." tournament \z coins to ".. targetPlayer:getName() .." account") return true end diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index cfc6805d751..d41d9fcc45f 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -34,8 +34,8 @@ Account::Account() { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; + coinBalance = 0; + tournamentCoinBalance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -47,8 +47,8 @@ Account::Account(uint32_t id) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; + coinBalance = 0; + tournamentCoinBalance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -59,8 +59,8 @@ Account::Account(const std::string &email) : email_(email) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coin_balance = 0; - tournament_coin_balance = 0; + coinBalance = 0; + tournamentCoinBalance = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -94,45 +94,74 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { * Coins Methods ******************************************************************************/ -error_t Account::GetCoins(CoinType_t coinType) { +error_t Account::getCoins() { if (db_ == nullptr || id_ == 0) { return ERROR_NOT_INITIALIZED; } - std::string coins = "coins"; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "coins"; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournament_coins"; + std::ostringstream query; + query << "SELECT `coins` FROM `accounts` WHERE `id` = " << id_; + + DBResult_ptr result = db_->storeQuery(query.str()); + if (!result) { + return ERROR_DB; } + return result->getNumber("coins"); +} + +error_t Account::addCoins(int32_t amount) +{ std::ostringstream query; - query << "SELECT `" << coins << "` FROM `accounts` WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `coins` = `coins` + " << amount << " WHERE `id` = " << id_; + + db_tasks_->addTask(query.str()); + return ERROR_NO; +} + +error_t Account::removeCoins(int32_t amount) { + + if (db_tasks_ == nullptr) { + return ERROR_NULLPTR; + } + + if (amount == 0) { + return ERROR_NO; + } + + std::ostringstream query; + query << "UPDATE `accounts` SET `coins` = `coins` - " << amount << " WHERE `id` = " << id_; + + db_tasks_->addTask(query.str()); + return ERROR_NO; +} + +error_t Account::getTournamentCoins() { + if (db_ == nullptr || id_ == 0) { + return ERROR_NOT_INITIALIZED; + } + + std::ostringstream query; + query << "SELECT `tournament_coins` FROM `accounts` WHERE `id` = " << id_; DBResult_ptr result = db_->storeQuery(query.str()); if (!result) { return ERROR_DB; } - return result->getNumber(coins); + return result->getNumber("tournament_coins"); } -error_t Account::AddCoins(int32_t amount, CoinType_t coinType) +error_t Account::addTournamentCoins(int32_t amount) { - std::string coins = "coins"; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "coins"; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "tournament_coins"; - } std::ostringstream query; - query << "UPDATE `accounts` SET `" << coins << "` = `" << coins << "` + " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `tournament_coins` = `tournament_coins` + " << amount << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); return ERROR_NO; } -error_t Account::RemoveCoins(int32_t amount, CoinType_t coinType) { +error_t Account::removeTournamentCoins(int32_t amount) { if (db_tasks_ == nullptr) { return ERROR_NULLPTR; @@ -142,14 +171,8 @@ error_t Account::RemoveCoins(int32_t amount, CoinType_t coinType) { return ERROR_NO; } - std::string coins = "`coins`"; - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - coins = "`coins`"; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - coins = "`tournamentBalance`"; - } std::ostringstream query; - query << "UPDATE `accounts` SET " << coins << " = " << coins << " - " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `tournament_coins` = `tournament_coins` - " << amount << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); return ERROR_NO; @@ -185,30 +208,30 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t * Database ******************************************************************************/ -error_t Account::LoadAccountDB() { +error_t Account::loadAccountDB() { if (id_ != 0) { - return this->LoadAccountDB(id_); + return this->loadAccountDB(id_); } else if (!email_.empty()) { - return this->LoadAccountDB(email_); + return this->loadAccountDB(email_); } return ERROR_NOT_INITIALIZED; } -error_t Account::LoadAccountDB(std::string email) { +error_t Account::loadAccountDB(std::string email) { std::ostringstream query; query << "SELECT * FROM `accounts` WHERE `email` = " << db_->escapeString(email); - return this->LoadAccountDB(query); + return this->loadAccountDB(query); } -error_t Account::LoadAccountDB(uint32_t id) { +error_t Account::loadAccountDB(uint32_t id) { std::ostringstream query; query << "SELECT * FROM `accounts` WHERE `id` = " << id; - return this->LoadAccountDB(query); + return this->loadAccountDB(query); } -error_t Account::LoadAccountDB(std::ostringstream &query) { +error_t Account::loadAccountDB(std::ostringstream &query) { if (db_ == nullptr) { return ERROR_NULLPTR; } @@ -224,8 +247,8 @@ error_t Account::LoadAccountDB(std::ostringstream &query) { this->SetPassword(result->getString("password")); this->SetPremiumRemaningDays(result->getNumber("premdays")); this->SetPremiumLastDay(result->getNumber("lastday")); - this->SetStoreCoinBalance(result->getNumber("coins")); - this->SetTournamentCoinBalance(result->getNumber("tournament_coins")); + this->setCoinBalance(result->getNumber("coins")); + this->setTournamentCoinBalance(result->getNumber("tournament_coins")); return ERROR_NO; } @@ -283,8 +306,8 @@ error_t Account::SaveAccountDB() { << "`email` = " << db_->escapeString(email_) << " , " << "`type` = " << account_type_ << " , " << "`password` = " << db_->escapeString(password_) << " , " - << "`coins` = " << coin_balance << " , " - << "`tournamentBalance` = " << tournament_coin_balance << " , " + << "`coins` = " << coinBalance << " , " + << "`tournament_coins` = " << tournamentCoinBalance << " , " << "`premdays` = " << premium_remaining_days_ << " , " << "`lastday` = " << premium_last_day_; @@ -387,39 +410,39 @@ error_t Account::GetPremiumLastDay(time_t *last_day) { return ERROR_NO; } -error_t Account::SetStoreCoinBalance(uint32_t coins) { +error_t Account::setCoinBalance(uint32_t coins) { if (coins == 0) { return ERROR_INVALID_ID; } - coin_balance = coins; + coinBalance = coins; return ERROR_NO; } -error_t Account::GetStoreCoinBalance(uint32_t *coins) { +error_t Account::getCoinBalance(uint32_t *coins) { if (coins == nullptr) { return ERROR_NULLPTR; } - *coins = coin_balance; + *coins = coinBalance; return ERROR_NO; } -error_t Account::SetTournamentCoinBalance(uint32_t tournamentCoins) { +error_t Account::setTournamentCoinBalance(uint32_t tournamentCoins) { if (tournamentCoins == 0) { return ERROR_INVALID_ID; } - tournament_coin_balance = tournamentCoins; + tournamentCoinBalance = tournamentCoins; return ERROR_NO; } -error_t Account::GetTournamentCoinBalance(uint32_t *tournamentCoins) { +error_t Account::getTournamentCoinBalance(uint32_t *tournamentCoins) { if (tournamentCoins == nullptr) { return ERROR_NULLPTR; } - *tournamentCoins = tournament_coin_balance; + *tournamentCoins = tournamentCoinBalance; return ERROR_NO; } diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index 66a865a8934..288137f8574 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -88,14 +88,14 @@ class Account { /** * @brief Construct a new Account object * - * @param id Set Account ID to be used by LoadAccountDB + * @param id Set Account ID to be used by loadAccountDB */ explicit Account(uint32_t id); /** * @brief Construct a new Account object * - * @param name Set Account Name to be used by LoadAccountDB + * @param name Set Account Name to be used by loadAccountDB */ explicit Account(const std::string &name); @@ -125,14 +125,13 @@ class Account { * Coins Methods **************************************************************************/ - /** + /** Normal coins * @brief Get the amount of coins that the account has from database. * - * @param accountId Account ID to get the coins. * @param coins Pointer to return the number of coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t GetCoins(CoinType_t coinType = COIN_TYPE_DEFAULT); + error_t getCoins(); /** * @brief Add coins to the account and update database. @@ -140,7 +139,7 @@ class Account { * @param amount Amount of coins to be added * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t AddCoins(int32_t amount, CoinType_t coinType = COIN_TYPE_DEFAULT); + error_t addCoins(int32_t amount); /** * @brief Removes coins from the account and update database. @@ -148,7 +147,7 @@ class Account { * @param amount Amount of coins to be removed * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t RemoveCoins(int32_t amount, CoinType_t coinType = COIN_TYPE_DEFAULT); + error_t removeCoins(int32_t amount); /** * @brief Register account coins transactions in database. @@ -158,6 +157,39 @@ class Account { * @param description Description of the transaction * @return error_t ERROR_NO(0) Success, otherwise Fail. */ + + + /** Tournament Coins + * @brief Get the amount of tournament coins that the account has from database. + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t getTournamentCoins(); + + /** + * @brief Add coins to the account and update database. + * + * @param amount Amount of coins to be added + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t addTournamentCoins(int32_t amount); + + /** + * @brief Removes tournament coins from the account and update database. + * + * @param amount Amount of coins to be removed + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t removeTournamentCoins(int32_t amount); + + /** + * @brief Register account tournament coins transactions in database. + * + * @param type Type of the transaction(Add/Remove). + * @param coins Amount of coins + * @param description Description of the transaction + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ error_t RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust); /*************************************************************************** @@ -170,7 +202,7 @@ class Account { * * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t LoadAccountDB(); + error_t loadAccountDB(); /** * @brief Try to @@ -178,7 +210,7 @@ class Account { * @param name * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t LoadAccountDB(std::string name); + error_t loadAccountDB(std::string name); /** * @brief @@ -186,7 +218,7 @@ class Account { * @param id * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t LoadAccountDB(uint32_t id); + error_t loadAccountDB(uint32_t id); /** * @brief @@ -211,11 +243,11 @@ class Account { error_t SetPremiumRemaningDays(uint32_t days); error_t GetPremiumRemaningDays(uint32_t *days); - error_t SetStoreCoinBalance(uint32_t coins); - error_t GetStoreCoinBalance(uint32_t *coins); + error_t setCoinBalance(uint32_t coins); + error_t getCoinBalance(uint32_t *coins); - error_t SetTournamentCoinBalance(uint32_t tournamentCoins); - error_t GetTournamentCoinBalance(uint32_t *tournamentCoins); + error_t setTournamentCoinBalance(uint32_t tournamentCoins); + error_t getTournamentCoinBalance(uint32_t *tournamentCoins); error_t SetPremiumLastDay(time_t last_day); error_t GetPremiumLastDay(time_t *last_day); @@ -228,7 +260,7 @@ class Account { private: error_t SetID(uint32_t id); - error_t LoadAccountDB(std::ostringstream &query); + error_t loadAccountDB(std::ostringstream &query); error_t LoadAccountPlayersDB(std::vector *players); error_t LoadAccountPlayerDB(Player *player, std::string& characterName); @@ -240,8 +272,8 @@ class Account { std::string password_; uint32_t premium_remaining_days_; time_t premium_last_day_; - uint32_t coin_balance; - uint32_t tournament_coin_balance; + uint32_t coinBalance; + uint32_t tournamentCoinBalance; AccountType account_type_; }; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 5c4e38a1f21..bed0e47bfb9 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4619,62 +4619,46 @@ void Player::setPremiumDays(int32_t v) sendBasicData(); } -void Player::setStoreCoins(int32_t coins, CoinType_t coinType) +void Player::setCoins(int32_t coins) { - switch (coinType) { - case COIN_TYPE_DEFAULT: - case COIN_TYPE_TRANSFERABLE: { - coinBalance = coins; - break; - } - - case COIN_TYPE_TOURNAMENT: { - tournamentCoinBalance = coins; - break; - } + coinBalance = coins; +} - default: { - coinBalance = coins; - break; - } - } +void Player::setTournamentCoins(int32_t tournamentCoins) +{ + tournamentCoinBalance = tournamentCoins; } -bool Player::canRemoveStoreCoins(int32_t coins, CoinType_t coinType) +bool Player::canRemoveCoins(int32_t coins) { if (lastUpdateCoin - OTSYS_TIME() < 2000) { // Update every 2 seconds lastUpdateCoin = OTSYS_TIME() + 2000; account::Account account(this->getAccount()); - account.LoadAccountDB(); - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - this->coinBalance = account.GetCoins(coinType); - } else if (coinType == COIN_TYPE_TOURNAMENT) { - this->tournamentCoinBalance = account.GetCoins(coinType); - } + account.loadAccountDB(); + this->coinBalance = account.getCoins(); } - int32_t removeCoins; - switch (coinType) { - case COIN_TYPE_DEFAULT: - case COIN_TYPE_TRANSFERABLE: { - removeCoins = coinBalance; - break; - } + int32_t removeCoins = coinBalance; - case COIN_TYPE_TOURNAMENT: { - removeCoins = tournamentCoinBalance; - break; - } + return (removeCoins - coins) >= 0; +} - default: { - removeCoins = coinBalance; - break; - } +bool Player::canRemoveTournamentCoins(int32_t tournamentCoins) +{ + if (lastUpdateCoin - OTSYS_TIME() < 2000) { + // Update every 2 seconds + lastUpdateCoin = OTSYS_TIME() + 2000; + + account::Account account(this->getAccount()); + account.loadAccountDB(); + this->tournamentCoinBalance = account.getTournamentCoins(); } - return (removeCoins - coins) >= 0; + int32_t removeTournamentCoins = tournamentCoinBalance; + + return (removeTournamentCoins - tournamentCoins) >= 0; } PartyShields_t Player::getPartyShield(const Player* player) const @@ -5167,36 +5151,6 @@ void Player::clearModalWindows() modalWindows.clear(); } -uint16_t Player::getHelpers() const -{ - uint16_t helpers; - - if (guild && party) { - std::unordered_set helperSet; - - const auto& guildMembers = guild->getMembersOnline(); - helperSet.insert(guildMembers.begin(), guildMembers.end()); - - const auto& partyMembers = party->getMembers(); - helperSet.insert(partyMembers.begin(), partyMembers.end()); - - const auto& partyInvitees = party->getInvitees(); - helperSet.insert(partyInvitees.begin(), partyInvitees.end()); - - helperSet.insert(party->getLeader()); - - helpers = helperSet.size(); - } else if (guild) { - helpers = guild->getMembersOnline().size(); - } else if (party) { - helpers = party->getMemberCount() + party->getInvitationCount() + 1; - } else { - helpers = 0; - } - - return helpers; -} - void Player::sendClosePrivate(uint16_t channelId) { if (channelId == CHANNEL_GUILD || channelId == CHANNEL_PARTY) { diff --git a/src/creatures/players/player.h b/src/creatures/players/player.h index 877d493a2d3..5a6a5aefb09 100644 --- a/src/creatures/players/player.h +++ b/src/creatures/players/player.h @@ -525,19 +525,17 @@ class Player final : public Creature, public Cylinder bool isPremium() const; void setPremiumDays(int32_t v); - void setStoreCoins(int32_t coins, CoinType_t coinType = COIN_TYPE_DEFAULT); - bool canRemoveStoreCoins(int32_t coins, CoinType_t coinType = COIN_TYPE_DEFAULT); - int32_t getStoreCoinBalance(CoinType_t coinType = COIN_TYPE_DEFAULT) { - if (coinType == COIN_TYPE_DEFAULT || coinType == COIN_TYPE_TRANSFERABLE) { - return coinBalance; - } else if (coinType == COIN_TYPE_TOURNAMENT) { - return tournamentCoinBalance; - } else { - return 0; - } + void setCoins(int32_t coins); + bool canRemoveCoins(int32_t coins); + int32_t getCoinBalance() { + return coinBalance; } - uint16_t getHelpers() const; + void setTournamentCoins(int32_t coins); + bool canRemoveTournamentCoins(int32_t coins); + int32_t getTournamentCoinBalance() { + return tournamentCoinBalance; + } bool setVocation(uint16_t vocId); uint16_t getVocationId() const { @@ -1049,9 +1047,9 @@ class Player final : public Creature, public Cylinder client->sendLockerItems(itemMap, count); } } - void sendStoreCoinBalance() { + void sendCoinBalance() { if (client) { - client->sendStoreCoinBalance(); + client->sendCoinBalance(); } } void sendInventoryItem(Slots_t slot, const Item* item) { @@ -1083,9 +1081,9 @@ class Player final : public Creature, public Cylinder client->openStore(); } } - void updateStoreCoinBalance() { + void updateCoinBalance() { if (client) { - client->updateStoreCoinBalance(); + client->updateCoinBalance(); } } @@ -2049,12 +2047,12 @@ class Player final : public Creature, public Cylinder int32_t offlineTrainingSkill = -1; int32_t offlineTrainingTime = 0; int32_t idleTime = 0; - int32_t tournamentCoinBalance = 0; uint16_t expBoostStamina = 0; uint16_t entriesPerPage = 26; uint32_t coinBalance = 0; + uint32_t tournamentCoinBalance = 0; uint32_t premiumDays = 0; uint16_t lastStatsTrainingTime = 0; diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index d3cdb4ce0c5..8b7707c2b6c 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -617,12 +617,10 @@ std::string StoreOffer::getDisabledReason(Player* player) { disabledReason = "This offer is deactivated."; } - if (player->getStoreCoinBalance(coinType) - getPrice(player) < 0) { - if (coinType == COIN_TYPE_TOURNAMENT) { - disabledReason = "You don't have tournament coins."; - } else { - disabledReason = "You don't have coins."; - } + if (player->getCoinBalance() - getPrice(player) < 0) { + disabledReason = "You don't have coins."; + } else if (player->getTournamentCoinBalance() - getPrice(player) < 0) { + disabledReason = "You don't have tournament coins."; } return disabledReason; diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index 2a9d7a94560..32fc8be77de 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -211,6 +211,7 @@ class StoreOffers { } StoreOffer* getOfferByID(uint32_t id); + protected: friend class Store; friend class StoreOffer; @@ -224,7 +225,6 @@ class StoreOffers { std::string parent; bool rookgaard = false; OfferStates_t state = OFFER_STATE_NONE; - }; class StoreOffer { diff --git a/src/game/game.cpp b/src/game/game.cpp index 1e26dc43ff2..8736f64fda7 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -7573,13 +7573,13 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr } if (it.id == ITEM_STORE_COIN) { - if (amount > player->getStoreCoinBalance()) { + if (amount > player->getCoinBalance()) { return; } account::Account account(player->getAccount()); - account.LoadAccountDB(); - account.AddCoins(amount); + account.loadAccountDB(); + account.addCoins(amount); } else { uint16_t stashmath = amount; uint16_t stashminus = player->getStashItemCount(it.wareId); @@ -7670,8 +7670,8 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; - account.LoadAccountDB(player->getAccount()); - account.AddCoins(offer.amount); + account.loadAccountDB(player->getAccount()); + account.addCoins(offer.amount); } else if (it.stackable) { uint16_t tmpAmount = offer.amount; @@ -7773,12 +7773,12 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; - account.LoadAccountDB(player->getAccount()); - if (amount > account.GetCoins()) { + account.loadAccountDB(player->getAccount()); + if (amount > account.getCoins()) { return; } - account.AddCoins(amount); + account.addCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); } else { std::forward_list itemList = getMarketItemList(it.wareId, amount, depotLocker); @@ -7807,8 +7807,8 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; - account.LoadAccountDB(buyerPlayer->getAccount()); - account.AddCoins(amount); + account.loadAccountDB(buyerPlayer->getAccount()); + account.addCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); } else if (it.stackable) { @@ -7870,8 +7870,8 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account; - account.LoadAccountDB(player->getAccount()); - account.AddCoins(amount); + account.loadAccountDB(player->getAccount()); + account.addCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); } else if (it.stackable) { uint16_t tmpAmount = amount; @@ -7906,7 +7906,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { account::Account account; - account.LoadAccountDB(sellerPlayer->getAccount()); + account.loadAccountDB(sellerPlayer->getAccount()); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); } } else { @@ -7916,7 +7916,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (IOLoginData::loadPlayerById(sellerPlayer, offer.playerId)) { account::Account account; - account.LoadAccountDB(sellerPlayer->getAccount()); + account.loadAccountDB(sellerPlayer->getAccount()); account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); } @@ -8339,7 +8339,7 @@ void Game::playerOpenStore(uint32_t playerId, bool openStore, StoreOffers* offer } // Update coins - player->updateStoreCoinBalance(); + player->updateCoinBalance(); if (openStore) { player->openStore(); @@ -8365,19 +8365,27 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: return; } - + player->updateCoinBalance(); OfferTypes_t offerType = thisOffer->getOfferType(); if (!g_store.isValidType(offerType)) { player->sendStoreError(STORE_ERROR_INFORMATION, "This offer is unavailable."); return; } - if (!player->canRemoveStoreCoins(thisOffer->getPrice(player)) ) { - player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have coins."); - return; + if (thisOffer->getCoinType() == COIN_TYPE_DEFAULT) { + if (!player->canRemoveCoins(thisOffer->getPrice(player))) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have coins."); + return; + } + } + if (thisOffer->getCoinType() == COIN_TYPE_TOURNAMENT) { + if (!player->canRemoveTournamentCoins(thisOffer->getPrice(player))) { + player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have tournament coins."); + return; + } } - player->updateStoreCoinBalance(); + player->updateCoinBalance(); std::string message = thisOffer->getDisabledReason(player); if (!message.empty()) { player->sendStoreError(STORE_ERROR_PURCHASE, message); @@ -8930,16 +8938,24 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: successfully = true; } + account::Account account(player->getAccount()); + account.loadAccountDB(); if (successfully) { - account::Account account(player->getAccount()); - account.LoadAccountDB(); - account.RemoveCoins(offerPrice*-1); - player->setStoreCoins(account.GetCoins(thisOffer->getCoinType()), thisOffer->getCoinType()); - if (returnmessage.str().empty()) { - returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" coins"; + if (thisOffer->getCoinType() == COIN_TYPE_DEFAULT) { + account.removeCoins(offerPrice*-1); + player->setCoins(account.getCoins()); + if (returnmessage.str().empty()) { + returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" coins"; + } + } else if (thisOffer->getCoinType() == COIN_TYPE_TOURNAMENT) { + account.removeTournamentCoins(offerPrice*-1); + player->setTournamentCoins(account.getTournamentCoins()); + if (returnmessage.str().empty()) { + returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" tournament coins"; + } } - player->updateStoreCoinBalance(); + player->updateCoinBalance(); player->sendStorePurchaseSuccessful(returnmessage.str()); account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), thisOffer->getCount(true), static_cast(thisOffer->getCoinType()), std::move(thisOffer->getName()), offerPrice); @@ -8991,15 +9007,15 @@ void Game::queueSendStoreAlertToUser(uint32_t playerId, std::string message, Sto player->sendStoreError(storeErrorCode, message); } -void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipient, uint16_t amount) { +void Game::playerCoinTransfer(uint32_t playerId, const std::string& recipient, uint16_t amount) { Player* player = getPlayerByID(playerId); if (!player) { - SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player with id '{}' not exist", player->getName()); + SPDLOG_INFO("[Game::playerCoinTransfer] - Player with id '{}' not exist", player->getName()); return; } - if (player->getStoreCoinBalance() < amount) { - SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player {} not have coins for transfer", player->getName()); + if (player->getCoinBalance() < amount) { + SPDLOG_INFO("[Game::playerCoinTransfer] - Player {} not have coins for transfer", player->getName()); return; } @@ -9011,7 +9027,7 @@ void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipie if (!recipientPlayer) { recipientPlayer = new Player(nullptr); if (!IOLoginData::loadPlayerByName(recipientPlayer, recipient)) { - SPDLOG_INFO("[Game::playerStoreCoinTransfer] - Player with name '{}' not exist", player->getName()); + SPDLOG_INFO("[Game::playerCoinTransfer] - Player with name '{}' not exist", player->getName()); delete recipientPlayer; return; } @@ -9024,13 +9040,13 @@ void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipie std::string description(player->getName() + " transferred to " + recipient); account::Account account; - account.LoadAccountDB(player->getAccount()); - account.AddCoins(-static_cast(amount)); + account.loadAccountDB(player->getAccount()); + account.addCoins(-static_cast(amount)); player->coinBalance -= amount; account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, -static_cast(amount)); - account.LoadAccountDB(recipientPlayer->getAccount()); - account.AddCoins(amount); + account.loadAccountDB(recipientPlayer->getAccount()); + account.addCoins(amount); account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, amount); recipientPlayer->coinBalance += amount; @@ -9038,8 +9054,8 @@ void Game::playerStoreCoinTransfer(uint32_t playerId, const std::string& recipie IOLoginData::savePlayer(recipientPlayer); delete recipientPlayer; } else { - recipientPlayer->sendStoreCoinBalance(); + recipientPlayer->sendCoinBalance(); } - player->sendStoreCoinBalance(); + player->sendCoinBalance(); } diff --git a/src/game/game.h b/src/game/game.h index 2b7a81c9ebc..0ebb8d556b5 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -521,7 +521,7 @@ class Game void playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std::string& param); void playerStoreTransactionHistory(uint32_t playerId, uint32_t pages, uint8_t entryPages); void queueSendStoreAlertToUser(uint32_t playerId, std::string message, StoreErrors_t storeErrorCode = STORE_ERROR_NETWORK); - void playerStoreCoinTransfer(uint32_t playerId, const std::string &recipient, uint16_t amount); + void playerCoinTransfer(uint32_t playerId, const std::string &recipient, uint16_t amount); private: void checkImbuements(); diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index d940796130f..df93d929240 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -33,7 +33,7 @@ extern Game g_game; extern Monsters g_monsters; bool IOLoginData::authenticateAccountPassword(const std::string& email, const std::string& password, account::Account *account) { - if (account::ERROR_NO != account->LoadAccountDB(email)) { + if (account::ERROR_NO != account->loadAccountDB(email)) { SPDLOG_ERROR("Email {} doesn't match any account.", email); return false; } @@ -135,7 +135,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) Database& db = Database::getInstance(); std::ostringstream query; - query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type`, (SELECT `coins` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `coinbalance`, (SELECT `tournamentBalance` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `tournamentBalance`"; + query << "SELECT `id`, `account_id`, `group_id`, `deletion`, (SELECT `type` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `account_type`, (SELECT `coins` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `coinbalance`, (SELECT `tournament_coins` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `tournamentcoins`"; if (!g_config.getBoolean(FREE_PREMIUM)) { query << ", (SELECT `premdays` FROM `accounts` WHERE `accounts`.`id` = `account_id`) AS `premium_days`"; } @@ -160,7 +160,7 @@ bool IOLoginData::preloadPlayer(Player* player, const std::string& name) player->accountNumber = result->getNumber("account_id"); player->accountType = static_cast(result->getNumber("account_type")); player->coinBalance = result->getNumber("coinbalance"); - player->tournamentCoinBalance = result->getNumber("tournament_coins"); + player->tournamentCoinBalance = result->getNumber("tournamentcoins"); if (!g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = result->getNumber("premium_days"); } else { @@ -301,14 +301,14 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) uint32_t accountId = result->getNumber("account_id"); account::Account account; account.SetDatabaseInterface(&db); - account.LoadAccountDB(accountId); + account.loadAccountDB(accountId); player->setGUID(result->getNumber("id")); player->name = result->getString("name"); account.GetID(&(player->accountNumber)); account.GetAccountType(&(player->accountType)); - account.GetStoreCoinBalance(&(player->coinBalance)); - account.GetTournamentCoinBalance(&(player->coinBalance)); + account.getCoinBalance(&(player->coinBalance)); + account.getTournamentCoinBalance(&(player->tournamentCoinBalance)); if (g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = std::numeric_limits::max(); @@ -316,8 +316,8 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) account.GetPremiumRemaningDays(&(player->premiumDays)); } - player->coinBalance = account.GetCoins(); - player->tournamentCoinBalance = account.GetCoins(COIN_TYPE_TOURNAMENT); + player->coinBalance = account.getCoins(); + player->tournamentCoinBalance = account.getTournamentCoins(); player->preyBonusRerolls = result->getNumber("bonus_rerolls"); diff --git a/src/lua/functions/core/game/lua_enums.hpp b/src/lua/functions/core/game/lua_enums.hpp index 87eeebe7d28..ee50460e913 100644 --- a/src/lua/functions/core/game/lua_enums.hpp +++ b/src/lua/functions/core/game/lua_enums.hpp @@ -1019,9 +1019,6 @@ class LuaEnums final : LuaScriptInterface { registerEnum(L, WEBHOOK_COLOR_WARNING); registerEnum(L, WEBHOOK_COLOR_RAID); - registerEnum(L, COIN_TYPE_DEFAULT); - registerEnum(L, COIN_TYPE_TRANSFERABLE); - registerEnum(L, COIN_TYPE_TOURNAMENT); #undef registerEnum } diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index fc053e6d0d8..035daa6cc5b 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2289,48 +2289,46 @@ int PlayerFunctions::luaPlayerRemovePremiumDays(lua_State* L) { return 1; } -int PlayerFunctions::luaPlayerGetStoreCoins(lua_State* L) { - // player:getStoreCoins(coinType) +int PlayerFunctions::luaPlayerGetCoins(lua_State* L) { + // player:getCoins() Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - CoinType_t coinType = getNumber(L, 2); - if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { - lua_pushnumber(L, player->getStoreCoinBalance(coinType)); + if (player->getCoinBalance() != std::numeric_limits::max()) { + lua_pushnumber(L, player->getCoinBalance()); } else { lua_pushnil(L); } return 1; } -int PlayerFunctions::luaPlayerAddStoreCoins(lua_State* L) { - // player:addStoreCoins(coins, coinType) +int PlayerFunctions::luaPlayerAddCoins(lua_State* L) { + // player:addCoins(coins) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); return 1; } - CoinType_t coinType = getNumber(L, 3); - if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { + if (player->getCoinBalance() != std::numeric_limits::max()) { int32_t coins = getNumber(L, 2); - int32_t addCoins = std::min(std::numeric_limits::max() - player->getStoreCoinBalance(coinType), coins); + int32_t addCoins = std::min(std::numeric_limits::max() - player->getCoinBalance(), coins); if (addCoins > 0) { account::Account account(player->getAccount()); - account.LoadAccountDB(); - player->setStoreCoins(player->getStoreCoinBalance(coinType) + addCoins, coinType); - account.AddCoins(addCoins, coinType); + account.loadAccountDB(); + player->setCoins(player->getCoinBalance() + addCoins); + account.addCoins(addCoins); lua_pushnumber(L, addCoins); } } return 1; } -int PlayerFunctions::luaPlayerRemoveStoreCoins(lua_State* L) { - // player:removeStoreCoins(coins, coinType) +int PlayerFunctions::luaPlayerRemoveCoins(lua_State* L) { + // player:removeCoins(coins) Player* player = getUserdata(L, 1); if (!player) { lua_pushnil(L); @@ -2338,19 +2336,82 @@ int PlayerFunctions::luaPlayerRemoveStoreCoins(lua_State* L) { } int32_t coins = getNumber(L, 2); - CoinType_t coinType = getNumber(L, 3); - if (!player->canRemoveStoreCoins(coins, coinType)) { + if (!player->canRemoveCoins(coins)) { pushBoolean(L, false); return 1; } - int32_t removeCoins = std::min(player->getStoreCoinBalance(coinType), coins); - if (player->getStoreCoinBalance(coinType) != std::numeric_limits::max()) { + int32_t removeCoins = std::min(player->getCoinBalance(), coins); + if (player->getCoinBalance() != std::numeric_limits::max()) { if (removeCoins > 0) { account::Account account(player->getAccount()); - account.LoadAccountDB(); - player->setStoreCoins(player->getStoreCoinBalance(coinType) - removeCoins, coinType); - account.RemoveCoins(removeCoins, coinType); + account.loadAccountDB(); + player->setCoins(player->getCoinBalance() - removeCoins); + account.removeCoins(removeCoins); + } + } + return 1; +} + +int PlayerFunctions::luaPlayerGetTournamentCoins(lua_State* L) { + // player:getTournamentCoins() + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + if (player->getTournamentCoinBalance() != std::numeric_limits::max()) { + lua_pushnumber(L, player->getTournamentCoinBalance()); + } else { + lua_pushnil(L); + } + return 1; +} + +int PlayerFunctions::luaPlayerAddTournamentCoins(lua_State* L) { + // player:addTournamentCoins(tournamentCoins) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + if (player->getTournamentCoinBalance() != std::numeric_limits::max()) { + int32_t tournamentCoins = getNumber(L, 2); + int32_t addTournamentCoins = std::min(std::numeric_limits::max() - player->getTournamentCoinBalance(), tournamentCoins); + if (addTournamentCoins > 0) { + account::Account account(player->getAccount()); + account.loadAccountDB(); + player->setTournamentCoins(player->getTournamentCoinBalance() + addTournamentCoins); + account.addTournamentCoins(addTournamentCoins); + lua_pushnumber(L, addTournamentCoins); + } + } + return 1; +} + +int PlayerFunctions::luaPlayerRemoveTournamentCoins(lua_State* L) { + // player:removeTournamentCoins(tournamentCoins) + Player* player = getUserdata(L, 1); + if (!player) { + lua_pushnil(L); + return 1; + } + + int32_t tournamentCoins = getNumber(L, 2); + if (!player->canRemoveTournamentCoins(tournamentCoins)) { + pushBoolean(L, false); + return 1; + } + + int32_t removeTournamentCoins = std::min(player->getTournamentCoinBalance(), tournamentCoins); + if (player->getTournamentCoinBalance() != std::numeric_limits::max()) { + if (removeTournamentCoins > 0) { + account::Account account(player->getAccount()); + account.loadAccountDB(); + player->setTournamentCoins(player->getTournamentCoinBalance() - removeTournamentCoins); + account.removeTournamentCoins(removeTournamentCoins); } } return 1; diff --git a/src/lua/functions/creatures/player/player_functions.hpp b/src/lua/functions/creatures/player/player_functions.hpp index 686e5831d45..c6b47940eec 100644 --- a/src/lua/functions/creatures/player/player_functions.hpp +++ b/src/lua/functions/creatures/player/player_functions.hpp @@ -213,9 +213,13 @@ class PlayerFunctions final : LuaScriptInterface { registerMethod(L, "Player", "addPremiumDays", PlayerFunctions::luaPlayerAddPremiumDays); registerMethod(L, "Player", "removePremiumDays", PlayerFunctions::luaPlayerRemovePremiumDays); - registerMethod(L, "Player", "getStoreCoins", PlayerFunctions::luaPlayerGetStoreCoins); - registerMethod(L, "Player", "addStoreCoins", PlayerFunctions::luaPlayerAddStoreCoins); - registerMethod(L, "Player", "removeStoreCoins", PlayerFunctions::luaPlayerRemoveStoreCoins); + registerMethod(L, "Player", "getCoins", PlayerFunctions::luaPlayerGetCoins); + registerMethod(L, "Player", "addCoins", PlayerFunctions::luaPlayerAddCoins); + registerMethod(L, "Player", "removeCoins", PlayerFunctions::luaPlayerRemoveCoins); + + registerMethod(L, "Player", "getTournamentCoins", PlayerFunctions::luaPlayerGetTournamentCoins); + registerMethod(L, "Player", "addTournamentCoins", PlayerFunctions::luaPlayerAddTournamentCoins); + registerMethod(L, "Player", "removeTournamentCoins", PlayerFunctions::luaPlayerRemoveTournamentCoins); registerMethod(L, "Player", "hasBlessing", PlayerFunctions::luaPlayerHasBlessing); registerMethod(L, "Player", "addBlessing", PlayerFunctions::luaPlayerAddBlessing); @@ -492,9 +496,13 @@ class PlayerFunctions final : LuaScriptInterface { static int luaPlayerAddPremiumDays(lua_State* L); static int luaPlayerRemovePremiumDays(lua_State* L); - static int luaPlayerGetStoreCoins(lua_State* L); - static int luaPlayerAddStoreCoins(lua_State* L); - static int luaPlayerRemoveStoreCoins(lua_State* L); + static int luaPlayerGetCoins(lua_State* L); + static int luaPlayerAddCoins(lua_State* L); + static int luaPlayerRemoveCoins(lua_State* L); + + static int luaPlayerGetTournamentCoins(lua_State* L); + static int luaPlayerAddTournamentCoins(lua_State* L); + static int luaPlayerRemoveTournamentCoins(lua_State* L); static int luaPlayerHasBlessing(lua_State* L); static int luaPlayerAddBlessing(lua_State* L); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 51c45a87e6f..61049aa7eef 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -740,7 +740,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt case 0xE7: /* thank you */ break; case 0xE8: parseSendDescription(msg); break; case 0xEE: parseGreet(msg); break; - case 0xEF: parseStoreCoinTransfer(msg); break; + case 0xEF: parseCoinTransfer(msg); break; case 0xF0: addGameTaskTimed(DISPATCHER_TASK_EXPIRATION, &Game::playerShowQuestLog, player->getID()); break; case 0xF1: parseQuestLine(msg); break; // case 0xF2: parseRuleViolationReport(msg); break; @@ -2426,11 +2426,11 @@ void ProtocolGame::parseGreet(NetworkMessage &msg) addGameTask(&Game::playerNpcGreet, player->getID(), npcId); } -void ProtocolGame::parseStoreCoinTransfer(NetworkMessage &msg) { +void ProtocolGame::parseCoinTransfer(NetworkMessage &msg) { std::string recipient = msg.getString(); uint16_t amount = msg.get(); - addGameTask(&Game::playerStoreCoinTransfer, player->getID(), recipient, amount); + addGameTask(&Game::playerCoinTransfer, player->getID(), recipient, amount); } void ProtocolGame::parseDebugAssert(NetworkMessage &msg) @@ -2531,7 +2531,7 @@ void ProtocolGame::parseMarketCancelOffer(NetworkMessage &msg) addGameTask(&Game::playerCancelMarketOffer, player->getID(), timestamp, counter); } - updateStoreCoinBalance(); + updateCoinBalance(); } void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) @@ -2544,7 +2544,7 @@ void ProtocolGame::parseMarketAcceptOffer(NetworkMessage &msg) addGameTask(&Game::playerAcceptMarketOffer, player->getID(), timestamp, counter, amount); } - updateStoreCoinBalance(); + updateCoinBalance(); } void ProtocolGame::parseModalWindowAnswer(NetworkMessage &msg) @@ -4000,11 +4000,11 @@ void ProtocolGame::sendMarketEnter(uint32_t depotId) writeToOutputBuffer(msg); - updateStoreCoinBalance(); + updateCoinBalance(); sendResourcesBalance(player->getMoney(), player->getBankBalance()); } -void ProtocolGame::sendStoreCoinBalance() +void ProtocolGame::sendCoinBalance() { if (!player) { @@ -4025,19 +4025,20 @@ void ProtocolGame::sendStoreCoinBalance() msg.addByte(0x01); // Total coins - msg.add(player->getStoreCoinBalance(COIN_TYPE_DEFAULT)); + msg.add(player->getCoinBalance()); // Transferable coins - msg.add(player->getStoreCoinBalance(COIN_TYPE_TRANSFERABLE)); + msg.add(player->getCoinBalance()); // Reserved Auction Coins // Version 1220+ msg.add(0); // Tournament Coins - msg.add(player->getStoreCoinBalance(COIN_TYPE_TOURNAMENT)); + msg.add(player->getTournamentCoinBalance()); writeToOutputBuffer(msg); } -void ProtocolGame::updateStoreCoinBalance() +// Update coin and tournament coin balance +void ProtocolGame::updateCoinBalance() { if (!player) { return; @@ -4048,10 +4049,12 @@ void ProtocolGame::updateStoreCoinBalance() Player* player = g_game.getPlayerByID(playerId); if (player != nullptr) { account::Account account(player->getAccount()); - account.LoadAccountDB(); - player->coinBalance = account.GetCoins(); - player->tournamentCoinBalance = account.GetCoins(COIN_TYPE_TOURNAMENT); - player->sendStoreCoinBalance(); + account.loadAccountDB(); + // Update coin balance + player->coinBalance = account.getCoins(); + // Update tournament coin balance + player->tournamentCoinBalance = account.getTournamentCoins(); + player->sendCoinBalance(); } }, player->getID())) ); @@ -4091,7 +4094,7 @@ void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList & msg.addString(offer.playerName); } - updateStoreCoinBalance(); + updateCoinBalance(); writeToOutputBuffer(msg); } @@ -6755,7 +6758,7 @@ void ProtocolGame::openStore() } writeToOutputBuffer(msg); - player->updateStoreCoinBalance(); + player->updateCoinBalance(); sendStoreHome(); } diff --git a/src/server/network/protocol/protocolgame.h b/src/server/network/protocol/protocolgame.h index c8140e52cdb..dcbb6621a91 100644 --- a/src/server/network/protocol/protocolgame.h +++ b/src/server/network/protocol/protocolgame.h @@ -142,7 +142,7 @@ class ProtocolGame final : public Protocol void parseTournamentLeaderboard(NetworkMessage &msg); void parseGreet(NetworkMessage &msg); - void parseStoreCoinTransfer(NetworkMessage &msg); + void parseCoinTransfer(NetworkMessage &msg); void parseBugReport(NetworkMessage &msg); void parseDebugAssert(NetworkMessage &msg); void parseRuleViolationReport(NetworkMessage &msg); @@ -329,7 +329,7 @@ class ProtocolGame final : public Protocol void sendResourceBalance(Resource_t resourceType, uint64_t value); void sendSaleItemList(const ShopInfoMap &shop, const std::map &inventoryMap); void sendMarketEnter(uint32_t depotId); - void updateStoreCoinBalance(); + void updateCoinBalance(); void sendMarketLeave(); void sendMarketBrowseItem(uint16_t itemId, const MarketOfferList &buyOffers, const MarketOfferList &sellOffers); void sendMarketAcceptOffer(const MarketOfferEx &offer); @@ -364,7 +364,7 @@ class ProtocolGame final : public Protocol void sendSpellCooldown(uint8_t spellId, uint32_t time); void sendSpellGroupCooldown(SpellGroup_t groupId, uint32_t time); - void sendStoreCoinBalance(); + void sendCoinBalance(); //tiles void sendMapDescription(const Position &pos); diff --git a/tests/account_test.cpp b/tests/account_test.cpp index dbce216fe40..751fc44686a 100644 --- a/tests/account_test.cpp +++ b/tests/account_test.cpp @@ -89,7 +89,7 @@ TEST_CASE("Get Coins Account Not Initialized", "[UnitTest]") { account::Account account; error_t result; uint32_t coins; - result = account.GetCoins(&coins); + result = account.getCoins(&coins); CHECK(result == account::ERROR_NOT_INITIALIZED); } @@ -250,21 +250,21 @@ TEST_CASE("Get Coins", "[UnitTest]") { account::Account account(1); error_t result; uint32_t coins; - result = account.GetCoins(&coins); + result = account.getCoins(&coins); CHECK(result == account::ERROR_DB); } TEST_CASE("Add Zero Coins", "[UnitTest]") { account::Account account(1); error_t result; - result = account.AddCoins(0); + result = account.addCoins(0); REQUIRE(result == account::ERROR_NO); } TEST_CASE("Remove Zero Coins", "[UnitTest]") { account::Account account(1); error_t result; - result = account.RemoveCoins(0); + result = account.removeCoins(0); REQUIRE(result == account::ERROR_NO); } /******************************************************************************* @@ -324,16 +324,16 @@ TEST_CASE("Remove Coins From Account With Zero Coins", "[IntegrationTest]") { // Clean account coins uint32_t get_coins; - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); - result = account.RemoveCoins(get_coins); + result = account.removeCoins(get_coins); CHECK(result == account::ERROR_NO); db_tasks.flush(); db_tasks.stop(); db_tasks.shutdown(); db_tasks.join(); - result = account.RemoveCoins(1); + result = account.removeCoins(1); REQUIRE(result == account::ERROR_VALUE_NOT_ENOUGH_COINS); } @@ -366,17 +366,17 @@ TEST_CASE("Add Maximum Number Of Coins", "[IntegrationTest]") { // Clean account coins uint32_t get_coins; - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); - result = account.RemoveCoins(get_coins); + result = account.removeCoins(get_coins); CHECK(result == account::ERROR_NO); db_tasks.flush(); - result = account.AddCoins(std::numeric_limits::max()); + result = account.addCoins(std::numeric_limits::max()); REQUIRE(result == account::ERROR_NO); db_tasks.flush(); - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); db_tasks.stop(); @@ -414,20 +414,20 @@ TEST_CASE("Add Maximum Number Of Coins Plus One", "[IntegrationTest]") { // Clean account coins uint32_t get_coins; - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); - result = account.RemoveCoins(get_coins); + result = account.removeCoins(get_coins); CHECK(result == account::ERROR_NO); db_tasks.flush(); - result = account.AddCoins(std::numeric_limits::max()); + result = account.addCoins(std::numeric_limits::max()); REQUIRE(result == account::ERROR_NO); db_tasks.flush(); db_tasks.stop(); db_tasks.shutdown(); db_tasks.join(); - result = account.AddCoins(1); + result = account.addCoins(1); REQUIRE(result == account::ERROR_VALUE_OVERFLOW); } @@ -460,18 +460,18 @@ TEST_CASE("Add/Remove Coins Operation", "[IntegrationTest]") { // Clean account coins uint32_t get_coins; - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); - result = account.RemoveCoins(get_coins); + result = account.removeCoins(get_coins); CHECK(result == account::ERROR_NO); db_tasks.flush(); uint32_t add_coins = 15; - result = account.AddCoins(add_coins); + result = account.addCoins(add_coins); REQUIRE(result == account::ERROR_NO); db_tasks.flush(); - result = account.GetCoins(&get_coins); + result = account.getCoins(&get_coins); CHECK(result == account::ERROR_NO); db_tasks.stop(); @@ -498,7 +498,7 @@ TEST_CASE("Load Account Using ID From Constructor", "[IntegrationTest]") { } error_t result; - result = account.LoadAccountDB(); + result = account.loadAccountDB(); REQUIRE(result == account::ERROR_NO); uint32_t id; @@ -550,7 +550,7 @@ TEST_CASE("Load Account Using Email From Constructor", "[IntegrationTest]") { } error_t result; - result = account.LoadAccountDB(); + result = account.loadAccountDB(); REQUIRE(result == account::ERROR_NO); uint32_t id; @@ -602,7 +602,7 @@ TEST_CASE("Load Account Using ID", "[IntegrationTest]") { } error_t result; - result = account.LoadAccountDB(1); + result = account.loadAccountDB(1); REQUIRE(result == account::ERROR_NO); uint32_t id; @@ -654,7 +654,7 @@ TEST_CASE("Load Account Using Email", "[IntegrationTest]") { } error_t result; - result = account.LoadAccountDB("@GOD"); + result = account.loadAccountDB("@GOD"); REQUIRE(result == account::ERROR_NO); uint32_t id; @@ -707,9 +707,9 @@ TEST_CASE("Save Account", "[IntegrationTest]") { } error_t result; - result = account_orig.LoadAccountDB(); + result = account_orig.loadAccountDB(); REQUIRE(result == account::ERROR_NO); - result = account.LoadAccountDB(); + result = account.loadAccountDB(); REQUIRE(result == account::ERROR_NO); // Check account @@ -772,7 +772,7 @@ TEST_CASE("Save Account", "[IntegrationTest]") { //Load Changed Account account::Account changed_account; - result = changed_account.LoadAccountDB(1); + result = changed_account.loadAccountDB(1); //Check Changed Account result = changed_account.GetID(&id); From af1b73de1574269b6aee8e0b30f95a334cef54b4 Mon Sep 17 00:00:00 2001 From: dudantas Date: Mon, 23 Aug 2021 13:41:34 -0300 Subject: [PATCH 23/30] Fix identations --- data/XML/store.xml | 2 +- data/lib/core/functions/player.lua | 1 - src/game/game.cpp | 2 +- src/server/network/protocol/protocolgame.cpp | 14 +++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/data/XML/store.xml b/data/XML/store.xml index 8cb61ce8c8b..8e8b0cec6e4 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -44,7 +44,7 @@ - + diff --git a/data/lib/core/functions/player.lua b/data/lib/core/functions/player.lua index b0410484eda..fbff3e29e99 100644 --- a/data/lib/core/functions/player.lua +++ b/data/lib/core/functions/player.lua @@ -184,7 +184,6 @@ function Player.hasRookgaardShield(self) or self:getItemCount(2530) > 0 end - function Player.isSorcerer(self) return table.contains({VOCATION.ID.SORCERER, VOCATION.ID.MASTER_SORCERER}, self:getVocation():getId()) end diff --git a/src/game/game.cpp b/src/game/game.cpp index 8736f64fda7..e9df3c6d31c 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -8345,7 +8345,7 @@ void Game::playerOpenStore(uint32_t playerId, bool openStore, StoreOffers* offer player->openStore(); } else if (offers == nullptr) { player->sendStoreHome(); - } else if (offers != nullptr) { + } else { player->sendShowStoreOffers(offers); } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 61049aa7eef..9e1e2a5daea 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -2582,9 +2582,7 @@ void ProtocolGame::parseRequestStoreOffers(NetworkMessage& msg) } StoreOffers* offers = nullptr; - if (actionType == 0) { - offers = g_store.getOfferByName(g_config.getString(DEFAULT_OFFER)); - } else if (actionType == 2) { + if (actionType == 2) { std::string categoryName = msg.getString(); offers = g_store.getOfferByName(categoryName); } else if (actionType == 4) { @@ -6627,8 +6625,9 @@ void ProtocolGame::sendShowStoreOffers(StoreOffers* offers) uint16_t count = 0; std::map> organized = g_store.getStoreOrganizedByName(offers); for (const auto& it : organized) { - if (!it.first.empty()) + if (!it.first.empty()) { count++; + } } msg.add(count); @@ -6776,8 +6775,9 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i lastitemid = (*offer)->getItemType(); lastoutfit = (player->getSex() == PLAYERSEX_FEMALE ? (*offer)->getOutfitFemale() : (*offer)->getOutfitMale()); lastmount = (*offer)->getMount(); - if (lastid == 0) + if (lastid == 0) { lastid = (*offer)->getId(); + } msg.add((*offer)->getId()); msg.add((*offer)->getCount()); @@ -6788,7 +6788,7 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i msg.addByte(!disabled.empty()); if (!disabled.empty()) { msg.addByte(0x01); - msg.addString(disabled); + msg.addString(disabled); } if ((*offer)->getOfferState() == OFFER_STATE_SALE) { @@ -6828,7 +6828,7 @@ void ProtocolGame::addStoreOffer(NetworkMessage& msg, std::vector i // Version 1220+ msg.addByte(0x00); - msg.add(0x00); // category + msg.add(0x00); // Category msg.add(298); msg.add(lasttype == OFFER_TYPE_NAME_CHANGE ? lastid : 0x00); From 86bc6da1759a01a808ee07eda983723bb8515a72 Mon Sep 17 00:00:00 2001 From: Renato Foot Date: Wed, 25 Aug 2021 00:20:29 -0300 Subject: [PATCH 24/30] Refactor account class Signed-off-by: Renato Foot --- data/migrations/0.lua | 5 +- schema.sql | 33 +- src/creatures/players/account/account.cpp | 334 ++++++++++-------- src/creatures/players/account/account.hpp | 108 +++--- src/creatures/players/player.cpp | 34 +- src/creatures/players/store/store.cpp | 20 +- src/game/game.cpp | 199 ++++++++--- src/io/iologindata.cpp | 91 +++-- .../creatures/player/player_functions.cpp | 63 +++- src/server/network/protocol/protocolgame.cpp | 25 +- src/server/network/protocol/protocollogin.cpp | 17 +- 11 files changed, 583 insertions(+), 346 deletions(-) diff --git a/data/migrations/0.lua b/data/migrations/0.lua index 50d8834d222..aa3370202b8 100644 --- a/data/migrations/0.lua +++ b/data/migrations/0.lua @@ -1,7 +1,6 @@ function onUpdateDatabase() Spdlog.info("Updating database to version 1 (store c++)") - db.query("ALTER TABLE `accounts` ADD `tournament_coins` INT(11) NOT NULL DEFAULT 0") - db.query("DROP TABLE `coins_transactions`") - db.query([[CREATE TABLE IF NOT EXISTS `store_history` (`account_id` int(11) NOT NULL, `mode` smallint(2) NOT NULL DEFAULT '0', `description` VARCHAR(3500) NOT NULL, `coin_amount` int(12) NOT NULL, `time` bigint(20) unsigned NOT NULL, KEY `account_id` (`account_id`), FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON DELETE CASCADE) ENGINE=InnoDB;]]) + db.query("ALTER TABLE `accounts` ADD `tournament_coins` int(11) NOT NULL DEFAULT 0") + db.query("ALTER TABLE `coins_transactions` ADD `coin_type` tinyint(1) NOT NULL DEFAULT 0") return true end diff --git a/schema.sql b/schema.sql index 79477d04a8b..710ec35b250 100644 --- a/schema.sql +++ b/schema.sql @@ -19,9 +19,27 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `lastday` int(10) UNSIGNED NOT NULL DEFAULT '0', `type` tinyint(1) UNSIGNED NOT NULL DEFAULT '1', `coins` int(12) UNSIGNED NOT NULL DEFAULT '0', - `tournament_coins` INT(11) NOT NULL DEFAULT '0', + `tournament_coins` int(11) NOT NULL DEFAULT '0', `creation` int(11) UNSIGNED NOT NULL DEFAULT '0', - `recruiter` INT(6) DEFAULT 0, + `recruiter` int(6) DEFAULT 0, + CONSTRAINT `accounts_pk` PRIMARY KEY (`id`), + CONSTRAINT `accounts_unique` UNIQUE (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Table structure `coins_transactions` +CREATE TABLE IF NOT EXISTS `coins_transactions` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `account_id` int(11) UNSIGNED NOT NULL, + `type` tinyint(1) UNSIGNED NOT NULL, + `coin_type` tinyint(1) UNSIGNED NOT NULL, + `amount` int(12) UNSIGNED NOT NULL, + `description` varchar(3500), + `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP, + INDEX `account_id` (`account_id`), + CONSTRAINT `coins_transactions_pk` PRIMARY KEY (`id`), + CONSTRAINT `coins_transactions_account_fk` + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) + ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `players` @@ -654,17 +672,6 @@ CREATE TABLE IF NOT EXISTS `player_storage` ( -- -------------------------------------------------------- --- Table structure `store_history` -CREATE TABLE IF NOT EXISTS `store_history` ( - `account_id` int(11) NOT NULL, - `mode` tinyint(1) NOT NULL DEFAULT 0, - `amount` int(11) NOT NULL, - `coinMode` tinyint(2) NOT NULL DEFAULT 0, - `description` varchar(255) DEFAULT NULL, - `cust` int(11) NOT NULL, - `time` bigint(20) DEFAULT NULL, - INDEX `account_id` (`account_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Table structure `tile_store` CREATE TABLE IF NOT EXISTS `tile_store` ( diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index d41d9fcc45f..fa7eb68909e 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -21,7 +21,6 @@ #include "creatures/players/account/account.hpp" #include "database/databasetasks.h" -#include "game/game.h" #include #include @@ -34,8 +33,8 @@ Account::Account() { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coinBalance = 0; - tournamentCoinBalance = 0; + coin_balance_ = 0; + tournament_coin_balance_ = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -47,8 +46,8 @@ Account::Account(uint32_t id) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coinBalance = 0; - tournamentCoinBalance = 0; + coin_balance_ = 0; + tournament_coin_balance_ = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -59,8 +58,8 @@ Account::Account(const std::string &email) : email_(email) { password_.clear(); premium_remaining_days_ = 0; premium_last_day_ = 0; - coinBalance = 0; - tournamentCoinBalance = 0; + coin_balance_ = 0; + tournament_coin_balance_ = 0; account_type_ = ACCOUNT_TYPE_NORMAL; db_ = &Database::getInstance(); db_tasks_ = &g_databaseTasks; @@ -94,9 +93,10 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { * Coins Methods ******************************************************************************/ -error_t Account::getCoins() { +std::tuple Account::GetCoins() { + if (db_ == nullptr || id_ == 0) { - return ERROR_NOT_INITIALIZED; + return std::make_tuple(0, ERROR_NOT_INITIALIZED); } std::ostringstream query; @@ -104,41 +104,80 @@ error_t Account::getCoins() { DBResult_ptr result = db_->storeQuery(query.str()); if (!result) { - return ERROR_DB; + return std::make_tuple(0, ERROR_DB); } - return result->getNumber("coins"); + return std::make_tuple(result->getNumber("coins"), ERROR_NO); } -error_t Account::addCoins(int32_t amount) -{ +error_t Account::AddCoins(const uint32_t &amount) { + + if (db_tasks_ == nullptr) { + return ERROR_NULLPTR; + } + + if (amount == 0) { + return ERROR_NO; + } + + int result = 0; + uint32_t current_coins = 0; + + if (auto [ current_coins, result ] = this->GetCoins(); ERROR_NO == result) { + if ((current_coins + amount) < current_coins) { + return ERROR_VALUE_OVERFLOW; + } + } else { + return ERROR_GET_COINS; + } + std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = `coins` + " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `coins` = " << (current_coins + amount) + << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); + + this->RegisterCoinsTransaction(COIN_ADD, amount, COIN, ""); + return ERROR_NO; } -error_t Account::removeCoins(int32_t amount) { +error_t Account::RemoveCoins(const uint32_t &amount) { if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; + return ERROR_NULLPTR; } - if (amount == 0) { + if (amount == 0) { return ERROR_NO; } + int result = 0; + uint32_t current_coins = 0; + + if (auto [ current_coins, result ] = this->GetCoins(); ERROR_NO == result) { + if ((current_coins - amount) > current_coins) { + return ERROR_VALUE_NOT_ENOUGH_COINS; + } + } else { + return ERROR_GET_COINS; + } + std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = `coins` - " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `coins` = "<< (current_coins - amount) + << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); + + this->RegisterCoinsTransaction(COIN_REMOVE, amount, COIN, ""); + return ERROR_NO; } -error_t Account::getTournamentCoins() { +std::tuple Account::GetTournamentCoins() { + if (db_ == nullptr || id_ == 0) { - return ERROR_NOT_INITIALIZED; + return std::make_tuple(0, ERROR_NOT_INITIALIZED); } std::ostringstream query; @@ -146,61 +185,95 @@ error_t Account::getTournamentCoins() { DBResult_ptr result = db_->storeQuery(query.str()); if (!result) { - return ERROR_DB; + return std::make_tuple(0, ERROR_DB); } - return result->getNumber("tournament_coins"); + return std::make_tuple(result->getNumber("tournament_coins"), ERROR_NO); } -error_t Account::addTournamentCoins(int32_t amount) -{ +error_t Account::AddTournamentCoins(const uint32_t &amount) { + + if (db_tasks_ == nullptr) { + return ERROR_NULLPTR; + } + if (amount == 0) { + return ERROR_NO; + } + + int result = 0; + uint32_t current_tournament_coins = 0; + + if (auto [ current_tournament_coins, result ] = this->GetTournamentCoins(); + ERROR_NO == result) { + if ((current_tournament_coins + amount) < current_tournament_coins) { + return ERROR_VALUE_OVERFLOW; + } + } else { + return ERROR_GET_COINS; + } + std::ostringstream query; - query << "UPDATE `accounts` SET `tournament_coins` = `tournament_coins` + " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `tournament_coins` = " + << (current_tournament_coins + amount) << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); + + this->RegisterCoinsTransaction(COIN_ADD, amount, TOURNAMENT, ""); + return ERROR_NO; } -error_t Account::removeTournamentCoins(int32_t amount) { +error_t Account::RemoveTournamentCoins(const uint32_t &amount) { if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; + return ERROR_NULLPTR; } - if (amount == 0) { + if (amount == 0) { return ERROR_NO; } + int result = 0; + uint32_t current_tournament_coins = 0; + + if (auto [ current_tournament_coins, result ] = this->GetTournamentCoins(); + ERROR_NO == result) { + if ((current_tournament_coins - amount) > current_tournament_coins) { + return ERROR_VALUE_NOT_ENOUGH_COINS; + } + } else { + return ERROR_GET_COINS; + } + std::ostringstream query; - query << "UPDATE `accounts` SET `tournament_coins` = `tournament_coins` - " << amount << " WHERE `id` = " << id_; + query << "UPDATE `accounts` SET `tournament_coins` = " + << (current_tournament_coins - amount) << " WHERE `id` = " << id_; db_tasks_->addTask(query.str()); + + this->RegisterCoinsTransaction(COIN_REMOVE, amount, TOURNAMENT, ""); + return ERROR_NO; } -error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust) -{ +error_t Account::RegisterCoinsTransaction(CoinTransactionType type, + uint32_t coins, CoinType coin_type, const std::string& description) { + if (db_ == nullptr) { - return ERROR_NULLPTR; + return ERROR_NULLPTR; } std::ostringstream query; - query << "INSERT INTO `store_history` (`account_id`, `time`, `mode`, `amount`, `coinMode`, `description`, `cust`) VALUES (" - << id_ << "," - << time << "," - << static_cast(mode) << "," - << amount << "," << static_cast(coinMode) << "," - << db_->escapeString(description) << "," - << cust << ")"; + query << "INSERT INTO `coins_transactions` (`account_id`, `type`, `amount`," + " `coin_type`, `description`) VALUES (" << id_ << ", " + << static_cast(type) << ", "<< coins << ", " + << static_cast(coin_type) << ", " + << db_->escapeString(description) << ")"; if (!db_->executeQuery(query.str())) { - return ERROR_DB; + return ERROR_DB; } - StoreHistory historyOffer(time, mode, amount, coinMode, description, cust); - g_game.addAccountHistory(id_, historyOffer); - - db_->executeQuery(query.str()); return ERROR_NO; } @@ -208,30 +281,30 @@ error_t Account::RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t * Database ******************************************************************************/ -error_t Account::loadAccountDB() { +error_t Account::LoadAccountDB() { if (id_ != 0) { - return this->loadAccountDB(id_); + return this->LoadAccountDB(id_); } else if (!email_.empty()) { - return this->loadAccountDB(email_); + return this->LoadAccountDB(email_); } return ERROR_NOT_INITIALIZED; } -error_t Account::loadAccountDB(std::string email) { +error_t Account::LoadAccountDB(const std::string email) { std::ostringstream query; query << "SELECT * FROM `accounts` WHERE `email` = " << db_->escapeString(email); - return this->loadAccountDB(query); + return this->LoadAccountDB(query); } -error_t Account::loadAccountDB(uint32_t id) { +error_t Account::LoadAccountDB(uint32_t id) { std::ostringstream query; query << "SELECT * FROM `accounts` WHERE `id` = " << id; - return this->loadAccountDB(query); + return this->LoadAccountDB(query); } -error_t Account::loadAccountDB(std::ostringstream &query) { +error_t Account::LoadAccountDB(const std::ostringstream &query) { if (db_ == nullptr) { return ERROR_NULLPTR; } @@ -247,36 +320,40 @@ error_t Account::loadAccountDB(std::ostringstream &query) { this->SetPassword(result->getString("password")); this->SetPremiumRemaningDays(result->getNumber("premdays")); this->SetPremiumLastDay(result->getNumber("lastday")); - this->setCoinBalance(result->getNumber("coins")); - this->setTournamentCoinBalance(result->getNumber("tournament_coins")); return ERROR_NO; } -error_t Account::LoadAccountPlayerDB(Player *player, std::string& characterName) { +std::tuple Account::LoadAccountPlayerDB(const std::string& characterName) { + + Player player; + if (id_ == 0) { - return ERROR_NOT_INITIALIZED; + std::make_tuple(player, ERROR_NOT_INITIALIZED); } std::ostringstream query; query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " - << id_ << " AND `name` = " << db_->escapeString(characterName) - << " ORDER BY `name` ASC"; + << id_ << " AND `name` = " << db_->escapeString(characterName) + << " ORDER BY `name` ASC"; DBResult_ptr result = db_->storeQuery(query.str()); if (!result || result->getNumber("deletion") != 0) { - return ERROR_PLAYER_NOT_FOUND; + return std::make_tuple(player, ERROR_PLAYER_NOT_FOUND); } - player->name = result->getString("name"); - player->deletion = result->getNumber("deletion"); + player.name = result->getString("name"); + player.deletion = result->getNumber("deletion"); - return ERROR_NO; + return std::make_tuple(player, ERROR_NO); } -error_t Account::LoadAccountPlayersDB(std::vector *players) { +std::tuple, error_t> Account::LoadAccountPlayersDB() { + + std::vector players; + if (id_ == 0) { - return ERROR_NOT_INITIALIZED; + return std::make_tuple(players, ERROR_NOT_INITIALIZED); } std::ostringstream query; @@ -285,7 +362,7 @@ error_t Account::LoadAccountPlayersDB(std::vector *players) { DBResult_ptr result = db_->storeQuery(query.str()); if (!result) { - return ERROR_DB; + return std::make_tuple(players, ERROR_DB); } do { @@ -293,10 +370,10 @@ error_t Account::LoadAccountPlayersDB(std::vector *players) { Player new_player; new_player.name = result->getString("name"); new_player.deletion = result->getNumber("deletion"); - players->push_back(new_player); + players.push_back(new_player); } } while (result->next()); - return ERROR_NO; + return std::make_tuple(players, ERROR_NO); } error_t Account::SaveAccountDB() { @@ -306,8 +383,8 @@ error_t Account::SaveAccountDB() { << "`email` = " << db_->escapeString(email_) << " , " << "`type` = " << account_type_ << " , " << "`password` = " << db_->escapeString(password_) << " , " - << "`coins` = " << coinBalance << " , " - << "`tournament_coins` = " << tournamentCoinBalance << " , " + << "`coins` = " << coin_balance_ << " , " + << "`tournament_coins` = " << tournament_coin_balance_ << " , " << "`premdays` = " << premium_remaining_days_ << " , " << "`lastday` = " << premium_last_day_; @@ -336,13 +413,8 @@ error_t Account::SetID(uint32_t id) { return ERROR_NO; } -error_t Account::GetID(uint32_t *id) { - if (id == nullptr) { - return ERROR_NULLPTR; - } - - *id = id_; - return ERROR_NO; +uint32_t Account::GetID() { + return id_; } error_t Account::SetEmail(std::string email) { @@ -353,13 +425,8 @@ error_t Account::SetEmail(std::string email) { return ERROR_NO; } -error_t Account::GetEmail(std::string *email) { - if (email == nullptr) { - return ERROR_NULLPTR; - } - - *email = email_; - return ERROR_NO; +std::string Account::GetEmail() { + return email_; } error_t Account::SetPassword(std::string password) { @@ -370,13 +437,8 @@ error_t Account::SetPassword(std::string password) { return ERROR_NO; } -error_t Account::GetPassword(std::string *password) { - if (password == nullptr) { - return ERROR_NULLPTR; - } - - *password = password_; - return ERROR_NO; +std::string Account::GetPassword() { + return password_; } error_t Account::SetPremiumRemaningDays(uint32_t days) { @@ -384,13 +446,8 @@ error_t Account::SetPremiumRemaningDays(uint32_t days) { return ERROR_NO; } -error_t Account::GetPremiumRemaningDays(uint32_t *days) { - if (days == nullptr) { - return ERROR_NULLPTR; - } - - *days = premium_remaining_days_; - return ERROR_NO; +uint32_t Account::GetPremiumRemaningDays() { + return premium_remaining_days_; } error_t Account::SetPremiumLastDay(time_t last_day) { @@ -401,49 +458,8 @@ error_t Account::SetPremiumLastDay(time_t last_day) { return ERROR_NO; } -error_t Account::GetPremiumLastDay(time_t *last_day) { - if (last_day == nullptr) { - return ERROR_NULLPTR; - } - - *last_day = premium_last_day_; - return ERROR_NO; -} - -error_t Account::setCoinBalance(uint32_t coins) { - if (coins == 0) { - return ERROR_INVALID_ID; - } - - coinBalance = coins; - return ERROR_NO; -} - -error_t Account::getCoinBalance(uint32_t *coins) { - if (coins == nullptr) { - return ERROR_NULLPTR; - } - - *coins = coinBalance; - return ERROR_NO; -} - -error_t Account::setTournamentCoinBalance(uint32_t tournamentCoins) { - if (tournamentCoins == 0) { - return ERROR_INVALID_ID; - } - - tournamentCoinBalance = tournamentCoins; - return ERROR_NO; -} - -error_t Account::getTournamentCoinBalance(uint32_t *tournamentCoins) { - if (tournamentCoins == nullptr) { - return ERROR_NULLPTR; - } - - *tournamentCoins = tournamentCoinBalance; - return ERROR_NO; +time_t Account::GetPremiumLastDay() { + return premium_last_day_; } error_t Account::SetAccountType(AccountType account_type) { @@ -454,29 +470,33 @@ error_t Account::SetAccountType(AccountType account_type) { return ERROR_NO; } -error_t Account::GetAccountType(AccountType *account_type) { - if (account_type == nullptr) { - return ERROR_NULLPTR; - } - - *account_type = account_type_; - return ERROR_NO; +AccountType Account::GetAccountType() { + return account_type_; } -error_t Account::GetAccountPlayer(Player *player, std::string& characterName) { - if (player == nullptr) { - return ERROR_NULLPTR; +std::tuple Account::GetAccountPlayer( + const std::string& characterName) { + + Player player; + int result; + if (auto [ player, result ] = this->LoadAccountPlayerDB(characterName); + ERROR_NO == result) { + return std::make_tuple(player, ERROR_NO); } - return this->LoadAccountPlayerDB(player, characterName); + return std::make_tuple(player, result); } -error_t Account::GetAccountPlayers(std::vector *players) { - if (players == nullptr) { - return ERROR_NULLPTR; - } - return this->LoadAccountPlayersDB(players); +std::tuple, error_t> Account::GetAccountPlayers() { + std::vector players; + int result; + if (auto [ players, result ] = this->LoadAccountPlayersDB(); + ERROR_NO == result) { + return std::make_tuple(players, ERROR_NO); + } else { + return std::make_tuple(players, result); + } } -} // namespace account +} // namespace account diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index 288137f8574..7d04abadfc0 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -33,6 +33,7 @@ namespace account { enum Errors : uint8_t { ERROR_NO = 0, ERROR_DB, + ERROR_GET_COINS, ERROR_INVALID_ACCOUNT_EMAIL, ERROR_INVALID_ACC_PASSWORD, ERROR_INVALID_ACC_TYPE, @@ -68,6 +69,12 @@ enum CoinTransactionType : uint8_t { COIN_REMOVE = 2 }; +enum CoinType : uint8_t { + COIN = 1, + TOURNAMENT = 2 +}; + + typedef struct { std::string name; uint64_t deletion; @@ -88,14 +95,14 @@ class Account { /** * @brief Construct a new Account object * - * @param id Set Account ID to be used by loadAccountDB + * @param id Set Account ID to be used by LoadAccountDB */ explicit Account(uint32_t id); /** * @brief Construct a new Account object * - * @param name Set Account Name to be used by loadAccountDB + * @param name Set Account Name to be used by LoadAccountDB */ explicit Account(const std::string &name); @@ -125,13 +132,13 @@ class Account { * Coins Methods **************************************************************************/ - /** Normal coins + /** Coins * @brief Get the amount of coins that the account has from database. * * @param coins Pointer to return the number of coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t getCoins(); + std::tuple GetCoins(); /** * @brief Add coins to the account and update database. @@ -139,7 +146,7 @@ class Account { * @param amount Amount of coins to be added * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t addCoins(int32_t amount); + error_t AddCoins(const uint32_t &amount); /** * @brief Removes coins from the account and update database. @@ -147,24 +154,14 @@ class Account { * @param amount Amount of coins to be removed * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t removeCoins(int32_t amount); + error_t RemoveCoins(const uint32_t &amount); - /** - * @brief Register account coins transactions in database. - * - * @param type Type of the transaction(Add/Remove). - * @param coins Amount of coins - * @param description Description of the transaction - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - - /** Tournament Coins * @brief Get the amount of tournament coins that the account has from database. * * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t getTournamentCoins(); + std::tuple GetTournamentCoins(); /** * @brief Add coins to the account and update database. @@ -172,7 +169,7 @@ class Account { * @param amount Amount of coins to be added * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t addTournamentCoins(int32_t amount); + error_t AddTournamentCoins(const uint32_t &amount); /** * @brief Removes tournament coins from the account and update database. @@ -180,17 +177,7 @@ class Account { * @param amount Amount of coins to be removed * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t removeTournamentCoins(int32_t amount); - - /** - * @brief Register account tournament coins transactions in database. - * - * @param type Type of the transaction(Add/Remove). - * @param coins Amount of coins - * @param description Description of the transaction - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RegisterCoinsTransaction(uint32_t time, uint8_t mode, uint32_t amount, uint8_t coinMode, std::string description, int32_t cust); + error_t RemoveTournamentCoins(const uint32_t &amount); /*************************************************************************** * Database @@ -202,7 +189,7 @@ class Account { * * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t loadAccountDB(); + error_t LoadAccountDB(); /** * @brief Try to @@ -210,7 +197,7 @@ class Account { * @param name * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t loadAccountDB(std::string name); + error_t LoadAccountDB(std::string name); /** * @brief @@ -218,7 +205,7 @@ class Account { * @param id * @return error_t ERROR_NO(0) Success, otherwise Fail. */ - error_t loadAccountDB(uint32_t id); + error_t LoadAccountDB(uint32_t id); /** * @brief @@ -232,37 +219,42 @@ class Account { * Setters and Getters **************************************************************************/ - error_t GetID(uint32_t *id); + uint32_t GetID(); - error_t SetEmail(std::string name); - error_t GetEmail(std::string *name); + error_t SetEmail(std::string name); + std::string GetEmail(); - error_t SetPassword(std::string password); - error_t GetPassword(std::string *password); + error_t SetPassword(std::string password); + std::string GetPassword(); - error_t SetPremiumRemaningDays(uint32_t days); - error_t GetPremiumRemaningDays(uint32_t *days); + error_t SetPremiumRemaningDays(uint32_t days); + uint32_t GetPremiumRemaningDays(); - error_t setCoinBalance(uint32_t coins); - error_t getCoinBalance(uint32_t *coins); + error_t SetPremiumLastDay(time_t last_day); + time_t GetPremiumLastDay(); - error_t setTournamentCoinBalance(uint32_t tournamentCoins); - error_t getTournamentCoinBalance(uint32_t *tournamentCoins); + error_t SetAccountType(AccountType account_type); + AccountType GetAccountType(); - error_t SetPremiumLastDay(time_t last_day); - error_t GetPremiumLastDay(time_t *last_day); - - error_t SetAccountType(AccountType account_type); - error_t GetAccountType(AccountType *account_type); - - error_t GetAccountPlayer(Player *player, std::string& characterName); - error_t GetAccountPlayers(std::vector *players); + std::tuple GetAccountPlayer(const std::string& characterName); + std::tuple, error_t> GetAccountPlayers(); private: error_t SetID(uint32_t id); - error_t loadAccountDB(std::ostringstream &query); - error_t LoadAccountPlayersDB(std::vector *players); - error_t LoadAccountPlayerDB(Player *player, std::string& characterName); + error_t LoadAccountDB(const std::ostringstream &query); + std::tuple LoadAccountPlayerDB(const std::string& characterName); + std::tuple, error_t> LoadAccountPlayersDB(); + + /** + * @brief Register account coins transactions in database. + * + * @param type Type of the transaction(Add/Remove). + * @param coins Amount of coins + * @param description Description of the transaction + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t RegisterCoinsTransaction(CoinTransactionType type, uint32_t coins, + CoinType coin_type, const std::string &description); Database *db_; DatabaseTasks *db_tasks_; @@ -272,11 +264,11 @@ class Account { std::string password_; uint32_t premium_remaining_days_; time_t premium_last_day_; - uint32_t coinBalance; - uint32_t tournamentCoinBalance; + uint32_t coin_balance_; + uint32_t tournament_coin_balance_; AccountType account_type_; }; -} // namespace account +} // namespace account -#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ +#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index bed0e47bfb9..c6f3f3a12d3 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4631,15 +4631,27 @@ void Player::setTournamentCoins(int32_t tournamentCoins) bool Player::canRemoveCoins(int32_t coins) { + int result; + if (lastUpdateCoin - OTSYS_TIME() < 2000) { // Update every 2 seconds lastUpdateCoin = OTSYS_TIME() + 2000; account::Account account(this->getAccount()); - account.loadAccountDB(); - this->coinBalance = account.getCoins(); - } + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return false; + }; + if(auto [ coinBalance, result ] = account.GetCoins() ; + account::ERROR_NO != result) + { + SPDLOG_ERROR("Failed to get Coins for account: [{}]", + this->getAccount()); + return false; + }; + } int32_t removeCoins = coinBalance; return (removeCoins - coins) >= 0; @@ -4652,8 +4664,20 @@ bool Player::canRemoveTournamentCoins(int32_t tournamentCoins) lastUpdateCoin = OTSYS_TIME() + 2000; account::Account account(this->getAccount()); - account.loadAccountDB(); - this->tournamentCoinBalance = account.getTournamentCoins(); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", + account.GetID()); + return false; + }; + + if(auto [ tournamentCoinBalance, result ] = account.GetTournamentCoins() ; + account::ERROR_NO != result) + { + SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", + this->getAccount()); + return false; + }; } int32_t removeTournamentCoins = tournamentCoinBalance; diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index 8b7707c2b6c..4829541f3b7 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -32,12 +32,14 @@ const std::unordered_map CoinTypeMap = { {"transferable", COIN_TYPE_TRANSFERABLE}, {"tournament", COIN_TYPE_TOURNAMENT} }; + const std::unordered_map OfferStatesMap = { {"none", OFFER_STATE_NONE}, {"new", OFFER_STATE_NEW}, {"sale", OFFER_STATE_SALE}, {"timed", OFFER_STATE_TIMED} }; + const std::unordered_map OfferTypesMap = { {"none", OFFER_TYPE_NONE}, {"item", OFFER_TYPE_ITEM}, @@ -518,7 +520,6 @@ std::map> Store::getStoreOrganizedByName(S } } - return filter; } @@ -599,22 +600,22 @@ std::string StoreOffer::getDisabledReason(Player* player) { } } else if (type == OFFER_TYPE_SKULL_REMOVE) { if (player->getSkull() != skull) { - disabledReason = "This offer is disabled for you"; + disabledReason = "This offer is disabled for you"; } } else if (type == OFFER_TYPE_FRAG_REMOVE) { if (player->unjustifiedKills.empty()) { - disabledReason = "You have no frag to remove."; + disabledReason = "You have no frag to remove."; } } else if (type == OFFER_TYPE_RECOVERY_KEY) { int32_t value; player->getAccountStorageValue(1, value); if (value > OS_TIME(nullptr)) { - disabledReason = "You recently generated an RK."; + disabledReason = "You recently generated an RK."; } } if (player->getVocation()->getId() == 0 && !rookgaard) { - disabledReason = "This offer is deactivated."; + disabledReason = "This offer is deactivated."; } if (player->getCoinBalance() - getPrice(player) < 0) { @@ -696,9 +697,12 @@ StoreOffer* Store::getOfferById(uint32_t id) { uint32_t StoreOffer::getPrice(Player* player) { uint32_t offerBasePrice = 0; if (state == OFFER_STATE_SALE) { - time_t mytime; - mytime = time(NULL); - struct tm tm = *localtime(&mytime); + time_t my_time; + struct tm tm; + + my_time = time(NULL); + localtime_r(&my_time, &tm); + int32_t daySub = validUntil - tm.tm_mday; if (daySub < 0) { offerBasePrice = basePrice; diff --git a/src/game/game.cpp b/src/game/game.cpp index e9df3c6d31c..6e2ae067eb9 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6863,8 +6863,10 @@ bool save = false; time_t timeNow = time(nullptr); uint32_t rem_days = 0; time_t last_day; - account.GetPremiumRemaningDays(&rem_days); - account.GetPremiumLastDay(&last_day); + + rem_days = account.GetPremiumRemaningDays(); + last_day = account.GetPremiumLastDay() ; + std::string email; if (rem_days != 0) { if (last_day == 0) { @@ -6875,7 +6877,7 @@ bool save = false; if (days > 0) { if (days >= rem_days) { if(!account.SetPremiumRemaningDays(0) || !account.SetPremiumLastDay(0)) { - account.GetEmail(&email); + email = account.GetEmail(); SPDLOG_ERROR("Failed to set account premium days, account email: {}", email); } @@ -6895,8 +6897,7 @@ bool save = false; } if (save && !account.SaveAccountDB()) { - account.GetEmail(&email); - SPDLOG_ERROR("Failed to save account: {}", email); + SPDLOG_ERROR("Failed to save account: {}", account.GetEmail()); } } @@ -7578,8 +7579,15 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr } account::Account account(player->getAccount()); - account.loadAccountDB(); - account.addCoins(amount); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.AddCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + return; + } } else { uint16_t stashmath = amount; uint16_t stashminus = player->getStashItemCount(it.wareId); @@ -7669,10 +7677,17 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.loadAccountDB(player->getAccount()); - account.addCoins(offer.amount); - } + account::Account account(player->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.AddCoins(offer.amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + return; + } + } else if (it.stackable) { uint16_t tmpAmount = offer.amount; while (tmpAmount > 0) { @@ -7772,14 +7787,29 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.loadAccountDB(player->getAccount()); - if (amount > account.getCoins()) { + account::Account account(player->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + }; + + uint32_t player_coins = 0; + if (auto [ player_coins, result ] = account.GetCoins(); + account::ERROR_NO == result) { + if (amount > player_coins) { + return; + } + } else { + SPDLOG_ERROR("Failed to add Account [{}] coins. (Error: [{}])", + account.GetID(), result); return; } - account.addCoins(amount); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + if (account::ERROR_NO != account.AddCoins(amount)) { + SPDLOG_ERROR("Failed to add Account [{}] coins.", account.GetID()); + return; + } } else { std::forward_list itemList = getMarketItemList(it.wareId, amount, depotLocker); if (itemList.empty()) { @@ -7806,10 +7836,17 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->setBankBalance(player->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.loadAccountDB(buyerPlayer->getAccount()); - account.addCoins(amount); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); + account::Account account(buyerPlayer->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.AddCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + return; + } } else if (it.stackable) { uint16_t tmpAmount = amount; @@ -7869,10 +7906,17 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.loadAccountDB(player->getAccount()); - account.addCoins(amount); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Purchased on Market", -static_cast(amount)); + account::Account account(player->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.AddCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + return; + } } else if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { @@ -7905,9 +7949,12 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (sellerPlayer) { sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { - account::Account account; - account.loadAccountDB(sellerPlayer->getAccount()); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + account::Account account(sellerPlayer->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } } } else { IOLoginData::increaseBankBalance(offer.playerId, totalPrice); @@ -7915,9 +7962,12 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 sellerPlayer = new Player(nullptr); if (IOLoginData::loadPlayerById(sellerPlayer, offer.playerId)) { - account::Account account; - account.loadAccountDB(sellerPlayer->getAccount()); - account.RegisterCoinsTransaction(OS_TIME(nullptr), 0, amount, 0, "Sold on Market", -static_cast(amount)); + account::Account account(sellerPlayer->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } } delete sellerPlayer; @@ -8376,7 +8426,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: if (!player->canRemoveCoins(thisOffer->getPrice(player))) { player->sendStoreError(STORE_ERROR_PURCHASE, "You don't have coins."); return; - } + } } if (thisOffer->getCoinType() == COIN_TYPE_TOURNAMENT) { if (!player->canRemoveTournamentCoins(thisOffer->getPrice(player))) { @@ -8492,7 +8542,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: uint64_t weight = static_cast(itemType.weight) * std::max(1, (isKeg ? 1 : thisOffer->getCount())); if (isCaskItem) { const ItemType& itemType2 = Item::items[TRANSFORM_BOX_ID]; - weight = static_cast(itemType2.weight); + weight = static_cast(itemType2.weight); } if (player->getFreeCapacity() < weight) { player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free capacity to hold this item."); @@ -8549,7 +8599,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: int32_t pack = thisOffer->getCharges(); tmpItem->setIntAttr(ITEM_ATTRIBUTE_CHARGES, pack); - removecount = pack; + removecount = pack; } else if (isHouseOffer) { std::ostringstream packagename; packagename << "You bought this item in the Store.\nUnwrap it in your own house to create a <" << itemType.name << ">."; @@ -8651,7 +8701,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: itemList.push_back(tmpItem); } - } + } if (player->getFreeCapacity() < capacity) { player->sendStoreError(STORE_ERROR_PURCHASE, "Please make sure you have free capacity to hold this item."); @@ -8887,7 +8937,7 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: if (player->getSkull() != thisOffer->getSkull()) { player->sendStoreError(STORE_ERROR_PURCHASE, "This offer is disabled for you!"); - return; + return; } player->removeFrags(); @@ -8939,26 +8989,59 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: } account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + if (successfully) { if (thisOffer->getCoinType() == COIN_TYPE_DEFAULT) { - account.removeCoins(offerPrice*-1); - player->setCoins(account.getCoins()); - if (returnmessage.str().empty()) { - returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" coins"; + if(account::ERROR_NO == account.RemoveCoins(offerPrice*-1)) { + int coins = 0; + if(auto [ coins, result ] = account.GetCoins() ; + account::ERROR_NO == result) { + player->setCoins(coins); + } else { + SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.GetID()); + return; + } + + if (returnmessage.str().empty()) { + returnmessage << "You have purchased " << thisOffer->getName() + << " for " << offerPrice*-1 <<" coins"; + } + } else { + SPDLOG_ERROR("Failed to remove Coins from account: [{}]", account.GetID()); } } else if (thisOffer->getCoinType() == COIN_TYPE_TOURNAMENT) { - account.removeTournamentCoins(offerPrice*-1); - player->setTournamentCoins(account.getTournamentCoins()); + + if(account::ERROR_NO != account.RemoveTournamentCoins(offerPrice*-1)) { + SPDLOG_ERROR("Failed to remove Tournament Coins from account: [{}]", + account.GetID()); + return; + } + + int tournament_coins = 0; + if(auto [ tournament_coins, result ] = account.GetTournamentCoins() ; + account::ERROR_NO != result) + { + SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", + account.GetID()); + return; + }; + + player->setTournamentCoins(tournament_coins); if (returnmessage.str().empty()) { returnmessage << "You have purchased " << thisOffer->getName() << " for " << offerPrice*-1 <<" tournament coins"; } + } else { + SPDLOG_WARN("Unkown offer coin type: [{}]", thisOffer->getCoinType()); } player->updateCoinBalance(); player->sendStorePurchaseSuccessful(returnmessage.str()); - account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), thisOffer->getCount(true), static_cast(thisOffer->getCoinType()), std::move(thisOffer->getName()), offerPrice); } else { player->sendStoreError(STORE_ERROR_PURCHASE, "Something went wrong with your purchase."); } @@ -9039,15 +9122,31 @@ void Game::playerCoinTransfer(uint32_t playerId, const std::string& recipient, u std::string description(player->getName() + " transferred to " + recipient); - account::Account account; - account.loadAccountDB(player->getAccount()); - account.addCoins(-static_cast(amount)); + account::Account account(player->getAccount()); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.RemoveCoins(static_cast(amount))) { + SPDLOG_ERROR("Failed to remove coins to account: [{}]", account.GetID()); + return; + } + player->coinBalance -= amount; - account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, -static_cast(amount)); - account.loadAccountDB(recipientPlayer->getAccount()); - account.addCoins(amount); - account.RegisterCoinsTransaction(OS_TIME(nullptr), static_cast(HISTORY_TYPE_NONE), amount, 0, description, amount); + if(account::ERROR_NO != account.LoadAccountDB(recipientPlayer->getAccount())) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + + if(account::ERROR_NO != account.AddCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + return; + } + recipientPlayer->coinBalance += amount; if (recipientPlayer->isOffline()) { diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index df93d929240..1d5c8587e50 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -33,37 +33,41 @@ extern Game g_game; extern Monsters g_monsters; bool IOLoginData::authenticateAccountPassword(const std::string& email, const std::string& password, account::Account *account) { - if (account::ERROR_NO != account->loadAccountDB(email)) { - SPDLOG_ERROR("Email {} doesn't match any account.", email); - return false; - } - - std::string accountPassword; - account->GetPassword(&accountPassword); - if (transformToSHA1(password) != accountPassword) { - SPDLOG_ERROR("Password '{}' doesn't match any account", transformToSHA1(password)); - return false; - } - - return true; + if (account && account::ERROR_NO != account->LoadAccountDB(email)) { + SPDLOG_ERROR("Email [{}] doesn't match any account.", email); + return false; + } + + std::string accountPassword = account->GetPassword(); + if (transformToSHA1(password) != accountPassword) { + SPDLOG_ERROR("Password [{}] doesn't match any account", + transformToSHA1(password)); + return false; + } + + return true; } -bool IOLoginData::gameWorldAuthentication(const std::string& email, const std::string& password, std::string& characterName, uint32_t *accountId) +bool IOLoginData::gameWorldAuthentication(const std::string& email, + const std::string& password, std::string& characterName, uint32_t *accountId) { - account::Account account; - if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { - return false; - } + account::Account account; + if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { + return false; + } - account::Player player; - if (account::ERROR_NO != account.GetAccountPlayer(&player, characterName)) { - SPDLOG_ERROR("Player not found or deleted for account."); - return false; - } + account::Player player; + int result; - account.GetID(accountId); + if (auto [player, result] = account.GetAccountPlayer(characterName); + account::ERROR_NO != result) { + SPDLOG_ERROR("Player [{}] not found or deleted for account.", characterName); + return false; + } - return true; + *accountId = account.GetID(); + + return true; } account::AccountType IOLoginData::getAccountType(uint32_t accountId) @@ -299,26 +303,43 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) Database& db = Database::getInstance(); uint32_t accountId = result->getNumber("account_id"); - account::Account account; + account::Account account(accountId); account.SetDatabaseInterface(&db); - account.loadAccountDB(accountId); + if(account::ERROR_NO != account.LoadAccountDB()) + { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return false; + } player->setGUID(result->getNumber("id")); player->name = result->getString("name"); - account.GetID(&(player->accountNumber)); - account.GetAccountType(&(player->accountType)); - account.getCoinBalance(&(player->coinBalance)); - account.getTournamentCoinBalance(&(player->tournamentCoinBalance)); + player->accountNumber = account.GetID(); + player->accountType = account.GetAccountType(); + + int res = 0; + uint32_t coins, tournament_coins; + + if (auto [ coins, res ] = account.GetCoins(); account::ERROR_NO != res) { + SPDLOG_ERROR("Failed to load Player [{}] coins. (Error: [{}])", + player->name, res); + return false; + } + player->coinBalance = coins; + + if (auto [ tournament_coins, res ] = account.GetTournamentCoins(); + account::ERROR_NO != res) { + SPDLOG_ERROR("Failed to load Player [{}] tournament coins. (Error: [{}])", + player->name, res); + return false; + } + player->tournamentCoinBalance = tournament_coins; if (g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = std::numeric_limits::max(); } else { - account.GetPremiumRemaningDays(&(player->premiumDays)); + player->premiumDays = account.GetPremiumRemaningDays(); } - player->coinBalance = account.getCoins(); - player->tournamentCoinBalance = account.getTournamentCoins(); - player->preyBonusRerolls = result->getNumber("bonus_rerolls"); Group* group = g_game.groups.getGroup(result->getNumber("group_id")); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 035daa6cc5b..e91715fa263 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2318,10 +2318,21 @@ int PlayerFunctions::luaPlayerAddCoins(lua_State* L) { int32_t addCoins = std::min(std::numeric_limits::max() - player->getCoinBalance(), coins); if (addCoins > 0) { account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + + if(account::ERROR_NO != account.AddCoins(addCoins)) { + SPDLOG_ERROR("Failed to add coins to Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + player->setCoins(player->getCoinBalance() + addCoins); - account.addCoins(addCoins); lua_pushnumber(L, addCoins); + } } return 1; @@ -2345,9 +2356,21 @@ int PlayerFunctions::luaPlayerRemoveCoins(lua_State* L) { if (player->getCoinBalance() != std::numeric_limits::max()) { if (removeCoins > 0) { account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + + if(account::ERROR_NO != account.RemoveCoins(removeCoins)) { + SPDLOG_ERROR("Failed to remove coins from Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + player->setCoins(player->getCoinBalance() - removeCoins); - account.removeCoins(removeCoins); + lua_pushnumber(L, removeCoins); + } } return 1; @@ -2382,9 +2405,20 @@ int PlayerFunctions::luaPlayerAddTournamentCoins(lua_State* L) { int32_t addTournamentCoins = std::min(std::numeric_limits::max() - player->getTournamentCoinBalance(), tournamentCoins); if (addTournamentCoins > 0) { account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + + if(account::ERROR_NO != account.AddTournamentCoins(addTournamentCoins)) { + SPDLOG_ERROR("Failed to add tournament coins to Account: [{}]", + account.GetID()); + pushBoolean(L, false); + return 1; + } + player->setTournamentCoins(player->getTournamentCoinBalance() + addTournamentCoins); - account.addTournamentCoins(addTournamentCoins); lua_pushnumber(L, addTournamentCoins); } } @@ -2409,9 +2443,22 @@ int PlayerFunctions::luaPlayerRemoveTournamentCoins(lua_State* L) { if (player->getTournamentCoinBalance() != std::numeric_limits::max()) { if (removeTournamentCoins > 0) { account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + pushBoolean(L, false); + return 1; + } + + account.RemoveTournamentCoins(removeTournamentCoins); + if(account::ERROR_NO != account.RemoveTournamentCoins(removeTournamentCoins)) { + SPDLOG_ERROR("Failed to add tournament coins to Account: [{}]", + account.GetID()); + pushBoolean(L, false); + return 1; + } + player->setTournamentCoins(player->getTournamentCoinBalance() - removeTournamentCoins); - account.removeTournamentCoins(removeTournamentCoins); + lua_pushnumber(L, removeTournamentCoins); } } return 1; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 9e1e2a5daea..672cd5faf9c 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -4046,12 +4046,31 @@ void ProtocolGame::updateCoinBalance() createTask(std::bind([](uint32_t playerId) { Player* player = g_game.getPlayerByID(playerId); if (player != nullptr) { + account::Account account(player->getAccount()); - account.loadAccountDB(); + if(account::ERROR_NO != account.LoadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + return; + } + // Update coin balance - player->coinBalance = account.getCoins(); + int coins = 0; + if(auto [ coins, result ] = account.GetCoins() ; account::ERROR_NO == result) { + player->coinBalance = coins; + } else { + SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.GetID()); + } + // Update tournament coin balance - player->tournamentCoinBalance = account.getTournamentCoins(); + int tournament_coins = 0; + if(auto [ tournament_coins, result ] = account.GetCoins() ; + account::ERROR_NO == result) { + player->tournamentCoinBalance = tournament_coins; + } else { + SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", + account.GetID()); + } + player->sendCoinBalance(); } }, player->getID())) diff --git a/src/server/network/protocol/protocollogin.cpp b/src/server/network/protocol/protocollogin.cpp index b6d9db3b959..fff778f9838 100644 --- a/src/server/network/protocol/protocollogin.cpp +++ b/src/server/network/protocol/protocollogin.cpp @@ -50,10 +50,11 @@ void ProtocolLogin::disconnectClient(const std::string& message, uint16_t versio void ProtocolLogin::getCharacterList(const std::string& email, const std::string& password, uint16_t version) { - account::Account account; - if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { - disconnectClient("Email or password is not correct", version); - return; + int result = 0; + account::Account account; + if (!IOLoginData::authenticateAccountPassword(email, password, &account)) { + disconnectClient("Email or password is not correct", version); + return; } // Update premium days @@ -76,7 +77,11 @@ void ProtocolLogin::getCharacterList(const std::string& email, const std::string // Add char list std::vector players; - account.GetAccountPlayers(&players); + if (auto [ players, result ] = account.GetAccountPlayers(); + account::ERROR_NO != result) { + SPDLOG_ERROR("Failed to load Account [{}] players. (Error: [{}])", + account.GetID(), result); + } output->addByte(0x64); output->addByte(1); // number of worlds @@ -104,7 +109,7 @@ void ProtocolLogin::getCharacterList(const std::string& email, const std::string output->add(0); } else { uint32_t days; - account.GetPremiumRemaningDays(&days); + days = account.GetPremiumRemaningDays(); output->addByte(0); output->add(time(nullptr) + (days * 86400)); } From 0ac7e445a1fd48881848f98e77def667e0e50694 Mon Sep 17 00:00:00 2001 From: Renato Foot Date: Wed, 25 Aug 2021 00:40:06 -0300 Subject: [PATCH 25/30] Update account documentation Signed-off-by: Renato Foot --- src/creatures/players/account/account.hpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index 7d04abadfc0..b6d43e22d71 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -135,7 +135,7 @@ class Account { /** Coins * @brief Get the amount of coins that the account has from database. * - * @param coins Pointer to return the number of coins + * @return uint32_t Number of coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ std::tuple GetCoins(); @@ -159,6 +159,7 @@ class Account { /** Tournament Coins * @brief Get the amount of tournament coins that the account has from database. * + * @return uint32_t Number of tournament coins * @return error_t ERROR_NO(0) Success, otherwise Fail. */ std::tuple GetTournamentCoins(); @@ -192,7 +193,7 @@ class Account { error_t LoadAccountDB(); /** - * @brief Try to + * @brief Try to load account from DB using Account Name * * @param name * @return error_t ERROR_NO(0) Success, otherwise Fail. @@ -200,15 +201,15 @@ class Account { error_t LoadAccountDB(std::string name); /** - * @brief + * @brief Try to load account from DB using Account ID * - * @param id + * @param id Account ID * @return error_t ERROR_NO(0) Success, otherwise Fail. */ error_t LoadAccountDB(uint32_t id); /** - * @brief + * @brief Save Account to DB * * @return error_t ERROR_NO(0) Success, otherwise Fail. */ @@ -250,7 +251,9 @@ class Account { * * @param type Type of the transaction(Add/Remove). * @param coins Amount of coins + * @param coin_type Type of the coin * @param description Description of the transaction + * * @return error_t ERROR_NO(0) Success, otherwise Fail. */ error_t RegisterCoinsTransaction(CoinTransactionType type, uint32_t coins, From 38ac3f2021d539a5762e4e933ae1d215b9d2846d Mon Sep 17 00:00:00 2001 From: Renato Foot Date: Fri, 27 Aug 2021 16:47:21 -0300 Subject: [PATCH 26/30] Fix windows build Signed-off-by: Renato Foot --- src/creatures/players/store/store.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index 4829541f3b7..9f209fae887 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -27,6 +27,10 @@ extern ConfigManager g_config; extern Game g_game; +#if defined(_MSC_VER) +# define localtime_r(T,Tm) (localtime_s(Tm,T) ? NULL : Tm) +#endif + const std::unordered_map CoinTypeMap = { {"coin", COIN_TYPE_DEFAULT}, {"transferable", COIN_TYPE_TRANSFERABLE}, From b7469767936466aa35e9df1009cc7842727a5654 Mon Sep 17 00:00:00 2001 From: Renato Foot Date: Mon, 30 Aug 2021 17:22:46 -0300 Subject: [PATCH 27/30] Adjust code style Signed-off-by: Renato Foot --- .clang-format | 64 + .editorconfig | 4 +- data/XML/store.xml | 552 +++---- src/creatures/players/account/account.cpp | 694 ++++----- src/creatures/players/account/account.hpp | 467 +++--- src/creatures/players/player.cpp | 12 +- src/creatures/players/store/store.cpp | 1345 +++++++++-------- src/creatures/players/store/store.hpp | 588 +++---- src/game/game.cpp | 108 +- src/io/iologindata.cpp | 24 +- .../creatures/player/player_functions.cpp | 34 +- src/server/network/protocol/protocolgame.cpp | 16 +- src/server/network/protocol/protocollogin.cpp | 6 +- tests/account_test.cpp | 164 +- 14 files changed, 2125 insertions(+), 1953 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000000..c415f789826 --- /dev/null +++ b/.clang-format @@ -0,0 +1,64 @@ +--- +Language: Cpp +BasedOnStyle: WebKit +AccessModifierOffset: -4 +AlignAfterOpenBracket: false +AlignEscapedNewlinesLeft: false +AlignOperands: false +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakTemplateDeclarations: false +AlwaysBreakBeforeMultilineStrings: false +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: true +BinPackParameters: true +BinPackArguments: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +DerivePointerAlignment: false +ExperimentalAutoDetectBinPacking: false +IndentCaseLabels: false +IndentWrappedFunctionNames: false +IndentFunctionDeclarationAfterType: false +MaxEmptyLinesToKeep: 2 +KeepEmptyLinesAtTheStartOfBlocks: true +NamespaceIndentation: Inner +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakBeforeFirstCallParameter: 79 +PenaltyBreakComment: 300 +PenaltyBreakString: 1000 +PenaltyBreakFirstLessLess: 120 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PointerAlignment: Left +SpacesBeforeTrailingComments: 1 +Cpp11BracedListStyle: false +Standard: Auto +IndentWidth: 4 +TabWidth: 8 +UseTab: Never +BreakBeforeBraces: Linux +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: false +SpaceInEmptyParentheses: false +SpacesInCStyleCastParentheses: false +SpaceAfterCStyleCast: true +SpacesInContainerLiterals: true +SpaceBeforeAssignmentOperators: true +ContinuationIndentWidth: 4 +CommentPragmas: '^ IWYU pragma:' +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +SpaceBeforeParens: ControlStatements +DisableFormat: false +... diff --git a/.editorconfig b/.editorconfig index 4a12040e217..2f6cee1da7b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,8 +10,8 @@ trim_trailing_whitespace = true # C/C++ [*.{cpp,hpp,h}] -indent_style = tab -indent_size = 2 +indent_style = space +indent_size = 4 # CMake [CMakeLists.txt] diff --git a/data/XML/store.xml b/data/XML/store.xml index 8e8b0cec6e4..141d2db3a4d 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -1,34 +1,34 @@ - + - + - + - + - + - + @@ -46,13 +46,13 @@ - - - - - + + + + + - + @@ -76,99 +76,99 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - + + + - - - + + + - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + @@ -243,7 +243,7 @@ - + @@ -279,174 +279,174 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + - + @@ -457,47 +457,47 @@ - + - + - + - + - - - - - + + + + + - + - - - - + + + + - + - + - + diff --git a/src/creatures/players/account/account.cpp b/src/creatures/players/account/account.cpp index fa7eb68909e..d8b56ae62be 100644 --- a/src/creatures/players/account/account.cpp +++ b/src/creatures/players/account/account.cpp @@ -25,44 +25,49 @@ #include #include -namespace account { - -Account::Account() { - id_ = 0; - email_.clear(); - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance_ = 0; - tournament_coin_balance_ = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; -} - -Account::Account(uint32_t id) { - id_ = id; - email_.clear(); - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance_ = 0; - tournament_coin_balance_ = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; -} - -Account::Account(const std::string &email) : email_(email) { - id_ = 0; - password_.clear(); - premium_remaining_days_ = 0; - premium_last_day_ = 0; - coin_balance_ = 0; - tournament_coin_balance_ = 0; - account_type_ = ACCOUNT_TYPE_NORMAL; - db_ = &Database::getInstance(); - db_tasks_ = &g_databaseTasks; +namespace account +{ + +Account::Account() +{ + m_id = 0; + m_email.clear(); + m_password.clear(); + m_premiumRemainingDays = 0; + m_premiumLastDay = 0; + m_coinBalance = 0; + m_tournamentCoinBalance = 0; + m_accountType = ACCOUNT_TYPE_NORMAL; + m_db = &Database::getInstance(); + m_dbTasks = &g_databaseTasks; +} + +Account::Account(uint32_t id) +{ + m_id = id; + m_email.clear(); + m_password.clear(); + m_premiumRemainingDays = 0; + m_premiumLastDay = 0; + m_coinBalance = 0; + m_tournamentCoinBalance = 0; + m_accountType = ACCOUNT_TYPE_NORMAL; + m_db = &Database::getInstance(); + m_dbTasks = &g_databaseTasks; +} + +Account::Account(const std::string& email) + : m_email(email) +{ + m_id = 0; + m_password.clear(); + m_premiumRemainingDays = 0; + m_premiumLastDay = 0; + m_coinBalance = 0; + m_tournamentCoinBalance = 0; + m_accountType = ACCOUNT_TYPE_NORMAL; + m_db = &Database::getInstance(); + m_dbTasks = &g_databaseTasks; } @@ -70,22 +75,24 @@ Account::Account(const std::string &email) : email_(email) { * Interfaces ******************************************************************************/ -error_t Account::SetDatabaseInterface(Database *database) { - if (database == nullptr) { - return ERROR_NULLPTR; - } +error_t Account::setDatabaseInterface(Database* database) +{ + if (database == nullptr) { + return ERROR_NULLPTR; + } - db_ = database; - return ERROR_NO; + m_db = database; + return ERROR_NO; } -error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { - if (database_tasks == nullptr) { - return ERROR_NULLPTR; - } +error_t Account::setDatabaseTasksInterface(DatabaseTasks* dbTasks) +{ + if (dbTasks == nullptr) { + return ERROR_NULLPTR; + } - db_tasks_ = database_tasks; - return ERROR_NO; + m_dbTasks = dbTasks; + return ERROR_NO; } @@ -93,410 +100,411 @@ error_t Account::SetDatabaseTasksInterface(DatabaseTasks *database_tasks) { * Coins Methods ******************************************************************************/ -std::tuple Account::GetCoins() { +std::tuple Account::getCoins() +{ - if (db_ == nullptr || id_ == 0) { - return std::make_tuple(0, ERROR_NOT_INITIALIZED); - } + if (m_db == nullptr || m_id == 0) { + return std::make_tuple(0, ERROR_NOT_INITIALIZED); + } - std::ostringstream query; - query << "SELECT `coins` FROM `accounts` WHERE `id` = " << id_; + std::ostringstream query; + query << "SELECT `coins` FROM `accounts` WHERE `id` = " << m_id; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return std::make_tuple(0, ERROR_DB); - } + DBResult_ptr result = m_db->storeQuery(query.str()); + if (!result) { + return std::make_tuple(0, ERROR_DB); + } - return std::make_tuple(result->getNumber("coins"), ERROR_NO); + return std::make_tuple(result->getNumber("coins"), ERROR_NO); } -error_t Account::AddCoins(const uint32_t &amount) { +error_t Account::addCoins(const uint32_t& amount) +{ - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } + if (m_dbTasks == nullptr) { + return ERROR_NULLPTR; + } - if (amount == 0) { - return ERROR_NO; - } + if (amount == 0) { + return ERROR_NO; + } - int result = 0; - uint32_t current_coins = 0; + int result = 0; + uint32_t current_coins = 0; - if (auto [ current_coins, result ] = this->GetCoins(); ERROR_NO == result) { - if ((current_coins + amount) < current_coins) { - return ERROR_VALUE_OVERFLOW; - } - } else { - return ERROR_GET_COINS; - } + if (auto [current_coins, result] = this->getCoins(); ERROR_NO == result) { + if ((current_coins + amount) < current_coins) { + return ERROR_VALUE_OVERFLOW; + } + } else { + return ERROR_GET_COINS; + } - std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = " << (current_coins + amount) - << " WHERE `id` = " << id_; + std::ostringstream query; + query << "UPDATE `accounts` SET `coins` = " << (current_coins + amount) + << " WHERE `id` = " << m_id; - db_tasks_->addTask(query.str()); + m_dbTasks->addTask(query.str()); - this->RegisterCoinsTransaction(COIN_ADD, amount, COIN, ""); + this->registerCoinsTransaction(COIN_ADD, amount, COIN, ""); - return ERROR_NO; + return ERROR_NO; } -error_t Account::RemoveCoins(const uint32_t &amount) { +error_t Account::removeCoins(const uint32_t& amount) +{ - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } + if (m_dbTasks == nullptr) { + return ERROR_NULLPTR; + } - if (amount == 0) { - return ERROR_NO; - } + if (amount == 0) { + return ERROR_NO; + } - int result = 0; - uint32_t current_coins = 0; + int result = 0; + uint32_t current_coins = 0; - if (auto [ current_coins, result ] = this->GetCoins(); ERROR_NO == result) { - if ((current_coins - amount) > current_coins) { - return ERROR_VALUE_NOT_ENOUGH_COINS; - } - } else { - return ERROR_GET_COINS; - } + if (auto [current_coins, result] = this->getCoins(); ERROR_NO == result) { + if ((current_coins - amount) > current_coins) { + return ERROR_VALUE_NOT_ENOUGH_COINS; + } + } else { + return ERROR_GET_COINS; + } - std::ostringstream query; - query << "UPDATE `accounts` SET `coins` = "<< (current_coins - amount) - << " WHERE `id` = " << id_; + std::ostringstream query; + query << "UPDATE `accounts` SET `coins` = " << (current_coins - amount) + << " WHERE `id` = " << m_id; - db_tasks_->addTask(query.str()); + m_dbTasks->addTask(query.str()); - this->RegisterCoinsTransaction(COIN_REMOVE, amount, COIN, ""); + this->registerCoinsTransaction(COIN_REMOVE, amount, COIN, ""); - return ERROR_NO; + return ERROR_NO; } -std::tuple Account::GetTournamentCoins() { +std::tuple Account::getTournamentCoins() +{ - if (db_ == nullptr || id_ == 0) { - return std::make_tuple(0, ERROR_NOT_INITIALIZED); - } + if (m_db == nullptr || m_id == 0) { + return std::make_tuple(0, ERROR_NOT_INITIALIZED); + } - std::ostringstream query; - query << "SELECT `tournament_coins` FROM `accounts` WHERE `id` = " << id_; + std::ostringstream query; + query << "SELECT `tournament_coins` FROM `accounts` WHERE `id` = " << m_id; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return std::make_tuple(0, ERROR_DB); - } + DBResult_ptr result = m_db->storeQuery(query.str()); + if (!result) { + return std::make_tuple(0, ERROR_DB); + } - return std::make_tuple(result->getNumber("tournament_coins"), ERROR_NO); + return std::make_tuple( + result->getNumber("tournament_coins"), ERROR_NO); } -error_t Account::AddTournamentCoins(const uint32_t &amount) { +error_t Account::addTournamentCoins(const uint32_t& amount) +{ - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } - if (amount == 0) { - return ERROR_NO; - } + if (m_dbTasks == nullptr) { + return ERROR_NULLPTR; + } + if (amount == 0) { + return ERROR_NO; + } - int result = 0; - uint32_t current_tournament_coins = 0; + int result = 0; + uint32_t current_tournament_coins = 0; - if (auto [ current_tournament_coins, result ] = this->GetTournamentCoins(); - ERROR_NO == result) { - if ((current_tournament_coins + amount) < current_tournament_coins) { - return ERROR_VALUE_OVERFLOW; - } - } else { - return ERROR_GET_COINS; - } + if (auto [current_tournament_coins, result] = this->getTournamentCoins(); + ERROR_NO == result) { + if ((current_tournament_coins + amount) < current_tournament_coins) { + return ERROR_VALUE_OVERFLOW; + } + } else { + return ERROR_GET_COINS; + } - std::ostringstream query; - query << "UPDATE `accounts` SET `tournament_coins` = " - << (current_tournament_coins + amount) << " WHERE `id` = " << id_; + std::ostringstream query; + query << "UPDATE `accounts` SET `tournament_coins` = " + << (current_tournament_coins + amount) << " WHERE `id` = " << m_id; - db_tasks_->addTask(query.str()); + m_dbTasks->addTask(query.str()); - this->RegisterCoinsTransaction(COIN_ADD, amount, TOURNAMENT, ""); + this->registerCoinsTransaction(COIN_ADD, amount, TOURNAMENT, ""); - return ERROR_NO; + return ERROR_NO; } -error_t Account::RemoveTournamentCoins(const uint32_t &amount) { +error_t Account::removeTournamentCoins(const uint32_t& amount) +{ - if (db_tasks_ == nullptr) { - return ERROR_NULLPTR; - } + if (m_dbTasks == nullptr) { + return ERROR_NULLPTR; + } - if (amount == 0) { - return ERROR_NO; - } + if (amount == 0) { + return ERROR_NO; + } - int result = 0; - uint32_t current_tournament_coins = 0; + int result = 0; + uint32_t current_tournament_coins = 0; - if (auto [ current_tournament_coins, result ] = this->GetTournamentCoins(); - ERROR_NO == result) { - if ((current_tournament_coins - amount) > current_tournament_coins) { - return ERROR_VALUE_NOT_ENOUGH_COINS; - } - } else { - return ERROR_GET_COINS; - } + if (auto [current_tournament_coins, result] = this->getTournamentCoins(); + ERROR_NO == result) { + if ((current_tournament_coins - amount) > current_tournament_coins) { + return ERROR_VALUE_NOT_ENOUGH_COINS; + } + } else { + return ERROR_GET_COINS; + } - std::ostringstream query; - query << "UPDATE `accounts` SET `tournament_coins` = " - << (current_tournament_coins - amount) << " WHERE `id` = " << id_; + std::ostringstream query; + query << "UPDATE `accounts` SET `tournament_coins` = " + << (current_tournament_coins - amount) << " WHERE `id` = " << m_id; - db_tasks_->addTask(query.str()); + m_dbTasks->addTask(query.str()); - this->RegisterCoinsTransaction(COIN_REMOVE, amount, TOURNAMENT, ""); + this->registerCoinsTransaction(COIN_REMOVE, amount, TOURNAMENT, ""); - return ERROR_NO; + return ERROR_NO; } -error_t Account::RegisterCoinsTransaction(CoinTransactionType type, - uint32_t coins, CoinType coin_type, const std::string& description) { +error_t Account::registerCoinsTransaction(CoinTransactionType type, + uint32_t coins, CoinType coinType, const std::string& description) +{ - if (db_ == nullptr) { - return ERROR_NULLPTR; - } + if (m_db == nullptr) { + return ERROR_NULLPTR; + } - std::ostringstream query; - query << "INSERT INTO `coins_transactions` (`account_id`, `type`, `amount`," - " `coin_type`, `description`) VALUES (" << id_ << ", " - << static_cast(type) << ", "<< coins << ", " - << static_cast(coin_type) << ", " - << db_->escapeString(description) << ")"; + std::ostringstream query; + query << "INSERT INTO `coins_transactions` (`account_id`, `type`, `amount`," + " `coin_type`, `description`) VALUES (" + << m_id << ", " << static_cast(type) << ", " << coins << ", " + << static_cast(coinType) << ", " + << m_db->escapeString(description) << ")"; - if (!db_->executeQuery(query.str())) { - return ERROR_DB; - } + if (!m_db->executeQuery(query.str())) { + return ERROR_DB; + } - return ERROR_NO; + return ERROR_NO; } /******************************************************************************* * Database ******************************************************************************/ -error_t Account::LoadAccountDB() { - if (id_ != 0) { - return this->LoadAccountDB(id_); - } else if (!email_.empty()) { - return this->LoadAccountDB(email_); - } +error_t Account::loadAccountDB() +{ + if (m_id != 0) { + return this->loadAccountDB(m_id); + } else if (!m_email.empty()) { + return this->loadAccountDB(m_email); + } - return ERROR_NOT_INITIALIZED; + return ERROR_NOT_INITIALIZED; } -error_t Account::LoadAccountDB(const std::string email) { - std::ostringstream query; - query << "SELECT * FROM `accounts` WHERE `email` = " - << db_->escapeString(email); - return this->LoadAccountDB(query); +error_t Account::loadAccountDB(const std::string email) +{ + std::ostringstream query; + query << "SELECT * FROM `accounts` WHERE `email` = " + << m_db->escapeString(email); + return this->loadAccountDB(query); } -error_t Account::LoadAccountDB(uint32_t id) { - std::ostringstream query; - query << "SELECT * FROM `accounts` WHERE `id` = " << id; - return this->LoadAccountDB(query); +error_t Account::loadAccountDB(uint32_t id) +{ + std::ostringstream query; + query << "SELECT * FROM `accounts` WHERE `id` = " << id; + return this->loadAccountDB(query); } -error_t Account::LoadAccountDB(const std::ostringstream &query) { - if (db_ == nullptr) { - return ERROR_NULLPTR; - } +error_t Account::loadAccountDB(const std::ostringstream& query) +{ + if (m_db == nullptr) { + return ERROR_NULLPTR; + } - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return false; - } + DBResult_ptr result = m_db->storeQuery(query.str()); + if (!result) { + return false; + } - this->SetID(result->getNumber("id")); - this->SetEmail(result->getString("email")); - this->SetAccountType(static_cast(result->getNumber("type"))); - this->SetPassword(result->getString("password")); - this->SetPremiumRemaningDays(result->getNumber("premdays")); - this->SetPremiumLastDay(result->getNumber("lastday")); + this->setID(result->getNumber("id")); + this->setEmail(result->getString("email")); + this->setAccountType( + static_cast(result->getNumber("type"))); + this->setPassword(result->getString("password")); + this->setPremiumRemaningDays(result->getNumber("premdays")); + this->setPremiumLastDay(result->getNumber("lastday")); - return ERROR_NO; + return ERROR_NO; } -std::tuple Account::LoadAccountPlayerDB(const std::string& characterName) { +std::tuple Account::loadAccountPlayerDB( + const std::string& characterName) +{ - Player player; + Player player; - if (id_ == 0) { - std::make_tuple(player, ERROR_NOT_INITIALIZED); - } + if (m_id == 0) { + std::make_tuple(player, ERROR_NOT_INITIALIZED); + } - std::ostringstream query; - query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " - << id_ << " AND `name` = " << db_->escapeString(characterName) - << " ORDER BY `name` ASC"; + std::ostringstream query; + query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " + << m_id << " AND `name` = " << m_db->escapeString(characterName) + << " ORDER BY `name` ASC"; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result || result->getNumber("deletion") != 0) { - return std::make_tuple(player, ERROR_PLAYER_NOT_FOUND); - } + DBResult_ptr result = m_db->storeQuery(query.str()); + if (!result || result->getNumber("deletion") != 0) { + return std::make_tuple(player, ERROR_PLAYER_NOT_FOUND); + } - player.name = result->getString("name"); - player.deletion = result->getNumber("deletion"); + player.name = result->getString("name"); + player.deletion = result->getNumber("deletion"); - return std::make_tuple(player, ERROR_NO); + return std::make_tuple(player, ERROR_NO); } -std::tuple, error_t> Account::LoadAccountPlayersDB() { +std::tuple, error_t> Account::loadAccountPlayersDB() +{ - std::vector players; + std::vector players; - if (id_ == 0) { - return std::make_tuple(players, ERROR_NOT_INITIALIZED); - } + if (m_id == 0) { + return std::make_tuple(players, ERROR_NOT_INITIALIZED); + } - std::ostringstream query; - query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " - << id_ << " ORDER BY `name` ASC"; + std::ostringstream query; + query << "SELECT `name`, `deletion` FROM `players` WHERE `account_id` = " + << m_id << " ORDER BY `name` ASC"; - DBResult_ptr result = db_->storeQuery(query.str()); - if (!result) { - return std::make_tuple(players, ERROR_DB); - } + DBResult_ptr result = m_db->storeQuery(query.str()); + if (!result) { + return std::make_tuple(players, ERROR_DB); + } - do { - if (result->getNumber("deletion") == 0) { - Player new_player; - new_player.name = result->getString("name"); - new_player.deletion = result->getNumber("deletion"); - players.push_back(new_player); - } - } while (result->next()); - return std::make_tuple(players, ERROR_NO); + do { + if (result->getNumber("deletion") == 0) { + Player new_player; + new_player.name = result->getString("name"); + new_player.deletion = result->getNumber("deletion"); + players.push_back(new_player); + } + } while (result->next()); + return std::make_tuple(players, ERROR_NO); } -error_t Account::SaveAccountDB() { - std::ostringstream query; - - query << "UPDATE `accounts` SET " - << "`email` = " << db_->escapeString(email_) << " , " - << "`type` = " << account_type_ << " , " - << "`password` = " << db_->escapeString(password_) << " , " - << "`coins` = " << coin_balance_ << " , " - << "`tournament_coins` = " << tournament_coin_balance_ << " , " - << "`premdays` = " << premium_remaining_days_ << " , " - << "`lastday` = " << premium_last_day_; - - if (id_ != 0) { - query << " WHERE `id` = " << id_; - } else if (!email_.empty()) { - query << " WHERE `email` = " << email_; - } - - if (!db_->executeQuery(query.str())) { - return ERROR_DB; - } - - return ERROR_NO; -} - -/******************************************************************************* - * Setters and Getters - ******************************************************************************/ - -error_t Account::SetID(uint32_t id) { - if (id == 0) { - return ERROR_INVALID_ID; - } - id_ = id; - return ERROR_NO; -} +error_t Account::saveAccountDB() +{ + std::ostringstream query; -uint32_t Account::GetID() { - return id_; -} + query << "UPDATE `accounts` SET " + << "`email` = " << m_db->escapeString(m_email) << " , " + << "`type` = " << m_accountType << " , " + << "`password` = " << m_db->escapeString(m_password) << " , " + << "`coins` = " << m_coinBalance << " , " + << "`tournament_coins` = " << m_tournamentCoinBalance << " , " + << "`premdays` = " << m_premiumRemainingDays << " , " + << "`lastday` = " << m_premiumLastDay; -error_t Account::SetEmail(std::string email) { - if (email.empty()) { - return ERROR_INVALID_ACCOUNT_EMAIL; - } - email_ = email; - return ERROR_NO; -} + if (m_id != 0) { + query << " WHERE `id` = " << m_id; + } else if (!m_email.empty()) { + query << " WHERE `email` = " << m_email; + } -std::string Account::GetEmail() { - return email_; -} + if (!m_db->executeQuery(query.str())) { + return ERROR_DB; + } -error_t Account::SetPassword(std::string password) { - if (password.empty()) { - return ERROR_INVALID_ACC_PASSWORD; - } - password_ = password; - return ERROR_NO; + return ERROR_NO; } -std::string Account::GetPassword() { - return password_; -} +/******************************************************************************* + * Setters and Getters + ******************************************************************************/ -error_t Account::SetPremiumRemaningDays(uint32_t days) { - premium_remaining_days_ = days; - return ERROR_NO; +error_t Account::setID(const uint32_t &id) +{ + if (id == 0) { + return ERROR_INVALID_ID; + } + m_id = id; + return ERROR_NO; } -uint32_t Account::GetPremiumRemaningDays() { - return premium_remaining_days_; +error_t Account::setEmail(const std::string &email) +{ + if (email.empty()) { + return ERROR_INVALID_ACCOUNT_EMAIL; + } + m_email = email; + return ERROR_NO; } -error_t Account::SetPremiumLastDay(time_t last_day) { - if (last_day < 0) { - return ERROR_INVALID_LAST_DAY; - } - premium_last_day_ = last_day; - return ERROR_NO; +error_t Account::setPassword(const std::string &password) +{ + if (password.empty()) { + return ERROR_INVALID_ACC_PASSWORD; + } + m_password = password; + return ERROR_NO; } -time_t Account::GetPremiumLastDay() { - return premium_last_day_; +error_t Account::setPremiumRemaningDays(const uint32_t &days) +{ + m_premiumRemainingDays = days; + return ERROR_NO; } -error_t Account::SetAccountType(AccountType account_type) { - if (account_type > 5) { - return ERROR_INVALID_ACC_TYPE; - } - account_type_ = account_type; - return ERROR_NO; +error_t Account::setPremiumLastDay(const time_t &lastDay) +{ + if (lastDay < 0) { + return ERROR_INVALID_LAST_DAY; + } + m_premiumLastDay = lastDay; + return ERROR_NO; } -AccountType Account::GetAccountType() { - return account_type_; +error_t Account::setAccountType(const AccountType &account_type) +{ + if (account_type > 5) { + return ERROR_INVALID_ACC_TYPE; + } + m_accountType = account_type; + return ERROR_NO; } -std::tuple Account::GetAccountPlayer( - const std::string& characterName) { +std::tuple Account::getAccountPlayer( + const std::string& characterName) +{ - Player player; - int result; - if (auto [ player, result ] = this->LoadAccountPlayerDB(characterName); - ERROR_NO == result) { - return std::make_tuple(player, ERROR_NO); - } + Player player; + int result; + if (auto [player, result] = this->loadAccountPlayerDB(characterName); + ERROR_NO == result) { + return std::make_tuple(player, ERROR_NO); + } - return std::make_tuple(player, result); + return std::make_tuple(player, result); } -std::tuple, error_t> Account::GetAccountPlayers() { - std::vector players; - int result; - if (auto [ players, result ] = this->LoadAccountPlayersDB(); - ERROR_NO == result) { - return std::make_tuple(players, ERROR_NO); - } else { - return std::make_tuple(players, result); - } +std::tuple, error_t> Account::getAccountPlayers() +{ + std::vector players; + int result; + if (auto [players, result] = this->loadAccountPlayersDB(); + ERROR_NO == result) { + return std::make_tuple(players, ERROR_NO); + } else { + return std::make_tuple(players, result); + } } -} // namespace account +} // namespace account diff --git a/src/creatures/players/account/account.hpp b/src/creatures/players/account/account.hpp index b6d43e22d71..f0b20623cd5 100644 --- a/src/creatures/players/account/account.hpp +++ b/src/creatures/players/account/account.hpp @@ -20,258 +20,275 @@ #ifndef SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ #define SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ +#include #include #include -#include #include "database/database.h" #include "database/databasetasks.h" #include "utils/definitions.h" -namespace account { +namespace account +{ enum Errors : uint8_t { - ERROR_NO = 0, - ERROR_DB, - ERROR_GET_COINS, - ERROR_INVALID_ACCOUNT_EMAIL, - ERROR_INVALID_ACC_PASSWORD, - ERROR_INVALID_ACC_TYPE, - ERROR_INVALID_ID, - ERROR_INVALID_LAST_DAY, - ERROR_LOADING_ACCOUNT_PLAYERS, - ERROR_NOT_INITIALIZED, - ERROR_NULLPTR, - ERROR_VALUE_NOT_ENOUGH_COINS, - ERROR_VALUE_OVERFLOW, - ERROR_PLAYER_NOT_FOUND + ERROR_NO = 0, + ERROR_DB, + ERROR_GET_COINS, + ERROR_INVALID_ACCOUNT_EMAIL, + ERROR_INVALID_ACC_PASSWORD, + ERROR_INVALID_ACC_TYPE, + ERROR_INVALID_ID, + ERROR_INVALID_LAST_DAY, + ERROR_LOADING_ACCOUNT_PLAYERS, + ERROR_NOT_INITIALIZED, + ERROR_NULLPTR, + ERROR_VALUE_NOT_ENOUGH_COINS, + ERROR_VALUE_OVERFLOW, + ERROR_PLAYER_NOT_FOUND }; enum AccountType : uint8_t { - ACCOUNT_TYPE_NORMAL = 1, - ACCOUNT_TYPE_TUTOR = 2, - ACCOUNT_TYPE_SENIORTUTOR = 3, - ACCOUNT_TYPE_GAMEMASTER = 4, - ACCOUNT_TYPE_GOD = 5 + ACCOUNT_TYPE_NORMAL = 1, + ACCOUNT_TYPE_TUTOR = 2, + ACCOUNT_TYPE_SENIORTUTOR = 3, + ACCOUNT_TYPE_GAMEMASTER = 4, + ACCOUNT_TYPE_GOD = 5 }; enum GroupType : uint8_t { - GROUP_TYPE_NORMAL = 1, - GROUP_TYPE_TUTOR = 2, - GROUP_TYPE_SENIORTUTOR = 3, - GROUP_TYPE_GAMEMASTER = 4, - GROUP_TYPE_COMMUNITYMANAGER = 5, - GROUP_TYPE_GOD = 6 + GROUP_TYPE_NORMAL = 1, + GROUP_TYPE_TUTOR = 2, + GROUP_TYPE_SENIORTUTOR = 3, + GROUP_TYPE_GAMEMASTER = 4, + GROUP_TYPE_COMMUNITYMANAGER = 5, + GROUP_TYPE_GOD = 6 }; -enum CoinTransactionType : uint8_t { - COIN_ADD = 1, - COIN_REMOVE = 2 -}; +enum CoinTransactionType : uint8_t { COIN_ADD = 1, COIN_REMOVE = 2 }; -enum CoinType : uint8_t { - COIN = 1, - TOURNAMENT = 2 -}; +enum CoinType : uint8_t { COIN = 1, TOURNAMENT = 2 }; typedef struct { - std::string name; - uint64_t deletion; + std::string name; + uint64_t deletion; } Player; /** * @brief Account class to handle account information * */ -class Account { - public: - /** - * @brief Construct a new Account object - * - */ - Account(); - - /** - * @brief Construct a new Account object - * - * @param id Set Account ID to be used by LoadAccountDB - */ - explicit Account(uint32_t id); - - /** - * @brief Construct a new Account object - * - * @param name Set Account Name to be used by LoadAccountDB - */ - explicit Account(const std::string &name); - - /*************************************************************************** - * Interfaces - **************************************************************************/ - - /** - * @brief Set the Database Interface used to get and set information from - * the database - * - * @param database Database Interface pointer to be used - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SetDatabaseInterface(Database *database); - - /** - * @brief Set the Database Tasks Interface used to schedule db update - * - * @param database Database Interface pointer to be used - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SetDatabaseTasksInterface(DatabaseTasks *db_tasks); - - - /*************************************************************************** - * Coins Methods - **************************************************************************/ - - /** Coins - * @brief Get the amount of coins that the account has from database. - * - * @return uint32_t Number of coins - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - std::tuple GetCoins(); - - /** - * @brief Add coins to the account and update database. - * - * @param amount Amount of coins to be added - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t AddCoins(const uint32_t &amount); - - /** - * @brief Removes coins from the account and update database. - * - * @param amount Amount of coins to be removed - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RemoveCoins(const uint32_t &amount); - - /** Tournament Coins - * @brief Get the amount of tournament coins that the account has from database. - * - * @return uint32_t Number of tournament coins - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - std::tuple GetTournamentCoins(); - - /** - * @brief Add coins to the account and update database. - * - * @param amount Amount of coins to be added - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t AddTournamentCoins(const uint32_t &amount); - - /** - * @brief Removes tournament coins from the account and update database. - * - * @param amount Amount of coins to be removed - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RemoveTournamentCoins(const uint32_t &amount); - - /*************************************************************************** - * Database - **************************************************************************/ - - /** - * @brief Try to load account from DB using Account ID or Name if they were - * initialized. - * - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(); - - /** - * @brief Try to load account from DB using Account Name - * - * @param name - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(std::string name); - - /** - * @brief Try to load account from DB using Account ID - * - * @param id Account ID - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t LoadAccountDB(uint32_t id); - - /** - * @brief Save Account to DB - * - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t SaveAccountDB(); - - - /*************************************************************************** - * Setters and Getters - **************************************************************************/ - - uint32_t GetID(); - - error_t SetEmail(std::string name); - std::string GetEmail(); - - error_t SetPassword(std::string password); - std::string GetPassword(); - - error_t SetPremiumRemaningDays(uint32_t days); - uint32_t GetPremiumRemaningDays(); - - error_t SetPremiumLastDay(time_t last_day); - time_t GetPremiumLastDay(); - - error_t SetAccountType(AccountType account_type); - AccountType GetAccountType(); - - std::tuple GetAccountPlayer(const std::string& characterName); - std::tuple, error_t> GetAccountPlayers(); - - private: - error_t SetID(uint32_t id); - error_t LoadAccountDB(const std::ostringstream &query); - std::tuple LoadAccountPlayerDB(const std::string& characterName); - std::tuple, error_t> LoadAccountPlayersDB(); - - /** - * @brief Register account coins transactions in database. - * - * @param type Type of the transaction(Add/Remove). - * @param coins Amount of coins - * @param coin_type Type of the coin - * @param description Description of the transaction - * - * @return error_t ERROR_NO(0) Success, otherwise Fail. - */ - error_t RegisterCoinsTransaction(CoinTransactionType type, uint32_t coins, - CoinType coin_type, const std::string &description); - - Database *db_; - DatabaseTasks *db_tasks_; - - uint32_t id_; - std::string email_; - std::string password_; - uint32_t premium_remaining_days_; - time_t premium_last_day_; - uint32_t coin_balance_; - uint32_t tournament_coin_balance_; - AccountType account_type_; +class Account +{ +public: + /** + * @brief Construct a new Account object + * + */ + Account(); + + /** + * @brief Construct a new Account object + * + * @param id Set Account ID to be used by loadAccountDB + */ + explicit Account(uint32_t id); + + /** + * @brief Construct a new Account object + * + * @param name Set Account Name to be used by loadAccountDB + */ + explicit Account(const std::string& name); + + /*************************************************************************** + * Interfaces + **************************************************************************/ + + /** + * @brief Set the Database Interface used to get and set information from + * the database + * + * @param database Database Interface pointer to be used + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t setDatabaseInterface(Database* database); + + /** + * @brief Set the Database Tasks Interface used to schedule db update + * + * @param database Database Interface pointer to be used + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t setDatabaseTasksInterface(DatabaseTasks* dbTasks); + + + /*************************************************************************** + * Coins Methods + **************************************************************************/ + + /** Coins + * @brief Get the amount of coins that the account has from database. + * + * @return uint32_t Number of coins + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + std::tuple getCoins(); + + /** + * @brief Add coins to the account and update database. + * + * @param amount Amount of coins to be added + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t addCoins(const uint32_t& amount); + + /** + * @brief Removes coins from the account and update database. + * + * @param amount Amount of coins to be removed + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t removeCoins(const uint32_t& amount); + + /** Tournament Coins + * @brief Get the amount of tournament coins that the account has from + * database. + * + * @return uint32_t Number of tournament coins + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + std::tuple getTournamentCoins(); + + /** + * @brief Add coins to the account and update database. + * + * @param amount Amount of coins to be added + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t addTournamentCoins(const uint32_t& amount); + + /** + * @brief Removes tournament coins from the account and update database. + * + * @param amount Amount of coins to be removed + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t removeTournamentCoins(const uint32_t& amount); + + /*************************************************************************** + * Database + **************************************************************************/ + + /** + * @brief Try to load account from DB using Account ID or Name if they were + * initialized. + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t loadAccountDB(); + + /** + * @brief Try to load account from DB using Account Name + * + * @param name + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t loadAccountDB(std::string name); + + /** + * @brief Try to load account from DB using Account ID + * + * @param id Account ID + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t loadAccountDB(uint32_t id); + + /** + * @brief Save Account to DB + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t saveAccountDB(); + + + /*************************************************************************** + * Setters and Getters + **************************************************************************/ + + inline uint32_t getID() + { + return m_id; + }; + + error_t setEmail(const std::string &name); + inline std::string getEmail() + { + return m_email; + } + + error_t setPassword(const std::string &password); + inline std::string getPassword() + { + return m_password; + } + + error_t setPremiumRemaningDays(const uint32_t &days); + inline uint32_t getPremiumRemaningDays() + { + return m_premiumRemainingDays; + } + + error_t setPremiumLastDay(const time_t &lastDay); + inline time_t getPremiumLastDay() + { + return m_premiumLastDay; + } + + error_t setAccountType(const AccountType &accountType); + inline AccountType getAccountType() + { + return m_accountType; + } + + std::tuple getAccountPlayer( + const std::string& characterName); + std::tuple, error_t> getAccountPlayers(); + +private: + error_t setID(const uint32_t &id); + error_t loadAccountDB(const std::ostringstream& query); + std::tuple loadAccountPlayerDB( + const std::string& characterName); + std::tuple, error_t> loadAccountPlayersDB(); + + /** + * @brief Register account coins transactions in database. + * + * @param type Type of the transaction(Add/Remove). + * @param coins Amount of coins + * @param coin_type Type of the coin + * @param description Description of the transaction + * + * @return error_t ERROR_NO(0) Success, otherwise Fail. + */ + error_t registerCoinsTransaction(CoinTransactionType type, uint32_t coins, + CoinType coinType, const std::string& description); + + Database* m_db; + DatabaseTasks* m_dbTasks; + + uint32_t m_id; + std::string m_email; + std::string m_password; + uint32_t m_premiumRemainingDays; + time_t m_premiumLastDay; + uint32_t m_coinBalance; + uint32_t m_tournamentCoinBalance; + AccountType m_accountType; }; -} // namespace account +} // namespace account -#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ +#endif // SRC_CREATURES_PLAYERS_ACCOUNT_ACCOUNT_HPP_ diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index c6f3f3a12d3..181fc02c5b8 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -4638,13 +4638,13 @@ bool Player::canRemoveCoins(int32_t coins) lastUpdateCoin = OTSYS_TIME() + 2000; account::Account account(this->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return false; }; - if(auto [ coinBalance, result ] = account.GetCoins() ; + if(auto [ coinBalance, result ] = account.getCoins() ; account::ERROR_NO != result) { SPDLOG_ERROR("Failed to get Coins for account: [{}]", @@ -4664,14 +4664,14 @@ bool Player::canRemoveTournamentCoins(int32_t tournamentCoins) lastUpdateCoin = OTSYS_TIME() + 2000; account::Account account(this->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { SPDLOG_ERROR("Failed to load Account: [{}]", - account.GetID()); + account.getID()); return false; }; - if(auto [ tournamentCoinBalance, result ] = account.GetTournamentCoins() ; + if(auto [ tournamentCoinBalance, result ] = account.getTournamentCoins() ; account::ERROR_NO != result) { SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", diff --git a/src/creatures/players/store/store.cpp b/src/creatures/players/store/store.cpp index 9f209fae887..64a2398d14a 100644 --- a/src/creatures/players/store/store.cpp +++ b/src/creatures/players/store/store.cpp @@ -15,742 +15,775 @@ * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ + */ #include "otpch.h" #include "config/configmanager.h" +#include "creatures/players/store/store.hpp" #include "game/game.h" #include "utils/pugicast.h" -#include "creatures/players/store/store.hpp" extern ConfigManager g_config; extern Game g_game; #if defined(_MSC_VER) -# define localtime_r(T,Tm) (localtime_s(Tm,T) ? NULL : Tm) +#define localtime_r(T, Tm) (localtime_s(Tm, T) ? NULL : Tm) #endif const std::unordered_map CoinTypeMap = { - {"coin", COIN_TYPE_DEFAULT}, - {"transferable", COIN_TYPE_TRANSFERABLE}, - {"tournament", COIN_TYPE_TOURNAMENT} + { "coin", COIN_TYPE_DEFAULT }, { "transferable", COIN_TYPE_TRANSFERABLE }, + { "tournament", COIN_TYPE_TOURNAMENT } }; const std::unordered_map OfferStatesMap = { - {"none", OFFER_STATE_NONE}, - {"new", OFFER_STATE_NEW}, - {"sale", OFFER_STATE_SALE}, - {"timed", OFFER_STATE_TIMED} + { "none", OFFER_STATE_NONE }, { "new", OFFER_STATE_NEW }, + { "sale", OFFER_STATE_SALE }, { "timed", OFFER_STATE_TIMED } }; const std::unordered_map OfferTypesMap = { - {"none", OFFER_TYPE_NONE}, - {"item", OFFER_TYPE_ITEM}, - {"stackeable", OFFER_TYPE_STACKABLE}, - {"outfit", OFFER_TYPE_OUTFIT}, - {"outfitaddon", OFFER_TYPE_OUTFIT_ADDON}, - {"mount", OFFER_TYPE_MOUNT}, - {"namechange", OFFER_TYPE_NAME_CHANGE}, - {"sexchange", OFFER_TYPE_SEX_CHANGE}, - {"promotion", OFFER_TYPE_PROMOTION}, - {"house", OFFER_TYPE_HOUSE}, - {"expboost", OFFER_TYPE_EXP_BOOST}, - {"preyslot", OFFER_TYPE_PREY_SLOT}, - {"preybonus", OFFER_TYPE_PREY_BONUS}, - {"temple", OFFER_TYPE_TEMPLE}, - {"blessing", OFFER_TYPE_BLESSINGS}, - {"premium", OFFER_TYPE_PREMIUM}, - {"pouch", OFFER_TYPE_POUCH}, - {"allblessing", OFFER_TYPE_ALL_BLESSINGS}, - {"reward", OFFER_TYPE_INSTANT_REWARD_ACCESS}, - {"training", OFFER_TYPE_TRAINING}, - {"charmexpansion", OFFER_TYPE_CHARM_EXPANSION}, - {"charmpoints", OFFER_TYPE_CHARM_POINTS}, - {"multiitems", OFFER_TYPE_MULTI_ITEMS}, - {"fragremove", OFFER_TYPE_FRAG_REMOVE}, - {"skullremove", OFFER_TYPE_SKULL_REMOVE}, - {"recoverykey", OFFER_TYPE_RECOVERY_KEY}, + { "none", OFFER_TYPE_NONE }, + { "item", OFFER_TYPE_ITEM }, + { "stackeable", OFFER_TYPE_STACKABLE }, + { "outfit", OFFER_TYPE_OUTFIT }, + { "outfitaddon", OFFER_TYPE_OUTFIT_ADDON }, + { "mount", OFFER_TYPE_MOUNT }, + { "namechange", OFFER_TYPE_NAME_CHANGE }, + { "sexchange", OFFER_TYPE_SEX_CHANGE }, + { "promotion", OFFER_TYPE_PROMOTION }, + { "house", OFFER_TYPE_HOUSE }, + { "expboost", OFFER_TYPE_EXP_BOOST }, + { "preyslot", OFFER_TYPE_PREY_SLOT }, + { "preybonus", OFFER_TYPE_PREY_BONUS }, + { "temple", OFFER_TYPE_TEMPLE }, + { "blessing", OFFER_TYPE_BLESSINGS }, + { "premium", OFFER_TYPE_PREMIUM }, + { "pouch", OFFER_TYPE_POUCH }, + { "allblessing", OFFER_TYPE_ALL_BLESSINGS }, + { "reward", OFFER_TYPE_INSTANT_REWARD_ACCESS }, + { "training", OFFER_TYPE_TRAINING }, + { "charmexpansion", OFFER_TYPE_CHARM_EXPANSION }, + { "charmpoints", OFFER_TYPE_CHARM_POINTS }, + { "multiitems", OFFER_TYPE_MULTI_ITEMS }, + { "fragremove", OFFER_TYPE_FRAG_REMOVE }, + { "skullremove", OFFER_TYPE_SKULL_REMOVE }, + { "recoverykey", OFFER_TYPE_RECOVERY_KEY }, }; const std::unordered_map OfferBuyTypesMap = { - {"none", OFFER_BUY_TYPE_OTHERS}, - {"offername", OFFER_BUY_TYPE_NAMECHANGE}, - {"teste", OFFER_BUY_TYPE_TESTE} + { "none", OFFER_BUY_TYPE_OTHERS }, + { "offername", OFFER_BUY_TYPE_NAMECHANGE }, + { "teste", OFFER_BUY_TYPE_TESTE } }; const std::unordered_map OfferSkullMap = { - {"none", SKULL_NONE}, - {"red", SKULL_RED}, - {"black", SKULL_BLACK} + { "none", SKULL_NONE }, { "red", SKULL_RED }, { "black", SKULL_BLACK } }; -bool Store::isValidType(OfferTypes_t type) { - auto it = std::find_if(OfferTypesMap.begin(), OfferTypesMap.end(), [type](std::pair const& pair) { - return pair.second == type; - }); +bool Store::isValidType(OfferTypes_t type) +{ + auto it = std::find_if(OfferTypesMap.begin(), OfferTypesMap.end(), + [type](std::pair const& pair) { + return pair.second == type; + }); - return it != OfferTypesMap.end(); + return it != OfferTypesMap.end(); } -/*bool Store::loadStore(const FileName& storeFileName) { - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(storeFileName.GetFullPath().mb_str()); - if(!result) { - printXMLError("[Store::loadStore] - ", storeFileName.GetFullName(), result); - return false; - } - - pugi::xml_node storeNode = doc.child("store"); - if(!storeNode) { - printXMLError("[Store::loadStore] - ", storeFileName.GetFullName(), result); - return false; - } - - loadFromXML(false); - return true; -}*/ - -bool Store::loadFromXML(bool /* reloading */) { - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file("data/XML/store.xml"); - if (!result) { - printXMLError("[Store::loadFromXML] - ", "data/XML/store.xml", result); - return false; - } - - loaded = true; - for (auto baseNode : doc.child("store").children()) { - pugi::xml_attribute storeAttribute; - // Load store category - if (strcasecmp(baseNode.name(), "category") == 0) { - // Check if the tag 'name' exist "" - pugi::xml_attribute categoryName = baseNode.attribute("name"); - if (!categoryName) { - SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for 'category' entry"); - continue; - } - - loadCategory(baseNode, categoryName); - // Load store home - } else if (strcasecmp(baseNode.name(), "home") == 0) { - loadHome(baseNode); - // Load store offers - } else if (strcasecmp(baseNode.name(), "offer") == 0) { - // Check if the tag 'name' exist "" - pugi::xml_attribute offerName = baseNode.attribute("name"); - if (!offerName) { - SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for 'offer' entry"); - continue; - } - - loadOffer(baseNode, storeAttribute, offerName); - } - } - return true; +bool Store::loadFromXML(bool /* reloading */) +{ + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file("data/XML/store.xml"); + if (!result) { + printXMLError("[Store::loadFromXML] - ", "data/XML/store.xml", result); + return false; + } + + loaded = true; + for (auto baseNode : doc.child("store").children()) { + pugi::xml_attribute storeAttribute; + // Load store category + if (strcasecmp(baseNode.name(), "category") == 0) { + // Check if the tag 'name' exist "" + pugi::xml_attribute categoryName = baseNode.attribute("name"); + if (!categoryName) { + SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for " + "'category' entry"); + continue; + } + + loadCategory(baseNode, categoryName); + // Load store home + } else if (strcasecmp(baseNode.name(), "home") == 0) { + loadHome(baseNode); + // Load store offers + } else if (strcasecmp(baseNode.name(), "offer") == 0) { + // Check if the tag 'name' exist "" + pugi::xml_attribute offerName = baseNode.attribute("name"); + if (!offerName) { + SPDLOG_WARN("[Store::loadFromXML] - Missing 'name' tag for " + "'offer' entry"); + continue; + } + + loadOffer(baseNode, storeAttribute, offerName); + } + } + return true; } -bool Store::loadCategory(pugi::xml_node node, pugi::xml_attribute name) { - std::vector offersName; - for (auto childNode : node.children()) { - if (strcasecmp(childNode.name(), "subcategory") == 0) { - offersName.push_back(childNode.attribute("name").as_string()); - } - } - - categories.emplace_back( - name.value(), - offersName, +bool Store::loadCategory(pugi::xml_node node, pugi::xml_attribute name) +{ + std::vector offersName; + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "subcategory") == 0) { + offersName.push_back(childNode.attribute("name").as_string()); + } + } + + categories.emplace_back(name.value(), offersName, node.attribute("icon").as_string(), - node.attribute("rookgaard").as_bool(true) - ); + node.attribute("rookgaard").as_bool(true)); - offercount++; + ++offercount; - categories.shrink_to_fit(); - return true; + categories.shrink_to_fit(); + return true; } -bool Store::loadHome(pugi::xml_node node) { - for (auto childNode : node.children()) { - if (strcasecmp(childNode.name(), "offer") == 0) { - home.offers.push_back(childNode.attribute("name").as_string()); - } else if (strcasecmp(childNode.name(), "banner") == 0) { - home.banners.push_back(childNode.attribute("image").as_string()); - } - } - return true; +bool Store::loadHome(pugi::xml_node node) +{ + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "offer") == 0) { + home.offers.push_back(childNode.attribute("name").as_string()); + } else if (strcasecmp(childNode.name(), "banner") == 0) { + home.banners.push_back(childNode.attribute("image").as_string()); + } + } + return true; } -bool Store::loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, pugi::xml_attribute attributeName) { - // Make store offers - std::string name = attributeName.value(); - - auto result = storeOffers.emplace( - std::piecewise_construct, - std::forward_as_tuple(name), - std::forward_as_tuple(name) - ); - - if (!result.second) { - SPDLOG_WARN("[Store::loadStore] - Duplicate category offer by name: '{}' ignored'", name); - return false; - } - - offercount++; - - // Editing store offers - StoreOffers& offers = result.first->second; - attributeName = node.attribute("description"); - if (attributeName) { - offers.description = attributeName.value(); - } - - attributeName = node.attribute("icon"); - if (attributeName) { - offers.icon = attributeName.value(); - } - - attributeName = node.attribute("rookgaard"); - if (attributeName) { - offers.rookgaard = attributeName.as_bool(); - } - - attributeName = node.attribute("state"); - if (attributeName) { - auto parseState = OfferStatesMap.find(attributeName.value()); - if (parseState != OfferStatesMap.end()) { - offers.state = parseState->second; - } - } - - if (offers.state == OFFER_STATE_SALE) { - saleoffer = true; - } else if (offers.state == OFFER_STATE_NEW) { - newoffer = true; - } - - attributeName = node.attribute("parent"); - if (attributeName) { - offers.parent = attributeName.value(); - } - - // Getting the offers - for (auto childNode : node.children()) { - if (strcasecmp(childNode.name(), "offers") == 0) { - if (!(storeAttribute = childNode.attribute("name"))) { - SPDLOG_WARN("[Store::loadStore] - Missing 'name' attribute in 'offers' entry"); - continue; - } - - name = storeAttribute.value(); - uint32_t id = 0; - pugi::xml_attribute childNodeName = childNode.attribute("id"); - if (childNodeName) { - id = pugi::cast(childNodeName.value()); - } else { - runningid++; - id = runningid; - } - - auto resultOffer = offers.offers.emplace( - std::piecewise_construct, - std::forward_as_tuple(id), - std::forward_as_tuple(id, name) - ); - - if (!resultOffer.second) { - SPDLOG_WARN("[Store::loadStore] - Duplicate offer by name: '{}'", node.value()); - continue; - } - - StoreOffer& offer = resultOffer.first->second; - childNodeName = childNode.attribute("price"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need price", offer.name); - continue; - } - offer.price = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("count"); - if (childNodeName) { - offer.count = pugi::cast(childNodeName.value()); - } - - childNodeName = childNode.attribute("icon"); - if (childNodeName) { - offer.icon = childNodeName.value(); - } - - childNodeName = childNode.attribute("description"); - if (childNodeName) { - offer.description = childNodeName.value(); - } - - childNodeName = childNode.attribute("type"); - if (childNodeName) { - auto parseType = OfferTypesMap.find(childNodeName.value()); - if (parseType != OfferTypesMap.end()) { - offer.type = parseType->second; - } - } - - childNodeName = childNode.attribute("disabled"); - if (childNodeName) { - offer.disabled = childNodeName.as_bool(); - } - - if (offer.type == OFFER_TYPE_OUTFIT || offer.type == OFFER_TYPE_OUTFIT_ADDON) { - childNodeName = childNode.attribute("female"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need female outfit", offer.name); - continue; - } - offer.female = pugi::cast(childNodeName.value()); - childNodeName = childNode.attribute("male"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' need male outfit", offer.name); - continue; - } - offer.male = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("addon"); - if (!childNodeName) { - offer.addon = 0; - } else { - offer.addon = pugi::cast(childNodeName.value()); - } - } else if (offer.type == OFFER_TYPE_BLESSINGS) { - childNodeName = childNode.attribute("blessid"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need bless id", offer.name); - continue; - } - offer.blessid = pugi::cast(childNodeName.value()); - } else if (offer.type == OFFER_TYPE_ITEM || offer.type == OFFER_TYPE_STACKABLE || - offer.type == OFFER_TYPE_HOUSE || offer.type == OFFER_TYPE_TRAINING || - offer.type == OFFER_TYPE_POUCH) { - - childNodeName = childNode.attribute("itemid"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need itemid", offer.name); - continue; - } - offer.itemId = pugi::cast(childNodeName.value()); - - childNodeName = childNode.attribute("charges"); - if (childNodeName) { - offer.charges = pugi::cast(childNodeName.value()); - } - - childNodeName = childNode.attribute("actionid"); - if (childNodeName) { - offer.actionid = pugi::cast(childNodeName.value()); - } - - if (offer.count == 0) { - offer.count = 1; - } - } else if (offer.type == OFFER_TYPE_MULTI_ITEMS) { - childNodeName = childNode.attribute("items"); - if (!childNodeName) { - SPDLOG_WARN("[Store::loadStore] Store Offer by name: '{}' need items", offer.name); - continue; - } - - StringVector itemsList = explodeString(childNodeName.value(), ";"); - for (const std::string& itemsInfo : itemsList) { - StringVector info = explodeString(itemsInfo, ","); - if (info.size() == 2) { - uint16_t itemid = std::stoi(info[0]); - uint16_t item_count = std::stoi(info[1]); - offer.itemList[itemid] = item_count; - } - } - - } else if (offer.type == OFFER_TYPE_SKULL_REMOVE) { - childNodeName = childNode.attribute("skull"); - if (childNodeName) { - auto parseSkull = OfferSkullMap.find(childNodeName.value()); - if (parseSkull != OfferSkullMap.end()) { - offer.skull = parseSkull->second; - } - } - } - - childNodeName = childNode.attribute("state"); - if (childNodeName) { - auto parseState = OfferStatesMap.find(childNodeName.value()); - if (parseState != OfferStatesMap.end()) { - offer.state = parseState->second; - } - } - - if (offer.state == OFFER_STATE_SALE) { - saleoffer = true; - childNodeName = childNode.attribute("validUntil"); - if (childNodeName) { - offer.validUntil = pugi::cast(childNodeName.value()); - } - childNodeName = childNode.attribute("basePrice"); - if (childNodeName) { - offer.basePrice = pugi::cast(childNodeName.value()); - } - } else if (offer.state == OFFER_STATE_NEW) { - newoffer = true; - } - - childNodeName = childNode.attribute("coinType"); - if (childNodeName) { - auto parseCoin = CoinTypeMap.find(childNodeName.value()); - if (parseCoin != CoinTypeMap.end()) { - offer.coinType = parseCoin->second; - } - } - - childNodeName = childNode.attribute("buyType"); - if (childNodeName) { - auto parsebtpe = OfferBuyTypesMap.find(childNodeName.value()); - if (parsebtpe != OfferBuyTypesMap.end()) { - offer.buyType = parsebtpe->second; - } - } - - offer.rookgaard = offers.rookgaard; - childNodeName = childNode.attribute("rookgaard"); - if (childNodeName) { - offer.rookgaard = childNodeName.as_bool(); - } - - } - } - return true; +bool Store::loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, + pugi::xml_attribute attributeName) +{ + + // Make store offers + std::string name = attributeName.value(); + + auto result = storeOffers.emplace(std::piecewise_construct, + std::forward_as_tuple(name), std::forward_as_tuple(name)); + + if (!result.second) { + SPDLOG_WARN("[Store::loadStore] - Duplicate category offer by name: " + "'{}' ignored'", + name); + return false; + } + + ++offercount; + + // Editing store offers + StoreOffers& offers = result.first->second; + attributeName = node.attribute("description"); + if (attributeName) { + offers.m_description = attributeName.value(); + } + + attributeName = node.attribute("icon"); + if (attributeName) { + offers.m_icon = attributeName.value(); + } + + attributeName = node.attribute("rookgaard"); + if (attributeName) { + offers.m_rookgaard = attributeName.as_bool(); + } + + attributeName = node.attribute("state"); + if (attributeName) { + auto parseState = OfferStatesMap.find(attributeName.value()); + if (parseState != OfferStatesMap.end()) { + offers.m_state = parseState->second; + } + } + + if (offers.m_state == OFFER_STATE_SALE) { + saleoffer = true; + } else if (offers.m_state == OFFER_STATE_NEW) { + newoffer = true; + } + + attributeName = node.attribute("parent"); + if (attributeName) { + offers.parent = attributeName.value(); + } + + // Getting the offers + for (auto childNode : node.children()) { + if (strcasecmp(childNode.name(), "offers") == 0) { + if (!(storeAttribute = childNode.attribute("name"))) { + SPDLOG_WARN("[Store::loadStore] - Missing 'name' attribute " + "in 'offers' entry"); + continue; + } + + name = storeAttribute.value(); + uint32_t id = 0; + pugi::xml_attribute childNodeName = childNode.attribute("id"); + if (childNodeName) { + id = pugi::cast(childNodeName.value()); + } else { + running_id++; + id = running_id; + } + + auto resultOffer = offers.offers.emplace(std::piecewise_construct, + std::forward_as_tuple(id), std::forward_as_tuple(id, name)); + + if (!resultOffer.second) { + SPDLOG_WARN( + "[Store::loadStore] - Duplicate offer by name: '{}'", + node.value()); + continue; + } + + StoreOffer& offer = resultOffer.first->second; + childNodeName = childNode.attribute("price"); + if (!childNodeName) { + SPDLOG_WARN( + "[Store::loadStore] - Offer by name: '{}' need m_price", + offer.m_name); + continue; + } + offer.m_price = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("count"); + if (childNodeName) { + offer.m_count = pugi::cast(childNodeName.value()); + } + + childNodeName = childNode.attribute("icon"); + if (childNodeName) { + offer.m_icon = childNodeName.value(); + } + + childNodeName = childNode.attribute("description"); + if (childNodeName) { + offer.m_description = childNodeName.value(); + } + + childNodeName = childNode.attribute("type"); + if (childNodeName) { + auto parseType = OfferTypesMap.find(childNodeName.value()); + if (parseType != OfferTypesMap.end()) { + offer.m_type = parseType->second; + } + } + + childNodeName = childNode.attribute("disabled"); + if (childNodeName) { + offer.m_disabled = childNodeName.as_bool(); + } + + if (offer.m_type == OFFER_TYPE_OUTFIT || + offer.m_type == OFFER_TYPE_OUTFIT_ADDON) { + childNodeName = childNode.attribute("female"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' " + "need m_female outfit", + offer.m_name); + continue; + } + offer.m_female = pugi::cast(childNodeName.value()); + childNodeName = childNode.attribute("male"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] - Offer by name: '{}' " + "need m_male outfit", + offer.m_name); + continue; + } + offer.m_male = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("addon"); + if (!childNodeName) { + offer.m_addon = 0; + } else { + offer.m_addon = pugi::cast(childNodeName.value()); + } + } else if (offer.m_type == OFFER_TYPE_BLESSINGS) { + childNodeName = childNode.attribute("blessId"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: " + "'{}' need bless m_id", + offer.m_name); + continue; + } + offer.m_blessId = pugi::cast(childNodeName.value()); + } else if (offer.m_type == OFFER_TYPE_ITEM || + offer.m_type == OFFER_TYPE_STACKABLE || + offer.m_type == OFFER_TYPE_HOUSE || + offer.m_type == OFFER_TYPE_TRAINING || + offer.m_type == OFFER_TYPE_POUCH) { + + childNodeName = childNode.attribute("itemId"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: " + "'{}' need itemid", + offer.m_name); + continue; + } + offer.m_itemId = pugi::cast(childNodeName.value()); + + childNodeName = childNode.attribute("charges"); + if (childNodeName) { + offer.m_charges = + pugi::cast(childNodeName.value()); + } + + childNodeName = childNode.attribute("actionId"); + if (childNodeName) { + offer.m_actionId = + pugi::cast(childNodeName.value()); + } + + if (offer.m_count == 0) { + offer.m_count = 1; + } + } else if (offer.m_type == OFFER_TYPE_MULTI_ITEMS) { + childNodeName = childNode.attribute("items"); + if (!childNodeName) { + SPDLOG_WARN("[Store::loadStore] Store Offer by name: " + "'{}' need items", + offer.m_name); + continue; + } + + StringVector itemsList = + explodeString(childNodeName.value(), ";"); + for (const std::string& itemsInfo : itemsList) { + StringVector info = explodeString(itemsInfo, ","); + if (info.size() == 2) { + uint16_t itemid = std::stoi(info[0]); + uint16_t item_count = std::stoi(info[1]); + offer.m_itemList[itemid] = item_count; + } + } + + } else if (offer.m_type == OFFER_TYPE_SKULL_REMOVE) { + childNodeName = childNode.attribute("skull"); + if (childNodeName) { + auto parseSkull = OfferSkullMap.find(childNodeName.value()); + if (parseSkull != OfferSkullMap.end()) { + offer.m_skull = parseSkull->second; + } + } + } + + childNodeName = childNode.attribute("state"); + if (childNodeName) { + auto parseState = OfferStatesMap.find(childNodeName.value()); + if (parseState != OfferStatesMap.end()) { + offer.m_state = parseState->second; + } + } + + if (offer.m_state == OFFER_STATE_SALE) { + saleoffer = true; + childNodeName = childNode.attribute("validUntil"); + if (childNodeName) { + offer.m_validUntil = + pugi::cast(childNodeName.value()); + } + childNodeName = childNode.attribute("basePrice"); + if (childNodeName) { + offer.m_basePrice = + pugi::cast(childNodeName.value()); + } + } else if (offer.m_state == OFFER_STATE_NEW) { + newoffer = true; + } + + childNodeName = childNode.attribute("coinType"); + if (childNodeName) { + auto parseCoin = CoinTypeMap.find(childNodeName.value()); + if (parseCoin != CoinTypeMap.end()) { + offer.m_coinType = parseCoin->second; + } + } + + childNodeName = childNode.attribute("buyType"); + if (childNodeName) { + auto parsebtpe = OfferBuyTypesMap.find(childNodeName.value()); + if (parsebtpe != OfferBuyTypesMap.end()) { + offer.m_buyType = parsebtpe->second; + } + } + + offer.m_rookgaard = offers.m_rookgaard; + childNodeName = childNode.attribute("rookgaard"); + if (childNodeName) { + offer.m_rookgaard = childNodeName.as_bool(); + } + } + } + return true; } -bool Store::reload() { - categories.clear(); - storeOffers.clear(); - home.offers.clear(); - home.banners.clear(); - runningid = beginid; - loaded = false; - offercount = 0; +bool Store::reload() +{ + categories.clear(); + storeOffers.clear(); + home.offers.clear(); + home.banners.clear(); + running_id = begin_id; + loaded = false; + offercount = 0; - newoffer = false; - saleoffer = false; + newoffer = false; + saleoffer = false; - return loadFromXML(true); + return loadFromXML(true); } -std::vector Store::getStoreOffers() { - std::vector filter; - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - filter.push_back(offers); - } +std::vector Store::getStoreOffers() +{ + std::vector filter; + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + filter.push_back(offers); + } - return filter; + return filter; } -std::vector Store::getStoreOffer(StoreOffers* offers) { - std::vector filter; - for (auto& info : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info.first); - if (offer) { - filter.push_back(offer); - } - } - return filter; +std::vector Store::getStoreOffer(StoreOffers* offers) +{ + std::vector filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + filter.push_back(offer); + } + } + return filter; } -StoreOffer* Store::getStoreOfferByName(std::string name) { - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - for (auto& info2 : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info2.first); - if (offer && strcasecmp(offer->getName().c_str(), name.c_str()) == 0) { - return offer; - } - } - } - return nullptr; +StoreOffer* Store::getStoreOfferByName(std::string name) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + for (auto& info2 : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info2.first); + if (offer && + strcasecmp(offer->getName().c_str(), name.c_str()) == 0) { + return offer; + } + } + } + return nullptr; } -std::vector Store::getHomeOffers() { - std::vector filter; - - for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* storeOffer = getStoreOfferByName((*off)); - if (storeOffer) { - bool hasDec = false; - for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; ++off2) { - if ((*off2)->getName() == storeOffer->getName()){ - hasDec = true; - break; - } - } - - if (!hasDec) { - filter.emplace_back(storeOffer); - } - } - } - - return filter; +std::vector Store::getHomeOffers() +{ + std::vector filter; + + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; + ++off) { + StoreOffer* storeOffer = getStoreOfferByName((*off)); + if (storeOffer) { + bool hasDec = false; + for (auto off2 = filter.begin(), end2 = filter.end(); off2 != end2; + ++off2) { + if ((*off2)->getName() == storeOffer->getName()) { + hasDec = true; + break; + } + } + + if (!hasDec) { + filter.emplace_back(storeOffer); + } + } + } + + return filter; } -std::map> Store::getHomeOffersOrganized() { - std::map> filter; - for (auto off = home.offers.begin(), end = home.offers.end(); off != end; ++off) { - StoreOffer* storeOffer = getStoreOfferByName((*off)); - if (storeOffer) { - std::string name = storeOffer->getName(); - filter[name].emplace_back(storeOffer); - } - } +std::map> Store::getHomeOffersOrganized() +{ + std::map> filter; + for (auto off = home.offers.begin(), end = home.offers.end(); off != end; + ++off) { + StoreOffer* storeOffer = getStoreOfferByName((*off)); + if (storeOffer) { + std::string name = storeOffer->getName(); + filter[name].emplace_back(storeOffer); + } + } - return filter; + return filter; } -std::map> Store::getStoreOrganizedByName(StoreOffers* offers) { - std::map> filter; - for (auto& info : offers->offers) { - StoreOffer* offer = offers->getOfferByID(info.first); - if (offer) { - std::string name = offer->getName(); - filter[name].emplace_back(offer); - } - } - - return filter; +std::map> Store::getStoreOrganizedByName( + StoreOffers* offers) +{ + std::map> filter; + for (auto& info : offers->offers) { + StoreOffer* offer = offers->getOfferByID(info.first); + if (offer) { + std::string name = offer->getName(); + filter[name].emplace_back(offer); + } + } + + return filter; } -StoreOffer* StoreOffers::getOfferByID(uint32_t id) { - auto it = offers.find(id); - if (it == offers.end()) { - return nullptr; - } +StoreOffer* StoreOffers::getOfferByID(uint32_t m_id) +{ + auto it = offers.find(m_id); + if (it == offers.end()) { + return nullptr; + } - return &it->second; + return &it->second; } -std::string StoreOffer::getDisabledReason(Player* player) { - - uint16_t outfitLookType = player->getSex() == PLAYERSEX_FEMALE ? female : male; - - std::string disabledReason; - if (disabled) { - disabledReason = "This offer is disabled."; - } else if (type == OFFER_TYPE_POUCH) { - Item* item = g_game.findItemOfType(player, 26377, true, -1); - if (item) - disabledReason = "You already have Loot Pouch."; - } else if (type == OFFER_TYPE_BLESSINGS) { - if (player->hasBlessing(blessid)) - disabledReason = "You already have this Bless."; - } else if (type == OFFER_TYPE_ALL_BLESSINGS) { - uint8_t count = 0; - uint8_t limitBless = 0; - uint8_t minBless = (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : BLESS_FIRST); - uint8_t maxBless = BLESS_LAST; - for (int i = minBless; i <= maxBless; ++i) { - limitBless++; - if (player->hasBlessing(i)) { - count++; - } - } - - if (count >= limitBless) - disabledReason = "You already have all Blessings."; - - } else if (type == OFFER_TYPE_OUTFIT && player->canWear(outfitLookType, addon)) { - disabledReason = "You already have this outfit."; - } else if (type == OFFER_TYPE_OUTFIT_ADDON) { - if (player->canWear(outfitLookType, 0)) { - if (player->canWear(outfitLookType, addon)) { - disabledReason = "You already have this addon."; - } - } - } else if (type == OFFER_TYPE_MOUNT) { - Mount* mount = g_game.mounts.getMountByID(id); - if (!mount) { - disabledReason = "Mount not found"; - } else if (player->hasMount(mount)) { - disabledReason = "You already have this mount."; - } - - } else if (type == OFFER_TYPE_PROMOTION) { - disabledReason = "This offer has disabled."; - } else if (type == OFFER_TYPE_PREY_SLOT) { - //if (player->isUnlockedPrey(2)) { - disabledReason = "You already have 3 slots released."; - //} - - } else if (type == OFFER_TYPE_EXP_BOOST) { - int32_t value1; - player->getStorageValue(51052, value1); - int32_t value2; - player->getStorageValue(51053, value2); - if (value1 >= 6) { - disabledReason = "Can be purchased up to 5 times between 2 server saves."; - } else if ((OS_TIME(nullptr) - value2) < (1*60*60)) { - disabledReason = "You still have active boost."; - } - } else if (type == OFFER_TYPE_CHARM_EXPANSION) { - if (player->hasCharmExpansion()) { - disabledReason = "You have charm expansion"; - } - } else if (type == OFFER_TYPE_SKULL_REMOVE) { - if (player->getSkull() != skull) { - disabledReason = "This offer is disabled for you"; - } - } else if (type == OFFER_TYPE_FRAG_REMOVE) { - if (player->unjustifiedKills.empty()) { - disabledReason = "You have no frag to remove."; - } - } else if (type == OFFER_TYPE_RECOVERY_KEY) { - int32_t value; - player->getAccountStorageValue(1, value); - if (value > OS_TIME(nullptr)) { - disabledReason = "You recently generated an RK."; - } - } - - if (player->getVocation()->getId() == 0 && !rookgaard) { - disabledReason = "This offer is deactivated."; - } - - if (player->getCoinBalance() - getPrice(player) < 0) { - disabledReason = "You don't have coins."; - } else if (player->getTournamentCoinBalance() - getPrice(player) < 0) { - disabledReason = "You don't have tournament coins."; - } - - return disabledReason; +std::string StoreOffer::getDisabledReason(Player* player) +{ + + uint16_t outfitLookType = + player->getSex() == PLAYERSEX_FEMALE ? m_female : m_male; + + std::string disabledReason; + if (m_disabled) { + disabledReason = "This offer is m_disabled."; + } else if (m_type == OFFER_TYPE_POUCH) { + Item* item = g_game.findItemOfType(player, 26377, true, -1); + if (item) + disabledReason = "You already have Loot Pouch."; + } else if (m_type == OFFER_TYPE_BLESSINGS) { + if (player->hasBlessing(m_blessId)) + disabledReason = "You already have this Bless."; + } else if (m_type == OFFER_TYPE_ALL_BLESSINGS) { + uint8_t m_count = 0; + uint8_t limitBless = 0; + uint8_t minBless = + (g_game.getWorldType() == WORLD_TYPE_PVP ? BLESS_PVE_FIRST : + BLESS_FIRST); + uint8_t maxBless = BLESS_LAST; + for (int i = minBless; i <= maxBless; ++i) { + limitBless++; + if (player->hasBlessing(i)) { + ++m_count; + } + } + + if (m_count >= limitBless) + disabledReason = "You already have all Blessings."; + + } else if (m_type == OFFER_TYPE_OUTFIT && + player->canWear(outfitLookType, m_addon)) { + disabledReason = "You already have this outfit."; + } else if (m_type == OFFER_TYPE_OUTFIT_ADDON) { + if (player->canWear(outfitLookType, 0)) { + if (player->canWear(outfitLookType, m_addon)) { + disabledReason = "You already have this m_addon."; + } + } + } else if (m_type == OFFER_TYPE_MOUNT) { + Mount* mount = g_game.mounts.getMountByID(m_id); + if (!mount) { + disabledReason = "Mount not found"; + } else if (player->hasMount(mount)) { + disabledReason = "You already have this mount."; + } + + } else if (m_type == OFFER_TYPE_PROMOTION) { + disabledReason = "This offer has m_disabled."; + } else if (m_type == OFFER_TYPE_PREY_SLOT) { + // if (player->isUnlockedPrey(2)) { + disabledReason = "You already have 3 slots released."; + //} + + } else if (m_type == OFFER_TYPE_EXP_BOOST) { + int32_t value1; + player->getStorageValue(51052, value1); + int32_t value2; + player->getStorageValue(51053, value2); + if (value1 >= 6) { + disabledReason = + "Can be purchased up to 5 times between 2 server saves."; + } else if ((OS_TIME(nullptr) - value2) < (1 * 60 * 60)) { + disabledReason = "You still have active boost."; + } + } else if (m_type == OFFER_TYPE_CHARM_EXPANSION) { + if (player->hasCharmExpansion()) { + disabledReason = "You have charm expansion"; + } + } else if (m_type == OFFER_TYPE_SKULL_REMOVE) { + if (player->getSkull() != m_skull) { + disabledReason = "This offer is m_disabled for you"; + } + } else if (m_type == OFFER_TYPE_FRAG_REMOVE) { + if (player->unjustifiedKills.empty()) { + disabledReason = "You have no frag to remove."; + } + } else if (m_type == OFFER_TYPE_RECOVERY_KEY) { + int32_t value; + player->getAccountStorageValue(1, value); + if (value > OS_TIME(nullptr)) { + disabledReason = "You recently generated an RK."; + } + } + + if (player->getVocation()->getId() == 0 && !m_rookgaard) { + disabledReason = "This offer is deactivated."; + } + + if (player->getCoinBalance() - getPrice(player) < 0) { + disabledReason = "You don't have coins."; + } else if (player->getTournamentCoinBalance() - getPrice(player) < 0) { + disabledReason = "You don't have tournament coins."; + } + + return disabledReason; } -Mount* StoreOffer::getMount() { - return g_game.mounts.getMountByID(id); +Mount* StoreOffer::getMount() +{ + return g_game.mounts.getMountByID(m_id); } -uint8_t Store::convertType(OfferTypes_t type) { - uint8_t offertype = 0; - if (type == OFFER_TYPE_POUCH || type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE || type == OFFER_TYPE_HOUSE || type == OFFER_TYPE_TRAINING) { - offertype = 3; - } else if (type == OFFER_TYPE_OUTFIT || type == OFFER_TYPE_OUTFIT_ADDON) { - offertype = 2; - } else if (type == OFFER_TYPE_MOUNT) { - offertype = 1; - } - - return offertype; +uint8_t Store::convertType(OfferTypes_t type) +{ + uint8_t offertype = 0; + if (type == OFFER_TYPE_POUCH || type == OFFER_TYPE_ITEM || + type == OFFER_TYPE_STACKABLE || type == OFFER_TYPE_HOUSE || + type == OFFER_TYPE_TRAINING) { + offertype = 3; + } else if (type == OFFER_TYPE_OUTFIT || type == OFFER_TYPE_OUTFIT_ADDON) { + offertype = 2; + } else if (type == OFFER_TYPE_MOUNT) { + offertype = 1; + } + + return offertype; } -StoreOffers* Store::getOfferByName(std::string name) { - // Check the categories first - for (auto offer = categories.begin(), end = categories.end(); offer != end; ++offer) { - if (strcasecmp((*offer).name.c_str(), name.c_str()) == 0) { - return getOfferByName((*offer).subcategory[0]); - } - } - - // Go to offers - auto it = storeOffers.find(name); - if (it == storeOffers.end()) { - // Clicking on the banner too calls an offer - // SPDLOG_WARN("[Store::getOfferByName] Offer '{}' not found", name); - return nullptr; - } - return &it->second; +StoreOffers* Store::getOfferByName(std::string name) +{ + // Check the categories first + for (auto offer = categories.begin(), end = categories.end(); offer != end; + ++offer) { + if (strcasecmp((*offer).m_name.c_str(), name.c_str()) == 0) { + return getOfferByName((*offer).subcategory[0]); + } + } + + // Go to offers + auto it = storeOffers.find(name); + if (it == storeOffers.end()) { + // Clicking on the banner too calls an offer + // SPDLOG_WARN("[Store::getOfferByName] Offer '{}' not found", name); + return nullptr; + } + return &it->second; } -StoreOffers* Store::getOffersByOfferId(uint32_t id) { - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - if (offers == nullptr){ - continue; - } - - StoreOffer* offer = offers->getOfferByID(id); - if (offer != nullptr && offer->getId() == id) { - return offers; - } - } - - return nullptr; +StoreOffers* Store::getOffersByOfferId(uint32_t m_id) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr) { + continue; + } + + StoreOffer* offer = offers->getOfferByID(m_id); + if (offer != nullptr && offer->getId() == m_id) { + return offers; + } + } + + return nullptr; } -StoreOffer* Store::getOfferById(uint32_t id) { - for (auto& info : storeOffers) { - StoreOffers* offers = &info.second; - if (offers == nullptr){ - continue; - } - - StoreOffer* offer = offers->getOfferByID(id); - if (offer != nullptr && offer->getId() == id) { - return offer; - } - } - - return nullptr; +StoreOffer* Store::getOfferById(uint32_t m_id) +{ + for (auto& info : storeOffers) { + StoreOffers* offers = &info.second; + if (offers == nullptr) { + continue; + } + + StoreOffer* offer = offers->getOfferByID(m_id); + if (offer != nullptr && offer->getId() == m_id) { + return offer; + } + } + + return nullptr; } -uint32_t StoreOffer::getPrice(Player* player) { - uint32_t offerBasePrice = 0; - if (state == OFFER_STATE_SALE) { - time_t my_time; - struct tm tm; - - my_time = time(NULL); - localtime_r(&my_time, &tm); - - int32_t daySub = validUntil - tm.tm_mday; - if (daySub < 0) { - offerBasePrice = basePrice; - } - } - - uint32_t discountPrice = price; - // Check if discount for premium is activated - if (g_config.getBoolean(STORE_PREMIUM_DISCOUNT)) { - if (player && player->isPremium()) { - offerBasePrice *= 1.0; - // Default premium discount rate (0.90 = 10% of discount) - discountPrice *= g_config.getFloat(RATE_STORE_PREMIUM_DISCOUNT); - } - } - - return offerBasePrice > 0 ? offerBasePrice : discountPrice; +uint32_t StoreOffer::getPrice(Player* player) +{ + uint32_t offerBasePrice = 0; + if (m_state == OFFER_STATE_SALE) { + time_t my_time; + struct tm tm; + + my_time = time(NULL); + localtime_r(&my_time, &tm); + + int32_t daySub = m_validUntil - tm.tm_mday; + if (daySub < 0) { + offerBasePrice = m_basePrice; + } + } + + uint32_t discountPrice = m_price; + // Check if discount for premium is activated + if (g_config.getBoolean(STORE_PREMIUM_DISCOUNT)) { + if (player && player->isPremium()) { + offerBasePrice *= 1.0; + // Default premium discount rate (0.90 = 10% of discount) + discountPrice *= g_config.getFloat(RATE_STORE_PREMIUM_DISCOUNT); + } + } + + return offerBasePrice > 0 ? offerBasePrice : discountPrice; } -uint16_t StoreOffer::getCount(bool inBuy) { - if (!inBuy && type == OFFER_TYPE_PREMIUM) { - return 1; - } +uint16_t StoreOffer::getCount(bool inBuy) +{ + if (!inBuy && m_type == OFFER_TYPE_PREMIUM) { + return 1; + } - return count; + return m_count; } -std::string StoreOffer::getDescription(Player* player /*= nullptr */) { - if (!player) { - return description; - } - - std::string showDesc = description; - if (showDesc.empty()) { - showDesc = description; - } - - if ((type == OFFER_TYPE_ITEM || type == OFFER_TYPE_STACKABLE) && showDesc.empty()) { - Item* virtualItem = Item::CreateItem(itemId, count); - if (virtualItem) { - showDesc = "You see " + virtualItem->getDescription(-2); - delete virtualItem; - } - } - - return showDesc; +std::string StoreOffer::getDescription(Player* player /*= nullptr */) +{ + if (!player) { + return m_description; + } + + std::string showDesc = m_description; + if (showDesc.empty()) { + showDesc = m_description; + } + + if ((m_type == OFFER_TYPE_ITEM || m_type == OFFER_TYPE_STACKABLE) && + showDesc.empty()) { + Item* virtualItem = Item::CreateItem(m_itemId, m_count); + if (virtualItem) { + showDesc = "You see " + virtualItem->getDescription(-2); + delete virtualItem; + } + } + + return showDesc; } diff --git a/src/creatures/players/store/store.hpp b/src/creatures/players/store/store.hpp index 32fc8be77de..d7552985fcc 100644 --- a/src/creatures/players/store/store.hpp +++ b/src/creatures/players/store/store.hpp @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ + */ #ifndef SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ #define SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ @@ -35,304 +35,354 @@ class StoreOffers; class StoreOffer; enum OfferTypes_t : uint8_t { - OFFER_TYPE_NONE = 0, // This will disable offer - OFFER_TYPE_ITEM = 1, - OFFER_TYPE_STACKABLE = 2, - OFFER_TYPE_OUTFIT = 3, - OFFER_TYPE_OUTFIT_ADDON = 4, - OFFER_TYPE_MOUNT = 5, - OFFER_TYPE_NAME_CHANGE = 6, - OFFER_TYPE_SEX_CHANGE = 7, - OFFER_TYPE_PROMOTION = 8, - OFFER_TYPE_HOUSE = 9, - OFFER_TYPE_EXP_BOOST = 10, - OFFER_TYPE_PREY_SLOT = 11, - OFFER_TYPE_PREY_BONUS = 12, - OFFER_TYPE_TEMPLE = 13, - OFFER_TYPE_BLESSINGS = 14, - OFFER_TYPE_PREMIUM = 15, - OFFER_TYPE_POUCH = 16, - OFFER_TYPE_ALL_BLESSINGS = 17, - OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, - OFFER_TYPE_TRAINING = 19, - OFFER_TYPE_CHARM_EXPANSION = 20, - OFFER_TYPE_CHARM_POINTS = 21, - OFFER_TYPE_MULTI_ITEMS = 22, - OFFER_TYPE_VIP = 24, - OFFER_TYPE_FRAG_REMOVE = 25, - OFFER_TYPE_SKULL_REMOVE = 26, - OFFER_TYPE_RECOVERY_KEY = 27, + OFFER_TYPE_NONE = 0, // This will disable offer + OFFER_TYPE_ITEM = 1, + OFFER_TYPE_STACKABLE = 2, + OFFER_TYPE_OUTFIT = 3, + OFFER_TYPE_OUTFIT_ADDON = 4, + OFFER_TYPE_MOUNT = 5, + OFFER_TYPE_NAME_CHANGE = 6, + OFFER_TYPE_SEX_CHANGE = 7, + OFFER_TYPE_PROMOTION = 8, + OFFER_TYPE_HOUSE = 9, + OFFER_TYPE_EXP_BOOST = 10, + OFFER_TYPE_PREY_SLOT = 11, + OFFER_TYPE_PREY_BONUS = 12, + OFFER_TYPE_TEMPLE = 13, + OFFER_TYPE_BLESSINGS = 14, + OFFER_TYPE_PREMIUM = 15, + OFFER_TYPE_POUCH = 16, + OFFER_TYPE_ALL_BLESSINGS = 17, + OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, + OFFER_TYPE_TRAINING = 19, + OFFER_TYPE_CHARM_EXPANSION = 20, + OFFER_TYPE_CHARM_POINTS = 21, + OFFER_TYPE_MULTI_ITEMS = 22, + OFFER_TYPE_VIP = 24, + OFFER_TYPE_FRAG_REMOVE = 25, + OFFER_TYPE_SKULL_REMOVE = 26, + OFFER_TYPE_RECOVERY_KEY = 27, }; enum OfferBuyTypes_t : uint8_t { - OFFER_BUY_TYPE_OTHERS = 0, - OFFER_BUY_TYPE_NAMECHANGE = 1, - OFFER_BUY_TYPE_TESTE = 3, + OFFER_BUY_TYPE_OTHERS = 0, + OFFER_BUY_TYPE_NAMECHANGE = 1, + OFFER_BUY_TYPE_TESTE = 3, }; enum ClientOfferTypes_t { - CLIENT_STORE_OFFER_OTHER = 0, - CLIENT_STORE_OFFER_NAMECHANGE = 1 + CLIENT_STORE_OFFER_OTHER = 0, + CLIENT_STORE_OFFER_NAMECHANGE = 1 }; enum OfferStates_t { - OFFER_STATE_NONE = 0, - OFFER_STATE_NEW = 1, - OFFER_STATE_SALE = 2, - OFFER_STATE_TIMED = 3 + OFFER_STATE_NONE = 0, + OFFER_STATE_NEW = 1, + OFFER_STATE_SALE = 2, + OFFER_STATE_TIMED = 3 }; enum StoreErrors_t { - STORE_ERROR_PURCHASE = 0, - STORE_ERROR_NETWORK = 1, - STORE_ERROR_HISTORY = 2, - STORE_ERROR_TRANSFER = 3, - STORE_ERROR_INFORMATION = 4 + STORE_ERROR_PURCHASE = 0, + STORE_ERROR_NETWORK = 1, + STORE_ERROR_HISTORY = 2, + STORE_ERROR_TRANSFER = 3, + STORE_ERROR_INFORMATION = 4 }; enum StoreServiceTypes_t { - STORE_SERVICE_STANDERD = 0, - STORE_SERVICE_OUTFITS = 3, - STORE_SERVICE_MOUNTS = 4, - STORE_SERVICE_BLESSINGS = 5 + STORE_SERVICE_STANDERD = 0, + STORE_SERVICE_OUTFITS = 3, + STORE_SERVICE_MOUNTS = 4, + STORE_SERVICE_BLESSINGS = 5 }; enum StoreHistoryTypes_t { - HISTORY_TYPE_NONE = 0, - HISTORY_TYPE_GIFT = 1, - HISTORY_TYPE_REFUND = 2 + HISTORY_TYPE_NONE = 0, + HISTORY_TYPE_GIFT = 1, + HISTORY_TYPE_REFUND = 2 }; struct StoreCategory { - StoreCategory(std::string name, std::vector subcategory, std::string icon, bool rookgaard) : - name(std::move(name)), subcategory(std::move(subcategory)), icon(std::move(icon)), rookgaard(rookgaard) {} - - std::string name; - std::vector subcategory; - std::string icon; - bool rookgaard; + StoreCategory(std::string m_name, std::vector subcategory, + std::string m_icon, bool m_rookgaard) + : m_name(std::move(m_name)) + , subcategory(std::move(subcategory)) + , m_icon(std::move(m_icon)) + , m_rookgaard(m_rookgaard) + { + } + + std::string m_name; + std::vector subcategory; + std::string m_icon; + bool m_rookgaard; }; struct StoreHome { - std::vector offers; - std::vector banners; + std::vector offers; + std::vector banners; }; -class Store { - public: - //bool loadStore(const FileName& identifier); - bool loadFromXML(bool reloading = false); - bool loadCategory(pugi::xml_node node, pugi::xml_attribute name); - bool loadHome(pugi::xml_node node); - bool loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, pugi::xml_attribute attributeName); - bool reload(); - - bool hasNewOffer() { - return newoffer; - } - - bool hasSaleOffer() { - return saleoffer; - } - - uint16_t getOfferCount() { - return offercount; - } - - bool isValidType(OfferTypes_t type); - - std::vector getStoreOffers(); - std::vector getStoreCategories() { - return categories; - } - StoreHome getStoreHome() { - return home; - } - - std::map> getStoreOrganizedByName(StoreOffers* offer); - std::map> getHomeOffersOrganized(); - std::vector getStoreOffer(StoreOffers* offer); - - std::vector getHomeOffers(); - const std::vector& getHomeBanners() const { - return home.banners; - } - - uint8_t convertType(OfferTypes_t type); - StoreOffers* getOfferByName(std::string name); - StoreOffers* getOffersByOfferId(uint32_t id); - StoreOffer* getStoreOfferByName(std::string name); - StoreOffer* getOfferById(uint32_t id); - protected: - friend class StoreOffers; - friend class StoreOffer; - - std::vector categories; - std::map storeOffers; - StoreHome home; - - bool loaded = false; - private: - // As mount uses uint16 as base, we can set the ids of the offers (which were not identified) - // As the maximum value of uint16 + 1 - uint16_t beginid = std::numeric_limits::max(); - uint32_t runningid = beginid; - uint16_t offercount = 0; - - bool newoffer = false; - bool saleoffer = false; - +class Store +{ +public: + // bool loadStore(const FileName& identifier); + bool loadFromXML(bool reloading = false); + bool loadCategory(pugi::xml_node node, pugi::xml_attribute m_name); + bool loadHome(pugi::xml_node node); + bool loadOffer(pugi::xml_node node, pugi::xml_attribute storeAttribute, + pugi::xml_attribute attributeName); + bool reload(); + + bool hasNewOffer() + { + return newoffer; + } + + bool hasSaleOffer() + { + return saleoffer; + } + + uint16_t getOfferCount() + { + return offercount; + } + + bool isValidType(OfferTypes_t type); + + std::vector getStoreOffers(); + std::vector getStoreCategories() + { + return categories; + } + StoreHome getStoreHome() + { + return home; + } + + std::map> getStoreOrganizedByName( + StoreOffers* offer); + std::map> getHomeOffersOrganized(); + std::vector getStoreOffer(StoreOffers* offer); + + std::vector getHomeOffers(); + const std::vector& getHomeBanners() const + { + return home.banners; + } + + uint8_t convertType(OfferTypes_t type); + StoreOffers* getOfferByName(std::string m_name); + StoreOffers* getOffersByOfferId(uint32_t m_id); + StoreOffer* getStoreOfferByName(std::string m_name); + StoreOffer* getOfferById(uint32_t m_id); + +protected: + friend class StoreOffers; + friend class StoreOffer; + + std::vector categories; + std::map storeOffers; + StoreHome home; + + bool loaded = false; + +private: + // As mount uses uint16 as base, we can set the ids of the offers (which + // were not identified) As the maximum value of uint16 + 1 + uint16_t begin_id = std::numeric_limits::max(); + uint32_t running_id = begin_id; + uint16_t offercount = 0; + + bool newoffer = false; + bool saleoffer = false; }; -class StoreOffers { - public: - explicit StoreOffers(std::string name) : - name(std::move(name)) {} - - std::string getName() { - return name; - } - std::string getDescription() { - return description; - } - std::string getIcon() { - return icon; - } - std::string getParent() { - return parent; - } - - bool canUseRookgaard() { - return rookgaard; - } - - OfferStates_t getOfferState() { - return state; - } - - StoreOffer* getOfferByID(uint32_t id); - - protected: - friend class Store; - friend class StoreOffer; - - std::map offers; - - private: - std::string name; - std::string icon = ""; - std::string description = ""; - std::string parent; - bool rookgaard = false; - OfferStates_t state = OFFER_STATE_NONE; +class StoreOffers +{ +public: + explicit StoreOffers(std::string m_name) + : m_name(std::move(m_name)) + { + } + + std::string getName() + { + return m_name; + } + std::string getDescription() + { + return m_description; + } + std::string getIcon() + { + return m_icon; + } + std::string getParent() + { + return parent; + } + + bool canUseRookgaard() + { + return m_rookgaard; + } + + OfferStates_t getOfferState() + { + return m_state; + } + + StoreOffer* getOfferByID(uint32_t m_id); + +protected: + friend class Store; + friend class StoreOffer; + + std::map offers; + +private: + std::string m_name; + std::string m_icon = ""; + std::string m_description = ""; + std::string parent; + bool m_rookgaard = false; + OfferStates_t m_state = OFFER_STATE_NONE; }; -class StoreOffer { - public: - StoreOffer(uint32_t _id, std::string _name) : - id(_id), name(std::move( _name)) {} - - std::string getDisabledReason(Player* player); - - std::string getName() { - return name; - } - std::string getDescription(Player* player = nullptr); - std::string getIcon() { - return icon; - } - - uint32_t getId() { - return id; - } - uint32_t getPrice(Player* player = nullptr); - uint32_t getBasePrice() { - return basePrice; - } - uint32_t getValidUntil() { - return validUntil; - } - uint16_t getCount(bool inBuy = false); - - uint16_t getBlessid() { - return blessid; - } - uint16_t getItemType() { - return itemId; - } - uint16_t getCharges() { - return charges; - } - uint16_t getActionID() { - return actionid; - } - uint8_t getAddon() { - return addon; - } - uint16_t getOutfitMale() { - return male; - } - uint16_t getOutfitFemale() { - return female; - } - - const std::map& getItems() const { - return itemList; - } - - OfferStates_t getOfferState() { - return state; - } - CoinType_t getCoinType() { - return coinType; - } - OfferTypes_t getOfferType() { - return type; - } - OfferBuyTypes_t getOfferBuyType() { - return buyType; - } - - Skulls_t getSkull() { - return skull; - } - - bool haveOfferRookgaard() { - return rookgaard; - } - - Mount* getMount(); - - protected: - friend class Store; - friend class StoreOffers; - - private: - uint32_t id = 0; - std::string name = ""; - - std::map itemList; - std::string description; - std::string icon = ""; - OfferStates_t state = OFFER_STATE_NONE; - CoinType_t coinType = COIN_TYPE_DEFAULT; - OfferBuyTypes_t buyType = OFFER_BUY_TYPE_OTHERS; - uint16_t count = 1; - uint32_t price = 150; // Default price (This preventing valueless offers from entering) - uint32_t basePrice = 0; // Default price (This preventing valueless offers from entering) - uint32_t validUntil = 0; - uint16_t blessid = 0; - uint16_t itemId = 0; - uint16_t charges = 1; - uint8_t addon = 0; - uint16_t male; - uint16_t female; - uint16_t actionid = 0; - Skulls_t skull = SKULL_NONE; - - bool disabled = false; - bool rookgaard = true; - OfferTypes_t type = OFFER_TYPE_NONE; +class StoreOffer +{ +public: + StoreOffer(uint32_t _id, std::string _name) + : m_id(_id) + , m_name(std::move(_name)) + { + } + + std::string getDisabledReason(Player* player); + + std::string getName() + { + return m_name; + } + std::string getDescription(Player* player = nullptr); + std::string getIcon() + { + return m_icon; + } + + uint32_t getId() + { + return m_id; + } + uint32_t getPrice(Player* player = nullptr); + uint32_t getBasePrice() + { + return m_basePrice; + } + uint32_t getValidUntil() + { + return m_validUntil; + } + uint16_t getCount(bool inBuy = false); + + uint16_t getBlessid() + { + return m_blessId; + } + uint16_t getItemType() + { + return m_itemId; + } + uint16_t getCharges() + { + return m_charges; + } + uint16_t getActionID() + { + return m_actionId; + } + uint8_t getAddon() + { + return m_addon; + } + uint16_t getOutfitMale() + { + return m_male; + } + uint16_t getOutfitFemale() + { + return m_female; + } + + const std::map& getItems() const + { + return m_itemList; + } + + OfferStates_t getOfferState() + { + return m_state; + } + CoinType_t getCoinType() + { + return m_coinType; + } + OfferTypes_t getOfferType() + { + return m_type; + } + OfferBuyTypes_t getOfferBuyType() + { + return m_buyType; + } + + Skulls_t getSkull() + { + return m_skull; + } + + bool haveOfferRookgaard() + { + return m_rookgaard; + } + + Mount* getMount(); + +protected: + friend class Store; + friend class StoreOffers; + +private: + uint32_t m_id = 0; + std::string m_name = ""; + + std::map m_itemList; + std::string m_description; + std::string m_icon = ""; + OfferStates_t m_state = OFFER_STATE_NONE; + CoinType_t m_coinType = COIN_TYPE_DEFAULT; + OfferBuyTypes_t m_buyType = OFFER_BUY_TYPE_OTHERS; + uint16_t m_count = 1; + uint32_t m_price = + 150; // Default m_price (This preventing valueless offers from entering) + uint32_t m_basePrice = + 0; // Default m_price (This preventing valueless offers from entering) + uint32_t m_validUntil = 0; + uint16_t m_blessId = 0; + uint16_t m_itemId = 0; + uint16_t m_charges = 1; + uint8_t m_addon = 0; + uint16_t m_male; + uint16_t m_female; + uint16_t m_actionId = 0; + Skulls_t m_skull = SKULL_NONE; + + bool m_disabled = false; + bool m_rookgaard = true; + OfferTypes_t m_type = OFFER_TYPE_NONE; }; #endif // SRC_CREATURES_PLAYERS_STORE_STORE_HPP_ diff --git a/src/game/game.cpp b/src/game/game.cpp index 6e2ae067eb9..2cb5d11cd4a 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6864,27 +6864,27 @@ bool save = false; uint32_t rem_days = 0; time_t last_day; - rem_days = account.GetPremiumRemaningDays(); - last_day = account.GetPremiumLastDay() ; + rem_days = account.getPremiumRemaningDays(); + last_day = account.getPremiumLastDay() ; std::string email; if (rem_days != 0) { if (last_day == 0) { - account.SetPremiumLastDay(timeNow); + account.setPremiumLastDay(timeNow); save = true; } else { uint32_t days = (timeNow - last_day) / 86400; if (days > 0) { if (days >= rem_days) { - if(!account.SetPremiumRemaningDays(0) || !account.SetPremiumLastDay(0)) { - email = account.GetEmail(); + if(!account.setPremiumRemaningDays(0) || !account.setPremiumLastDay(0)) { + email = account.getEmail(); SPDLOG_ERROR("Failed to set account premium days, account email: {}", email); } } else { - account.SetPremiumRemaningDays((rem_days - days)); + account.setPremiumRemaningDays((rem_days - days)); time_t remainder = (timeNow - last_day) % 86400; - account.SetPremiumLastDay(timeNow - remainder); + account.setPremiumLastDay(timeNow - remainder); } save = true; @@ -6892,12 +6892,12 @@ bool save = false; } } else if (last_day != 0) { - account.SetPremiumLastDay(0); + account.setPremiumLastDay(0); save = true; } - if (save && !account.SaveAccountDB()) { - SPDLOG_ERROR("Failed to save account: {}", account.GetEmail()); + if (save && !account.saveAccountDB()) { + SPDLOG_ERROR("Failed to save account: {}", account.getEmail()); } } @@ -7579,13 +7579,13 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t spr } account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.AddCoins(amount)) { - SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.getID()); return; } } else { @@ -7678,13 +7678,13 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.AddCoins(offer.amount)) { - SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(offer.amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.getID()); return; } } @@ -7788,26 +7788,26 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; }; uint32_t player_coins = 0; - if (auto [ player_coins, result ] = account.GetCoins(); + if (auto [ player_coins, result ] = account.getCoins(); account::ERROR_NO == result) { if (amount > player_coins) { return; } } else { SPDLOG_ERROR("Failed to add Account [{}] coins. (Error: [{}])", - account.GetID(), result); + account.getID(), result); return; } - if (account::ERROR_NO != account.AddCoins(amount)) { - SPDLOG_ERROR("Failed to add Account [{}] coins.", account.GetID()); + if (account::ERROR_NO != account.addCoins(amount)) { + SPDLOG_ERROR("Failed to add Account [{}] coins.", account.getID()); return; } } else { @@ -7837,14 +7837,14 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account(buyerPlayer->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.AddCoins(amount)) { - SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.getID()); return; } } @@ -7907,14 +7907,14 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.AddCoins(amount)) { - SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.getID()); return; } } else if (it.stackable) { @@ -7950,9 +7950,9 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); if (it.id == ITEM_STORE_COIN) { account::Account account(sellerPlayer->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } } @@ -7963,9 +7963,9 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (IOLoginData::loadPlayerById(sellerPlayer, offer.playerId)) { account::Account account(sellerPlayer->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } } @@ -8989,21 +8989,21 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: } account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } if (successfully) { if (thisOffer->getCoinType() == COIN_TYPE_DEFAULT) { - if(account::ERROR_NO == account.RemoveCoins(offerPrice*-1)) { + if(account::ERROR_NO == account.removeCoins(offerPrice*-1)) { int coins = 0; - if(auto [ coins, result ] = account.GetCoins() ; + if(auto [ coins, result ] = account.getCoins() ; account::ERROR_NO == result) { player->setCoins(coins); } else { - SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.getID()); return; } @@ -9012,22 +9012,22 @@ void Game::playerBuyStoreOffer(uint32_t playerId, const StoreOffer& offer, std:: << " for " << offerPrice*-1 <<" coins"; } } else { - SPDLOG_ERROR("Failed to remove Coins from account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to remove Coins from account: [{}]", account.getID()); } } else if (thisOffer->getCoinType() == COIN_TYPE_TOURNAMENT) { - if(account::ERROR_NO != account.RemoveTournamentCoins(offerPrice*-1)) { + if(account::ERROR_NO != account.removeTournamentCoins(offerPrice*-1)) { SPDLOG_ERROR("Failed to remove Tournament Coins from account: [{}]", - account.GetID()); + account.getID()); return; } int tournament_coins = 0; - if(auto [ tournament_coins, result ] = account.GetTournamentCoins() ; + if(auto [ tournament_coins, result ] = account.getTournamentCoins() ; account::ERROR_NO != result) { SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", - account.GetID()); + account.getID()); return; }; @@ -9123,27 +9123,27 @@ void Game::playerCoinTransfer(uint32_t playerId, const std::string& recipient, u std::string description(player->getName() + " transferred to " + recipient); account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.RemoveCoins(static_cast(amount))) { - SPDLOG_ERROR("Failed to remove coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.removeCoins(static_cast(amount))) { + SPDLOG_ERROR("Failed to remove coins to account: [{}]", account.getID()); return; } player->coinBalance -= amount; - if(account::ERROR_NO != account.LoadAccountDB(recipientPlayer->getAccount())) + if(account::ERROR_NO != account.loadAccountDB(recipientPlayer->getAccount())) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } - if(account::ERROR_NO != account.AddCoins(amount)) { - SPDLOG_ERROR("Failed to add coins to account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(amount)) { + SPDLOG_ERROR("Failed to add coins to account: [{}]", account.getID()); return; } diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 1d5c8587e50..5aaae6a01cc 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -33,12 +33,12 @@ extern Game g_game; extern Monsters g_monsters; bool IOLoginData::authenticateAccountPassword(const std::string& email, const std::string& password, account::Account *account) { - if (account && account::ERROR_NO != account->LoadAccountDB(email)) { + if (account && account::ERROR_NO != account->loadAccountDB(email)) { SPDLOG_ERROR("Email [{}] doesn't match any account.", email); return false; } - std::string accountPassword = account->GetPassword(); + std::string accountPassword = account->getPassword(); if (transformToSHA1(password) != accountPassword) { SPDLOG_ERROR("Password [{}] doesn't match any account", transformToSHA1(password)); @@ -59,13 +59,13 @@ bool IOLoginData::gameWorldAuthentication(const std::string& email, account::Player player; int result; - if (auto [player, result] = account.GetAccountPlayer(characterName); + if (auto [player, result] = account.getAccountPlayer(characterName); account::ERROR_NO != result) { SPDLOG_ERROR("Player [{}] not found or deleted for account.", characterName); return false; } - *accountId = account.GetID(); + *accountId = account.getID(); return true; } @@ -304,29 +304,29 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) uint32_t accountId = result->getNumber("account_id"); account::Account account(accountId); - account.SetDatabaseInterface(&db); - if(account::ERROR_NO != account.LoadAccountDB()) + account.setDatabaseInterface(&db); + if(account::ERROR_NO != account.loadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return false; } player->setGUID(result->getNumber("id")); player->name = result->getString("name"); - player->accountNumber = account.GetID(); - player->accountType = account.GetAccountType(); + player->accountNumber = account.getID(); + player->accountType = account.getAccountType(); int res = 0; uint32_t coins, tournament_coins; - if (auto [ coins, res ] = account.GetCoins(); account::ERROR_NO != res) { + if (auto [ coins, res ] = account.getCoins(); account::ERROR_NO != res) { SPDLOG_ERROR("Failed to load Player [{}] coins. (Error: [{}])", player->name, res); return false; } player->coinBalance = coins; - if (auto [ tournament_coins, res ] = account.GetTournamentCoins(); + if (auto [ tournament_coins, res ] = account.getTournamentCoins(); account::ERROR_NO != res) { SPDLOG_ERROR("Failed to load Player [{}] tournament coins. (Error: [{}])", player->name, res); @@ -337,7 +337,7 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) if (g_config.getBoolean(FREE_PREMIUM)) { player->premiumDays = std::numeric_limits::max(); } else { - player->premiumDays = account.GetPremiumRemaningDays(); + player->premiumDays = account.getPremiumRemaningDays(); } player->preyBonusRerolls = result->getNumber("bonus_rerolls"); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index e91715fa263..df132add141 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -2318,14 +2318,14 @@ int PlayerFunctions::luaPlayerAddCoins(lua_State* L) { int32_t addCoins = std::min(std::numeric_limits::max() - player->getCoinBalance(), coins); if (addCoins > 0) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } - if(account::ERROR_NO != account.AddCoins(addCoins)) { - SPDLOG_ERROR("Failed to add coins to Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.addCoins(addCoins)) { + SPDLOG_ERROR("Failed to add coins to Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } @@ -2356,14 +2356,14 @@ int PlayerFunctions::luaPlayerRemoveCoins(lua_State* L) { if (player->getCoinBalance() != std::numeric_limits::max()) { if (removeCoins > 0) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } - if(account::ERROR_NO != account.RemoveCoins(removeCoins)) { - SPDLOG_ERROR("Failed to remove coins from Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.removeCoins(removeCoins)) { + SPDLOG_ERROR("Failed to remove coins from Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } @@ -2405,15 +2405,15 @@ int PlayerFunctions::luaPlayerAddTournamentCoins(lua_State* L) { int32_t addTournamentCoins = std::min(std::numeric_limits::max() - player->getTournamentCoinBalance(), tournamentCoins); if (addTournamentCoins > 0) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } - if(account::ERROR_NO != account.AddTournamentCoins(addTournamentCoins)) { + if(account::ERROR_NO != account.addTournamentCoins(addTournamentCoins)) { SPDLOG_ERROR("Failed to add tournament coins to Account: [{}]", - account.GetID()); + account.getID()); pushBoolean(L, false); return 1; } @@ -2443,16 +2443,16 @@ int PlayerFunctions::luaPlayerRemoveTournamentCoins(lua_State* L) { if (player->getTournamentCoinBalance() != std::numeric_limits::max()) { if (removeTournamentCoins > 0) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); pushBoolean(L, false); return 1; } - account.RemoveTournamentCoins(removeTournamentCoins); - if(account::ERROR_NO != account.RemoveTournamentCoins(removeTournamentCoins)) { + account.removeTournamentCoins(removeTournamentCoins); + if(account::ERROR_NO != account.removeTournamentCoins(removeTournamentCoins)) { SPDLOG_ERROR("Failed to add tournament coins to Account: [{}]", - account.GetID()); + account.getID()); pushBoolean(L, false); return 1; } diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 672cd5faf9c..dc47c1aaed7 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -4048,27 +4048,27 @@ void ProtocolGame::updateCoinBalance() if (player != nullptr) { account::Account account(player->getAccount()); - if(account::ERROR_NO != account.LoadAccountDB()) { - SPDLOG_ERROR("Failed to load Account: [{}]", account.GetID()); + if(account::ERROR_NO != account.loadAccountDB()) { + SPDLOG_ERROR("Failed to load Account: [{}]", account.getID()); return; } // Update coin balance int coins = 0; - if(auto [ coins, result ] = account.GetCoins() ; account::ERROR_NO == result) { + if(auto [ coins, result ] = account.getCoins() ; account::ERROR_NO == result) { player->coinBalance = coins; } else { - SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.GetID()); + SPDLOG_ERROR("Failed to get Coins for account: [{}]", account.getID()); } // Update tournament coin balance int tournament_coins = 0; - if(auto [ tournament_coins, result ] = account.GetCoins() ; + if(auto [ tournament_coins, result ] = account.getCoins() ; account::ERROR_NO == result) { player->tournamentCoinBalance = tournament_coins; } else { SPDLOG_ERROR("Failed to get Tournament Coins for account: [{}]", - account.GetID()); + account.getID()); } player->sendCoinBalance(); @@ -6753,12 +6753,12 @@ void ProtocolGame::openStore() // First sending categories without subcategories std::vector categories = g_store.getStoreCategories(); for (auto it = categories.begin(), end = categories.end(); it != end; ++it) { - msg.addString((*it).name); + msg.addString((*it).m_name); msg.addByte(OFFER_STATE_NONE); msg.addByte(1); - msg.addString((*it).icon); + msg.addString((*it).m_icon); msg.add(0x00); } diff --git a/src/server/network/protocol/protocollogin.cpp b/src/server/network/protocol/protocollogin.cpp index fff778f9838..77fb1010acd 100644 --- a/src/server/network/protocol/protocollogin.cpp +++ b/src/server/network/protocol/protocollogin.cpp @@ -77,10 +77,10 @@ void ProtocolLogin::getCharacterList(const std::string& email, const std::string // Add char list std::vector players; - if (auto [ players, result ] = account.GetAccountPlayers(); + if (auto [ players, result ] = account.getAccountPlayers(); account::ERROR_NO != result) { SPDLOG_ERROR("Failed to load Account [{}] players. (Error: [{}])", - account.GetID(), result); + account.getID(), result); } output->addByte(0x64); @@ -109,7 +109,7 @@ void ProtocolLogin::getCharacterList(const std::string& email, const std::string output->add(0); } else { uint32_t days; - days = account.GetPremiumRemaningDays(); + days = account.getPremiumRemaningDays(); output->addByte(0); output->add(time(nullptr) + (days * 86400)); } diff --git a/tests/account_test.cpp b/tests/account_test.cpp index 751fc44686a..c1853f8c7c4 100644 --- a/tests/account_test.cpp +++ b/tests/account_test.cpp @@ -12,31 +12,31 @@ TEST_CASE("Default Constructor", "[UnitTest]") { SECTION("Default ID") { uint32_t id; - normal.GetID(&id); + normal.getID(&id); CHECK(id == 0); } SECTION("@DefaultEmail") { std::string email; - normal.GetEmail(&email); + normal.getEmail(&email); CHECK(email.empty() == true); } SECTION("Default Password") { std::string password; - normal.GetPassword(&password); + normal.getPassword(&password); CHECK(password.empty() == true); } SECTION("Default Premium Remaining Days") { uint32_t days; - normal.GetPremiumRemaningDays(&days); + normal.getPremiumRemaningDays(&days); CHECK(days == 0); } SECTION("Default Premium Remaining Days") { time_t time; - normal.GetPremiumLastDay(&time); + normal.getPremiumLastDay(&time); CHECK(time == 0); } } @@ -44,14 +44,14 @@ TEST_CASE("Default Constructor", "[UnitTest]") { TEST_CASE("Constructor ID", "[UnitTest]") { account::Account with_id(14); uint32_t id; - with_id.GetID(&id); + with_id.getID(&id); CHECK(id == 14); } TEST_CASE("Constructor Email", "[UnitTest]") { account::Account with_email("@test"); std::string email; - with_email.GetEmail(&email); + with_email.getEmail(&email); CHECK(email == "@test"); } @@ -59,14 +59,14 @@ TEST_CASE("Set Database Interface", "[UnitTest]") { account::Account account; error_t result; Database new_database; - result = account.SetDatabaseInterface(&new_database); + result = account.setDatabaseInterface(&new_database); CHECK(result == account::ERROR_NO); } TEST_CASE("Set Database Interface to Nullptr Must Fail", "[UnitTest]") { account::Account account; error_t result; - result = account.SetDatabaseInterface(nullptr); + result = account.setDatabaseInterface(nullptr); CHECK(result == account::ERROR_NULLPTR); } @@ -74,14 +74,14 @@ TEST_CASE("Set Database Task Interface", "[UnitTest]") { account::Account account; error_t result; DatabaseTasks new_database_tasks; - result = account.SetDatabaseTasksInterface(&new_database_tasks); + result = account.setDatabaseTasksInterface(&new_database_tasks); CHECK(result == account::ERROR_NO); } TEST_CASE("Set Database Task Interface to Nullptr Must Fail", "[UnitTest]") { account::Account account; error_t result; - result = account.SetDatabaseTasksInterface(nullptr); + result = account.setDatabaseTasksInterface(nullptr); CHECK(result == account::ERROR_NULLPTR); } @@ -97,7 +97,7 @@ TEST_CASE("Get ID", "[UnitTest]") { account::Account account(15); error_t result; uint32_t new_id; - result = account.GetID(&new_id); + result = account.getID(&new_id); REQUIRE(result == account::ERROR_NO); REQUIRE(new_id == 15); } @@ -105,18 +105,18 @@ TEST_CASE("Get ID", "[UnitTest]") { TEST_CASE("Get ID - Nullptr", "[UnitTest]") { account::Account account(15); error_t result; - result = account.GetID(nullptr); + result = account.getID(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } TEST_CASE("Set/Get Email", "[UnitTest]") { account::Account account; error_t result; - result = account.SetEmail("@RickMaru"); + result = account.setEmail("@RickMaru"); REQUIRE(result == account::ERROR_NO); std::string new_email; - result = account.GetEmail(&new_email); + result = account.getEmail(&new_email); REQUIRE(result == account::ERROR_NO); REQUIRE(new_email == "@RickMaru"); } @@ -125,25 +125,25 @@ TEST_CASE("Set Email - Empty", "[UnitTest]") { account::Account account; error_t result; std::string new_email; - result = account.SetEmail(new_email); + result = account.setEmail(new_email); REQUIRE(result == account::ERROR_INVALID_ACCOUNT_EMAIL); } TEST_CASE("Get Email - Nullptr", "[UnitTest]") { account::Account account; error_t result; - result = account.GetEmail(nullptr); + result = account.getEmail(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } TEST_CASE("Set/Get Password", "[UnitTest]") { account::Account account; error_t result; - result = account.SetPassword("password123"); + result = account.setPassword("password123"); REQUIRE(result == account::ERROR_NO); std::string new_password; - result = account.GetPassword(&new_password); + result = account.getPassword(&new_password); REQUIRE(result == account::ERROR_NO); REQUIRE(new_password == "password123"); } @@ -152,7 +152,7 @@ TEST_CASE("Set Password - Empty", "[UnitTest]") { account::Account account; error_t result; std::string new_password; - result = account.SetPassword(new_password); + result = account.setPassword(new_password); REQUIRE(result == account::ERROR_INVALID_ACC_PASSWORD); } @@ -160,7 +160,7 @@ TEST_CASE("Get Password - Nullptr", "[UnitTest]") { account::Account account; error_t result; std::string new_password; - result = account.GetPassword(nullptr); + result = account.getPassword(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } @@ -168,11 +168,11 @@ TEST_CASE("Get Password - Nullptr", "[UnitTest]") { TEST_CASE("Set/Get Premium Days Remaining", "[UnitTest]") { account::Account account; error_t result; - result = account.SetPremiumRemaningDays(20); + result = account.setPremiumRemaningDays(20); REQUIRE(result == account::ERROR_NO); uint32_t new_days; - result = account.GetPremiumRemaningDays(&new_days); + result = account.getPremiumRemaningDays(&new_days); REQUIRE(result == account::ERROR_NO); REQUIRE(new_days == 20); } @@ -180,7 +180,7 @@ TEST_CASE("Set/Get Premium Days Remaining", "[UnitTest]") { TEST_CASE("Get Premium Days Remaining - Nullptr", "[UnitTest]") { account::Account account; error_t result; - result = account.GetPremiumRemaningDays(nullptr); + result = account.getPremiumRemaningDays(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } @@ -188,11 +188,11 @@ TEST_CASE("Set/Get Premium Last Day", "[UnitTest]") { account::Account account; error_t result; time_t last_day = time(nullptr); - result = account.SetPremiumLastDay(last_day); + result = account.setPremiumLastDay(last_day); REQUIRE(result == account::ERROR_NO); time_t new_last_day; - result = account.GetPremiumLastDay(&new_last_day); + result = account.getPremiumLastDay(&new_last_day); REQUIRE(result == account::ERROR_NO); REQUIRE(new_last_day == last_day); } @@ -200,14 +200,14 @@ TEST_CASE("Set/Get Premium Last Day", "[UnitTest]") { TEST_CASE("Set Premium Last Day - Zero", "[UnitTest]") { account::Account account; error_t result; - result = account.SetPremiumLastDay(-1); + result = account.setPremiumLastDay(-1); REQUIRE(result == account::ERROR_INVALID_LAST_DAY); } TEST_CASE("Get Premium Last Day - Nullptr", "[UnitTest]") { account::Account account; error_t result; - result = account.GetPremiumLastDay(nullptr); + result = account.getPremiumLastDay(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } @@ -215,11 +215,11 @@ TEST_CASE("Get Premium Last Day - Nullptr", "[UnitTest]") { TEST_CASE("Set/Get Account Type", "[UnitTest]") { account::Account account; error_t result; - result = account.SetAccountType(account::ACCOUNT_TYPE_NORMAL); + result = account.setAccountType(account::ACCOUNT_TYPE_NORMAL); REQUIRE(result == account::ERROR_NO); account::AccountType new_account_type; - result = account.GetAccountType(&new_account_type); + result = account.getAccountType(&new_account_type); REQUIRE(result == account::ERROR_NO); REQUIRE(new_account_type == account::ACCOUNT_TYPE_NORMAL); } @@ -227,21 +227,21 @@ TEST_CASE("Set/Get Account Type", "[UnitTest]") { TEST_CASE("Set Account Type - Undefine", "[UnitTest]") { account::Account account; error_t result; - result = account.SetAccountType(static_cast(20)); + result = account.setAccountType(static_cast(20)); REQUIRE(result == account::ERROR_INVALID_ACC_TYPE); } TEST_CASE("Get Account Type - Nullptr", "[UnitTest]") { account::Account account; error_t result; - result = account.GetAccountType(nullptr); + result = account.getAccountType(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } TEST_CASE("Get Account Players - Nullptr", "[UnitTest]") { account::Account account(1); error_t result; - result = account.GetAccountPlayers(nullptr); + result = account.getAccountPlayers(nullptr); REQUIRE(result == account::ERROR_NULLPTR); } @@ -290,7 +290,7 @@ TEST_CASE("Get Account Players", "[UnitTest]") { error_t result; std::vector players; - result = account.GetAccountPlayers(&players); + result = account.getAccountPlayers(&players); REQUIRE(result == account::ERROR_NO); REQUIRE(players.size() >= 1); } @@ -502,32 +502,32 @@ TEST_CASE("Load Account Using ID From Constructor", "[IntegrationTest]") { REQUIRE(result == account::ERROR_NO); uint32_t id; - result = account.GetID(&id); + result = account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); std::string email; - result = account.GetEmail(&email); + result = account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == "@GOD"); std::string password; - result = account.GetPassword(&password); + result = account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == "21298df8a3277357ee55b01df9530b535cf08ec1"); uint32_t premium_remaining_days; - result = account.GetPremiumRemaningDays(&premium_remaining_days); + result = account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == 0); time_t premium_last_day; - result = account.GetPremiumLastDay(&premium_last_day); + result = account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == 0); account::AccountType account_type; - result = account.GetAccountType(&account_type); + result = account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == account::ACCOUNT_TYPE_GOD); } @@ -554,32 +554,32 @@ TEST_CASE("Load Account Using Email From Constructor", "[IntegrationTest]") { REQUIRE(result == account::ERROR_NO); uint32_t id; - result = account.GetID(&id); + result = account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); std::string email; - result = account.GetEmail(&email); + result = account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == "@GOD"); std::string password; - result = account.GetPassword(&password); + result = account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == "21298df8a3277357ee55b01df9530b535cf08ec1"); uint32_t premium_remaining_days; - result = account.GetPremiumRemaningDays(&premium_remaining_days); + result = account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == 0); time_t premium_last_day; - result = account.GetPremiumLastDay(&premium_last_day); + result = account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == 0); account::AccountType account_type; - result = account.GetAccountType(&account_type); + result = account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == account::ACCOUNT_TYPE_GOD); } @@ -606,32 +606,32 @@ TEST_CASE("Load Account Using ID", "[IntegrationTest]") { REQUIRE(result == account::ERROR_NO); uint32_t id; - result = account.GetID(&id); + result = account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); std::string email; - result = account.GetEmail(&email); + result = account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == "@GOD"); std::string password; - result = account.GetPassword(&password); + result = account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == "21298df8a3277357ee55b01df9530b535cf08ec1"); uint32_t premium_remaining_days; - result = account.GetPremiumRemaningDays(&premium_remaining_days); + result = account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == 0); time_t premium_last_day; - result = account.GetPremiumLastDay(&premium_last_day); + result = account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == 0); account::AccountType account_type; - result = account.GetAccountType(&account_type); + result = account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == account::ACCOUNT_TYPE_GOD); } @@ -658,32 +658,32 @@ TEST_CASE("Load Account Using Email", "[IntegrationTest]") { REQUIRE(result == account::ERROR_NO); uint32_t id; - result = account.GetID(&id); + result = account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); std::string email; - result = account.GetEmail(&email); + result = account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == "@GOD"); std::string password; - result = account.GetPassword(&password); + result = account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == "21298df8a3277357ee55b01df9530b535cf08ec1"); uint32_t premium_remaining_days; - result = account.GetPremiumRemaningDays(&premium_remaining_days); + result = account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == 0); time_t premium_last_day; - result = account.GetPremiumLastDay(&premium_last_day); + result = account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == 0); account::AccountType account_type; - result = account.GetAccountType(&account_type); + result = account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == account::ACCOUNT_TYPE_GOD); } @@ -714,60 +714,60 @@ TEST_CASE("Save Account", "[IntegrationTest]") { // Check account uint32_t id; - result = account.GetID(&id); + result = account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); std::string email; - result = account.GetEmail(&email); + result = account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == "@GOD"); std::string password; - result = account.GetPassword(&password); + result = account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == "21298df8a3277357ee55b01df9530b535cf08ec1"); uint32_t premium_remaining_days; - result = account.GetPremiumRemaningDays(&premium_remaining_days); + result = account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == 0); time_t premium_last_day; - result = account.GetPremiumLastDay(&premium_last_day); + result = account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == 0); account::AccountType account_type; - result = account.GetAccountType(&account_type); + result = account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == account::ACCOUNT_TYPE_GOD); // Change Account std::string new_email("@NewEmail"); - result = account.SetEmail(new_email); + result = account.setEmail(new_email); CHECK(result == account::ERROR_NO); std::string new_password("123456789"); - result = account.SetPassword(new_password); + result = account.setPassword(new_password); CHECK(result == account::ERROR_NO); uint32_t new_premium_remaining_days = 10; - result = account.SetPremiumRemaningDays(new_premium_remaining_days); + result = account.setPremiumRemaningDays(new_premium_remaining_days); CHECK(result == account::ERROR_NO); time_t new_premium_last_day = time(nullptr); - result = account.SetPremiumLastDay(new_premium_last_day); + result = account.setPremiumLastDay(new_premium_last_day); CHECK(result == account::ERROR_NO); account::AccountType new_account_type = account::ACCOUNT_TYPE_NORMAL; - result = account.SetAccountType(new_account_type); + result = account.setAccountType(new_account_type); CHECK(result == account::ERROR_NO); //Save Account - result = account.SaveAccountDB(); + result = account.saveAccountDB(); REQUIRE(result == account::ERROR_NO); //Load Changed Account @@ -775,32 +775,32 @@ TEST_CASE("Save Account", "[IntegrationTest]") { result = changed_account.loadAccountDB(1); //Check Changed Account - result = changed_account.GetID(&id); + result = changed_account.getID(&id); CHECK(result == account::ERROR_NO); CHECK(id == 1); - result = changed_account.GetEmail(&email); + result = changed_account.getEmail(&email); CHECK(result == account::ERROR_NO); CHECK(email == new_email); - result = changed_account.GetPassword(&password); + result = changed_account.getPassword(&password); CHECK(result == account::ERROR_NO); CHECK(password == new_password); - result = changed_account.GetPremiumRemaningDays(&premium_remaining_days); + result = changed_account.getPremiumRemaningDays(&premium_remaining_days); CHECK(result == account::ERROR_NO); CHECK(premium_remaining_days == new_premium_remaining_days); - result = changed_account.GetPremiumLastDay(&premium_last_day); + result = changed_account.getPremiumLastDay(&premium_last_day); CHECK(result == account::ERROR_NO); CHECK(premium_last_day == new_premium_last_day); - result = changed_account.GetAccountType(&account_type); + result = changed_account.getAccountType(&account_type); CHECK(result == account::ERROR_NO); CHECK(account_type == new_account_type); //Restore Account Values - result = account_orig.SaveAccountDB(); + result = account_orig.saveAccountDB(); REQUIRE(result == account::ERROR_NO); } @@ -822,23 +822,23 @@ TEST_CASE("Register Coin Transaction", "[IntegrationTest]") { } error_t result; - result = account.RegisterCoinsTransaction(account::COIN_ADD, 50, + result = account.registerCoinsTransaction(account::COIN_ADD, 50, "Test Register Add Coin 1"); CHECK(result == account::ERROR_NO); - result = account.RegisterCoinsTransaction(account::COIN_ADD, 100, + result = account.registerCoinsTransaction(account::COIN_ADD, 100, "Test Register Add Coin 2"); CHECK(result == account::ERROR_NO); - result = account.RegisterCoinsTransaction(account::COIN_REMOVE, 250, + result = account.registerCoinsTransaction(account::COIN_REMOVE, 250, "Test Register Remove Coin 3"); CHECK(result == account::ERROR_NO); - result = account.RegisterCoinsTransaction(account::COIN_REMOVE, 500, + result = account.registerCoinsTransaction(account::COIN_REMOVE, 500, "Test Register Remove Coin 4"); CHECK(result == account::ERROR_NO); - result = account.RegisterCoinsTransaction(account::COIN_ADD, 1000, + result = account.registerCoinsTransaction(account::COIN_ADD, 1000, "Test Register Add Coin 5"); CHECK(result == account::ERROR_NO); } From 73290dc278a93ba94e65b20d3647b55c33c81df2 Mon Sep 17 00:00:00 2001 From: Beats-Dh Date: Wed, 19 Jan 2022 12:14:30 -0400 Subject: [PATCH 28/30] init storegame.lua > store.xml Co-Authored-By: guispiller <11639062+guispiller@users.noreply.github.com> --- data/XML/store.xml | 929 ++++++++++++++++++++++++--------------------- 1 file changed, 490 insertions(+), 439 deletions(-) diff --git a/data/XML/store.xml b/data/XML/store.xml index 141d2db3a4d..be6d5db95c7 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -3,15 +3,11 @@ - - - - - - - + - + + + @@ -24,480 +20,535 @@ - - - - + + + + + + - - - - - - - - + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - + + + - - - - - - - - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + From 39af790445836c84eb99c07a86d61446da51e3f7 Mon Sep 17 00:00:00 2001 From: Beats-Dh Date: Thu, 20 Jan 2022 00:29:23 -0400 Subject: [PATCH 29/30] fixs store.xml --- data/XML/store.xml | 114 ++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/data/XML/store.xml b/data/XML/store.xml index be6d5db95c7..4139e4d4681 100644 --- a/data/XML/store.xml +++ b/data/XML/store.xml @@ -29,39 +29,47 @@ - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - + + + + + + - - - - - - - + + - + + + + + + @@ -77,24 +85,24 @@ - - - - - - - - - - - + + + + + + + + + + + - - - - - - + + + + + + @@ -306,7 +314,7 @@ - + @@ -429,7 +437,7 @@ - + @@ -509,24 +517,24 @@ - - - + + + - + - - + + - - - - + + + + - - + + From fea3ee0a4451340ca8d9c76b84fef9538ecac21a Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Thu, 20 Jan 2022 12:41:39 -0300 Subject: [PATCH 30/30] Remove whitespace --- src/lua/functions/items/item_functions.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lua/functions/items/item_functions.cpp b/src/lua/functions/items/item_functions.cpp index 94190640f61..18e01736adb 100644 --- a/src/lua/functions/items/item_functions.cpp +++ b/src/lua/functions/items/item_functions.cpp @@ -26,7 +26,6 @@ #include "lua/functions/items/item_functions.hpp" #include "items/decay/decay.h" - class Imbuement; // Item