From 216653966cb2be4416de80b1aef698c71605f021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Lu=C3=ADs=20Lucarelo=20Lamonato?= Date: Fri, 17 May 2024 09:42:33 -0300 Subject: [PATCH 01/11] fix: doctor marrow's spell null reference (#2624) --- .../monster/doctor_marrow_explosion.lua | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) 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 From c67424169752c1e417dc86bd0843993745d17598 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 17 May 2024 21:17:20 -0300 Subject: [PATCH 02/11] fix: check nil storage key from setStorageValue (#2636) When a key from Lua is nil, it is automatically transformed into 0 without being checked. Consequently, even with a nil key, no error is thrown, and the code continues as if everything had occurred normally, resulting in the storage not being set. Removed two duplicated checks in creature.cpp; they are already verified in the if statement above. --- src/creatures/creature.cpp | 2 +- src/lua/functions/creatures/player/player_functions.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 7f4d96f3773..50774e35e2a 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); From 7dda937c32e7caaf66151e5152c9ab7b86c81b39 Mon Sep 17 00:00:00 2001 From: Guilherme Date: Mon, 20 May 2024 12:41:57 -0300 Subject: [PATCH 03/11] fix: items on npcs shop, update monsters spawn and items (#2616) --- data-otservbr-global/npc/alaistar.lua | 5 ++- data-otservbr-global/npc/alexander.lua | 22 +++++++++ data-otservbr-global/npc/asima.lua | 33 ++++++++++++++ data-otservbr-global/npc/briasol.lua | 2 +- data-otservbr-global/npc/chantalle.lua | 2 +- data-otservbr-global/npc/chuckles.lua | 32 +++++++++++++ data-otservbr-global/npc/edmund.lua | 2 +- data-otservbr-global/npc/fenech.lua | 14 ++++++ data-otservbr-global/npc/frans.lua | 14 ++++++ data-otservbr-global/npc/frederik.lua | 14 ++++++ data-otservbr-global/npc/gail.lua | 2 +- data-otservbr-global/npc/gnomegica.lua | 14 ++++++ data-otservbr-global/npc/hanna.lua | 2 +- data-otservbr-global/npc/ishina.lua | 2 +- data-otservbr-global/npc/iwan.lua | 2 +- data-otservbr-global/npc/jessica.lua | 2 +- data-otservbr-global/npc/khanna.lua | 42 ++++++++++++++++- data-otservbr-global/npc/mordecai.lua | 11 +++++ data-otservbr-global/npc/nelly.lua | 14 ++++++ data-otservbr-global/npc/nipuna.lua | 11 +++++ data-otservbr-global/npc/odemara.lua | 2 +- data-otservbr-global/npc/oiriz.lua | 2 +- data-otservbr-global/npc/rabaz.lua | 14 ++++++ data-otservbr-global/npc/rachel.lua | 17 +++++++ data-otservbr-global/npc/romir.lua | 14 ++++++ data-otservbr-global/npc/shiriel.lua | 14 ++++++ data-otservbr-global/npc/sigurd.lua | 14 ++++++ data-otservbr-global/npc/sundara.lua | 11 +++++ data-otservbr-global/npc/tandros.lua | 14 ++++++ data-otservbr-global/npc/tesha.lua | 2 +- data-otservbr-global/npc/tezila.lua | 2 +- data-otservbr-global/npc/topsy.lua | 14 ++++++ data-otservbr-global/npc/valindara.lua | 2 +- data-otservbr-global/npc/xodet.lua | 14 ++++++ data-otservbr-global/npc/yasir.lua | 10 +++++ data-otservbr-global/npc/yonan.lua | 2 +- .../world/otservbr-monster.xml | 45 ++++--------------- data/items/items.xml | 16 ++++++- 38 files changed, 396 insertions(+), 55 deletions(-) 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/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/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/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"/> + + + + + + + + + From 50b4c26a3fcab7fb01130186eb18a196c24e3db4 Mon Sep 17 00:00:00 2001 From: Guilherme Date: Mon, 20 May 2024 16:03:10 -0300 Subject: [PATCH 04/11] feat: Galthen's Satchel and Artefact Box (#2149) Fixes from map on pr #2314 --- data-otservbr-global/npc/an_idol.lua | 68 +++++++++++++++++++ .../adventures_of_galthen/galthens_tree.lua | 19 ++++++ data-otservbr-global/world/otservbr-npc.xml | 5 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 data-otservbr-global/npc/an_idol.lua create mode 100644 data-otservbr-global/scripts/actions/quests/adventures_of_galthen/galthens_tree.lua 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/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/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 @@ - + + + + From 360e00683afe6dd92f0237f42b09fdd5bd902c7a Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Tue, 21 May 2024 19:14:27 -0300 Subject: [PATCH 05/11] fix: update market average price and configurable refresh interval (#2642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Optimized Resource Usage: Reduced unnecessary copying of the statistics and sale maps in protocolgame.cpp, enhancing safety with the use of find for more secure access. • Bug Fix: Addressed a loading issue with items in market_offers and market_history. • Improved Readability: Replaced ostringstream with fmt::format to enhance code clarity and readability. • Feature: change price update interval to 30 by default and added a setting to enable/disable it (only set to 0). --- config.lua.dist | 3 + src/config/config_enums.hpp | 1 + src/config/configmanager.cpp | 1 + src/game/game.cpp | 30 +++++----- src/game/game.hpp | 3 +- src/io/iomarket.cpp | 24 ++++++-- src/io/iomarket.hpp | 7 +-- src/server/network/protocol/protocolgame.cpp | 60 ++++++++++++-------- 8 files changed, 78 insertions(+), 51 deletions(-) diff --git a/config.lua.dist b/config.lua.dist index ed2561d4532..447a90647c2 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 diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 30a4b172af1..22043f42525 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -139,6 +139,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..e1345bbac4b 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); diff --git a/src/game/game.cpp b/src/game/game.cpp index 00ec056ccb3..6188973781a 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) { diff --git a/src/game/game.hpp b/src/game/game.hpp index 554c418a2c8..0e7b5fc147f 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 @@ -517,7 +516,7 @@ class Game { return lightHour; } - bool loadItemsPrice(); + void loadItemsPrice(); void loadMotdNum(); void saveMotdNum() const; 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/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index ada1340a7c6..e16f015eb49 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -5804,35 +5804,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(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); + } } } else { msg.addByte(0x00); // send to old protocol ? } - auto sale = IOMarket::getInstance().getSaleStatistics()[itemId][tier]; - if (const MarketStatistics* saleStatistics = &sale; saleStatistics) { - msg.addByte(0x01); - msg.add(saleStatistics->numTransactions); - if (oldProtocol) { - msg.add(std::min(std::numeric_limits::max(), saleStatistics->totalPrice)); - msg.add(std::min(std::numeric_limits::max(), saleStatistics->highestPrice)); - msg.add(std::min(std::numeric_limits::max(), saleStatistics->lowestPrice)); - } else { - msg.add(std::min(std::numeric_limits::max(), saleStatistics->totalPrice)); - msg.add(saleStatistics->highestPrice); - msg.add(saleStatistics->lowestPrice); + const auto &saleStatsMap = IOMarket::getInstance().getSaleStatistics(); + auto saleIterator = saleStatsMap.find(itemId); + if (saleIterator != saleStatsMap.end()) { + const auto &tierStatsMap = saleIterator->second; + auto tierStatsIter = tierStatsMap.find(tier); + if (tierStatsIter != tierStatsMap.end()) { + const auto &saleStatistics = tierStatsIter->second; + msg.addByte(0x01); + msg.add(saleStatistics.numTransactions); + if (oldProtocol) { + msg.add(std::min(std::numeric_limits::max(), saleStatistics.totalPrice)); + msg.add(std::min(std::numeric_limits::max(), saleStatistics.highestPrice)); + msg.add(std::min(std::numeric_limits::max(), saleStatistics.lowestPrice)); + } else { + msg.add(std::min(std::numeric_limits::max(), saleStatistics.totalPrice)); + msg.add(saleStatistics.highestPrice); + msg.add(saleStatistics.lowestPrice); + } } } else { msg.addByte(0x00); // send to old protocol ? From c57f5a28d6affef23dcf5a801b321d7e72465c52 Mon Sep 17 00:00:00 2001 From: Renato Machado Date: Wed, 22 May 2024 20:14:22 -0300 Subject: [PATCH 06/11] enhance: MapSector system for improved performance and flexibility Introduces a new map sector system using a hashmap instead of a node-based structure, which significantly improves search performance. It stores tiles and creatures for each area, and is critical for server functionality. Credits @saiyansking --- src/items/tile.cpp | 2 +- .../functions/core/game/global_functions.cpp | 2 +- src/map/CMakeLists.txt | 2 +- src/map/map.cpp | 67 +++++++----- src/map/map.hpp | 8 +- src/map/map_const.hpp | 7 +- src/map/mapcache.cpp | 46 +++++++- src/map/mapcache.hpp | 61 ++++------- src/map/spectators.cpp | 70 ++++++------ src/map/utils/mapsector.cpp | 46 ++++++++ src/map/utils/mapsector.hpp | 92 ++++++++++++++++ src/map/utils/qtreenode.cpp | 103 ------------------ src/map/utils/qtreenode.hpp | 92 ---------------- vcproj/canary.vcxproj | 4 +- 14 files changed, 290 insertions(+), 312 deletions(-) create mode 100644 src/map/utils/mapsector.cpp create mode 100644 src/map/utils/mapsector.hpp delete mode 100644 src/map/utils/qtreenode.cpp delete mode 100644 src/map/utils/qtreenode.hpp 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/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/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..d4d1c4147b1 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,7 +315,7 @@ 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; } @@ -351,13 +351,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,32 +687,47 @@ 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; - for (const auto &tile : g_game().getTilesToClean()) { - if (!tile) { - continue; - } - if (const auto items = tile->getItemList()) { - ++tiles; - for (const auto &item : *items) { - if (item->isCleanable()) { - toRemove.emplace_back(item); + ItemVector toRemove; + toRemove.reserve(128); + for (const auto &mit : mapSectors) { + for (uint8_t z = 0; z < MAP_MAX_LAYERS; ++z) { + if (const auto &floor = mit.second.getFloor(z)) { + for (auto &tiles : floor->getTiles()) { + for (const auto &[tile, cachedTile] : tiles) { + if (!tile || tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + continue; + } + + TileItemVector* itemList = tile->getItemList(); + if (!itemList) { + continue; + } + + ++qntTiles; + + for (auto it = ItemVector::const_reverse_iterator(itemList->getEndDownItem()), end = ItemVector::const_reverse_iterator(itemList->getBeginDownItem()); it != end; ++it) { + const auto &item = *it; + if (item->isCleanable()) { + toRemove.push_back(item); + } + } + } } } } } + 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 +735,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/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index 1c21dc87210..b3e4eaffb42 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -203,7 +203,7 @@ - + @@ -388,7 +388,7 @@ - + From caa35461ea75fea7fdbdf34a308b2183e499b5b9 Mon Sep 17 00:00:00 2001 From: Eduardo Dantas Date: Fri, 24 May 2024 00:46:48 -0300 Subject: [PATCH 07/11] improve: move wheel scrolls to kv (#2637) --- ...e_wheel_scrolls_from_storagename_to_kv.lua | 24 +++++++++++++ data/scripts/actions/items/wheel_scrolls.lua | 17 +++++----- data/scripts/lib/register_migrations.lua | 2 +- src/creatures/players/wheel/player_wheel.cpp | 34 ++++++++++++------- .../creatures/player/player_functions.cpp | 2 ++ 5 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 data-otservbr-global/scripts/game_migrations/20241715984279_move_wheel_scrolls_from_storagename_to_kv.lua 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/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/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/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 50774e35e2a..413f1bd3f47 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -1767,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; @@ -1781,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); From c545325193705989e2e912a08b3d763ad0672301 Mon Sep 17 00:00:00 2001 From: Karin Date: Fri, 24 May 2024 08:56:43 -0300 Subject: [PATCH 08/11] fix: prevent lag stacking items on npc with shopping bags (#2640) --- src/game/game.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/game/game.cpp b/src/game/game.cpp index 6188973781a..afc58e75198 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -5165,6 +5165,23 @@ void Game::playerBuyItem(uint32_t playerId, uint16_t itemId, uint8_t count, uint return; } + if (inBackpacks) { + 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(); } From 1df6ce57d6ab19912dcb3b326e997be38b09926b Mon Sep 17 00:00:00 2001 From: Pedro Cruz Date: Fri, 24 May 2024 08:57:04 -0300 Subject: [PATCH 09/11] feat: vip groups (#2635) --- data-otservbr-global/migrations/45.lua | 55 +++- data-otservbr-global/migrations/46.lua | 3 + schema.sql | 57 +++- src/creatures/CMakeLists.txt | 1 + src/creatures/creatures_definitions.hpp | 33 ++- src/creatures/players/player.cpp | 105 ++------ src/creatures/players/player.hpp | 19 +- src/creatures/players/vip/player_vip.cpp | 247 ++++++++++++++++++ src/creatures/players/vip/player_vip.hpp | 74 ++++++ src/game/game.cpp | 16 +- src/game/game.hpp | 2 +- src/io/functions/iologindata_load_player.cpp | 30 ++- src/io/iologindata.cpp | 77 ++++-- src/io/iologindata.hpp | 7 + .../creatures/player/player_functions.cpp | 4 +- src/server/network/protocol/protocolgame.cpp | 84 +++++- src/server/network/protocol/protocolgame.hpp | 4 + vcproj/canary.vcxproj | 2 + 18 files changed, 658 insertions(+), 162 deletions(-) create mode 100644 data-otservbr-global/migrations/46.lua create mode 100644 src/creatures/players/vip/player_vip.cpp create mode 100644 src/creatures/players/vip/player_vip.hpp diff --git a/data-otservbr-global/migrations/45.lua b/data-otservbr-global/migrations/45.lua index 86a6d8ffec1..c606f18522e 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` tinyint(3) UNSIGNED NOT NULL, + `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` (`id`, `account_id`, `name`, `customizable`) VALUES (1, NEW.`id`, 'Enemies', 0); + INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) VALUES (2, NEW.`id`, 'Friends', 0); + INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) VALUES (3, 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` tinyint(3) 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/schema.sql b/schema.sql index 624f434509e..7bbf6c86ac5 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,6 +215,44 @@ 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` tinyint(3) UNSIGNED NOT NULL, + `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` (`id`, `account_id`, `name`, `customizable`) VALUES (1, NEW.`id`, 'Enemies', 0); + INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) VALUES (2, NEW.`id`, 'Friends', 0); + INSERT INTO `account_vipgroups` (`id`, `account_id`, `name`, `customizable`) VALUES (3, 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` tinyint(3) 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, @@ -372,9 +410,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,9 +466,8 @@ 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 ; @@ -815,3 +852,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` (`id`, `name`, `account_id`. `customizable`) VALUES +(1, 'Friends', 1, 0), +(2, 'Enemies', 1, 0), +(3, 'Trading Partners', 1, 0); 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/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/players/player.cpp b/src/creatures/players/player.cpp index 3182607ecba..553ca2e3b04 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); } @@ -2985,7 +2986,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 +3040,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 +3061,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 +6218,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 +7995,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..58bebfb9f2f 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; @@ -830,13 +833,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 +1046,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 +2625,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 +2715,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 +2909,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 +3023,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/game/game.cpp b/src/game/game.cpp index afc58e75198..621e3593816 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -5773,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); } } } @@ -5798,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 0e7b5fc147f..3a199254259 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -393,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); 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/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 413f1bd3f47..5f14db6ea99 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -3024,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/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index e16f015eb49..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) { @@ -6535,6 +6570,8 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos // player light level sendCreatureLight(creature); + sendVIPGroups(); + const std::forward_list &vipEntries = IOLoginData::getVIPEntries(player->getAccountId()); if (player->isAccessPlayer()) { @@ -6543,9 +6580,9 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos std::shared_ptr vipPlayer = g_game().getPlayerByGUID(entry.guid); if (!vipPlayer) { - vipStatus = VIPSTATUS_OFFLINE; + vipStatus = VipStatus_t::Offline; } else { - vipStatus = vipPlayer->statusVipList; + vipStatus = vipPlayer->vip()->getStatus(); } sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus); @@ -6556,9 +6593,9 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos std::shared_ptr vipPlayer = g_game().getPlayerByGUID(entry.guid); if (!vipPlayer || vipPlayer->isInGhostMode()) { - vipStatus = VIPSTATUS_OFFLINE; + vipStatus = VipStatus_t::Offline; } else { - vipStatus = vipPlayer->statusVipList; + vipStatus = vipPlayer->vip()->getStatus(); } sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus); @@ -7085,19 +7122,19 @@ void ProtocolGame::sendPodiumWindow(std::shared_ptr podium, const Position } void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus) { - if (oldProtocol && newStatus == VIPSTATUS_TRAINING) { + if (oldProtocol && newStatus == VipStatus_t::Training) { return; } NetworkMessage msg; msg.addByte(0xD3); msg.add(guid); - msg.addByte(newStatus); + msg.addByte(enumToValue(newStatus)); writeToOutputBuffer(msg); } void ProtocolGame::sendVIP(uint32_t guid, const std::string &name, const std::string &description, uint32_t icon, bool notify, VipStatus_t status) { - if (oldProtocol && status == VIPSTATUS_TRAINING) { + if (oldProtocol && status == VipStatus_t::Training) { return; } @@ -7108,10 +7145,37 @@ void ProtocolGame::sendVIP(uint32_t guid, const std::string &name, const std::st msg.addString(description, "ProtocolGame::sendVIP - description"); msg.add(std::min(10, icon)); msg.addByte(notify ? 0x01 : 0x00); - msg.addByte(status); + msg.addByte(enumToValue(status)); + + const auto &vipGuidGroups = player->vip()->getGroupsIdGuidBelongs(guid); + if (!oldProtocol) { - msg.addByte(0x00); // vipGroups + msg.addByte(vipGuidGroups.size()); // vipGroups + for (const auto &vipGroupID : vipGuidGroups) { + msg.addByte(vipGroupID); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendVIPGroups() { + if (oldProtocol) { + return; + } + + const auto &vipGroups = player->vip()->getGroups(); + + NetworkMessage msg; + msg.addByte(0xD4); + msg.addByte(vipGroups.size()); // vipGroups.size() + for (const auto &vipGroup : vipGroups) { + msg.addByte(vipGroup->id); + msg.addString(vipGroup->name, "ProtocolGame::sendVIP - vipGroup.name"); + msg.addByte(vipGroup->customizable ? 0x01 : 0x00); // 0x00 = not Customizable, 0x01 = Customizable } + msg.addByte(player->vip()->getMaxGroupEntries() - vipGroups.size()); // max vip groups + writeToOutputBuffer(msg); } diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 16105ee0b8b..0386093cfc4 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -18,6 +18,7 @@ class NetworkMessage; class Player; +class VIPGroup; class Game; class House; class Container; @@ -213,6 +214,7 @@ class ProtocolGame final : public Protocol { void parseAddVip(NetworkMessage &msg); void parseRemoveVip(NetworkMessage &msg); void parseEditVip(NetworkMessage &msg); + void parseVipGroupActions(NetworkMessage &msg); void parseRotateItem(NetworkMessage &msg); void parseConfigureShowOffSocket(NetworkMessage &msg); @@ -360,6 +362,7 @@ class ProtocolGame final : public Protocol { void sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus); void sendVIP(uint32_t guid, const std::string &name, const std::string &description, uint32_t icon, bool notify, VipStatus_t status); + void sendVIPGroups(); void sendPendingStateEntered(); void sendEnterWorld(); @@ -477,6 +480,7 @@ class ProtocolGame final : public Protocol { friend class Player; friend class PlayerWheel; + friend class PlayerVIP; std::unordered_set knownCreatureSet; std::shared_ptr player = nullptr; diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index b3e4eaffb42..4cb91d1cb94 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -47,6 +47,7 @@ + @@ -261,6 +262,7 @@ + From 5f88c6d635018debc3fbc6168782d504aeec19cb Mon Sep 17 00:00:00 2001 From: Luan Luciano Date: Fri, 24 May 2024 08:58:51 -0300 Subject: [PATCH 10/11] fix: onDeEquip properly handled at logout/death (#2625) --- src/creatures/players/player.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 553ca2e3b04..4109eb77472 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -1862,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); From dabbeada591b89445c73d4e8a524fff640f8a7c9 Mon Sep 17 00:00:00 2001 From: Pedro Cruz Date: Fri, 24 May 2024 22:14:53 -0300 Subject: [PATCH 11/11] fix: vip groups schema (#2651) Fixes the schema of vip groups. --- schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema.sql b/schema.sql index 7bbf6c86ac5..bf37d1c2f37 100644 --- a/schema.sql +++ b/schema.sql @@ -854,7 +854,7 @@ INSERT INTO `players` (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` (`id`, `name`, `account_id`. `customizable`) VALUES +INSERT INTO `account_vipgroups` (`id`, `name`, `account_id`, `customizable`) VALUES (1, 'Friends', 1, 0), (2, 'Enemies', 1, 0), (3, 'Trading Partners', 1, 0);