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 1023fd37d5a..00d1ae72c2a 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -486,3 +486,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/scripts/spawn_zones/catacombs.lua b/data-otservbr-global/scripts/spawn_zones/catacombs.lua new file mode 100644 index 00000000000..84a1e815f87 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/catacombs.lua @@ -0,0 +1,50 @@ +-- +-- +-- + +local minos = SpawnZone("spawn.catacombs.minos") + +minos:setMonstersPerCluster(2, 3) +minos:setClusterRadius(4, 7) +minos:setClusterSpacing(12, 14) +minos:setOutlierChance(0.5) +minos:setPeriod("180s") + +minos:addMonster("Minotaur Amazon", 10) +minos:addMonster("Execowtioner", 30) +minos:addMonster("Moohtant", 30) + +minos:register() + +local golems = SpawnZone("spawn.catacombs.golems") + +golems:setMonstersPerCluster(2, 3) +golems:setClusterRadius(4, 7) +golems:setClusterSpacing(12, 14) +golems:setOutlierChance(0.5) +golems:setPeriod("180s") + +golems:addMonster("Glooth Golem", 15) +golems:addMonster("Rustheap Golem", 25) +golems:addMonster("Worker Golem", 35) +golems:addMonster("War Golem", 20) + +golems:register() + +local demons = SpawnZone("spawn.catacombs.demons") + +demons:setMonstersPerCluster(2, 3) +demons:setClusterRadius(4, 7) +demons:setClusterSpacing(12, 14) +demons:setOutlierChance(0.5) +demons:setPeriod("180s") + +demons:addMonster("Destroyer", 21) +demons:addMonster("Hellspawn", 22) +demons:addMonster("Grim Reaper", 18) +demons:addMonster("Dark Torturer", 28) +demons:addMonster("Demon", 12) +demons:addMonster("Hellhound", 2) +demons:addMonster("Juggernaut", 2) + +demons:register() diff --git a/data-otservbr-global/scripts/spawn_zones/coryms.lua b/data-otservbr-global/scripts/spawn_zones/coryms.lua new file mode 100644 index 00000000000..8172050d74d --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/coryms.lua @@ -0,0 +1,27 @@ +local coryms1 = SpawnZone("spawn.venore.coryms-1") + +coryms1:setMonstersPerCluster(3, 4) +coryms1:setClusterRadius(2, 4) +coryms1:setClusterSpacing(8, 10) +coryms1:setOutlierChance(0.5) +coryms1:setPeriod("180s") + +coryms1:addMonster("Corym Charlatan", 45) +coryms1:addMonster("Corym Skirmisher", 30) +coryms1:addMonster("Corym Vanguard", 10) + +coryms1:register() + +local coryms2 = SpawnZone("spawn.venore.coryms-2") + +coryms2:setMonstersPerCluster(3, 4) +coryms2:setClusterRadius(2, 4) +coryms2:setClusterSpacing(7, 10) +coryms2:setOutlierChance(1.5) +coryms2:setPeriod("180s") + +coryms2:addMonster("Corym Charlatan", 30) +coryms2:addMonster("Corym Skirmisher", 32) +coryms2:addMonster("Corym Vanguard", 20) + +coryms2:register() diff --git a/data-otservbr-global/scripts/spawn_zones/falcon.lua b/data-otservbr-global/scripts/spawn_zones/falcon.lua new file mode 100644 index 00000000000..718324c6bb2 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/falcon.lua @@ -0,0 +1,12 @@ +local falcon = SpawnZone("spawn.falcon") + +falcon:setMonstersPerCluster(3, 4) +falcon:setClusterRadius(2, 4) +falcon:setClusterSpacing(10, 12) +falcon:setOutlierChance(0.5) +falcon:setPeriod("180s") + +falcon:addMonster("Falcon Knight", 2) +falcon:addMonster("Falcon Paladin", 2) + +falcon:register() diff --git a/data-otservbr-global/scripts/spawn_zones/ingol.lua b/data-otservbr-global/scripts/spawn_zones/ingol.lua new file mode 100644 index 00000000000..e0f48c7a564 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/ingol.lua @@ -0,0 +1,33 @@ +local surface = SpawnZone("spawn.ingol.surface") + +surface:setMonstersPerCluster(2, 3) +surface:setClusterRadius(2, 4) +surface:setClusterSpacing(12, 14) +surface:setOutlierChance(0.5) +surface:setPeriod("180s") + +surface:addMonster("Carnivostrich", 4) +surface:addMonster("Harpy", 13) +surface:addMonster("Liodile", 4) +surface:addMonster("Rhindeer", 11) +surface:addMonster("Crape Man", 12) +surface:addMonster("Boar Man", 9) + +surface:register() + +local deep = SpawnZone("spawn.ingol.deep") + +deep:setMonstersPerCluster(3, 4) +deep:setClusterRadius(2, 4) +deep:setClusterSpacing(10, 12) +deep:setOutlierChance(0.5) +deep:setPeriod("180s") + +deep:addMonster("Carnivostrich", 9) +deep:addMonster("Harpy", 9) +deep:addMonster("Liodile", 11) +deep:addMonster("Rhindeer", 8) +deep:addMonster("Crape Man", 7) +deep:addMonster("Boar Man", 12) + +deep:register() diff --git a/data-otservbr-global/scripts/spawn_zones/issavi.lua b/data-otservbr-global/scripts/spawn_zones/issavi.lua new file mode 100644 index 00000000000..b7467983d29 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/issavi.lua @@ -0,0 +1,34 @@ +local goannas = SpawnZone("spawn.issavi-south") + +goannas:setMonstersPerCluster(3, 4) +goannas:setClusterRadius(2, 4) +goannas:setClusterSpacing(9, 12) +goannas:setOutlierChance(0.7) +goannas:setPeriod("180s") + +goannas:addMonster("Adult Goanna", 28) +goannas:addMonster("Young Goanna", 30) +goannas:addMonster("Feral Sphinx", 26) +goannas:addMonster("Manticore", 11) +goannas:addMonster("Scorpion", 2) +goannas:addMonster("Hyaena", 1) +goannas:addMonster("Cobra", 2) + +goannas:register() + +local sphinx = SpawnZone("spawn.issavi-sphinx") + +sphinx:setMonstersPerCluster(3, 4) +sphinx:setClusterRadius(2, 4) +sphinx:setClusterSpacing(9, 12) +sphinx:setOutlierChance(0.7) +sphinx:setPeriod("180s") + +sphinx:addMonster("Sphinx", 28) +sphinx:addMonster("Lamassu", 30) +sphinx:addMonster("Feral Sphinx", 2) +sphinx:addMonster("Manticore", 0.5) +sphinx:addMonster("Young Goanna", 0.5) +sphinx:addMonster("Cobra", 2) + +sphinx:register() diff --git a/data-otservbr-global/scripts/spawn_zones/jakundaf-souls.lua b/data-otservbr-global/scripts/spawn_zones/jakundaf-souls.lua new file mode 100644 index 00000000000..ab0453f62c7 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/jakundaf-souls.lua @@ -0,0 +1,40 @@ +local soulsMinus1 = SpawnZone("spawn.jakundaf.souls-1") + +soulsMinus1:setMonstersPerCluster(3, 4) +soulsMinus1:setClusterRadius(2, 4) +soulsMinus1:setClusterSpacing(9, 11) +soulsMinus1:setOutlierChance(0.5) +soulsMinus1:setPeriod("180s") + +soulsMinus1:addMonster("Flimsy Lost Soul", 87) +soulsMinus1:addMonster("Grim Reaper", 13) + +soulsMinus1:register() + +local soundsMinus2 = SpawnZone("spawn.jakundaf.souls-2") + +soundsMinus2:setMonstersPerCluster(3, 4) +soundsMinus2:setClusterRadius(2, 4) +soundsMinus2:setClusterSpacing(10, 13) +soundsMinus2:setOutlierChance(0.5) +soundsMinus2:setPeriod("180s") + +soundsMinus2:addMonster("Flimsy Lost Soul", 50) +soundsMinus2:addMonster("Mean Lost Soul", 37) +soundsMinus2:addMonster("Grim Reaper", 13) + +soundsMinus2:register() + +local soulsMinus3 = SpawnZone("spawn.jakundaf.souls-3") + +soulsMinus3:setMonstersPerCluster(3, 5) +soulsMinus3:setClusterRadius(2, 4) +soulsMinus3:setClusterSpacing(10, 13) +soulsMinus3:setOutlierChance(0.5) +soulsMinus3:setPeriod("180s") + +soulsMinus3:addMonster("Flimsy Lost Soul", 40) +soulsMinus3:addMonster("Mean Lost Soul", 37) +soulsMinus3:addMonster("Freakish Lost Soul", 23) + +soulsMinus3:register() diff --git a/data-otservbr-global/scripts/spawn_zones/laguna.lua b/data-otservbr-global/scripts/spawn_zones/laguna.lua new file mode 100644 index 00000000000..78bd2ff7148 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/laguna.lua @@ -0,0 +1,47 @@ +local surface = SpawnZone("spawn.laguna.surface") + +surface:setMonstersPerCluster(2, 4) +surface:setClusterRadius(2, 4) +surface:setClusterSpacing(8, 10) +surface:setOutlierChance(0.5) +surface:setPeriod("180s") + +surface:addMonster("Tortoise", 28) +surface:addMonster("Toad", 20) +surface:addMonster("Crab", 12) +surface:addMonster("Crimson Frog", 12) +surface:addMonster("Azure Frog", 11) +surface:addMonster("Coral Frog", 11) +surface:addMonster("Green Frog", 1) +surface:addMonster("Seagull", 1) + +surface:register() + +local turtles = SpawnZone("spawn.laguna.turtles") + +turtles:setMonstersPerCluster(3, 4) +turtles:setClusterRadius(3, 4) +turtles:setClusterSpacing(9, 11) +turtles:setOutlierChance(0.5) +turtles:setPeriod("180s") + +turtles:addMonster("Mutant Turtle", 25) +turtles:addMonster("Ninja Turtle", 15) +turtles:addMonster("Shelled Spiker", 25) +turtles:addMonster("Shelled Crusher", 15) + +turtles:register() + +local crabs = SpawnZone("spawn.laguna.crabs") + +crabs:setMonstersPerCluster(3, 4) +crabs:setClusterRadius(3, 4) +crabs:setClusterSpacing(9, 11) +crabs:setOutlierChance(0.5) +crabs:setPeriod("180s") + +crabs:addMonster("Crimson Crab", 35) +crabs:addMonster("Laguna Lobster", 15) +crabs:addMonster("Slime Hopper", 20) + +crabs:register() diff --git a/data-otservbr-global/scripts/spawn_zones/naga.lua b/data-otservbr-global/scripts/spawn_zones/naga.lua new file mode 100644 index 00000000000..8704df0e5bc --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/naga.lua @@ -0,0 +1,27 @@ +local minus1 = SpawnZone("spawn.naga-1") + +minus1:setMonstersPerCluster(2, 3) +minus1:setClusterRadius(2, 3) +minus1:setClusterSpacing(10, 12) +minus1:setOutlierChance(0.5) +minus1:setPeriod("180s") + +minus1:addMonster("Naga Warrior", 2) +minus1:addMonster("Naga Archer", 2) +minus1:addMonster("Makara", 1) + +minus1:register() + +local minus2 = SpawnZone("spawn.naga-2") + +minus2:setMonstersPerCluster(3, 4) +minus2:setClusterRadius(2, 3) +minus2:setClusterSpacing(9, 11) +minus2:setOutlierChance(0.5) +minus2:setPeriod("180s") + +minus2:addMonster("Naga Warrior", 2) +minus2:addMonster("Naga Archer", 2) +minus2:addMonster("Makara", 1) + +minus2:register() diff --git a/data-otservbr-global/scripts/spawn_zones/nargor.lua b/data-otservbr-global/scripts/spawn_zones/nargor.lua new file mode 100644 index 00000000000..86d4a1085d2 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/nargor.lua @@ -0,0 +1,29 @@ +local pirates = SpawnZone("spawn.nargor.pirates") + +pirates:setMonstersPerCluster(3, 4) +pirates:setClusterRadius(4, 6) +pirates:setClusterSpacing(7, 9) +pirates:setOutlierChance(0.5) +pirates:setPeriod("180s") + +pirates:addMonster("Bootleg Marauder", 28) +pirates:addMonster("Bootleg Buccaneer", 10) +pirates:addMonster("Bootleg Cutthroat", 26) +pirates:addMonster("Bootleg Corsair", 11) +pirates:addMonster("Bootleg Smuggler", 7) + +pirates:register() + +local undead = SpawnZone("spawn.nargor.undeads") + +undead:setMonstersPerCluster(3, 4) +undead:setClusterRadius(4, 6) +undead:setClusterSpacing(7, 9) +undead:setOutlierChance(0.5) +undead:setPeriod("180s") + +undead:addMonster("Bootleg Ghost", 13) +undead:addMonster("Bootleg Skeleton", 11) +undead:addMonster("Fetid Ghoul", 9) + +undead:register() diff --git a/data-otservbr-global/scripts/spawn_zones/nightmares.lua b/data-otservbr-global/scripts/spawn_zones/nightmares.lua new file mode 100644 index 00000000000..94225d6a0d5 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/nightmares.lua @@ -0,0 +1,12 @@ +local nightmares = SpawnZone("spawn.yalahar.nightmare-dungeon") + +nightmares:setMonstersPerCluster(2, 4) +nightmares:setClusterRadius(3, 4) +nightmares:setClusterSpacing(9, 11) +nightmares:setOutlierChance(0.6) +nightmares:setPeriod("180s") + +nightmares:addMonster("Nightmare", 50) +nightmares:addMonster("Nightmare Scion", 10) + +nightmares:register() diff --git a/data-otservbr-global/scripts/spawn_zones/oskayaat.lua b/data-otservbr-global/scripts/spawn_zones/oskayaat.lua new file mode 100644 index 00000000000..504ef880cd0 --- /dev/null +++ b/data-otservbr-global/scripts/spawn_zones/oskayaat.lua @@ -0,0 +1,81 @@ +local west0 = SpawnZone("spawn.oskayaat.west.surface") + +west0:setMonstersPerCluster(1, 2) +west0:setClusterRadius(4, 5) +west0:setClusterSpacing(8, 10) +west0:setOutlierChance(1.5) +west0:setPeriod("240s") + +west0:addMonster("Tiger", 2) +west0:addMonster("White Tiger", 1) + +west0:register() + +local east0 = SpawnZone("spawn.oskayaat.east.surface") + +east0:setMonstersPerCluster(1, 2) +east0:setClusterRadius(4, 5) +east0:setClusterSpacing(8, 10) +east0:setOutlierChance(1.5) +east0:setPeriod("240s") + +east0:addMonster("Scorpion", 2) +east0:addMonster("Cobra", 1) + +east0:register() + +local westMinus1 = SpawnZone("spawn.oskayaat.west-1") + +westMinus1:setMonstersPerCluster(3, 4) +westMinus1:setClusterRadius(1, 4) +westMinus1:setClusterSpacing(8, 10) +westMinus1:setOutlierChance(0.5) +westMinus1:setPeriod("240s") + +westMinus1:addMonster("Werepanther", 4) +westMinus1:addMonster("Werecrocodile", 2) +westMinus1:addMonster("Feral Werecrocodile", 1) + +westMinus1:register() + +local westMinus2 = SpawnZone("spawn.oskayaat.west-2") + +westMinus2:setMonstersPerCluster(3, 4) +westMinus2:setClusterRadius(1, 4) +westMinus2:setClusterSpacing(8, 10) +westMinus2:setOutlierChance(0.5) +westMinus2:setPeriod("240s") + +westMinus2:addMonster("Werepanther", 1) +westMinus2:addMonster("Werecrocodile", 2) +westMinus2:addMonster("Feral Werecrocodile", 2) + +westMinus2:register() + +local eastMinus1 = SpawnZone("spawn.oskayaat.east-1") + +eastMinus1:setMonstersPerCluster(3, 4) +eastMinus1:setClusterRadius(2, 4) +eastMinus1:setClusterSpacing(8, 10) +eastMinus1:setOutlierChance(0.5) +eastMinus1:setPeriod("240s") + +eastMinus1:addMonster("Cunning Werepanther", 3) +eastMinus1:addMonster("Weretiger", 2) +eastMinus1:addMonster("White Weretiger", 1) + +eastMinus1:register() + +local eastMinus2 = SpawnZone("spawn.oskayaat.east-2") + +eastMinus2:setMonstersPerCluster(3, 4) +eastMinus2:setClusterRadius(2, 4) +eastMinus2:setClusterSpacing(8, 10) +eastMinus2:setOutlierChance(0.5) +eastMinus2:setPeriod("240s") + +eastMinus2:addMonster("Weretiger", 1) +eastMinus2:addMonster("White Weretiger", 1) +eastMinus2:addMonster("Cunning Werepanther", 1) + +eastMinus2:register() diff --git a/data/libs/features_lib.lua b/data/libs/features_lib.lua new file mode 100644 index 00000000000..bf30a1a19ce --- /dev/null +++ b/data/libs/features_lib.lua @@ -0,0 +1,30 @@ +Features = { + DisableDiscordEvents = "discord-events-disabled", +} + +local function validateFeature(feature) + local found = false + for _, v in pairs(Features) do + if v == feature then + found = true + end + end + if not found then + error("Invalid feature: " .. feature) + end +end + +function Player:hasFeature(feature) + validateFeature(feature) + local kv = self:kv():scoped("features") + if kv:get(feature) then + return true + end + return false +end + +function Player:setFeature(feature, value) + validateFeature(feature) + local kv = self:kv():scoped("features") + kv:set(feature, value) +end diff --git a/data/libs/functions/queue.lua b/data/libs/functions/queue.lua new file mode 100644 index 00000000000..33a6c240947 --- /dev/null +++ b/data/libs/functions/queue.lua @@ -0,0 +1,73 @@ +Queue = {} + +---@param initial table|Queue +---@param options table +---@return Queue +setmetatable(Queue, { + __call = function(self) + local set = setmetatable({ + head = 0, + tail = -1, + items = {}, + }, { __index = Queue }) + return set + end, +}) + +function Queue:isEmpty() + return self.head > self.tail +end + +function Queue:enqueue(value) + self.tail = self.tail + 1 + self.items[self.tail] = value +end + +function Queue:dequeue() + if self:isEmpty() then + error("Queue is empty") + end + + local value = self.items[self.head] + self.items[self.head] = nil -- to allow garbage collection + self.head = self.head + 1 + return value +end + +function Queue:peek() + if self:isEmpty() then + error("Queue is empty") + end + + return self.items[self.head] +end + +function Queue:size() + return self.tail - self.head + 1 +end + +RandomQueue = {} + +setmetatable(RandomQueue, { + __index = Queue, + __call = function(self) + local instance = setmetatable(Queue(), { __index = RandomQueue }) + return instance + end, +}) + +function RandomQueue:dequeue() + if self:isEmpty() then + error("RandomQueue is empty") + end + + local index = math.random(self.head, self.tail) + local value = self.items[index] + + -- Move the last item to the place of the removed item to maintain contiguity + self.items[index] = self.items[self.tail] + self.items[self.tail] = nil -- to allow garbage collection + self.tail = self.tail - 1 + + return value +end diff --git a/data/libs/spawn_zones_lib.lua b/data/libs/spawn_zones_lib.lua new file mode 100644 index 00000000000..28cfb87384f --- /dev/null +++ b/data/libs/spawn_zones_lib.lua @@ -0,0 +1,3 @@ +function SpawnZone:setPeriod(period) + self:setInterval(ParseDuration(period)) +end diff --git a/data/scripts/talkactions/god/spawnnzones.lua b/data/scripts/talkactions/god/spawnnzones.lua new file mode 100644 index 00000000000..b962e5596c9 --- /dev/null +++ b/data/scripts/talkactions/god/spawnnzones.lua @@ -0,0 +1,137 @@ +local zones = TalkAction("/szones") + +function zones.onSay(player, words, param) + local params = string.split(param, ",") + local cmd = params[1] + if not cmd then + player:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, "Command not found.") + return false + end + + if cmd == "list" then + local list = {} + local filter = params[2] and params[2]:trim() + for _, sz in ipairs(SpawnZone.getAll()) do + local name = sz:getName() + if filter then + if not name:lower():find(filter:lower()) then + goto continue + end + end + table.insert(list, name) + ::continue:: + end + player:sendTextMessage(MESSAGE_HEALED, "Spawn Zones:\n" .. table.concat(list, "\n ")) + return true + end + + local function zoneFromParam() + local zoneName = params[2]:trim() + if not zoneName then + player:sendTextMessage(MESSAGE_HEALED, "Spawn Zone not found.") + return false + end + local zone = SpawnZone.getByName(zoneName) + if not zone then + player:sendTextMessage(MESSAGE_HEALED, "Spawn Zone not found.") + return false + end + return zone + end + + local commands = { + monstersPerCluster = function(sz) + if #params < 3 then + local min, max = sz:getMonstersPerCluster() + player:sendTextMessage(MESSAGE_HEALED, "Monsters per cluster: " .. min .. " to " .. max .. ".") + return false + end + local min = tonumber(params[3]) + local max = tonumber(params[4]) + if not min or not max then + player:sendTextMessage(MESSAGE_HEALED, "Invalid parameters.") + return false + end + sz:setMonstersPerCluster(min, max) + sz:register() + player:sendTextMessage(MESSAGE_HEALED, "Monsters per cluster set to: " .. min .. " to " .. max .. ".") + end, + clusterRadius = function(sz) + if #params < 3 then + local min, max = sz:getClusterRadius() + player:sendTextMessage(MESSAGE_HEALED, "Cluster radius: " .. min .. " to " .. max .. ".") + return false + end + local min = tonumber(params[3]) + local max = tonumber(params[4]) + if not min or not max then + player:sendTextMessage(MESSAGE_HEALED, "Invalid parameters.") + return false + end + sz:setClusterRadius(min, max) + sz:register() + player:sendTextMessage(MESSAGE_HEALED, "Cluster radius set to: " .. min .. " to " .. max .. ".") + end, + clusterSpacing = function(sz) + if #params < 3 then + local min, max = sz:getClusterSpacing() + player:sendTextMessage(MESSAGE_HEALED, "Cluster spacing: " .. min .. " to " .. max .. ".") + return false + end + local min = tonumber(params[3]) + local max = tonumber(params[4]) + if not min or not max then + player:sendTextMessage(MESSAGE_HEALED, "Invalid parameters.") + return false + end + sz:setClusterSpacing(min, max) + sz:register() + player:sendTextMessage(MESSAGE_HEALED, "Cluster spacing set to: " .. min .. " to " .. max .. ".") + end, + outlierChance = function(sz) + if #params < 3 then + local chance = sz:getOutlierChance() + player:sendTextMessage(MESSAGE_HEALED, "Outlier chance: " .. chance .. ".") + return false + end + local chance = tonumber(params[3]) + if not chance then + player:sendTextMessage(MESSAGE_HEALED, "Invalid parameters.") + return false + end + sz:setOutlierChance(chance) + sz:register() + player:sendTextMessage(MESSAGE_HEALED, "Outlier chance set to: " .. chance .. ".") + end, + period = function(sz) + if #params < 3 then + local period = sz:getInterval() / 1000 + player:sendTextMessage(MESSAGE_HEALED, "Period: " .. period .. "s.") + return false + end + local period = params[3] + if not period then + player:sendTextMessage(MESSAGE_HEALED, "Invalid parameters.") + return false + end + sz:setPeriod(period) + sz:register() + player:sendTextMessage(MESSAGE_HEALED, "Period set to: " .. period .. ".") + end, + } + + local command = commands[cmd] + if not command then + player:sendTextMessage(MESSAGE_HEALED, "Command not found.") + return false + end + local sz = zoneFromParam() + if not sz then + return false + end + return command(sz) +end + +zones:separator(" ") +zones:groupType("god") +zones:register() diff --git a/data/scripts/talkactions/player/features.lua b/data/scripts/talkactions/player/features.lua new file mode 100644 index 00000000000..e69de29bb2d diff --git a/metrics/docker-compose.yml b/metrics/docker-compose.yml new file mode 100644 index 00000000000..afff477eb68 --- /dev/null +++ b/metrics/docker-compose.yml @@ -0,0 +1,42 @@ +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 # Change this! + - 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..8637b5158ad --- /dev/null +++ b/metrics/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +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/configmanager.cpp b/src/config/configmanager.cpp index 2931464270a..ee087991e27 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -347,6 +347,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/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 0c68ec3a659..2999234e5ff 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; @@ -6963,6 +6998,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; } @@ -7036,6 +7072,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; } @@ -7167,6 +7204,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..3f5aa91b43c 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]; @@ -8470,6 +8496,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 +8506,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 +8568,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 +8729,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 +8800,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 +8855,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 +8964,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 +9048,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/spawn_zone.cpp b/src/game/zones/spawn_zone.cpp new file mode 100644 index 00000000000..53d6c6f227b --- /dev/null +++ b/src/game/zones/spawn_zone.cpp @@ -0,0 +1,148 @@ +/** + * 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 "creatures/monsters/monster.hpp" +#include "creatures/monsters/monsters.hpp" +#include "game/game.hpp" +#include "spawn_zone.hpp" +#include "game/scheduling/dispatcher.hpp" + +phmap::parallel_flat_hash_map_m> SpawnZone::spawnZonesByName = {}; +std::shared_ptr sentinelType = std::make_shared(); +std::shared_ptr sentinelCreature = std::make_shared(sentinelType)->getCreature(); + +void SpawnZone::registerZone() { + if (registered) { + auto zone = getZone(); + if (!zone) { + g_logger().error("[SpawnZone::doRegisterZone]: Zone not found: {}", zoneName); + return; + } + zone->removeMonsters(); + for (const auto &spawnMonster : spawnMonsters) { + spawnMonster->removeMonsters(); + spawnMonster->stopEvent(); + } + } + g_dispatcher().scheduleEvent( + 50, + [this]() { + doRegisterZone(); + }, + "SpawnZone::registerZone" + ); +} + +void SpawnZone::doRegisterZone() { + Benchmark bm; + auto zone = getZone(); + if (!zone) { + g_logger().error("[SpawnZone::doRegisterZone]: Zone not found: {}", zoneName); + return; + } + bool delayed = !registered; + registered = true; + spawnZonesByName[zone->getName()] = shared_from_this(); + usableTiles.clear(); + spawnMonsters.clear(); + + for (const auto &p : zone->getPositions()) { + const auto &tile = g_game().map.getTile(p); + if (!tile) { + continue; + } + if (!canUseTile(tile)) { + continue; + } + usableTiles.insert(tile); + } + generateSpawnMonsters(delayed); + g_logger().debug("[SpawnZone::doRegisterZone]: {} | Generated {} clusters in {} milliseconds", zone->getName(), spawnMonsters.size(), bm.duration()); +} + +bool SpawnZone::canUseTile(const std::shared_ptr &tile) { + return tile->queryAdd(0, sentinelCreature, 1, FLAG_PATHFINDING | FLAG_IGNOREFIELDDAMAGE) == RETURNVALUE_NOERROR; +} + +void SpawnZone::generateSpawnMonsters(bool delayed) { + if (!registered) { + return; + } + + std::set> outlierTiles; + + std::vector> vector = std::vector>(usableTiles.begin(), usableTiles.end()); + std::ranges::shuffle(vector.begin(), vector.end(), getRandomGenerator()); + std::queue> queue = std::queue>(std::deque>(vector.begin(), vector.end())); + + while (!queue.empty()) { + auto tile = queue.front(); + queue.pop(); + if (!usableTiles.contains(tile)) { + continue; + } + std::vector> usedTiles; + usedTiles.push_back(tile); + auto radius = uniform_random(clusterRadius.first, clusterRadius.second); + for (const auto pos : tile->getPosition().getSurroundingPositions(radius)) { + auto other = g_game().map.getTile(pos); + if (!other || !canUseTile(other)) { + continue; + } + if (usableTiles.contains(other)) { + usedTiles.push_back(other); + } + } + auto monsterCount = uniform_random(monstersPerCluster.first, monstersPerCluster.second); + if (monsterCount > usedTiles.size()) { + continue; + } + for (const auto &tile : usedTiles) { + usableTiles.erase(tile); + } + std::ranges::shuffle(usedTiles.begin(), usedTiles.end(), getRandomGenerator()); + + for (int32_t i = 0; i < monsterCount; ++i) { + auto &dst = usedTiles[i]; + auto spawnMonster = std::make_shared(dst->getPosition(), radius); + for (const auto &[monster, weight] : monsters) { + spawnMonster->addMonster(monster, dst->getPosition(), DIRECTION_SOUTH, getInterval(), weight); + } + spawnMonsters.push_back(spawnMonster); + spawnMonster->startup(delayed); + } + + auto spacing = uniform_random(clusterSpacing.first, clusterSpacing.second); + for (const auto pos : tile->getPosition().getSurroundingPositions(spacing)) { + auto tile = g_game().map.getTile(pos); + if (usableTiles.contains(tile)) { + usableTiles.erase(tile); + outlierTiles.insert(tile); + } + } + } + + for (const auto &tile : outlierTiles) { + if (tile->getCreatureCount() > 0) { + continue; + } + auto chance = uniform_random(0, 10000); + if (chance > outlierChance * 100) { + continue; + } + auto spawnMonster = std::make_shared(tile->getPosition(), 0); + for (const auto &[monster, weight] : monsters) { + spawnMonster->addMonster(monster, tile->getPosition(), DIRECTION_SOUTH, getInterval(), weight); + } + spawnMonsters.push_back(spawnMonster); + spawnMonster->startup(delayed); + } +} diff --git a/src/game/zones/spawn_zone.hpp b/src/game/zones/spawn_zone.hpp new file mode 100644 index 00000000000..3e1c4246a5e --- /dev/null +++ b/src/game/zones/spawn_zone.hpp @@ -0,0 +1,132 @@ +/** + * 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 "zone.hpp" + +class MonsterType; + +class SpawnZone : public std::enable_shared_from_this { +public: + explicit SpawnZone(const std::string &zoneName) : + zoneName(zoneName) { } + + void setOutlierChance(double chance) { + outlierChance = chance; + } + + void setMonstersPerCluster(uint16_t min, uint16_t max) { + monstersPerCluster = { min, max }; + } + + void setClusterRadius(uint16_t min, uint16_t max) { + clusterRadius = { min, max }; + } + + void setClusterSpacing(uint16_t min, uint16_t max) { + clusterSpacing = { min, max }; + } + + void setInterval(uint32_t newInterval) { + interval = newInterval; + } + + void addMonster(std::string monster, uint32_t weight = 1) { + monsters[monster] = weight; + } + + void removeMonster(std::string monster) { + monsters.erase(monster); + } + + void registerZone(); + + std::shared_ptr getZone() const { + return Zone::getZone(zoneName); + } + + std::string getName() const { + return zoneName; + } + + // getters + + double getOutlierChance() const { + return outlierChance; + } + + std::pair getMonstersPerCluster() const { + return monstersPerCluster; + } + + std::pair getClusterRadius() const { + return clusterRadius; + } + + std::pair getClusterSpacing() const { + return clusterSpacing; + } + + uint32_t getInterval() const { + if (interval == 0) { + auto firstMonster = monsters.empty() ? "none" : monsters.begin()->first; + g_logger().info("[SpawnZone::getInterval] interval for block containing monster {} was not set, defaultint to {}", firstMonster, 90 * 1000); + return 90 * 1000; + } + return interval; + } + + std::unordered_map getMonsters() const { + return monsters; + } + + static std::vector> getSpawnZones() { + std::vector> spawnZones; + for (const auto &it : spawnZonesByName) { + spawnZones.push_back(it.second); + } + return spawnZones; + } + + static std::shared_ptr getSpawnZone(const std::string &zoneName) { + auto it = spawnZonesByName.find(zoneName); + if (it != spawnZonesByName.end()) { + return it->second; + } + + auto spawnZone = std::make_shared(zoneName); + spawnZonesByName[zoneName] = spawnZone; + return spawnZone; + } + + static std::shared_ptr addSpawnZone(const std::string &zoneName) { + auto spawnZone = std::make_shared(zoneName); + spawnZonesByName[zoneName] = spawnZone; + return spawnZone; + } + +private: + std::string zoneName; + bool registered = false; + uint32_t interval = 0; + double outlierChance = 0.0; + std::pair monstersPerCluster = { 2, 3 }; + std::pair clusterRadius = { 2, 5 }; + std::pair clusterSpacing = { 7, 10 }; + std::unordered_map monsters; + std::vector> spawnMonsters; + std::unordered_set> usableTiles; + + void doRegisterZone(); + void generateSpawnMonsters(bool delayed); + bool canUseTile(const std::shared_ptr &tile); + + static phmap::parallel_flat_hash_map_m> spawnZonesByName; +}; 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/game/zones/zone.hpp b/src/game/zones/zone.hpp index 91fa6f25e29..99adf9d985d 100644 --- a/src/game/zones/zone.hpp +++ b/src/game/zones/zone.hpp @@ -210,7 +210,7 @@ class Zone { static bool loadFromXML(const std::string &fileName, uint16_t shiftID = 0); -private: +protected: bool contains(const Position &position) const; Position removeDestination = Position(); 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/kv/kv.cpp b/src/kv/kv.cpp index 0e2f4b2f836..f3133dec9ca 100644 --- a/src/kv/kv.cpp +++ b/src/kv/kv.cpp @@ -32,7 +32,7 @@ void KVStore::set(const std::string &key, const ValueWrapper &value) { } void KVStore::setLocked(const std::string &key, const ValueWrapper &value) { - logger.debug("KVStore::set({})", key); + logger.trace("KVStore::set({})", key); auto it = store_.find(key); if (it != store_.end()) { it->second.first = value; @@ -53,7 +53,7 @@ void KVStore::setLocked(const std::string &key, const ValueWrapper &value) { } std::optional KVStore::get(const std::string &key, bool forceLoad /*= false */) { - logger.debug("KVStore::get({})", key); + logger.trace("KVStore::get({})", key); std::scoped_lock lock(mutex_); if (forceLoad || !store_.contains(key)) { auto value = load(key); @@ -72,11 +72,26 @@ std::optional KVStore::get(const std::string &key, bool forceLoad return value; } +std::unordered_set KVStore::keys(const std::string &prefix /*= ""*/) { + std::scoped_lock lock(mutex_); + std::unordered_set keys; + for (const auto &[key, value] : store_) { + if (key.find(prefix) == 0) { + std::string suffix = key.substr(prefix.size()); + keys.insert(suffix); + } + } + for (const auto &key : loadPrefix(prefix)) { + keys.insert(key); + } + return keys; +} + void KV::remove(const std::string &key) { set(key, ValueWrapper::deleted()); } std::shared_ptr KVStore::scoped(const std::string &scope) { - logger.debug("KVStore::scoped({})", scope); + logger.trace("KVStore::scoped({})", scope); return std::make_shared(logger, *this, scope); } diff --git a/src/kv/kv.hpp b/src/kv/kv.hpp index 40fe449834f..ea76276c80c 100644 --- a/src/kv/kv.hpp +++ b/src/kv/kv.hpp @@ -32,6 +32,8 @@ class KV : public std::enable_shared_from_this { virtual std::shared_ptr scoped(const std::string &scope) = 0; + virtual std::unordered_set keys(const std::string &prefix = "") = 0; + void remove(const std::string &key); virtual void flush() { @@ -41,7 +43,7 @@ class KV : public std::enable_shared_from_this { class KVStore : public KV { public: - static constexpr size_t MAX_SIZE = 10000; + static constexpr size_t MAX_SIZE = 1000000; static KVStore &getInstance(); explicit KVStore(Logger &logger) : @@ -60,6 +62,7 @@ class KVStore : public KV { } std::shared_ptr scoped(const std::string &scope) override final; + std::unordered_set keys(const std::string &prefix = ""); protected: phmap::parallel_flat_hash_map::iterator>> getStore() { @@ -76,6 +79,7 @@ class KVStore : public KV { virtual std::optional load(const std::string &key) = 0; virtual bool save(const std::string &key, const ValueWrapper &value) = 0; + virtual std::vector loadPrefix(const std::string &prefix = "") = 0; private: void setLocked(const std::string &key, const ValueWrapper &value); @@ -118,10 +122,14 @@ class ScopedKV final : public KV { } std::shared_ptr scoped(const std::string &scope) override final { - logger.debug("ScopedKV::scoped({})", buildKey(scope)); + logger.trace("ScopedKV::scoped({})", buildKey(scope)); return std::make_shared(logger, rootKV_, buildKey(scope)); } + std::unordered_set keys(const std::string &prefix = "") override { + return rootKV_.keys(buildKey(prefix)); + } + private: std::string buildKey(const std::string &key) const { return fmt::format("{}.{}", prefix_, key); diff --git a/src/kv/kv_sql.cpp b/src/kv/kv_sql.cpp index a0f1623ed2c..2aa8e10f7e5 100644 --- a/src/kv/kv_sql.cpp +++ b/src/kv/kv_sql.cpp @@ -41,6 +41,24 @@ std::optional KVSQL::load(const std::string &key) { return std::nullopt; } +std::vector KVSQL::loadPrefix(const std::string &prefix /* = ""*/) { + std::vector keys; + std::string keySearch = db.escapeString(prefix + "%"); + auto query = fmt::format("SELECT `key_name` FROM `kv_store` WHERE `key_name` LIKE {}", keySearch); + auto result = db.storeQuery(query); + if (result == nullptr) { + return keys; + } + + do { + std::string key = result->getString("key_name"); + replaceString(key, prefix, ""); + keys.push_back(key); + } while (result->next()); + + return keys; +} + bool KVSQL::save(const std::string &key, const ValueWrapper &value) { auto update = dbUpdate(); prepareSave(key, value, update); diff --git a/src/kv/kv_sql.hpp b/src/kv/kv_sql.hpp index 8cb4dce89f7..fd181b1cef7 100644 --- a/src/kv/kv_sql.hpp +++ b/src/kv/kv_sql.hpp @@ -25,6 +25,7 @@ class KVSQL final : public KVStore { bool saveAll() override; private: + std::vector loadPrefix(const std::string &prefix = "") override; std::optional load(const std::string &key) override; bool save(const std::string &key, const ValueWrapper &value) override; bool prepareSave(const std::string &key, const ValueWrapper &value, DBInsert &update); 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..ad83ad4d3e7 --- /dev/null +++ b/src/lib/metrics/metrics.cpp @@ -0,0 +1,107 @@ +/** + * 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) { + 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"); + } + + if (opts.enableOStreamExporter || opts.enablePrometheusExporter) { + metrics_api::Provider::SetMeterProvider(provider); + } else { + std::shared_ptr none; + metrics_api::Provider::SetMeterProvider(none); + } +} + +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) { + g_logger().debug("ScopedLatency: Histogram {} not found", histogramName); + 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..9edb1e64859 --- /dev/null +++ b/src/lib/metrics/metrics.hpp @@ -0,0 +1,162 @@ +/** + * 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 (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 (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() { + return provider->GetMeter(meterName, otelVersion); + } + + private: + std::mutex mutex_; + std::shared_ptr provider; + + 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/game/spawn_zone_functions.cpp b/src/lua/functions/core/game/spawn_zone_functions.cpp new file mode 100644 index 00000000000..5b4449f2cb0 --- /dev/null +++ b/src/lua/functions/core/game/spawn_zone_functions.cpp @@ -0,0 +1,262 @@ +#include "pch.hpp" +#include "lua/functions/core/game/spawn_zone_functions.hpp" +#include "game/zones/spawn_zone.hpp" +#include "game/game.hpp" + +// SpawnZone +int SpawnZoneFunctions::luaSpawnZoneCreate(lua_State* L) { + // SpawnZone(name) + auto name = getString(L, 2); + auto spawnZone = SpawnZone::getSpawnZone(name); + if (!spawnZone) { + spawnZone = SpawnZone::addSpawnZone(name); + } + pushUserdata(L, spawnZone); + setMetatable(L, -1, "SpawnZone"); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneCompare(lua_State* L) { + auto spawnZone1 = getUserdataShared(L, 1); + auto spawnZone2 = getUserdataShared(L, 2); + if (!spawnZone1) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + if (!spawnZone2) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + + pushBoolean(L, spawnZone1->getZone() == spawnZone2->getZone()); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneSetMonstersPerCluster(lua_State* L) { + // spawnZone:setMonstersPerCluster(min, max) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto min = getNumber(L, 2); + auto max = getNumber(L, 3); + spawnZone->setMonstersPerCluster(min, max); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneSetClusterRadius(lua_State* L) { + // spawnZone:setClusterRadius(min, max) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto min = getNumber(L, 2); + auto max = getNumber(L, 3); + spawnZone->setClusterRadius(min, max); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneSetClusterSpacing(lua_State* L) { + // spawnZone:setClusterSpacing(min, max) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto min = getNumber(L, 2); + auto max = getNumber(L, 3); + spawnZone->setClusterSpacing(min, max); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneSetOutlierChance(lua_State* L) { + // spawnZone:setOutlierChance(chance) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto chance = getNumber(L, 2); + spawnZone->setOutlierChance(chance); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneSetInterval(lua_State* L) { + // spawnZone:setInterval(interval) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto interval = getNumber(L, 2); + spawnZone->setInterval(interval); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneAddMonster(lua_State* L) { + // spawnZone:addMonster(monster, weight) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto monster = getString(L, 2); + auto weight = getNumber(L, 3); + spawnZone->addMonster(monster, weight); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneRemoveMonster(lua_State* L) { + // spawnZone:removeMonster(monster) + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + auto monster = getString(L, 2); + spawnZone->removeMonster(monster); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetMonstersPerCluster(lua_State* L) { + // spawnZone:getMonstersPerCluster() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + return 2; + } + auto [min, max] = spawnZone->getMonstersPerCluster(); + lua_pushnumber(L, min); + lua_pushnumber(L, max); + return 2; +} + +int SpawnZoneFunctions::luaSpawnZoneGetClusterRadius(lua_State* L) { + // spawnZone:getClusterRadius() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + return 2; + } + auto [min, max] = spawnZone->getClusterRadius(); + lua_pushnumber(L, min); + lua_pushnumber(L, max); + return 2; +} + +int SpawnZoneFunctions::luaSpawnZoneGetClusterSpacing(lua_State* L) { + // spawnZone:getClusterSpacing() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + return 2; + } + auto [min, max] = spawnZone->getClusterSpacing(); + lua_pushnumber(L, min); + lua_pushnumber(L, max); + return 2; +} + +int SpawnZoneFunctions::luaSpawnZoneGetOutlierChance(lua_State* L) { + // spawnZone:getOutlierChance() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnumber(L, 0); + return 1; + } + lua_pushnumber(L, spawnZone->getOutlierChance()); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetInterval(lua_State* L) { + // spawnZone:getInterval() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnumber(L, 0); + return 1; + } + lua_pushnumber(L, spawnZone->getInterval()); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetName(lua_State* L) { + // spawnZone:getName() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + pushBoolean(L, false); + return 1; + } + pushString(L, spawnZone->getName()); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetZone(lua_State* L) { + // spawnZone:getZone() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + lua_pushnil(L); + return 1; + } + pushUserdata(L, spawnZone->getZone()); + setMetatable(L, -1, "Zone"); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneRegister(lua_State* L) { + // spawnZone:register() + auto spawnZone = getUserdataShared(L, 1); + if (!spawnZone) { + reportErrorFunc(getErrorDesc(LUA_ERROR_SPAWN_ZONE_NOT_FOUND)); + return 1; + } + spawnZone->registerZone(); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetByName(lua_State* L) { + // SpawnZone.getByName(name) + auto name = getString(L, 1); + auto spawnZone = SpawnZone::getSpawnZone(name); + if (!spawnZone) { + lua_pushnil(L); + return 1; + } + pushUserdata(L, spawnZone); + setMetatable(L, -1, "SpawnZone"); + return 1; +} + +int SpawnZoneFunctions::luaSpawnZoneGetAll(lua_State* L) { + // SpawnZone.getAll() + auto spawnZones = SpawnZone::getSpawnZones(); + lua_createtable(L, static_cast(spawnZones.size()), 0); + int index = 0; + for (auto spawnZone : spawnZones) { + index++; + pushUserdata(L, spawnZone); + setMetatable(L, -1, "SpawnZone"); + lua_rawseti(L, -2, index); + } + return 1; +} diff --git a/src/lua/functions/core/game/spawn_zone_functions.hpp b/src/lua/functions/core/game/spawn_zone_functions.hpp new file mode 100644 index 00000000000..2922f422eba --- /dev/null +++ b/src/lua/functions/core/game/spawn_zone_functions.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "lua/scripts/luascript.hpp" + +class SpawnZone; + +class SpawnZoneFunctions final : LuaScriptInterface { +public: + static void init(lua_State* L) { + registerSharedClass(L, "SpawnZone", "", SpawnZoneFunctions::luaSpawnZoneCreate); + registerMetaMethod(L, "SpawnZone", "__eq", SpawnZoneFunctions::luaSpawnZoneCompare); + + registerMethod(L, "SpawnZone", "setMonstersPerCluster", SpawnZoneFunctions::luaSpawnZoneSetMonstersPerCluster); + registerMethod(L, "SpawnZone", "setClusterRadius", SpawnZoneFunctions::luaSpawnZoneSetClusterRadius); + registerMethod(L, "SpawnZone", "setClusterSpacing", SpawnZoneFunctions::luaSpawnZoneSetClusterSpacing); + registerMethod(L, "SpawnZone", "setOutlierChance", SpawnZoneFunctions::luaSpawnZoneSetOutlierChance); + registerMethod(L, "SpawnZone", "setInterval", SpawnZoneFunctions::luaSpawnZoneSetInterval); + registerMethod(L, "SpawnZone", "addMonster", SpawnZoneFunctions::luaSpawnZoneAddMonster); + registerMethod(L, "SpawnZone", "removeMonster", SpawnZoneFunctions::luaSpawnZoneRemoveMonster); + + // getters + registerMethod(L, "SpawnZone", "getMonstersPerCluster", SpawnZoneFunctions::luaSpawnZoneGetMonstersPerCluster); + registerMethod(L, "SpawnZone", "getClusterRadius", SpawnZoneFunctions::luaSpawnZoneGetClusterRadius); + registerMethod(L, "SpawnZone", "getClusterSpacing", SpawnZoneFunctions::luaSpawnZoneGetClusterSpacing); + registerMethod(L, "SpawnZone", "getOutlierChance", SpawnZoneFunctions::luaSpawnZoneGetOutlierChance); + registerMethod(L, "SpawnZone", "getInterval", SpawnZoneFunctions::luaSpawnZoneGetInterval); + + registerMethod(L, "SpawnZone", "getName", SpawnZoneFunctions::luaSpawnZoneGetName); + registerMethod(L, "SpawnZone", "getZone", SpawnZoneFunctions::luaSpawnZoneGetZone); + + registerMethod(L, "SpawnZone", "register", SpawnZoneFunctions::luaSpawnZoneRegister); + + // static methods + registerMethod(L, "SpawnZone", "getByName", SpawnZoneFunctions::luaSpawnZoneGetByName); + registerMethod(L, "SpawnZone", "getAll", SpawnZoneFunctions::luaSpawnZoneGetAll); + } + +private: + static int luaSpawnZoneCreate(lua_State* L); + static int luaSpawnZoneCompare(lua_State* L); + + static int luaSpawnZoneSetMonstersPerCluster(lua_State* L); + static int luaSpawnZoneSetClusterRadius(lua_State* L); + static int luaSpawnZoneSetClusterSpacing(lua_State* L); + static int luaSpawnZoneSetOutlierChance(lua_State* L); + static int luaSpawnZoneSetInterval(lua_State* L); + static int luaSpawnZoneAddMonster(lua_State* L); + static int luaSpawnZoneRemoveMonster(lua_State* L); + + static int luaSpawnZoneGetMonstersPerCluster(lua_State* L); + static int luaSpawnZoneGetClusterRadius(lua_State* L); + static int luaSpawnZoneGetClusterSpacing(lua_State* L); + static int luaSpawnZoneGetOutlierChance(lua_State* L); + static int luaSpawnZoneGetInterval(lua_State* L); + + static int luaSpawnZoneGetName(lua_State* L); + static int luaSpawnZoneGetZone(lua_State* L); + + static int luaSpawnZoneRegister(lua_State* L); + + static int luaSpawnZoneGetByName(lua_State* L); + static int luaSpawnZoneGetAll(lua_State* L); +}; 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/kv_functions.cpp b/src/lua/functions/core/libs/kv_functions.cpp index 769c20bff8a..dbef916364d 100644 --- a/src/lua/functions/core/libs/kv_functions.cpp +++ b/src/lua/functions/core/libs/kv_functions.cpp @@ -72,7 +72,7 @@ int KVFunctions::luaKVGet(lua_State* L) { valueWrapper = g_kv().get(key, forceLoad); } - if (valueWrapper) { + if (valueWrapper.has_value()) { pushValueWrapper(L, *valueWrapper); } else { lua_pushnil(L); @@ -80,6 +80,44 @@ int KVFunctions::luaKVGet(lua_State* L) { return 1; } +int KVFunctions::luaKVRemove(lua_State* L) { + // KV.remove(key) | scopedKV:remove(key) + auto key = getString(L, -1); + if (isUserdata(L, 1)) { + auto scopedKV = getUserdataShared(L, 1); + scopedKV->remove(key); + } else { + g_kv().remove(key); + } + lua_pushnil(L); + return 1; +} + +int KVFunctions::luaKVKeys(lua_State* L) { + // KV.keys([prefix = ""]) | scopedKV:keys([prefix = ""]) + std::unordered_set keys; + std::string prefix = ""; + + if (isString(L, -1)) { + prefix = getString(L, -1); + } + + if (isUserdata(L, 1)) { + auto scopedKV = getUserdataShared(L, 1); + keys = scopedKV->keys(); + } else { + keys = g_kv().keys(prefix); + } + + int index = 0; + lua_createtable(L, static_cast(keys.size()), 0); + for (const auto &key : keys) { + pushString(L, key); + lua_rawseti(L, -2, ++index); + } + return 1; +} + std::optional KVFunctions::getValueWrapper(lua_State* L) { if (isBoolean(L, -1)) { return ValueWrapper(getBoolean(L, -1)); diff --git a/src/lua/functions/core/libs/kv_functions.hpp b/src/lua/functions/core/libs/kv_functions.hpp index 243a0ed36e3..3abddfb0b53 100644 --- a/src/lua/functions/core/libs/kv_functions.hpp +++ b/src/lua/functions/core/libs/kv_functions.hpp @@ -18,17 +18,23 @@ class KVFunctions final : LuaScriptInterface { registerMethod(L, "kv", "scoped", KVFunctions::luaKVScoped); registerMethod(L, "kv", "set", KVFunctions::luaKVSet); registerMethod(L, "kv", "get", KVFunctions::luaKVGet); + registerMethod(L, "kv", "keys", KVFunctions::luaKVKeys); + registerMethod(L, "kv", "remove", KVFunctions::luaKVRemove); registerClass(L, "KV", ""); registerMethod(L, "KV", "scoped", KVFunctions::luaKVScoped); registerMethod(L, "KV", "set", KVFunctions::luaKVSet); registerMethod(L, "KV", "get", KVFunctions::luaKVGet); + registerMethod(L, "KV", "keys", KVFunctions::luaKVKeys); + registerMethod(L, "KV", "remove", KVFunctions::luaKVRemove); } private: static int luaKVScoped(lua_State* L); static int luaKVSet(lua_State* L); static int luaKVGet(lua_State* L); + static int luaKVKeys(lua_State* L); + static int luaKVRemove(lua_State* L); static std::optional getValueWrapper(lua_State* L); static void pushStringValue(lua_State* L, const StringType &value); 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/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/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..ccd3315cfe0 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 @@ +