diff --git a/.github/workflows/mysql-schema-check.yml b/.github/workflows/mysql-schema-check.yml
new file mode 100644
index 00000000000..b0291956edc
--- /dev/null
+++ b/.github/workflows/mysql-schema-check.yml
@@ -0,0 +1,43 @@
+---
+name: MySQL Schema Check
+on:
+ workflow_dispatch:
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review]
+ paths:
+ - "schema.sql"
+ merge_group:
+ push:
+ paths:
+ - "schema.sql"
+ branches:
+ - main
+
+jobs:
+ mysql-schema-check:
+ runs-on: ubuntu-latest
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: canary
+ MYSQL_USER: canary
+ MYSQL_PASSWORD: canary
+ ports:
+ - 3306/tcp
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+ strategy:
+ fail-fast: false
+ name: Check
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@main
+ - name: 📌 MySQL Start & init & show db
+ run: |
+ sudo /etc/init.d/mysql start
+ mysql -e 'CREATE DATABASE canary;' -uroot -proot
+ mysql -e "SHOW DATABASES" -uroot -proot
+ - name: Import Canary Schema
+ run: |
+ mysql -uroot -proot canary < schema.sql
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 52c35a471f7..07701451aad 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.22 FATAL_ERROR)
# VCPKG
# cmake -DCMAKE_TOOLCHAIN_FILE=/opt/workspace/vcpkg/scripts/buildsystems/vcpkg.cmake ..
# Needed libs is in file vcpkg.json
-# Windows required libs: .\vcpkg install --triplet x64-windows asio pugixml spdlog curl protobuf parallel-hashmap magic-enum mio luajit libmariadb mpir abseil
+# Windows required libs: .\vcpkg install --triplet x64-windows asio pugixml spdlog curl protobuf parallel-hashmap magic-enum mio luajit libmariadb mpir abseil bshoshany-thread-pool
if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
CACHE STRING "")
@@ -124,4 +124,4 @@ add_subdirectory(src)
if(BUILD_TESTS)
add_subdirectory(tests)
-endif()
\ No newline at end of file
+endif()
diff --git a/config.lua.dist b/config.lua.dist
index ed2561d4532..9d1ed2fa681 100644
--- a/config.lua.dist
+++ b/config.lua.dist
@@ -377,7 +377,10 @@ partyListMaxDistance = 30
toggleMapCustom = true
-- Market
+-- NOTE: marketRefreshPricesInterval (in minutes, minimum is 1 minute)
+-- NOTE: set it to 0 for disable, is the time in which the task will run updating the prices of the items that will be sent to the client
marketOfferDuration = 30 * 24 * 60 * 60
+marketRefreshPricesInterval = 30
premiumToCreateMarketOffer = true
checkExpiredMarketOffersEachMinutes = 60
maxMarketOffersAtATimePerPlayer = 100
@@ -508,6 +511,7 @@ bossDefaultTimeToFightAgain = 20 * 60 * 60 -- 20 hours
bossDefaultTimeToDefeat = 20 * 60 -- 20 minutes
-- Monsters
+defaultRespawnTime = 60
deSpawnRange = 2
deSpawnRadius = 50
diff --git a/data-otservbr-global/migrations/45.lua b/data-otservbr-global/migrations/45.lua
index 86a6d8ffec1..4ceb5f7e3fd 100644
--- a/data-otservbr-global/migrations/45.lua
+++ b/data-otservbr-global/migrations/45.lua
@@ -1,3 +1,56 @@
function onUpdateDatabase()
- return false -- true = There are others migrations file | false = this is the last migration file
+ logger.info("Updating database to version 46 (feat: vip groups)")
+
+ db.query([[
+ CREATE TABLE IF NOT EXISTS `account_vipgroups` (
+ `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `account_id` int(11) UNSIGNED NOT NULL COMMENT 'id of account whose vip group entry it is',
+ `name` varchar(128) NOT NULL,
+ `customizable` BOOLEAN NOT NULL DEFAULT '1',
+ CONSTRAINT `account_vipgroups_pk` PRIMARY KEY (`id`, `account_id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+ ]])
+
+ db.query([[
+ CREATE TRIGGER `oncreate_accounts` AFTER INSERT ON `accounts` FOR EACH ROW BEGIN
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Enemies', 0);
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Friends', 0);
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Trading Partner', 0);
+ END;
+ ]])
+
+ db.query([[
+ CREATE TABLE IF NOT EXISTS `account_vipgrouplist` (
+ `account_id` int(11) UNSIGNED NOT NULL COMMENT 'id of account whose viplist entry it is',
+ `player_id` int(11) NOT NULL COMMENT 'id of target player of viplist entry',
+ `vipgroup_id` int(11) UNSIGNED NOT NULL COMMENT 'id of vip group that player belongs',
+ INDEX `account_id` (`account_id`),
+ INDEX `player_id` (`player_id`),
+ INDEX `vipgroup_id` (`vipgroup_id`),
+ CONSTRAINT `account_vipgrouplist_unique` UNIQUE (`account_id`, `player_id`, `vipgroup_id`),
+ CONSTRAINT `account_vipgrouplist_player_fk`
+ FOREIGN KEY (`player_id`) REFERENCES `players` (`id`)
+ ON DELETE CASCADE,
+ CONSTRAINT `account_vipgrouplist_vipgroup_fk`
+ FOREIGN KEY (`vipgroup_id`, `account_id`) REFERENCES `account_vipgroups` (`id`, `account_id`)
+ ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+ ]])
+
+ db.query([[
+ INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`)
+ SELECT 1, id, 'Friends', 0 FROM `accounts`;
+ ]])
+
+ db.query([[
+ INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`)
+ SELECT 2, id, 'Enemies', 0 FROM `accounts`;
+ ]])
+
+ db.query([[
+ INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`)
+ SELECT 3, id, 'Trading Partners', 0 FROM `accounts`;
+ ]])
+
+ return true
end
diff --git a/data-otservbr-global/migrations/46.lua b/data-otservbr-global/migrations/46.lua
new file mode 100644
index 00000000000..86a6d8ffec1
--- /dev/null
+++ b/data-otservbr-global/migrations/46.lua
@@ -0,0 +1,3 @@
+function onUpdateDatabase()
+ return false -- true = There are others migrations file | false = this is the last migration file
+end
diff --git a/data-otservbr-global/npc/alaistar.lua b/data-otservbr-global/npc/alaistar.lua
index e893975b24b..68aaa75679a 100644
--- a/data-otservbr-global/npc/alaistar.lua
+++ b/data-otservbr-global/npc/alaistar.lua
@@ -36,13 +36,14 @@ local itemsTable = {
{ itemName = "strong health potion", clientId = 236, buy = 115 },
{ itemName = "strong mana potion", clientId = 237, buy = 93 },
{ itemName = "supreme health potion", clientId = 23375, buy = 625 },
- { itemName = "ultimate health potion", clientId = 7643, buy = 438 },
- { itemName = "ultimate mana potion", clientId = 23373, buy = 379 },
+ { itemName = "ultimate health potion", clientId = 7643, buy = 379 },
+ { itemName = "ultimate mana potion", clientId = 23373, buy = 438 },
{ itemName = "ultimate spirit potion", clientId = 23374, buy = 438 },
{ itemName = "vial", clientId = 2874, sell = 5 },
},
["creature products"] = {
{ itemName = "cowbell", clientId = 21204, sell = 210 },
+ { itemName = "execowtioner mask", clientId = 21201, sell = 240 },
{ itemName = "giant pacifier", clientId = 21199, sell = 170 },
{ itemName = "glob of glooth", clientId = 21182, sell = 125 },
{ itemName = "glooth injection tube", clientId = 21103, sell = 350 },
diff --git a/data-otservbr-global/npc/alexander.lua b/data-otservbr-global/npc/alexander.lua
index 78f51e486c8..fda2799bf4d 100644
--- a/data-otservbr-global/npc/alexander.lua
+++ b/data-otservbr-global/npc/alexander.lua
@@ -30,6 +30,28 @@ npcConfig.voices = {
}
local itemsTable = {
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["creature products"] = {
+ { itemName = "crystal ball", clientId = 3076, buy = 530, sell = 190 },
+ { itemName = "life crystal", clientId = 3061, sell = 83 },
+ { itemName = "mind stone", clientId = 3062, sell = 170 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook of enlightenment", clientId = 8072, sell = 4000 },
+ { itemName = "spellbook of warding", clientId = 8073, sell = 8000 },
+ { itemName = "spellbook of mind control", clientId = 8074, sell = 13000 },
+ { itemName = "spellbook of lost souls", clientId = 8075, sell = 19000 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
["runes"] = {
{ itemName = "animate dead rune", clientId = 3203, buy = 375 },
{ itemName = "blank rune", clientId = 3147, buy = 10 },
diff --git a/data-otservbr-global/npc/an_idol.lua b/data-otservbr-global/npc/an_idol.lua
new file mode 100644
index 00000000000..69977dc81ec
--- /dev/null
+++ b/data-otservbr-global/npc/an_idol.lua
@@ -0,0 +1,68 @@
+local internalNpcName = "An Idol"
+local npcType = Game.createNpcType(internalNpcName)
+local npcConfig = {}
+
+npcConfig.name = internalNpcName
+npcConfig.description = internalNpcName
+
+npcConfig.health = 100
+npcConfig.maxHealth = npcConfig.health
+npcConfig.walkInterval = 0
+npcConfig.walkRadius = 2
+
+npcConfig.outfit = {
+ lookTypeEx = 15894,
+}
+
+npcConfig.flags = {
+ floorchange = false,
+}
+
+local keywordHandler = KeywordHandler:new()
+local npcHandler = NpcHandler:new(keywordHandler)
+
+npcType.onThink = function(npc, interval)
+ npcHandler:onThink(npc, interval)
+end
+
+npcType.onAppear = function(npc, creature)
+ npcHandler:onAppear(npc, creature)
+end
+
+npcType.onDisappear = function(npc, creature)
+ npcHandler:onDisappear(npc, creature)
+end
+
+npcType.onMove = function(npc, creature, fromPosition, toPosition)
+ npcHandler:onMove(npc, creature, fromPosition, toPosition)
+end
+
+npcType.onSay = function(npc, creature, type, message)
+ npcHandler:onSay(npc, creature, type, message)
+end
+
+npcType.onCloseChannel = function(npc, creature)
+ npcHandler:onCloseChannel(npc, creature)
+end
+
+local function creatureSayCallback(npc, creature, type, message)
+ local player = Player(creature)
+
+ if not npcHandler:checkInteraction(npc, creature) then
+ return false
+ end
+
+ if MsgContains(message, "VBOX") then
+ npcHandler:say("J-T B^C J^BXT°", npc, creature)
+ player:teleportTo(Position(32366, 32531, 8), false)
+ player:getPosition():sendMagicEffect(CONST_ME_TELEPORT)
+ end
+
+ return true
+end
+
+npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback)
+npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, false)
+
+-- npcType registering the npcConfig table
+npcType:register(npcConfig)
diff --git a/data-otservbr-global/npc/asima.lua b/data-otservbr-global/npc/asima.lua
index 645689c42d4..c3aca3fcd5e 100644
--- a/data-otservbr-global/npc/asima.lua
+++ b/data-otservbr-global/npc/asima.lua
@@ -24,6 +24,14 @@ npcConfig.flags = {
}
local itemsTable = {
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
["potions"] = {
{ itemName = "empty potion flask", clientId = 283, sell = 5 },
{ itemName = "empty potion flask", clientId = 284, sell = 5 },
@@ -41,6 +49,12 @@ local itemsTable = {
{ itemName = "ultimate spirit potion", clientId = 23374, buy = 438 },
{ itemName = "vial", clientId = 2874, sell = 5 },
},
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
["runes"] = {
{ itemName = "avalanche rune", clientId = 3161, buy = 57 },
{ itemName = "blank rune", clientId = 3147, buy = 10 },
@@ -61,8 +75,27 @@ local itemsTable = {
{ itemName = "poison field rune", clientId = 3172, buy = 21 },
{ itemName = "poison wall rune", clientId = 3176, buy = 52 },
{ itemName = "sudden death rune", clientId = 3155, buy = 135 },
+ { itemName = "stalagmite rune", clientId = 3179, buy = 12 },
{ itemName = "ultimate healing rune", clientId = 3160, buy = 175 },
},
+ ["wands"] = {
+ { itemName = "hailstorm rod", clientId = 3067, buy = 15000 },
+ { itemName = "moonlight rod", clientId = 3070, buy = 1000 },
+ { itemName = "necrotic rod", clientId = 3069, buy = 5000 },
+ { itemName = "northwind rod", clientId = 8083, buy = 7500 },
+ { itemName = "snakebite rod", clientId = 3066, buy = 500 },
+ { itemName = "springsprout rod", clientId = 8084, buy = 18000 },
+ { itemName = "terra rod", clientId = 3065, buy = 10000 },
+ { itemName = "underworld rod", clientId = 8082, buy = 22000 },
+ { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 },
+ { itemName = "wand of decay", clientId = 3072, buy = 5000 },
+ { itemName = "wand of draconia", clientId = 8093, buy = 7500 },
+ { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
+ { itemName = "wand of inferno", clientId = 3071, buy = 15000 },
+ { itemName = "wand of starstorm", clientId = 8092, buy = 18000 },
+ { itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
+ { itemName = "wand of vortex", clientId = 3074, buy = 500 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/briasol.lua b/data-otservbr-global/npc/briasol.lua
index 6905011fee9..38dcd074159 100644
--- a/data-otservbr-global/npc/briasol.lua
+++ b/data-otservbr-global/npc/briasol.lua
@@ -113,7 +113,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/chantalle.lua b/data-otservbr-global/npc/chantalle.lua
index 2615f6557da..3ed42984f5c 100644
--- a/data-otservbr-global/npc/chantalle.lua
+++ b/data-otservbr-global/npc/chantalle.lua
@@ -99,7 +99,7 @@ npcConfig.shop = {
{ itemName = "diamond", clientId = 32770, sell = 15000 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/chuckles.lua b/data-otservbr-global/npc/chuckles.lua
index 51fb3f2add6..4eb06b6bb3d 100644
--- a/data-otservbr-global/npc/chuckles.lua
+++ b/data-otservbr-global/npc/chuckles.lua
@@ -59,6 +59,38 @@ local itemsTable = {
{ itemName = "sudden death rune", clientId = 3155, buy = 135 },
{ itemName = "ultimate healing rune", clientId = 3160, buy = 175 },
},
+ ["wands"] = {
+ { itemName = "hailstorm rod", clientId = 3067, buy = 15000 },
+ { itemName = "moonlight rod", clientId = 3070, buy = 1000 },
+ { itemName = "necrotic rod", clientId = 3069, buy = 5000 },
+ { itemName = "northwind rod", clientId = 8083, buy = 7500 },
+ { itemName = "snakebite rod", clientId = 3066, buy = 500 },
+ { itemName = "springsprout rod", clientId = 8084, buy = 18000 },
+ { itemName = "terra rod", clientId = 3065, buy = 10000 },
+ { itemName = "underworld rod", clientId = 8082, buy = 22000 },
+ { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 },
+ { itemName = "wand of decay", clientId = 3072, buy = 5000 },
+ { itemName = "wand of draconia", clientId = 8093, buy = 7500 },
+ { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
+ { itemName = "wand of inferno", clientId = 3071, buy = 15000 },
+ { itemName = "wand of starstorm", clientId = 8092, buy = 18000 },
+ { itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
+ { itemName = "wand of vortex", clientId = 3074, buy = 500 },
+ },
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/edmund.lua b/data-otservbr-global/npc/edmund.lua
index d8635297949..c61a95bcd72 100644
--- a/data-otservbr-global/npc/edmund.lua
+++ b/data-otservbr-global/npc/edmund.lua
@@ -68,7 +68,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/fenech.lua b/data-otservbr-global/npc/fenech.lua
index 7152c0ef9c5..f69dcafaa89 100644
--- a/data-otservbr-global/npc/fenech.lua
+++ b/data-otservbr-global/npc/fenech.lua
@@ -71,6 +71,20 @@ local itemsTable = {
{ itemName = "sudden death rune", clientId = 3155, buy = 135 },
{ itemName = "ultimate healing rune", clientId = 3160, buy = 175 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/frans.lua b/data-otservbr-global/npc/frans.lua
index 8761a7d89d6..a1b695adf14 100644
--- a/data-otservbr-global/npc/frans.lua
+++ b/data-otservbr-global/npc/frans.lua
@@ -58,6 +58,20 @@ local itemsTable = {
{ itemName = "sudden death rune", clientId = 3155, buy = 135 },
{ itemName = "ultimate healing rune", clientId = 3160, buy = 175 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/frederik.lua b/data-otservbr-global/npc/frederik.lua
index 9b33ccf9684..81ff1ec58b6 100644
--- a/data-otservbr-global/npc/frederik.lua
+++ b/data-otservbr-global/npc/frederik.lua
@@ -82,6 +82,20 @@ local itemsTable = {
{ itemName = "ultimate spirit potion", clientId = 23374, buy = 438 },
{ itemName = "vial", clientId = 2874, sell = 5 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/gail.lua b/data-otservbr-global/npc/gail.lua
index 80a9b54b3e5..de2a52dc7f4 100644
--- a/data-otservbr-global/npc/gail.lua
+++ b/data-otservbr-global/npc/gail.lua
@@ -109,7 +109,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/gnomegica.lua b/data-otservbr-global/npc/gnomegica.lua
index 0805ca33ace..b860caf3962 100644
--- a/data-otservbr-global/npc/gnomegica.lua
+++ b/data-otservbr-global/npc/gnomegica.lua
@@ -78,6 +78,20 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/hanna.lua b/data-otservbr-global/npc/hanna.lua
index 7fca4c908aa..dfea1547135 100644
--- a/data-otservbr-global/npc/hanna.lua
+++ b/data-otservbr-global/npc/hanna.lua
@@ -145,7 +145,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/ishina.lua b/data-otservbr-global/npc/ishina.lua
index 1fd61200c15..358ee2619a3 100644
--- a/data-otservbr-global/npc/ishina.lua
+++ b/data-otservbr-global/npc/ishina.lua
@@ -139,7 +139,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/iwan.lua b/data-otservbr-global/npc/iwan.lua
index 2cc24843318..77689b12003 100644
--- a/data-otservbr-global/npc/iwan.lua
+++ b/data-otservbr-global/npc/iwan.lua
@@ -78,7 +78,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/jessica.lua b/data-otservbr-global/npc/jessica.lua
index 37ac18ed54a..43b1839b3ee 100644
--- a/data-otservbr-global/npc/jessica.lua
+++ b/data-otservbr-global/npc/jessica.lua
@@ -98,7 +98,7 @@ npcConfig.shop = {
{ itemName = "diamond", clientId = 32770, sell = 15000 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/khanna.lua b/data-otservbr-global/npc/khanna.lua
index 02fcee7b6d5..76b5c1e70da 100644
--- a/data-otservbr-global/npc/khanna.lua
+++ b/data-otservbr-global/npc/khanna.lua
@@ -34,7 +34,7 @@ local itemsTable = {
["runes"] = {
{ itemName = "animate dead rune", clientId = 3203, buy = 375 },
{ itemName = "avalanche rune", clientId = 3161, buy = 57 },
- { itemName = "blank rune", clientId = 3147, buy = 10 },
+ { itemName = "blank rune", clientId = 3147, buy = 20 },
{ itemName = "chameleon rune", clientId = 3178, buy = 210 },
{ itemName = "convince creature rune", clientId = 3177, buy = 80 },
{ itemName = "cure poison rune", clientId = 3153, buy = 65 },
@@ -84,6 +84,46 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["creature products"] = {
+ { itemName = "bashmu fang", clientId = 36820, sell = 600 },
+ { itemName = "bashmu feather", clientId = 36820, sell = 350 },
+ { itemName = "bashmu tongue", clientId = 36820, sell = 400 },
+ { itemName = "blue goanna scale", clientId = 31559, sell = 230 },
+ { itemName = "crystal ball", clientId = 3076, buy = 650 },
+ { itemName = "fafnar symbol", clientId = 31443, sell = 950 },
+ { itemName = "goanna claw", clientId = 31561, sell = 950 },
+ { itemName = "goanna meat", clientId = 31560, sell = 190 },
+ { itemName = "lamassu hoof", clientId = 31441, sell = 330 },
+ { itemName = "lamassu horn", clientId = 31442, sell = 240 },
+ { itemName = "life crystal", clientId = 3061, sell = 85 },
+ { itemName = "lizard heart", clientId = 31340, sell = 530 },
+ { itemName = "manticore ear", clientId = 31440, sell = 310 },
+ { itemName = "manticore tail", clientId = 31439, sell = 220 },
+ { itemName = "mind stone", clientId = 3062, sell = 170 },
+ { itemName = "old girtablilu carapace", clientId = 36972, sell = 570 },
+ { itemName = "red goanna scale", clientId = 31558, sell = 270 },
+ { itemName = "scorpion charm", clientId = 36822, sell = 620 },
+ { itemName = "sphinx feather", clientId = 31437, sell = 470 },
+ { itemName = "sphinx tiara", clientId = 31438, sell = 360 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ { itemName = "spellbook of enlightenment", clientId = 8072, sell = 4000 },
+ { itemName = "spellbook of lost souls", clientId = 8075, sell = 19000 },
+ { itemName = "spellbook of mind control", clientId = 8074, sell = 13000 },
+ { itemName = "spellbook of warding", clientId = 8073, sell = 8000 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/mordecai.lua b/data-otservbr-global/npc/mordecai.lua
index 60063a423ba..dc0e3c07a77 100644
--- a/data-otservbr-global/npc/mordecai.lua
+++ b/data-otservbr-global/npc/mordecai.lua
@@ -88,6 +88,17 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/nelly.lua b/data-otservbr-global/npc/nelly.lua
index 8c3b123c47a..911464524c6 100644
--- a/data-otservbr-global/npc/nelly.lua
+++ b/data-otservbr-global/npc/nelly.lua
@@ -94,6 +94,20 @@ local itemsTable = {
{ itemName = "letter", clientId = 3505, buy = 8 },
{ itemName = "parcel", clientId = 3503, buy = 15 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/nipuna.lua b/data-otservbr-global/npc/nipuna.lua
index aa9d74cec52..ef3211bce42 100644
--- a/data-otservbr-global/npc/nipuna.lua
+++ b/data-otservbr-global/npc/nipuna.lua
@@ -101,6 +101,17 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/odemara.lua b/data-otservbr-global/npc/odemara.lua
index bcfe94bf8eb..3e1986714c7 100644
--- a/data-otservbr-global/npc/odemara.lua
+++ b/data-otservbr-global/npc/odemara.lua
@@ -70,7 +70,7 @@ npcConfig.shop = {
{ itemName = "diamond", clientId = 32770, sell = 15000 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/oiriz.lua b/data-otservbr-global/npc/oiriz.lua
index 3a5d3a6b411..4b731c015a2 100644
--- a/data-otservbr-global/npc/oiriz.lua
+++ b/data-otservbr-global/npc/oiriz.lua
@@ -68,7 +68,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/rabaz.lua b/data-otservbr-global/npc/rabaz.lua
index 3e47da6a5ca..e532e7def66 100644
--- a/data-otservbr-global/npc/rabaz.lua
+++ b/data-otservbr-global/npc/rabaz.lua
@@ -82,6 +82,20 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/rachel.lua b/data-otservbr-global/npc/rachel.lua
index 3206c99864a..057787692c9 100644
--- a/data-otservbr-global/npc/rachel.lua
+++ b/data-otservbr-global/npc/rachel.lua
@@ -71,6 +71,23 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["valuables"] = {
+ { itemName = "talon", clientId = 3034, sell = 320 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/romir.lua b/data-otservbr-global/npc/romir.lua
index 11ea038d266..09ab62ab1a4 100644
--- a/data-otservbr-global/npc/romir.lua
+++ b/data-otservbr-global/npc/romir.lua
@@ -82,6 +82,20 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/shiriel.lua b/data-otservbr-global/npc/shiriel.lua
index 546bb26447b..fd982f345b9 100644
--- a/data-otservbr-global/npc/shiriel.lua
+++ b/data-otservbr-global/npc/shiriel.lua
@@ -70,6 +70,20 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/sigurd.lua b/data-otservbr-global/npc/sigurd.lua
index cca63a33e3e..e4d1b585307 100644
--- a/data-otservbr-global/npc/sigurd.lua
+++ b/data-otservbr-global/npc/sigurd.lua
@@ -72,6 +72,20 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/sundara.lua b/data-otservbr-global/npc/sundara.lua
index c1364240fa9..95ff206f2ad 100644
--- a/data-otservbr-global/npc/sundara.lua
+++ b/data-otservbr-global/npc/sundara.lua
@@ -101,6 +101,17 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/tandros.lua b/data-otservbr-global/npc/tandros.lua
index e25ba65f2a3..ae647b26127 100644
--- a/data-otservbr-global/npc/tandros.lua
+++ b/data-otservbr-global/npc/tandros.lua
@@ -88,6 +88,20 @@ local itemsTable = {
{ itemName = "wand of voodoo", clientId = 8094, buy = 22000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/tesha.lua b/data-otservbr-global/npc/tesha.lua
index 95e31f97cb7..6a3c4b9fadb 100644
--- a/data-otservbr-global/npc/tesha.lua
+++ b/data-otservbr-global/npc/tesha.lua
@@ -98,7 +98,7 @@ npcConfig.shop = {
{ itemName = "diamond", clientId = 32770, sell = 15000 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/tezila.lua b/data-otservbr-global/npc/tezila.lua
index dab92c807f5..fe667133ad7 100644
--- a/data-otservbr-global/npc/tezila.lua
+++ b/data-otservbr-global/npc/tezila.lua
@@ -67,7 +67,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/topsy.lua b/data-otservbr-global/npc/topsy.lua
index 395fd23cbeb..66ea6f27e6a 100644
--- a/data-otservbr-global/npc/topsy.lua
+++ b/data-otservbr-global/npc/topsy.lua
@@ -78,6 +78,20 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/valindara.lua b/data-otservbr-global/npc/valindara.lua
index 69655358dfe..cf9506fcdd5 100644
--- a/data-otservbr-global/npc/valindara.lua
+++ b/data-otservbr-global/npc/valindara.lua
@@ -111,7 +111,7 @@ npcConfig.shop = {
{ itemName = "fire wall rune", clientId = 3190, buy = 61 },
{ itemName = "fireball rune", clientId = 3189, buy = 30 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/npc/xodet.lua b/data-otservbr-global/npc/xodet.lua
index 2d8832bd964..f687b491e89 100644
--- a/data-otservbr-global/npc/xodet.lua
+++ b/data-otservbr-global/npc/xodet.lua
@@ -72,6 +72,20 @@ local itemsTable = {
{ itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 },
{ itemName = "wand of vortex", clientId = 3074, buy = 500 },
},
+ ["exercise weapons"] = {
+ { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 },
+ { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 },
+ { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 },
+ { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 },
+ { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 },
+ { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 },
+ },
+ ["others"] = {
+ { itemName = "spellwand", clientId = 651, sell = 299 },
+ },
+ ["shields"] = {
+ { itemName = "spellbook", clientId = 3059, buy = 150 },
+ },
}
npcConfig.shop = {}
diff --git a/data-otservbr-global/npc/yasir.lua b/data-otservbr-global/npc/yasir.lua
index d576b611e53..9c5cf3dbf69 100644
--- a/data-otservbr-global/npc/yasir.lua
+++ b/data-otservbr-global/npc/yasir.lua
@@ -60,6 +60,7 @@ npcConfig.shop = {
{ itemName = "ape fur", clientId = 5883, sell = 120 },
{ itemName = "apron", clientId = 33933, sell = 1300 },
{ itemName = "badger fur", clientId = 903, sell = 15 },
+ { itemName = "bakragore's amalgamation", clientId = 43968, sell = 2000000 },
{ itemName = "bamboo stick", clientId = 11445, sell = 30 },
{ itemName = "banana sash", clientId = 11511, sell = 55 },
{ itemName = "basalt fetish", clientId = 17856, sell = 210 },
@@ -75,6 +76,7 @@ npcConfig.shop = {
{ itemName = "black hood", clientId = 9645, sell = 190 },
{ itemName = "black wool", clientId = 11448, sell = 300 },
{ itemName = "blazing bone", clientId = 16131, sell = 610 },
+ { itemName = "bloated maggot", clientId = 43856, sell = 5200 },
{ itemName = "blood preservation", clientId = 11449, sell = 320 },
{ itemName = "blood tincture in a vial", clientId = 18928, sell = 360 },
{ itemName = "bloody dwarven beard", clientId = 17827, sell = 110 },
@@ -173,7 +175,11 @@ npcConfig.shop = {
{ itemName = "dandelion seeds", clientId = 25695, sell = 200 },
{ itemName = "dangerous proto matter", clientId = 23515, sell = 300 },
{ itemName = "dark bell", clientId = 32596, sell = 310000 },
+ { itemName = "dark obsidian splinter", clientId = 43850, sell = 4400 },
{ itemName = "dark rosary", clientId = 10303, sell = 48 },
+ { itemName = "darklight core", clientId = 43853, sell = 4100 },
+ { itemName = "darklight figurine", clientId = 43961, sell = 3400000 },
+ { itemName = "darklight matter", clientId = 43851, sell = 5500 },
{ itemName = "dead weight", clientId = 20202, sell = 450 },
{ itemName = "deepling breaktime snack", clientId = 14011, sell = 90 },
{ itemName = "deepling claw", clientId = 14044, sell = 430 },
@@ -229,6 +235,7 @@ npcConfig.shop = {
{ itemName = "falcon crest", clientId = 28823, sell = 650 },
{ itemName = "fern", clientId = 3737, sell = 20 },
{ itemName = "fiery heart", clientId = 9636, sell = 375 },
+ { itemName = "fiery tear", clientId = 39040, sell = 1070000 },
{ itemName = "fig leaf", clientId = 25742, sell = 200 },
{ itemName = "figurine of cruelty", clientId = 34019, sell = 3100000 },
{ itemName = "figurine of greed", clientId = 34021, sell = 2900000 },
@@ -480,6 +487,8 @@ npcConfig.shop = {
{ itemName = "rorc feather", clientId = 18993, sell = 70 },
{ itemName = "rotten heart", clientId = 31589, sell = 74000 },
{ itemName = "rotten piece of cloth", clientId = 10291, sell = 30 },
+ { itemName = "rotten roots", clientId = 43849, sell = 3800 },
+ { itemName = "rotten vermin ichor", clientId = 43847, sell = 4500 },
{ itemName = "sabretooth", clientId = 10311, sell = 400 },
{ itemName = "sabretooth fur", clientId = 39378, sell = 2500 },
{ itemName = "safety pin", clientId = 11493, sell = 120 },
@@ -636,6 +645,7 @@ npcConfig.shop = {
{ itemName = "wolf paw", clientId = 5897, sell = 70 },
{ itemName = "wood", clientId = 5901, sell = 5 },
{ itemName = "wool", clientId = 10319, sell = 15 },
+ { itemName = "worm sponge", clientId = 43848, sell = 4200 },
{ itemName = "writhing brain", clientId = 32600, sell = 370000 },
{ itemName = "writhing heart", clientId = 32599, sell = 185000 },
{ itemName = "wyrm scale", clientId = 9665, sell = 400 },
diff --git a/data-otservbr-global/npc/yonan.lua b/data-otservbr-global/npc/yonan.lua
index 44dbb8dd83c..69bddee5cb3 100644
--- a/data-otservbr-global/npc/yonan.lua
+++ b/data-otservbr-global/npc/yonan.lua
@@ -39,7 +39,7 @@ npcConfig.shop = {
{ itemName = "cyan crystal fragment", clientId = 16125, sell = 800 },
{ itemName = "dragon figurine", clientId = 30053, sell = 45000 },
{ itemName = "gemmed figurine", clientId = 24392, sell = 3500 },
- { itemName = "giant amethyst", clientId = 30061, sell = 60000 },
+ { itemName = "giant amethyst", clientId = 32622, sell = 60000 },
{ itemName = "giant emerald", clientId = 30060, sell = 90000 },
{ itemName = "giant ruby", clientId = 30059, sell = 70000 },
{ itemName = "giant sapphire", clientId = 30061, sell = 50000 },
diff --git a/data-otservbr-global/scripts/actions/bosses_levers/the_fear_feaster.lua b/data-otservbr-global/scripts/actions/bosses_levers/the_fear_feaster.lua
index af870accd41..41dfa1ffe33 100644
--- a/data-otservbr-global/scripts/actions/bosses_levers/the_fear_feaster.lua
+++ b/data-otservbr-global/scripts/actions/bosses_levers/the_fear_feaster.lua
@@ -15,7 +15,7 @@ local config = {
from = Position(33705, 31463, 14),
to = Position(33719, 31477, 14),
},
- exit = Position(33609, 31499, 10),
+ exit = Position(33609, 31495, 10),
}
local lever = BossLever(config)
diff --git a/data-otservbr-global/scripts/actions/quests/adventures_of_galthen/galthens_tree.lua b/data-otservbr-global/scripts/actions/quests/adventures_of_galthen/galthens_tree.lua
new file mode 100644
index 00000000000..441b91fa345
--- /dev/null
+++ b/data-otservbr-global/scripts/actions/quests/adventures_of_galthen/galthens_tree.lua
@@ -0,0 +1,19 @@
+local galthensTree = Action()
+function galthensTree.onUse(player, item, fromPosition, target, toPosition, isHotkey)
+ local hasExhaustion, message = player:kv():get("galthens-satchel") or 0, "Empty."
+ if hasExhaustion < os.time() then
+ local container = player:addItem(36813)
+ container:addItem(36810, 1)
+ player:kv():set("galthens-satchel", os.time() + 30 * 24 * 60 * 60)
+ message = "You have found a galthens satchel."
+ end
+
+ player:teleportTo(Position(32396, 32520, 7))
+ player:getPosition():sendMagicEffect(CONST_ME_WATERSPLASH)
+ player:sendTextMessage(MESSAGE_EVENT_ADVANCE, message)
+
+ return true
+end
+
+galthensTree:position(Position(32366, 32542, 8))
+galthensTree:register()
diff --git a/data-otservbr-global/scripts/game_migrations/20241715984279_move_wheel_scrolls_from_storagename_to_kv.lua b/data-otservbr-global/scripts/game_migrations/20241715984279_move_wheel_scrolls_from_storagename_to_kv.lua
new file mode 100644
index 00000000000..a5cc9a123f4
--- /dev/null
+++ b/data-otservbr-global/scripts/game_migrations/20241715984279_move_wheel_scrolls_from_storagename_to_kv.lua
@@ -0,0 +1,24 @@
+local promotionScrolls = {
+ { oldScroll = "wheel.scroll.abridged", newScroll = "abridged" },
+ { oldScroll = "wheel.scroll.basic", newScroll = "basic" },
+ { oldScroll = "wheel.scroll.revised", newScroll = "revised" },
+ { oldScroll = "wheel.scroll.extended", newScroll = "extended" },
+ { oldScroll = "wheel.scroll.advanced", newScroll = "advanced" },
+}
+
+local function migrate(player)
+ for _, scrollTable in ipairs(promotionScrolls) do
+ local oldStorage = player:getStorageValueByName(scrollTable.oldScroll)
+ if oldStorage > 0 then
+ player:kv():scoped("wheel-of-destiny"):scoped("scrolls"):set(scrollTable.newScroll, true)
+ end
+ end
+end
+
+local migration = Migration("20241715984279_move_wheel_scrolls_from_storagename_to_kv")
+
+function migration:onExecute()
+ self:forEachPlayer(migrate)
+end
+
+migration:register()
diff --git a/data-otservbr-global/scripts/globalevents/quests/secret_library_preceptor_lazare.lua b/data-otservbr-global/scripts/globalevents/quests/secret_library_preceptor_lazare.lua
index 4da496fb659..ea05353ad09 100644
--- a/data-otservbr-global/scripts/globalevents/quests/secret_library_preceptor_lazare.lua
+++ b/data-otservbr-global/scripts/globalevents/quests/secret_library_preceptor_lazare.lua
@@ -1,7 +1,7 @@
local config = {
monsterName = "Preceptor Lazare",
bossPosition = Position(33374, 31338, 3),
- range = 5,
+ range = 50,
}
local preceptorLazare = GlobalEvent("PreceptorLazareRespawn")
diff --git a/data-otservbr-global/scripts/spells/monster/doctor_marrow_explosion.lua b/data-otservbr-global/scripts/spells/monster/doctor_marrow_explosion.lua
index f616e8b8cee..491d9f2516d 100644
--- a/data-otservbr-global/scripts/spells/monster/doctor_marrow_explosion.lua
+++ b/data-otservbr-global/scripts/spells/monster/doctor_marrow_explosion.lua
@@ -21,7 +21,7 @@ end
local spell = Spell("instant")
function onTargetCreature(creature, target)
- if not targetPos then
+ if not target then
return true
end
local master = target:getMaster()
@@ -29,7 +29,7 @@ function onTargetCreature(creature, target)
return true
end
- local distance = math.floor(targetPos:getDistance(target:getPosition()))
+ local distance = math.floor(creature:getPosition():getDistance(target:getPosition()))
local actualDamage = damage / (2 ^ distance)
doTargetCombatHealth(0, target, COMBAT_EARTHDAMAGE, actualDamage, actualDamage, CONST_ME_NONE)
if crit then
@@ -63,21 +63,26 @@ function spell.onCastSpell(creature, var)
end, i * 100, targetPos)
end
- addEvent(function(cid)
+ addEvent(function(cid, pos)
local creature = Creature(cid)
- creature:getPosition():sendMagicEffect(CONST_ME_ORANGE_ENERGY_SPARK)
- targetPos:sendMagicEffect(CONST_ME_ORANGETELEPORT)
- end, 2000, creature:getId())
+ if creature then
+ creature:getPosition():sendMagicEffect(CONST_ME_ORANGE_ENERGY_SPARK)
+ pos:sendMagicEffect(CONST_ME_ORANGETELEPORT)
+ end
+ end, 2000, creature:getId(), targetPos)
addEvent(function(cid, pos)
- damage = -math.random(3500, 7000)
- if math.random(1, 100) <= 10 then
- crit = true
- damage = damage * 1.5
- else
- crit = false
+ local creature = Creature(cid)
+ if creature then
+ damage = -math.random(3500, 7000)
+ if math.random(1, 100) <= 10 then
+ crit = true
+ damage = damage * 1.5
+ else
+ crit = false
+ end
+ spellCombat:execute(creature, Variant(pos))
end
- spellCombat:execute(creature, Variant(pos))
end, totalDelay, creature:getId(), targetPos)
return true
end
diff --git a/data-otservbr-global/world/otservbr-monster.xml b/data-otservbr-global/world/otservbr-monster.xml
index d3bbc69e858..974c2ea6809 100644
--- a/data-otservbr-global/world/otservbr-monster.xml
+++ b/data-otservbr-global/world/otservbr-monster.xml
@@ -4076,7 +4076,6 @@
-
@@ -4122,9 +4121,6 @@
-
-
-
@@ -6928,19 +6924,13 @@
-
-
-
-
-
-
@@ -6953,9 +6943,6 @@
-
-
-
@@ -6963,7 +6950,6 @@
-
@@ -6985,15 +6971,11 @@
-
-
-
-
@@ -7009,9 +6991,6 @@
-
-
-
@@ -63284,9 +63263,6 @@
-
-
-
@@ -63491,9 +63467,6 @@
-
-
-
@@ -96125,15 +96098,15 @@
+
+
+
-
-
-
@@ -96718,6 +96691,9 @@
+
+
+
@@ -96725,9 +96701,6 @@
-
-
-
@@ -118889,12 +118862,12 @@
-
-
-
+
+
+
diff --git a/data-otservbr-global/world/otservbr-npc.xml b/data-otservbr-global/world/otservbr-npc.xml
index 10772902341..f97bc118fdc 100644
--- a/data-otservbr-global/world/otservbr-npc.xml
+++ b/data-otservbr-global/world/otservbr-npc.xml
@@ -2994,5 +2994,8 @@
-
+
+
+
+
diff --git a/data/items/items.xml b/data/items/items.xml
index 45cde62bd9d..e7e73d9753b 100644
--- a/data/items/items.xml
+++ b/data/items/items.xml
@@ -64466,8 +64466,8 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
-
@@ -74873,11 +74873,14 @@ Granted by TibiaGoals.com"/>
-
-
+
+
+
+
-
@@ -74994,6 +74997,15 @@ Granted by TibiaGoals.com"/>
+ -
+
+
+
+
+
+
+
+
-
diff --git a/data/scripts/actions/items/wheel_scrolls.lua b/data/scripts/actions/items/wheel_scrolls.lua
index b42339a706e..61aaa6fe40a 100644
--- a/data/scripts/actions/items/wheel_scrolls.lua
+++ b/data/scripts/actions/items/wheel_scrolls.lua
@@ -1,9 +1,9 @@
local promotionScrolls = {
- [43946] = { storageName = "wheel.scroll.abridged", points = 3, name = "abridged promotion scroll" },
- [43947] = { storageName = "wheel.scroll.basic", points = 5, name = "basic promotion scroll" },
- [43948] = { storageName = "wheel.scroll.revised", points = 9, name = "revised promotion scroll" },
- [43949] = { storageName = "wheel.scroll.extended", points = 13, name = "extended promotion scroll" },
- [43950] = { storageName = "wheel.scroll.advanced", points = 20, name = "advanced promotion scroll" },
+ [43946] = { name = "abridged", points = 3, itemName = "abridged promotion scroll" },
+ [43947] = { name = "basic", points = 5, itemName = "basic promotion scroll" },
+ [43948] = { name = "revised", points = 9, itemName = "revised promotion scroll" },
+ [43949] = { name = "extended", points = 13, itemName = "extended promotion scroll" },
+ [43950] = { name = "advanced", points = 20, itemName = "advanced promotion scroll" },
}
local scroll = Action()
@@ -15,13 +15,14 @@ function scroll.onUse(player, item, fromPosition, target, toPosition, isHotkey)
end
local scrollData = promotionScrolls[item:getId()]
- if player:getStorageValueByName(scrollData.storageName) == 1 then
+ local scrollKV = player:kv():scoped("wheel-of-destiny"):scoped("scrolls")
+ if scrollKV:get(scrollData.name) then
player:sendTextMessage(MESSAGE_LOOK, "You have already deciphered this scroll.")
return true
end
- player:setStorageValueByName(scrollData.storageName, 1)
- player:sendTextMessage(MESSAGE_LOOK, "You have gained " .. scrollData.points .. " promotion points for the Wheel of Destiny by deciphering the " .. scrollData.name .. ".")
+ scrollKV:set(scrollData.name, true)
+ player:sendTextMessage(MESSAGE_LOOK, "You have gained " .. scrollData.points .. " promotion points for the Wheel of Destiny by deciphering the " .. scrollData.itemName .. ".")
item:remove(1)
return true
end
diff --git a/data/scripts/lib/register_migrations.lua b/data/scripts/lib/register_migrations.lua
index 5a2734dfc51..26b9a7b1a94 100644
--- a/data/scripts/lib/register_migrations.lua
+++ b/data/scripts/lib/register_migrations.lua
@@ -45,7 +45,7 @@ function Migration:register()
return
end
if not self:_validateName() then
- error("Invalid migration name: " .. self.name .. ". Migration names must be in the format: _. Example: 20231128213149_add_new_monsters")
+ logger.error("Invalid migration name: " .. self.name .. ". Migration names must be in the format: _. Example: 20231128213149_add_new_monsters")
end
table.insert(Migration.registry, self)
diff --git a/data/scripts/talkactions/god/add_skill.lua b/data/scripts/talkactions/god/add_skill.lua
index 20644582325..09d240ece4d 100644
--- a/data/scripts/talkactions/god/add_skill.lua
+++ b/data/scripts/talkactions/god/add_skill.lua
@@ -16,11 +16,6 @@ local function getSkillId(skillName)
end
end
-local function getExpForLevel(level)
- level = level - 1
- return ((50 * level * level * level) - (150 * level * level) + (400 * level)) / 3
-end
-
local addSkill = TalkAction("/addskill")
function addSkill.onSay(player, words, param)
@@ -54,7 +49,7 @@ function addSkill.onSay(player, words, param)
local ch = split[2]:sub(1, 1)
if ch == "l" or ch == "e" then
targetLevel = target:getLevel() + count
- targetExp = getExpForLevel(targetLevel)
+ targetExp = Game.getExperienceForLevel(targetLevel)
addExp = targetExp - target:getExperience()
target:addExperience(addExp, false)
elseif ch == "m" then
diff --git a/schema.sql b/schema.sql
index 624f434509e..2fbc7dd649b 100644
--- a/schema.sql
+++ b/schema.sql
@@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` (
CONSTRAINT `server_config_pk` PRIMARY KEY (`config`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '45'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');
+INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '46'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0');
-- Table structure `accounts`
CREATE TABLE IF NOT EXISTS `accounts` (
@@ -215,40 +215,79 @@ CREATE TABLE IF NOT EXISTS `account_viplist` (
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+-- Table structure `account_vipgroup`
+CREATE TABLE IF NOT EXISTS `account_vipgroups` (
+ `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `account_id` int(11) UNSIGNED NOT NULL COMMENT 'id of account whose vip group entry it is',
+ `name` varchar(128) NOT NULL,
+ `customizable` BOOLEAN NOT NULL DEFAULT '1',
+ CONSTRAINT `account_vipgroups_pk` PRIMARY KEY (`id`, `account_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+--
+-- Trigger
+--
+DELIMITER //
+CREATE TRIGGER `oncreate_accounts` AFTER INSERT ON `accounts` FOR EACH ROW BEGIN
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Enemies', 0);
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Friends', 0);
+ INSERT INTO `account_vipgroups` (`account_id`, `name`, `customizable`) VALUES (NEW.`id`, 'Trading Partner', 0);
+END
+//
+DELIMITER ;
+
+-- Table structure `account_vipgrouplist`
+CREATE TABLE IF NOT EXISTS `account_vipgrouplist` (
+ `account_id` int(11) UNSIGNED NOT NULL COMMENT 'id of account whose viplist entry it is',
+ `player_id` int(11) NOT NULL COMMENT 'id of target player of viplist entry',
+ `vipgroup_id` int(11) UNSIGNED NOT NULL COMMENT 'id of vip group that player belongs',
+ INDEX `account_id` (`account_id`),
+ INDEX `player_id` (`player_id`),
+ INDEX `vipgroup_id` (`vipgroup_id`),
+ CONSTRAINT `account_vipgrouplist_unique` UNIQUE (`account_id`, `player_id`, `vipgroup_id`),
+ CONSTRAINT `account_vipgrouplist_player_fk`
+ FOREIGN KEY (`player_id`) REFERENCES `players` (`id`)
+ ON DELETE CASCADE,
+ CONSTRAINT `account_vipgrouplist_vipgroup_fk`
+ FOREIGN KEY (`vipgroup_id`, `account_id`) REFERENCES `account_vipgroups` (`id`, `account_id`)
+ ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
-- Table structure `boosted_boss`
CREATE TABLE IF NOT EXISTS `boosted_boss` (
`boostname` TEXT,
`date` varchar(250) NOT NULL DEFAULT '',
`raceid` varchar(250) NOT NULL DEFAULT '',
- `looktypeEx` int(11) NOT NULL DEFAULT "0",
- `looktype` int(11) NOT NULL DEFAULT "136",
- `lookfeet` int(11) NOT NULL DEFAULT "0",
- `looklegs` int(11) NOT NULL DEFAULT "0",
- `lookhead` int(11) NOT NULL DEFAULT "0",
- `lookbody` int(11) NOT NULL DEFAULT "0",
- `lookaddons` int(11) NOT NULL DEFAULT "0",
- `lookmount` int(11) DEFAULT "0",
+ `looktypeEx` int(11) NOT NULL DEFAULT 0,
+ `looktype` int(11) NOT NULL DEFAULT 136,
+ `lookfeet` int(11) NOT NULL DEFAULT 0,
+ `looklegs` int(11) NOT NULL DEFAULT 0,
+ `lookhead` int(11) NOT NULL DEFAULT 0,
+ `lookbody` int(11) NOT NULL DEFAULT 0,
+ `lookaddons` int(11) NOT NULL DEFAULT 0,
+ `lookmount` int(11) DEFAULT 0,
PRIMARY KEY (`date`)
-) AS SELECT 0 AS date, "default" AS boostname, 0 AS raceid;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+INSERT INTO `boosted_boss` (`boostname`, `date`, `raceid`) VALUES ('default', 0, 0);
-- Table structure `boosted_creature`
CREATE TABLE IF NOT EXISTS `boosted_creature` (
`boostname` TEXT,
`date` varchar(250) NOT NULL DEFAULT '',
`raceid` varchar(250) NOT NULL DEFAULT '',
- `looktype` int(11) NOT NULL DEFAULT "136",
- `lookfeet` int(11) NOT NULL DEFAULT "0",
- `looklegs` int(11) NOT NULL DEFAULT "0",
- `lookhead` int(11) NOT NULL DEFAULT "0",
- `lookbody` int(11) NOT NULL DEFAULT "0",
- `lookaddons` int(11) NOT NULL DEFAULT "0",
- `lookmount` int(11) DEFAULT "0",
+ `looktype` int(11) NOT NULL DEFAULT 136,
+ `lookfeet` int(11) NOT NULL DEFAULT 0,
+ `looklegs` int(11) NOT NULL DEFAULT 0,
+ `lookhead` int(11) NOT NULL DEFAULT 0,
+ `lookbody` int(11) NOT NULL DEFAULT 0,
+ `lookaddons` int(11) NOT NULL DEFAULT 0,
+ `lookmount` int(11) DEFAULT 0,
PRIMARY KEY (`date`)
-) AS SELECT 0 AS date, "default" AS boostname, 0 AS raceid;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--- --------------------------------------------------------
+INSERT INTO `boosted_creature` (`boostname`, `date`, `raceid`) VALUES ('default', 0, 0);
---
-- Tabble Structure `daily_reward_history`
CREATE TABLE IF NOT EXISTS `daily_reward_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
@@ -372,9 +411,9 @@ CREATE TABLE IF NOT EXISTS `guild_ranks` (
--
DELIMITER //
CREATE TRIGGER `oncreate_guilds` AFTER INSERT ON `guilds` FOR EACH ROW BEGIN
- INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('The Leader', 3, NEW.`id`);
- INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('Vice-Leader', 2, NEW.`id`);
- INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('Member', 1, NEW.`id`);
+ INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('The Leader', 3, NEW.`id`);
+ INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('Vice-Leader', 2, NEW.`id`);
+ INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('Member', 1, NEW.`id`);
END
//
DELIMITER ;
@@ -428,15 +467,13 @@ CREATE TABLE IF NOT EXISTS `houses` (
-- trigger
--
DELIMITER //
-CREATE TRIGGER `ondelete_players` BEFORE DELETE ON `players`
- FOR EACH ROW BEGIN
- UPDATE `houses` SET `owner` = 0 WHERE `owner` = OLD.`id`;
+CREATE TRIGGER `ondelete_players` BEFORE DELETE ON `players` FOR EACH ROW BEGIN
+ UPDATE `houses` SET `owner` = 0 WHERE `owner` = OLD.`id`;
END
//
DELIMITER ;
-- Table structure `house_lists`
-
CREATE TABLE IF NOT EXISTS `house_lists` (
`house_id` int NOT NULL,
`listid` int NOT NULL,
@@ -448,7 +485,6 @@ CREATE TABLE IF NOT EXISTS `house_lists` (
CONSTRAINT `houses_list_house_fk` FOREIGN KEY (`house_id`) REFERENCES `houses` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
-
-- Table structure `ip_bans`
CREATE TABLE IF NOT EXISTS `ip_bans` (
`ip` int(11) NOT NULL,
@@ -503,7 +539,6 @@ CREATE TABLE IF NOT EXISTS `market_offers` (
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
-- Table structure `players_online`
CREATE TABLE IF NOT EXISTS `players_online` (
`player_id` int(11) NOT NULL,
@@ -634,7 +669,6 @@ CREATE TABLE IF NOT EXISTS `player_wheeldata` (
PRIMARY KEY (`player_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
-- Table structure `player_kills`
CREATE TABLE IF NOT EXISTS `player_kills` (
`player_id` int(11) NOT NULL,
@@ -793,6 +827,7 @@ CREATE TABLE IF NOT EXISTS `account_sessions` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+-- Table structure `kv_store`
CREATE TABLE IF NOT EXISTS `kv_store` (
`key_name` varchar(191) NOT NULL,
`timestamp` bigint NOT NULL,
@@ -815,3 +850,9 @@ INSERT INTO `players`
(4, 'Paladin Sample', 1, 1, 8, 3, 185, 185, 4200, 113, 115, 95, 39, 129, 0, 90, 90, 0, 8, '', 470, 1, 10, 0, 10, 0, 10, 0, 10, 0),
(5, 'Knight Sample', 1, 1, 8, 4, 185, 185, 4200, 113, 115, 95, 39, 129, 0, 90, 90, 0, 8, '', 470, 1, 10, 0, 10, 0, 10, 0, 10, 0),
(6, 'GOD', 6, 1, 2, 0, 155, 155, 100, 113, 115, 95, 39, 75, 0, 60, 60, 0, 8, '', 410, 1, 10, 0, 10, 0, 10, 0, 10, 0);
+
+-- Create vip groups for GOD account
+INSERT INTO `account_vipgroups` (`name`, `account_id`, `customizable`) VALUES
+('Friends', 1, 0),
+('Enemies', 1, 0),
+('Trading Partners', 1, 0);
diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp
index 30a4b172af1..0e53c97546e 100644
--- a/src/config/config_enums.hpp
+++ b/src/config/config_enums.hpp
@@ -50,6 +50,7 @@ enum ConfigKey_t : uint16_t {
DATA_DIRECTORY,
DAY_KILLS_TO_RED,
DEATH_LOSE_PERCENT,
+ DEFAULT_RESPAWN_TIME,
DEFAULT_DESPAWNRADIUS,
DEFAULT_DESPAWNRANGE,
DEFAULT_PRIORITY,
@@ -139,6 +140,7 @@ enum ConfigKey_t : uint16_t {
MAP_DOWNLOAD_URL,
MAP_NAME,
MARKET_OFFER_DURATION,
+ MARKET_REFRESH_PRICES,
MARKET_PREMIUM,
MAX_ALLOWED_ON_A_DUMMY,
MAX_CONTAINER_ITEM,
diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp
index 37634abf514..35d2cf3b1be 100644
--- a/src/config/configmanager.cpp
+++ b/src/config/configmanager.cpp
@@ -61,6 +61,7 @@ bool ConfigManager::load() {
loadIntConfig(L, GAME_PORT, "gameProtocolPort", 7172);
loadIntConfig(L, LOGIN_PORT, "loginProtocolPort", 7171);
loadIntConfig(L, MARKET_OFFER_DURATION, "marketOfferDuration", 30 * 24 * 60 * 60);
+ loadIntConfig(L, MARKET_REFRESH_PRICES, "marketRefreshPricesInterval", 30);
loadIntConfig(L, PREMIUM_DEPOT_LIMIT, "premiumDepotLimit", 8000);
loadIntConfig(L, SQL_PORT, "mysqlPort", 3306);
loadIntConfig(L, STASH_ITEMS, "stashItemCount", 5000);
@@ -224,6 +225,7 @@ bool ConfigManager::load() {
loadIntConfig(L, CRITICALCHANCE, "criticalChance", 10);
loadIntConfig(L, DAY_KILLS_TO_RED, "dayKillsToRedSkull", 3);
loadIntConfig(L, DEATH_LOSE_PERCENT, "deathLosePercent", -1);
+ loadIntConfig(L, DEFAULT_RESPAWN_TIME, "defaultRespawnTime", 60);
loadIntConfig(L, DEFAULT_DESPAWNRADIUS, "deSpawnRadius", 50);
loadIntConfig(L, DEFAULT_DESPAWNRANGE, "deSpawnRange", 2);
loadIntConfig(L, DEPOTCHEST, "depotChest", 4);
diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt
index 6715281439f..af6ba129eac 100644
--- a/src/creatures/CMakeLists.txt
+++ b/src/creatures/CMakeLists.txt
@@ -27,4 +27,5 @@ target_sources(${PROJECT_NAME}_lib PRIVATE
players/wheel/player_wheel.cpp
players/wheel/wheel_gems.cpp
players/vocations/vocation.cpp
+ players/vip/player_vip.cpp
)
diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp
index 43e7ab89ccf..7796863f066 100644
--- a/src/creatures/creature.cpp
+++ b/src/creatures/creature.cpp
@@ -824,7 +824,7 @@ bool Creature::dropCorpse(std::shared_ptr lastHitCreature, std::shared
auto isReachable = g_game().map.getPathMatching(player->getPosition(), dirList, FrozenPathingConditionCall(corpse->getPosition()), fpp);
- if (player->checkAutoLoot(monster->isRewardBoss()) && corpseContainer && mostDamageCreature->getPlayer() && isReachable) {
+ if (player->checkAutoLoot(monster->isRewardBoss()) && isReachable) {
g_dispatcher().addEvent([player, corpseContainer, corpsePosition = corpse->getPosition()] {
g_game().playerQuickLootCorpse(player, corpseContainer, corpsePosition);
},
diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp
index 136f587a64a..33f62dbd2aa 100644
--- a/src/creatures/creatures_definitions.hpp
+++ b/src/creatures/creatures_definitions.hpp
@@ -714,11 +714,11 @@ enum ChannelEvent_t : uint8_t {
CHANNELEVENT_EXCLUDE = 3,
};
-enum VipStatus_t : uint8_t {
- VIPSTATUS_OFFLINE = 0,
- VIPSTATUS_ONLINE = 1,
- VIPSTATUS_PENDING = 2,
- VIPSTATUS_TRAINING = 3
+enum class VipStatus_t : uint8_t {
+ Offline = 0,
+ Online = 1,
+ Pending = 2,
+ Training = 3
};
enum Vocation_t : uint16_t {
@@ -1397,18 +1397,29 @@ struct CreatureIcon {
struct Position;
struct VIPEntry {
- VIPEntry(uint32_t initGuid, std::string initName, std::string initDescription, uint32_t initIcon, bool initNotify) :
+ VIPEntry(uint32_t initGuid, const std::string &initName, const std::string &initDescription, uint32_t initIcon, bool initNotify) :
guid(initGuid),
name(std::move(initName)),
description(std::move(initDescription)),
icon(initIcon),
notify(initNotify) { }
- uint32_t guid;
- std::string name;
- std::string description;
- uint32_t icon;
- bool notify;
+ uint32_t guid = 0;
+ std::string name = "";
+ std::string description = "";
+ uint32_t icon = 0;
+ bool notify = false;
+};
+
+struct VIPGroupEntry {
+ VIPGroupEntry(uint8_t initId, const std::string &initName, bool initCustomizable) :
+ id(initId),
+ name(std::move(initName)),
+ customizable(initCustomizable) { }
+
+ uint8_t id = 0;
+ std::string name = "";
+ bool customizable = false;
};
struct Skill {
diff --git a/src/creatures/monsters/spawns/spawn_monster.cpp b/src/creatures/monsters/spawns/spawn_monster.cpp
index ef5912c8052..968b90d8cae 100644
--- a/src/creatures/monsters/spawns/spawn_monster.cpp
+++ b/src/creatures/monsters/spawns/spawn_monster.cpp
@@ -94,7 +94,15 @@ bool SpawnsMonster::loadFromXML(const std::string &filemonstername) {
weight = pugi::cast(weightAttribute.value());
}
- spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, pugi::cast(childMonsterNode.attribute("spawntime").value()) * 1000, weight);
+ uint32_t scheduleInterval = g_configManager().getNumber(DEFAULT_RESPAWN_TIME, __FUNCTION__);
+
+ try {
+ scheduleInterval = pugi::cast(childMonsterNode.attribute("spawntime").value());
+ } catch (...) {
+ g_logger().warn("Failed to add schedule interval to monster: {}, interval: {}. Setting to default respawn time: {}", nameAttribute.value(), childMonsterNode.attribute("spawntime").value(), scheduleInterval);
+ }
+
+ spawnMonster.addMonster(nameAttribute.as_string(), pos, dir, scheduleInterval * 1000, weight);
}
}
}
diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp
index 3182607ecba..4109eb77472 100644
--- a/src/creatures/players/player.cpp
+++ b/src/creatures/players/player.cpp
@@ -49,6 +49,7 @@ Player::Player(ProtocolGame_ptr p) :
lastPong(lastPing),
inbox(std::make_shared(ITEM_INBOX)),
client(std::move(p)) {
+ m_playerVIP = std::make_unique(*this);
m_wheelPlayer = std::make_unique(*this);
m_playerAchievement = std::make_unique(*this);
m_playerBadge = std::make_unique(*this);
@@ -649,10 +650,10 @@ phmap::flat_hash_map Player::getBlessingNames() const
void Player::setTraining(bool value) {
for (const auto &[key, player] : g_game().getPlayers()) {
if (!this->isInGhostMode() || player->isAccessPlayer()) {
- player->notifyStatusChange(static_self_cast(), value ? VIPSTATUS_TRAINING : VIPSTATUS_ONLINE, false);
+ player->vip()->notifyStatusChange(static_self_cast(), value ? VipStatus_t::Training : VipStatus_t::Online, false);
}
}
- this->statusVipList = VIPSTATUS_TRAINING;
+ vip()->setStatus(VipStatus_t::Training);
setExerciseTraining(value);
}
@@ -1861,6 +1862,13 @@ void Player::onRemoveCreature(std::shared_ptr creature, bool isLogout)
Creature::onRemoveCreature(creature, isLogout);
if (auto player = getPlayer(); player == creature) {
+ for (uint8_t slot = CONST_SLOT_FIRST; slot <= CONST_SLOT_LAST; ++slot) {
+ const auto item = inventory[slot];
+ if (item) {
+ g_moveEvents().onPlayerDeEquip(getPlayer(), item, static_cast(slot));
+ }
+ }
+
if (isLogout) {
if (m_party) {
m_party->leaveParty(player);
@@ -2985,7 +2993,7 @@ void Player::despawn() {
// show player as pending
for (const auto &[key, player] : g_game().getPlayers()) {
- player->notifyStatusChange(static_self_cast(), VIPSTATUS_PENDING, false);
+ player->vip()->notifyStatusChange(static_self_cast(), VipStatus_t::Pending, false);
}
setDead(true);
@@ -3039,13 +3047,13 @@ void Player::removeList() {
g_game().removePlayer(static_self_cast());
for (const auto &[key, player] : g_game().getPlayers()) {
- player->notifyStatusChange(static_self_cast(), VIPSTATUS_OFFLINE);
+ player->vip()->notifyStatusChange(static_self_cast(), VipStatus_t::Offline);
}
}
void Player::addList() {
for (const auto &[key, player] : g_game().getPlayers()) {
- player->notifyStatusChange(static_self_cast(), this->statusVipList);
+ player->vip()->notifyStatusChange(static_self_cast(), vip()->getStatus());
}
g_game().addPlayer(static_self_cast());
@@ -3060,82 +3068,6 @@ void Player::removePlayer(bool displayEffect, bool forced /*= true*/) {
}
}
-void Player::notifyStatusChange(std::shared_ptr loginPlayer, VipStatus_t status, bool message) const {
- if (!client) {
- return;
- }
-
- if (!VIPList.contains(loginPlayer->guid)) {
- return;
- }
-
- client->sendUpdatedVIPStatus(loginPlayer->guid, status);
-
- if (message) {
- if (status == VIPSTATUS_ONLINE) {
- client->sendTextMessage(TextMessage(MESSAGE_FAILURE, loginPlayer->getName() + " has logged in."));
- } else if (status == VIPSTATUS_OFFLINE) {
- client->sendTextMessage(TextMessage(MESSAGE_FAILURE, loginPlayer->getName() + " has logged out."));
- }
- }
-}
-
-bool Player::removeVIP(uint32_t vipGuid) {
- if (!VIPList.erase(vipGuid)) {
- return false;
- }
-
- VIPList.erase(vipGuid);
- if (account) {
- IOLoginData::removeVIPEntry(account->getID(), vipGuid);
- }
-
- return true;
-}
-
-bool Player::addVIP(uint32_t vipGuid, const std::string &vipName, VipStatus_t status) {
- if (VIPList.size() >= getMaxVIPEntries() || VIPList.size() == 200) { // max number of buddies is 200 in 9.53
- sendTextMessage(MESSAGE_FAILURE, "You cannot add more buddies.");
- return false;
- }
-
- if (!VIPList.insert(vipGuid).second) {
- sendTextMessage(MESSAGE_FAILURE, "This player is already in your list.");
- return false;
- }
-
- if (account) {
- IOLoginData::addVIPEntry(account->getID(), vipGuid, "", 0, false);
- }
-
- if (client) {
- client->sendVIP(vipGuid, vipName, "", 0, false, status);
- }
-
- return true;
-}
-
-bool Player::addVIPInternal(uint32_t vipGuid) {
- if (VIPList.size() >= getMaxVIPEntries() || VIPList.size() == 200) { // max number of buddies is 200 in 9.53
- return false;
- }
-
- return VIPList.insert(vipGuid).second;
-}
-
-bool Player::editVIP(uint32_t vipGuid, const std::string &description, uint32_t icon, bool notify) const {
- auto it = VIPList.find(vipGuid);
- if (it == VIPList.end()) {
- return false; // player is not in VIP
- }
-
- if (account) {
- IOLoginData::editVIPEntry(account->getID(), vipGuid, description, icon, notify);
- }
-
- return true;
-}
-
// close container and its child containers
void Player::autoCloseContainers(std::shared_ptr container) {
std::vector closeList;
@@ -6293,15 +6225,6 @@ std::pair Player::getForgeSliversAndCores() const {
return std::make_pair(sliverCount, coreCount);
}
-size_t Player::getMaxVIPEntries() const {
- if (group->maxVipEntries != 0) {
- return group->maxVipEntries;
- } else if (isPremium()) {
- return 100;
- }
- return 20;
-}
-
size_t Player::getMaxDepotItems() const {
if (group->maxDepotItems != 0) {
return group->maxDepotItems;
@@ -8079,6 +8002,15 @@ const std::unique_ptr &Player::title() const {
return m_playerTitle;
}
+// VIP interface
+std::unique_ptr &Player::vip() {
+ return m_playerVIP;
+}
+
+const std::unique_ptr &Player::vip() const {
+ return m_playerVIP;
+}
+
void Player::sendLootMessage(const std::string &message) const {
auto party = getParty();
if (!party) {
diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp
index f61e33a37ef..c0761704db4 100644
--- a/src/creatures/players/player.hpp
+++ b/src/creatures/players/player.hpp
@@ -37,6 +37,7 @@
#include "enums/player_cyclopedia.hpp"
#include "creatures/players/cyclopedia/player_badge.hpp"
#include "creatures/players/cyclopedia/player_title.hpp"
+#include "creatures/players/vip/player_vip.hpp"
class House;
class NetworkMessage;
@@ -54,6 +55,7 @@ class PlayerWheel;
class PlayerAchievement;
class PlayerBadge;
class PlayerTitle;
+class PlayerVIP;
class Spectators;
class Account;
@@ -61,6 +63,7 @@ struct ModalWindow;
struct Achievement;
struct Badge;
struct Title;
+struct VIPGroup;
struct ForgeHistory {
ForgeAction_t actionType = ForgeAction_t::FUSION;
@@ -227,9 +230,8 @@ class Player final : public Creature, public Cylinder, public Bankable {
void addList() override;
void removePlayer(bool displayEffect, bool forced = true);
- static uint64_t getExpForLevel(int32_t lv) {
- lv--;
- return ((50ULL * lv * lv * lv) - (150ULL * lv * lv) + (400ULL * lv)) / 3ULL;
+ static uint64_t getExpForLevel(const uint32_t level) {
+ return (((level - 6ULL) * level + 17ULL) * level - 12ULL) / 6ULL * 100ULL;
}
uint16_t getStaminaMinutes() const {
@@ -830,13 +832,6 @@ class Player final : public Creature, public Cylinder, public Bankable {
return shopOwner;
}
- // V.I.P. functions
- void notifyStatusChange(std::shared_ptr player, VipStatus_t status, bool message = true) const;
- bool removeVIP(uint32_t vipGuid);
- bool addVIP(uint32_t vipGuid, const std::string &vipName, VipStatus_t status);
- bool addVIPInternal(uint32_t vipGuid);
- bool editVIP(uint32_t vipGuid, const std::string &description, uint32_t icon, bool notify) const;
-
// follow functions
bool setFollowCreature(std::shared_ptr creature) override;
void goToFollowCreature() override;
@@ -1050,7 +1045,6 @@ class Player final : public Creature, public Cylinder, public Bankable {
bool hasKilled(std::shared_ptr player) const;
- size_t getMaxVIPEntries() const;
size_t getMaxDepotItems() const;
// tile
@@ -2630,6 +2624,10 @@ class Player final : public Creature, public Cylinder, public Bankable {
std::unique_ptr &title();
const std::unique_ptr &title() const;
+ // Player vip interface
+ std::unique_ptr &vip();
+ const std::unique_ptr &vip() const;
+
void sendLootMessage(const std::string &message) const;
std::shared_ptr getLootPouch();
@@ -2716,7 +2714,6 @@ class Player final : public Creature, public Cylinder, public Bankable {
void addBosstiaryKill(const std::shared_ptr &mType);
phmap::flat_hash_set attackedSet;
- phmap::flat_hash_set VIPList;
std::map openContainers;
std::map> depotLockerMap;
@@ -2911,7 +2908,6 @@ class Player final : public Creature, public Cylinder, public Bankable {
FightMode_t fightMode = FIGHTMODE_ATTACK;
Faction_t faction = FACTION_PLAYER;
QuickLootFilter_t quickLootFilter;
- VipStatus_t statusVipList = VIPSTATUS_ONLINE;
PlayerPronoun_t pronoun = PLAYERPRONOUN_THEY;
bool chaseMode = false;
@@ -3026,11 +3022,13 @@ class Player final : public Creature, public Cylinder, public Bankable {
friend class PlayerAchievement;
friend class PlayerBadge;
friend class PlayerTitle;
+ friend class PlayerVIP;
std::unique_ptr m_wheelPlayer;
std::unique_ptr m_playerAchievement;
std::unique_ptr m_playerBadge;
std::unique_ptr m_playerTitle;
+ std::unique_ptr m_playerVIP;
std::mutex quickLootMutex;
diff --git a/src/creatures/players/vip/player_vip.cpp b/src/creatures/players/vip/player_vip.cpp
new file mode 100644
index 00000000000..b4b1642ec69
--- /dev/null
+++ b/src/creatures/players/vip/player_vip.cpp
@@ -0,0 +1,247 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#include "pch.hpp"
+
+#include "creatures/players/vip/player_vip.hpp"
+
+#include "io/iologindata.hpp"
+
+#include "game/game.hpp"
+#include "creatures/players/player.hpp"
+
+const uint8_t PlayerVIP::firstID = 1;
+const uint8_t PlayerVIP::lastID = 8;
+
+PlayerVIP::PlayerVIP(Player &player) :
+ m_player(player) { }
+
+size_t PlayerVIP::getMaxEntries() const {
+ if (m_player.group && m_player.group->maxVipEntries != 0) {
+ return m_player.group->maxVipEntries;
+ } else if (m_player.isPremium()) {
+ return 100;
+ }
+ return 20;
+}
+
+uint8_t PlayerVIP::getMaxGroupEntries() const {
+ if (m_player.isPremium()) {
+ return 8; // max number of groups is 8 (5 custom and 3 default)
+ }
+ return 0;
+}
+
+void PlayerVIP::notifyStatusChange(std::shared_ptr loginPlayer, VipStatus_t status, bool message) const {
+ if (!m_player.client) {
+ return;
+ }
+
+ if (!vipGuids.contains(loginPlayer->getGUID())) {
+ return;
+ }
+
+ m_player.client->sendUpdatedVIPStatus(loginPlayer->getGUID(), status);
+
+ if (message) {
+ if (status == VipStatus_t::Online) {
+ m_player.sendTextMessage(TextMessage(MESSAGE_FAILURE, fmt::format("{} has logged in.", loginPlayer->getName())));
+ } else if (status == VipStatus_t::Offline) {
+ m_player.sendTextMessage(TextMessage(MESSAGE_FAILURE, fmt::format("{} has logged out.", loginPlayer->getName())));
+ }
+ }
+}
+
+bool PlayerVIP::remove(uint32_t vipGuid) {
+ if (!vipGuids.erase(vipGuid)) {
+ return false;
+ }
+
+ vipGuids.erase(vipGuid);
+ if (m_player.account) {
+ IOLoginData::removeVIPEntry(m_player.account->getID(), vipGuid);
+ }
+
+ return true;
+}
+
+bool PlayerVIP::add(uint32_t vipGuid, const std::string &vipName, VipStatus_t status) {
+ if (vipGuids.size() >= getMaxEntries() || vipGuids.size() == 200) { // max number of buddies is 200 in 9.53
+ m_player.sendTextMessage(MESSAGE_FAILURE, "You cannot add more buddies.");
+ return false;
+ }
+
+ if (!vipGuids.insert(vipGuid).second) {
+ m_player.sendTextMessage(MESSAGE_FAILURE, "This player is already in your list.");
+ return false;
+ }
+
+ if (m_player.account) {
+ IOLoginData::addVIPEntry(m_player.account->getID(), vipGuid, "", 0, false);
+ }
+
+ if (m_player.client) {
+ m_player.client->sendVIP(vipGuid, vipName, "", 0, false, status);
+ }
+
+ return true;
+}
+
+bool PlayerVIP::addInternal(uint32_t vipGuid) {
+ if (vipGuids.size() >= getMaxEntries() || vipGuids.size() == 200) { // max number of buddies is 200 in 9.53
+ return false;
+ }
+
+ return vipGuids.insert(vipGuid).second;
+}
+
+bool PlayerVIP::edit(uint32_t vipGuid, const std::string &description, uint32_t icon, bool notify, std::vector groupsId) const {
+ const auto it = vipGuids.find(vipGuid);
+ if (it == vipGuids.end()) {
+ return false; // player is not in VIP
+ }
+
+ if (m_player.account) {
+ IOLoginData::editVIPEntry(m_player.account->getID(), vipGuid, description, icon, notify);
+ }
+
+ IOLoginData::removeGuidVIPGroupEntry(m_player.account->getID(), vipGuid);
+
+ for (const auto groupId : groupsId) {
+ const auto &group = getGroupByID(groupId);
+ if (group) {
+ group->vipGroupGuids.insert(vipGuid);
+ IOLoginData::addGuidVIPGroupEntry(group->id, m_player.account->getID(), vipGuid);
+ }
+ }
+
+ return true;
+}
+
+std::shared_ptr PlayerVIP::getGroupByID(uint8_t groupId) const {
+ auto it = std::find_if(vipGroups.begin(), vipGroups.end(), [groupId](const std::shared_ptr vipGroup) {
+ return vipGroup->id == groupId;
+ });
+
+ return it != vipGroups.end() ? *it : nullptr;
+}
+
+std::shared_ptr PlayerVIP::getGroupByName(const std::string &name) const {
+ const auto groupName = name.c_str();
+ auto it = std::find_if(vipGroups.begin(), vipGroups.end(), [groupName](const std::shared_ptr vipGroup) {
+ return strcmp(groupName, vipGroup->name.c_str()) == 0;
+ });
+
+ return it != vipGroups.end() ? *it : nullptr;
+}
+
+void PlayerVIP::addGroupInternal(uint8_t groupId, const std::string &name, bool customizable) {
+ if (getGroupByName(name) != nullptr) {
+ g_logger().warn("{} - Group name already exists.", __FUNCTION__);
+ return;
+ }
+
+ const auto freeId = getFreeId();
+ if (freeId == 0) {
+ g_logger().warn("{} - No id available.", __FUNCTION__);
+ return;
+ }
+
+ vipGroups.emplace_back(std::make_shared(freeId, name, customizable));
+}
+
+void PlayerVIP::removeGroup(uint8_t groupId) {
+ auto it = std::find_if(vipGroups.begin(), vipGroups.end(), [groupId](const std::shared_ptr vipGroup) {
+ return vipGroup->id == groupId;
+ });
+
+ if (it == vipGroups.end()) {
+ return;
+ }
+
+ vipGroups.erase(it);
+
+ if (m_player.account) {
+ IOLoginData::removeVIPGroupEntry(groupId, m_player.account->getID());
+ }
+
+ if (m_player.client) {
+ m_player.client->sendVIPGroups();
+ }
+}
+
+void PlayerVIP::addGroup(const std::string &name, bool customizable /*= true */) {
+ if (getGroupByName(name) != nullptr) {
+ m_player.sendCancelMessage("A group with this name already exists. Please choose another name.");
+ return;
+ }
+
+ const auto freeId = getFreeId();
+ if (freeId == 0) {
+ g_logger().warn("{} - No id available.", __FUNCTION__);
+ return;
+ }
+
+ std::shared_ptr vipGroup = std::make_shared(freeId, name, customizable);
+ vipGroups.emplace_back(vipGroup);
+
+ if (m_player.account) {
+ IOLoginData::addVIPGroupEntry(vipGroup->id, m_player.account->getID(), vipGroup->name, vipGroup->customizable);
+ }
+
+ if (m_player.client) {
+ m_player.client->sendVIPGroups();
+ }
+}
+
+void PlayerVIP::editGroup(uint8_t groupId, const std::string &newName, bool customizable /*= true*/) {
+ if (getGroupByName(newName) != nullptr) {
+ m_player.sendCancelMessage("A group with this name already exists. Please choose another name.");
+ return;
+ }
+
+ const auto &vipGroup = getGroupByID(groupId);
+ vipGroup->name = newName;
+ vipGroup->customizable = customizable;
+
+ if (m_player.account) {
+ IOLoginData::editVIPGroupEntry(vipGroup->id, m_player.account->getID(), vipGroup->name, vipGroup->customizable);
+ }
+
+ if (m_player.client) {
+ m_player.client->sendVIPGroups();
+ }
+}
+
+uint8_t PlayerVIP::getFreeId() const {
+ for (uint8_t i = firstID; i <= lastID; ++i) {
+ if (getGroupByID(i) == nullptr) {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+const std::vector PlayerVIP::getGroupsIdGuidBelongs(uint32_t guid) {
+ std::vector guidBelongs;
+ for (const auto &vipGroup : vipGroups) {
+ if (vipGroup->vipGroupGuids.contains(guid)) {
+ guidBelongs.emplace_back(vipGroup->id);
+ }
+ }
+ return guidBelongs;
+}
+
+void PlayerVIP::addGuidToGroupInternal(uint8_t groupId, uint32_t guid) {
+ const auto &group = getGroupByID(groupId);
+ if (group) {
+ group->vipGroupGuids.insert(guid);
+ }
+}
diff --git a/src/creatures/players/vip/player_vip.hpp b/src/creatures/players/vip/player_vip.hpp
new file mode 100644
index 00000000000..e9aeaf85326
--- /dev/null
+++ b/src/creatures/players/vip/player_vip.hpp
@@ -0,0 +1,74 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2024 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#pragma once
+
+#include "creatures/creatures_definitions.hpp"
+
+class Player;
+
+struct VIPGroup {
+ uint8_t id = 0;
+ std::string name = "";
+ bool customizable = false;
+ phmap::flat_hash_set vipGroupGuids;
+
+ VIPGroup() = default;
+ VIPGroup(uint8_t id, const std::string &name, bool customizable) :
+ id(id), name(std::move(name)), customizable(customizable) { }
+};
+class PlayerVIP {
+
+public:
+ explicit PlayerVIP(Player &player);
+
+ static const uint8_t firstID;
+ static const uint8_t lastID;
+
+ size_t getMaxEntries() const;
+ uint8_t getMaxGroupEntries() const;
+
+ VipStatus_t getStatus() const {
+ return status;
+ }
+ void setStatus(VipStatus_t newStatus) {
+ status = newStatus;
+ }
+
+ void notifyStatusChange(std::shared_ptr loginPlayer, VipStatus_t status, bool message = true) const;
+ bool remove(uint32_t vipGuid);
+ bool add(uint32_t vipGuid, const std::string &vipName, VipStatus_t status);
+ bool addInternal(uint32_t vipGuid);
+ bool edit(uint32_t vipGuid, const std::string &description, uint32_t icon, bool notify, std::vector groupsId) const;
+
+ // VIP Group
+ std::shared_ptr getGroupByID(uint8_t groupId) const;
+ std::shared_ptr getGroupByName(const std::string &name) const;
+
+ void addGroupInternal(uint8_t groupId, const std::string &name, bool customizable);
+ void removeGroup(uint8_t groupId);
+ void addGroup(const std::string &name, bool customizable = true);
+ void editGroup(uint8_t groupId, const std::string &newName, bool customizable = true);
+
+ void addGuidToGroupInternal(uint8_t groupId, uint32_t guid);
+
+ uint8_t getFreeId() const;
+ const std::vector getGroupsIdGuidBelongs(uint32_t guid);
+
+ [[nodiscard]] const std::vector> &getGroups() const {
+ return vipGroups;
+ }
+
+private:
+ Player &m_player;
+
+ VipStatus_t status = VipStatus_t::Online;
+ std::vector> vipGroups;
+ phmap::flat_hash_set vipGuids;
+};
diff --git a/src/creatures/players/wheel/player_wheel.cpp b/src/creatures/players/wheel/player_wheel.cpp
index c4fdbf5e169..4c7a3dc52e0 100644
--- a/src/creatures/players/wheel/player_wheel.cpp
+++ b/src/creatures/players/wheel/player_wheel.cpp
@@ -130,16 +130,16 @@ namespace {
struct PromotionScroll {
uint16_t itemId;
- std::string storageKey;
+ std::string name;
uint8_t extraPoints;
};
std::vector WheelOfDestinyPromotionScrolls = {
- { 43946, "wheel.scroll.abridged", 3 },
- { 43947, "wheel.scroll.basic", 5 },
- { 43948, "wheel.scroll.revised", 9 },
- { 43949, "wheel.scroll.extended", 13 },
- { 43950, "wheel.scroll.advanced", 20 },
+ { 43946, "abridged", 3 },
+ { 43947, "basic", 5 },
+ { 43948, "revised", 9 },
+ { 43949, "extended", 13 },
+ { 43950, "advanced", 20 },
};
} // namespace
@@ -744,18 +744,21 @@ int PlayerWheel::getSpellAdditionalDuration(const std::string &spellName) const
}
void PlayerWheel::addPromotionScrolls(NetworkMessage &msg) const {
- uint16_t count = 0;
std::vector unlockedScrolls;
for (const auto &scroll : WheelOfDestinyPromotionScrolls) {
- auto storageValue = m_player.getStorageValueByName(scroll.storageKey);
- if (storageValue > 0) {
- count++;
+ const auto &scrollKv = m_player.kv()->scoped("wheel-of-destiny")->scoped("scrolls");
+ if (!scrollKv) {
+ continue;
+ }
+
+ auto scrollOpt = scrollKv->get(scroll.name);
+ if (scrollOpt && scrollOpt->get()) {
unlockedScrolls.push_back(scroll.itemId);
}
}
- msg.add(count);
+ msg.add(unlockedScrolls.size());
for (const auto &itemId : unlockedScrolls) {
msg.add(itemId);
}
@@ -1239,8 +1242,13 @@ uint16_t PlayerWheel::getExtraPoints() const {
uint16_t totalBonus = 0;
for (const auto &scroll : WheelOfDestinyPromotionScrolls) {
- auto storageValue = m_player.getStorageValueByName(scroll.storageKey);
- if (storageValue > 0) {
+ const auto &scrollKv = m_player.kv()->scoped("wheel-of-destiny")->scoped("scrolls");
+ if (!scrollKv) {
+ continue;
+ }
+
+ auto scrollKV = scrollKv->get(scroll.name);
+ if (scrollKV && scrollKV->get()) {
totalBonus += scroll.extraPoints;
}
}
diff --git a/src/database/databasetasks.cpp b/src/database/databasetasks.cpp
index 6d43992ac81..06cfda93fc0 100644
--- a/src/database/databasetasks.cpp
+++ b/src/database/databasetasks.cpp
@@ -23,7 +23,7 @@ DatabaseTasks &DatabaseTasks::getInstance() {
}
void DatabaseTasks::execute(const std::string &query, std::function callback /* nullptr */) {
- threadPool.addLoad([this, query, callback]() {
+ threadPool.detach_task([this, query, callback]() {
bool success = db.executeQuery(query);
if (callback != nullptr) {
g_dispatcher().addEvent([callback, success]() { callback(nullptr, success); }, "DatabaseTasks::execute");
@@ -32,7 +32,7 @@ void DatabaseTasks::execute(const std::string &query, std::function callback /* nullptr */) {
- threadPool.addLoad([this, query, callback]() {
+ threadPool.detach_task([this, query, callback]() {
DBResult_ptr result = db.storeQuery(query);
if (callback != nullptr) {
g_dispatcher().addEvent([callback, result]() { callback(result, true); }, "DatabaseTasks::store");
diff --git a/src/game/game.cpp b/src/game/game.cpp
index 00ec056ccb3..899a89549d9 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -486,9 +486,16 @@ void Game::start(ServiceManager* manager) {
g_dispatcher().cycleEvent(
EVENT_LUA_GARBAGE_COLLECTION, [this] { g_luaEnvironment().collectGarbage(); }, "Calling GC"
);
- g_dispatcher().cycleEvent(
- EVENT_REFRESH_MARKET_PRICES, [this] { loadItemsPrice(); }, "Game::loadItemsPrice"
- );
+ auto marketItemsPriceIntervalMinutes = g_configManager().getNumber(MARKET_REFRESH_PRICES, __FUNCTION__);
+ if (marketItemsPriceIntervalMinutes > 0) {
+ auto marketItemsPriceIntervalMS = marketItemsPriceIntervalMinutes * 60000;
+ if (marketItemsPriceIntervalMS < 60000) {
+ marketItemsPriceIntervalMS = 60000;
+ }
+ g_dispatcher().cycleEvent(
+ marketItemsPriceIntervalMS, [this] { loadItemsPrice(); }, "Game::loadItemsPrice"
+ );
+ }
}
GameState_t Game::getGameState() const {
@@ -580,18 +587,11 @@ void Game::setGameState(GameState_t newState) {
}
}
-bool Game::loadItemsPrice() {
+void Game::loadItemsPrice() {
IOMarket::getInstance().updateStatistics();
- std::ostringstream query, marketQuery;
- query << "SELECT DISTINCT `itemtype` FROM `market_offers`;";
-
- Database &db = Database::getInstance();
- DBResult_ptr result = db.storeQuery(query.str());
- if (!result) {
- return false;
- }
- auto stats = IOMarket::getInstance().getPurchaseStatistics();
+ // Update purchased offers (market_history)
+ const auto &stats = IOMarket::getInstance().getPurchaseStatistics();
for (const auto &[itemId, itemStats] : stats) {
std::map tierToPrice;
for (const auto &[tier, tierStats] : itemStats) {
@@ -600,12 +600,12 @@ bool Game::loadItemsPrice() {
}
itemsPriceMap[itemId] = tierToPrice;
}
+
+ // Update active buy offers (market_offers)
auto offers = IOMarket::getInstance().getActiveOffers(MARKETACTION_BUY);
for (const auto &offer : offers) {
itemsPriceMap[offer.itemId][offer.tier] = std::max(itemsPriceMap[offer.itemId][offer.tier], offer.price);
}
-
- return true;
}
void Game::loadMainMap(const std::string &filename) {
@@ -5165,6 +5165,23 @@ void Game::playerBuyItem(uint32_t playerId, uint16_t itemId, uint8_t count, uint
return;
}
+ if (inBackpacks || it.isContainer()) {
+ uint32_t maxContainer = static_cast(g_configManager().getNumber(MAX_CONTAINER, __FUNCTION__));
+ auto backpack = player->getInventoryItem(CONST_SLOT_BACKPACK);
+ auto mainBackpack = backpack ? backpack->getContainer() : nullptr;
+
+ if (mainBackpack && mainBackpack->getContainerHoldingCount() >= maxContainer) {
+ player->sendCancelMessage(RETURNVALUE_CONTAINERISFULL);
+ return;
+ }
+
+ std::shared_ptr tile = player->getTile();
+ if (tile && tile->getItemCount() >= 20) {
+ player->sendCancelMessage(RETURNVALUE_CONTAINERISFULL);
+ return;
+ }
+ }
+
merchant->onPlayerBuyItem(player, it.id, count, amount, ignoreCap, inBackpacks);
player->updateUIExhausted();
}
@@ -5756,21 +5773,21 @@ void Game::playerRequestAddVip(uint32_t playerId, const std::string &name) {
}
if (specialVip && !player->hasFlag(PlayerFlags_t::SpecialVIP)) {
- player->sendTextMessage(MESSAGE_FAILURE, "You can not add this player->");
+ player->sendTextMessage(MESSAGE_FAILURE, "You can not add this player");
return;
}
- player->addVIP(guid, formattedName, VIPSTATUS_OFFLINE);
+ player->vip()->add(guid, formattedName, VipStatus_t::Offline);
} else {
if (vipPlayer->hasFlag(PlayerFlags_t::SpecialVIP) && !player->hasFlag(PlayerFlags_t::SpecialVIP)) {
- player->sendTextMessage(MESSAGE_FAILURE, "You can not add this player->");
+ player->sendTextMessage(MESSAGE_FAILURE, "You can not add this player");
return;
}
if (!vipPlayer->isInGhostMode() || player->isAccessPlayer()) {
- player->addVIP(vipPlayer->getGUID(), vipPlayer->getName(), vipPlayer->statusVipList);
+ player->vip()->add(vipPlayer->getGUID(), vipPlayer->getName(), vipPlayer->vip()->getStatus());
} else {
- player->addVIP(vipPlayer->getGUID(), vipPlayer->getName(), VIPSTATUS_OFFLINE);
+ player->vip()->add(vipPlayer->getGUID(), vipPlayer->getName(), VipStatus_t::Offline);
}
}
}
@@ -5781,16 +5798,16 @@ void Game::playerRequestRemoveVip(uint32_t playerId, uint32_t guid) {
return;
}
- player->removeVIP(guid);
+ player->vip()->remove(guid);
}
-void Game::playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string &description, uint32_t icon, bool notify) {
+void Game::playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string &description, uint32_t icon, bool notify, std::vector vipGroupsId) {
std::shared_ptr player = getPlayerByID(playerId);
if (!player) {
return;
}
- player->editVIP(guid, description, icon, notify);
+ player->vip()->edit(guid, description, icon, notify, vipGroupsId);
}
void Game::playerApplyImbuement(uint32_t playerId, uint16_t imbuementid, uint8_t slot, bool protectionCharm) {
diff --git a/src/game/game.hpp b/src/game/game.hpp
index 554c418a2c8..3a199254259 100644
--- a/src/game/game.hpp
+++ b/src/game/game.hpp
@@ -60,7 +60,6 @@ static constexpr int32_t EVENT_DECAYINTERVAL = 250;
static constexpr int32_t EVENT_DECAY_BUCKETS = 4;
static constexpr int32_t EVENT_FORGEABLEMONSTERCHECKINTERVAL = 300000;
static constexpr int32_t EVENT_LUA_GARBAGE_COLLECTION = 60000 * 10; // 10min
-static constexpr int32_t EVENT_REFRESH_MARKET_PRICES = 60000; // 1min
static constexpr std::chrono::minutes CACHE_EXPIRATION_TIME { 10 }; // 10min
static constexpr std::chrono::minutes HIGHSCORE_CACHE_EXPIRATION_TIME { 10 }; // 10min
@@ -394,7 +393,7 @@ class Game {
void playerRequestAddVip(uint32_t playerId, const std::string &name);
void playerRequestRemoveVip(uint32_t playerId, uint32_t guid);
- void playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string &description, uint32_t icon, bool notify);
+ void playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string &description, uint32_t icon, bool notify, std::vector vipGroupsId);
void playerApplyImbuement(uint32_t playerId, uint16_t imbuementid, uint8_t slot, bool protectionCharm);
void playerClearImbuement(uint32_t playerid, uint8_t slot);
void playerCloseImbuementWindow(uint32_t playerid);
@@ -517,7 +516,7 @@ class Game {
return lightHour;
}
- bool loadItemsPrice();
+ void loadItemsPrice();
void loadMotdNum();
void saveMotdNum() const;
diff --git a/src/game/scheduling/dispatcher.cpp b/src/game/scheduling/dispatcher.cpp
index ec999848a92..7cff69d66bf 100644
--- a/src/game/scheduling/dispatcher.cpp
+++ b/src/game/scheduling/dispatcher.cpp
@@ -23,10 +23,10 @@ Dispatcher &Dispatcher::getInstance() {
void Dispatcher::init() {
UPDATE_OTSYS_TIME();
- threadPool.addLoad([this] {
+ threadPool.detach_task([this] {
std::unique_lock asyncLock(dummyMutex);
- while (!threadPool.getIoContext().stopped()) {
+ while (!threadPool.isStopped()) {
UPDATE_OTSYS_TIME();
executeEvents();
@@ -60,7 +60,7 @@ void Dispatcher::executeParallelEvents(std::vector &tasks, const uint8_t g
std::atomic_bool isTasksCompleted = false;
for (const auto &task : tasks) {
- threadPool.addLoad([groupId, &task, &isTasksCompleted, &totalTaskSize] {
+ threadPool.detach_task([groupId, &task, &isTasksCompleted, &totalTaskSize] {
dispacherContext.type = DispatcherType::AsyncEvent;
dispacherContext.group = static_cast(groupId);
dispacherContext.taskName = task.getContext();
diff --git a/src/game/scheduling/dispatcher.hpp b/src/game/scheduling/dispatcher.hpp
index d9e26a0b5d5..a6cbc8dd6dd 100644
--- a/src/game/scheduling/dispatcher.hpp
+++ b/src/game/scheduling/dispatcher.hpp
@@ -84,7 +84,7 @@ class Dispatcher {
public:
explicit Dispatcher(ThreadPool &threadPool) :
threadPool(threadPool) {
- threads.reserve(threadPool.getNumberOfThreads() + 1);
+ threads.reserve(threadPool.get_thread_count() + 1);
for (uint_fast16_t i = 0; i < threads.capacity(); ++i) {
threads.emplace_back(std::make_unique());
}
diff --git a/src/game/scheduling/save_manager.cpp b/src/game/scheduling/save_manager.cpp
index 2c1eff6657c..fbad528598b 100644
--- a/src/game/scheduling/save_manager.cpp
+++ b/src/game/scheduling/save_manager.cpp
@@ -41,7 +41,7 @@ void SaveManager::scheduleAll() {
return;
}
- threadPool.addLoad([this, scheduledAt]() {
+ threadPool.detach_task([this, scheduledAt]() {
if (m_scheduledAt.load() != scheduledAt) {
logger.warn("Skipping save for server because another save has been scheduled.");
return;
@@ -69,7 +69,7 @@ void SaveManager::schedulePlayer(std::weak_ptr playerPtr) {
logger.debug("Scheduling player {} for saving.", playerToSave->getName());
auto scheduledAt = std::chrono::steady_clock::now();
m_playerMap[playerToSave->getGUID()] = scheduledAt;
- threadPool.addLoad([this, playerPtr, scheduledAt]() {
+ threadPool.detach_task([this, playerPtr, scheduledAt]() {
auto player = playerPtr.lock();
if (!player) {
logger.debug("Skipping save for player because player is no longer online.");
diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp
index bcbfc531468..6273f8fefbb 100644
--- a/src/io/functions/iologindata_load_player.cpp
+++ b/src/io/functions/iologindata_load_player.cpp
@@ -678,12 +678,34 @@ void IOLoginDataLoad::loadPlayerVip(std::shared_ptr player, DBResult_ptr
return;
}
+ uint32_t accountId = player->getAccountId();
+
Database &db = Database::getInstance();
- std::ostringstream query;
- query << "SELECT `player_id` FROM `account_viplist` WHERE `account_id` = " << player->getAccountId();
- if ((result = db.storeQuery(query.str()))) {
+ std::string query = fmt::format("SELECT `player_id` FROM `account_viplist` WHERE `account_id` = {}", accountId);
+ if ((result = db.storeQuery(query))) {
+ do {
+ player->vip()->addInternal(result->getNumber("player_id"));
+ } while (result->next());
+ }
+
+ query = fmt::format("SELECT `id`, `name`, `customizable` FROM `account_vipgroups` WHERE `account_id` = {}", accountId);
+ if ((result = db.storeQuery(query))) {
+ do {
+ player->vip()->addGroupInternal(
+ result->getNumber("id"),
+ result->getString("name"),
+ result->getNumber("customizable") == 0 ? false : true
+ );
+ } while (result->next());
+ }
+
+ query = fmt::format("SELECT `player_id`, `vipgroup_id` FROM `account_vipgrouplist` WHERE `account_id` = {}", accountId);
+ if ((result = db.storeQuery(query))) {
do {
- player->addVIPInternal(result->getNumber("player_id"));
+ player->vip()->addGuidToGroupInternal(
+ result->getNumber("vipgroup_id"),
+ result->getNumber("player_id")
+ );
} while (result->next());
}
}
diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp
index 46426451ddc..122ade42c72 100644
--- a/src/io/iologindata.cpp
+++ b/src/io/iologindata.cpp
@@ -352,10 +352,9 @@ bool IOLoginData::hasBiddedOnHouse(uint32_t guid) {
std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) {
std::forward_list entries;
- std::ostringstream query;
- query << "SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = " << accountId;
+ std::string query = fmt::format("SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {}", accountId);
- DBResult_ptr result = Database::getInstance().storeQuery(query.str());
+ DBResult_ptr result = Database::getInstance().storeQuery(query);
if (result) {
do {
entries.emplace_front(
@@ -371,27 +370,69 @@ std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) {
}
void IOLoginData::addVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify) {
- Database &db = Database::getInstance();
-
- std::ostringstream query;
- query << "INSERT INTO `account_viplist` (`account_id`, `player_id`, `description`, `icon`, `notify`) VALUES (" << accountId << ',' << guid << ',' << db.escapeString(description) << ',' << icon << ',' << notify << ')';
- if (!db.executeQuery(query.str())) {
- g_logger().error("Failed to add VIP entry for account %u. QUERY: %s", accountId, query.str().c_str());
+ std::string query = fmt::format("INSERT INTO `account_viplist` (`account_id`, `player_id`, `description`, `icon`, `notify`) VALUES ({}, {}, {}, {}, {})", accountId, guid, g_database().escapeString(description), icon, notify);
+ if (!g_database().executeQuery(query)) {
+ g_logger().error("Failed to add VIP entry for account {}. QUERY: {}", accountId, query.c_str());
}
}
void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify) {
- Database &db = Database::getInstance();
-
- std::ostringstream query;
- query << "UPDATE `account_viplist` SET `description` = " << db.escapeString(description) << ", `icon` = " << icon << ", `notify` = " << notify << " WHERE `account_id` = " << accountId << " AND `player_id` = " << guid;
- if (!db.executeQuery(query.str())) {
- g_logger().error("Failed to edit VIP entry for account %u. QUERY: %s", accountId, query.str().c_str());
+ std::string query = fmt::format("UPDATE `account_viplist` SET `description` = {}, `icon` = {}, `notify` = {} WHERE `account_id` = {} AND `player_id` = {}", g_database().escapeString(description), icon, notify, accountId, guid);
+ if (!g_database().executeQuery(query)) {
+ g_logger().error("Failed to edit VIP entry for account {}. QUERY: {}", accountId, query.c_str());
}
}
void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) {
- std::ostringstream query;
- query << "DELETE FROM `account_viplist` WHERE `account_id` = " << accountId << " AND `player_id` = " << guid;
- Database::getInstance().executeQuery(query.str());
+ std::string query = fmt::format("DELETE FROM `account_viplist` WHERE `account_id` = {} AND `player_id` = {}", accountId, guid);
+ g_database().executeQuery(query);
+}
+
+std::forward_list IOLoginData::getVIPGroupEntries(uint32_t accountId, uint32_t guid) {
+ std::forward_list entries;
+
+ std::string query = fmt::format("SELECT `id`, `name`, `customizable` FROM `account_vipgroups` WHERE `account_id` = {}", accountId);
+
+ DBResult_ptr result = g_database().storeQuery(query);
+ if (result) {
+ do {
+ entries.emplace_front(
+ result->getNumber("id"),
+ result->getString("name"),
+ result->getNumber("customizable") == 0 ? false : true
+ );
+ } while (result->next());
+ }
+ return entries;
+}
+
+void IOLoginData::addVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable) {
+ std::string query = fmt::format("INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) VALUES ({}, {}, {}, {})", groupId, accountId, g_database().escapeString(groupName), customizable);
+ if (!g_database().executeQuery(query)) {
+ g_logger().error("Failed to add VIP Group entry for account {} and group {}. QUERY: {}", accountId, groupId, query.c_str());
+ }
+}
+
+void IOLoginData::editVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable) {
+ std::string query = fmt::format("UPDATE `account_vipgroups` SET `name` = {}, `customizable` = {} WHERE `id` = {} AND `account_id` = {}", g_database().escapeString(groupName), customizable, groupId, accountId);
+ if (!g_database().executeQuery(query)) {
+ g_logger().error("Failed to update VIP Group entry for account {} and group {}. QUERY: {}", accountId, groupId, query.c_str());
+ }
+}
+
+void IOLoginData::removeVIPGroupEntry(uint8_t groupId, uint32_t accountId) {
+ std::string query = fmt::format("DELETE FROM `account_vipgroups` WHERE `id` = {} AND `account_id` = {}", groupId, accountId);
+ g_database().executeQuery(query);
+}
+
+void IOLoginData::addGuidVIPGroupEntry(uint8_t groupId, uint32_t accountId, uint32_t guid) {
+ std::string query = fmt::format("INSERT INTO `account_vipgrouplist` (`account_id`, `player_id`, `vipgroup_id`) VALUES ({}, {}, {})", accountId, guid, groupId);
+ if (!g_database().executeQuery(query)) {
+ g_logger().error("Failed to add guid VIP Group entry for account {}, player {} and group {}. QUERY: {}", accountId, guid, groupId, query.c_str());
+ }
+}
+
+void IOLoginData::removeGuidVIPGroupEntry(uint32_t accountId, uint32_t guid) {
+ std::string query = fmt::format("DELETE FROM `account_vipgrouplist` WHERE `account_id` = {} AND `player_id` = {}", accountId, guid);
+ g_database().executeQuery(query);
}
diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp
index b9fcc124ea0..be414739837 100644
--- a/src/io/iologindata.hpp
+++ b/src/io/iologindata.hpp
@@ -36,6 +36,13 @@ class IOLoginData {
static void editVIPEntry(uint32_t accountId, uint32_t guid, const std::string &description, uint32_t icon, bool notify);
static void removeVIPEntry(uint32_t accountId, uint32_t guid);
+ static std::forward_list getVIPGroupEntries(uint32_t accountId, uint32_t guid);
+ static void addVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable);
+ static void editVIPGroupEntry(uint8_t groupId, uint32_t accountId, const std::string &groupName, bool customizable);
+ static void removeVIPGroupEntry(uint8_t groupId, uint32_t accountId);
+ static void addGuidVIPGroupEntry(uint8_t groupId, uint32_t accountId, uint32_t guid);
+ static void removeGuidVIPGroupEntry(uint32_t accountId, uint32_t guid);
+
private:
static bool savePlayerGuard(std::shared_ptr player);
};
diff --git a/src/io/iomarket.cpp b/src/io/iomarket.cpp
index ca1bdb209d2..a0253e2ea01 100644
--- a/src/io/iomarket.cpp
+++ b/src/io/iomarket.cpp
@@ -29,10 +29,14 @@ uint8_t IOMarket::getTierFromDatabaseTable(const std::string &string) {
MarketOfferList IOMarket::getActiveOffers(MarketAction_t action) {
MarketOfferList offerList;
- std::ostringstream query;
- query << "SELECT `id`, `amount`, `price`, `tier`, `created`, `anonymous`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` FROM `market_offers` WHERE `sale` = " << action;
+ std::string query = fmt::format(
+ "SELECT `id`, `itemtype`, `amount`, `price`, `tier`, `created`, `anonymous`, "
+ "(SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `player_name` "
+ "FROM `market_offers` WHERE `sale` = {}",
+ action
+ );
- DBResult_ptr result = Database::getInstance().storeQuery(query.str());
+ DBResult_ptr result = g_database().storeQuery(query);
if (!result) {
return offerList;
}
@@ -41,6 +45,7 @@ MarketOfferList IOMarket::getActiveOffers(MarketAction_t action) {
do {
MarketOffer offer;
+ offer.itemId = result->getNumber("itemtype");
offer.amount = result->getNumber("amount");
offer.price = result->getNumber("price");
offer.timestamp = result->getNumber("created") + marketOfferDuration;
@@ -71,6 +76,7 @@ MarketOfferList IOMarket::getActiveOffers(MarketAction_t action, uint16_t itemId
do {
MarketOffer offer;
+ offer.itemId = itemId;
offer.amount = result->getNumber("amount");
offer.price = result->getNumber("price");
offer.timestamp = result->getNumber("created") + marketOfferDuration;
@@ -333,9 +339,15 @@ bool IOMarket::moveOfferToHistory(uint32_t offerId, MarketOfferState_t state) {
}
void IOMarket::updateStatistics() {
- std::ostringstream query;
- query << "SELECT `sale` AS `sale`, `itemtype` AS `itemtype`, COUNT(`price`) AS `num`, MIN(`price`) AS `min`, MAX(`price`) AS `max`, SUM(`price`) AS `sum`, `tier` AS `tier` FROM `market_history` WHERE `state` = " << OFFERSTATE_ACCEPTED << " GROUP BY `itemtype`, `sale`, `tier`";
- DBResult_ptr result = Database::getInstance().storeQuery(query.str());
+ auto query = fmt::format(
+ "SELECT sale, itemtype, COUNT(price) AS num, MIN(price) AS min, MAX(price) AS max, SUM(price) AS sum, tier "
+ "FROM market_history "
+ "WHERE state = '{}' "
+ "GROUP BY itemtype, sale, tier",
+ OFFERSTATE_ACCEPTED
+ );
+
+ DBResult_ptr result = g_database().storeQuery(query);
if (!result) {
return;
}
diff --git a/src/io/iomarket.hpp b/src/io/iomarket.hpp
index 33180053584..4292651fb47 100644
--- a/src/io/iomarket.hpp
+++ b/src/io/iomarket.hpp
@@ -14,8 +14,6 @@
#include "lib/di/container.hpp"
class IOMarket {
- using StatisticsMap = std::map>;
-
public:
IOMarket() = default;
@@ -43,10 +41,11 @@ class IOMarket {
void updateStatistics();
- StatisticsMap getPurchaseStatistics() const {
+ using StatisticsMap = std::map>;
+ const StatisticsMap &getPurchaseStatistics() const {
return purchaseStatistics;
}
- StatisticsMap getSaleStatistics() const {
+ const StatisticsMap &getSaleStatistics() const {
return saleStatistics;
}
diff --git a/src/items/tile.cpp b/src/items/tile.cpp
index 11e3fafd6fd..f2006f627e1 100644
--- a/src/items/tile.cpp
+++ b/src/items/tile.cpp
@@ -1272,7 +1272,7 @@ void Tile::removeThing(std::shared_ptr thing, uint32_t count) {
}
void Tile::removeCreature(std::shared_ptr creature) {
- g_game().map.getQTNode(tilePos.x, tilePos.y)->removeCreature(creature);
+ g_game().map.getMapSector(tilePos.x, tilePos.y)->removeCreature(creature);
removeThing(creature, 0);
}
diff --git a/src/lib/thread/README.md b/src/lib/thread/README.md
index ddb5cd6239c..52792d07b3d 100644
--- a/src/lib/thread/README.md
+++ b/src/lib/thread/README.md
@@ -20,7 +20,7 @@ int main() {
ThreadPool &pool = inject(); // preferrably uses constructor injection or setter injection.
// Post a task to the thread pool
- pool.addLoad([]() {
+ pool.detach_task([]() {
std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
});
}
diff --git a/src/lib/thread/thread_pool.cpp b/src/lib/thread/thread_pool.cpp
index 3971d3a6a02..702f0c05504 100644
--- a/src/lib/thread/thread_pool.cpp
+++ b/src/lib/thread/thread_pool.cpp
@@ -14,84 +14,27 @@
#include "game/game.hpp"
#include "utils/tools.hpp"
+/**
+ * Regardless of how many cores your computer have, we want at least
+ * 4 threads because, even though they won't improve processing they
+ * will make processing non-blocking in some way and that would allow
+ * single core computers to process things concurrently, but not in parallel.
+ */
+
#ifndef DEFAULT_NUMBER_OF_THREADS
#define DEFAULT_NUMBER_OF_THREADS 4
#endif
ThreadPool::ThreadPool(Logger &logger) :
- logger(logger) {
+ logger(logger), BS::thread_pool(std::max(getNumberOfCores(), DEFAULT_NUMBER_OF_THREADS)) {
start();
}
void ThreadPool::start() {
- logger.info("Setting up thread pool");
-
- /**
- * Regardless of how many cores your computer have, we want at least
- * 4 threads because, even though they won't improve processing they
- * will make processing non-blocking in some way and that would allow
- * single core computers to process things concurrently, but not in parallel.
- */
- nThreads = std::max(static_cast(getNumberOfCores()), DEFAULT_NUMBER_OF_THREADS);
-
- for (std::size_t i = 0; i < nThreads; ++i) {
- threads.emplace_back([this] { ioService.run(); });
- }
-
- logger.info("Running with {} threads.", threads.size());
+ logger.info("Running with {} threads.", get_thread_count());
}
void ThreadPool::shutdown() {
- if (ioService.stopped()) {
- return;
- }
-
+ stopped = true;
logger.info("Shutting down thread pool...");
-
- ioService.stop();
-
- std::vector> futures;
- for (std::size_t i = 0; i < threads.size(); i++) {
- logger.debug("Joining thread {}/{}.", i + 1, threads.size());
-
- if (threads[i].joinable()) {
- futures.emplace_back(std::async(std::launch::async, [&]() {
- threads[i].join();
- }));
- }
- }
-
- std::future_status status = std::future_status::timeout;
- auto timeout = std::chrono::seconds(5);
- auto start = std::chrono::steady_clock::now();
- int tries = 0;
- while (status == std::future_status::timeout && std::chrono::steady_clock::now() - start < timeout) {
- tries++;
- if (tries > 5) {
- break;
- }
- for (auto &future : futures) {
- status = future.wait_for(std::chrono::seconds(0));
- if (status != std::future_status::timeout) {
- break;
- }
- }
- }
-}
-
-asio::io_context &ThreadPool::getIoContext() {
- return ioService;
-}
-
-void ThreadPool::addLoad(const std::function &load) {
- asio::post(ioService, [this, load]() {
- if (ioService.stopped()) {
- if (g_game().getGameState() != GAME_STATE_SHUTDOWN) {
- logger.error("Shutting down, cannot execute task.");
- }
- return;
- }
-
- load();
- });
}
diff --git a/src/lib/thread/thread_pool.hpp b/src/lib/thread/thread_pool.hpp
index e36d8beca57..ea24d3486cb 100644
--- a/src/lib/thread/thread_pool.hpp
+++ b/src/lib/thread/thread_pool.hpp
@@ -9,8 +9,9 @@
#pragma once
#include "lib/logging/logger.hpp"
+#include "BS_thread_pool.hpp"
-class ThreadPool {
+class ThreadPool : public BS::thread_pool {
public:
explicit ThreadPool(Logger &logger);
@@ -20,12 +21,6 @@ class ThreadPool {
void start();
void shutdown();
- asio::io_context &getIoContext();
- void addLoad(const std::function &load);
-
- uint16_t getNumberOfThreads() const {
- return nThreads;
- }
static int16_t getThreadId() {
static std::atomic_int16_t lastId = -1;
@@ -39,11 +34,11 @@ class ThreadPool {
return id;
};
+ bool isStopped() const {
+ return stopped;
+ }
+
private:
Logger &logger;
- asio::io_context ioService;
- std::vector threads;
- asio::io_context::work work { ioService };
-
- uint16_t nThreads = 0;
+ bool stopped = false;
};
diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp
index 0fdbc96f4d6..4858b3ab264 100644
--- a/src/lua/functions/core/game/game_functions.cpp
+++ b/src/lua/functions/core/game/game_functions.cpp
@@ -191,6 +191,17 @@ int GameFunctions::luaGameloadMapChunk(lua_State* L) {
return 0;
}
+int GameFunctions::luaGameGetExperienceForLevel(lua_State* L) {
+ // Game.getExperienceForLevel(level)
+ const uint32_t level = getNumber(L, 1);
+ if (level == 0) {
+ reportErrorFunc("Level must be greater than 0.");
+ } else {
+ lua_pushnumber(L, Player::getExpForLevel(level));
+ }
+ return 1;
+}
+
int GameFunctions::luaGameGetMonsterCount(lua_State* L) {
// Game.getMonsterCount()
lua_pushnumber(L, g_game().getMonstersOnline());
diff --git a/src/lua/functions/core/game/game_functions.hpp b/src/lua/functions/core/game/game_functions.hpp
index 7f3b642e97d..70e81061c9c 100644
--- a/src/lua/functions/core/game/game_functions.hpp
+++ b/src/lua/functions/core/game/game_functions.hpp
@@ -28,6 +28,7 @@ class GameFunctions final : LuaScriptInterface {
registerMethod(L, "Game", "loadMap", GameFunctions::luaGameLoadMap);
registerMethod(L, "Game", "loadMapChunk", GameFunctions::luaGameloadMapChunk);
+ registerMethod(L, "Game", "getExperienceForLevel", GameFunctions::luaGameGetExperienceForLevel);
registerMethod(L, "Game", "getMonsterCount", GameFunctions::luaGameGetMonsterCount);
registerMethod(L, "Game", "getPlayerCount", GameFunctions::luaGameGetPlayerCount);
registerMethod(L, "Game", "getNpcCount", GameFunctions::luaGameGetNpcCount);
@@ -103,6 +104,7 @@ class GameFunctions final : LuaScriptInterface {
static int luaGameLoadMap(lua_State* L);
static int luaGameloadMapChunk(lua_State* L);
+ static int luaGameGetExperienceForLevel(lua_State* L);
static int luaGameGetMonsterCount(lua_State* L);
static int luaGameGetPlayerCount(lua_State* L);
static int luaGameGetNpcCount(lua_State* L);
diff --git a/src/lua/functions/core/game/global_functions.cpp b/src/lua/functions/core/game/global_functions.cpp
index 0403e88dd95..af89c1854f7 100644
--- a/src/lua/functions/core/game/global_functions.cpp
+++ b/src/lua/functions/core/game/global_functions.cpp
@@ -712,7 +712,7 @@ int GlobalFunctions::luaSaveServer(lua_State* L) {
}
int GlobalFunctions::luaCleanMap(lua_State* L) {
- lua_pushnumber(L, Map::clean());
+ lua_pushnumber(L, g_game().map.clean());
return 1;
}
diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp
index 7f4d96f3773..5f14db6ea99 100644
--- a/src/lua/functions/creatures/player/player_functions.cpp
+++ b/src/lua/functions/creatures/player/player_functions.cpp
@@ -1744,6 +1744,11 @@ int PlayerFunctions::luaPlayerSetStorageValue(lua_State* L) {
return 1;
}
+ if (key == 0) {
+ reportErrorFunc("Storage key is nil");
+ return 1;
+ }
+
if (player) {
player->addStorageValue(key, value);
pushBoolean(L, true);
@@ -1762,6 +1767,7 @@ int PlayerFunctions::luaPlayerGetStorageValueByName(lua_State* L) {
return 0;
}
+ g_logger().warn("The function 'player:getStorageValueByName' is deprecated and will be removed in future versions, please use KV system");
auto name = getString(L, 2);
lua_pushnumber(L, player->getStorageValueByName(name));
return 1;
@@ -1776,6 +1782,7 @@ int PlayerFunctions::luaPlayerSetStorageValueByName(lua_State* L) {
return 0;
}
+ g_logger().warn("The function 'player:setStorageValueByName' is deprecated and will be removed in future versions, please use KV system");
auto storageName = getString(L, 2);
int32_t value = getNumber(L, 3);
@@ -3017,14 +3024,14 @@ int PlayerFunctions::luaPlayerSetGhostMode(lua_State* L) {
if (player->isInGhostMode()) {
for (const auto &it : g_game().getPlayers()) {
if (!it.second->isAccessPlayer()) {
- it.second->notifyStatusChange(player, VIPSTATUS_OFFLINE);
+ it.second->vip()->notifyStatusChange(player, VipStatus_t::Offline);
}
}
IOLoginData::updateOnlineStatus(player->getGUID(), false);
} else {
for (const auto &it : g_game().getPlayers()) {
if (!it.second->isAccessPlayer()) {
- it.second->notifyStatusChange(player, player->statusVipList);
+ it.second->vip()->notifyStatusChange(player, player->vip()->getStatus());
}
}
IOLoginData::updateOnlineStatus(player->getGUID(), true);
diff --git a/src/map/CMakeLists.txt b/src/map/CMakeLists.txt
index 7d744950c1f..cab0eb03b15 100644
--- a/src/map/CMakeLists.txt
+++ b/src/map/CMakeLists.txt
@@ -2,7 +2,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE
house/house.cpp
house/housetile.cpp
utils/astarnodes.cpp
- utils/qtreenode.cpp
+ utils/mapsector.cpp
map.cpp
mapcache.cpp
spectators.cpp
diff --git a/src/map/map.cpp b/src/map/map.cpp
index fcf14262c30..8b8cebeb539 100644
--- a/src/map/map.cpp
+++ b/src/map/map.cpp
@@ -163,7 +163,7 @@ std::shared_ptr Map::getLoadedTile(uint16_t x, uint16_t y, uint8_t z) {
return nullptr;
}
- const auto leaf = getQTNode(x, y);
+ const auto leaf = getMapSector(x, y);
if (!leaf) {
return nullptr;
}
@@ -182,12 +182,12 @@ std::shared_ptr Map::getTile(uint16_t x, uint16_t y, uint8_t z) {
return nullptr;
}
- const auto leaf = getQTNode(x, y);
- if (!leaf) {
+ const auto sector = getMapSector(x, y);
+ if (!sector) {
return nullptr;
}
- const auto &floor = leaf->getFloor(z);
+ const auto &floor = sector->getFloor(z);
if (!floor) {
return nullptr;
}
@@ -215,10 +215,10 @@ void Map::setTile(uint16_t x, uint16_t y, uint8_t z, std::shared_ptr newTi
return;
}
- if (const auto leaf = getQTNode(x, y)) {
- leaf->createFloor(z)->setTile(x, y, newTile);
+ if (const auto sector = getMapSector(x, y)) {
+ sector->createFloor(z)->setTile(x, y, newTile);
} else {
- root.getBestLeaf(x, y, 15)->createFloor(z)->setTile(x, y, newTile);
+ getBestMapSector(x, y)->createFloor(z)->setTile(x, y, newTile);
}
}
@@ -315,18 +315,27 @@ bool Map::placeCreature(const Position ¢erPos, std::shared_ptr cre
toCylinder->internalAddThing(creature);
const Position &dest = toCylinder->getPosition();
- getQTNode(dest.x, dest.y)->addCreature(creature);
+ getMapSector(dest.x, dest.y)->addCreature(creature);
return true;
}
void Map::moveCreature(const std::shared_ptr &creature, const std::shared_ptr &newTile, bool forceTeleport /* = false*/) {
- auto oldTile = creature->getTile();
+ if (!creature || !newTile) {
+ return;
+ }
- Position oldPos = oldTile->getPosition();
- Position newPos = newTile->getPosition();
+ const auto &oldTile = creature->getTile();
+
+ if (!oldTile) {
+ return;
+ }
+
+ const auto &oldPos = oldTile->getPosition();
+ const auto &newPos = newTile->getPosition();
const auto &fromZones = oldTile->getZones();
const auto &toZones = newTile->getZones();
+
if (auto ret = g_game().beforeCreatureZoneChange(creature, fromZones, toZones); ret != RETURNVALUE_NOERROR) {
return;
}
@@ -351,13 +360,13 @@ void Map::moveCreature(const std::shared_ptr &creature, const std::sha
// remove the creature
oldTile->removeThing(creature, 0);
- auto leaf = getQTNode(oldPos.x, oldPos.y);
- auto new_leaf = getQTNode(newPos.x, newPos.y);
+ MapSector* old_sector = getMapSector(oldPos.x, oldPos.y);
+ MapSector* new_sector = getMapSector(newPos.x, newPos.y);
// Switch the node ownership
- if (leaf != new_leaf) {
- leaf->removeCreature(creature);
- new_leaf->addCreature(creature);
+ if (old_sector != new_sector) {
+ old_sector->removeCreature(creature);
+ new_sector->addCreature(creature);
}
// add the creature
@@ -687,19 +696,22 @@ bool Map::getPathMatching(const std::shared_ptr &creature, const Posit
uint32_t Map::clean() {
uint64_t start = OTSYS_TIME();
- size_t tiles = 0;
+ size_t qntTiles = 0;
if (g_game().getGameState() == GAME_STATE_NORMAL) {
g_game().setGameState(GAME_STATE_MAINTAIN);
}
- std::vector> toRemove;
+ ItemVector toRemove;
+ toRemove.reserve(128);
+
for (const auto &tile : g_game().getTilesToClean()) {
if (!tile) {
continue;
}
+
if (const auto items = tile->getItemList()) {
- ++tiles;
+ ++qntTiles;
for (const auto &item : *items) {
if (item->isCleanable()) {
toRemove.emplace_back(item);
@@ -708,11 +720,11 @@ uint32_t Map::clean() {
}
}
+ const size_t count = toRemove.size();
for (const auto &item : toRemove) {
g_game().internalRemoveItem(item, -1);
}
- size_t count = toRemove.size();
g_game().clearTilesToClean();
if (g_game().getGameState() == GAME_STATE_MAINTAIN) {
@@ -720,6 +732,6 @@ uint32_t Map::clean() {
}
uint64_t end = OTSYS_TIME();
- g_logger().info("CLEAN: Removed {} item{} from {} tile{} in {} seconds", count, (count != 1 ? "s" : ""), tiles, (tiles != 1 ? "s" : ""), (end - start) / (1000.f));
+ g_logger().info("CLEAN: Removed {} item{} from {} tile{} in {} seconds", count, (count != 1 ? "s" : ""), qntTiles, (qntTiles != 1 ? "s" : ""), (end - start) / (1000.f));
return count;
}
diff --git a/src/map/map.hpp b/src/map/map.hpp
index 0894853e30e..e57328e12b3 100644
--- a/src/map/map.hpp
+++ b/src/map/map.hpp
@@ -29,9 +29,9 @@ class FrozenPathingConditionCall;
* Map class.
* Holds all the actual map-data
*/
-class Map : protected MapCache {
+class Map : public MapCache {
public:
- static uint32_t clean();
+ uint32_t clean();
std::filesystem::path getPath() const {
return path;
@@ -131,10 +131,6 @@ class Map : protected MapCache {
std::map waypoints;
- QTreeLeafNode* getQTNode(uint16_t x, uint16_t y) {
- return QTreeNode::getLeafStatic(&root, x, y);
- }
-
// Storage made by "loadFromXML" of houses, monsters and npcs for main map
SpawnsMonster spawnsMonster;
SpawnsNpc spawnsNpc;
diff --git a/src/map/map_const.hpp b/src/map/map_const.hpp
index 10f814d7f8b..109641d6bfe 100644
--- a/src/map/map_const.hpp
+++ b/src/map/map_const.hpp
@@ -18,6 +18,7 @@ static constexpr int8_t MAP_MAX_LAYERS = 16;
static constexpr int8_t MAP_INIT_SURFACE_LAYER = 7; // (MAP_MAX_LAYERS / 2) -1
static constexpr int8_t MAP_LAYER_VIEW_LIMIT = 2;
-static constexpr int32_t FLOOR_BITS = 3;
-static constexpr int32_t FLOOR_SIZE = (1 << FLOOR_BITS);
-static constexpr int32_t FLOOR_MASK = (FLOOR_SIZE - 1);
+// SECTOR_SIZE must be power of 2 value
+// The bigger the SECTOR_SIZE is the less hash map collision there should be but it'll consume more memory
+static constexpr int32_t SECTOR_SIZE = 16;
+static constexpr int32_t SECTOR_MASK = SECTOR_SIZE - 1;
diff --git a/src/map/mapcache.cpp b/src/map/mapcache.cpp
index ede4d3fd862..0448615ee99 100644
--- a/src/map/mapcache.cpp
+++ b/src/map/mapcache.cpp
@@ -154,10 +154,10 @@ void MapCache::setBasicTile(uint16_t x, uint16_t y, uint8_t z, const std::shared
}
const auto tile = static_tryGetTileFromCache(newTile);
- if (const auto leaf = QTreeNode::getLeafStatic(&root, x, y)) {
- leaf->createFloor(z)->setTileCache(x, y, tile);
+ if (const auto sector = getMapSector(x, y)) {
+ sector->createFloor(z)->setTileCache(x, y, tile);
} else {
- root.getBestLeaf(x, y, 15)->createFloor(z)->setTileCache(x, y, tile);
+ getBestMapSector(x, y)->createFloor(z)->setTileCache(x, y, tile);
}
}
@@ -165,6 +165,46 @@ std::shared_ptr MapCache::tryReplaceItemFromCache(const std::shared_p
return static_tryGetItemFromCache(ref);
}
+MapSector* MapCache::createMapSector(const uint32_t x, const uint32_t y) {
+ const uint32_t index = x / SECTOR_SIZE | y / SECTOR_SIZE << 16;
+ const auto it = mapSectors.find(index);
+ if (it != mapSectors.end()) {
+ return &it->second;
+ }
+
+ MapSector::newSector = true;
+ return &mapSectors[index];
+}
+
+MapSector* MapCache::getBestMapSector(uint32_t x, uint32_t y) {
+ MapSector::newSector = false;
+ const auto sector = createMapSector(x, y);
+
+ if (MapSector::newSector) {
+ // update north sector
+ if (const auto northSector = getMapSector(x, y - SECTOR_SIZE)) {
+ northSector->sectorS = sector;
+ }
+
+ // update west sector
+ if (const auto westSector = getMapSector(x - SECTOR_SIZE, y)) {
+ westSector->sectorE = sector;
+ }
+
+ // update south sector
+ if (const auto southSector = getMapSector(x, y + SECTOR_SIZE)) {
+ sector->sectorS = southSector;
+ }
+
+ // update east sector
+ if (const auto eastSector = getMapSector(x + SECTOR_SIZE, y)) {
+ sector->sectorE = eastSector;
+ }
+ }
+
+ return sector;
+}
+
void BasicTile::hash(size_t &h) const {
std::array arr = { flags, houseId, type, isStatic };
for (const auto v : arr) {
diff --git a/src/map/mapcache.hpp b/src/map/mapcache.hpp
index bc3e59a700a..429d786972b 100644
--- a/src/map/mapcache.hpp
+++ b/src/map/mapcache.hpp
@@ -10,7 +10,7 @@
#pragma once
#include "items/items_definitions.hpp"
-#include "utils/qtreenode.hpp"
+#include "utils/mapsector.hpp"
class Map;
class Tile;
@@ -79,42 +79,6 @@ struct BasicTile {
#pragma pack()
-struct Floor {
- explicit Floor(uint8_t z) :
- z(z) { }
-
- std::shared_ptr getTile(uint16_t x, uint16_t y) const {
- std::shared_lock sl(mutex);
- return tiles[x & FLOOR_MASK][y & FLOOR_MASK].first;
- }
-
- void setTile(uint16_t x, uint16_t y, std::shared_ptr tile) {
- tiles[x & FLOOR_MASK][y & FLOOR_MASK].first = tile;
- }
-
- std::shared_ptr getTileCache(uint16_t x, uint16_t y) const {
- std::shared_lock sl(mutex);
- return tiles[x & FLOOR_MASK][y & FLOOR_MASK].second;
- }
-
- void setTileCache(uint16_t x, uint16_t y, const std::shared_ptr &newTile) {
- tiles[x & FLOOR_MASK][y & FLOOR_MASK].second = newTile;
- }
-
- uint8_t getZ() const {
- return z;
- }
-
- auto &getMutex() const {
- return mutex;
- }
-
-private:
- std::pair, std::shared_ptr> tiles[FLOOR_SIZE][FLOOR_SIZE] = {};
- mutable std::shared_mutex mutex;
- uint8_t z { 0 };
-};
-
class MapCache {
public:
virtual ~MapCache() = default;
@@ -125,10 +89,31 @@ class MapCache {
void flush();
+ /**
+ * Creates a map sector.
+ * \returns A pointer to that map sector.
+ */
+ MapSector* createMapSector(uint32_t x, uint32_t y);
+ MapSector* getBestMapSector(uint32_t x, uint32_t y);
+
+ /**
+ * Gets a map sector.
+ * \returns A pointer to that map sector.
+ */
+ MapSector* getMapSector(const uint32_t x, const uint32_t y) {
+ const auto it = mapSectors.find(x / SECTOR_SIZE | y / SECTOR_SIZE << 16);
+ return it != mapSectors.end() ? &it->second : nullptr;
+ }
+
+ const MapSector* getMapSector(const uint32_t x, const uint32_t y) const {
+ const auto it = mapSectors.find(x / SECTOR_SIZE | y / SECTOR_SIZE << 16);
+ return it != mapSectors.end() ? &it->second : nullptr;
+ }
+
protected:
std::shared_ptr getOrCreateTileFromCache(const std::unique_ptr &floor, uint16_t x, uint16_t y);
- QTreeNode root;
+ std::unordered_map mapSectors;
private:
void parseItemAttr(const std::shared_ptr &BasicItem, std::shared_ptr
- item);
diff --git a/src/map/spectators.cpp b/src/map/spectators.cpp
index 7c0e80a0412..36cce6d535b 100644
--- a/src/map/spectators.cpp
+++ b/src/map/spectators.cpp
@@ -154,59 +154,57 @@ Spectators Spectators::find(const Position ¢erPos, bool multifloor, bool onl
}
}
- const int_fast32_t min_y = centerPos.y + minRangeY;
- const int_fast32_t min_x = centerPos.x + minRangeX;
- const int_fast32_t max_y = centerPos.y + maxRangeY;
- const int_fast32_t max_x = centerPos.x + maxRangeX;
+ const int32_t min_y = centerPos.y + minRangeY;
+ const int32_t min_x = centerPos.x + minRangeX;
+ const int32_t max_y = centerPos.y + maxRangeY;
+ const int32_t max_x = centerPos.x + maxRangeX;
- const int_fast16_t minoffset = centerPos.getZ() - maxRangeZ;
- const int_fast32_t x1 = std::min(0xFFFF, std::max(0, (min_x + minoffset)));
- const int_fast32_t y1 = std::min(0xFFFF, std::max(0, (min_y + minoffset)));
+ const auto width = static_cast(max_x - min_x);
+ const auto height = static_cast(max_y - min_y);
+ const auto depth = static_cast(maxRangeZ - minRangeZ);
- const int_fast16_t maxoffset = centerPos.getZ() - minRangeZ;
- const int_fast32_t x2 = std::min(0xFFFF, std::max(0, (max_x + maxoffset)));
- const int_fast32_t y2 = std::min(0xFFFF, std::max(0, (max_y + maxoffset)));
+ const int32_t minoffset = centerPos.getZ() - maxRangeZ;
+ const int32_t x1 = std::min(0xFFFF, std::max(0, min_x + minoffset));
+ const int32_t y1 = std::min(0xFFFF, std::max(0, min_y + minoffset));
- const uint_fast16_t startx1 = x1 - (x1 % FLOOR_SIZE);
- const uint_fast16_t starty1 = y1 - (y1 % FLOOR_SIZE);
- const uint_fast16_t endx2 = x2 - (x2 % FLOOR_SIZE);
- const uint_fast16_t endy2 = y2 - (y2 % FLOOR_SIZE);
+ const int32_t maxoffset = centerPos.getZ() - minRangeZ;
+ const int32_t x2 = std::min(0xFFFF, std::max(0, max_x + maxoffset));
+ const int32_t y2 = std::min(0xFFFF, std::max(0, max_y + maxoffset));
- const auto startLeaf = g_game().map.getQTNode(static_cast(startx1), static_cast(starty1));
- const QTreeLeafNode* leafS = startLeaf;
- const QTreeLeafNode* leafE;
+ const int32_t startx1 = x1 - (x1 & SECTOR_MASK);
+ const int32_t starty1 = y1 - (y1 & SECTOR_MASK);
+ const int32_t endx2 = x2 - (x2 & SECTOR_MASK);
+ const int32_t endy2 = y2 - (y2 & SECTOR_MASK);
SpectatorList spectators;
spectators.reserve(std::max(MAP_MAX_VIEW_PORT_X, MAP_MAX_VIEW_PORT_Y) * 2);
- for (uint_fast16_t ny = starty1; ny <= endy2; ny += FLOOR_SIZE) {
- leafE = leafS;
- for (uint_fast16_t nx = startx1; nx <= endx2; nx += FLOOR_SIZE) {
- if (leafE) {
- const auto &node_list = (onlyPlayers ? leafE->player_list : leafE->creature_list);
+ const MapSector* startSector = g_game().map.getMapSector(startx1, starty1);
+ const MapSector* sectorS = startSector;
+ for (int32_t ny = starty1; ny <= endy2; ny += SECTOR_SIZE) {
+ const MapSector* sectorE = sectorS;
+ for (int32_t nx = startx1; nx <= endx2; nx += SECTOR_SIZE) {
+ if (sectorE) {
+ const auto &node_list = onlyPlayers ? sectorE->player_list : sectorE->creature_list;
for (const auto &creature : node_list) {
const auto &cpos = creature->getPosition();
- if (minRangeZ > cpos.z || maxRangeZ < cpos.z) {
- continue;
+ if (static_cast(static_cast(cpos.z) - minRangeZ) <= depth) {
+ const int_fast16_t offsetZ = Position::getOffsetZ(centerPos, cpos);
+ if (static_cast(cpos.x - offsetZ - min_x) <= width && static_cast(cpos.y - offsetZ - min_y) <= height) {
+ spectators.emplace_back(creature);
+ }
}
-
- const int_fast16_t offsetZ = Position::getOffsetZ(centerPos, cpos);
- if ((min_y + offsetZ) > cpos.y || (max_y + offsetZ) < cpos.y || (min_x + offsetZ) > cpos.x || (max_x + offsetZ) < cpos.x) {
- continue;
- }
-
- spectators.emplace_back(creature);
}
- leafE = leafE->leafE;
+ sectorE = sectorE->sectorE;
} else {
- leafE = g_game().map.getQTNode(static_cast(nx + FLOOR_SIZE), static_cast(ny));
+ sectorE = g_game().map.getMapSector(nx + SECTOR_SIZE, ny);
}
}
- if (leafS) {
- leafS = leafS->leafS;
+ if (sectorS) {
+ sectorS = sectorS->sectorS;
} else {
- leafS = g_game().map.getQTNode(static_cast(startx1), static_cast(ny + FLOOR_SIZE));
+ sectorS = g_game().map.getMapSector(startx1, ny + SECTOR_SIZE);
}
}
diff --git a/src/map/utils/mapsector.cpp b/src/map/utils/mapsector.cpp
new file mode 100644
index 00000000000..de036728b76
--- /dev/null
+++ b/src/map/utils/mapsector.cpp
@@ -0,0 +1,46 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2023 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#include "pch.hpp"
+
+#include "creatures/creature.hpp"
+#include "mapsector.hpp"
+
+bool MapSector::newSector = false;
+
+void MapSector::addCreature(const std::shared_ptr &c) {
+ creature_list.emplace_back(c);
+ if (c->getPlayer()) {
+ player_list.emplace_back(c);
+ }
+}
+
+void MapSector::removeCreature(const std::shared_ptr &c) {
+ auto iter = std::find(creature_list.begin(), creature_list.end(), c);
+ if (iter == creature_list.end()) {
+ g_logger().error("[{}]: Creature not found in creature_list!", __FUNCTION__);
+ return;
+ }
+
+ assert(iter != creature_list.end());
+ *iter = creature_list.back();
+ creature_list.pop_back();
+
+ if (c->getPlayer()) {
+ iter = std::find(player_list.begin(), player_list.end(), c);
+ if (iter == player_list.end()) {
+ g_logger().error("[{}]: Player not found in player_list!", __FUNCTION__);
+ return;
+ }
+
+ assert(iter != player_list.end());
+ *iter = player_list.back();
+ player_list.pop_back();
+ }
+}
diff --git a/src/map/utils/mapsector.hpp b/src/map/utils/mapsector.hpp
new file mode 100644
index 00000000000..7b95db7f78b
--- /dev/null
+++ b/src/map/utils/mapsector.hpp
@@ -0,0 +1,92 @@
+/**
+ * Canary - A free and open-source MMORPG server emulator
+ * Copyright (©) 2019-2023 OpenTibiaBR
+ * Repository: https://github.com/opentibiabr/canary
+ * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
+ * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
+ * Website: https://docs.opentibiabr.com/
+ */
+
+#pragma once
+
+#include "map/map_const.hpp"
+
+class Creature;
+class Tile;
+struct BasicTile;
+
+struct Floor {
+ explicit Floor(uint8_t z) :
+ z(z) { }
+
+ std::shared_ptr getTile(uint16_t x, uint16_t y) const {
+ std::shared_lock sl(mutex);
+ return tiles[x & SECTOR_MASK][y & SECTOR_MASK].first;
+ }
+
+ void setTile(uint16_t x, uint16_t y, std::shared_ptr tile) {
+ tiles[x & SECTOR_MASK][y & SECTOR_MASK].first = tile;
+ }
+
+ std::shared_ptr getTileCache(uint16_t x, uint16_t y) const {
+ std::shared_lock sl(mutex);
+ return tiles[x & SECTOR_MASK][y & SECTOR_MASK].second;
+ }
+
+ void setTileCache(uint16_t x, uint16_t y, const std::shared_ptr &newTile) {
+ tiles[x & SECTOR_MASK][y & SECTOR_MASK].second = newTile;
+ }
+
+ const auto &getTiles() const {
+ return tiles;
+ }
+
+ uint8_t getZ() const {
+ return z;
+ }
+
+ auto &getMutex() const {
+ return mutex;
+ }
+
+private:
+ std::pair, std::shared_ptr> tiles[SECTOR_SIZE][SECTOR_SIZE] = {};
+ mutable std::shared_mutex mutex;
+ uint8_t z { 0 };
+};
+
+class MapSector {
+public:
+ MapSector() = default;
+
+ // non-copyable
+ MapSector(const MapSector &) = delete;
+ MapSector &operator=(const MapSector &) = delete;
+
+ // non-moveable
+ MapSector(const MapSector &&) = delete;
+ MapSector &operator=(const MapSector &&) = delete;
+
+ const std::unique_ptr &createFloor(uint32_t z) {
+ return floors[z] ? floors[z] : (floors[z] = std::make_unique(z));
+ }
+
+ const std::unique_ptr &getFloor(uint8_t z) const {
+ return floors[z];
+ }
+
+ void addCreature(const std::shared_ptr &c);
+ void removeCreature(const std::shared_ptr &c);
+
+private:
+ static bool newSector;
+ MapSector* sectorS = nullptr;
+ MapSector* sectorE = nullptr;
+ std::vector> creature_list;
+ std::vector> player_list;
+ std::unique_ptr floors[MAP_MAX_LAYERS] = {};
+ uint32_t floorBits = 0;
+
+ friend class Spectators;
+ friend class MapCache;
+};
diff --git a/src/map/utils/qtreenode.cpp b/src/map/utils/qtreenode.cpp
deleted file mode 100644
index 279bcabe3fa..00000000000
--- a/src/map/utils/qtreenode.cpp
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Canary - A free and open-source MMORPG server emulator
- * Copyright (©) 2019-2023 OpenTibiaBR
- * Repository: https://github.com/opentibiabr/canary
- * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
- * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
- * Website: https://docs.opentibiabr.com/
- */
-
-#include "pch.hpp"
-
-#include "creatures/creature.hpp"
-#include "qtreenode.hpp"
-
-bool QTreeLeafNode::newLeaf = false;
-
-QTreeLeafNode* QTreeNode::getLeaf(uint32_t x, uint32_t y) {
- if (leaf) {
- return static_cast(this);
- }
-
- const auto node = child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)];
- return node ? node->getLeaf(x << 1, y << 1) : nullptr;
-}
-
-QTreeLeafNode* QTreeNode::createLeaf(uint32_t x, uint32_t y, uint32_t level) {
- if (isLeaf()) {
- return static_cast(this);
- }
-
- const uint32_t index = ((x & 0x8000) >> 15) | ((y & 0x8000) >> 14);
- if (!child[index]) {
- if (level != FLOOR_BITS) {
- child[index] = new QTreeNode();
- } else {
- child[index] = new QTreeLeafNode();
- QTreeLeafNode::newLeaf = true;
- }
- }
-
- return child[index]->createLeaf(x * 2, y * 2, level - 1);
-}
-
-QTreeLeafNode* QTreeNode::getBestLeaf(uint32_t x, uint32_t y, uint32_t level) {
- QTreeLeafNode::newLeaf = false;
- auto tempLeaf = createLeaf(x, y, level);
-
- if (QTreeLeafNode::newLeaf) {
- // update north
- if (const auto northLeaf = getLeaf(x, y - FLOOR_SIZE)) {
- northLeaf->leafS = tempLeaf;
- }
-
- // update west leaf
- if (const auto westLeaf = getLeaf(x - FLOOR_SIZE, y)) {
- westLeaf->leafE = tempLeaf;
- }
-
- // update south
- if (const auto southLeaf = getLeaf(x, y + FLOOR_SIZE)) {
- tempLeaf->leafS = southLeaf;
- }
-
- // update east
- if (const auto eastLeaf = getLeaf(x + FLOOR_SIZE, y)) {
- tempLeaf->leafE = eastLeaf;
- }
- }
-
- return tempLeaf;
-}
-
-void QTreeLeafNode::addCreature(const std::shared_ptr &c) {
- creature_list.push_back(c);
-
- if (c->getPlayer()) {
- player_list.push_back(c);
- }
-}
-
-void QTreeLeafNode::removeCreature(std::shared_ptr c) {
- auto iter = std::find(creature_list.begin(), creature_list.end(), c);
- if (iter == creature_list.end()) {
- g_logger().error("[{}]: Creature not found in creature_list!", __FUNCTION__);
- return;
- }
-
- assert(iter != creature_list.end());
- *iter = creature_list.back();
- creature_list.pop_back();
-
- if (c->getPlayer()) {
- iter = std::find(player_list.begin(), player_list.end(), c);
- if (iter == player_list.end()) {
- g_logger().error("[{}]: Player not found in player_list!", __FUNCTION__);
- return;
- }
-
- assert(iter != player_list.end());
- *iter = player_list.back();
- player_list.pop_back();
- }
-}
diff --git a/src/map/utils/qtreenode.hpp b/src/map/utils/qtreenode.hpp
deleted file mode 100644
index 83f052a6abf..00000000000
--- a/src/map/utils/qtreenode.hpp
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Canary - A free and open-source MMORPG server emulator
- * Copyright (©) 2019-2023 OpenTibiaBR
- * Repository: https://github.com/opentibiabr/canary
- * License: https://github.com/opentibiabr/canary/blob/main/LICENSE
- * Contributors: https://github.com/opentibiabr/canary/graphs/contributors
- * Website: https://docs.opentibiabr.com/
- */
-
-#pragma once
-
-#include "map/map_const.hpp"
-
-struct Floor;
-class QTreeLeafNode;
-class Creature;
-
-class QTreeNode {
-public:
- constexpr QTreeNode() = default;
-
- virtual ~QTreeNode() { }
-
- // non-copyable
- QTreeNode(const QTreeNode &) = delete;
- QTreeNode &operator=(const QTreeNode &) = delete;
-
- bool isLeaf() const {
- return leaf;
- }
-
- template
- static Leaf getLeafStatic(Node node, uint32_t x, uint32_t y) {
- do {
- node = node->child[((x & 0x8000) >> 15) | ((y & 0x8000) >> 14)];
- if (!node) {
- return nullptr;
- }
-
- x <<= 1;
- y <<= 1;
- } while (!node->leaf);
- return static_cast(node);
- }
-
- QTreeLeafNode* getLeaf(uint32_t x, uint32_t y);
- QTreeLeafNode* getBestLeaf(uint32_t x, uint32_t y, uint32_t level);
-
- QTreeLeafNode* createLeaf(uint32_t x, uint32_t y, uint32_t level);
-
-protected:
- QTreeNode* child[4] = {};
- bool leaf = false;
-};
-
-class QTreeLeafNode final : public QTreeNode {
-public:
- QTreeLeafNode() {
- QTreeNode::leaf = true;
- newLeaf = true;
- }
-
- // non-copyable
- QTreeLeafNode(const QTreeLeafNode &) = delete;
- QTreeLeafNode &operator=(const QTreeLeafNode &) = delete;
-
- const std::unique_ptr &createFloor(uint32_t z) {
- return array[z] ? array[z] : (array[z] = std::make_unique(z));
- }
-
- const std::unique_ptr &getFloor(uint8_t z) const {
- return array[z];
- }
-
- void addCreature(const std::shared_ptr &c);
- void removeCreature(std::shared_ptr c);
-
-private:
- static bool newLeaf;
- QTreeLeafNode* leafS = nullptr;
- QTreeLeafNode* leafE = nullptr;
-
- std::unique_ptr array[MAP_MAX_LAYERS] = {};
-
- std::vector> creature_list;
- std::vector> player_list;
-
- friend class Map;
- friend class MapCache;
- friend class QTreeNode;
- friend class Spectators;
-};
diff --git a/src/server/network/connection/connection.cpp b/src/server/network/connection/connection.cpp
index 1c575ff00d0..3effd52f829 100644
--- a/src/server/network/connection/connection.cpp
+++ b/src/server/network/connection/connection.cpp
@@ -105,7 +105,7 @@ void Connection::accept(Protocol_ptr protocolPtr) {
void Connection::acceptInternal(bool toggleParseHeader) {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ readTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
try {
asio::async_read(socket, asio::buffer(msg.getBuffer(), HEADER_LENGTH), [self = shared_from_this(), toggleParseHeader](const std::error_code &error, std::size_t N) {
@@ -147,7 +147,7 @@ void Connection::parseProxyIdentification(const std::error_code &error) {
connectionState = CONNECTION_STATE_READINGS;
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ readTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
// Read the remainder of proxy identification
asio::async_read(socket, asio::buffer(msg.getBuffer(), remainder), [self = shared_from_this()](const std::error_code &error, std::size_t N) { self->parseProxyIdentification(error); });
@@ -208,7 +208,7 @@ void Connection::parseHeader(const std::error_code &error) {
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ readTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
// Read packet content
msg.setLength(size + HEADER_LENGTH);
@@ -275,7 +275,7 @@ void Connection::parsePacket(const std::error_code &error) {
try {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ readTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
if (!skipReadingNextPacket) {
// Wait to the next packet
@@ -289,7 +289,7 @@ void Connection::parsePacket(const std::error_code &error) {
void Connection::resumeWork() {
readTimer.expires_from_now(std::chrono::seconds(CONNECTION_READ_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ readTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
try {
asio::async_read(socket, asio::buffer(msg.getBuffer(), HEADER_LENGTH), [self = shared_from_this()](const std::error_code &error, std::size_t N) { self->parseHeader(error); });
@@ -358,7 +358,7 @@ uint32_t Connection::getIP() {
void Connection::internalSend(const OutputMessage_ptr &outputMessage) {
writeTimer.expires_from_now(std::chrono::seconds(CONNECTION_WRITE_TIMEOUT));
- readTimer.async_wait([self = shared_from_this()](const std::error_code &error) { Connection::handleTimeout(std::weak_ptr(self), error); });
+ writeTimer.async_wait([self = std::weak_ptr(shared_from_this())](const std::error_code &error) { Connection::handleTimeout(self, error); });
try {
asio::async_write(socket, asio::buffer(outputMessage->getOutputBuffer(), outputMessage->getLength()), [self = shared_from_this()](const std::error_code &error, std::size_t N) { self->onWriteOperation(error); });
diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp
index ada1340a7c6..751da531273 100644
--- a/src/server/network/protocol/protocolgame.cpp
+++ b/src/server/network/protocol/protocolgame.cpp
@@ -1276,6 +1276,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage msg, uint8_t recvbyt
case 0xDE:
parseEditVip(msg);
break;
+ case 0xDF:
+ parseVipGroupActions(msg);
+ break;
case 0xE1:
parseBestiarysendRaces();
break;
@@ -1972,11 +1975,43 @@ void ProtocolGame::parseRemoveVip(NetworkMessage &msg) {
}
void ProtocolGame::parseEditVip(NetworkMessage &msg) {
+ std::vector vipGroupsId;
uint32_t guid = msg.get();
const std::string description = msg.getString();
uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63
bool notify = msg.getByte() != 0;
- g_game().playerRequestEditVip(player->getID(), guid, description, icon, notify);
+ uint8_t groupsAmount = msg.getByte();
+ for (uint8_t i = 0; i < groupsAmount; ++i) {
+ uint8_t groupId = msg.getByte();
+ vipGroupsId.emplace_back(groupId);
+ }
+ g_game().playerRequestEditVip(player->getID(), guid, description, icon, notify, vipGroupsId);
+}
+
+void ProtocolGame::parseVipGroupActions(NetworkMessage &msg) {
+ uint8_t action = msg.getByte();
+
+ switch (action) {
+ case 0x01: {
+ const std::string groupName = msg.getString();
+ player->vip()->addGroup(groupName);
+ break;
+ }
+ case 0x02: {
+ const uint8_t groupId = msg.getByte();
+ const std::string newGroupName = msg.getString();
+ player->vip()->editGroup(groupId, newGroupName);
+ break;
+ }
+ case 0x03: {
+ const uint8_t groupId = msg.getByte();
+ player->vip()->removeGroup(groupId);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
}
void ProtocolGame::parseRotateItem(NetworkMessage &msg) {
@@ -5804,35 +5839,47 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) {
}
}
- auto purchase = IOMarket::getInstance().getPurchaseStatistics()[itemId][tier];
- if (const MarketStatistics* purchaseStatistics = &purchase; purchaseStatistics) {
- msg.addByte(0x01);
- msg.add(purchaseStatistics->numTransactions);
- if (oldProtocol) {
- msg.add(std::min(std::numeric_limits::max(), purchaseStatistics->totalPrice));
- msg.add(std::min(std::numeric_limits::max(), purchaseStatistics->highestPrice));
- msg.add(std::min(std::numeric_limits::max(), purchaseStatistics->lowestPrice));
- } else {
- msg.add(purchaseStatistics->totalPrice);
- msg.add(purchaseStatistics->highestPrice);
- msg.add(purchaseStatistics->lowestPrice);
+ const auto &purchaseStatsMap = IOMarket::getInstance().getPurchaseStatistics();
+ auto purchaseIterator = purchaseStatsMap.find(itemId);
+ if (purchaseIterator != purchaseStatsMap.end()) {
+ const auto &tierStatsMap = purchaseIterator->second;
+ auto tierStatsIter = tierStatsMap.find(tier);
+ if (tierStatsIter != tierStatsMap.end()) {
+ const auto &purchaseStatistics = tierStatsIter->second;
+ msg.addByte(0x01);
+ msg.add(purchaseStatistics.numTransactions);
+ if (oldProtocol) {
+ msg.add(std::min