diff --git a/cmake/modules/BaseConfig.cmake b/cmake/modules/BaseConfig.cmake index 86b2890d11e..b79167ed6a2 100644 --- a/cmake/modules/BaseConfig.cmake +++ b/cmake/modules/BaseConfig.cmake @@ -31,6 +31,8 @@ find_package(asio CONFIG REQUIRED) find_package(eventpp CONFIG REQUIRED) find_package(jsoncpp CONFIG REQUIRED) find_package(magic_enum CONFIG REQUIRED) +find_package(opentelemetry-cpp CONFIG REQUIRED) +find_package(prometheus-cpp CONFIG REQUIRED) find_package(mio REQUIRED) find_package(pugixml CONFIG REQUIRED) find_package(spdlog REQUIRED) diff --git a/cmake/modules/CanaryLib.cmake b/cmake/modules/CanaryLib.cmake index 4b7d69d2b80..d5fdd782b1f 100644 --- a/cmake/modules/CanaryLib.cmake +++ b/cmake/modules/CanaryLib.cmake @@ -90,6 +90,14 @@ target_link_libraries(${PROJECT_NAME}_lib unofficial::argon2::libargon2 unofficial::libmariadb unofficial::mariadbclient + opentelemetry-cpp::common + opentelemetry-cpp::metrics + opentelemetry-cpp::api + opentelemetry-cpp::ext + opentelemetry-cpp::sdk + opentelemetry-cpp::logs + opentelemetry-cpp::ostream_metrics_exporter + opentelemetry-cpp::prometheus_exporter ) if(CMAKE_BUILD_TYPE MATCHES Debug) diff --git a/config.lua.dist b/config.lua.dist index efef42dc91a..57257748195 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -490,3 +490,12 @@ vipKeepHouse = false -- NOTE set rewardChestMaxCollectItems max items per collect action rewardChestCollectEnabled = true rewardChestMaxCollectItems = 200 + +-- Metrics +--- Prometheus +metricsEnablePrometheus = false +metricsPrometheusAddress = "0.0.0.0:9464" + +--- OStream +metricsEnableOstream = false +metricsOstreamInterval = 1000 diff --git a/data-otservbr-global/monster/quests/forgotten_knowledge/icicle.lua b/data-otservbr-global/monster/quests/forgotten_knowledge/icicle.lua index 4f154635a47..7b795b7d3e8 100644 --- a/data-otservbr-global/monster/quests/forgotten_knowledge/icicle.lua +++ b/data-otservbr-global/monster/quests/forgotten_knowledge/icicle.lua @@ -70,20 +70,20 @@ monster.defenses = { defense = 199, armor = 199, mitigation = 0.50, - { name = "icicle heal", interval = 2000, chance = 25, target = false }, + { name = "icicle heal", interval = 2000, chance = 60, target = false }, } monster.elements = { - { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, - { type = COMBAT_ENERGYDAMAGE, percent = 0 }, - { type = COMBAT_EARTHDAMAGE, percent = 0 }, - { type = COMBAT_FIREDAMAGE, percent = 0 }, - { type = COMBAT_LIFEDRAIN, percent = 0 }, - { type = COMBAT_MANADRAIN, percent = 0 }, - { type = COMBAT_DROWNDAMAGE, percent = 0 }, - { type = COMBAT_ICEDAMAGE, percent = 0 }, - { type = COMBAT_HOLYDAMAGE, percent = 0 }, - { type = COMBAT_DEATHDAMAGE, percent = 0 }, + { type = COMBAT_PHYSICALDAMAGE, percent = 100 }, + { type = COMBAT_ENERGYDAMAGE, percent = 100 }, + { type = COMBAT_EARTHDAMAGE, percent = 100 }, + { type = COMBAT_FIREDAMAGE, percent = 100 }, + { type = COMBAT_LIFEDRAIN, percent = 100 }, + { type = COMBAT_MANADRAIN, percent = 100 }, + { type = COMBAT_DROWNDAMAGE, percent = 100 }, + { type = COMBAT_ICEDAMAGE, percent = 100 }, + { type = COMBAT_HOLYDAMAGE, percent = 100 }, + { type = COMBAT_DEATHDAMAGE, percent = 100 }, } monster.immunities = { diff --git a/data-otservbr-global/npc/alaistar.lua b/data-otservbr-global/npc/alaistar.lua index 883f877aaf3..07bf107650e 100644 --- a/data-otservbr-global/npc/alaistar.lua +++ b/data-otservbr-global/npc/alaistar.lua @@ -23,40 +23,50 @@ npcConfig.flags = { floorchange = false, } -npcConfig.shop = { - { itemName = "cowbell", clientId = 21204, sell = 210 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { 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 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "metal jaw", clientId = 21193, sell = 260 }, - { itemName = "metal toe", clientId = 21198, sell = 430 }, - { itemName = "mooh'tah shell", clientId = 21202, sell = 110 }, - { itemName = "moohtant horn", clientId = 21200, sell = 140 }, - { itemName = "necromantic rust", clientId = 21196, sell = 390 }, - { itemName = "poisoned fang", clientId = 21195, sell = 130 }, - { itemName = "seacrest hair", clientId = 21801, sell = 260 }, - { itemName = "seacrest pearl", clientId = 21747, sell = 400 }, - { itemName = "seacrest scale", clientId = 21800, sell = 150 }, - { itemName = "slime heart", clientId = 21194, sell = 160 }, - { itemName = "slimy leaf tentacle", clientId = 21197, sell = 320 }, - { 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 spirit potion", clientId = 23374, buy = 438 }, - { itemName = "vial", clientId = 2874, sell = 5 }, +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["creature products"] = { + { itemName = "cowbell", clientId = 21204, sell = 210 }, + { itemName = "giant pacifier", clientId = 21199, sell = 170 }, + { itemName = "glob of glooth", clientId = 21182, sell = 125 }, + { itemName = "glooth injection tube", clientId = 21103, sell = 350 }, + { itemName = "metal jaw", clientId = 21193, sell = 260 }, + { itemName = "metal toe", clientId = 21198, sell = 430 }, + { itemName = "mooh'tah shell", clientId = 21202, sell = 110 }, + { itemName = "moohtant horn", clientId = 21200, sell = 140 }, + { itemName = "necromantic rust", clientId = 21196, sell = 390 }, + { itemName = "poisoned fang", clientId = 21195, sell = 130 }, + { itemName = "seacrest hair", clientId = 21801, sell = 260 }, + { itemName = "seacrest pearl", clientId = 21747, sell = 400 }, + { itemName = "seacrest scale", clientId = 21800, sell = 150 }, + { itemName = "slime heart", clientId = 21194, sell = 160 }, + { itemName = "slimy leaf tentacle", clientId = 21197, sell = 320 }, + }, } +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) @@ -95,6 +105,30 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) + npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) npcType:register(npcConfig) diff --git a/data-otservbr-global/npc/alexander.lua b/data-otservbr-global/npc/alexander.lua index be63cecf32e..522a43da1d1 100644 --- a/data-otservbr-global/npc/alexander.lua +++ b/data-otservbr-global/npc/alexander.lua @@ -29,6 +29,49 @@ npcConfig.voices = { { text = "Selling all sorts of magic equipment. Come and have a look" }, } +local itemsTable = { + ["runes"] = { + { itemName = "animate dead rune", clientId = 3203, buy = 375 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "soulfire rune", clientId = 3195, buy = 46 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["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 = "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 = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -69,6 +112,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -91,6 +140,9 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end @@ -104,53 +156,6 @@ npcHandler:setMessage(MESSAGE_WALKAWAY, "See you, |PLAYERNAME|.") npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {runes} or {wands}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "crystal ball", clientId = 3076, buy = 530, sell = 190 }, - { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "fireball rune", clientId = 3189, buy = 30 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "holy missile rune", clientId = 3182, buy = 16 }, - { itemName = "icicle rune", clientId = 3158, buy = 30 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "life crystal", clientId = 3061, sell = 85 }, - { itemName = "life ring", clientId = 3052, buy = 900 }, - { itemName = "magic wall rune", clientId = 3180, buy = 116 }, - { itemName = "mind stone", clientId = 3062, sell = 170 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "paralyze rune", clientId = 3165, buy = 700 }, - { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "soulfire rune", clientId = 3195, buy = 46 }, - { 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 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stone shower rune", clientId = 3175, buy = 37 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, - { 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 }, - { itemName = "wild growth rune", clientId = 3156, buy = 160 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/asima.lua b/data-otservbr-global/npc/asima.lua index 5e4e1713ad8..329683e1e34 100644 --- a/data-otservbr-global/npc/asima.lua +++ b/data-otservbr-global/npc/asima.lua @@ -23,6 +23,55 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { 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 = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -63,6 +112,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -85,6 +140,9 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end @@ -92,68 +150,6 @@ end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/black_bert.lua b/data-otservbr-global/npc/black_bert.lua index 08006ac1230..c68c5f43bc5 100644 --- a/data-otservbr-global/npc/black_bert.lua +++ b/data-otservbr-global/npc/black_bert.lua @@ -22,83 +22,137 @@ npcConfig.outfit = { npcConfig.flags = { floorchange = false, } + npcConfig.shop = { - { clientId = 123, buy = 16000, sell = 16000, count = 1 }, - { clientId = 130, buy = 100, count = 1 }, - { clientId = 135, buy = 5000, count = 1 }, - { clientId = 138, buy = 600, count = 1 }, - { clientId = 141, buy = 2000, count = 1 }, - { clientId = 142, buy = 6000, count = 1 }, - { clientId = 349, buy = 15000, count = 1 }, - { clientId = 396, buy = 5000, count = 1 }, - { clientId = 406, buy = 15000, count = 1 }, - { clientId = 3216, buy = 8000, count = 1 }, - { clientId = 3217, buy = 8000, count = 1 }, - { clientId = 3232, buy = 3000, count = 1 }, - { clientId = 3233, buy = 16000, count = 1 }, - { clientId = 3234, buy = 150, count = 1 }, - { clientId = 4827, buy = 18000, count = 1 }, - { clientId = 4832, buy = 24000, count = 1 }, - { clientId = 4834, buy = 1000, count = 1 }, - { clientId = 4835, buy = 8000, count = 1 }, - { clientId = 4836, buy = 15000, count = 1 }, - { clientId = 4841, buy = 3000, count = 1 }, - { clientId = 4843, buy = 500, count = 1 }, - { clientId = 4846, buy = 4000, count = 1 }, - { clientId = 4847, buy = 6000, count = 1 }, - { clientId = 5940, buy = 10000, count = 1 }, - { clientId = 6124, buy = 40000, count = 1 }, - { clientId = 7281, buy = 500, count = 1 }, - { clientId = 7924, buy = 10000, count = 1 }, - { clientId = 7936, buy = 7000, count = 1 }, - { clientId = 8453, buy = 50000, count = 1 }, - { clientId = 8746, buy = 50000, count = 1 }, - { clientId = 8818, buy = 8000, count = 1 }, - { clientId = 8822, buy = 20000, count = 1 }, - { clientId = 9107, buy = 600, count = 1 }, - { clientId = 9188, buy = 5000, count = 1 }, - { clientId = 9191, buy = 5000, count = 1 }, - { clientId = 9236, buy = 10000, count = 1 }, - { clientId = 9237, buy = 12500, count = 1 }, - { clientId = 9238, buy = 17000, count = 1 }, - { clientId = 9239, buy = 12500, count = 1 }, - { clientId = 9240, buy = 13000, count = 1 }, - { clientId = 9241, buy = 10000, count = 1 }, - { clientId = 9247, buy = 13500, count = 1 }, - { clientId = 9248, buy = 12500, count = 1 }, - { clientId = 9249, buy = 13000, count = 1 }, - { clientId = 9251, buy = 8000, count = 1 }, - { clientId = 9252, buy = 13000, count = 1 }, - { clientId = 9255, buy = 25000, count = 1 }, - { clientId = 9308, buy = 5250, count = 1 }, - { clientId = 9390, buy = 8500, count = 1 }, - { clientId = 9391, buy = 10000, count = 1 }, - { clientId = 9537, buy = 350, count = 1 }, - { clientId = 9696, buy = 1000, count = 1 }, - { clientId = 9698, buy = 1000, count = 1 }, - { clientId = 9699, buy = 1000, count = 1 }, - { clientId = 10009, buy = 700, count = 1 }, - { clientId = 10011, buy = 650, count = 1 }, - { clientId = 10025, buy = 600, count = 1 }, - { clientId = 10028, buy = 666, count = 1 }, - { clientId = 10183, buy = 1000, count = 1 }, - { clientId = 10187, buy = 1000, count = 1 }, - { clientId = 10189, buy = 1000, count = 1 }, - { clientId = 11329, buy = 1000, count = 1 }, - { clientId = 11339, buy = 550, count = 1 }, - { clientId = 11341, buy = 1000, count = 1 }, - { clientId = 11544, buy = 600, count = 1 }, - { clientId = 11545, buy = 4000, count = 1 }, - { clientId = 11546, buy = 4000, count = 1 }, - { clientId = 11547, buy = 4000, count = 1 }, - { clientId = 11548, buy = 4000, count = 1 }, - { clientId = 11549, buy = 4000, count = 1 }, - { clientId = 11550, buy = 4000, count = 1 }, - { clientId = 11551, buy = 4000, count = 1 }, - { clientId = 11552, buy = 4000, count = 1 }, - { clientId = 13974, buy = 5000, count = 1 }, - { clientId = 31414, buy = 50000, count = 1 }, - { clientId = 31447, buy = 5000, count = 1 }, + { itemname = "almanac of magic", clientid = 10025, buy = 600 }, + { itemname = "animal fetish", clientid = 9236, buy = 10000 }, + { itemname = "baby rotworm", clientid = 10026, buy = 600 }, + { itemname = "bag with naga eggs", clientid = 39577, buy = 10000 }, + { itemname = "bale of white cloth", clientid = 142, buy = 6000 }, + { itemname = "beer bottle", clientid = 136, buy = 600 }, + { itemname = "bill", clientid = 3216, buy = 8000 }, + { itemname = "blood crystal", clientid = 8453, buy = 50000 }, + { itemname = "bloodkiss flower", clientid = 9241, buy = 10000 }, + { itemname = "book with old legends", clientid = 25239, buy = 2000 }, + { itemname = "bundle of rags", clientid = 9191, buy = 5000 }, + { itemname = "butterfly conservation kit", clientid = 39340, buy = 40000 }, + { itemname = "carrying device", clientid = 9698, buy = 1000 }, + { itemname = "case of rust bugs", clientid = 350, buy = 7000 }, + { itemname = "cask of brown ale", clientid = 8774, buy = 7000 }, + { itemname = "celestial chart", clientid = 39136, buy = 10000 }, + { itemname = "cigar", clientid = 141, buy = 2000 }, + { itemname = "conch shell", clientid = 43861, buy = 10000 }, + { itemname = "cookbook", clientid = 3234, buy = 1000 }, + { itemname = "copied research notes", clientid = 40515, buy = 5000 }, + { itemname = "crate", clientid = 117, buy = 1000 }, + { itemname = "crimson nightshade blossoms", clientid = 27465, buy = 7000 }, + { itemname = "damaged logbook", clientid = 6124, buy = 40000 }, + { itemname = "dark essence", clientid = 9238, buy = 17000 }, + { itemname = "dark moon mirror", clientid = 25729, buy = 5000 }, + { itemname = "dark sun catcher", clientid = 25733, buy = 5000 }, + { itemname = "deep crystal", clientid = 9240, buy = 13000 }, + { itemname = "dragha's spellbook", clientid = 6120, buy = 16000 }, + { itemname = "elemental crystal", clientid = 9251, buy = 8000 }, + { itemname = "empty starlight vial", clientid = 25731, buy = 5000 }, + { itemname = "exploding cookie", clientid = 130, buy = 100 }, + { itemname = "exquisite silk", clientid = 11545, buy = 4000 }, + { itemname = "exquisite wood", clientid = 11547, buy = 4000 }, + { itemname = "faded last will", clientid = 11544, buy = 600 }, + { itemname = "fae talisman", clientid = 25295, buy = 5000 }, + { itemname = "family brooch", clientid = 4834, buy = 1000 }, + { itemname = "family signet ring", clientid = 406, buy = 15000 }, + { itemname = "fan club membership card", clientid = 9391, buy = 10000 }, + { itemname = "filled carrying device", clientid = 9699, buy = 1000 }, + { itemname = "fishnapped goldfish", clientid = 7936, buy = 7000 }, + { itemname = "flask of crown polisher", clientid = 10009, buy = 700 }, + { itemname = "flask of extra greasy oil", clientid = 10189, buy = 1000 }, + { itemname = "flask of poison", clientid = 10183, buy = 1000 }, + { itemname = "flexible dragon scale", clientid = 11550, buy = 4000 }, + { itemname = "formula for a memory potion", clientid = 9188, buy = 5000 }, + { itemname = "funeral urn", clientid = 4847, buy = 6000 }, + { itemname = "fur of a wolf whelp", clientid = 25238, buy = 5000 }, + { itemname = "ghost charm", clientid = 8822, buy = 20000 }, + { itemname = "ghost's tear", clientid = 8746, buy = 50000 }, + { itemname = "ghostsilver lantern", clientid = 23734, buy = 20000 }, + { itemname = "giant ape's hair", clientid = 4832, buy = 24000 }, + { itemname = "golden symbol of suon", clientid = 27499, buy = 10000 }, + { itemname = "gold nuggets", clientid = 27444, buy = 10000 }, + { itemname = "golem blueprint", clientid = 9247, buy = 13500 }, + { itemname = "golem head", clientid = 9255, buy = 25000 }, + { itemname = "hastily scribbled note", clientid = 27370, buy = 3000 }, + { itemname = "headache pill", clientid = 9537, buy = 350 }, + { itemname = "heliodor's scrolls", clientid = 43969, buy = 50000 }, + { itemname = "icicle chisel", clientid = 39578, buy = 12000 }, + { itemname = "incantation fragment", clientid = 18933, buy = 4000 }, + { itemname = "ivory lyre", clientid = 31447, buy = 50000 }, + { itemname = "julius' map", clientid = 8200, buy = 25000 }, + { itemname = "letterbag", clientid = 3217, buy = 8000 }, + { itemname = "letter to markwin", clientid = 3220, buy = 8000 }, + { itemname = "lump of clay", clientid = 1000, buy = 1000 }, + { itemname = "machine crate", clientid = 9390, buy = 8500 }, + { itemname = "magic crystal", clientid = 11552, buy = 4000 }, + { itemname = "mago mechanic core", clientid = 9249, buy = 13000 }, + { itemname = "map to the unknown", clientid = 10011, buy = 650 }, + { itemname = "memory crystal", clientid = 7281, buy = 500 }, + { itemname = "memory stone", clientid = 4841, buy = 3000 }, + { itemname = "monk's diary", clientid = 3212, buy = 3000 }, + { itemname = "morik's helmet", clientid = 8820, buy = 8000 }, + { itemname = "mystic root", clientid = 11551, buy = 4000 }, + { itemname = "nautical map", clientid = 9308, buy = 5250 }, + { itemname = "nightshade distillate", clientid = 27461, buy = 15000 }, + { itemname = "old iron", clientid = 11549, buy = 4000 }, + { itemname = "old map", clientid = 24947, buy = 2000 }, + { itemname = "old power core", clientid = 9252, buy = 13000 }, + { itemname = "part of an old map", clientid = 24943, buy = 2000 }, + { itemname = "part of an old map", clientid = 24944, buy = 2000 }, + { itemname = "part of an old map", clientid = 24945, buy = 2000 }, + { itemname = "piece of parchment", clientid = 27372, buy = 1500 }, + { itemname = "piece of parchment", clientid = 27443, buy = 1500 }, + { itemname = "piece of parchment", clientid = 27371, buy = 1500 }, + { itemname = "plans for a strange device", clientid = 9696, buy = 1000 }, + { itemname = "poison salt crystal", clientid = 22694, buy = 20000 }, + { itemname = "present", clientid = 3218, buy = 16000 }, + { itemname = "rare crystal", clientid = 9697, buy = 1000 }, + { itemname = "research notes", clientid = 8764, buy = 3000 }, + { itemname = "sacred earth", clientid = 11341, buy = 1000 }, + { itemname = "sceptre of sun and sea", clientid = 31414, buy = 50000 }, + { itemname = "secret letter", clientid = 402, buy = 1000 }, + { itemname = "shadow orb", clientid = 9237, buy = 12500 }, + { itemname = "sheet of tracing paper", clientid = 4843, buy = 500 }, + { itemname = "sheet of tracing paper", clientid = 4842, buy = 500 }, + { itemname = "silver nuggets", clientid = 27445, buy = 7000 }, + { itemname = "snake destroyer", clientid = 4835, buy = 8000 }, + { itemname = "soul contract", clientid = 10028, buy = 666 }, + { itemname = "special flask", clientid = 100, buy = 5000 }, + { itemname = "spectral cloth", clientid = 11546, buy = 4000 }, + { itemname = "spectral dress", clientid = 4836, buy = 15000 }, + { itemname = "stabilizer", clientid = 9248, buy = 12500 }, + { itemname = "stone tablet with ley lines", clientid = 27464, buy = 8000 }, + { itemname = "strange powder", clientid = 4838, buy = 5000 }, + { itemname = "striker's favourite pillow", clientid = 6105, buy = 16000 }, + { itemname = "strong sinew", clientid = 11548, buy = 4000 }, + { itemname = "suspicious documents", clientid = 400, buy = 2000 }, + { itemname = "suspicious signet ring", clientid = 349, buy = 15000 }, + { itemname = "tattered swan feather", clientid = 25244, buy = 2000 }, + { itemname = "tear of daraman", clientid = 3233, buy = 16000 }, + { itemname = "technomancer beard", clientid = 396, buy = 5000 }, + { itemname = "the alchemists' formulas", clientid = 8818, buy = 8000 }, + { itemname = "the dust of arthei", clientid = 8720, buy = 4000 }, + { itemname = "the dust of boreth", clientid = 8717, buy = 20000 }, + { itemname = "the dust of lersatio", clientid = 8718, buy = 25000 }, + { itemname = "the dust of marziel", clientid = 8719, buy = 30000 }, + { itemname = "the ring of the count", clientid = 7924, buy = 10000 }, + { itemname = "the witches' grimoire", clientid = 7874, buy = 25000 }, + { itemname = "toy mouse", clientid = 123, buy = 16000 }, + { itemname = "universal tool", clientid = 10027, buy = 550 }, + { itemname = "unworked sacred wood", clientid = 11339, buy = 1000 }, + { itemname = "waldo's post horn", clientid = 3219, buy = 2000 }, + { itemname = "whisper moss", clientid = 4827, buy = 18000 }, + { itemname = "whoopee cushion", clientid = 121, buy = 2000 }, + { itemname = "wolf tooth chain", clientid = 5940, buy = 10000 }, + { itemname = "worm queen tooth", clientid = 9239, buy = 12500 }, + { itemname = "wrinkled parchment", clientid = 4846, buy = 4000 }, + { itemname = "xodet's first wand", clientid = 9187, buy = 5000 }, } -- On buy npc shop message diff --git a/data-otservbr-global/npc/chuckles.lua b/data-otservbr-global/npc/chuckles.lua index 494e207d90d..cc53b0c9340 100644 --- a/data-otservbr-global/npc/chuckles.lua +++ b/data-otservbr-global/npc/chuckles.lua @@ -18,6 +18,56 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -45,70 +95,31 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of draconia", clientId = 8093, buy = 5000 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/fenech.lua b/data-otservbr-global/npc/fenech.lua index 9427983b9ba..792f6a26614 100644 --- a/data-otservbr-global/npc/fenech.lua +++ b/data-otservbr-global/npc/fenech.lua @@ -29,6 +29,57 @@ npcConfig.voices = { { text = "Offering all sorts of magic equipment." }, } +local itemsTable = { + ["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 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -69,6 +120,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -91,6 +148,9 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end @@ -99,56 +159,9 @@ npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) npcHandler:setMessage(MESSAGE_GREET, "Be mourned pilgrim in flesh and welcome to the magic store.") npcHandler:setMessage(MESSAGE_FAREWELL, "May enlightenment be your path.") npcHandler:setMessage(MESSAGE_WALKAWAY, "May enlightenment be your path.") -npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at wands or runes?") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/frans.lua b/data-otservbr-global/npc/frans.lua index a765815e597..a606f3137ba 100644 --- a/data-otservbr-global/npc/frans.lua +++ b/data-otservbr-global/npc/frans.lua @@ -18,6 +18,55 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Aaaaah... ruuunes... waaaaaands... rooooods... spellboooooks..." }, +} + +local itemsTable = { + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -45,47 +94,35 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Beeee greeeeted, Simula! What is your neeeed?") +npcHandler:setMessage(MESSAGE_FAREWELL, "Bye.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Bye.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/frederik.lua b/data-otservbr-global/npc/frederik.lua index e19513213ab..c95baed2a4a 100644 --- a/data-otservbr-global/npc/frederik.lua +++ b/data-otservbr-global/npc/frederik.lua @@ -23,6 +23,74 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["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 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,70 +118,35 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Hi there and welcome to my little magic shop.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Yeah, bye.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Yeah, bye.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/ghorza.lua b/data-otservbr-global/npc/ghorza.lua index fcd7f2949af..d085363c4e6 100644 --- a/data-otservbr-global/npc/ghorza.lua +++ b/data-otservbr-global/npc/ghorza.lua @@ -23,6 +23,75 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Great Hunger is upset." }, + { text = "Spirits are restless." }, + { text = "All water drying up and Vuzrog does nothing!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "animate dead rune", clientId = 3203, buy = 375 }, + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "disintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "soulfire rune", clientId = 3195, buy = 46 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -46,59 +115,35 @@ npcType.onThink = function(npc, interval) npcHandler:onThink(npc, interval) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Hello, scraggy human.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Take care of evil spirits.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Take care of evil spirits.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {potions} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "disintegrate rune", clientId = 3197, buy = 26 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "fireball rune", clientId = 3189, buy = 30 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "holy missile rune", clientId = 3182, buy = 16 }, - { itemName = "icicle rune", clientId = 3158, buy = 30 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "magic wall rune", clientId = 3180, buy = 116 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "paralyze rune", clientId = 3165, buy = 700 }, - { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "soulfire rune", clientId = 3195, buy = 46 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "stone shower rune", clientId = 3175, buy = 37 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, - { itemName = "wild growth rune", clientId = 3156, buy = 160 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/gnomegica.lua b/data-otservbr-global/npc/gnomegica.lua index 2f264240b3d..9e61340a24f 100644 --- a/data-otservbr-global/npc/gnomegica.lua +++ b/data-otservbr-global/npc/gnomegica.lua @@ -23,6 +23,70 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Oh my, such a mess! I really should clean up things more often." }, + { text = "Laaa Laaa LaLa Laa Laaa!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,59 +114,35 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Greetings! You came to the right gnome to buy {potions}, {runes} or {wands}. So let's talk about a {trade}.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye and come again.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye and come again.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3101, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/hamish.lua b/data-otservbr-global/npc/hamish.lua index 45b5ced3027..9d9029b556f 100644 --- a/data-otservbr-global/npc/hamish.lua +++ b/data-otservbr-global/npc/hamish.lua @@ -26,13 +26,52 @@ npcConfig.flags = { npcConfig.voices = { interval = 15000, chance = 50, - { text = "Health potions to refill your health in combat!" }, - { text = "Potions! Wand! Runes! Get them here!" }, + { text = "That's the spirit!" }, + { text = "Potions! Wands! Runes! Get them here!" }, + { text = "You levelled up but your wand is old? Come and buy a new one here!" }, + { text = "Ran out of mana or a little kablooie? Come to me to resupply!" }, + { text = "Low on magic and need a little extra? Get yourself a rune!" }, { text = "Pack of monsters give you trouble? Throw an area rune at them!" }, + { text = "Health potions to refill your health in combat!" }, + { text = "Taking back empty potion flasks! Get your deposit back here!" }, { text = "Careful with that! That's a highly reactive potion you have there!" }, - { text = "Run out of mana or a little kablooie? Come to me to resupply!" }, + { text = "Mana potions to refill your magic power!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "small health potion", clientId = 7876, buy = 20 }, + }, + ["runes"] = { + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light stone shower rune", clientId = 21351, buy = 25 }, + { itemName = "lightest missile rune", clientId = 21352, buy = 20 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, } +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -123,6 +162,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] if MsgContains(message, "dawnport") then npcHandler:say({ "Small and deceptively friendly-looking island. Well, I used to study the plants and herbs here for my potions.", @@ -145,41 +190,24 @@ local function creatureSayCallback(npc, creature, type, message) "Anyway, whatever he was before he joined, Tybald now fits the bill of the legendary hero. \z He even has a crush on lady Oressa. Cute. ", }, npc, creature, 200) + elseif categoryTable then + npcHandler:say("Take your pick!", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) npcHandler:setMessage( MESSAGE_GREET, "Hi there, fellow adventurer. \z - What's your need? Say {trade} and we'll soon get you fixed up. Or ask me about potions, wands, or runes." + What's your need? Say {trade} and we'll soon get you fixed up. Or ask me about {potions}, {wands}, or {runes}." ) npcHandler:setMessage(MESSAGE_FAREWELL, "Use your runes wisely!") - -npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) - +npcHandler:setMessage(MESSAGE_WALKAWAY, "Use your runes wisely!") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Take your pick! Or maybe you want to look only at {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "light stone shower rune", clientId = 21351, buy = 25 }, - { itemName = "lightest missile rune", clientId = 21352, buy = 20 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "small health potion", clientId = 7876, buy = 20 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/khanna.lua b/data-otservbr-global/npc/khanna.lua index 0adc4b8e4fb..f3537057b10 100644 --- a/data-otservbr-global/npc/khanna.lua +++ b/data-otservbr-global/npc/khanna.lua @@ -23,6 +23,76 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "If you need runes, this is the market stall for you!" }, + { text = "I'm selling magic equipment. Come and have a look." }, +} + +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 = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["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 = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -63,6 +133,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -85,81 +161,17 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Take your pick!", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) - +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 20 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "crystal ball", clientId = 3076, buy = 650 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "fireball rune", clientId = 3189, buy = 30 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "holy missile rune", clientId = 3182, buy = 16 }, - { itemName = "icicle rune", clientId = 3158, buy = 30 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "life crystal", clientId = 3061, sell = 75 }, - { itemName = "life ring", clientId = 3052, buy = 1000 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "magic wall rune", clientId = 3180, buy = 116 }, - { itemName = "mind stone", clientId = 3062, sell = 150 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "paralyze rune", clientId = 3165, buy = 700 }, - { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "soulfire rune", clientId = 3195, buy = 46 }, - { itemName = "spellbook of enlightenment", clientId = 8072, sell = 3500 }, - { itemName = "spellbook of lost souls", clientId = 8075, sell = 17500 }, - { itemName = "spellbook of mind control", clientId = 8074, sell = 12000 }, - { itemName = "spellbook of warding", clientId = 8073, sell = 7500 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "stone shower rune", clientId = 3175, buy = 37 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, - { itemName = "wild growth rune", clientId = 3156, buy = 160 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/mordecai.lua b/data-otservbr-global/npc/mordecai.lua index 624b800dca7..00e744a9ebd 100644 --- a/data-otservbr-global/npc/mordecai.lua +++ b/data-otservbr-global/npc/mordecai.lua @@ -23,6 +23,80 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Come in, young mages! Purchase the latest in runes and wands!" }, + { text = "Perhaps I need to bake them hotter? Or was it longer?" }, + { text = "Offering all sorts of magic equipment!" }, + { text = "Now where did Alaistar put the wood for the new wands?" }, + { text = "Hmmm.. Maybe those crushed gems caused the strange side effect?" }, + { text = "Wands and rods, the true magician's weapons of choice!" }, +} + +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 = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["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 = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,63 +124,35 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Choose wisely!", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Welcome to the magic emporium, child. Ask me for a trade if you need {runes}, {wands}, or spellbooks.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Take care, child.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Ah, the impetuosity of youth.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Choose wisely! Or maybe you want to look only at {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "holy missile rune", clientId = 3182, buy = 16 }, - { itemName = "icicle rune", clientId = 3158, buy = 30 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "magic wall rune", clientId = 3180, buy = 116 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "paralyze rune", clientId = 3165, buy = 700 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "soulfire rune", clientId = 3195, buy = 46 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, - { itemName = "wild growth rune", clientId = 3156, buy = 160 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/nelly.lua b/data-otservbr-global/npc/nelly.lua index b54709dd21d..88dff5dfbc6 100644 --- a/data-otservbr-global/npc/nelly.lua +++ b/data-otservbr-global/npc/nelly.lua @@ -23,6 +23,86 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Welcome to the post office!" }, + { text = "Also selling runes, potions and magical equipment!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { 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 }, + }, + ["post stuff"] = { + { itemName = "label", clientId = 3507, buy = 1 }, + { itemName = "letter", clientId = 3505, buy = 8 }, + { itemName = "parcel", clientId = 3503, buy = 15 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,73 +130,35 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Hello |PLAYERNAME|, What can I do for you? I'm working for the post office, but I also trade with important {potions}, {runes} and other magical equipment.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye, |PLAYERNAME|.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Ah, the impetuosity of youth.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {post stuff}, {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "label", clientId = 3507, buy = 1 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "letter", clientId = 3505, buy = 8 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "parcel", clientId = 3503, buy = 15 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/nipuna.lua b/data-otservbr-global/npc/nipuna.lua index b24c55bee06..32623777ff7 100644 --- a/data-otservbr-global/npc/nipuna.lua +++ b/data-otservbr-global/npc/nipuna.lua @@ -23,6 +23,93 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "I'm selling magic equipment. Come and have a look." }, + { text = "If you need runes, this is the market stall for you!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "animate dead rune", clientId = 3203, buy = 375 }, + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["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 = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,48 +137,32 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_SENDTRADE, "Of course, just browse through my wares. Or do you want to look only at {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "dragonfruit", clientId = 11682, buy = 5 }, - { itemName = "egg", clientId = 3606, buy = 3 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/rabaz.lua b/data-otservbr-global/npc/rabaz.lua index b6f7f188cf8..83bb3254597 100644 --- a/data-otservbr-global/npc/rabaz.lua +++ b/data-otservbr-global/npc/rabaz.lua @@ -23,6 +23,74 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { 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 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -58,6 +126,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] if MsgContains(message, "mission") then if player:getStorageValue(Storage.TibiaTales.AnInterestInBotany) < 1 then npcHandler:setTopic(playerId, 1) @@ -100,75 +174,20 @@ local function creatureSayCallback(npc, creature, type, message) npcHandler:setTopic(playerId, 0) end end + elseif categoryTable then + npcHandler:say("Have a look.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Ah, a customer! Please feel free to browse my wares, |PLAYERNAME|.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye.") +npcHandler:setMessage(MESSAGE_SENDTRADE, "Have a look. But perhaps you just want to see my {potions}, {wands} or {runes}?") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/rachel.lua b/data-otservbr-global/npc/rachel.lua index 301f7ae2543..2153bb201ac 100644 --- a/data-otservbr-global/npc/rachel.lua +++ b/data-otservbr-global/npc/rachel.lua @@ -23,6 +23,63 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -68,6 +125,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -90,12 +153,14 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) - npcHandler:setMessage(MESSAGE_GREET, "Welcome |PLAYERNAME|! Whats your need?") npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye, |PLAYERNAME|.") npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye, |PLAYERNAME|.") @@ -104,61 +169,8 @@ npcHandler:setMessage( "Of course, just browse through my wares. \z Or do you want to look only at {potions}, {wands} or {runes}?" ) - npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "talon", clientId = 3034, sell = 320 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/rock_in_a_hard_place.lua b/data-otservbr-global/npc/rock_in_a_hard_place.lua index 29c36f10442..1a9fa25a8d5 100644 --- a/data-otservbr-global/npc/rock_in_a_hard_place.lua +++ b/data-otservbr-global/npc/rock_in_a_hard_place.lua @@ -45,214 +45,270 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local itemsTable = { + ["magic stuff"] = { + { itemName = "animate dead rune", clientId = 3203, buy = 375 }, + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire sword", clientId = 3280, sell = 1000 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "soulfire rune", clientId = 3195, buy = 46 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "supreme health potion", clientId = 23375, buy = 625 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { 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 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["local equipment"] = { + { itemName = "backpack", clientId = 2854, buy = 20 }, + { itemName = "basket", clientId = 2855, buy = 6 }, + { itemName = "bottle", clientId = 2875, buy = 3 }, + { itemName = "bucket", clientId = 2873, buy = 4 }, + { itemName = "calopteryx cape", clientId = 14086, sell = 15000 }, + { itemName = "carapace shield", clientId = 14088, sell = 32000 }, + { itemName = "candelabrum", clientId = 2911, buy = 8 }, + { itemName = "candlestick", clientId = 2917, buy = 2 }, + { itemName = "closed trap", clientId = 3481, buy = 280, sell = 75 }, + { itemName = "crowbar", clientId = 3304, buy = 260, sell = 50 }, + { itemName = "deepling axe", clientId = 13991, sell = 40000 }, + { itemName = "deepling squelcher", clientId = 14250, sell = 7000 }, + { itemName = "deepling staff", clientId = 13987, sell = 4000 }, + { itemName = "depth calcei", clientId = 13997, sell = 25000 }, + { itemName = "depth galea", clientId = 13995, sell = 35000 }, + { itemName = "depth lorica", clientId = 13994, sell = 30000 }, + { itemName = "depth ocrea", clientId = 13996, sell = 16000 }, + { itemName = "depth scutum", clientId = 13998, sell = 36000 }, + { itemName = "fishing rod", clientId = 3483, buy = 150, sell = 40 }, + { itemName = "grasshopper legs", clientId = 14087, sell = 15000 }, + { itemName = "guardian axe", clientId = 14043, sell = 9000 }, + { itemName = "hive bow", clientId = 14246, sell = 28000 }, + { itemName = "hive scythe", clientId = 14089, sell = 17000 }, + { itemName = "machete", clientId = 3308, buy = 35, sell = 6 }, + { itemName = "necklace of the deep", clientId = 13990, sell = 3000 }, + { itemName = "ornate chestplate", clientId = 13993, sell = 60000 }, + { itemName = "ornate crossbow", clientId = 14247, sell = 12000 }, + { itemName = "ornate legs", clientId = 13999, sell = 40000 }, + { itemName = "ornate mace", clientId = 14001, sell = 42000 }, + { itemName = "ornate shield", clientId = 14000, sell = 42000 }, + { itemName = "pick", clientId = 3456, buy = 50, sell = 15 }, + { itemName = "present", clientId = 2856, buy = 10 }, + { itemName = "rope", clientId = 3003, buy = 50, sell = 15 }, + { itemName = "scythe", clientId = 3453, buy = 50, sell = 10 }, + { itemName = "shovel", clientId = 3457, buy = 50, sell = 8 }, + { itemName = "torch", clientId = 2920, buy = 2 }, + { itemName = "warrior's axe", clientId = 14040, sell = 11000 }, + { itemName = "warrior's shield", clientId = 14042, sell = 9000 }, + { itemName = "watch", clientId = 2906, buy = 20, sell = 6 }, + { itemName = "wooden hammer", clientId = 3459, sell = 15 }, + { itemName = "worm", clientId = 3492, buy = 1 }, + }, + ["weapons"] = { + { itemName = "axe", clientId = 3274, buy = 20, sell = 7 }, + { itemName = "battle axe", clientId = 3266, buy = 235, sell = 80 }, + { itemName = "battle hammer", clientId = 3305, buy = 350, sell = 120 }, + { itemName = "bone club", clientId = 3337, sell = 5 }, + { itemName = "bone sword", clientId = 3338, buy = 75, sell = 20 }, + { itemName = "carlin sword", clientId = 3283, buy = 473, sell = 118 }, + { itemName = "club", clientId = 3270, buy = 5, sell = 1 }, + { itemName = "crowbar", clientId = 3304, buy = 260 }, + { itemName = "dagger", clientId = 3267, buy = 5, sell = 2 }, + { itemName = "double axe", clientId = 3275, sell = 260 }, + { itemName = "fire sword", clientId = 3280, sell = 1000 }, + { itemName = "halberd", clientId = 3269, sell = 400 }, + { itemName = "hand axe", clientId = 3268, buy = 8, sell = 4 }, + { itemName = "hatchet", clientId = 3276, sell = 25 }, + { itemName = "katana", clientId = 3300, sell = 35 }, + { itemName = "longsword", clientId = 3285, buy = 160, sell = 51 }, + { itemName = "mace", clientId = 3286, buy = 90, sell = 30 }, + { itemName = "morning star", clientId = 3282, buy = 430, sell = 100 }, + { itemName = "orcish axe", clientId = 3316, sell = 350 }, + { itemName = "rapier", clientId = 3272, buy = 15, sell = 5 }, + { itemName = "sabre", clientId = 3273, buy = 35, sell = 12 }, + { itemName = "short sword", clientId = 3294, buy = 26, sell = 10 }, + { itemName = "sickle", clientId = 3293, buy = 7, sell = 3 }, + { itemName = "spike sword", clientId = 3271, buy = 8000, sell = 240 }, + { itemName = "small axe", clientId = 3462, sell = 5 }, + { itemName = "studded club", clientId = 3336, sell = 10 }, + { itemName = "sword", clientId = 3264, buy = 85, sell = 25 }, + { itemName = "throwing knife", clientId = 3298, buy = 25, sell = 2 }, + { itemName = "two handed sword", clientId = 3265, buy = 950, sell = 450 }, + { itemName = "war hammer", clientId = 3279, buy = 10000, sell = 470 }, + }, + ["armor"] = { + { itemName = "battle shield", clientId = 3413, sell = 95 }, + { itemName = "brass armor", clientId = 3359, buy = 450, sell = 150 }, + { itemName = "brass helmet", clientId = 3354, buy = 120, sell = 30 }, + { itemName = "brass legs", clientId = 3372, buy = 195, sell = 49 }, + { itemName = "chain armor", clientId = 3358, buy = 200, sell = 70 }, + { itemName = "chain helmet", clientId = 3352, buy = 52, sell = 17 }, + { itemName = "chain legs", clientId = 3558, buy = 80, sell = 25 }, + { itemName = "coat", clientId = 3562, buy = 8, sell = 1 }, + { itemName = "copper shield", clientId = 3430, sell = 50 }, + { itemName = "doublet", clientId = 3379, buy = 16, sell = 3 }, + { itemName = "dwarven shield", clientId = 3425, buy = 500, sell = 100 }, + { itemName = "iron helmet", clientId = 3353, buy = 390, sell = 150 }, + { itemName = "jacket", clientId = 3561, buy = 12, sell = 1 }, + { itemName = "leather armor", clientId = 3361, buy = 35, sell = 12 }, + { itemName = "leather boots", clientId = 3552, buy = 10, sell = 2 }, + { itemName = "leather helmet", clientId = 3355, buy = 12, sell = 4 }, + { itemName = "leather legs", clientId = 3559, buy = 10, sell = 9 }, + { itemName = "legion helmet", clientId = 3374, sell = 22 }, + { itemName = "plate armor", clientId = 3357, buy = 1200, sell = 400 }, + { itemName = "plate legs", clientId = 3557, sell = 115 }, + { itemName = "plate shield", clientId = 3410, buy = 125, sell = 45 }, + { itemName = "scale armor", clientId = 3377, buy = 260, sell = 75 }, + { itemName = "soldier helmet", clientId = 3375, buy = 110, sell = 16 }, + { itemName = "steel helmet", clientId = 3351, buy = 580, sell = 293 }, + { itemName = "steel shield", clientId = 3409, buy = 240, sell = 80 }, + { itemName = "studded armor", clientId = 3378, buy = 90, sell = 25 }, + { itemName = "studded helmet", clientId = 3376, buy = 63, sell = 20 }, + { itemName = "studded legs", clientId = 3362, buy = 50, sell = 15 }, + { itemName = "studded shield", clientId = 3426, buy = 50, sell = 16 }, + { itemName = "swampling club", clientId = 17824, sell = 40 }, + { itemName = "viking helmet", clientId = 3367, buy = 265, sell = 66 }, + { itemName = "viking shield", clientId = 3431, buy = 260, sell = 85 }, + { itemName = "wooden shield", clientId = 3412, buy = 15, sell = 5 }, + }, + ["ammunition"] = { + { itemName = "arrow", clientId = 3447, buy = 3 }, + { itemName = "blue quiver", clientId = 35848, buy = 400 }, + { itemName = "bolt", clientId = 3446, buy = 4 }, + { itemName = "bow", clientId = 3350, buy = 400, sell = 100 }, + { itemName = "crossbow", clientId = 3349, buy = 500, sell = 120 }, + { itemName = "crystalline arrow", clientId = 15793, buy = 20 }, + { itemName = "diamond arrow", clientId = 35901, buy = 100 }, + { itemName = "drill bolt", clientId = 16142, buy = 12 }, + { itemName = "earth arrow", clientId = 774, buy = 5 }, + { itemName = "envenomed arrow", clientId = 16143, buy = 12 }, + { itemName = "flaming arrow", clientId = 763, buy = 5 }, + { itemName = "flash arrow", clientId = 761, buy = 5 }, + { itemName = "onyx arrow", clientId = 7365, buy = 7 }, + { itemName = "piercing bolt", clientId = 7363, buy = 5 }, + { itemName = "power bolt", clientId = 3450, buy = 7 }, + { itemName = "prismatic bolt", clientId = 16141, buy = 20 }, + { itemName = "quiver", clientId = 35562, buy = 400 }, + { itemName = "red quiver", clientId = 35849, buy = 400 }, + { itemName = "royal spear", clientId = 7378, buy = 15 }, + { itemName = "shiver arrow", clientId = 762, buy = 5 }, + { itemName = "sniper arrow", clientId = 7364, buy = 5 }, + { itemName = "spear", clientId = 3277, buy = 9, sell = 3 }, + { itemName = "spectral bolt", clientId = 35902, buy = 70 }, + { itemName = "tarsal arrow", clientId = 14251, buy = 6 }, + { itemName = "throwing star", clientId = 3287, buy = 42 }, + { itemName = "vortex bolt", clientId = 14252, buy = 6 }, + }, + ["post things"] = { + { itemName = "label", clientId = 3507, buy = 1 }, + { itemName = "letter", clientId = 3505, buy = 8 }, + { itemName = "parcel", clientId = 3503, buy = 15 }, + }, + ["creature products"] = { + { itemName = "compound eye", clientId = 14083, sell = 150 }, + { itemName = "crawler head plating", clientId = 14079, sell = 210 }, + { itemName = "deepling breaktime snack", clientId = 14011, sell = 90 }, + { itemName = "deepling claw", clientId = 14044, sell = 430 }, + { itemName = "deepling guard belt buckle", clientId = 14010, sell = 230 }, + { itemName = "deepling ridge", clientId = 14041, sell = 360 }, + { itemName = "deepling scales", clientId = 14017, sell = 80 }, + { itemName = "deepling warts", clientId = 14012, sell = 180 }, + { itemName = "deeptags", clientId = 14013, sell = 290 }, + { itemName = "dung ball", clientId = 14225, sell = 130 }, + { itemName = "eye of a deepling", clientId = 12730, sell = 150 }, + { itemName = "hand auger", clientId = 31334, buy = 25 }, + { itemName = "key to the drowned library", clientId = 14009, sell = 330 }, + { itemName = "kollos shell", clientId = 14077, sell = 420 }, + { itemName = "net", clientId = 31489, buy = 50 }, + { itemName = "spellsinger's seal", clientId = 14008, sell = 280 }, + { itemName = "spidris mandible", clientId = 14082, sell = 450 }, + { itemName = "spitter nose", clientId = 14078, sell = 340 }, + { itemName = "swarmer antenna", clientId = 14076, sell = 130 }, + { itemName = "waspoid claw", clientId = 14080, sell = 320 }, + { itemName = "waspoid wing", clientId = 14081, sell = 190 }, + }, +} + +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Here, have a look", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +local function onTradeRequest(npc, creature) + local player = Player(creature) + local playerId = player:getId() + + npcHandler:say("You would be surprised how many things are washed ashore here. I trade {magic stuff}, {local equipment}, {weapons}, {armor}, {ammunition}, {post things} and {creature products}.", npc, creature) + return true +end + +keywordHandler:addKeyword({ "job" }, StdModule.say, { npcHandler = npcHandler, text = "Have you noticed that I'm actually the only rock on this island with a proper job? Those lazy pebbleheads! I'm proud to announce: I'm a trader" }) +keywordHandler:addKeyword({ "name" }, StdModule.say, { npcHandler = npcHandler, text = "No, you got it all wrong! I said I'm stuck between a rock and a hard place!" }) +keywordHandler:addKeyword({ "help" }, StdModule.say, { npcHandler = npcHandler, text = "I can help you buy trading stuff with you. Good for me, good for you. It's a win-win!" }) +keywordHandler:addKeyword({ "job" }, StdModule.say, { npcHandler = npcHandler, text = "Have you noticed that I'm actually the only rock on this island with a proper job? Those lazy pebbleheads! I'm proud to announce: I'm a trader" }) + +npcHandler:setMessage(MESSAGE_GREET, "Everyone on this island has gone crazy! Except for me and you, it seems. Let's {trade} like normal people would.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Promise to come back sometime, will ya?") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Hey! Don't leave me alone with all these lunatics!") + +npcHandler:setCallback(CALLBACK_ON_TRADE_REQUEST, onTradeRequest) +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "animate dead rune", clientId = 3203, buy = 375 }, - { itemName = "arrow", clientId = 3447, buy = 3 }, - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "axe", clientId = 3274, buy = 20, sell = 7 }, - { itemName = "backpack", clientId = 2854, buy = 20 }, - { itemName = "basket", clientId = 2855, buy = 6 }, - { itemName = "battle axe", clientId = 3266, buy = 235, sell = 80 }, - { itemName = "battle hammer", clientId = 3305, buy = 350, sell = 120 }, - { itemName = "battle shield", clientId = 3413, sell = 95 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "blue quiver", clientId = 35848, buy = 400 }, - { itemName = "bolt", clientId = 3446, buy = 4 }, - { itemName = "bone club", clientId = 3337, sell = 5 }, - { itemName = "bone sword", clientId = 3338, buy = 75, sell = 20 }, - { itemName = "bottle", clientId = 2875, buy = 3 }, - { itemName = "bow", clientId = 3350, buy = 400, sell = 100 }, - { itemName = "brass armor", clientId = 3359, buy = 450, sell = 150 }, - { itemName = "brass helmet", clientId = 3354, buy = 120, sell = 30 }, - { itemName = "brass legs", clientId = 3372, buy = 195, sell = 49 }, - { itemName = "brass shield", clientId = 3411, buy = 65, sell = 25 }, - { itemName = "bucket", clientId = 2873, buy = 4 }, - { itemName = "calopteryx cape", clientId = 14086, sell = 15000 }, - { itemName = "candelabrum", clientId = 2911, buy = 8 }, - { itemName = "candlestick", clientId = 2917, buy = 2 }, - { itemName = "carapace shield", clientId = 14088, sell = 32000 }, - { itemName = "carlin sword", clientId = 3283, buy = 473, sell = 118 }, - { itemName = "chain armor", clientId = 3358, buy = 200, sell = 70 }, - { itemName = "chain helmet", clientId = 3352, buy = 52, sell = 17 }, - { itemName = "chain legs", clientId = 3558, buy = 80, sell = 25 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "closed trap", clientId = 3481, buy = 280, sell = 75 }, - { itemName = "club", clientId = 3270, buy = 5, sell = 1 }, - { itemName = "coat", clientId = 3562, buy = 8, sell = 1 }, - { itemName = "compound eye", clientId = 14083, sell = 150 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "copper shield", clientId = 3430, sell = 50 }, - { itemName = "crawler head plating", clientId = 14079, sell = 210 }, - { itemName = "crossbow", clientId = 3349, buy = 500, sell = 120 }, - { itemName = "crowbar", clientId = 3304, buy = 260, sell = 50 }, - { itemName = "crowbar", clientId = 3304, buy = 260 }, - { itemName = "crystalline arrow", clientId = 15793, buy = 20 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "dagger", clientId = 3267, buy = 5, sell = 2 }, - { itemName = "deepling axe", clientId = 13991, sell = 40000 }, - { itemName = "deepling breaktime snack", clientId = 14011, sell = 90 }, - { itemName = "deepling claw", clientId = 14044, sell = 430 }, - { itemName = "deepling guard belt buckle", clientId = 14010, sell = 230 }, - { itemName = "deepling ridge", clientId = 14041, sell = 360 }, - { itemName = "deepling scales", clientId = 14017, sell = 80 }, - { itemName = "deepling squelcher", clientId = 14250, sell = 7000 }, - { itemName = "deepling staff", clientId = 13987, sell = 4000 }, - { itemName = "deepling warts", clientId = 14012, sell = 180 }, - { itemName = "deeptags", clientId = 14013, sell = 290 }, - { itemName = "depth calcei", clientId = 13997, sell = 25000 }, - { itemName = "depth galea", clientId = 13995, sell = 35000 }, - { itemName = "depth lorica", clientId = 13994, sell = 30000 }, - { itemName = "depth ocrea", clientId = 13996, sell = 16000 }, - { itemName = "depth scutum", clientId = 13998, sell = 36000 }, - { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "diamond arrow", clientId = 35901, buy = 100 }, - { itemName = "double axe", clientId = 3275, sell = 260 }, - { itemName = "doublet", clientId = 3379, buy = 16, sell = 3 }, - { itemName = "drill bolt", clientId = 16142, buy = 12 }, - { itemName = "dung ball", clientId = 14225, sell = 130 }, - { itemName = "dwarven shield", clientId = 3425, buy = 500, sell = 100 }, - { itemName = "earth arrow", clientId = 774, buy = 5 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "envenomed arrow", clientId = 16143, buy = 12 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "eye of a deepling", clientId = 12730, sell = 150 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire sword", clientId = 3280, sell = 1000 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "fireball rune", clientId = 3189, buy = 30 }, - { itemName = "fishing rod", clientId = 3483, buy = 150, sell = 40 }, - { itemName = "flaming arrow", clientId = 763, buy = 5 }, - { itemName = "flash arrow", clientId = 761, buy = 5 }, - { itemName = "grasshopper legs", clientId = 14087, sell = 15000 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "guardian axe", clientId = 14043, sell = 9000 }, - { itemName = "halberd", clientId = 3269, sell = 400 }, - { itemName = "hand auger", clientId = 31334, buy = 25 }, - { itemName = "hand axe", clientId = 3268, buy = 8, sell = 4 }, - { itemName = "hatchet", clientId = 3276, sell = 25 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "hive bow", clientId = 14246, sell = 28000 }, - { itemName = "hive scythe", clientId = 14089, sell = 17000 }, - { itemName = "holy missile rune", clientId = 3182, buy = 16 }, - { itemName = "icicle rune", clientId = 3158, buy = 30 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "iron helmet", clientId = 3353, buy = 390, sell = 150 }, - { itemName = "jacket", clientId = 3561, buy = 12, sell = 1 }, - { itemName = "katana", clientId = 3300, sell = 35 }, - { itemName = "key to the drowned library", clientId = 14009, sell = 330 }, - { itemName = "kollos shell", clientId = 14077, sell = 420 }, - { itemName = "label", clientId = 3507, buy = 1 }, - { itemName = "leather armor", clientId = 3361, buy = 35, sell = 12 }, - { itemName = "leather boots", clientId = 3552, buy = 10, sell = 2 }, - { itemName = "leather helmet", clientId = 3355, buy = 12, sell = 4 }, - { itemName = "leather legs", clientId = 3559, buy = 10, sell = 9 }, - { itemName = "legion helmet", clientId = 3374, sell = 22 }, - { itemName = "letter", clientId = 3505, buy = 8 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "longsword", clientId = 3285, buy = 160, sell = 51 }, - { itemName = "mace", clientId = 3286, buy = 90, sell = 30 }, - { itemName = "machete", clientId = 3308, buy = 35, sell = 6 }, - { itemName = "magic wall rune", clientId = 3180, buy = 116 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "morning star", clientId = 3282, buy = 430, sell = 100 }, - { itemName = "necklace of the deep", clientId = 13990, sell = 3000 }, - { itemName = "net", clientId = 31489, buy = 50 }, - { itemName = "onyx arrow", clientId = 7365, buy = 7 }, - { itemName = "orcish axe", clientId = 3316, sell = 350 }, - { itemName = "ornate chestplate", clientId = 13993, sell = 60000 }, - { itemName = "ornate crossbow", clientId = 14247, sell = 12000 }, - { itemName = "ornate legs", clientId = 13999, sell = 40000 }, - { itemName = "ornate mace", clientId = 14001, sell = 42000 }, - { itemName = "ornate shield", clientId = 14000, sell = 42000 }, - { itemName = "paralyze rune", clientId = 3165, buy = 700 }, - { itemName = "parcel", clientId = 3503, buy = 15 }, - { itemName = "pick", clientId = 3456, buy = 50, sell = 15 }, - { itemName = "piercing bolt", clientId = 7363, buy = 5 }, - { itemName = "plate armor", clientId = 3357, buy = 1200, sell = 400 }, - { itemName = "plate legs", clientId = 3557, sell = 115 }, - { itemName = "plate shield", clientId = 3410, buy = 125, sell = 45 }, - { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "power bolt", clientId = 3450, buy = 7 }, - { itemName = "present", clientId = 2856, buy = 10 }, - { itemName = "prismatic bolt", clientId = 16141, buy = 20 }, - { itemName = "quiver", clientId = 35562, buy = 400 }, - { itemName = "rapier", clientId = 3272, buy = 15, sell = 5 }, - { itemName = "red quiver", clientId = 35849, buy = 400 }, - { itemName = "rope", clientId = 3003, buy = 50, sell = 15 }, - { itemName = "royal spear", clientId = 7378, buy = 15 }, - { itemName = "sabre", clientId = 3273, buy = 35, sell = 12 }, - { itemName = "scale armor", clientId = 3377, buy = 260, sell = 75 }, - { itemName = "scythe", clientId = 3453, buy = 50, sell = 10 }, - { itemName = "shiver arrow", clientId = 762, buy = 5 }, - { itemName = "short sword", clientId = 3294, buy = 26, sell = 10 }, - { itemName = "shovel", clientId = 3457, buy = 50, sell = 8 }, - { itemName = "sickle", clientId = 3293, buy = 7, sell = 3 }, - { itemName = "small axe", clientId = 3462, sell = 5 }, - { itemName = "sniper arrow", clientId = 7364, buy = 5 }, - { itemName = "soldier helmet", clientId = 3375, buy = 110, sell = 16 }, - { itemName = "soulfire rune", clientId = 3195, buy = 46 }, - { itemName = "spear", clientId = 3277, buy = 9, sell = 3 }, - { itemName = "spectral bolt", clientId = 35902, buy = 70 }, - { itemName = "spellsinger's seal", clientId = 14008, sell = 280 }, - { itemName = "spidris mandible", clientId = 14082, sell = 450 }, - { itemName = "spike sword", clientId = 3271, buy = 8000, sell = 240 }, - { itemName = "spitter nose", clientId = 14078, sell = 340 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "steel helmet", clientId = 3351, buy = 580, sell = 293 }, - { itemName = "steel shield", clientId = 3409, buy = 240, sell = 80 }, - { itemName = "stone shower rune", clientId = 3175, buy = 37 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "studded armor", clientId = 3378, buy = 90, sell = 25 }, - { itemName = "studded club", clientId = 3336, sell = 10 }, - { itemName = "studded helmet", clientId = 3376, buy = 63, sell = 20 }, - { itemName = "studded legs", clientId = 3362, buy = 50, sell = 15 }, - { itemName = "studded shield", clientId = 3426, buy = 50, sell = 16 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "swampling club", clientId = 17824, sell = 40 }, - { itemName = "swarmer antenna", clientId = 14076, sell = 130 }, - { itemName = "sword", clientId = 3264, buy = 85, sell = 25 }, - { itemName = "tarsal arrow", clientId = 14251, buy = 6 }, - { itemName = "throwing knife", clientId = 3298, buy = 25, sell = 2 }, - { itemName = "throwing star", clientId = 3287, buy = 42 }, - { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, - { itemName = "torch", clientId = 2920, buy = 2 }, - { itemName = "two handed sword", clientId = 3265, buy = 950, sell = 450 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, - { itemName = "viking helmet", clientId = 3367, buy = 265, sell = 66 }, - { itemName = "viking shield", clientId = 3431, buy = 260, sell = 85 }, - { itemName = "vortex bolt", clientId = 14252, buy = 6 }, - { itemName = "war hammer", clientId = 3279, buy = 10000, sell = 470 }, - { itemName = "warrior's axe", clientId = 14040, sell = 11000 }, - { itemName = "warrior's shield", clientId = 14042, sell = 9000 }, - { itemName = "waspoid claw", clientId = 14080, sell = 320 }, - { itemName = "waspoid wing", clientId = 14081, sell = 190 }, - { itemName = "watch", clientId = 2906, buy = 20, sell = 6 }, - { itemName = "wild growth rune", clientId = 3156, buy = 160 }, - { itemName = "wooden hammer", clientId = 3459, sell = 15 }, - { itemName = "wooden shield", clientId = 3412, buy = 15, sell = 5 }, - { itemName = "worm", clientId = 3492, buy = 1 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/romir.lua b/data-otservbr-global/npc/romir.lua index e64a7ee0e97..70bcb0e8da0 100644 --- a/data-otservbr-global/npc/romir.lua +++ b/data-otservbr-global/npc/romir.lua @@ -23,6 +23,74 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { 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 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,70 +118,39 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Greetings, traveller. I'm a magical trader and trainer for sorcerer spells. How may I help you?") +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye.") +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/shiriel.lua b/data-otservbr-global/npc/shiriel.lua index 8793fe4940c..4c6408e7874 100644 --- a/data-otservbr-global/npc/shiriel.lua +++ b/data-otservbr-global/npc/shiriel.lua @@ -22,6 +22,63 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -49,65 +106,44 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Greetings, |PLAYERNAME|.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Goodbye, |PLAYERNAME|.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Goodbye, |PLAYERNAME|.") +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) +npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) + -- Greeting message keywordHandler:addGreetKeyword({ "ashari" }, { npcHandler = npcHandler, text = "Greetings, |PLAYERNAME|." }) --Farewell message keywordHandler:addFarewellKeyword({ "asgha thrazi" }, { npcHandler = npcHandler, text = "Goodbye, |PLAYERNAME|." }) -npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye!") - -npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/sigurd.lua b/data-otservbr-global/npc/sigurd.lua index 78b630963dd..0ac9eaea26a 100644 --- a/data-otservbr-global/npc/sigurd.lua +++ b/data-otservbr-global/npc/sigurd.lua @@ -18,6 +18,69 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Hiho adventurers, get your runes, potions, wands and rods here!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -45,59 +108,39 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage(MESSAGE_GREET, "Welcome to the magic store, |PLAYERNAME|! Ask me for a trade if you need something.") +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye. Come back soon.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye. Come back soon.") +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/sundara.lua b/data-otservbr-global/npc/sundara.lua index b919dbca971..a4b973e0db8 100644 --- a/data-otservbr-global/npc/sundara.lua +++ b/data-otservbr-global/npc/sundara.lua @@ -23,6 +23,93 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Health potions! Mana potions! Buy them here!" }, + { text = "All kinds of potions available here!" }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "animate dead rune", clientId = 3203, buy = 375 }, + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "desintegrate rune", clientId = 3197, buy = 26 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "energy bomb rune", clientId = 3149, buy = 203 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "fireball rune", clientId = 3189, buy = 30 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "holy missile rune", clientId = 3182, buy = 16 }, + { itemName = "icicle rune", clientId = 3158, buy = 30 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "magic wall rune", clientId = 3180, buy = 116 }, + { itemName = "paralyze rune", clientId = 3165, buy = 700 }, + { itemName = "poison bomb rune", clientId = 3173, buy = 85 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "stone shower rune", clientId = 3175, buy = 37 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "thunderstorm rune", clientId = 3202, buy = 47 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + { itemName = "wild growth rune", clientId = 3156, buy = 160 }, + }, + ["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 = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,48 +137,36 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "dragonfruit", clientId = 11682, buy = 5 }, - { itemName = "egg", clientId = 3606, buy = 3 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/tandros.lua b/data-otservbr-global/npc/tandros.lua index efc5ee49a57..204a32e10b8 100644 --- a/data-otservbr-global/npc/tandros.lua +++ b/data-otservbr-global/npc/tandros.lua @@ -23,6 +23,80 @@ npcConfig.flags = { floorchange = false, } +npcConfig.voices = { + interval = 15000, + chance = 50, + { text = "Potions, wands and runes for aspiring magicians." }, +} + +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { 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 = 379 }, + { itemName = "ultimate mana potion", clientId = 23373, buy = 438 }, + { itemName = "ultimate spirit potion", clientId = 23374, buy = 438 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { 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 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -50,69 +124,36 @@ npcType.onCloseChannel = function(npc, creature) npcHandler:onCloseChannel(npc, creature) end +local function creatureSayCallback(npc, creature, type, message) + local player = Player(creature) + local playerId = player:getId() + + if not npcHandler:checkInteraction(npc, creature) then + return false + end + + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] + + if categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) + end + return true +end + +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "hailstorm rod", clientId = 3067, buy = 15000 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "northwind rod", clientId = 8083, buy = 7500 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "supreme health potion", clientId = 23375, buy = 625 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { 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 = "underworld rod", clientId = 8082, buy = 22000 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { 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 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/topsy.lua b/data-otservbr-global/npc/topsy.lua index 06d4353dd66..751d1595c4b 100644 --- a/data-otservbr-global/npc/topsy.lua +++ b/data-otservbr-global/npc/topsy.lua @@ -29,6 +29,64 @@ npcConfig.voices = { { text = "Runes, wands, rods, health and mana potions! Have a look!" }, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -69,6 +127,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -91,73 +155,28 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) - npcHandler:setMessage( MESSAGE_GREET, "Hello, dear |PLAYERNAME|. How can I help you? \z - If you need magical equipment such as runes or wands, just ask me for a trade. " + If you need magical equipment such as {runes} or {wands}, just ask me for a {trade}." +) +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" ) npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye, |PLAYERNAME|. Do come again!") npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye, |PLAYERNAME|. Do come again!") - npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/npc/xodet.lua b/data-otservbr-global/npc/xodet.lua index 5ac76ecada8..f519a5ca2a4 100644 --- a/data-otservbr-global/npc/xodet.lua +++ b/data-otservbr-global/npc/xodet.lua @@ -23,6 +23,64 @@ npcConfig.flags = { floorchange = false, } +local itemsTable = { + ["potions"] = { + { itemName = "empty potion flask", clientId = 283, sell = 5 }, + { itemName = "empty potion flask", clientId = 284, sell = 5 }, + { itemName = "empty potion flask", clientId = 285, sell = 5 }, + { itemName = "great health potion", clientId = 239, buy = 225 }, + { itemName = "great mana potion", clientId = 238, buy = 144 }, + { itemName = "great spirit potion", clientId = 7642, buy = 228 }, + { itemName = "health potion", clientId = 266, buy = 50 }, + { itemName = "mana potion", clientId = 268, buy = 56 }, + { itemName = "strong health potion", clientId = 236, buy = 115 }, + { itemName = "strong mana potion", clientId = 237, buy = 93 }, + { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, + { itemName = "vial", clientId = 2874, sell = 5 }, + }, + ["runes"] = { + { itemName = "avalanche rune", clientId = 3161, buy = 57 }, + { itemName = "blank rune", clientId = 3147, buy = 10 }, + { itemName = "chameleon rune", clientId = 3178, buy = 210 }, + { itemName = "convince creature rune", clientId = 3177, buy = 80 }, + { itemName = "cure poison rune", clientId = 3153, buy = 65 }, + { itemName = "destroy field rune", clientId = 3148, buy = 15 }, + { itemName = "energy field rune", clientId = 3164, buy = 38 }, + { itemName = "energy wall rune", clientId = 3166, buy = 85 }, + { itemName = "explosion rune", clientId = 3200, buy = 31 }, + { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, + { itemName = "fire field rune", clientId = 3188, buy = 28 }, + { itemName = "fire wall rune", clientId = 3190, buy = 61 }, + { itemName = "great fireball rune", clientId = 3191, buy = 57 }, + { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, + { itemName = "intense healing rune", clientId = 3152, buy = 95 }, + { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, + { itemName = "poison field rune", clientId = 3172, buy = 21 }, + { itemName = "poison wall rune", clientId = 3176, buy = 52 }, + { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, + { itemName = "sudden death rune", clientId = 3155, buy = 135 }, + { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, + }, + ["wands"] = { + { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, + { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, + { itemName = "snakebite rod", clientId = 3066, buy = 500 }, + { itemName = "springsprout rod", clientId = 8084, buy = 18000 }, + { itemName = "terra rod", clientId = 3065, buy = 10000 }, + { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, + { itemName = "wand of decay", clientId = 3072, buy = 5000 }, + { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, + { itemName = "wand of vortex", clientId = 3074, buy = 500 }, + }, +} + +npcConfig.shop = {} +for _, categoryTable in pairs(itemsTable) do + for _, itemTable in ipairs(categoryTable) do + table.insert(npcConfig.shop, itemTable) + end +end + local keywordHandler = KeywordHandler:new() local npcHandler = NpcHandler:new(keywordHandler) @@ -63,6 +121,12 @@ local function creatureSayCallback(npc, creature, type, message) return false end + local formattedCategoryNames = {} + for categoryName, _ in pairs(itemsTable) do + table.insert(formattedCategoryNames, "{" .. categoryName .. "}") + end + + local categoryTable = itemsTable[message:lower()] local itemId = items[player:getVocation():getBaseId()] if MsgContains(message, "first rod") or MsgContains(message, "first wand") then if player:isMage() then @@ -85,65 +149,28 @@ local function creatureSayCallback(npc, creature, type, message) elseif MsgContains(message, "no") and npcHandler:getTopic(playerId) == 1 then npcHandler:say("Ok then.", npc, creature) npcHandler:setTopic(playerId, 0) + elseif categoryTable then + npcHandler:say("Of course, just browse through my wares.", npc, player) + npc:openShopWindowTable(player, categoryTable) end return true end npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) - +npcHandler:setMessage( + MESSAGE_GREET, + "Oh, please come in, |PLAYERNAME| \z + If you need magical equipment such as {runes} or {wands}, just ask me for a {trade}." +) +npcHandler:setMessage( + MESSAGE_SENDTRADE, + "Of course, just browse through my wares. \z + Or do you want to look only at {potions}, {wands} or {runes}?" +) +npcHandler:setMessage(MESSAGE_FAREWELL, "Good bye and come again.") +npcHandler:setMessage(MESSAGE_WALKAWAY, "Good bye and come again.") npcHandler:addModule(FocusModule:new(), npcConfig.name, true, true, true) -npcConfig.shop = { - { itemName = "avalanche rune", clientId = 3161, buy = 57 }, - { itemName = "blank rune", clientId = 3147, buy = 10 }, - { itemName = "chameleon rune", clientId = 3178, buy = 210 }, - { itemName = "convince creature rune", clientId = 3177, buy = 80 }, - { itemName = "cure poison rune", clientId = 3153, buy = 65 }, - { itemName = "destroy field rune", clientId = 3148, buy = 15 }, - { itemName = "durable exercise rod", clientId = 35283, buy = 945000, count = 1800 }, - { itemName = "durable exercise wand", clientId = 35284, buy = 945000, count = 1800 }, - { itemName = "empty potion flask", clientId = 283, sell = 5 }, - { itemName = "empty potion flask", clientId = 284, sell = 5 }, - { itemName = "empty potion flask", clientId = 285, sell = 5 }, - { itemName = "energy field rune", clientId = 3164, buy = 38 }, - { itemName = "energy wall rune", clientId = 3166, buy = 85 }, - { itemName = "exercise rod", clientId = 28556, buy = 262500, count = 500 }, - { itemName = "exercise wand", clientId = 28557, buy = 262500, count = 500 }, - { itemName = "explosion rune", clientId = 3200, buy = 31 }, - { itemName = "fire bomb rune", clientId = 3192, buy = 147 }, - { itemName = "fire field rune", clientId = 3188, buy = 28 }, - { itemName = "fire wall rune", clientId = 3190, buy = 61 }, - { itemName = "great fireball rune", clientId = 3191, buy = 57 }, - { itemName = "great health potion", clientId = 239, buy = 225 }, - { itemName = "great mana potion", clientId = 238, buy = 144 }, - { itemName = "great spirit potion", clientId = 7642, buy = 228 }, - { itemName = "health potion", clientId = 266, buy = 50 }, - { itemName = "heavy magic missile rune", clientId = 3198, buy = 12 }, - { itemName = "intense healing rune", clientId = 3152, buy = 95 }, - { itemName = "lasting exercise rod", clientId = 35289, buy = 7560000, count = 14400 }, - { itemName = "lasting exercise wand", clientId = 35290, buy = 7560000, count = 14400 }, - { itemName = "light magic missile rune", clientId = 3174, buy = 4 }, - { itemName = "mana potion", clientId = 268, buy = 56 }, - { itemName = "moonlight rod", clientId = 3070, buy = 1000 }, - { itemName = "necrotic rod", clientId = 3069, buy = 5000 }, - { itemName = "poison field rune", clientId = 3172, buy = 21 }, - { itemName = "poison wall rune", clientId = 3176, buy = 52 }, - { itemName = "snakebite rod", clientId = 3066, buy = 500 }, - { itemName = "spellbook", clientId = 3059, buy = 150 }, - { itemName = "spellwand", clientId = 651, sell = 299 }, - { itemName = "stalagmite rune", clientId = 3179, buy = 12 }, - { itemName = "strong health potion", clientId = 236, buy = 115 }, - { itemName = "strong mana potion", clientId = 237, buy = 93 }, - { itemName = "sudden death rune", clientId = 3155, buy = 135 }, - { itemName = "terra rod", clientId = 3065, buy = 10000 }, - { itemName = "ultimate healing rune", clientId = 3160, buy = 175 }, - { itemName = "ultimate health potion", clientId = 7643, buy = 379 }, - { itemName = "vial", clientId = 2874, sell = 5 }, - { itemName = "wand of cosmic energy", clientId = 3073, buy = 10000 }, - { itemName = "wand of decay", clientId = 3072, buy = 5000 }, - { itemName = "wand of dragonbreath", clientId = 3075, buy = 1000 }, - { itemName = "wand of vortex", clientId = 3074, buy = 500 }, -} -- On buy npc shop message npcType.onBuyItem = function(npc, player, itemId, subType, amount, ignore, inBackpacks, totalCost) npc:sellItem(player, itemId, amount, subType, 0, ignore, inBackpacks) diff --git a/data-otservbr-global/scripts/actions/other/outfit_memorial.lua b/data-otservbr-global/scripts/actions/other/outfit_memorial.lua index d722bb13274..6061d5f11b9 100644 --- a/data-otservbr-global/scripts/actions/other/outfit_memorial.lua +++ b/data-otservbr-global/scripts/actions/other/outfit_memorial.lua @@ -39,7 +39,7 @@ function outfitMemorial.onUse(player, item, fromPosition, target, toPosition, is for i = 1, 3 do response:addU16(#goldenOutfitCache[i]) for j = 1, #goldenOutfitCache[i] do - response:addString(goldenOutfitCache[i][j]) + response:addString(goldenOutfitCache[i][j], "outfitMemorial.onUse - goldenOutfitCache[i][j]") end end @@ -52,7 +52,7 @@ function outfitMemorial.onUse(player, item, fromPosition, target, toPosition, is for i = 1, 3 do response:addU16(#royalOutfitCache[i]) for j = 1, #royalOutfitCache[i] do - response:addString(royalOutfitCache[i][j]) + response:addString(royalOutfitCache[i][j], "outfitMemorial.onUse - royalOutfitCache[i][j]") end end diff --git a/data-otservbr-global/scripts/actions/other/temple_scroll.lua b/data-otservbr-global/scripts/actions/other/temple_scroll.lua index fdfeff177e1..1f170d72be9 100644 --- a/data-otservbr-global/scripts/actions/other/temple_scroll.lua +++ b/data-otservbr-global/scripts/actions/other/temple_scroll.lua @@ -4,12 +4,13 @@ function templeScroll.onUse(player, item, fromPosition, target, toPosition, isHo local inPz = player:getTile():hasFlag(TILESTATE_PROTECTIONZONE) local inFight = player:isPzLocked() or player:getCondition(CONDITION_INFIGHT, CONDITIONID_DEFAULT) if inPz or not inFight then - player:teleportTo(getTownTemplePosition(player:getTown():getId())) + fromPosition:sendMagicEffect(CONST_ME_TELEPORT) + player:teleportTo(player:getTown():getTemplePosition()) + player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) item:remove() - Position(fromPosition):sendMagicEffect(CONST_ME_TELEPORT) else player:sendCancelMessage("You can't use this when you're in a fight.") - Position(fromPosition):sendMagicEffect(CONST_ME_POFF) + fromPosition:sendMagicEffect(CONST_ME_POFF) end return true end diff --git a/data-otservbr-global/scripts/spells/monster/icicle_heal.lua b/data-otservbr-global/scripts/spells/monster/icicle_heal.lua index 65231af0a4b..f2bae458bd0 100644 --- a/data-otservbr-global/scripts/spells/monster/icicle_heal.lua +++ b/data-otservbr-global/scripts/spells/monster/icicle_heal.lua @@ -4,16 +4,17 @@ combat:setParameter(COMBAT_PARAM_AGGRESSIVE, 0) combat:setArea(createCombatArea(AREA_CIRCLE3X3)) function onTargetCreature(creature, target) - local min = 400 - local max = 600 - - local master = target:getMaster() - if target:isPlayer() and not master or master and master:isPlayer() then - return true + local spectators, spectator = Game.getSpectators(creature:getPosition(), false, false, 3, 3, 3, 3) + for i = 1, #spectators do + spectator = spectators[i] + if spectator:isMonster() and spectator:getName():lower() == "dragon egg" then + creature:setTarget("dragon egg") + if target:getName():lower() == "dragon egg" then + target:addHealth(-100) + return true + end + end end - - doTargetCombatHealth(0, target, COMBAT_HEALING, min, max, CONST_ME_NONE) - return true end combat:setCallback(CALLBACK_PARAM_TARGETCREATURE, "onTargetCreature") @@ -29,5 +30,5 @@ spell:words("###436") spell:isAggressive(true) spell:blockWalls(true) spell:needLearn(true) -spell:isSelfTarget(true) +spell:isSelfTarget(false) spell:register() diff --git a/data/items/items.xml b/data/items/items.xml index 407f909b6e8..67bb50a17ff 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -24931,13 +24931,13 @@ - + + - + - @@ -61565,6 +61565,7 @@ + @@ -65289,6 +65290,7 @@ + diff --git a/data/libs/functions/player.lua b/data/libs/functions/player.lua index 90565679a12..7be4b743228 100644 --- a/data/libs/functions/player.lua +++ b/data/libs/functions/player.lua @@ -64,7 +64,7 @@ function Player.sendExtendedOpcode(self, opcode, buffer) local networkMessage = NetworkMessage() networkMessage:addByte(0x32) networkMessage:addByte(opcode) - networkMessage:addString(buffer) + networkMessage:addString(buffer, "Player.sendExtendedOpcode - buffer") networkMessage:sendToPlayer(self) networkMessage:delete() return true diff --git a/data/libs/functions/quests.lua b/data/libs/functions/quests.lua index 66414115eaf..02487c55031 100644 --- a/data/libs/functions/quests.lua +++ b/data/libs/functions/quests.lua @@ -297,7 +297,7 @@ function Player.sendQuestLog(self) for questId = 1, #Quests do if self:questIsStarted(questId) then msg:addU16(questId) - msg:addString(Quests[questId].name .. (self:questIsCompleted(questId) and " (completed)" or "")) + msg:addString(Quests[questId].name .. (self:questIsCompleted(questId) and " (completed)" or ""), "Player.sendQuestLog") msg:addByte(self:questIsCompleted(questId)) end end @@ -319,8 +319,8 @@ function Player.sendQuestLine(self, questId) if self:getClient().version >= 1200 then msg:addU16(self:getMissionId(questId, missionId)) end - msg:addString(self:getMissionName(questId, missionId)) - msg:addString(self:getMissionDescription(questId, missionId)) + msg:addString(self:getMissionName(questId, missionId), "Player.sendQuestLine - self:getMissionName(questId, missionId)") + msg:addString(self:getMissionDescription(questId, missionId), "Player.sendQuestLine - self:getMissionDescription(questId, missionId)") end end end @@ -338,9 +338,9 @@ function Player.sendTrackedQuests(self, remainingQuests, missions) msg:addByte(#missions) for _, mission in ipairs(missions) do msg:addU16(mission.missionId) - msg:addString(mission.questName) - msg:addString(mission.missionName) - msg:addString(mission.missionDesc) + msg:addString(mission.questName, "Player.sendTrackedQuests - mission.questName") + msg:addString(mission.missionName, "Player.sendTrackedQuests - mission.missionName") + msg:addString(mission.missionDesc, "Player.sendTrackedQuests - mission.missionDesc") end msg:sendToPlayer(self) msg:delete() @@ -351,8 +351,8 @@ function Player.sendUpdateTrackedQuest(self, mission) msg:addByte(0xD0) msg:addByte(0x00) msg:addU16(mission.missionId) - msg:addString(mission.missionName) - msg:addString(mission.missionDesc) + msg:addString(mission.missionName, "Player.sendUpdateTrackedQuest - mission.missionName") + msg:addString(mission.missionDesc, "Player.sendUpdateTrackedQuest - mission.missionDesc") msg:sendToPlayer(self) msg:delete() end diff --git a/data/libs/hireling_lib.lua b/data/libs/hireling_lib.lua index 92c5436e15d..ee52150c5ea 100644 --- a/data/libs/hireling_lib.lua +++ b/data/libs/hireling_lib.lua @@ -609,7 +609,7 @@ function Player:sendHirelingOutfitWindow(hireling) msg:addU16(#availableOutfits) for _, outfit in ipairs(availableOutfits) do msg:addU16(outfit.lookType) - msg:addString(outfit.name) + msg:addString(outfit.name, "Player:sendHirelingOutfitWindow - outfit.name") msg:addByte(0x00) -- addons msg:addByte(0x00) -- Store bool end diff --git a/data/modules/scripts/blessings/blessings.lua b/data/modules/scripts/blessings/blessings.lua index 0582c6154ba..2f3aa90fe6b 100644 --- a/data/modules/scripts/blessings/blessings.lua +++ b/data/modules/scripts/blessings/blessings.lua @@ -183,7 +183,7 @@ Blessings.sendBlessDialog = function(player) for i = 1, historyAmount do msg:addU32(os.time()) -- timestamp msg:addByte(0) -- Color message (1 - Red | 0 = White loss) - msg:addString("Blessing Purchased") -- History message + msg:addString("Blessing Purchased", "Blessings.sendBlessDialog - Blessing Purchased") -- History message end msg:sendToPlayer(player) diff --git a/data/modules/scripts/daily_reward/daily_reward.lua b/data/modules/scripts/daily_reward/daily_reward.lua index 640ec0b2203..de4116f7ed3 100644 --- a/data/modules/scripts/daily_reward/daily_reward.lua +++ b/data/modules/scripts/daily_reward/daily_reward.lua @@ -367,7 +367,7 @@ function Player.sendOpenRewardWall(self, shrine) msg:addByte(self:getDayStreak()) -- current reward? day = 0, day 1, ... this should be resetted to 0 every week imo if DailyReward.isRewardTaken(self:getId()) then -- state (player already took reward? but just make sure noone wpe) msg:addByte(1) - msg:addString("Sorry, you have already taken your daily reward or you are unable to collect it.") -- Unknown message + msg:addString("Sorry, you have already taken your daily reward or you are unable to collect it.", "Player.sendOpenRewardWall - Sorry, you have already taken your daily reward or you are unable to collect it.") -- Unknown message if self:getJokerTokens() > 0 then msg:addByte(1) msg:addU16(self:getJokerTokens()) @@ -529,7 +529,7 @@ function Player.sendError(self, error) local msg = NetworkMessage() msg:addByte(ServerPackets.ShowDialog) msg:addByte(0x14) - msg:addString(error) + msg:addString(error, "Player.sendError - error") msg:sendToPlayer(self) end @@ -562,7 +562,7 @@ function Player.sendRewardHistory(self) for k, entry in ipairs(entries) do msg:addU32(entry.timestamp) msg:addByte(0) -- (self:isPremium() and 0 or 0) - msg:addString(entry.description) + msg:addString(entry.description, "Player.sendRewardHistory - entry.description") msg:addU16(entry.daystreak + 1) end msg:sendToPlayer(self) @@ -595,7 +595,7 @@ function Player.readDailyReward(self, msg, currentDay, state) local itemName = itemType:getArticle() .. " " .. itemType:getName() local itemWeight = itemType:getWeight() msg:addU16(itemId) - msg:addString(itemName) + msg:addString(itemName, "Player.readDailyReward - itemName") msg:addU32(itemWeight) end end @@ -605,7 +605,7 @@ function Player.readDailyReward(self, msg, currentDay, state) -- for i = 1, #rewards.things do -- msg:addByte(DAILY_REWARD_SYSTEM_TYPE_OTHER) -- type -- msg:addU16(rewards.things[i].id * 100) - -- msg:addString(rewards.things[i].name) + -- msg:addString(rewards.things[i].name, "Player.readDailyReward - rewards.things[i].name") -- msg:addByte(rewards.things[i].quantity) -- end elseif type == DAILY_REWARD_TYPE_PREY_REROLL then @@ -636,7 +636,7 @@ function Player.sendDailyReward(self) local maxBonus = 7 msg:addByte(maxBonus - 1) for i = 2, maxBonus do - msg:addString(DailyReward.strikeBonuses[i].text) + msg:addString(DailyReward.strikeBonuses[i].text, "Player.sendDailyReward - DailyReward.strikeBonuses[i].text") msg:addByte(i) end msg:addByte(1) -- Unknown diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua index 14d15c48b81..981db5cec6b 100644 --- a/data/modules/scripts/gamestore/init.lua +++ b/data/modules/scripts/gamestore/init.lua @@ -572,9 +572,9 @@ function openStore(playerId) GameStoreCategories, GameStoreCount = GameStore.Categories, #GameStore.Categories end local addCategory = function(category) - msg:addString(category.name) + msg:addString(category.name, "openStore - category.name") if oldProtocol then - msg:addString(category.description) + msg:addString(category.description, "openStore - category.description") end msg:addByte(category.state or GameStore.States.STATE_NONE) @@ -582,13 +582,13 @@ function openStore(playerId) msg:addByte(size) for _, icon in ipairs(category.icons) do if size > 0 then - msg:addString(icon) + msg:addString(icon, "openStore - icon") size = size - 1 end end if category.parent then - msg:addString(category.parent) + msg:addString(category.parent, "openStore - category.parent") else msg:addU16(0) end @@ -608,7 +608,7 @@ function sendOfferDescription(player, offerId, description) local msg = NetworkMessage() msg:addByte(0xEA) msg:addU32(offerId) - msg:addString(description) + msg:addString(description, "sendOfferDescription - description") msg:sendToPlayer(player) end @@ -777,7 +777,7 @@ function sendShowStoreOffers(playerId, category, redirectId) local msg = NetworkMessage() local haveSaleOffer = 0 msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString(category.name) + msg:addString(category.name, "sendShowStoreOffers - category.name") local categoryLimit = 65535 if oldProtocol then @@ -844,7 +844,7 @@ function sendShowStoreOffers(playerId, category, redirectId) msg:addU16(#disableReasons) for _, reason in ipairs(disableReasons) do - msg:addString(reason) + msg:addString(reason, "sendShowStoreOffers - reason") end if count > categoryLimit then @@ -855,7 +855,7 @@ function sendShowStoreOffers(playerId, category, redirectId) for name, offer in pairs(offers) do if count > 0 then count = count - 1 - msg:addString(name) + msg:addString(name, "sendShowStoreOffers - name") msg:addByte(#offer.offers) sendOfferDescription(player, offer.id and offer.id or 0xFFFF, offer.description) for _, off in ipairs(offer.offers) do @@ -900,7 +900,7 @@ function sendShowStoreOffers(playerId, category, redirectId) msg:addByte(type) if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1]) + msg:addString(offer.icons[1], "sendShowStoreOffers - offer.icons[1]") elseif type == GameStore.ConverType.SHOW_MOUNT then local mount = Mount(offer.id) msg:addU16(mount:getClientId()) @@ -966,7 +966,7 @@ function sendShowStoreOffersOnOldProtocol(playerId, category) local msg = NetworkMessage() local haveSaleOffer = 0 msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString(category.name) + msg:addString(category.name, "sendShowStoreOffersOnOldProtocol - category.name") if not category.offers then msg:addU16(0) @@ -1012,8 +1012,8 @@ function sendShowStoreOffersOnOldProtocol(playerId, category) local disabled, disabledReason = player:canBuyOffer(offer).disabled, player:canBuyOffer(offer).disabledReason local offerPrice = offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] or (newPrice or offer.price or 0xFFFF) msg:addU32(offer.id and offer.id or 0xFFFF) - msg:addString(name) - msg:addString(offer.description or GameStore.getDefaultDescription(offer.type, offer.count)) + msg:addString(name, "sendShowStoreOffersOnOldProtocol - name") + msg:addString(offer.description or GameStore.getDefaultDescription(offer.type, offer.count), "sendShowStoreOffersOnOldProtocol - offer.description or GameStore.getDefaultDescription(offer.type, offer.count)") msg:addU32(offerPrice) if offer.state then if offer.state == GameStore.States.STATE_SALE then @@ -1035,20 +1035,20 @@ function sendShowStoreOffersOnOldProtocol(playerId, category) msg:addByte(disabled) if disabled == 1 then - msg:addString(disabledReason) + msg:addString(disabledReason, "sendShowStoreOffersOnOldProtocol - disabledReason") end if offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then msg:addByte(1) - msg:addString((offer.name):gsub("% ", "_") .. ".png") + msg:addString((offer.name):gsub("% ", "_") .. ".png", "sendShowStoreOffersOnOldProtocol - (offer.name).png") elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT then msg:addByte(2) - msg:addString(offer.icons[1]) - msg:addString(offer.icons[2]) + msg:addString(offer.icons[1], "sendShowStoreOffersOnOldProtocol - offer.icons[1]") + msg:addString(offer.icons[2], "sendShowStoreOffersOnOldProtocol - offer.icons[2]") else msg:addByte(#offer.icons) for k, icon in ipairs(offer.icons) do - msg:addString(icon) + msg:addString(icon, "sendShowStoreOffersOnOldProtocol - icon") end end @@ -1089,7 +1089,7 @@ function sendStoreTransactionHistory(playerId, page, entriesPerPage) if not oldProtocol then msg:addByte(0x0) -- 0 = transferable tibia coin, 1 = normal tibia coin end - msg:addString(entry.description) + msg:addString(entry.description, "sendStoreTransactionHistory - entry.description") if not oldProtocol then msg:addByte(0) -- details end @@ -1107,7 +1107,7 @@ function sendStorePurchaseSuccessful(playerId, message) local msg = NetworkMessage() msg:addByte(GameStore.SendingPackets.S_CompletePurchase) msg:addByte(0x00) - msg:addString(message) + msg:addString(message, "sendStorePurchaseSuccessful - message") if oldProtocol then -- Send all coins can be used for buy store offers local totalCoins = player:getTibiaCoins() + player:getTransferableCoins() @@ -1129,7 +1129,7 @@ function sendStoreError(playerId, errorType, message) msg:addByte(GameStore.SendingPackets.S_StoreError) msg:addByte(errorType) - msg:addString(message) + msg:addString(message, "sendStoreError - message") msg:sendToPlayer(player) end @@ -2080,7 +2080,7 @@ function sendHomePage(playerId) local msg = NetworkMessage() msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString("Home") + msg:addString("Home", "sendHomePage - Home") msg:addU32(0x0) -- Redirect ID (not used here) msg:addByte(0x0) -- Window Type msg:addByte(0x0) -- Collections Size @@ -2106,13 +2106,13 @@ function sendHomePage(playerId) msg:addU16(#disableReasons) for _, reason in ipairs(disableReasons) do - msg:addString(reason) + msg:addString(reason, "sendHomePage - reason") end msg:addU16(#homeOffers) -- offers for p, offer in pairs(homeOffers) do - msg:addString(offer.name) + msg:addString(offer.name, "sendHomePage - offer.name") msg:addByte(0x1) -- ? msg:addU32(offer.id or 0) -- id msg:addU16(0x1) @@ -2132,7 +2132,7 @@ function sendHomePage(playerId) msg:addByte(type) if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1]) + msg:addString(offer.icons[1], "sendHomePage - offer.icons[1]") elseif type == GameStore.ConverType.SHOW_MOUNT then local mount = Mount(offer.id) if mount then @@ -2163,7 +2163,7 @@ function sendHomePage(playerId) local banner = HomeBanners msg:addByte(#banner.images) for m, image in ipairs(banner.images) do - msg:addString(image) + msg:addString(image, "sendHomePage - image") msg:addByte(0x04) -- Banner Type (offer) msg:addU32(0x00) -- Offer Id msg:addByte(0) diff --git a/data/npclib/npc_system/npc_handler.lua b/data/npclib/npc_system/npc_handler.lua index 27b941366ee..d72dca40553 100644 --- a/data/npclib/npc_system/npc_handler.lua +++ b/data/npclib/npc_system/npc_handler.lua @@ -467,6 +467,7 @@ if NpcHandler == nil then -- If is npc shop, send shop window and parse default message (if not have callback on the npc) if npc:isMerchant() then + npc:closeShopWindow(player) npc:openShopWindow(player) self:say(msg, npc, player) end diff --git a/docker/Dockerfile.arm b/docker/Dockerfile.arm index 200bed73a02..ef159ba142f 100644 --- a/docker/Dockerfile.arm +++ b/docker/Dockerfile.arm @@ -4,6 +4,7 @@ FROM ubuntu:23.04 AS dependencies RUN apt-get update && apt-get install -y --no-install-recommends cmake git \ unzip build-essential ca-certificates curl zip unzip tar \ pkg-config ninja-build autoconf automake libtool libluajit-5.1-dev libluajit-5.1-common \ + python3 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 1329df0462a..161a80a2e21 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -5,6 +5,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ apt-get update && apt-get install -y --no-install-recommends cmake git \ unzip build-essential ca-certificates curl zip unzip tar \ pkg-config ninja-build autoconf automake libtool \ + python3 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/docker/Dockerfile.x86 b/docker/Dockerfile.x86 index 32653f9c09f..3b035ecfd37 100644 --- a/docker/Dockerfile.x86 +++ b/docker/Dockerfile.x86 @@ -4,6 +4,7 @@ FROM ubuntu:23.04 AS dependencies RUN apt-get update && apt-get install -y --no-install-recommends cmake git \ unzip build-essential ca-certificates curl zip unzip tar \ pkg-config ninja-build autoconf automake libtool \ + python3 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/metrics/README.md b/metrics/README.md new file mode 100644 index 00000000000..551fc6d8731 --- /dev/null +++ b/metrics/README.md @@ -0,0 +1,65 @@ +# Canary Metrics (OpenTelemetry) + +By default, no metrics are collected or exported. To enable metrics, you must setup a metrics exporter. The following example shows how to setup a Prometheus exporter. + +config.lua + +```lua +metricsEnablePrometheus = true +metricsPrometheusAddress = "0.0.0.0:9464" +``` + +This, in and of itself will expose a Prometheus endpoint at `http://localhost:9464/metrics`. However, you will need to configure Prometheus to scrape this endpoint. + +The easiest, batteries included way, to do this is using the provided `docker-compose.yml` file provided in this `metrics` directory. Simply run `docker-compose up` and you will have a Prometheus instance running and scraping the Canary metrics endpoint. + +The `docker-compose.yml` file also includes a Grafana instance that is preconfigured to use the Prometheus instance as a data source. The Grafana instance is exposed at `http://localhost:3000` and the default username and password are `admin` and `admin` respectively (you will be prompted to change the password on first login). + +## Usage + +This is an **advanced** feature. While you can simply enable OStream and get metrics in your logs, that is not recommended to do in production. Prometheus can be run efficiently in production with minimal impact to server performance. + +_Enabling OStream:_ + +```config.lua +metricsEnableOstream = true +metricsOstreamInterval = 1000 +``` + +If you **don't** how what Prometheus and Grafana are, you need to learn that first: https://prometheus.io/ is your starting point. You can come back to this feature once you've understood how to install and run this software. + +## Metrics + +We export all kinds of metrics, but the most important ones are: + +Here's an interactive demo of a dashboard from a real production server: https://snapshots.raintank.io/dashboard/snapshot/bpiq45inK3I2Xixa2d7oNHWekdiDE6zr + +- Latency metrics for C++ methods +- Latency metrics for Lua functions +- Latency metrics for SQL queries +- Latency metrics for Dispatcher tasks +- Latency metrics for DB Lock contention + +**Screenshot** +![grafana](https://github.com/opentibiabr/canary/assets/223760/b307c335-9af9-4c1a-bf7e-5c3dc86a016d) + +## Analytics + +We also export analytic event, counters and other useful data. This is useful for debugging and understanding the behavior of the server. Some interesting ones are: + +- Stats around monsters killed (per monster type, player, etc) +- Stats around raw exp and total exp gained +- Stats around wealth gained (based on gold and item drops, with their NPC value) + +### Examples: + +_Note: you can normally see player names here, I've hidden those for privacy._ + +**Raw exp/h** +![exp-per-hour](https://github.com/opentibiabr/canary/assets/223760/3a873aca-f2e4-4d19-8e61-ed20c176a30f) + +**Raw gold/h** +![gold-per-hour](https://github.com/opentibiabr/canary/assets/223760/1c0d1e99-c4b9-4d9a-aced-75ac376b4673) + +**Monsters killed/h** +![monsters-per-hour](https://github.com/opentibiabr/canary/assets/223760/4d8c9e19-d579-4405-a018-fc69c79a11c2) diff --git a/metrics/docker-compose.yml b/metrics/docker-compose.yml new file mode 100644 index 00000000000..28ce37108fe --- /dev/null +++ b/metrics/docker-compose.yml @@ -0,0 +1,43 @@ +--- +version: "3" + +services: + prometheus: + image: prom/prometheus:latest + restart: unless-stopped + volumes: + - ./prometheus:/etc/prometheus + - prometheus-data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.enable-lifecycle" + - "--log.level=debug" + ports: + - "9090:9090" + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - monitoring-net + + grafana: + image: grafana/grafana:latest + restart: unless-stopped + volumes: + - grafana-data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + depends_on: + - prometheus + ports: + - "4444:3000" + networks: + - monitoring-net + +volumes: + prometheus-data: + grafana-data: + +networks: + monitoring-net: diff --git a/metrics/prometheus/prometheus.yml b/metrics/prometheus/prometheus.yml new file mode 100644 index 00000000000..97a21bc19f2 --- /dev/null +++ b/metrics/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +--- +global: + scrape_interval: 5s + scrape_timeout: 2s + evaluation_interval: 5s +scrape_configs: + - job_name: canary + static_configs: + - targets: ['host.docker.internal:9464'] diff --git a/src/account/account.cpp b/src/account/account.cpp index 881b9491406..2179482ef87 100644 --- a/src/account/account.cpp +++ b/src/account/account.cpp @@ -170,8 +170,9 @@ namespace account { } void Account::addPremiumDays(const int32_t &days) { - auto timeLeft = static_cast((m_account.premiumLastDay - getTimeNow()) % 86400); + auto timeLeft = std::max(0, static_cast((m_account.premiumLastDay - getTimeNow()) % 86400)); setPremiumDays(m_account.premiumRemainingDays + days); + m_account.premiumDaysPurchased += days; if (timeLeft > 0) { m_account.premiumLastDay += timeLeft; diff --git a/src/account/account.hpp b/src/account/account.hpp index 839b9928383..061f7dcdc31 100644 --- a/src/account/account.hpp +++ b/src/account/account.hpp @@ -13,6 +13,7 @@ #include "config/configmanager.hpp" #include "utils/definitions.hpp" #include "security/argon.hpp" +#include "utils/tools.hpp" namespace account { class Account { @@ -106,7 +107,7 @@ namespace account { void addPremiumDays(const int32_t &days); void setPremiumDays(const int32_t &days); [[nodiscard]] inline uint32_t getPremiumRemainingDays() const { - return m_account.premiumRemainingDays; + return m_account.premiumLastDay > getTimeNow() ? static_cast((m_account.premiumLastDay - getTimeNow()) / 86400) : 0; } [[nodiscard]] inline uint32_t getPremiumDaysPurchased() const { diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index c4a960e6244..321741bd7bb 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -160,11 +160,11 @@ namespace account { acc.id = result->getNumber("id"); acc.accountType = static_cast(result->getNumber("type")); - acc.premiumRemainingDays = result->getNumber("premdays"); acc.premiumLastDay = result->getNumber("lastday"); acc.sessionExpires = result->getNumber("expires"); acc.premiumDaysPurchased = result->getNumber("premdays_purchased"); acc.creationTime = result->getNumber("creation"); + acc.premiumRemainingDays = acc.premiumLastDay > getTimeNow() ? (acc.premiumLastDay - getTimeNow()) / 86400 : 0; setupLoyaltyInfo(acc); diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 5b644d4ed10..25439532b8c 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -61,6 +61,16 @@ int CanaryServer::run() { loadConfigLua(); logger.info("Server protocol: {}.{}{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER, g_configManager().getBoolean(OLD_PROTOCOL, __FUNCTION__) ? " and 10x allowed!" : ""); + metrics::Options metricsOptions; + metricsOptions.enablePrometheusExporter = g_configManager().getBoolean(METRICS_ENABLE_PROMETHEUS, __FUNCTION__); + if (metricsOptions.enablePrometheusExporter) { + metricsOptions.prometheusOptions.url = g_configManager().getString(METRICS_PROMETHEUS_ADDRESS, __FUNCTION__); + } + metricsOptions.enableOStreamExporter = g_configManager().getBoolean(METRICS_ENABLE_OSTREAM, __FUNCTION__); + if (metricsOptions.enableOStreamExporter) { + metricsOptions.ostreamOptions.export_interval_millis = std::chrono::milliseconds(g_configManager().getNumber(METRICS_OSTREAM_INTERVAL, __FUNCTION__)); + } + g_metrics().init(metricsOptions); rsa.start(); initializeDatabase(); @@ -375,4 +385,5 @@ void CanaryServer::modulesLoadHelper(bool loaded, std::string moduleName) { void CanaryServer::shutdown() { inject().shutdown(); g_dispatcher().shutdown(); + g_metrics().shutdown(); } diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 3381e671a98..700f5bfdb0c 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -11,270 +11,274 @@ // Enum enum ConfigKey_t : uint16_t { - ALLOW_CHANGEOUTFIT, - ONE_PLAYER_ON_ACCOUNT, + ACTIONS_DELAY_INTERVAL, + ADVENTURERSBLESSING_LEVEL, AIMBOT_HOTKEY_ENABLED, - REMOVE_RUNE_CHARGES, - EXPERIENCE_FROM_PLAYERS, - FREE_PREMIUM, - REPLACE_KICK_ON_LOGIN, - BIND_ONLY_GLOBAL_ADDRESS, - OPTIMIZE_DATABASE, - MARKET_PREMIUM, - EMOTE_SPELLS, - STAMINA_SYSTEM, - WARN_UNSAFE_SCRIPTS, - CONVERT_UNSAFE_SCRIPTS, - CLASSIC_ATTACK_SPEED, - SCRIPTS_CONSOLE_LOGS, - REMOVE_WEAPON_AMMO, - REMOVE_BEGINNING_WEAPON_AMMO, - REFUND_BEGINNING_WEAPON_MANA, - REMOVE_WEAPON_CHARGES, - REMOVE_POTION_CHARGES, - GLOBAL_SERVER_SAVE_NOTIFY_MESSAGE, - GLOBAL_SERVER_SAVE_CLEAN_MAP, - GLOBAL_SERVER_SAVE_CLOSE, - GLOBAL_SERVER_SAVE_SHUTDOWN, - FORCE_MONSTERTYPE_LOAD, - HOUSE_OWNED_BY_ACCOUNT, - CLEAN_PROTECTION_ZONES, ALLOW_BLOCK_SPAWN, - HOUSE_PURSHASED_SHOW_PRICE, - ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS, - WEATHER_RAIN, - WEATHER_THUNDER, - TOGGLE_FREE_QUEST, - ONLY_PREMIUM_ACCOUNT, - TOGGLE_MAP_CUSTOM, + ALLOW_CHANGEOUTFIT, + ALLOW_RELOAD, ALL_CONSOLE_LOG, - STAMINA_TRAINER, - STAMINA_PZ, - PUSH_WHEN_ATTACKING, - SORT_LOOT_BY_CHANCE, - TOGGLE_SAVE_INTERVAL, - TOGGLE_SAVE_INTERVAL_CLEAN_MAP, - PREY_ENABLED, - PREY_FREE_THIRD_SLOT, - TASK_HUNTING_ENABLED, - TASK_HUNTING_FREE_THIRD_SLOT, - STASH_MOVING, - TOGGLE_IMBUEMENT_SHRINE_STORAGE, - TOGGLE_IMBUEMENT_NON_AGGRESSIVE_FIGHT_ONLY, - AUTOLOOT, + AUTH_TYPE, AUTOBANK, - RATE_USE_STAGES, - INVENTORY_GLOW, - TELEPORT_SUMMONS, - TOGGLE_DOWNLOAD_MAP, - USE_ANY_DATAPACK_FOLDER, - ALLOW_RELOAD, + AUTOLOOT, + BESTIARY_KILL_MULTIPLIER, + BESTIARY_RATE_CHARM_SHOP_PRICE, + BIND_ONLY_GLOBAL_ADDRESS, + BLACK_SKULL_DURATION, + BOOSTED_BOSS_KILL_BONUS, + BOOSTED_BOSS_LOOT_BONUS, BOOSTED_BOSS_SLOT, - XP_DISPLAY_MODE, - TOGGLE_GOLD_POUCH_ALLOW_ANYTHING, - TOGGLE_GOLD_POUCH_QUICKLOOT_ONLY, - TOGGLE_SERVER_IS_RETRO, - TOGGLE_TRAVELS_FREE, - TELEPORT_PLAYER_TO_VOCATION_ROOM, - OLD_PROTOCOL, - TOGGLE_HAZARDSYSTEM, - LOYALTY_ENABLED, - PARTY_AUTO_SHARE_EXPERIENCE, - PARTY_SHARE_LOOT_BOOSTS, - RESET_SESSIONS_ON_STARTUP, - TOGGLE_WHEELSYSTEM, - TOGGLE_ATTACK_SPEED_ONFIST, - VIP_SYSTEM_ENABLED, - VIP_AUTOLOOT_VIP_ONLY, - VIP_KEEP_HOUSE, - VIP_STAY_ONLINE, - REWARD_CHEST_COLLECT_ENABLED, - TOGGLE_MOUNT_IN_PZ, - TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART, - TOGGLE_RECEIVE_REWARD, - TOGGLE_MAINTAIN_MODE, - MAP_NAME, - MAP_DOWNLOAD_URL, - MAP_AUTHOR, - HOUSE_RENT_PERIOD, - SERVER_NAME, - SERVER_MOTD, - OWNER_NAME, - OWNER_EMAIL, - URL, - LOCATION, - IP, - WORLD_TYPE, - MYSQL_HOST, - MYSQL_USER, - MYSQL_PASS, - MYSQL_DB, - MYSQL_SOCK, - AUTH_TYPE, - DEFAULT_PRIORITY, - STORE_IMAGES_URL, - MAP_CUSTOM_NAME, - MAP_CUSTOM_AUTHOR, - DISCORD_WEBHOOK_URL, - SAVE_INTERVAL_TYPE, - GLOBAL_SERVER_SAVE_TIME, - DATA_DIRECTORY, - CORE_DIRECTORY, - FORGE_FIENDISH_INTERVAL_TYPE, - FORGE_FIENDISH_INTERVAL_TIME, - TIBIADROME_CONCOCTION_TICK_TYPE, - M_CONST, - MAINTAIN_MODE_MESSAGE, - SQL_PORT, - MAX_PLAYERS, - PZ_LOCKED, - DEFAULT_DESPAWNRANGE, - DEFAULT_DESPAWNRADIUS, - RATE_EXPERIENCE, - RATE_SKILL, - RATE_LOOT, - RATE_MAGIC, - RATE_SPAWN, - RATE_KILLING_IN_THE_NAME_OF_POINTS, - HOUSE_PRICE_PER_SQM, - HOUSE_BUY_LEVEL, - HOUSE_LOSE_AFTER_INACTIVITY, - MAX_MESSAGEBUFFER, - ACTIONS_DELAY_INTERVAL, - EX_ACTIONS_DELAY_INTERVAL, - KICK_AFTER_MINUTES, - PROTECTION_LEVEL, - DEATH_LOSE_PERCENT, - STATUSQUERY_TIMEOUT, - FRAG_TIME, - WHITE_SKULL_TIME, - GAME_PORT, - LOGIN_PORT, - STATUS_PORT, - STAIRHOP_DELAY, - MAX_CONTAINER, - MAX_CONTAINER_ITEM, - MARKET_OFFER_DURATION, - DEPOT_BOXES, - FREE_DEPOT_LIMIT, - PREMIUM_DEPOT_LIMIT, + BOSSTIARY_KILL_MULTIPLIER, + BOSS_DEFAULT_TIME_TO_DEFEAT, + BOSS_DEFAULT_TIME_TO_FIGHT_AGAIN, + BUY_AOL_COMMAND_FEE, + BUY_BLESS_COMMAND_FEE, CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES, - MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER, - EXP_FROM_PLAYERS_LEVEL_RANGE, - MAX_PACKETS_PER_SECOND, + CLASSIC_ATTACK_SPEED, + CLEAN_PROTECTION_ZONES, COMPRESSION_LEVEL, - STORE_COIN_PACKET, + CONVERT_UNSAFE_SCRIPTS, + CORE_DIRECTORY, + CRITICALCHANCE, + DATA_DIRECTORY, DAY_KILLS_TO_RED, - WEEK_KILLS_TO_RED, - MONTH_KILLS_TO_RED, - RED_SKULL_DURATION, - BLACK_SKULL_DURATION, - ORANGE_SKULL_DURATION, - GLOBAL_SERVER_SAVE_NOTIFY_DURATION, - PUSH_DELAY, - PUSH_DISTANCE_DELAY, - STASH_ITEMS, - PARTY_LIST_MAX_DISTANCE, - STAMINA_ORANGE_DELAY, - STAMINA_GREEN_DELAY, - STAMINA_TRAINER_DELAY, - STAMINA_PZ_GAIN, - STAMINA_TRAINER_GAIN, - SAVE_INTERVAL_TIME, - PREY_REROLL_PRICE_LEVEL, - PREY_SELECTION_LIST_PRICE, - PREY_BONUS_TIME, - PREY_BONUS_REROLL_PRICE, - PREY_FREE_REROLL_TIME, - TASK_HUNTING_LIMIT_EXHAUST, - TASK_HUNTING_REROLL_PRICE_LEVEL, - TASK_HUNTING_SELECTION_LIST_PRICE, - TASK_HUNTING_BONUS_REROLL_PRICE, - TASK_HUNTING_FREE_REROLL_TIME, - MAX_ALLOWED_ON_A_DUMMY, - FREE_QUEST_STAGE, + DEATH_LOSE_PERCENT, + DEFAULT_DESPAWNRADIUS, + DEFAULT_DESPAWNRANGE, + DEFAULT_PRIORITY, DEPOTCHEST, - CRITICALCHANCE, - ADVENTURERSBLESSING_LEVEL, - FORGE_MAX_ITEM_TIER, - FORGE_COST_ONE_SLIVER, - FORGE_SLIVER_AMOUNT, - FORGE_CORE_COST, - FORGE_MAX_DUST, - FORGE_FUSION_DUST_COST, - FORGE_TRANSFER_DUST_COST, + DEPOT_BOXES, + DISCORD_WEBHOOK_DELAY_MS, + DISCORD_WEBHOOK_URL, + EMOTE_SPELLS, + EXPERIENCE_FROM_PLAYERS, + EXP_FROM_PLAYERS_LEVEL_RANGE, + EX_ACTIONS_DELAY_INTERVAL, + FAMILIAR_TIME, + FORCE_MONSTERTYPE_LOAD, + FORGE_AMOUNT_MULTIPLIER, FORGE_BASE_SUCCESS_RATE, FORGE_BONUS_SUCCESS_RATE, - FORGE_TIER_LOSS_REDUCTION, - FORGE_AMOUNT_MULTIPLIER, - FORGE_MIN_SLIVERS, - FORGE_MAX_SLIVERS, - FORGE_INFLUENCED_CREATURES_LIMIT, + FORGE_CORE_COST, + FORGE_COST_ONE_SLIVER, FORGE_FIENDISH_CREATURES_LIMIT, - BESTIARY_KILL_MULTIPLIER, - BOSSTIARY_KILL_MULTIPLIER, - BOOSTED_BOSS_LOOT_BONUS, - BOOSTED_BOSS_KILL_BONUS, - FAMILIAR_TIME, - BUY_AOL_COMMAND_FEE, - BUY_BLESS_COMMAND_FEE, - HAZARD_CRITICAL_INTERVAL, + FORGE_FIENDISH_INTERVAL_TIME, + FORGE_FIENDISH_INTERVAL_TYPE, + FORGE_FUSION_DUST_COST, + FORGE_INFLUENCED_CREATURES_LIMIT, + FORGE_MAX_DUST, + FORGE_MAX_ITEM_TIER, + FORGE_MAX_SLIVERS, + FORGE_MIN_SLIVERS, + FORGE_SLIVER_AMOUNT, + FORGE_TIER_LOSS_REDUCTION, + FORGE_TRANSFER_DUST_COST, + FRAG_TIME, + FREE_DEPOT_LIMIT, + FREE_PREMIUM, + FREE_QUEST_STAGE, + GAME_PORT, + GLOBAL_SERVER_SAVE_CLEAN_MAP, + GLOBAL_SERVER_SAVE_CLOSE, + GLOBAL_SERVER_SAVE_NOTIFY_DURATION, + GLOBAL_SERVER_SAVE_NOTIFY_MESSAGE, + GLOBAL_SERVER_SAVE_SHUTDOWN, + GLOBAL_SERVER_SAVE_TIME, HAZARD_CRITICAL_CHANCE, + HAZARD_CRITICAL_INTERVAL, HAZARD_CRITICAL_MULTIPLIER, HAZARD_DAMAGE_MULTIPLIER, HAZARD_DODGE_MULTIPLIER, - HAZARD_PODS_DROP_MULTIPLIER, - HAZARD_PODS_TIME_TO_DAMAGE, HAZARD_EXP_BONUS_MULTIPLIER, HAZARD_LOOT_BONUS_MULTIPLIER, HAZARD_PODS_DAMAGE, + HAZARD_PODS_DROP_MULTIPLIER, + HAZARD_PODS_TIME_TO_DAMAGE, HAZARD_PODS_TIME_TO_SPAWN, HAZARD_SPAWN_PLUNDER_MULTIPLIER, + HOUSE_BUY_LEVEL, + HOUSE_LOSE_AFTER_INACTIVITY, + HOUSE_OWNED_BY_ACCOUNT, + HOUSE_PRICE_PER_SQM, + HOUSE_PRICE_RENT_MULTIPLIER, + HOUSE_PURSHASED_SHOW_PRICE, + HOUSE_RENT_PERIOD, + HOUSE_RENT_RATE, + INVENTORY_GLOW, + IP, + KICK_AFTER_MINUTES, + LOCATION, + LOGIN_PORT, + LOGLEVEL, LOW_LEVEL_BONUS_EXP, + LOYALTY_BONUS_PERCENTAGE_MULTIPLIER, + LOYALTY_ENABLED, LOYALTY_POINTS_PER_CREATION_DAY, - LOYALTY_POINTS_PER_PREMIUM_DAY_SPENT, LOYALTY_POINTS_PER_PREMIUM_DAY_PURCHASED, - WHEEL_POINTS_PER_LEVEL, - MULTIPLIER_ATTACKONFIST, + LOYALTY_POINTS_PER_PREMIUM_DAY_SPENT, + MAINTAIN_MODE_MESSAGE, + MAP_AUTHOR, + MAP_CUSTOM_AUTHOR, + MAP_CUSTOM_NAME, + MAP_DOWNLOAD_URL, + MAP_NAME, + MARKET_OFFER_DURATION, + MARKET_PREMIUM, + MAX_ALLOWED_ON_A_DUMMY, + MAX_CONTAINER, + MAX_CONTAINER_ITEM, + MAX_MARKET_OFFERS_AT_A_TIME_PER_PLAYER, + MAX_MESSAGEBUFFER, + MAX_PACKETS_PER_SECOND, + MAX_PLAYERS, MAX_SPEED_ATTACKONFIST, - TIBIADROME_CONCOCTION_COOLDOWN, - TIBIADROME_CONCOCTION_DURATION, - T_CONST, + METRICS_ENABLE_OSTREAM, + METRICS_ENABLE_PROMETHEUS, + METRICS_OSTREAM_INTERVAL, + METRICS_PROMETHEUS_ADDRESS, + MONTH_KILLS_TO_RED, + MULTIPLIER_ATTACKONFIST, + MYSQL_DB, + MYSQL_HOST, + MYSQL_PASS, + MYSQL_SOCK, + MYSQL_USER, + M_CONST, + OLD_PROTOCOL, + ONE_PLAYER_ON_ACCOUNT, + ONLY_INVITED_CAN_MOVE_HOUSE_ITEMS, + ONLY_PREMIUM_ACCOUNT, + OPTIMIZE_DATABASE, + ORANGE_SKULL_DURATION, + OWNER_EMAIL, + OWNER_NAME, PARALLELISM, - BOSS_DEFAULT_TIME_TO_FIGHT_AGAIN, - BOSS_DEFAULT_TIME_TO_DEFEAT, - VIP_BONUS_EXP, - VIP_BONUS_LOOT, - VIP_BONUS_SKILL, - VIP_FAMILIAR_TIME_COOLDOWN_REDUCTION, - REWARD_CHEST_MAX_COLLECT_ITEMS, - DISCORD_WEBHOOK_DELAY_MS, + PARTY_AUTO_SHARE_EXPERIENCE, + PARTY_LIST_MAX_DISTANCE, + PARTY_SHARE_LOOT_BOOSTS, + PARTY_SHARE_LOOT_BOOSTS_DIMINISHING_FACTOR, + PREMIUM_DEPOT_LIMIT, + PREY_BONUS_REROLL_PRICE, + PREY_BONUS_TIME, + PREY_ENABLED, + PREY_FREE_REROLL_TIME, + PREY_FREE_THIRD_SLOT, + PREY_REROLL_PRICE_LEVEL, + PREY_SELECTION_LIST_PRICE, + PROTECTION_LEVEL, + PUSH_DELAY, + PUSH_DISTANCE_DELAY, + PUSH_WHEN_ATTACKING, PVP_MAX_LEVEL_DIFFERENCE, - BESTIARY_RATE_CHARM_SHOP_PRICE, + PVP_RATE_DAMAGE_REDUCTION_PER_LEVEL, + PVP_RATE_DAMAGE_TAKEN_PER_LEVEL, + PZ_LOCKED, + RATE_ATTACK_SPEED, + RATE_BOSS_ATTACK, + RATE_BOSS_DEFENSE, + RATE_BOSS_HEALTH, + RATE_EXERCISE_TRAINING_SPEED, + RATE_EXPERIENCE, RATE_HEALTH_REGEN, RATE_HEALTH_REGEN_SPEED, + RATE_KILLING_IN_THE_NAME_OF_POINTS, + RATE_LOOT, + RATE_MAGIC, RATE_MANA_REGEN, RATE_MANA_REGEN_SPEED, - RATE_SOUL_REGEN, - RATE_SOUL_REGEN_SPEED, - RATE_SPELL_COOLDOWN, - RATE_ATTACK_SPEED, - RATE_OFFLINE_TRAINING_SPEED, - RATE_EXERCISE_TRAINING_SPEED, - RATE_MONSTER_HEALTH, RATE_MONSTER_ATTACK, RATE_MONSTER_DEFENSE, - RATE_BOSS_HEALTH, - RATE_BOSS_ATTACK, - RATE_BOSS_DEFENSE, - RATE_NPC_HEALTH, + RATE_MONSTER_HEALTH, RATE_NPC_ATTACK, RATE_NPC_DEFENSE, - LOYALTY_BONUS_PERCENTAGE_MULTIPLIER, - PARTY_SHARE_LOOT_BOOSTS_DIMINISHING_FACTOR, - PVP_RATE_DAMAGE_TAKEN_PER_LEVEL, - PVP_RATE_DAMAGE_REDUCTION_PER_LEVEL, - HOUSE_PRICE_RENT_MULTIPLIER, - HOUSE_RENT_RATE, - LOGLEVEL, + RATE_NPC_HEALTH, + RATE_OFFLINE_TRAINING_SPEED, + RATE_SKILL, + RATE_SOUL_REGEN, + RATE_SOUL_REGEN_SPEED, + RATE_SPAWN, + RATE_SPELL_COOLDOWN, + RATE_USE_STAGES, + RED_SKULL_DURATION, + REFUND_BEGINNING_WEAPON_MANA, + REMOVE_BEGINNING_WEAPON_AMMO, + REMOVE_POTION_CHARGES, + REMOVE_RUNE_CHARGES, + REMOVE_WEAPON_AMMO, + REMOVE_WEAPON_CHARGES, + REPLACE_KICK_ON_LOGIN, + RESET_SESSIONS_ON_STARTUP, + REWARD_CHEST_COLLECT_ENABLED, + REWARD_CHEST_MAX_COLLECT_ITEMS, + SAVE_INTERVAL_TIME, + SAVE_INTERVAL_TYPE, + SCRIPTS_CONSOLE_LOGS, + SERVER_MOTD, + SERVER_NAME, + SORT_LOOT_BY_CHANCE, + SQL_PORT, + STAIRHOP_DELAY, + STAMINA_GREEN_DELAY, + STAMINA_ORANGE_DELAY, + STAMINA_PZ, + STAMINA_PZ_GAIN, + STAMINA_SYSTEM, + STAMINA_TRAINER, + STAMINA_TRAINER_DELAY, + STAMINA_TRAINER_GAIN, + STASH_ITEMS, + STASH_MOVING, + STATUSQUERY_TIMEOUT, + STATUS_PORT, + STORE_COIN_PACKET, + STORE_IMAGES_URL, + TASK_HUNTING_BONUS_REROLL_PRICE, + TASK_HUNTING_ENABLED, + TASK_HUNTING_FREE_REROLL_TIME, + TASK_HUNTING_FREE_THIRD_SLOT, + TASK_HUNTING_LIMIT_EXHAUST, + TASK_HUNTING_REROLL_PRICE_LEVEL, + TASK_HUNTING_SELECTION_LIST_PRICE, + TELEPORT_PLAYER_TO_VOCATION_ROOM, + TELEPORT_SUMMONS, + TIBIADROME_CONCOCTION_COOLDOWN, + TIBIADROME_CONCOCTION_DURATION, + TIBIADROME_CONCOCTION_TICK_TYPE, + TOGGLE_ATTACK_SPEED_ONFIST, + TOGGLE_DOWNLOAD_MAP, + TOGGLE_FREE_QUEST, + TOGGLE_GOLD_POUCH_ALLOW_ANYTHING, + TOGGLE_GOLD_POUCH_QUICKLOOT_ONLY, + TOGGLE_HAZARDSYSTEM, + TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART, + TOGGLE_IMBUEMENT_NON_AGGRESSIVE_FIGHT_ONLY, + TOGGLE_IMBUEMENT_SHRINE_STORAGE, + TOGGLE_MAINTAIN_MODE, + TOGGLE_MAP_CUSTOM, + TOGGLE_MOUNT_IN_PZ, + TOGGLE_RECEIVE_REWARD, + TOGGLE_SAVE_INTERVAL, + TOGGLE_SAVE_INTERVAL_CLEAN_MAP, + TOGGLE_SERVER_IS_RETRO, + TOGGLE_TRAVELS_FREE, + TOGGLE_WHEELSYSTEM, + T_CONST, + URL, + USE_ANY_DATAPACK_FOLDER, + VIP_AUTOLOOT_VIP_ONLY, + VIP_BONUS_EXP, + VIP_BONUS_LOOT, + VIP_BONUS_SKILL, + VIP_FAMILIAR_TIME_COOLDOWN_REDUCTION, + VIP_KEEP_HOUSE, + VIP_STAY_ONLINE, + VIP_SYSTEM_ENABLED, + WARN_UNSAFE_SCRIPTS, + WEATHER_RAIN, + WEATHER_THUNDER, + WEEK_KILLS_TO_RED, + WHEEL_POINTS_PER_LEVEL, + WHITE_SKULL_TIME, + WORLD_TYPE, + XP_DISPLAY_MODE, }; diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index f7603ba5143..d90c89f0e26 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -349,6 +349,12 @@ bool ConfigManager::load() { loadBoolConfig(L, TOGGLE_RECEIVE_REWARD, "toggleReceiveReward", false); + loadBoolConfig(L, METRICS_ENABLE_PROMETHEUS, "metricsEnablePrometheus", false); + loadStringConfig(L, METRICS_PROMETHEUS_ADDRESS, "metricsPrometheusAddress", "localhost:9464"); + + loadBoolConfig(L, METRICS_ENABLE_OSTREAM, "metricsEnableOstream", false); + loadIntConfig(L, METRICS_OSTREAM_INTERVAL, "metricsOstreamInterval", 1000); + loaded = true; lua_close(L); return true; diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp index 151e5458a85..a348f364078 100644 --- a/src/creatures/combat/combat.cpp +++ b/src/creatures/combat/combat.cpp @@ -14,11 +14,13 @@ #include "lua/creature/events.hpp" #include "creatures/players/wheel/player_wheel.hpp" #include "game/game.hpp" +#include "game/scheduling/dispatcher.hpp" #include "io/iobestiary.hpp" #include "creatures/monsters/monster.hpp" #include "creatures/monsters/monsters.hpp" #include "items/weapons/weapons.hpp" #include "map/spectators.hpp" +#include "lib/metrics/metrics.hpp" int32_t Combat::getLevelFormula(std::shared_ptr player, const std::shared_ptr wheelSpell, const CombatDamage &damage) const { if (!player) { @@ -584,7 +586,7 @@ void Combat::CombatHealthFunc(std::shared_ptr caster, std::shared_ptr< } damage.damageMultiplier += attackerPlayer->wheel()->getMajorStatConditional("Divine Empowerment", WheelMajor_t::DAMAGE); - g_logger().debug("Wheel Divine Empowerment damage multiplier {}", damage.damageMultiplier); + g_logger().trace("Wheel Divine Empowerment damage multiplier {}", damage.damageMultiplier); } if (g_game().combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) { @@ -919,6 +921,7 @@ void Combat::doChainEffect(const Position &origin, const Position &dest, uint8_t } bool Combat::doCombatChain(std::shared_ptr caster, std::shared_ptr target, bool aggressive) const { + metrics::method_latency measure(__METHOD_NAME__); if (!params.chainCallback) { return false; } @@ -1309,6 +1312,7 @@ void Combat::setRuneSpellName(const std::string &value) { } std::vector>> Combat::pickChainTargets(std::shared_ptr caster, const CombatParams ¶ms, uint8_t chainDistance, uint8_t maxTargets, bool backtracking, bool aggressive, std::shared_ptr initialTarget /* = nullptr */) { + metrics::method_latency measure(__METHOD_NAME__); if (!caster) { return {}; } @@ -2016,6 +2020,7 @@ void MagicField::onStepInField(const std::shared_ptr &creature) { } void Combat::applyExtensions(std::shared_ptr caster, std::shared_ptr target, CombatDamage &damage, const CombatParams ¶ms) { + metrics::method_latency measure(__METHOD_NAME__); if (damage.extension || !caster || damage.primary.type == COMBAT_HEALING) { return; } diff --git a/src/creatures/combat/spells.cpp b/src/creatures/combat/spells.cpp index 4ae9d69ae6b..d8c71c2a93c 100644 --- a/src/creatures/combat/spells.cpp +++ b/src/creatures/combat/spells.cpp @@ -609,14 +609,19 @@ void Spell::setWheelOfDestinyBoost(WheelSpellBoost_t boost, WheelSpellGrade_t gr void Spell::applyCooldownConditions(std::shared_ptr player) const { WheelSpellGrade_t spellGrade = player->wheel()->getSpellUpgrade(getName()); bool isUpgraded = getWheelOfDestinyUpgraded() && static_cast(spellGrade) > 0; - auto rate_cooldown = (int32_t)g_configManager().getFloat(RATE_SPELL_COOLDOWN, __FUNCTION__); + // Safety check to prevent division by zero + auto rateCooldown = g_configManager().getFloat(RATE_SPELL_COOLDOWN, __FUNCTION__); + if (std::abs(rateCooldown) < std::numeric_limits::epsilon()) { + rateCooldown = 0.1; // Safe minimum value + } + if (cooldown > 0) { int32_t spellCooldown = cooldown; if (isUpgraded) { spellCooldown -= getWheelOfDestinyBoost(WheelSpellBoost_t::COOLDOWN, spellGrade); } if (spellCooldown > 0) { - std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLCOOLDOWN, spellCooldown / rate_cooldown, 0, false, spellId); + std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLCOOLDOWN, spellCooldown / rateCooldown, 0, false, spellId); player->addCondition(condition); } } @@ -627,7 +632,7 @@ void Spell::applyCooldownConditions(std::shared_ptr player) const { spellGroupCooldown -= getWheelOfDestinyBoost(WheelSpellBoost_t::GROUP_COOLDOWN, spellGrade); } if (spellGroupCooldown > 0) { - std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, spellGroupCooldown / rate_cooldown, 0, false, group); + std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, spellGroupCooldown / rateCooldown, 0, false, group); player->addCondition(condition); } } @@ -638,7 +643,7 @@ void Spell::applyCooldownConditions(std::shared_ptr player) const { spellSecondaryGroupCooldown -= getWheelOfDestinyBoost(WheelSpellBoost_t::SECONDARY_GROUP_COOLDOWN, spellGrade); } if (spellSecondaryGroupCooldown > 0) { - std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, spellSecondaryGroupCooldown / rate_cooldown, 0, false, secondaryGroup); + std::shared_ptr condition = Condition::createCondition(CONDITIONID_DEFAULT, CONDITION_SPELLGROUPCOOLDOWN, spellSecondaryGroupCooldown / rateCooldown, 0, false, secondaryGroup); player->addCondition(condition); } } diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index e9b674c54f5..4720b903233 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -16,6 +16,7 @@ #include "creatures/monsters/monster.hpp" #include "game/zones/zone.hpp" #include "map/spectators.hpp" +#include "lib/metrics/metrics.hpp" Creature::Creature() { onIdleStatus(); @@ -29,6 +30,7 @@ Creature::~Creature() { } bool Creature::canSee(const Position &myPos, const Position &pos, int32_t viewRangeX, int32_t viewRangeY) { + metrics::method_latency measure(__METHOD_NAME__); if (myPos.z <= MAP_INIT_SURFACE_LAYER) { // we are on ground level or above (7 -> 0) // view is from 7 -> 0 @@ -88,6 +90,7 @@ int32_t Creature::getWalkSize() { } void Creature::onThink(uint32_t interval) { + metrics::method_latency measure(__METHOD_NAME__); if (!isMapLoaded && useCacheMap()) { isMapLoaded = true; updateMapCache(); @@ -158,6 +161,7 @@ void Creature::onIdleStatus() { } void Creature::onCreatureWalk() { + metrics::method_latency measure(__METHOD_NAME__); if (getWalkDelay() <= 0) { Direction dir; uint32_t flags = FLAG_IGNOREFIELDDAMAGE; @@ -268,6 +272,7 @@ void Creature::stopEventWalk() { } void Creature::updateMapCache() { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr newTile; const Position &myPos = getPosition(); Position pos(0, 0, myPos.z); @@ -283,6 +288,7 @@ void Creature::updateMapCache() { } void Creature::updateTileCache(std::shared_ptr newTile, int32_t dx, int32_t dy) { + metrics::method_latency measure(__METHOD_NAME__); if (std::abs(dx) <= maxWalkCacheWidth && std::abs(dy) <= maxWalkCacheHeight) { localMapCache[maxWalkCacheHeight + dy][maxWalkCacheWidth + dx] = newTile && newTile->queryAdd(0, getCreature(), 1, FLAG_PATHFINDING | FLAG_IGNOREFIELDDAMAGE) == RETURNVALUE_NOERROR; } @@ -298,6 +304,7 @@ void Creature::updateTileCache(std::shared_ptr upTile, const Position &pos } int32_t Creature::getWalkCache(const Position &pos) { + metrics::method_latency measure(__METHOD_NAME__); if (!useCacheMap()) { return 2; } @@ -354,6 +361,7 @@ void Creature::onRemoveTileItem(std::shared_ptr updateTile, const Position } void Creature::onCreatureAppear(std::shared_ptr creature, bool isLogin) { + metrics::method_latency measure(__METHOD_NAME__); if (creature == getCreature()) { if (useCacheMap()) { isMapLoaded = true; @@ -371,6 +379,7 @@ void Creature::onCreatureAppear(std::shared_ptr creature, bool isLogin } void Creature::onRemoveCreature(std::shared_ptr creature, bool) { + metrics::method_latency measure(__METHOD_NAME__); onCreatureDisappear(creature, true); if (creature != getCreature() && isMapLoaded) { if (creature->getPosition().z == getPosition().z) { @@ -386,6 +395,7 @@ void Creature::onRemoveCreature(std::shared_ptr creature, bool) { } void Creature::onCreatureDisappear(std::shared_ptr creature, bool isLogout) { + metrics::method_latency measure(__METHOD_NAME__); if (getAttackedCreature() == creature) { setAttackedCreature(nullptr); onAttackedCreatureDisappear(isLogout); @@ -398,6 +408,7 @@ void Creature::onCreatureDisappear(std::shared_ptr creature, bool isLo } void Creature::onChangeZone(ZoneType_t zone) { + metrics::method_latency measure(__METHOD_NAME__); auto attackedCreature = getAttackedCreature(); if (attackedCreature && zone == ZONE_PROTECTION) { onCreatureDisappear(attackedCreature, false); @@ -405,6 +416,7 @@ void Creature::onChangeZone(ZoneType_t zone) { } void Creature::onAttackedCreatureChangeZone(ZoneType_t zone) { + metrics::method_latency measure(__METHOD_NAME__); if (zone == ZONE_PROTECTION) { auto attackedCreature = getAttackedCreature(); if (attackedCreature) { @@ -414,6 +426,7 @@ void Creature::onAttackedCreatureChangeZone(ZoneType_t zone) { } void Creature::checkSummonMove(const Position &newPos, bool teleportSummon) { + metrics::method_latency measure(__METHOD_NAME__); if (hasSummons()) { std::vector> despawnMonsterList; for (const auto &summon : getSummons()) { @@ -456,6 +469,7 @@ void Creature::checkSummonMove(const Position &newPos, bool teleportSummon) { } void Creature::onCreatureMove(const std::shared_ptr &creature, const std::shared_ptr &newTile, const Position &newPos, const std::shared_ptr &oldTile, const Position &oldPos, bool teleport) { + metrics::method_latency measure(__METHOD_NAME__); if (creature == getCreature()) { lastStep = OTSYS_TIME(); lastStepCost = 1; @@ -612,6 +626,7 @@ void Creature::onCreatureMove(const std::shared_ptr &creature, const s } void Creature::onDeath() { + metrics::method_latency measure(__METHOD_NAME__); bool lastHitUnjustified = false; bool mostDamageUnjustified = false; std::shared_ptr lastHitCreature = g_game().getCreatureByID(lastHitCreatureId); @@ -687,13 +702,47 @@ void Creature::onDeath() { /** * @deprecated -- This is here to trigger the deprecated onKill events in lua */ + auto mostDamageCreatureMaster = mostDamageCreature ? mostDamageCreature->getMaster() : nullptr; if (mostDamageCreature && (mostDamageCreature != lastHitCreature || getMonster()) && mostDamageCreature != lastHitCreatureMaster) { - auto mostDamageCreatureMaster = mostDamageCreature->getMaster(); if (lastHitCreature != mostDamageCreatureMaster && (lastHitCreatureMaster == nullptr || mostDamageCreatureMaster != lastHitCreatureMaster)) { mostDamageUnjustified = mostDamageCreature->deprecatedOnKilledCreature(getCreature(), false); } } + bool killedByPlayer = mostDamageCreature && mostDamageCreature->getPlayer() || mostDamageCreatureMaster && mostDamageCreatureMaster->getPlayer(); + if (getPlayer()) { + g_metrics().addCounter( + "player_death", + 1, + { + { "name", getNameDescription() }, + { "level", std::to_string(getPlayer()->getLevel()) }, + { "most_damage_creature", mostDamageCreature ? mostDamageCreature->getName() : "(none)" }, + { "last_hit_creature", lastHitCreature ? lastHitCreature->getName() : "(none)" }, + { "most_damage_dealt", std::to_string(mostDamage) }, + { "most_damage_creature_master", mostDamageCreatureMaster ? mostDamageCreatureMaster->getName() : "(none)" }, + { "most_damage_unjustified", std::to_string(mostDamageUnjustified) }, + { "last_hit_unjustified", std::to_string(lastHitUnjustified) }, + { "by_player", std::to_string(killedByPlayer) }, + } + ); + } else { + std::string killerName = mostDamageCreature ? mostDamageCreature->getName() : "(none)"; + if (mostDamageCreatureMaster) { + killerName = mostDamageCreatureMaster->getName(); + } + g_metrics().addCounter( + "monster_death", + 1, + { + { "name", getName() }, + { "killer", killerName }, + { "is_summon", std::to_string(getMaster() ? true : false) }, + { "by_player", std::to_string(killedByPlayer) }, + } + ); + } + bool droppedCorpse = dropCorpse(lastHitCreature, mostDamageCreature, lastHitUnjustified, mostDamageUnjustified); death(lastHitCreature); @@ -707,6 +756,7 @@ void Creature::onDeath() { } bool Creature::dropCorpse(std::shared_ptr lastHitCreature, std::shared_ptr mostDamageCreature, bool lastHitUnjustified, bool mostDamageUnjustified) { + metrics::method_latency measure(__METHOD_NAME__); if (!lootDrop && getMonster()) { if (getMaster()) { // Scripting event onDeath @@ -974,6 +1024,7 @@ void Creature::getPathSearchParams(const std::shared_ptr &, FindPathPa } void Creature::goToFollowCreature_async(std::function &&onComplete) { + metrics::method_latency measure(__METHOD_NAME__); if (pathfinderRunning.load()) { return; } @@ -990,6 +1041,7 @@ void Creature::goToFollowCreature_async(std::function &&onComplete) { } void Creature::goToFollowCreature() { + metrics::method_latency measure(__METHOD_NAME__); const auto &followCreature = getFollowCreature(); if (!followCreature) { return; @@ -1043,6 +1095,7 @@ bool Creature::canFollowMaster() { } bool Creature::setFollowCreature(std::shared_ptr creature) { + metrics::method_latency measure(__METHOD_NAME__); if (creature) { if (getFollowCreature() == creature) { return true; @@ -1187,6 +1240,7 @@ void Creature::onAttackedCreatureDrainHealth(std::shared_ptr target, i } void Creature::onAttackedCreatureKilled(std::shared_ptr target) { + metrics::method_latency measure(__METHOD_NAME__); if (target != getCreature()) { uint64_t gainExp = target->getGainedExperience(static_self_cast()); onGainExperience(gainExp, target); @@ -1194,6 +1248,7 @@ void Creature::onAttackedCreatureKilled(std::shared_ptr target) { } bool Creature::deprecatedOnKilledCreature(std::shared_ptr target, bool lastHit) { + metrics::method_latency measure(__METHOD_NAME__); auto master = getMaster(); if (master) { master->deprecatedOnKilledCreature(target, lastHit); @@ -1208,6 +1263,7 @@ bool Creature::deprecatedOnKilledCreature(std::shared_ptr target, bool } void Creature::onGainExperience(uint64_t gainExp, std::shared_ptr target) { + metrics::method_latency measure(__METHOD_NAME__); auto master = getMaster(); if (gainExp == 0 || !master) { return; @@ -1238,6 +1294,7 @@ void Creature::onGainExperience(uint64_t gainExp, std::shared_ptr targ } bool Creature::setMaster(std::shared_ptr newMaster, bool reloadCreature /* = false*/) { + metrics::method_latency measure(__METHOD_NAME__); // Persists if this creature has ever been a summon this->summoned = true; auto oldMaster = getMaster(); @@ -1270,6 +1327,7 @@ bool Creature::setMaster(std::shared_ptr newMaster, bool reloadCreatur } bool Creature::addCondition(std::shared_ptr condition) { + metrics::method_latency measure(__METHOD_NAME__); if (condition == nullptr) { return false; } @@ -1302,6 +1360,7 @@ bool Creature::addCombatCondition(std::shared_ptr condition) { } void Creature::removeCondition(ConditionType_t type) { + metrics::method_latency measure(__METHOD_NAME__); auto it = conditions.begin(), end = conditions.end(); while (it != end) { std::shared_ptr condition = *it; @@ -1319,6 +1378,7 @@ void Creature::removeCondition(ConditionType_t type) { } void Creature::removeCondition(ConditionType_t conditionType, ConditionId_t conditionId, bool force /* = false*/) { + metrics::method_latency measure(__METHOD_NAME__); auto it = conditions.begin(), end = conditions.end(); while (it != end) { std::shared_ptr condition = *it; @@ -1382,6 +1442,7 @@ std::shared_ptr Creature::getCondition(ConditionType_t type) const { } std::shared_ptr Creature::getCondition(ConditionType_t type, ConditionId_t conditionId, uint32_t subId /* = 0*/) const { + metrics::method_latency measure(__METHOD_NAME__); for (const auto &condition : conditions) { if (condition->getType() == type && condition->getId() == conditionId && condition->getSubId() == subId) { return condition; @@ -1401,6 +1462,7 @@ std::vector> Creature::getConditionsByType(ConditionT } void Creature::executeConditions(uint32_t interval) { + metrics::method_latency measure(__METHOD_NAME__); auto it = conditions.begin(), end = conditions.end(); while (it != end) { std::shared_ptr condition = *it; @@ -1419,6 +1481,7 @@ void Creature::executeConditions(uint32_t interval) { } bool Creature::hasCondition(ConditionType_t type, uint32_t subId /* = 0*/) const { + metrics::method_latency measure(__METHOD_NAME__); if (isSuppress(type)) { return false; } @@ -1652,6 +1715,7 @@ bool Creature::isInvisible() const { } bool Creature::getPathTo(const Position &targetPos, stdext::arraylist &dirList, const FindPathParams &fpp) { + metrics::method_latency measure(__METHOD_NAME__); return g_game().map.getPathMatching(getCreature(), dirList, FrozenPathingConditionCall(targetPos), fpp); } diff --git a/src/creatures/npcs/npc.cpp b/src/creatures/npcs/npc.cpp index 3991d01c1c1..5602f145807 100644 --- a/src/creatures/npcs/npc.cpp +++ b/src/creatures/npcs/npc.cpp @@ -16,6 +16,7 @@ #include "lua/callbacks/creaturecallback.hpp" #include "game/scheduling/dispatcher.hpp" #include "map/spectators.hpp" +#include "lib/metrics/metrics.hpp" int32_t Npc::despawnRange; int32_t Npc::despawnRadius; @@ -279,6 +280,7 @@ void Npc::onPlayerBuyItem(std::shared_ptr player, uint16_t itemId, uint8 if (getCurrency() == ITEM_GOLD_COIN && (player->getMoney() + player->getBankBalance()) < totalCost) { g_logger().error("[Npc::onPlayerBuyItem (getMoney)] - Player {} have a problem for buy item {} on shop for npc {}", player->getName(), itemId, getName()); g_logger().debug("[Information] Player {} tried to buy item {} on shop for npc {}, at position {}", player->getName(), itemId, getName(), player->getPosition().toString()); + g_metrics().addCounter("balance_decrease", totalCost, { { "player", player->getName() }, { "context", "npc_purchase" } }); return; } else if (getCurrency() != ITEM_GOLD_COIN && (player->getItemTypeCount(getCurrency()) < totalCost || ((player->getMoney() + player->getBankBalance()) < bagsCost))) { g_logger().error("[Npc::onPlayerBuyItem (getItemTypeCount)] - Player {} have a problem for buy item {} on shop for npc {}", player->getName(), itemId, getName()); @@ -416,6 +418,7 @@ void Npc::onPlayerSellItem(std::shared_ptr player, uint16_t itemId, uint } else { g_game().addMoney(player, totalCost); } + g_metrics().addCounter("balance_increase", totalCost, { { "player", player->getName() }, { "context", "npc_sale" } }); } else { std::shared_ptr newItem = Item::CreateItem(getCurrency(), totalCost); if (newItem) { diff --git a/src/creatures/npcs/npcs.cpp b/src/creatures/npcs/npcs.cpp index 0af1b090201..50fff3aa75b 100644 --- a/src/creatures/npcs/npcs.cpp +++ b/src/creatures/npcs/npcs.cpp @@ -97,10 +97,8 @@ void NpcType::loadShop(const std::shared_ptr &npcType, ShopBlock shopBl shopBlock.childShop.push_back(child); } } - npcType->info.shopItemVector.push_back(shopBlock); - } else { - npcType->info.shopItemVector.push_back(shopBlock); } + npcType->info.shopItemVector.push_back(shopBlock); info.speechBubble = SPEECHBUBBLE_TRADE; } diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 1fe7a0b1ca2..45073f9d878 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -31,6 +31,7 @@ #include "items/weapons/weapons.hpp" #include "core.hpp" #include "map/spectators.hpp" +#include "lib/metrics/metrics.hpp" MuteCountMap Player::muteCountMap; @@ -1107,6 +1108,23 @@ void Player::checkLootContainers(std::shared_ptr item) { } void Player::sendLootStats(std::shared_ptr item, uint8_t count) { + uint64_t value = 0; + if (item->getID() == ITEM_GOLD_COIN || item->getID() == ITEM_PLATINUM_COIN || item->getID() == ITEM_CRYSTAL_COIN) { + if (item->getID() == ITEM_PLATINUM_COIN) { + value = count * 100; + } else if (item->getID() == ITEM_CRYSTAL_COIN) { + value = count * 10000; + } else { + value = count; + } + } else if ( + auto npc = g_game().getNpcByName("The Lootmonger") + ) { + const auto &iType = Item::items.getItemType(item->getID()); + value = iType.sellPrice * count; + } + g_metrics().addCounter("player_loot", value, { { "player", getName() } }); + if (client) { client->sendLootStats(item, count); } @@ -1256,6 +1274,10 @@ void Player::sendStats() { } void Player::updateSupplyTracker(std::shared_ptr item) { + const auto &iType = Item::items.getItemType(item->getID()); + auto value = iType.buyPrice; + g_metrics().addCounter("player_supply", value, { { "player", getName() } }); + if (client) { client->sendUpdateSupplyTracker(item); } @@ -1382,6 +1404,8 @@ void Player::onApplyImbuement(Imbuement* imbuement, std::shared_ptr item, return; } + g_metrics().addCounter("balance_decrease", price, { { "player", getName() }, { "context", "apply_imbuement" } }); + for (auto &[key, value] : items) { std::stringstream withdrawItemMessage; @@ -1444,6 +1468,7 @@ void Player::onClearImbuement(std::shared_ptr item, uint8_t slot) { this->openImbuementWindow(item); return; } + g_metrics().addCounter("balance_decrease", baseImbuement->removeCost, { { "player", getName() }, { "context", "clear_imbuement" } }); if (item->getParent() == getPlayer()) { removeItemImbuementStats(imbuementInfo.imbuement); @@ -2236,6 +2261,16 @@ void Player::addExperience(std::shared_ptr target, uint64_t exp, bool return; } + auto rate = exp / rawExp; + std::map attrs({ { "player", getName() }, { "level", std::to_string(getLevel()) }, { "rate", std::to_string(rate) } }); + if (sendText) { + g_metrics().addCounter("player_experience_raw", rawExp, attrs); + g_metrics().addCounter("player_experience_actual", exp, attrs); + } else { + g_metrics().addCounter("player_experience_bonus_raw", rawExp, attrs); + g_metrics().addCounter("player_experience_bonus_actual", exp, attrs); + } + // Hazard system experience std::shared_ptr monster = target && target->getMonster() ? target->getMonster() : nullptr; bool handleHazardExperience = monster && monster->getHazard() && getHazardSystemPoints() > 0; @@ -6961,6 +6996,7 @@ void Player::forgeFuseItems(uint16_t itemId, uint8_t tier, bool success, bool re sendForgeError(RETURNVALUE_CONTACTADMINISTRATOR); return; } + g_metrics().addCounter("balance_decrease", cost, { { "player", getName() }, { "context", "forge_fuse" } }); history.cost = cost; } @@ -7034,6 +7070,7 @@ void Player::forgeFuseItems(uint16_t itemId, uint8_t tier, bool success, bool re sendForgeError(RETURNVALUE_CONTACTADMINISTRATOR); return; } + g_metrics().addCounter("balance_decrease", cost, { { "player", getName() }, { "context", "forge_fuse" } }); history.cost = cost; } @@ -7165,6 +7202,7 @@ void Player::forgeTransferItemTier(uint16_t donorItemId, uint8_t tier, uint16_t return; } history.cost = cost; + g_metrics().addCounter("balance_decrease", cost, { { "player", getName() }, { "context", "forge_transfer" } }); returnValue = g_game().internalAddItem(static_self_cast(), exaltationContainer, INDEX_WHEREEVER); if (returnValue != RETURNVALUE_NOERROR) { diff --git a/src/database/database.cpp b/src/database/database.cpp index a097c9f9b03..916ffd1ee44 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -12,6 +12,7 @@ #include "config/configmanager.hpp" #include "database/database.hpp" #include "lib/di/container.hpp" +#include "lib/metrics/metrics.hpp" Database::~Database() { if (handle != nullptr) { @@ -60,7 +61,10 @@ bool Database::beginTransaction() { if (!executeQuery("BEGIN")) { return false; } + metrics::lock_latency measureLock("database"); databaseLock.lock(); + measureLock.stop(); + return true; } @@ -121,11 +125,14 @@ bool Database::executeQuery(const std::string_view &query) { g_logger().trace("Executing Query: {}", query); + metrics::lock_latency measureLock("database"); std::scoped_lock lock { databaseLock }; + measureLock.stop(); + metrics::query_latency measure(query.substr(0, 50)); bool success = retryQuery(query, 10); - mysql_free_result(mysql_store_result(handle)); + return success; } @@ -136,8 +143,11 @@ DBResult_ptr Database::storeQuery(const std::string_view &query) { } g_logger().trace("Storing Query: {}", query); + metrics::lock_latency measureLock("database"); std::scoped_lock lock { databaseLock }; + measureLock.stop(); + metrics::query_latency measure(query.substr(0, 50)); retry: if (mysql_query(handle, query.data()) != 0) { g_logger().error("Query: {}", query); diff --git a/src/game/bank/bank.cpp b/src/game/bank/bank.cpp index f1a700388e0..713ee645733 100644 --- a/src/game/bank/bank.cpp +++ b/src/game/bank/bank.cpp @@ -14,6 +14,7 @@ #include "creatures/players/player.hpp" #include "io/iologindata.hpp" #include "game/scheduling/save_manager.hpp" +#include "lib/metrics/metrics.hpp" Bank::Bank(const std::shared_ptr bankable) : m_bankable(bankable) { @@ -110,7 +111,12 @@ bool Bank::transferTo(const std::shared_ptr destination, uint64_t amount) } } - return debit(amount) && destination->credit(amount); + if (!(debit(amount) && destination->credit(amount))) { + return false; + } + g_metrics().addCounter("balance_increase", amount, { { "player", destination->getBankable()->getPlayer()->getName() }, { "context", "bank_transfer" } }); + g_metrics().addCounter("balance_decrease", amount, { { "player", getBankable()->getPlayer()->getName() }, { "context", "bank_transfer" } }); + return true; } bool Bank::withdraw(std::shared_ptr player, uint64_t amount) { @@ -118,6 +124,7 @@ bool Bank::withdraw(std::shared_ptr player, uint64_t amount) { return false; } g_game().addMoney(player, amount); + g_metrics().addCounter("balance_decrease", amount, { { "player", player->getName() }, { "context", "bank_withdraw" } }); return true; } @@ -144,5 +151,6 @@ bool Bank::deposit(const std::shared_ptr destination, uint64_t amount) { if (!g_game().removeMoney(bankable->getPlayer(), amount)) { return false; } + g_metrics().addCounter("balance_increase", amount, { { "player", bankable->getPlayer()->getName() }, { "context", "bank_deposit" } }); return destination->credit(amount); } diff --git a/src/game/functions/game_reload.cpp b/src/game/functions/game_reload.cpp index 873cd60ecb9..f40e9cf9047 100644 --- a/src/game/functions/game_reload.cpp +++ b/src/game/functions/game_reload.cpp @@ -72,6 +72,7 @@ bool GameReload::reloadAll() const { reloadResults.reserve(magic_enum::enum_count()); for (auto value : magic_enum::enum_values()) { + g_logger().info("Reloading: {}", magic_enum::enum_name(value)); if (value == Reload_t::RELOAD_TYPE_ALL) { continue; } diff --git a/src/game/game.cpp b/src/game/game.cpp index 5dcba79adc1..bc758180bf6 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -691,13 +691,13 @@ std::shared_ptr Game::getNpcByID(uint32_t id) { return it->second; } -std::shared_ptr Game::getPlayerByID(uint32_t id, bool loadTmp /* = false */) { +std::shared_ptr Game::getPlayerByID(uint32_t id, bool allowOffline /* = false */) { auto playerMap = players.find(id); if (playerMap != players.end()) { return playerMap->second; } - if (!loadTmp) { + if (!allowOffline) { return nullptr; } std::shared_ptr tmpPlayer = std::make_shared(nullptr); @@ -748,14 +748,14 @@ std::shared_ptr Game::getNpcByName(const std::string &s) { return nullptr; } -std::shared_ptr Game::getPlayerByName(const std::string &s, bool loadTmp /* = false */) { +std::shared_ptr Game::getPlayerByName(const std::string &s, bool allowOffline /* = false */) { if (s.empty()) { return nullptr; } auto it = mappedPlayerNames.find(asLowerCaseString(s)); if (it == mappedPlayerNames.end() || it->second.expired()) { - if (!loadTmp) { + if (!allowOffline) { return nullptr; } std::shared_ptr tmpPlayer = std::make_shared(nullptr); @@ -769,7 +769,7 @@ std::shared_ptr Game::getPlayerByName(const std::string &s, bool loadTmp return it->second.lock(); } -std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid, bool loadTmp /* = false */) { +std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid, bool allowOffline /* = false */) { if (guid == 0) { return nullptr; } @@ -778,7 +778,7 @@ std::shared_ptr Game::getPlayerByGUID(const uint32_t &guid, bool loadTmp return it.second; } } - if (!loadTmp) { + if (!allowOffline) { return nullptr; } std::shared_ptr tmpPlayer = std::make_shared(nullptr); @@ -869,6 +869,7 @@ bool Game::internalPlaceCreature(std::shared_ptr creature, const Posit } bool Game::placeCreature(std::shared_ptr creature, const Position &pos, bool extendedPos /*=false*/, bool forced /*= false*/) { + metrics::method_latency measure(__METHOD_NAME__); if (!internalPlaceCreature(creature, pos, extendedPos, forced)) { return false; } @@ -895,6 +896,7 @@ bool Game::placeCreature(std::shared_ptr creature, const Position &pos } bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = true*/) { + metrics::method_latency measure(__METHOD_NAME__); if (!creature || creature->isRemoved()) { return false; } @@ -962,6 +964,7 @@ bool Game::removeCreature(std::shared_ptr creature, bool isLogout /* = } void Game::executeDeath(uint32_t creatureId) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr creature = getCreatureByID(creatureId); if (creature && !creature->isRemoved()) { afterCreatureZoneChange(creature, creature->getZones(), {}); @@ -970,6 +973,7 @@ void Game::executeDeath(uint32_t creatureId) { } void Game::playerTeleport(uint32_t playerId, const Position &newPosition) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player || !player->hasFlag(PlayerFlags_t::CanMapClickTeleport)) { return; @@ -982,6 +986,7 @@ void Game::playerTeleport(uint32_t playerId, const Position &newPosition) { } void Game::playerInspectItem(std::shared_ptr player, const Position &pos) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr thing = internalGetThing(player, pos, 0, 0, STACKPOS_TOPDOWN_ITEM); if (!thing) { player->sendCancelMessage(RETURNVALUE_NOTPOSSIBLE); @@ -998,6 +1003,7 @@ void Game::playerInspectItem(std::shared_ptr player, const Position &pos } void Game::playerInspectItem(std::shared_ptr player, uint16_t itemId, uint8_t itemCount, bool cyclopedia) { + metrics::method_latency measure(__METHOD_NAME__); player->sendItemInspection(itemId, itemCount, nullptr, cyclopedia); } @@ -1051,6 +1057,7 @@ FILELOADER_ERRORS Game::loadAppearanceProtobuf(const std::string &file) { } void Game::playerMoveThing(uint32_t playerId, const Position &fromPos, uint16_t itemId, uint8_t fromStackPos, const Position &toPos, uint8_t count) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player) { return; @@ -1133,6 +1140,7 @@ void Game::playerMoveCreatureByID(uint32_t playerId, uint32_t movingCreatureId, } void Game::playerMoveCreature(std::shared_ptr player, std::shared_ptr movingCreature, const Position &movingCreatureOrigPos, std::shared_ptr toTile) { + metrics::method_latency measure(__METHOD_NAME__); if (!player->canDoAction()) { uint32_t delay = 600; std::shared_ptr task = createPlayerTask(delay, std::bind(&Game::playerMoveCreatureByID, this, player->getID(), movingCreature->getID(), movingCreatureOrigPos, toTile->getPosition()), "Game::playerMoveCreatureByID"); @@ -1272,6 +1280,7 @@ ReturnValue Game::internalMoveCreature(std::shared_ptr creature, Direc } ReturnValue Game::internalMoveCreature(const std::shared_ptr &creature, const std::shared_ptr &toTile, uint32_t flags /*= 0*/) { + metrics::method_latency measure(__METHOD_NAME__); if (creature->hasCondition(CONDITION_ROOTED)) { return RETURNVALUE_NOTPOSSIBLE; } @@ -1668,6 +1677,7 @@ ReturnValue Game::checkMoveItemToCylinder(std::shared_ptr player, std::s } ReturnValue Game::internalMoveItem(std::shared_ptr fromCylinder, std::shared_ptr toCylinder, int32_t index, std::shared_ptr item, uint32_t count, std::shared_ptr* movedItem, uint32_t flags /*= 0*/, std::shared_ptr actor /*=nullptr*/, std::shared_ptr tradeItem /* = nullptr*/, bool checkTile /* = true*/) { + metrics::method_latency measure(__METHOD_NAME__); if (fromCylinder == nullptr) { g_logger().error("[{}] fromCylinder is nullptr", __FUNCTION__); return RETURNVALUE_NOTPOSSIBLE; @@ -1918,6 +1928,7 @@ ReturnValue Game::internalAddItem(std::shared_ptr toCylinder, std::sha } ReturnValue Game::internalAddItem(std::shared_ptr toCylinder, std::shared_ptr item, int32_t index, uint32_t flags, bool test, uint32_t &remainderCount) { + metrics::method_latency measure(__METHOD_NAME__); if (toCylinder == nullptr) { g_logger().error("[{}] fromCylinder is nullptr", __FUNCTION__); return RETURNVALUE_NOTPOSSIBLE; @@ -2004,6 +2015,7 @@ ReturnValue Game::internalAddItem(std::shared_ptr toCylinder, std::sha } ReturnValue Game::internalRemoveItem(std::shared_ptr item, int32_t count /*= -1*/, bool test /*= false*/, uint32_t flags /*= 0*/, bool force /*= false*/) { + metrics::method_latency measure(__METHOD_NAME__); if (item == nullptr) { g_logger().debug("{} - Item is nullptr", __FUNCTION__); return RETURNVALUE_NOTPOSSIBLE; @@ -2063,6 +2075,7 @@ ReturnValue Game::internalRemoveItem(std::shared_ptr item, int32_t count / } std::tuple Game::addItemBatch(const std::shared_ptr &toCylinder, const std::vector> &items, uint32_t flags /* = 0 */, bool dropOnMap /* = true */, uint32_t autoContainerId /* = 0 */) { + metrics::method_latency measure(__METHOD_NAME__); const auto player = toCylinder->getPlayer(); bool dropping = false; ReturnValue ret = RETURNVALUE_NOTPOSSIBLE; @@ -2130,6 +2143,7 @@ std::tuple Game::addItemBatch(const std::shared } std::tuple Game::createItemBatch(const std::shared_ptr &toCylinder, const std::vector> &itemCounts, uint32_t flags /* = 0 */, bool dropOnMap /* = true */, uint32_t autoContainerId /* = 0 */) { + metrics::method_latency measure(__METHOD_NAME__); std::vector> items; for (const auto &[itemId, count, subType] : itemCounts) { const auto &itemType = Item::items[itemId]; @@ -2163,6 +2177,7 @@ std::tuple Game::createItem(const std::shared_p } ReturnValue Game::internalPlayerAddItem(std::shared_ptr player, std::shared_ptr item, bool dropOnMap /*= true*/, Slots_t slot /*= CONST_SLOT_WHEREEVER*/) { + metrics::method_latency measure(__METHOD_NAME__); uint32_t remainderCount = 0; ReturnValue ret = internalAddItem(player, item, static_cast(slot), 0, false, remainderCount); if (remainderCount != 0) { @@ -2185,6 +2200,7 @@ ReturnValue Game::internalPlayerAddItem(std::shared_ptr player, std::sha } std::shared_ptr Game::findItemOfType(std::shared_ptr cylinder, uint16_t itemId, bool depthSearch /*= true*/, int32_t subType /*= -1*/) const { + metrics::method_latency measure(__METHOD_NAME__); if (cylinder == nullptr) { g_logger().error("[{}] Cylinder is nullptr", __FUNCTION__); return nullptr; @@ -2360,6 +2376,7 @@ void Game::addMoney(std::shared_ptr cylinder, uint64_t money, uint32_t } std::shared_ptr Game::transformItem(std::shared_ptr item, uint16_t newId, int32_t newCount /*= -1*/) { + metrics::method_latency measure(__METHOD_NAME__); if (item->getID() == newId && (newCount == -1 || (newCount == item->getSubType() && newCount != 0))) { // chargeless item placed on map = infinite return item; } @@ -2490,7 +2507,8 @@ std::shared_ptr Game::transformItem(std::shared_ptr item, uint16_t n return newItem; } -ReturnValue Game::internalTeleport(std::shared_ptr thing, const Position &newPos, bool pushMove /* = true*/, uint32_t flags /*= 0*/) { +ReturnValue Game::internalTeleport(const std::shared_ptr &thing, const Position &newPos, bool pushMove /* = true*/, uint32_t flags /*= 0*/) { + metrics::method_latency measure(__METHOD_NAME__); if (thing == nullptr) { g_logger().error("[{}] thing is nullptr", __FUNCTION__); return RETURNVALUE_NOTPOSSIBLE; @@ -2781,6 +2799,7 @@ ReturnValue Game::internalCollectLootItems(std::shared_ptr player, std:: return RETURNVALUE_NOTPOSSIBLE; } player->setBankBalance(player->getBankBalance() + money); + g_metrics().addCounter("balance_increase", money, { { "player", player->getName() }, { "context", "loot" } }); return RETURNVALUE_NOERROR; } } @@ -3239,6 +3258,7 @@ void Game::playerStopAutoWalk(uint32_t playerId) { } void Game::playerUseItemEx(uint32_t playerId, const Position &fromPos, uint8_t fromStackPos, uint16_t fromItemId, const Position &toPos, uint8_t toStackPos, uint16_t toItemId) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player) { return; @@ -3368,6 +3388,7 @@ void Game::playerUseItemEx(uint32_t playerId, const Position &fromPos, uint8_t f } void Game::playerUseItem(uint32_t playerId, const Position &pos, uint8_t stackPos, uint8_t index, uint16_t itemId) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player) { return; @@ -3462,6 +3483,7 @@ void Game::playerUseItem(uint32_t playerId, const Position &pos, uint8_t stackPo } void Game::playerUseWithCreature(uint32_t playerId, const Position &fromPos, uint8_t fromStackPos, uint32_t creatureId, uint16_t itemId) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player) { return; @@ -4671,6 +4693,7 @@ void Game::internalCloseTrade(std::shared_ptr player) { } void Game::playerBuyItem(uint32_t playerId, uint16_t itemId, uint8_t count, uint16_t amount, bool ignoreCap /* = false*/, bool inBackpacks /* = false*/) { + metrics::method_latency measure(__METHOD_NAME__); if (amount == 0) { return; } @@ -4709,6 +4732,7 @@ void Game::playerBuyItem(uint32_t playerId, uint16_t itemId, uint8_t count, uint } void Game::playerSellItem(uint32_t playerId, uint16_t itemId, uint8_t count, uint16_t amount, bool ignoreEquipped) { + metrics::method_latency measure(__METHOD_NAME__); if (amount == 0) { return; } @@ -5786,12 +5810,14 @@ void Game::addCreatureCheck(const std::shared_ptr &creature) { } void Game::removeCreatureCheck(const std::shared_ptr &creature) { + metrics::method_latency measure(__METHOD_NAME__); if (creature->inCheckCreaturesVector) { creature->creatureCheck = false; } } void Game::checkCreatures() { + metrics::method_latency measure(__METHOD_NAME__); static size_t index = 0; auto &checkCreatureList = checkCreatureLists[index]; @@ -5990,6 +6016,31 @@ bool Game::combatBlockHit(CombatDamage &damage, std::shared_ptr attack std::shared_ptr targetPlayer = target->getPlayer(); if (damage.primary.type != COMBAT_NONE) { + + damage.primary.value = -damage.primary.value; + // Damage healing primary + if (attacker) { + if (target->getMonster()) { + uint32_t primaryHealing = target->getMonster()->getHealingCombatValue(damage.primary.type); + if (primaryHealing > 0) { + damageHeal.primary.value = std::ceil((damage.primary.value) * (primaryHealing / 100.)); + canHeal = true; + } + } + if (targetPlayer && attacker->getAbsorbPercent(damage.primary.type) != 0) { + damageAbsorbMessage = true; + } + if (attacker->getPlayer() && attacker->getIncreasePercent(damage.primary.type) != 0) { + damageIncreaseMessage = true; + } + damage.primary.value *= attacker->getBuff(BUFF_DAMAGEDEALT) / 100.; + } + damage.primary.value *= target->getBuff(BUFF_DAMAGERECEIVED) / 100.; + + primaryBlockType = target->blockHit(attacker, damage.primary.type, damage.primary.value, checkDefense, checkArmor, field); + + damage.primary.value = -damage.primary.value; + InternalGame::sendBlockEffect(primaryBlockType, damage.primary.type, target->getPosition(), attacker); // Damage reflection primary if (!damage.extension && attacker) { if (targetPlayer && attacker->getMonster() && damage.primary.type != COMBAT_HEALING) { @@ -6028,57 +6079,12 @@ bool Game::combatBlockHit(CombatDamage &damage, std::shared_ptr attack } } } - damage.primary.value = -damage.primary.value; - // Damage healing primary - if (attacker) { - if (target->getMonster()) { - uint32_t primaryHealing = target->getMonster()->getHealingCombatValue(damage.primary.type); - if (primaryHealing > 0) { - damageHeal.primary.value = std::ceil((damage.primary.value) * (primaryHealing / 100.)); - canHeal = true; - } - } - if (targetPlayer && attacker->getAbsorbPercent(damage.primary.type) != 0) { - damageAbsorbMessage = true; - } - if (attacker->getPlayer() && attacker->getIncreasePercent(damage.primary.type) != 0) { - damageIncreaseMessage = true; - } - damage.primary.value *= attacker->getBuff(BUFF_DAMAGEDEALT) / 100.; - } - damage.primary.value *= target->getBuff(BUFF_DAMAGERECEIVED) / 100.; - - primaryBlockType = target->blockHit(attacker, damage.primary.type, damage.primary.value, checkDefense, checkArmor, field); - - damage.primary.value = -damage.primary.value; - InternalGame::sendBlockEffect(primaryBlockType, damage.primary.type, target->getPosition(), attacker); } else { primaryBlockType = BLOCK_NONE; } if (damage.secondary.type != COMBAT_NONE) { - // Damage reflection secondary - if (!damage.extension && attacker && target->getMonster()) { - uint32_t secondaryReflectPercent = target->getReflectPercent(damage.secondary.type, true); - uint32_t secondaryReflectFlat = target->getReflectFlat(damage.secondary.type, true); - if (secondaryReflectPercent > 0 || secondaryReflectFlat > 0) { - if (!canReflect) { - damageReflected.primary.type = damage.secondary.type; - damageReflected.primary.value = std::ceil(damage.secondary.value * secondaryReflectPercent / 100.) + std::max(-static_cast(std::ceil(attacker->getMaxHealth() * 0.01)), std::max(damage.secondary.value, -(static_cast(secondaryReflectFlat)))); - if (!damageReflected.exString.empty()) { - damageReflected.exString += ", "; - } - damageReflected.extension = true; - damageReflected.exString += "damage reflection"; - damageReflectedParams.combatType = damage.primary.type; - damageReflectedParams.aggressive = true; - canReflect = true; - } else { - damageReflected.secondary.type = damage.secondary.type; - damageReflected.primary.value = std::ceil(damage.secondary.value * secondaryReflectPercent / 100.) + std::max(-static_cast(std::ceil(attacker->getMaxHealth() * 0.01)), std::max(damage.secondary.value, -(static_cast(secondaryReflectFlat)))); - } - } - } + damage.secondary.value = -damage.secondary.value; // Damage healing secondary if (attacker && target->getMonster()) { @@ -6102,9 +6108,32 @@ bool Game::combatBlockHit(CombatDamage &damage, std::shared_ptr attack damage.secondary.value = -damage.secondary.value; InternalGame::sendBlockEffect(secondaryBlockType, damage.secondary.type, target->getPosition(), attacker); + + if (!damage.extension && attacker && target->getMonster()) { + int32_t secondaryReflectPercent = target->getReflectPercent(damage.secondary.type, true); + int32_t secondaryReflectFlat = target->getReflectFlat(damage.secondary.type, true); + if (secondaryReflectPercent > 0 || secondaryReflectFlat > 0) { + if (!canReflect) { + damageReflected.primary.type = damage.secondary.type; + damageReflected.primary.value = std::ceil(damage.secondary.value * secondaryReflectPercent / 100.) + std::max(-static_cast(std::ceil(attacker->getMaxHealth() * 0.01)), std::max(damage.secondary.value, -(static_cast(secondaryReflectFlat)))); + if (!damageReflected.exString.empty()) { + damageReflected.exString += ", "; + } + damageReflected.extension = true; + damageReflected.exString += "damage reflection"; + damageReflectedParams.combatType = damage.primary.type; + damageReflectedParams.aggressive = true; + canReflect = true; + } else { + damageReflected.secondary.type = damage.secondary.type; + damageReflected.primary.value = std::ceil(damage.secondary.value * secondaryReflectPercent / 100.) + std::max(-static_cast(std::ceil(attacker->getMaxHealth() * 0.01)), std::max(damage.secondary.value, -(static_cast(secondaryReflectFlat)))); + } + } + } } else { secondaryBlockType = BLOCK_NONE; } + // Damage reflection secondary if (damage.primary.type == COMBAT_HEALING) { damage.primary.value *= target->getBuff(BUFF_HEALINGRECEIVED) / 100.; @@ -7223,13 +7252,7 @@ bool Game::combatChangeMana(std::shared_ptr attacker, std::shared_ptr< } target->drainMana(attacker, manaLoss); - if (targetPlayer) { - std::string cause = "(other)"; - if (attacker) { - cause = attacker->getName(); - } - targetPlayer->updateInputAnalyzer(damage.primary.type, damage.primary.value * -1, cause); - } + std::stringstream ss; std::string damageString = std::to_string(manaLoss); @@ -8470,6 +8493,7 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite } g_game().removeMoney(player, fee, 0, true); + g_metrics().addCounter("balance_decrease", fee, { { "player", player->getName() }, { "context", "market_fee" } }); } else { uint64_t totalPrice = price * amount; totalPrice += fee; @@ -8479,6 +8503,7 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite } g_game().removeMoney(player, totalPrice, 0, true); + g_metrics().addCounter("balance_decrease", totalPrice, { { "player", player->getName() }, { "context", "market_offer" } }); } // Send market window again for update item stats and avoid item clone @@ -8540,6 +8565,7 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (offer.type == MARKETACTION_BUY) { player->setBankBalance(player->getBankBalance() + offer.price * offer.amount); + g_metrics().addCounter("balance_decrease", offer.price * offer.amount, { { "player", player->getName() }, { "context", "market_purchase" } }); // Send market window again for update stats player->sendMarketEnter(player->getLastDepotId()); } else { @@ -8700,6 +8726,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } player->setBankBalance(player->getBankBalance() + totalPrice); + g_metrics().addCounter("balance_increase", totalPrice, { { "player", player->getName() }, { "context", "market_sale" } }); if (it.id == ITEM_STORE_COIN) { buyerPlayer->getAccount()->addCoins(account::CoinType::TRANSFERABLE, amount, "Purchased on Market"); @@ -8770,6 +8797,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 player->setBankBalance(0); g_game().removeMoney(player, remainsPrice); } + g_metrics().addCounter("balance_decrease", totalPrice, { { "player", player->getName() }, { "context", "market_purchase" } }); if (it.id == ITEM_STORE_COIN) { player->getAccount()->addCoins(account::CoinType::TRANSFERABLE, amount, "Purchased on Market"); @@ -8824,6 +8852,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 } sellerPlayer->setBankBalance(sellerPlayer->getBankBalance() + totalPrice); + g_metrics().addCounter("balance_increase", totalPrice, { { "player", sellerPlayer->getName() }, { "context", "market_sale" } }); if (it.id == ITEM_STORE_COIN) { sellerPlayer->getAccount()->registerCoinTransaction(account::CoinTransactionType::REMOVE, account::CoinType::TRANSFERABLE, amount, "Sold on Market"); } @@ -8932,6 +8961,7 @@ void Game::playerAnswerModalWindow(uint32_t playerId, uint32_t modalWindowId, ui } void Game::playerForgeFuseItems(uint32_t playerId, uint16_t itemId, uint8_t tier, bool usedCore, bool reduceTierLoss) { + metrics::method_latency measure(__METHOD_NAME__); std::shared_ptr player = getPlayerByID(playerId); if (!player) { return; @@ -9015,6 +9045,7 @@ void Game::playerBosstiarySlot(uint32_t playerId, uint8_t slotId, uint32_t selec uint8_t removeTimes = player->getRemoveTimes(); uint32_t removePrice = g_ioBosstiary().calculteRemoveBoss(removeTimes); g_game().removeMoney(player, removePrice, 0, true); + g_metrics().addCounter("balance_decrease", removePrice, { { "player", player->getName() }, { "context", "bosstiary_remove" } }); player->addRemoveTime(); } diff --git a/src/game/game.hpp b/src/game/game.hpp index d1676e19823..0ae4065ab06 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -236,7 +236,7 @@ class Game { std::shared_ptr transformItem(std::shared_ptr item, uint16_t newId, int32_t newCount = -1); - ReturnValue internalTeleport(std::shared_ptr thing, const Position &newPos, bool pushMove = true, uint32_t flags = 0); + ReturnValue internalTeleport(const std::shared_ptr &thing, const Position &newPos, bool pushMove = true, uint32_t flags = 0); bool internalCreatureTurn(std::shared_ptr creature, Direction dir); diff --git a/src/game/scheduling/task.cpp b/src/game/scheduling/task.cpp index 28418a4a169..1cc4c4cba06 100644 --- a/src/game/scheduling/task.cpp +++ b/src/game/scheduling/task.cpp @@ -10,15 +10,17 @@ #include "pch.hpp" #include "task.hpp" #include "lib/logging/log_with_spd_log.hpp" +#include "lib/metrics/metrics.hpp" std::atomic_uint_fast64_t Task::LAST_EVENT_ID = 0; bool Task::execute() const { + metrics::task_latency measure(context); if (isCanceled()) { return false; } if (hasExpired()) { - g_logger().info("The task '{}' has expired, it has not been executed in {} ms.", getContext(), expiration - utime); + g_logger().info("The task '{}' has expired, it has not been executed in {}.", getContext(), expiration - utime); return false; } diff --git a/src/game/scheduling/task.hpp b/src/game/scheduling/task.hpp index 18dfd5d39de..480072f427b 100644 --- a/src/game/scheduling/task.hpp +++ b/src/game/scheduling/task.hpp @@ -111,7 +111,7 @@ class Task { }; std::function func = nullptr; - std::string_view context; + std::string context; int64_t utime = 0; int64_t expiration = 0; diff --git a/src/game/zones/zone.cpp b/src/game/zones/zone.cpp index fdeccb9bfaa..97ef72cf24f 100644 --- a/src/game/zones/zone.cpp +++ b/src/game/zones/zone.cpp @@ -26,7 +26,7 @@ std::shared_ptr Zone::addZone(const std::string &name, uint32_t zoneID /* return nullZone; } if (zoneID != 0 && zonesByID.contains(zoneID)) { - g_logger().debug("Found with ID {} while adding {}, linking them together...", zoneID, name); + g_logger().trace("[Zone::addZone] Found with ID {} while adding {}, linking them together...", zoneID, name); auto zone = zonesByID[zoneID]; zone->name = name; zones[name] = zone; @@ -244,7 +244,7 @@ void Zone::refresh() { for (const auto &position : getPositions()) { g_game().map.refreshZones(position); } - g_logger().debug("Refreshed zone '{}' in {} milliseconds", name, bm_refresh.duration()); + g_logger().trace("Refreshed zone '{}' in {} milliseconds", name, bm_refresh.duration()); } void Zone::setMonsterVariant(const std::string &variant) { diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index 3f2e7a233c3..e6adeb2f4b2 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -177,6 +177,7 @@ bool IOLoginDataSave::savePlayerFirst(std::shared_ptr player) { // First, an UPDATE query to write the player itself query.str(""); query << "UPDATE `players` SET "; + query << "`name` = " << db.escapeString(player->name) << ","; query << "`level` = " << player->level << ","; query << "`group_id` = " << player->group->id << ","; query << "`vocation` = " << player->getVocationId() << ","; diff --git a/src/io/io_wheel.cpp b/src/io/io_wheel.cpp index 3db059a8023..3a8d69d7dac 100644 --- a/src/io/io_wheel.cpp +++ b/src/io/io_wheel.cpp @@ -49,7 +49,7 @@ namespace InternalPlayerWheel { void registerWheelSpellTable(const T &spellData, const std::string &name, WheelSpellGrade_t gradeType) { if (name == "Any_Focus_Mage_Spell") { for (const std::string &focusSpellName : m_focusSpells) { - g_logger().debug("[{}] registered any spell: {}", __FUNCTION__, focusSpellName); + g_logger().trace("[{}] registered any spell: {}", __FUNCTION__, focusSpellName); registerWheelSpellTable(spellData, focusSpellName, gradeType); } return; @@ -57,7 +57,7 @@ namespace InternalPlayerWheel { auto spell = g_spells().getInstantSpellByName(name); if (spell) { - g_logger().debug("[{}] registering instant spell with name {}", __FUNCTION__, spell->getName()); + g_logger().trace("[{}] registering instant spell with name {}", __FUNCTION__, spell->getName()); // Increase data const auto increaseData = spellData.increase; if (increaseData.damage > 0) { diff --git a/src/io/iobestiary.cpp b/src/io/iobestiary.cpp index 2f66c53ad26..a84d3091a3d 100644 --- a/src/io/iobestiary.cpp +++ b/src/io/iobestiary.cpp @@ -14,6 +14,7 @@ #include "io/iobestiary.hpp" #include "creatures/monsters/monsters.hpp" #include "creatures/players/player.hpp" +#include "lib/metrics/metrics.hpp" SoftSingleton IOBestiary::instanceTracker("IOBestiary"); @@ -336,6 +337,7 @@ void IOBestiary::sendBuyCharmRune(std::shared_ptr player, charmRune_t ru resetCharmRuneCreature(player, charm); player->sendFYIBox("You successfully removed the creature."); player->BestiarysendCharms(); + g_metrics().addCounter("balance_decrease", fee, { { "player", player->getName() }, { "context", "charm_removal" } }); return; } player->sendFYIBox("You don't have enough gold."); diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 584f36e2085..8531ff0860f 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -15,6 +15,7 @@ #include "game/game.hpp" #include "creatures/monsters/monster.hpp" #include "creatures/players/wheel/player_wheel.hpp" +#include "lib/metrics/metrics.hpp" bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, const std::string &password, std::string &characterName, uint32_t &accountId, bool oldProtocol) { account::Account account(accountDescriptor); @@ -74,9 +75,11 @@ void IOLoginData::updateOnlineStatus(uint32_t guid, bool login) { std::ostringstream query; if (login) { + g_metrics().addUpDownCounter("players_online", 1); query << "INSERT INTO `players_online` VALUES (" << guid << ')'; updateOnline[guid] = true; } else { + g_metrics().addUpDownCounter("players_online", -1); query << "DELETE FROM `players_online` WHERE `player_id` = " << guid; updateOnline.erase(guid); } diff --git a/src/io/ioprey.cpp b/src/io/ioprey.cpp index b953d1807f1..2a1c0eebd7a 100644 --- a/src/io/ioprey.cpp +++ b/src/io/ioprey.cpp @@ -14,6 +14,7 @@ #include "config/configmanager.hpp" #include "game/game.hpp" #include "io/ioprey.hpp" +#include "lib/metrics/metrics.hpp" // Prey class PreySlot::PreySlot(PreySlot_t id) : @@ -301,6 +302,8 @@ void IOPrey::parsePreyAction(std::shared_ptr player, PreySlot_t slotId, return; } else if (slot->freeRerollTimeStamp <= OTSYS_TIME()) { slot->freeRerollTimeStamp = OTSYS_TIME() + g_configManager().getNumber(PREY_FREE_REROLL_TIME, __FUNCTION__) * 1000; + } else { + g_metrics().addCounter("balance_decrease", player->getPreyRerollPrice(), { { "player", player->getName() }, { "context", "prey_reroll" } }); } slot->eraseBonus(true); @@ -406,6 +409,8 @@ void IOPrey::parseTaskHuntingAction(std::shared_ptr player, PreySlot_t s return; } else if (slot->freeRerollTimeStamp <= OTSYS_TIME()) { slot->freeRerollTimeStamp = OTSYS_TIME() + g_configManager().getNumber(TASK_HUNTING_FREE_REROLL_TIME, __FUNCTION__) * 1000; + } else { + g_metrics().addCounter("balance_decrease", player->getTaskHuntingRerollPrice(), { { "player", player->getName() }, { "context", "hunting_task_reroll" } }); } slot->eraseTask(); @@ -465,6 +470,7 @@ void IOPrey::parseTaskHuntingAction(std::shared_ptr player, PreySlot_t s return; } + g_metrics().addCounter("balance_decrease", player->getTaskHuntingRerollPrice(), { { "player", player->getName() }, { "context", "hunting_task_cancel" } }); slot->eraseTask(); slot->reloadReward(); slot->state = PreyTaskDataState_Selection; diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 00b8329f456..402c6a34a79 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,5 +1,6 @@ target_sources(${PROJECT_NAME}_lib PRIVATE di/soft_singleton.cpp logging/log_with_spd_log.cpp + metrics/metrics.cpp thread/thread_pool.cpp ) diff --git a/src/lib/metrics/metrics.cpp b/src/lib/metrics/metrics.cpp new file mode 100644 index 00000000000..77a4bb8b3a8 --- /dev/null +++ b/src/lib/metrics/metrics.cpp @@ -0,0 +1,105 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 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 "metrics.hpp" +#include "lib/di/container.hpp" + +using namespace metrics; + +Metrics &Metrics::getInstance() { + return inject(); +} + +void Metrics::init(Options opts) { + if (!opts.enableOStreamExporter && !opts.enablePrometheusExporter) { + return; + } + + auto provider = metrics_sdk::MeterProviderFactory::Create(); + auto* p = static_cast(provider.get()); + + if (opts.enableOStreamExporter) { + opts.ostreamOptions.export_timeout_millis = std::chrono::milliseconds(1000); + auto ostreamExporter = metrics_exporter::OStreamMetricExporterFactory::Create(); + auto reader = metrics_sdk::PeriodicExportingMetricReaderFactory::Create(std::move(ostreamExporter), opts.ostreamOptions); + p->AddMetricReader(std::move(reader)); + } + + if (opts.enablePrometheusExporter) { + g_logger().info("Starting Prometheus exporter at {}", opts.prometheusOptions.url); + auto prometheusExporter = metrics_exporter::PrometheusExporterFactory::Create(opts.prometheusOptions); + p->AddMetricReader(std::move(prometheusExporter)); + } + + for (auto name : latencyNames) { + auto instrumentSelector = metrics_sdk::InstrumentSelectorFactory::Create(metrics_sdk::InstrumentType::kHistogram, name, "us"); + auto meterSelector = metrics_sdk::MeterSelectorFactory::Create("performance", otelVersion, otelSchema); + + auto aggregationConfig = std::make_unique(); + // TODO: migrate to ExponentialHistogramIndexer when that's available + // clang-format off + aggregationConfig->boundaries_ = { + // Ultra-fine granularity below 10µs + 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, + 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, + 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, + 85.0, 90.0, 95.0, 100.0, + // Fine granularity between 100µs and 500µs + 120.0, 140.0, 160.0, 180.0, 200.0, 225.0, 250.0, 275.0, 300.0, 325.0, 350.0, 375.0, 400.0, 425.0, 450.0, 475.0, 500.0, + // Moderate granularity from 500µs to 1ms (1000µs) + 550.0, 600.0, 650.0, 700.0, 750.0, 800.0, 850.0, 900.0, 950.0, 1000.0, + // Coarser granularity for higher latencies (in microseconds) + 1100.0, 1200.0, 1300.0, 1400.0, 1500.0, 2000.0, 2500.0, 3000.0, 3500.0, 4000.0, 4500.0, 5000.0, 10000.0, + // Very coarse granularity for latencies in milliseconds + 20000.0, 30000.0, 40000.0, 50000.0, 60000.0,70000.0, 80000.0, 90000.0, 100000.0, + 200000.0, 300000.0, 400000.0, 500000.0, 600000.0,700000.0, 800000.0, 900000.0, 1000000.0, + // Even coarser granularity for latencies in seconds + 2000000.0, 3000000.0, 4000000.0, 5000000.0, 6000000.0,7000000.0, 8000000.0, 9000000.0, 10000000.0, + 20000000.0, 30000000.0, 40000000.0, 50000000.0, 60000000.0,70000000.0, 80000000.0, 90000000.0, 100000000.0, + // And finally a catch-all for anything else + std::numeric_limits::infinity(), + }; + // clang-format on + + auto view = metrics_sdk::ViewFactory::Create(name, "Latency", "us", metrics_sdk::AggregationType::kHistogram, std::move(aggregationConfig)); + p->AddView(std::move(instrumentSelector), std::move(meterSelector), std::move(view)); + + latencyHistograms[name] = getMeter()->CreateDoubleHistogram(name, "Latency", "us"); + } + + metrics_api::Provider::SetMeterProvider(std::move(provider)); +} + +void Metrics::shutdown() { + std::shared_ptr none; + metrics_api::Provider::SetMeterProvider(none); +} + +ScopedLatency::ScopedLatency(std::string_view name, const std::string &histogramName, const std::string &scopeKey) : + ScopedLatency(name, g_metrics().latencyHistograms[histogramName], { { scopeKey, std::string(name) } }, g_metrics().defaultContext) { + if (histogram == nullptr) { + stopped = true; + return; + } +} + +ScopedLatency::~ScopedLatency() { + stop(); +} + +void ScopedLatency::stop() { + if (stopped) { + return; + } + stopped = true; + auto end = std::chrono::steady_clock::now(); + double elapsed = static_cast(std::chrono::duration_cast(end - begin).count()) / 1000; + auto attrskv = opentelemetry::common::KeyValueIterableView { attrs }; + histogram->Record(elapsed, attrskv, context); +} diff --git a/src/lib/metrics/metrics.hpp b/src/lib/metrics/metrics.hpp new file mode 100644 index 00000000000..116676fb3b7 --- /dev/null +++ b/src/lib/metrics/metrics.hpp @@ -0,0 +1,171 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 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 "game/scheduling/dispatcher.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace metrics_sdk = opentelemetry::sdk::metrics; +namespace common = opentelemetry::common; +namespace metrics_exporter = opentelemetry::exporter::metrics; +namespace metrics_api = opentelemetry::metrics; + +constexpr std::string_view methodName(const char* s) { + std::string_view prettyFunction(s); + size_t bracket = prettyFunction.rfind("("); + size_t space = prettyFunction.rfind(" ", bracket) + 1; + return prettyFunction.substr(space, bracket - space); +} + +#if defined(__GNUC__) || defined(__clang__) + #define __METHOD_NAME__ methodName(__PRETTY_FUNCTION__) +#elif defined(_MSC_VER) + #define __METHOD_NAME__ methodName(__FUNCSIG__) +#else + #error "Compiler not supported" +#endif + +namespace metrics { + using Meter = opentelemetry::nostd::shared_ptr; + + template + using Histogram = opentelemetry::nostd::unique_ptr>; + + template + using Counter = opentelemetry::nostd::unique_ptr>; + + template + using UpDownCounter = opentelemetry::nostd::unique_ptr>; + + struct Options { + bool enablePrometheusExporter; + bool enableOStreamExporter; + + metrics_sdk::PeriodicExportingMetricReaderOptions ostreamOptions; + metrics_exporter::PrometheusExporterOptions prometheusOptions; + }; + + class ScopedLatency { + public: + explicit ScopedLatency(std::string_view name, const std::string &histogramName, const std::string &scopeKey); + explicit ScopedLatency(std::string_view name, Histogram &histogram, std::map attrs = {}, opentelemetry::context::Context context = opentelemetry::context::Context()) : + begin(std::chrono::steady_clock::now()), histogram(histogram), attrs(attrs), context(context) { + } + + void stop(); + + ~ScopedLatency(); + + private: + opentelemetry::context::Context context; + Histogram &histogram; + std::chrono::steady_clock::time_point begin; + std::map attrs; + bool stopped { false }; + }; + +#define DEFINE_LATENCY_CLASS(class_name, histogram_name, category) \ + class class_name##_latency final : public ScopedLatency { \ + public: \ + class_name##_latency(std::string_view name) : \ + ScopedLatency(name, histogram_name "_latency", category) { } \ + } + + DEFINE_LATENCY_CLASS(method, "method", "method"); + DEFINE_LATENCY_CLASS(lua, "lua", "scope"); + DEFINE_LATENCY_CLASS(query, "query", "truncated_query"); + DEFINE_LATENCY_CLASS(task, "task", "task"); + DEFINE_LATENCY_CLASS(lock, "lock", "scope"); + + const std::vector latencyNames { + "method_latency", + "lua_latency", + "query_latency", + "task_latency", + "lock_latency", + }; + + class Metrics final { + public: + Metrics() { } + ~Metrics() = default; + + void init(Options opts); + void shutdown(); + + static Metrics &getInstance(); + + void addCounter(std::string_view name, double value, std::map attrs = {}) { + std::scoped_lock lock(mutex_); + if (!getMeter()) { + return; + } + if (counters.find(name) == counters.end()) { + std::string nameStr(name); + counters[name] = getMeter()->CreateDoubleCounter(nameStr); + } + auto attrskv = opentelemetry::common::KeyValueIterableView { attrs }; + counters[name]->Add(value, attrskv); + } + + void addUpDownCounter(std::string_view name, int value, std::map attrs = {}) { + std::scoped_lock lock(mutex_); + if (!getMeter()) { + return; + } + if (upDownCounters.find(name) == upDownCounters.end()) { + std::string nameStr(name); + upDownCounters[name] = getMeter()->CreateInt64UpDownCounter(nameStr); + } + auto attrskv = opentelemetry::common::KeyValueIterableView { attrs }; + upDownCounters[name]->Add(value, attrskv); + } + + friend class ScopedLatency; + + protected: + opentelemetry::context::Context defaultContext {}; + phmap::parallel_flat_hash_map> latencyHistograms; + phmap::flat_hash_map> upDownCounters; + phmap::flat_hash_map> counters; + + Meter getMeter() { + auto provider = metrics_api::Provider::GetMeterProvider(); + if (provider == nullptr) { + return {}; + } + return provider->GetMeter(meterName, otelVersion); + } + + private: + std::mutex mutex_; + + std::string meterName { "stats" }; + std::string otelVersion { "1.2.0" }; + std::string otelSchema { "https://opentelemetry.io/schemas/1.2.0" }; + }; +} + +constexpr auto g_metrics + = metrics::Metrics::getInstance; diff --git a/src/lua/functions/core/CMakeLists.txt b/src/lua/functions/core/CMakeLists.txt index 6f4168f0fac..1cf919da3ec 100644 --- a/src/lua/functions/core/CMakeLists.txt +++ b/src/lua/functions/core/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE libs/db_functions.cpp libs/result_functions.cpp libs/logger_functions.cpp + libs/metrics_functions.cpp libs/kv_functions.cpp network/network_message_functions.cpp network/webhook_functions.cpp diff --git a/src/lua/functions/core/game/game_functions.cpp b/src/lua/functions/core/game/game_functions.cpp index cab8c599a21..633f39489de 100644 --- a/src/lua/functions/core/game/game_functions.cpp +++ b/src/lua/functions/core/game/game_functions.cpp @@ -611,13 +611,23 @@ int GameFunctions::luaGameHasDistanceEffect(lua_State* L) { } int GameFunctions::luaGameGetOfflinePlayer(lua_State* L) { - uint32_t playerId = getNumber(L, 1); - - auto offlinePlayer = std::make_shared(nullptr); - if (!IOLoginData::loadPlayerById(offlinePlayer, playerId)) { + // Game.getOfflinePlayer(name or id) + std::shared_ptr player = nullptr; + if (isNumber(L, 1)) { + uint32_t id = getNumber(L, 1); + if (id >= Player::getFirstID() && id <= Player::getLastID()) { + player = g_game().getPlayerByID(id, true); + } else { + player = g_game().getPlayerByGUID(id, true); + } + } else if (isString(L, 1)) { + auto name = getString(L, 1); + player = g_game().getPlayerByName(name, true); + } + if (!player) { lua_pushnil(L); } else { - pushUserdata(L, offlinePlayer); + pushUserdata(L, player); setMetatable(L, -1, "Player"); } diff --git a/src/lua/functions/core/libs/core_libs_functions.hpp b/src/lua/functions/core/libs/core_libs_functions.hpp index 29b8c8896e7..4badabe262f 100644 --- a/src/lua/functions/core/libs/core_libs_functions.hpp +++ b/src/lua/functions/core/libs/core_libs_functions.hpp @@ -14,6 +14,7 @@ #include "lua/functions/core/libs/db_functions.hpp" #include "lua/functions/core/libs/result_functions.hpp" #include "lua/functions/core/libs/logger_functions.hpp" +#include "lua/functions/core/libs/metrics_functions.hpp" #include "lua/functions/core/libs/kv_functions.hpp" class CoreLibsFunctions final : LuaScriptInterface { @@ -23,6 +24,7 @@ class CoreLibsFunctions final : LuaScriptInterface { DBFunctions::init(L); ResultFunctions::init(L); LoggerFunctions::init(L); + MetricsFunctions::init(L); KVFunctions::init(L); } diff --git a/src/lua/functions/core/libs/metrics_functions.cpp b/src/lua/functions/core/libs/metrics_functions.cpp new file mode 100644 index 00000000000..4c0b916c94d --- /dev/null +++ b/src/lua/functions/core/libs/metrics_functions.cpp @@ -0,0 +1,40 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 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 "lua/functions/core/libs/metrics_functions.hpp" +#include "lib/metrics/metrics.hpp" + +void MetricsFunctions::init(lua_State* L) { + registerTable(L, "metrics"); + registerMethod(L, "metrics", "addCounter", MetricsFunctions::luaMetricsAddCounter); +} + +// Metrics +int MetricsFunctions::luaMetricsAddCounter(lua_State* L) { + // metrics.addCounter(name, value, attributes) + auto name = getString(L, 1); + auto value = getNumber(L, 2); + auto attributes = getAttributes(L, 3); + g_metrics().addCounter(name, value, attributes); + return 1; +} + +std::map MetricsFunctions::getAttributes(lua_State* L, int32_t index) { + std::map attributes; + if (isTable(L, index)) { + lua_pushnil(L); + while (lua_next(L, index) != 0) { + attributes[getString(L, -2)] = getString(L, -1); + lua_pop(L, 1); + } + } + return attributes; +} diff --git a/src/lua/functions/core/libs/metrics_functions.hpp b/src/lua/functions/core/libs/metrics_functions.hpp new file mode 100644 index 00000000000..47d492c3d42 --- /dev/null +++ b/src/lua/functions/core/libs/metrics_functions.hpp @@ -0,0 +1,21 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 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 "lua/scripts/luascript.hpp" + +class MetricsFunctions final : public LuaScriptInterface { +public: + static void init(lua_State* L); + +private: + static int luaMetricsAddCounter(lua_State* L); + static std::map getAttributes(lua_State* L, int32_t index); +}; diff --git a/src/lua/functions/core/network/network_message_functions.cpp b/src/lua/functions/core/network/network_message_functions.cpp index 61567b1e1f0..fc1c1688613 100644 --- a/src/lua/functions/core/network/network_message_functions.cpp +++ b/src/lua/functions/core/network/network_message_functions.cpp @@ -191,11 +191,12 @@ int NetworkMessageFunctions::luaNetworkMessageAdd64(lua_State* L) { } int NetworkMessageFunctions::luaNetworkMessageAddString(lua_State* L) { - // networkMessage:addString(string) + // networkMessage:addString(string, function) const std::string &string = getString(L, 2); + const std::string &function = getString(L, 3); const auto &message = getUserdataShared(L, 1); if (message) { - message->addString(string); + message->addString(string, function); pushBoolean(L, true); } else { lua_pushnil(L); diff --git a/src/lua/functions/creatures/player/player_functions.cpp b/src/lua/functions/creatures/player/player_functions.cpp index 5b646264d95..df80a42b13c 100644 --- a/src/lua/functions/creatures/player/player_functions.cpp +++ b/src/lua/functions/creatures/player/player_functions.cpp @@ -20,6 +20,7 @@ #include "items/item.hpp" #include "lua/functions/creatures/player/player_functions.hpp" #include "game/scheduling/save_manager.hpp" +#include "game/scheduling/dispatcher.hpp" #include "map/spectators.hpp" int PlayerFunctions::luaPlayerSendInventory(lua_State* L) { diff --git a/src/lua/scripts/luascript.cpp b/src/lua/scripts/luascript.cpp index e4f230a5d69..adefef958ed 100644 --- a/src/lua/scripts/luascript.cpp +++ b/src/lua/scripts/luascript.cpp @@ -11,6 +11,7 @@ #include "lua/scripts/luascript.hpp" #include "lua/scripts/lua_environment.hpp" +#include "lib/metrics/metrics.hpp" ScriptEnvironment::DBResultMap ScriptEnvironment::tempResults; uint32_t ScriptEnvironment::lastResultId = 0; @@ -236,7 +237,35 @@ bool LuaScriptInterface::closeState() { return true; } +std::string LuaScriptInterface::getMetricsScope() { + metrics::method_latency measure(__METHOD_NAME__); + int32_t scriptId; + int32_t callbackId; + bool timerEvent; + LuaScriptInterface* scriptInterface; + getScriptEnv()->getEventInfo(scriptId, scriptInterface, callbackId, timerEvent); + + std::string name; + if (scriptId == EVENT_ID_LOADING) { + name = "loading"; + } else if (scriptId == EVENT_ID_USER) { + name = "user"; + } else { + name = scriptInterface->getFileById(scriptId); + if (name.empty()) { + return "unknown"; + } + auto pos = name.find("data"); + if (pos != std::string::npos) { + name = name.substr(pos); + } + } + + return fmt::format("{}:{}", name, timerEvent ? "timer" : ""); +} + bool LuaScriptInterface::callFunction(int params) { + metrics::lua_latency measure(getMetricsScope()); bool result = false; int size = lua_gettop(luaState); if (protectedCall(luaState, params, 1) != 0) { @@ -255,6 +284,7 @@ bool LuaScriptInterface::callFunction(int params) { } void LuaScriptInterface::callVoidFunction(int params) { + metrics::lua_latency measure(getMetricsScope()); int size = lua_gettop(luaState); if (protectedCall(luaState, params, 0) != 0) { LuaScriptInterface::reportError(nullptr, LuaScriptInterface::popString(luaState)); diff --git a/src/lua/scripts/luascript.hpp b/src/lua/scripts/luascript.hpp index 8f0b3b36c19..b7845c84558 100644 --- a/src/lua/scripts/luascript.hpp +++ b/src/lua/scripts/luascript.hpp @@ -73,6 +73,8 @@ class LuaScriptInterface : public LuaFunctionsLoader { std::map cacheFiles; private: + std::string getMetricsScope(); + std::string lastLuaError; std::string interfaceName; std::string loadingFile; diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index c4bf34c95b4..fb2430e05a6 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -15,6 +15,7 @@ #include "game/game.hpp" #include "items/bed.hpp" #include "game/scheduling/save_manager.hpp" +#include "lib/metrics/metrics.hpp" House::House(uint32_t houseId) : id(houseId) { } @@ -788,6 +789,7 @@ void Houses::payHouses(RentPeriod_t rentPeriod) const { if (player->getBankBalance() >= rent) { g_game().removeMoney(player, rent, 0, true); + g_metrics().addCounter("balance_decrease", rent, { { "player", player->getName() }, { "context", "house_rent" } }); time_t paidUntil = currentTime; switch (rentPeriod) { diff --git a/src/server/network/message/networkmessage.cpp b/src/server/network/message/networkmessage.cpp index 457e3b32be0..9c10b4907df 100644 --- a/src/server/network/message/networkmessage.cpp +++ b/src/server/network/message/networkmessage.cpp @@ -40,17 +40,17 @@ Position NetworkMessage::getPosition() { return pos; } -void NetworkMessage::addString(const std::string &value) { +void NetworkMessage::addString(const std::string &value, const std::string &function) { size_t stringLen = value.length(); if (value.empty()) { - g_logger().debug("[NetworkMessage::addString] - Value string is empty"); + g_logger().debug("[NetworkMessage::addString] - Value string is empty, function '{}'", function); } if (!canAdd(stringLen + 2)) { - g_logger().error("[NetworkMessage::addString] - NetworkMessage size is wrong: {}", stringLen); + g_logger().error("[NetworkMessage::addString] - NetworkMessage size is wrong: {}, function '{}'", stringLen, function); return; } if (stringLen > NETWORKMESSAGE_MAXSIZE) { - g_logger().error("[NetworkMessage::addString] - Exceded NetworkMessage max size: {}, actually size: {}", NETWORKMESSAGE_MAXSIZE, stringLen); + g_logger().error("[NetworkMessage::addString] - Exceded NetworkMessage max size: {}, actually size: {}, function '{}'", NETWORKMESSAGE_MAXSIZE, stringLen, function); return; } diff --git a/src/server/network/message/networkmessage.hpp b/src/server/network/message/networkmessage.hpp index 3635475a8d7..3051e87bb22 100644 --- a/src/server/network/message/networkmessage.hpp +++ b/src/server/network/message/networkmessage.hpp @@ -90,7 +90,7 @@ class NetworkMessage { void addBytes(const char* bytes, size_t size); void addPaddingBytes(size_t n); - void addString(const std::string &value); + void addString(const std::string &value, const std::string &function); void addDouble(double value, uint8_t precision = 2); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 958d19f2a71..30153af5ac7 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -228,7 +228,7 @@ namespace { g_logger().debug("Sendding category number '{}', category name '{}'", static_cast(value), magic_enum::enum_name(value).data()); msg.addByte(static_cast(value)); - msg.addString(toStartCaseWithSpace(magic_enum::enum_name(value).data())); + msg.addString(toStartCaseWithSpace(magic_enum::enum_name(value).data()), "void sendContainerCategory - toStartCaseWithSpace(magic_enum::enum_name(value).data())"); } } } // namespace @@ -553,7 +553,7 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x16); - output->addString(ss.str()); + output->addString(ss.str(), "ProtocolGame::login - ss.str()"); output->addByte(retryTime); send(output); disconnect(); @@ -802,7 +802,7 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) { auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x14); - output->addString(ss.str()); + output->addString(ss.str(), "ProtocolGame::onRecvFirstMessage - ss.str()"); send(output); g_dispatcher().scheduleEvent(1000, std::bind(&ProtocolGame::disconnect, getThis()), "ProtocolGame::disconnect"); return; @@ -842,7 +842,7 @@ void ProtocolGame::onConnect() { void ProtocolGame::disconnectClient(const std::string &message) const { auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x14); - output->addString(message); + output->addString(message, "ProtocolGame::disconnectClient - message"); send(output); disconnect(); } @@ -1994,10 +1994,10 @@ void ProtocolGame::sendItemInspection(uint16_t itemId, uint8_t itemCount, std::s const ItemType &it = Item::items[itemId]; if (item) { - msg.addString(item->getName()); + msg.addString(item->getName(), "ProtocolGame::sendItemInspection - item->getName()"); AddItem(msg, item); } else { - msg.addString(it.name); + msg.addString(it.name, "ProtocolGame::sendItemInspection - it.name"); AddItem(msg, it.id, itemCount, 0); } msg.addByte(0); @@ -2005,8 +2005,8 @@ void ProtocolGame::sendItemInspection(uint16_t itemId, uint8_t itemCount, std::s auto descriptions = Item::getDescriptions(it, item); msg.addByte(descriptions.size()); for (const auto &description : descriptions) { - msg.addString(description.first); - msg.addString(description.second); + msg.addString(description.first, "ProtocolGame::sendItemInspection - description.first"); + msg.addString(description.second, "ProtocolGame::sendItemInspection - description.second"); } writeToOutputBuffer(msg); } @@ -2088,8 +2088,8 @@ void ProtocolGame::sendHighscores(const std::vector &charact msg.addByte(0x00); // No data available msg.addByte(1); // Worlds - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__)); // First World - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__)); // Selected World + msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // First World + msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // Selected World msg.addByte(0); // Game World Category: 0xFF(-1) - Selected World msg.addByte(0); // BattlEye World Type @@ -2099,7 +2099,7 @@ void ProtocolGame::sendHighscores(const std::vector &charact msg.skipBytes(1); // Vocation Count msg.add(0xFFFFFFFF); // All Vocations - hardcoded - msg.addString("(all)"); // All Vocations - hardcoded + msg.addString("(all)", "ProtocolGame::sendHighscores - (all)"); // All Vocations - hardcoded uint32_t selectedVocation = 0xFFFFFFFF; const auto vocationsMap = g_vocations().getVocations(); @@ -2107,7 +2107,7 @@ void ProtocolGame::sendHighscores(const std::vector &charact const Vocation &vocation = it.second; if (vocation.getFromVocation() == static_cast(vocation.getId())) { msg.add(vocation.getFromVocation()); // Vocation Id - msg.addString(vocation.getVocName()); // Vocation Name + msg.addString(vocation.getVocName(), "ProtocolGame::sendHighscores - vocation.getVocName()"); // Vocation Name ++vocations; if (vocation.getFromVocation() == vocationId) { selectedVocation = vocationId; @@ -2132,7 +2132,7 @@ void ProtocolGame::sendHighscores(const std::vector &charact msg.addByte(sizeof(highscoreCategories) / sizeof(HighscoreCategory)); // Category Count for (HighscoreCategory &category : highscoreCategories) { msg.addByte(category.id); // Category Id - msg.addString(category.name); // Category Name + msg.addString(category.name, "ProtocolGame::sendHighscores - category.name"); // Category Name if (category.id == categoryId) { selectedCategory = categoryId; } @@ -2145,10 +2145,10 @@ void ProtocolGame::sendHighscores(const std::vector &charact msg.addByte(characters.size()); // Character Count for (const HighscoreCharacter &character : characters) { msg.add(character.rank); // Rank - msg.addString(character.name); // Character Name - msg.addString(""); // Probably Character Title(not visible in window) + msg.addString(character.name, "ProtocolGame::sendHighscores - character.name"); // Character Name + msg.addString("", "ProtocolGame::sendHighscores - empty"); // Probably Character Title(not visible in window) msg.addByte(character.vocation); // Vocation Id - msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__)); // World + msg.addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolGame::sendHighscores - g_configManager().getString(SERVER_NAME)"); // World msg.add(character.level); // Level msg.addByte((player->getGUID() == character.id)); // Player Indicator Boolean msg.add(character.points); // Points @@ -2212,7 +2212,7 @@ void ProtocolGame::parseBestiarysendRaces() { BestClass = mtype->info.bestiaryClass; } } - msg.addString(BestClass); + msg.addString(BestClass, "ProtocolGame::parseBestiarysendRaces - BestClass"); msg.add(count); uint16_t unlockedCount = g_iobestiary().getBestiaryRaceUnlocked(player, static_cast(i)); msg.add(unlockedCount); @@ -2264,7 +2264,7 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { NetworkMessage newmsg; newmsg.addByte(0xd7); newmsg.add(raceId); - newmsg.addString(Class); + newmsg.addString(Class, "ProtocolGame::parseBestiarysendMonsterData - Class"); newmsg.addByte(currentLevel); newmsg.add(killCounter); @@ -2305,7 +2305,7 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { newmsg.addByte(difficult); newmsg.addByte(0); // 1 if special event - 0 if regular loot (?) if (shouldAddItem == true) { - newmsg.addString(loot.name); + newmsg.addString(loot.name, "ProtocolGame::parseBestiarysendMonsterData - loot.name"); newmsg.addByte(loot.countmax > 0 ? 0x1 : 0x0); } } @@ -2338,7 +2338,7 @@ void ProtocolGame::parseBestiarysendMonsterData(NetworkMessage &msg) { } newmsg.add(1); - newmsg.addString(mtype->info.bestiaryLocations); + newmsg.addString(mtype->info.bestiaryLocations, "ProtocolGame::parseBestiarysendMonsterData - mtype->info.bestiaryLocations"); } if (currentLevel > 3) { @@ -2416,7 +2416,7 @@ void ProtocolGame::sendTeamFinderList() { uint8_t status = 0; uint16_t membersSize = 0; msg.add(leader->getGUID()); - msg.addString(leader->getName()); + msg.addString(leader->getName(), "ProtocolGame::sendTeamFinderList - leader->getName()"); msg.add(teamAssemble->minLevel); msg.add(teamAssemble->maxLevel); msg.addByte(teamAssemble->vocationIDs); @@ -2522,7 +2522,7 @@ void ProtocolGame::sendLeaderTeamFinder(bool reset) { } msg.add(leader->getGUID()); - msg.addString(leader->getName()); + msg.addString(leader->getName(), "ProtocolGame::sendLeaderTeamFinder - leader->getName()"); msg.add(leader->getLevel()); msg.addByte(leader->getVocation()->getClientId()); msg.addByte(3); @@ -2533,7 +2533,7 @@ void ProtocolGame::sendLeaderTeamFinder(bool reset) { continue; } msg.add(member->getGUID()); - msg.addString(member->getName()); + msg.addString(member->getName(), "ProtocolGame::sendLeaderTeamFinder - member->getName()"); msg.add(member->getLevel()); msg.addByte(member->getVocation()->getClientId()); msg.addByte(memberPair.second); @@ -2799,8 +2799,8 @@ void ProtocolGame::BestiarysendCharms() { msg.addByte(charmList.size()); for (const auto &c_type : charmList) { msg.addByte(c_type->id); - msg.addString(c_type->name); - msg.addString(c_type->description); + msg.addString(c_type->name, "ProtocolGame::BestiarysendCharms - c_type->name"); + msg.addString(c_type->description, "ProtocolGame::BestiarysendCharms - c_type->description"); msg.addByte(0); // Unknown msg.add(c_type->points); if (g_iobestiary().hasCharmUnlockedRuneBit(c_type, player->getUnlockedRunesBit())) { @@ -2872,7 +2872,7 @@ void ProtocolGame::parseBestiarysendCreatures(NetworkMessage &msg) { } NetworkMessage newmsg; newmsg.addByte(0xd6); - newmsg.addString(text); + newmsg.addString(text, "ProtocolGame::parseBestiarysendCreatures - text"); newmsg.add(race.size()); std::map creaturesKilled = g_iobestiary().getBestiaryKillCountByMonsterIDs(player, race); @@ -3092,7 +3092,7 @@ void ProtocolGame::parseSeekInContainer(NetworkMessage &msg) { void ProtocolGame::sendOpenPrivateChannel(const std::string &receiver) { NetworkMessage msg; msg.addByte(0xAD); - msg.addString(receiver); + msg.addString(receiver, "ProtocolGame::sendOpenPrivateChannel - receiver"); writeToOutputBuffer(msg); } @@ -3112,7 +3112,7 @@ void ProtocolGame::sendChannelEvent(uint16_t channelId, const std::string &playe NetworkMessage msg; msg.addByte(0xF3); msg.add(channelId); - msg.addString(playerName); + msg.addString(playerName, "ProtocolGame::sendChannelEvent - playerName"); msg.addByte(channelEvent); writeToOutputBuffer(msg); } @@ -3306,7 +3306,7 @@ void ProtocolGame::sendAddMarker(const Position &pos, uint8_t markType, const st msg.addPosition(pos); msg.addByte(markType); - msg.addString(desc); + msg.addString(desc, "ProtocolGame::sendAddMarker - desc"); writeToOutputBuffer(msg); } @@ -3331,13 +3331,13 @@ void ProtocolGame::sendCyclopediaCharacterBaseInformation() { msg.addByte(0xDA); msg.addByte(CYCLOPEDIA_CHARACTERINFO_BASEINFORMATION); msg.addByte(0x00); - msg.addString(player->getName()); - msg.addString(player->getVocation()->getVocName()); + msg.addString(player->getName(), "ProtocolGame::sendCyclopediaCharacterBaseInformation - player->getName()"); + msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterBaseInformation - player->getVocation()->getVocName()"); msg.add(player->getLevel()); AddOutfit(msg, player->getDefaultOutfit(), false); msg.addByte(0x00); // hide stamina - msg.addString(""); // character title + msg.addString("", "ProtocolGame::sendCyclopediaCharacterBaseInformation - empty"); // character title writeToOutputBuffer(msg); } @@ -3577,7 +3577,7 @@ void ProtocolGame::sendCyclopediaCharacterRecentDeaths(uint16_t page, uint16_t p msg.add(entries.size()); for (const RecentDeathEntry &entry : entries) { msg.add(entry.timestamp); - msg.addString(entry.cause); + msg.addString(entry.cause, "ProtocolGame::sendCyclopediaCharacterRecentDeaths - entry.cause"); } writeToOutputBuffer(msg); } @@ -3596,7 +3596,7 @@ void ProtocolGame::sendCyclopediaCharacterRecentPvPKills(uint16_t page, uint16_t msg.add(entries.size()); for (const RecentPvPKillEntry &entry : entries) { msg.add(entry.timestamp); - msg.addString(entry.description); + msg.addString(entry.description, "ProtocolGame::sendCyclopediaCharacterRecentPvPKills - entry.description"); msg.addByte(entry.status); } writeToOutputBuffer(msg); @@ -3661,7 +3661,7 @@ void ProtocolGame::sendCyclopediaCharacterOutfitsMounts() { ++outfitSize; msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendCyclopediaCharacterOutfitsMounts - outfit->name"); msg.addByte(addons); if (from == "store") { msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_STORE); @@ -3692,7 +3692,7 @@ void ProtocolGame::sendCyclopediaCharacterOutfitsMounts() { ++mountSize; msg.add(mount->clientId); - msg.addString(mount->name); + msg.addString(mount->name, "ProtocolGame::sendCyclopediaCharacterOutfitsMounts - mount->name"); if (type == "store") { msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_STORE); } else if (type == "quest") { @@ -3721,7 +3721,7 @@ void ProtocolGame::sendCyclopediaCharacterOutfitsMounts() { } ++familiarsSize; msg.add(familiar.lookType); - msg.addString(familiar.name); + msg.addString(familiar.name, "ProtocolGame::sendCyclopediaCharacterOutfitsMounts - familiar.name"); if (type == "quest") { msg.addByte(CYCLOPEDIA_CHARACTERINFO_OUTFITTYPE_QUEST); } else { @@ -3782,19 +3782,19 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { ++inventoryItems; msg.addByte(slot); - msg.addString(inventoryItem->getName()); + msg.addString(inventoryItem->getName(), "ProtocolGame::sendCyclopediaCharacterInspection - inventoryItem->getName()"); AddItem(msg, inventoryItem); msg.addByte(0); auto descriptions = Item::getDescriptions(Item::items[inventoryItem->getID()], inventoryItem); msg.addByte(descriptions.size()); for (const auto &description : descriptions) { - msg.addString(description.first); - msg.addString(description.second); + msg.addString(description.first, "ProtocolGame::sendCyclopediaCharacterInspection - description.first"); + msg.addString(description.second, "ProtocolGame::sendCyclopediaCharacterInspection - description.second"); } } } - msg.addString(player->getName()); + msg.addString(player->getName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getName()"); AddOutfit(msg, player->getDefaultOutfit(), false); // Player overall summary @@ -3804,28 +3804,28 @@ void ProtocolGame::sendCyclopediaCharacterInspection() { // Level description playerDescriptionSize++; - msg.addString("Level"); + msg.addString("Level", "ProtocolGame::sendCyclopediaCharacterInspection - Level"); // Vocation description playerDescriptionSize++; - msg.addString(std::to_string(player->getLevel())); - msg.addString("Vocation"); - msg.addString(player->getVocation()->getVocName()); + msg.addString(std::to_string(player->getLevel()), "ProtocolGame::sendCyclopediaCharacterInspection - std::to_string(player->getLevel())"); + msg.addString("Vocation", "ProtocolGame::sendCyclopediaCharacterInspection - Vocation"); + msg.addString(player->getVocation()->getVocName(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getVocation()->getVocName()"); // Loyalty title if (player->getLoyaltyTitle().length() != 0) { playerDescriptionSize++; - msg.addString("Loyalty Title"); - msg.addString(player->getLoyaltyTitle()); + msg.addString("Loyalty Title", "ProtocolGame::sendCyclopediaCharacterInspection - Loyalty Title"); + msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterInspection - player->getLoyaltyTitle()"); } // Outfit description playerDescriptionSize++; - msg.addString("Outfit"); + msg.addString("Outfit", "ProtocolGame::sendCyclopediaCharacterInspection - Outfit"); if (const auto outfit = Outfits::getInstance().getOutfitByLookType(player->getSex(), player->getDefaultOutfit().lookType)) { - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendCyclopediaCharacterInspection - outfit->name"); } else { - msg.addString("unknown"); + msg.addString("unknown", "ProtocolGame::sendCyclopediaCharacterInspection - unknown"); } msg.setBufferPosition(startInventory); @@ -3855,7 +3855,7 @@ void ProtocolGame::sendCyclopediaCharacterBadges() { // IsPremium (GOD has always 'Premium') msg.addByte(player->isPremium() ? 0x01 : 0x00); // Character loyalty title - msg.addString(player->getLoyaltyTitle()); + msg.addString(player->getLoyaltyTitle(), "ProtocolGame::sendCyclopediaCharacterBadges - player->getLoyaltyTitle()"); // Enable badges msg.addByte(0x00); // Todo badges loop @@ -4101,7 +4101,7 @@ void ProtocolGame::sendTextMessage(const TextMessage &message) { default: break; } - msg.addString(message.text); + msg.addString(message.text, "ProtocolGame::sendTextMessage - message.text"); writeToOutputBuffer(msg); } @@ -4116,9 +4116,9 @@ void ProtocolGame::sendCreatePrivateChannel(uint16_t channelId, const std::strin NetworkMessage msg; msg.addByte(0xB2); msg.add(channelId); - msg.addString(channelName); + msg.addString(channelName, "ProtocolGame::sendCreatePrivateChannel - channelName"); msg.add(0x01); - msg.addString(player->getName()); + msg.addString(player->getName(), "ProtocolGame::sendCreatePrivateChannel - player->getName()"); msg.add(0x00); writeToOutputBuffer(msg); } @@ -4131,7 +4131,7 @@ void ProtocolGame::sendChannelsDialog() { msg.addByte(list.size()); for (const auto &channel : list) { msg.add(channel->getId()); - msg.addString(channel->getName()); + msg.addString(channel->getName(), "ProtocolGame::sendChannelsDialog - channel->getName()"); } writeToOutputBuffer(msg); @@ -4142,12 +4142,12 @@ void ProtocolGame::sendChannel(uint16_t channelId, const std::string &channelNam msg.addByte(0xAC); msg.add(channelId); - msg.addString(channelName); + msg.addString(channelName, "ProtocolGame::sendChannel - channelName"); if (channelUsers) { msg.add(channelUsers->size()); for (const auto &it : *channelUsers) { - msg.addString(it.second->getName()); + msg.addString(it.second->getName(), "ProtocolGame::sendChannel - it.second->getName()"); } } else { msg.add(0x00); @@ -4156,7 +4156,7 @@ void ProtocolGame::sendChannel(uint16_t channelId, const std::string &channelNam if (invitedUsers) { msg.add(invitedUsers->size()); for (const auto &it : *invitedUsers) { - msg.addString(it.second->getName()); + msg.addString(it.second->getName(), "ProtocolGame::sendChannel - it.second->getName()"); } } else { msg.add(0x00); @@ -4168,11 +4168,11 @@ void ProtocolGame::sendChannelMessage(const std::string &author, const std::stri NetworkMessage msg; msg.addByte(0xAA); msg.add(0x00); - msg.addString(author); + msg.addString(author, "ProtocolGame::sendChannelMessage - author"); msg.add(0x00); msg.addByte(type); msg.add(channel); - msg.addString(text); + msg.addString(text, "ProtocolGame::sendChannelMessage - text"); writeToOutputBuffer(msg); } @@ -4213,10 +4213,10 @@ void ProtocolGame::sendContainer(uint8_t cid, std::shared_ptr contain if (container->getID() == ITEM_BROWSEFIELD) { AddItem(msg, ITEM_BAG, 1, container->getTier()); - msg.addString("Browse Field"); + msg.addString("Browse Field", "ProtocolGame::sendContainer - Browse Field"); } else { AddItem(msg, container); - msg.addString(container->getName()); + msg.addString(container->getName(), "ProtocolGame::sendContainer - container->getName()"); } const auto itemsStoreInboxToSend = container->getStoreInboxFilteredItems(); @@ -4343,7 +4343,7 @@ void ProtocolGame::sendLootStats(std::shared_ptr item, uint8_t count) { NetworkMessage msg; msg.addByte(0xCF); AddItem(msg, lootedItem); - msg.addString(lootedItem->getName()); + msg.addString(lootedItem->getName(), "ProtocolGame::sendLootStats - lootedItem->getName()"); item->setIsLootTrackeable(false); writeToOutputBuffer(msg); @@ -4353,11 +4353,11 @@ void ProtocolGame::sendLootStats(std::shared_ptr item, uint8_t count) { void ProtocolGame::sendShop(std::shared_ptr npc) { NetworkMessage msg; msg.addByte(0x7A); - msg.addString(npc->getName()); + msg.addString(npc->getName(), "ProtocolGame::sendShop - npc->getName()"); if (!oldProtocol) { msg.add(npc->getCurrency()); - msg.addString(std::string()); // Currency name + msg.addString(std::string(), "ProtocolGame::sendShop - std::string()"); // Currency name } std::vector shoplist = npc->getShopItemVector(player->getGUID()); @@ -4622,7 +4622,7 @@ void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList & } else { msg.add(static_cast(offer.price)); } - msg.addString(offer.playerName); + msg.addString(offer.playerName, "ProtocolGame::sendMarketBrowseItem - offer.playerName"); } msg.add(sellOffers.size()); @@ -4635,7 +4635,7 @@ void ProtocolGame::sendMarketBrowseItem(uint16_t itemId, const MarketOfferList & } else { msg.add(static_cast(offer.price)); } - msg.addString(offer.playerName); + msg.addString(offer.playerName, "ProtocolGame::sendMarketBrowseItem - offer.playerName"); } updateCoinBalance(); @@ -4664,7 +4664,7 @@ void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx &offer) { } else { msg.add(static_cast(offer.price)); } - msg.addString(offer.playerName); + msg.addString(offer.playerName, "ProtocolGame::sendMarketAcceptOffer - offer.playerName"); msg.add(0x00); } else { msg.add(0x00); @@ -4677,7 +4677,7 @@ void ProtocolGame::sendMarketAcceptOffer(const MarketOfferEx &offer) { } else { msg.add(static_cast(offer.price)); } - msg.addString(offer.playerName); + msg.addString(offer.playerName, "ProtocolGame::sendMarketAcceptOffer - offer.playerName"); } writeToOutputBuffer(msg); @@ -5098,7 +5098,7 @@ void ProtocolGame::sendForgeHistory(uint8_t page) { auto action = magic_enum::enum_integer(history.actionType); msg.add(static_cast(history.createdAt)); msg.addByte(action); - msg.addString(history.description); + msg.addString(history.description, "ProtocolGame::sendForgeHistory - history.description"); msg.addByte((history.bonus >= 1 && history.bonus < 8) ? 0x01 : 0x00); } } @@ -5128,7 +5128,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { } if (it.armor != 0) { - msg.addString(std::to_string(it.armor)); + msg.addString(std::to_string(it.armor), "ProtocolGame::sendMarketDetail - std::to_string(it.armor)"); } else { msg.add(0x00); } @@ -5156,21 +5156,21 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { } ss << static_cast(it.shootRange) << " fields"; } - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else if (!it.isRanged() && it.attack != 0) { if (it.abilities && it.abilities->elementType != COMBAT_NONE && it.abilities->elementDamage != 0) { std::ostringstream ss; ss << it.attack << " physical +" << it.abilities->elementDamage << ' ' << getCombatName(it.abilities->elementType); - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { - msg.addString(std::to_string(it.attack)); + msg.addString(std::to_string(it.attack), "ProtocolGame::sendMarketDetail - std::to_string(it.attack)"); } } else { msg.add(0x00); } if (it.isContainer()) { - msg.addString(std::to_string(it.maxItems)); + msg.addString(std::to_string(it.maxItems), "ProtocolGame::sendMarketDetail - std::to_string(it.maxItems)"); } else { msg.add(0x00); } @@ -5179,9 +5179,9 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.extraDefense != 0) { std::ostringstream ss; ss << it.defense << ' ' << std::showpos << it.extraDefense << std::noshowpos; - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { - msg.addString(std::to_string(it.defense)); + msg.addString(std::to_string(it.defense), "ProtocolGame::sendMarketDetail - std::to_string(it.defense)"); } } else { msg.add(0x00); @@ -5190,9 +5190,9 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (!it.description.empty()) { const std::string &descr = it.description; if (descr.back() == '.') { - msg.addString(std::string(descr, 0, descr.length() - 1)); + msg.addString(std::string(descr, 0, descr.length() - 1), "ProtocolGame::sendMarketDetail - std::string(descr, 0, descr.length() - 1)"); } else { - msg.addString(descr); + msg.addString(descr, "ProtocolGame::sendMarketDetail - descr"); } } else { msg.add(0x00); @@ -5201,7 +5201,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.decayTime != 0) { std::ostringstream ss; ss << it.decayTime << " seconds"; - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { msg.add(0x00); } @@ -5224,25 +5224,25 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { ss << fmt::format("{} {:+}%", getCombatName(indexToCombatType(i)), it.abilities->absorbPercent[i]); } - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { msg.add(0x00); } if (it.minReqLevel != 0) { - msg.addString(std::to_string(it.minReqLevel)); + msg.addString(std::to_string(it.minReqLevel), "ProtocolGame::sendMarketDetail - std::to_string(it.minReqLevel)"); } else { msg.add(0x00); } if (it.minReqMagicLevel != 0) { - msg.addString(std::to_string(it.minReqMagicLevel)); + msg.addString(std::to_string(it.minReqMagicLevel), "ProtocolGame::sendMarketDetail - std::to_string(it.minReqMagicLevel)"); } else { msg.add(0x00); } - msg.addString(it.vocationString); - msg.addString(it.runeSpellName); + msg.addString(it.vocationString, "ProtocolGame::sendMarketDetail - it.vocationString"); + msg.addString(it.runeSpellName, "ProtocolGame::sendMarketDetail - it.runeSpellName"); if (it.abilities) { std::ostringstream ss; @@ -5319,13 +5319,13 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { ss << fmt::format("speed {:+}", (it.abilities->speed >> 1)); } - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { msg.add(0x00); } if (it.charges != 0) { - msg.addString(std::to_string(it.charges)); + msg.addString(std::to_string(it.charges), "ProtocolGame::sendMarketDetail - std::to_string(it.charges)"); } else { msg.add(0x00); } @@ -5340,7 +5340,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { } } - msg.addString(weaponName); + msg.addString(weaponName, "ProtocolGame::sendMarketDetail - weaponName"); if (it.weight != 0) { std::ostringstream ss; @@ -5354,7 +5354,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { ss << weightString; } ss << " oz"; - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else { msg.add(0x00); } @@ -5364,7 +5364,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { } if (it.imbuementSlot > 0) { - msg.addString(std::to_string(it.imbuementSlot)); + msg.addString(std::to_string(it.imbuementSlot), "ProtocolGame::sendMarketDetail - std::to_string(it.imbuementSlot)"); } else { msg.add(0x00); } @@ -5376,7 +5376,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.abilities->magicShieldCapacityFlat > 0) { string.clear(); string << std::showpos << it.abilities->magicShieldCapacityFlat << std::noshowpos << " and " << it.abilities->magicShieldCapacityPercent << "%"; - msg.addString(string.str()); + msg.addString(string.str(), "ProtocolGame::sendMarketDetail - string.str()"); } else { msg.add(0x00); } @@ -5384,7 +5384,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.abilities->cleavePercent > 0) { string.clear(); string << it.abilities->cleavePercent << "%"; - msg.addString(string.str()); + msg.addString(string.str(), "ProtocolGame::sendMarketDetail - string.str()"); } else { msg.add(0x00); } @@ -5392,7 +5392,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.abilities->reflectFlat[COMBAT_PHYSICALDAMAGE] > 0) { string.clear(); string << it.abilities->reflectFlat[COMBAT_PHYSICALDAMAGE]; - msg.addString(string.str()); + msg.addString(string.str(), "ProtocolGame::sendMarketDetail - string.str()"); } else { msg.add(0x00); } @@ -5400,7 +5400,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { if (it.abilities->perfectShotDamage > 0) { string.clear(); string << std::showpos << it.abilities->perfectShotDamage << std::noshowpos << " at " << it.abilities->perfectShotRange << "%"; - msg.addString(string.str()); + msg.addString(string.str(), "ProtocolGame::sendMarketDetail - string.str()"); } else { msg.add(0x00); } @@ -5418,7 +5418,7 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { // Upgrade and tier detail modifier if (it.upgradeClassification > 0 && tier > 0) { - msg.addString(std::to_string(it.upgradeClassification)); + msg.addString(std::to_string(it.upgradeClassification), "ProtocolGame::sendMarketDetail - std::to_string(it.upgradeClassification)"); std::ostringstream ss; double chance; @@ -5432,10 +5432,10 @@ void ProtocolGame::sendMarketDetail(uint16_t itemId, uint8_t tier) { chance = (0.0307576 * tier * tier) + (0.440697 * tier) + 0.026; ss << fmt::format("{} ({:.2f}% Ruse)", static_cast(tier), chance); } - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendMarketDetail - ss.str()"); } else if (it.upgradeClassification > 0 && tier == 0) { - msg.addString(std::to_string(it.upgradeClassification)); - msg.addString(std::to_string(tier)); + msg.addString(std::to_string(it.upgradeClassification), "ProtocolGame::sendMarketDetail - std::to_string(it.upgradeClassification)"); + msg.addString(std::to_string(tier), "ProtocolGame::sendMarketDetail - std::to_string(tier)"); } else { msg.add(0x00); msg.add(0x00); @@ -5488,7 +5488,7 @@ void ProtocolGame::sendTradeItemRequest(const std::string &traderName, std::shar msg.addByte(0x7E); } - msg.addString(traderName); + msg.addString(traderName, "ProtocolGame::sendTradeItemRequest - traderName"); if (std::shared_ptr tradeContainer = item->getContainer()) { std::list> listContainer { tradeContainer }; @@ -5553,7 +5553,7 @@ void ProtocolGame::sendCreatureSay(std::shared_ptr creature, SpeakClas static uint32_t statementId = 0; msg.add(++statementId); - msg.addString(creature->getName()); + msg.addString(creature->getName(), "ProtocolGame::sendCreatureSay - creature->getName()"); if (!oldProtocol) { msg.addByte(0x00); // Show (Traded) @@ -5578,7 +5578,7 @@ void ProtocolGame::sendCreatureSay(std::shared_ptr creature, SpeakClas msg.addPosition(creature->getPosition()); } - msg.addString(text); + msg.addString(text, "ProtocolGame::sendCreatureSay - text"); writeToOutputBuffer(msg); } @@ -5600,7 +5600,7 @@ void ProtocolGame::sendToChannel(std::shared_ptr creature, SpeakClasse } type = TALKTYPE_CHANNEL_R1; } else { - msg.addString(creature->getName()); + msg.addString(creature->getName(), "ProtocolGame::sendToChannel - creature->getName()"); if (!oldProtocol && statementId != 0) { msg.addByte(0x00); // Show (Traded) } @@ -5620,7 +5620,7 @@ void ProtocolGame::sendToChannel(std::shared_ptr creature, SpeakClasse } msg.add(channelId); - msg.addString(text); + msg.addString(text, "ProtocolGame::sendToChannel - text"); writeToOutputBuffer(msg); } @@ -5630,7 +5630,7 @@ void ProtocolGame::sendPrivateMessage(std::shared_ptr speaker, SpeakClas static uint32_t statementId = 0; msg.add(++statementId); if (speaker) { - msg.addString(speaker->getName()); + msg.addString(speaker->getName(), "ProtocolGame::sendPrivateMessage - speaker->getName()"); if (!oldProtocol && statementId != 0) { msg.addByte(0x00); // Show (Traded) } @@ -5648,7 +5648,7 @@ void ProtocolGame::sendPrivateMessage(std::shared_ptr speaker, SpeakClas msg.addByte(type); } - msg.addString(text); + msg.addString(text, "ProtocolGame::sendPrivateMessage - text"); writeToOutputBuffer(msg); } @@ -5730,7 +5730,7 @@ void ProtocolGame::sendRestingStatus(uint8_t protection) { int32_t PlayerdailyStreak = player->getStorageValue(STORAGEVALUE_DAILYREWARD); msg.addByte(PlayerdailyStreak < 2 ? 0 : 1); if (PlayerdailyStreak < 2) { - msg.addString("Resting Area (no active bonus)"); + msg.addString("Resting Area (no active bonus)", "ProtocolGame::sendRestingStatus - Resting Area (no active bonus)"); } else { std::ostringstream ss; ss << "Active Resting Area Bonuses: "; @@ -5753,7 +5753,7 @@ void ProtocolGame::sendRestingStatus(uint8_t protection) { ss << ",\nSoul Points Regeneration"; } ss << "."; - msg.addString(ss.str()); + msg.addString(ss.str(), "ProtocolGame::sendRestingStatus - ss.str()"); } writeToOutputBuffer(msg); } @@ -5945,7 +5945,7 @@ void ProtocolGame::sendPlayerVocation(std::shared_ptr target) { void ProtocolGame::sendFYIBox(const std::string &message) { NetworkMessage msg; msg.addByte(0x15); - msg.addString(message); + msg.addString(message, "ProtocolGame::sendFYIBox - message"); writeToOutputBuffer(msg); } @@ -6108,7 +6108,7 @@ void ProtocolGame::sendAddCreature(std::shared_ptr creature, const Pos msg.addByte(0x00); // can change pvp framing option msg.addByte(0x00); // expert mode button enabled - msg.addString(g_configManager().getString(STORE_IMAGES_URL, __FUNCTION__)); + msg.addString(g_configManager().getString(STORE_IMAGES_URL, __FUNCTION__), "ProtocolGame::sendAddCreature - g_configManager().getString(STORE_IMAGES_URL)"); msg.add(static_cast(g_configManager().getNumber(STORE_COIN_PACKET, __FUNCTION__))); if (!oldProtocol) { @@ -6351,16 +6351,16 @@ void ProtocolGame::sendTextWindow(uint32_t windowTextId, std::shared_ptr i if (canWrite) { msg.add(maxlen); - msg.addString(item->getAttribute(ItemAttribute_t::TEXT)); + msg.addString(item->getAttribute(ItemAttribute_t::TEXT), "ProtocolGame::sendTextWindow - item->getAttribute(ItemAttribute_t::TEXT)"); } else { const std::string &text = item->getAttribute(ItemAttribute_t::TEXT); msg.add(text.size()); - msg.addString(text); + msg.addString(text, "ProtocolGame::sendTextWindow - text"); } const std::string &writer = item->getAttribute(ItemAttribute_t::WRITER); if (!writer.empty()) { - msg.addString(writer); + msg.addString(writer, "ProtocolGame::sendTextWindow - writer"); } else { msg.add(0x00); } @@ -6371,7 +6371,7 @@ void ProtocolGame::sendTextWindow(uint32_t windowTextId, std::shared_ptr i auto writtenDate = item->getAttribute(ItemAttribute_t::DATE); if (writtenDate != 0) { - msg.addString(formatDateShort(writtenDate)); + msg.addString(formatDateShort(writtenDate), "ProtocolGame::sendTextWindow - formatDateShort(writtenDate)"); } else { msg.add(0x00); } @@ -6385,7 +6385,7 @@ void ProtocolGame::sendTextWindow(uint32_t windowTextId, uint32_t itemId, const msg.add(windowTextId); AddItem(msg, itemId, 1, 0); msg.add(text.size()); - msg.addString(text); + msg.addString(text, "ProtocolGame::sendTextWindow - text"); msg.add(0x00); if (!oldProtocol) { @@ -6401,7 +6401,7 @@ void ProtocolGame::sendHouseWindow(uint32_t windowTextId, const std::string &tex msg.addByte(0x97); msg.addByte(0x00); msg.add(windowTextId); - msg.addString(text); + msg.addString(text, "ProtocolGame::sendHouseWindow - text"); writeToOutputBuffer(msg); } @@ -6448,7 +6448,7 @@ void ProtocolGame::sendOutfitWindow() { msg.addByte(protocolOutfits.size()); for (const ProtocolOutfit &outfit : protocolOutfits) { msg.add(outfit.lookType); - msg.addString(outfit.name); + msg.addString(outfit.name, "ProtocolGame::sendOutfitWindow - outfit.name"); msg.addByte(outfit.addons); } @@ -6462,7 +6462,7 @@ void ProtocolGame::sendOutfitWindow() { msg.addByte(mounts.size()); for (const auto mount : mounts) { msg.add(mount->clientId); - msg.addString(mount->name); + msg.addString(mount->name, "ProtocolGame::sendOutfitWindow - mount->name"); } writeToOutputBuffer(msg); @@ -6493,19 +6493,19 @@ void ProtocolGame::sendOutfitWindow() { if (player->isAccessPlayer()) { msg.add(75); - msg.addString("Gamemaster"); + msg.addString("Gamemaster", "ProtocolGame::sendOutfitWindow - Gamemaster"); msg.addByte(0); msg.addByte(0x00); ++outfitSize; msg.add(266); - msg.addString("Customer Support"); + msg.addString("Customer Support", "ProtocolGame::sendOutfitWindow - Customer Support"); msg.addByte(0); msg.addByte(0x00); ++outfitSize; msg.add(302); - msg.addString("Community Manager"); + msg.addString("Community Manager", "ProtocolGame::sendOutfitWindow - Community Manager"); msg.addByte(0); msg.addByte(0x00); ++outfitSize; @@ -6517,25 +6517,25 @@ void ProtocolGame::sendOutfitWindow() { uint8_t addons; if (player->getOutfitAddons(outfit, addons)) { msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendOutfitWindow - outfit->name"); msg.addByte(addons); msg.addByte(0x00); ++outfitSize; } else if (outfit->lookType == 1210 || outfit->lookType == 1211) { msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendOutfitWindow - outfit->name"); msg.addByte(3); msg.addByte(0x02); ++outfitSize; } else if (outfit->lookType == 1456 || outfit->lookType == 1457) { msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendOutfitWindow - outfit->name"); msg.addByte(3); msg.addByte(0x03); ++outfitSize; } else if (outfit->from == "store") { msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendOutfitWindow - outfit->name"); msg.addByte(outfit->lookType >= 962 && outfit->lookType <= 975 ? 0 : 3); msg.addByte(0x01); msg.add(0x00); @@ -6561,12 +6561,12 @@ void ProtocolGame::sendOutfitWindow() { for (const auto mount : mounts) { if (player->hasMount(mount)) { msg.add(mount->clientId); - msg.addString(mount->name); + msg.addString(mount->name, "ProtocolGame::sendOutfitWindow - mount->name"); msg.addByte(0x00); ++mountSize; } else if (mount->type == "store") { msg.add(mount->clientId); - msg.addString(mount->name); + msg.addString(mount->name, "ProtocolGame::sendOutfitWindow - mount->name"); msg.addByte(0x01); msg.add(0x00); ++mountSize; @@ -6595,7 +6595,7 @@ void ProtocolGame::sendOutfitWindow() { } msg.add(familiar.lookType); - msg.addString(familiar.name); + msg.addString(familiar.name, "ProtocolGame::sendOutfitWindow - familiar.name"); msg.addByte(0x00); if (++familiarSize == limitFamiliars) { break; @@ -6659,7 +6659,7 @@ void ProtocolGame::sendPodiumWindow(std::shared_ptr podium, const Position } msg.add(outfit->lookType); - msg.addString(outfit->name); + msg.addString(outfit->name, "ProtocolGame::sendPodiumWindow - outfit->name"); msg.addByte(addons); msg.addByte(0x00); if (++outfitSize == limitOutfits) { @@ -6681,7 +6681,7 @@ void ProtocolGame::sendPodiumWindow(std::shared_ptr podium, const Position for (const auto mount : mounts) { if (player->hasMount(mount)) { msg.add(mount->clientId); - msg.addString(mount->name); + msg.addString(mount->name, "ProtocolGame::sendPodiumWindow - mount->name"); msg.addByte(0x00); if (++mountSize == limitMounts) { break; @@ -6731,8 +6731,8 @@ void ProtocolGame::sendVIP(uint32_t guid, const std::string &name, const std::st NetworkMessage msg; msg.addByte(0xD2); msg.add(guid); - msg.addString(name); - msg.addString(description); + msg.addString(name, "ProtocolGame::sendVIP - name"); + msg.addString(description, "ProtocolGame::sendVIP - description"); msg.add(std::min(10, icon)); msg.addByte(notify ? 0x01 : 0x00); msg.addByte(status); @@ -6829,7 +6829,7 @@ void ProtocolGame::sendPreyData(const std::unique_ptr &slot) { // Empty } else if (slot->state == PreyDataState_Active) { if (const auto mtype = g_monsters().getMonsterTypeByRaceId(slot->selectedRaceId)) { - msg.addString(mtype->name); + msg.addString(mtype->name, "ProtocolGame::sendPreyData - mtype->name"); const Outfit_t outfit = mtype->info.outfit; msg.add(outfit.lookType); if (outfit.lookType == 0) { @@ -6855,7 +6855,7 @@ void ProtocolGame::sendPreyData(const std::unique_ptr &slot) { continue; } - msg.addString(mtype->name); + msg.addString(mtype->name, "ProtocolGame::sendPreyData - mtype->name"); const Outfit_t outfit = mtype->info.outfit; msg.add(outfit.lookType); if (outfit.lookType == 0) { @@ -6880,7 +6880,7 @@ void ProtocolGame::sendPreyData(const std::unique_ptr &slot) { continue; } - msg.addString(mtype->name); + msg.addString(mtype->name, "ProtocolGame::sendPreyData - mtype->name"); const Outfit_t outfit = mtype->info.outfit; msg.add(outfit.lookType); if (outfit.lookType == 0) { @@ -6948,18 +6948,18 @@ void ProtocolGame::sendModalWindow(const ModalWindow &modalWindow) { msg.addByte(0xFA); msg.add(modalWindow.id); - msg.addString(modalWindow.title); - msg.addString(modalWindow.message); + msg.addString(modalWindow.title, "ProtocolGame::sendModalWindow - modalWindow.title"); + msg.addString(modalWindow.message, "ProtocolGame::sendModalWindow - modalWindow.message"); msg.addByte(modalWindow.buttons.size()); for (const auto &it : modalWindow.buttons) { - msg.addString(it.first); + msg.addString(it.first, "ProtocolGame::sendModalWindow - it.first"); msg.addByte(it.second); } msg.addByte(modalWindow.choices.size()); for (const auto &it : modalWindow.choices) { - msg.addString(it.first); + msg.addString(it.first, "ProtocolGame::sendModalWindow - it.first"); msg.addByte(it.second); } @@ -6997,9 +6997,9 @@ void ProtocolGame::AddCreature(NetworkMessage &msg, std::shared_ptr cr } if (!oldProtocol && creature->isHealthHidden()) { - msg.addString(""); + msg.addString("", "ProtocolGame::AddCreature - empty"); } else { - msg.addString(creature->getName()); + msg.addString(creature->getName(), "ProtocolGame::AddCreature - creature->getName()"); } } @@ -7218,9 +7218,10 @@ void ProtocolGame::addImbuementInfo(NetworkMessage &msg, uint16_t imbuementId) c const CategoryImbuement* categoryImbuement = g_imbuements().getCategoryByID(imbuement->getCategory()); msg.add(imbuementId); - msg.addString(baseImbuement->name + " " + imbuement->getName()); - msg.addString(imbuement->getDescription()); - msg.addString(categoryImbuement->name + imbuement->getSubGroup()); + msg.addString(baseImbuement->name + " " + imbuement->getName(), "ProtocolGame::addImbuementInfo - baseImbuement->name + " + " + imbuement->getName()"); + msg.addString(imbuement->getDescription(), "ProtocolGame::addImbuementInfo - imbuement->getDescription()"); + msg.addString(categoryImbuement->name + imbuement->getSubGroup(), "ProtocolGame::addImbuementInfo - categoryImbuement->name + imbuement->getSubGroup()"); msg.add(imbuement->getIconID()); msg.add(baseImbuement->duration); @@ -7233,7 +7234,7 @@ void ProtocolGame::addImbuementInfo(NetworkMessage &msg, uint16_t imbuementId) c for (const auto &itm : items) { const ItemType &it = Item::items[itm.first]; msg.add(itm.first); - msg.addString(it.name); + msg.addString(it.name, "ProtocolGame::addImbuementInfo - it.name"); msg.add(itm.second); } @@ -7305,7 +7306,7 @@ void ProtocolGame::sendMessageDialog(const std::string &message) { NetworkMessage msg; msg.addByte(0xED); msg.addByte(0x14); // Unknown type - msg.addString(message); + msg.addString(message, "ProtocolGame::sendMessageDialog - message"); writeToOutputBuffer(msg); } @@ -7313,7 +7314,7 @@ void ProtocolGame::sendImbuementResult(const std::string message) { NetworkMessage msg; msg.addByte(0xED); msg.addByte(0x01); - msg.addString(message); + msg.addString(message, "ProtocolGame::sendImbuementResult - message"); writeToOutputBuffer(msg); } @@ -7374,7 +7375,7 @@ void ProtocolGame::updatePartyTrackerAnalyzer(const std::shared_ptr party msg.addByte(static_cast(party->membersData.size())); for (const std::shared_ptr analyzer : party->membersData) { msg.add(analyzer->id); - msg.addString(analyzer->name); + msg.addString(analyzer->name, "ProtocolGame::updatePartyTrackerAnalyzer - analyzer->name"); } } @@ -7410,7 +7411,7 @@ void ProtocolGame::sendKillTrackerUpdate(std::shared_ptr corpse, cons NetworkMessage msg; msg.addByte(0xD1); - msg.addString(name); + msg.addString(name, "ProtocolGame::sendKillTrackerUpdate - name"); msg.add(creatureOutfit.lookType ? creatureOutfit.lookType : 21); msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookHead : 0x00); msg.addByte(creatureOutfit.lookType ? creatureOutfit.lookBody : 0x00); @@ -7478,7 +7479,7 @@ void ProtocolGame::sendUpdateInputAnalyzer(CombatType_t type, int32_t amount, st msg.addByte(ANALYZER_DAMAGE_RECEIVED); msg.add(amount); msg.addByte(clientElement); - msg.addString(target); + msg.addString(target, "ProtocolGame::sendUpdateInputAnalyzer - target"); writeToOutputBuffer(msg); } @@ -7642,7 +7643,7 @@ void ProtocolGame::AddHiddenShopItem(NetworkMessage &msg) { // Empty bytes from AddShopItem msg.add(0); msg.addByte(0); - msg.addString(std::string()); + msg.addString(std::string(), "ProtocolGame::AddHiddenShopItem - std::string()"); msg.add(0); msg.add(0); msg.add(0); @@ -7677,9 +7678,9 @@ void ProtocolGame::AddShopItem(NetworkMessage &msg, const ShopBlock &shopBlock) // If not send "itemName" variable from the npc shop, will registered the name that is in items.xml if (shopBlock.itemName.empty()) { - msg.addString(it.name); + msg.addString(it.name, "ProtocolGame::AddShopItem - it.name"); } else { - msg.addString(shopBlock.itemName); + msg.addString(shopBlock.itemName, "ProtocolGame::AddShopItem - shopBlock.itemName"); } msg.add(it.weight); msg.add(shopBlock.itemBuyPrice == 4294967295 ? 0 : shopBlock.itemBuyPrice); @@ -7761,7 +7762,8 @@ void ProtocolGame::sendInventoryImbuements(const std::mapgetBaseID()); msg.addByte(0x01); - msg.addString(baseImbuement->name + " " + imbuement->getName()); + msg.addString(baseImbuement->name + " " + imbuement->getName(), "ProtocolGame::sendInventoryImbuements - baseImbuement->name + " + " + imbuement->getName()"); msg.add(imbuement->getIconID()); msg.add(imbuementInfo.duration); @@ -8354,9 +8356,9 @@ void ProtocolGame::sendPodiumDetails(NetworkMessage &msg, const std::vectorname); + msg.addString(mType->name, "ProtocolGame::sendPodiumDetails - mType->name"); } msg.add(monsterOutfit.lookType); if (isLookType) { diff --git a/src/server/network/protocol/protocollogin.cpp b/src/server/network/protocol/protocollogin.cpp index 1b8753b01d3..53dc9f05126 100644 --- a/src/server/network/protocol/protocollogin.cpp +++ b/src/server/network/protocol/protocollogin.cpp @@ -22,7 +22,7 @@ void ProtocolLogin::disconnectClient(const std::string &message) { auto output = OutputMessagePool::getOutputMessage(); output->addByte(0x0B); - output->addString(message); + output->addString(message, "ProtocolLogin::disconnectClient - message"); send(output); disconnect(); @@ -56,12 +56,12 @@ void ProtocolLogin::getCharacterList(const std::string &accountDescriptor, const std::ostringstream ss; ss << g_game().getMotdNum() << "\n" << motd; - output->addString(ss.str()); + output->addString(ss.str(), "ProtocolLogin::getCharacterList - ss.str()"); } // Add session key output->addByte(0x28); - output->addString(accountDescriptor + "\n" + password); + output->addString(accountDescriptor + "\n" + password, "ProtocolLogin::getCharacterList - accountDescriptor + password"); // Add char list auto [players, result] = account.getAccountPlayers(); @@ -74,8 +74,8 @@ void ProtocolLogin::getCharacterList(const std::string &accountDescriptor, const output->addByte(1); // number of worlds output->addByte(0); // world id - output->addString(g_configManager().getString(SERVER_NAME, __FUNCTION__)); - output->addString(g_configManager().getString(IP, __FUNCTION__)); + output->addString(g_configManager().getString(SERVER_NAME, __FUNCTION__), "ProtocolLogin::getCharacterList - _configManager().getString(SERVER_NAME)"); + output->addString(g_configManager().getString(IP, __FUNCTION__), "ProtocolLogin::getCharacterList - g_configManager().getString(IP)"); output->add(g_configManager().getNumber(GAME_PORT, __FUNCTION__)); @@ -85,7 +85,7 @@ void ProtocolLogin::getCharacterList(const std::string &accountDescriptor, const output->addByte(size); for (const auto &[name, deletion] : players) { output->addByte(0); - output->addString(name); + output->addString(name, "ProtocolLogin::getCharacterList - name"); } // Add premium days diff --git a/src/server/network/protocol/protocolstatus.cpp b/src/server/network/protocol/protocolstatus.cpp index 50522fc003d..23b6b0b2498 100644 --- a/src/server/network/protocol/protocolstatus.cpp +++ b/src/server/network/protocol/protocolstatus.cpp @@ -154,22 +154,22 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact if (requestedInfo & REQUEST_BASIC_SERVER_INFO) { output->addByte(0x10); - output->addString(g_configManager().getString(ConfigKey_t::SERVER_NAME, __FUNCTION__)); - output->addString(g_configManager().getString(IP, __FUNCTION__)); - output->addString(std::to_string(g_configManager().getNumber(LOGIN_PORT, __FUNCTION__))); + output->addString(g_configManager().getString(ConfigKey_t::SERVER_NAME, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(stringConfig_t::SERVER_NAME)"); + output->addString(g_configManager().getString(IP, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(IP)"); + output->addString(std::to_string(g_configManager().getNumber(LOGIN_PORT, __FUNCTION__)), "ProtocolStatus::sendInfo - std::to_string(g_configManager().getNumber(LOGIN_PORT))"); } if (requestedInfo & REQUEST_OWNER_SERVER_INFO) { output->addByte(0x11); - output->addString(g_configManager().getString(OWNER_NAME, __FUNCTION__)); - output->addString(g_configManager().getString(OWNER_EMAIL, __FUNCTION__)); + output->addString(g_configManager().getString(OWNER_NAME, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(OWNER_NAME)"); + output->addString(g_configManager().getString(OWNER_EMAIL, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(OWNER_EMAIL)"); } if (requestedInfo & REQUEST_MISC_SERVER_INFO) { output->addByte(0x12); - output->addString(g_configManager().getString(SERVER_MOTD, __FUNCTION__)); - output->addString(g_configManager().getString(LOCATION, __FUNCTION__)); - output->addString(g_configManager().getString(URL, __FUNCTION__)); + output->addString(g_configManager().getString(SERVER_MOTD, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(SERVER_MOTD)"); + output->addString(g_configManager().getString(LOCATION, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(LOCATION)"); + output->addString(g_configManager().getString(URL, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(URL)"); output->add((OTSYS_TIME() - ProtocolStatus::start) / 1000); } @@ -182,8 +182,8 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact if (requestedInfo & REQUEST_MAP_INFO) { output->addByte(0x30); - output->addString(g_configManager().getString(MAP_NAME, __FUNCTION__)); - output->addString(g_configManager().getString(MAP_AUTHOR, __FUNCTION__)); + output->addString(g_configManager().getString(MAP_NAME, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(MAP_NAME)"); + output->addString(g_configManager().getString(MAP_AUTHOR, __FUNCTION__), "ProtocolStatus::sendInfo - g_configManager().getString(MAP_AUTHOR)"); uint32_t mapWidth, mapHeight; g_game().getMapDimensions(mapWidth, mapHeight); output->add(mapWidth); @@ -196,7 +196,7 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact const auto players = g_game().getPlayers(); output->add(players.size()); for (const auto &it : players) { - output->addString(it.second->getName()); + output->addString(it.second->getName(), "ProtocolStatus::sendInfo - it.second->getName()"); output->add(it.second->getLevel()); } } @@ -212,9 +212,9 @@ void ProtocolStatus::sendInfo(uint16_t requestedInfo, const std::string &charact if (requestedInfo & REQUEST_SERVER_SOFTWARE_INFO) { output->addByte(0x23); // server software info - output->addString(ProtocolStatus::SERVER_NAME); - output->addString(ProtocolStatus::SERVER_VERSION); - output->addString(fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER)); + output->addString(ProtocolStatus::SERVER_NAME, "ProtocolStatus::sendInfo - ProtocolStatus::SERVER_NAME"); + output->addString(ProtocolStatus::SERVER_VERSION, "ProtocolStatus::sendInfo - ProtocolStatus::SERVER_VERSION)"); + output->addString(fmt::format("{}.{}", CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER), "ProtocolStatus::sendInfo - fmt::format(CLIENT_VERSION_UPPER, CLIENT_VERSION_LOWER)"); } send(output); disconnect(); diff --git a/src/server/server.hpp b/src/server/server.hpp index 1760fdafa85..ae6f805e4ff 100644 --- a/src/server/server.hpp +++ b/src/server/server.hpp @@ -10,6 +10,7 @@ #pragma once #include "lib/logging/logger.hpp" +#include "lib/metrics/metrics.hpp" #include "server/network/connection/connection.hpp" #include "server/signals.hpp" diff --git a/vcpkg.json b/vcpkg.json index dfe6b261241..7201d4a5556 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,25 +2,30 @@ "name": "canary", "version-string": "1.0.0", "dependencies": [ + "abseil", "argon2", "asio", "bext-di", "bext-ut", - "eventpp", - "pugixml", - "spdlog", "curl", + "eventpp", "jsoncpp", - "protobuf", - "parallel-hashmap", + "luajit", "magic-enum", - "zlib", "mio", - "luajit", - "abseil", + { + "name": "opentelemetry-cpp", + "default-features": true, + "features": ["otlp-http", "prometheus"] + }, + "parallel-hashmap", + "protobuf", + "pugixml", + "spdlog", + "zlib", { "name": "libmariadb", - "features": [ "mariadbclient" ] + "features": ["mariadbclient"] }, { "name": "gmp", @@ -31,5 +36,5 @@ "platform": "windows" } ], - "builtin-baseline": "c9fa965c2a1b1334469b4539063f3ce95383653c" + "builtin-baseline": "98a562a04cd03728f399e79e1b37bcccb5a69b37" } diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj index bdb891be8df..e387dbddbea 100644 --- a/vcproj/canary.vcxproj +++ b/vcproj/canary.vcxproj @@ -108,6 +108,7 @@ + @@ -128,6 +129,7 @@ + @@ -136,6 +138,7 @@ + @@ -303,6 +306,7 @@ + @@ -325,6 +329,7 @@ + @@ -502,6 +507,8 @@ true true true + Default + /Zc:__cplusplus %(AdditionalOptions) Console @@ -534,6 +541,7 @@ true true true + /Zc:__cplusplus %(AdditionalOptions) Console @@ -551,4 +559,4 @@ - + \ No newline at end of file diff --git a/vcproj/settings.props b/vcproj/settings.props index 9f01a78902b..1f7471f48c6 100644 --- a/vcproj/settings.props +++ b/vcproj/settings.props @@ -24,6 +24,15 @@ jsoncpp.lib; abseil_dll.lib; argon2.lib; + opentelemetry_common.lib; + opentelemetry_resources.lib; + opentelemetry_metrics.lib; + opentelemetry_exporter_ostream_metrics.lib; + opentelemetry_exporter_prometheus.lib; + prometheus-cpp-core.lib; + prometheus-cpp-pull.lib; + civetweb.lib; + civetweb-cpp.lib comctl32.lib; @@ -41,6 +50,15 @@ jsoncpp.lib; abseil_dll.lib; argon2.lib; + opentelemetry_common.lib; + opentelemetry_resources.lib; + opentelemetry_metrics.lib; + opentelemetry_exporter_ostream_metrics.lib; + opentelemetry_exporter_prometheus.lib; + prometheus-cpp-core.lib; + prometheus-cpp-pull.lib; + civetweb.lib; + civetweb-cpp.lib