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