1181 lines
49 KiB
Lua
1181 lines
49 KiB
Lua
local ____lualib = require("lualib_bundle")
|
||
local __TS__Class = ____lualib.__TS__Class
|
||
local Map = ____lualib.Map
|
||
local __TS__New = ____lualib.__TS__New
|
||
local Set = ____lualib.Set
|
||
local __TS__ArrayMap = ____lualib.__TS__ArrayMap
|
||
local __TS__ArraySort = ____lualib.__TS__ArraySort
|
||
local __TS__Iterator = ____lualib.__TS__Iterator
|
||
local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
|
||
local __TS__Number = ____lualib.__TS__Number
|
||
local __TS__ArrayFrom = ____lualib.__TS__ArrayFrom
|
||
local ____exports = {}
|
||
local ____server_config = require("server_config")
|
||
local SERVER_CONFIG = ____server_config.SERVER_CONFIG
|
||
local ____api_helper = require("api_helper")
|
||
local encodeApiBody = ____api_helper.encodeApiBody
|
||
local setApiHeadersLong = ____api_helper.setApiHeadersLong
|
||
local ____difficulty_manager = require("difficulty_manager")
|
||
local Difficulty = ____difficulty_manager.Difficulty
|
||
local ____player_connection_state = require("utils.player_connection_state")
|
||
local DOTA_CONNECTION_STATE = ____player_connection_state.DOTA_CONNECTION_STATE
|
||
local isConnectionStateActivelyConnected = ____player_connection_state.isConnectionStateActivelyConnected
|
||
local isConnectionStateEffectivelyInGame = ____player_connection_state.isConnectionStateEffectivelyInGame
|
||
local ____real_lobby_player = require("utils.real_lobby_player")
|
||
local collectStatsEligiblePlayerIds = ____real_lobby_player.collectStatsEligiblePlayerIds
|
||
local isRealLobbyPlayer = ____real_lobby_player.isRealLobbyPlayer
|
||
local ____match_end_combat_stats = require("match_end_combat_stats")
|
||
local MatchEndCombatStats = ____match_end_combat_stats.MatchEndCombatStats
|
||
local function rawPrintFn(____, ...)
|
||
_G:print(...)
|
||
end
|
||
local ENABLE_VERBOSE_GAME_STATS_LOGS = false
|
||
local ____print = ENABLE_VERBOSE_GAME_STATS_LOGS and rawPrintFn or (function(____, ...) return nil end)
|
||
--- Секунд подряд «никого в игре» до срабатывания «все вышли» (антидребезг).
|
||
local ALL_LEFT_DEBOUNCE_SECONDS = 8
|
||
--- Игровое время после DISCONNECTED до поражения.
|
||
local DISCONNECT_TIMEOUT_SECONDS = 90
|
||
--- Игровое время после ABANDONED/FAILED до поражения.
|
||
local ABANDON_DISCONNECT_TIMEOUT_SECONDS = 15
|
||
--- Интервал успешного heartbeat во время матча.
|
||
local HEARTBEAT_INTERVAL_SECONDS = 60
|
||
--- Повторная отправка, если сервер не принял сигнал.
|
||
local HEARTBEAT_RETRY_SECONDS = 5
|
||
local ALLOW_STATS_WITH_CHEATS = false
|
||
local ALLOW_STATS_IN_TOOLS_MODE = false
|
||
--- Релиз: при читах/sv_cheats — не начислять BP/осколки за матч (см. shouldBlockMatchEndRewards).
|
||
local ALLOW_MATCH_END_REWARDS_WITH_CHEATS = false
|
||
--- Релиз: Workshop Tools — без наград конца матча (локальная отладка без выдачи на аккаунт).
|
||
local ALLOW_MATCH_END_REWARDS_IN_TOOLS = false
|
||
____exports.GameStatsTracker = __TS__Class()
|
||
local GameStatsTracker = ____exports.GameStatsTracker
|
||
GameStatsTracker.name = "GameStatsTracker"
|
||
GameStatsTracker.____file_path = "scripts/vscripts/game_stats_tracker.lua"
|
||
function GameStatsTracker.prototype.____constructor(self)
|
||
self.gameStartTime = 0
|
||
self.isGameStarted = false
|
||
self.isGameEnded = false
|
||
self.playerGameData = __TS__New(Map)
|
||
self.lastKnownPlayers = __TS__New(Set)
|
||
self.playerDisconnectTime = __TS__New(Map)
|
||
self.pendingPlayers = __TS__New(Set)
|
||
self.registerStartInFlight = __TS__New(Set)
|
||
self.queuedPlayersForMatchBootstrap = __TS__New(Set)
|
||
self.isStatsDisabledForCurrentMatch = false
|
||
self.statsDisabledReason = ""
|
||
self.sessionParticipantsOrdered = {}
|
||
self.sessionParticipantIdSet = __TS__New(Set)
|
||
self.sessionParticipantsSteamOrdered = {}
|
||
self:setupListeners()
|
||
end
|
||
function GameStatsTracker.getInstance(self)
|
||
if not ____exports.GameStatsTracker.instance then
|
||
____exports.GameStatsTracker.instance = __TS__New(____exports.GameStatsTracker)
|
||
end
|
||
return ____exports.GameStatsTracker.instance
|
||
end
|
||
function GameStatsTracker.prototype.isPlayerEffectivelyInGame(self, playerId)
|
||
if not isRealLobbyPlayer(nil, playerId) then
|
||
return false
|
||
end
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if not hero then
|
||
return false
|
||
end
|
||
local cs = PlayerResource:GetConnectionState(playerId)
|
||
return isConnectionStateEffectivelyInGame(nil, cs)
|
||
end
|
||
function GameStatsTracker.prototype.isPlayerActivelyConnected(self, playerId)
|
||
if not isRealLobbyPlayer(nil, playerId) then
|
||
return false
|
||
end
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if not hero then
|
||
return false
|
||
end
|
||
local cs = PlayerResource:GetConnectionState(playerId)
|
||
return isConnectionStateActivelyConnected(nil, cs)
|
||
end
|
||
function GameStatsTracker.prototype.collectEffectivePlayersInGame(self)
|
||
local set = __TS__New(Set)
|
||
do
|
||
local i = 0
|
||
while i < DOTA_MAX_PLAYERS do
|
||
do
|
||
local playerId = i
|
||
if not PlayerResource:IsValidPlayerID(playerId) then
|
||
goto __continue14
|
||
end
|
||
if self:isPlayerEffectivelyInGame(playerId) then
|
||
set:add(playerId)
|
||
end
|
||
end
|
||
::__continue14::
|
||
i = i + 1
|
||
end
|
||
end
|
||
return set
|
||
end
|
||
function GameStatsTracker.prototype.collectActivelyConnectedPlayers(self)
|
||
local set = __TS__New(Set)
|
||
do
|
||
local i = 0
|
||
while i < DOTA_MAX_PLAYERS do
|
||
do
|
||
local playerId = i
|
||
if not PlayerResource:IsValidPlayerID(playerId) then
|
||
goto __continue18
|
||
end
|
||
if self:isPlayerActivelyConnected(playerId) then
|
||
set:add(playerId)
|
||
end
|
||
end
|
||
::__continue18::
|
||
i = i + 1
|
||
end
|
||
end
|
||
return set
|
||
end
|
||
function GameStatsTracker.prototype.getDisconnectTimeoutForPlayer(self, playerId)
|
||
if not PlayerResource:IsValidPlayer(playerId) then
|
||
return ABANDON_DISCONNECT_TIMEOUT_SECONDS
|
||
end
|
||
local cs = PlayerResource:GetConnectionState(playerId)
|
||
if cs == DOTA_CONNECTION_STATE.ABANDONED or cs == DOTA_CONNECTION_STATE.FAILED then
|
||
return ABANDON_DISCONNECT_TIMEOUT_SECONDS
|
||
end
|
||
return DISCONNECT_TIMEOUT_SECONDS
|
||
end
|
||
function GameStatsTracker.prototype.setupListeners(self)
|
||
ListenToGameEvent(
|
||
"game_rules_state_change",
|
||
function() return self:onGameStateChange() end,
|
||
self
|
||
)
|
||
ListenToGameEvent(
|
||
"npc_spawned",
|
||
function(____, event) return self:onNpcSpawned(event) end,
|
||
self
|
||
)
|
||
end
|
||
function GameStatsTracker.prototype.onGameStateChange(self)
|
||
local gameState = GameRules:State_Get()
|
||
if gameState == 5 and not self.isGameStarted then
|
||
self:onGameStart()
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.onNpcSpawned(self, event)
|
||
local unit = EntIndexToHScript(event.entindex)
|
||
if not unit or not unit:IsRealHero() then
|
||
return
|
||
end
|
||
local playerId = unit:GetPlayerOwnerID()
|
||
if playerId == -1 or not PlayerResource:IsValidPlayer(playerId) or PlayerResource:IsFakeClient(playerId) then
|
||
return
|
||
end
|
||
if self.playerGameData:has(playerId) then
|
||
return
|
||
end
|
||
if self.registerStartInFlight:has(playerId) then
|
||
return
|
||
end
|
||
local hero = unit
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local gameState = GameRules:State_Get()
|
||
____print(
|
||
nil,
|
||
(((((("[GameStatsTracker] 🔍 npc_spawned: герой " .. hero:GetUnitName()) .. ", игрок ") .. playerName) .. ", gameState: ") .. tostring(gameState)) .. ", isGameStarted: ") .. tostring(self.isGameStarted)
|
||
)
|
||
if self.isGameEnded then
|
||
____print(nil, "[GameStatsTracker] ⚠️ npc_spawned: игра уже завершена, пропускаем")
|
||
return
|
||
end
|
||
local eligibleNow = collectStatsEligiblePlayerIds(nil)
|
||
local inEligibleRoster = false
|
||
for ____, id in ipairs(eligibleNow) do
|
||
if id == playerId then
|
||
inEligibleRoster = true
|
||
break
|
||
end
|
||
end
|
||
if not inEligibleRoster then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⏭️ npc_spawned: слот " .. tostring(playerId)) .. " не в составе лобби для статистики, пропуск"
|
||
)
|
||
return
|
||
end
|
||
if self.isGameStarted then
|
||
if not self.sessionParticipantIdSet:has(playerId) then
|
||
____print(nil, ("[GameStatsTracker] ⏭️ npc_spawned: игрок " .. playerName) .. " вне снимка сессии, пропуск")
|
||
return
|
||
end
|
||
____print(
|
||
nil,
|
||
((((("[GameStatsTracker] 🦸 Герой появился! Регистрируем игрока " .. playerName) .. " (герой: ") .. hero:GetUnitName()) .. ", gameState: ") .. tostring(gameState)) .. ")"
|
||
)
|
||
self:registerPlayerGame(playerId, hero)
|
||
return
|
||
end
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⏳ npc_spawned: игра еще не началась (state: " .. tostring(gameState)) .. "), сохраняем для отложенной регистрации"
|
||
)
|
||
self.pendingPlayers:add(playerId)
|
||
end
|
||
function GameStatsTracker.prototype.onGameStart(self)
|
||
Difficulty:ensureHeroSelectionResolvedForMatchStart()
|
||
self.isGameStarted = true
|
||
self.isGameEnded = false
|
||
self.gameStartTime = GameRules:GetGameTime()
|
||
self.playerGameData:clear()
|
||
self.lastKnownPlayers:clear()
|
||
self.playerDisconnectTime:clear()
|
||
self.registerStartInFlight:clear()
|
||
self.sharedMatchId = nil
|
||
local gameTimeMs = math.floor(GameRules:GetGameTime() * 1000)
|
||
local rHigh = RandomInt(1, 2147483647)
|
||
local rLow = RandomInt(1, 2147483647)
|
||
self.currentSessionId = (((("session_" .. tostring(gameTimeMs)) .. "_") .. tostring(rHigh)) .. "_") .. tostring(rLow)
|
||
self.sessionParticipantsOrdered = collectStatsEligiblePlayerIds(nil)
|
||
self.sessionParticipantIdSet = __TS__New(Set, self.sessionParticipantsOrdered)
|
||
self.sessionParticipantsSteamOrdered = __TS__ArrayMap(
|
||
self.sessionParticipantsOrdered,
|
||
function(____, id) return tostring(PlayerResource:GetSteamAccountID(id)) end
|
||
)
|
||
__TS__ArraySort(
|
||
self.sessionParticipantsSteamOrdered,
|
||
function(____, a, b) return (tonumber(a) or 0) - (tonumber(b) or 0) end
|
||
)
|
||
rawPrintFn(
|
||
nil,
|
||
(("[GameStatsTracker] Участники сессии (" .. tostring(#self.sessionParticipantsOrdered)) .. "): ") .. table.concat(self.sessionParticipantsSteamOrdered, ",")
|
||
)
|
||
self.matchBootstrapPlayerId = nil
|
||
self.queuedPlayersForMatchBootstrap:clear()
|
||
self.allLeftDebounceSinceGameTime = nil
|
||
self.isStatsDisabledForCurrentMatch = self:shouldDisableStatsForCurrentMatch()
|
||
self.statsDisabledReason = self.isStatsDisabledForCurrentMatch and self:getStatsDisabledReason() or ""
|
||
rawPrintFn(
|
||
nil,
|
||
((("[GameStatsTracker] Старт матча: session=" .. self.currentSessionId) .. ", gameTime=") .. tostring(math.floor(self.gameStartTime))) .. "s"
|
||
)
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
____print(nil, "[GameStatsTracker] 🚫 Трекинг матча отключен: " .. self.statsDisabledReason)
|
||
end
|
||
if self.pendingPlayers.size > 0 then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] 📋 Регистрируем " .. tostring(self.pendingPlayers.size)) .. " игроков, которые появились до начала игры"
|
||
)
|
||
for ____, playerId in __TS__Iterator(self.pendingPlayers) do
|
||
do
|
||
if not self.sessionParticipantIdSet:has(playerId) then
|
||
goto __continue46
|
||
end
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if hero and hero:IsRealHero() then
|
||
self:registerPlayerGame(playerId, hero)
|
||
end
|
||
end
|
||
::__continue46::
|
||
end
|
||
self.pendingPlayers:clear()
|
||
end
|
||
self:startGameForAllPlayers()
|
||
self:startDisconnectCheck()
|
||
self:startHeartbeat()
|
||
end
|
||
function GameStatsTracker.prototype.shouldDisableStatsForCurrentMatch(self)
|
||
if not ALLOW_STATS_IN_TOOLS_MODE and self:isToolsModeActive() then
|
||
return true
|
||
end
|
||
if not ALLOW_STATS_WITH_CHEATS and self:isCheatsEnabled() then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
function GameStatsTracker.prototype.getStatsDisabledReason(self)
|
||
local reasons = {}
|
||
if not ALLOW_STATS_IN_TOOLS_MODE and self:isToolsModeActive() then
|
||
reasons[#reasons + 1] = "tools_mode"
|
||
end
|
||
if not ALLOW_STATS_WITH_CHEATS and self:isCheatsEnabled() then
|
||
reasons[#reasons + 1] = "cheats_enabled"
|
||
end
|
||
return table.concat(reasons, "+")
|
||
end
|
||
function GameStatsTracker.prototype.isToolsModeActive(self)
|
||
do
|
||
local function ____catch()
|
||
return true, false
|
||
end
|
||
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
||
local fn = _G.IsInToolsMode
|
||
local ____fn_0
|
||
if fn then
|
||
____fn_0 = fn(nil)
|
||
else
|
||
____fn_0 = false
|
||
end
|
||
return true, ____fn_0
|
||
end)
|
||
if not ____try then
|
||
____hasReturned, ____returnValue = ____catch()
|
||
end
|
||
if ____hasReturned then
|
||
return ____returnValue
|
||
end
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.isCheatsEnabled(self)
|
||
do
|
||
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
||
local ____this_2
|
||
____this_2 = GameRules
|
||
local ____opt_1 = ____this_2.IsCheatMode
|
||
if ____opt_1 ~= nil then
|
||
____opt_1 = ____opt_1(____this_2)
|
||
end
|
||
if ____opt_1 then
|
||
return true, true
|
||
end
|
||
end)
|
||
if ____try and ____hasReturned then
|
||
return ____returnValue
|
||
end
|
||
end
|
||
do
|
||
local ____try, ____hasReturned, ____returnValue = pcall(function()
|
||
local cv = _G.Convars
|
||
if cv and type(cv.GetBool) == "function" and cv:GetBool("sv_cheats") then
|
||
return true, true
|
||
end
|
||
end)
|
||
if ____try and ____hasReturned then
|
||
return ____returnValue
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
function GameStatsTracker.prototype.startDisconnectCheck(self)
|
||
if self.disconnectCheckInterval ~= nil then
|
||
Timers:RemoveTimer(self.disconnectCheckInterval)
|
||
end
|
||
self.disconnectCheckInterval = Timers:CreateTimer(
|
||
2,
|
||
function()
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return nil
|
||
end
|
||
self:checkForDisconnectedPlayers()
|
||
return 2
|
||
end
|
||
)
|
||
end
|
||
function GameStatsTracker.prototype.checkForDisconnectedPlayers(self)
|
||
local currentTime = GameRules:GetGameTime()
|
||
local activelyConnected = self:collectActivelyConnectedPlayers()
|
||
for ____, playerId in __TS__Iterator(activelyConnected) do
|
||
if self.playerDisconnectTime:has(playerId) then
|
||
self.playerDisconnectTime:delete(playerId)
|
||
end
|
||
end
|
||
for ____, playerId in __TS__Iterator(self.lastKnownPlayers) do
|
||
do
|
||
if not self.playerGameData:has(playerId) then
|
||
goto __continue72
|
||
end
|
||
if not PlayerResource:IsValidPlayer(playerId) then
|
||
if not self.playerDisconnectTime:has(playerId) then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ Игрок " .. tostring(playerId)) .. " больше не валиден (полностью вышел из игры)"
|
||
)
|
||
self.playerDisconnectTime:set(playerId, currentTime)
|
||
else
|
||
local disconnectTime = self.playerDisconnectTime:get(playerId)
|
||
if currentTime - disconnectTime >= self:getDisconnectTimeoutForPlayer(playerId) then
|
||
self:handlePermanentDisconnect(playerId)
|
||
end
|
||
end
|
||
goto __continue72
|
||
end
|
||
if activelyConnected:has(playerId) then
|
||
goto __continue72
|
||
end
|
||
if not self.playerDisconnectTime:has(playerId) then
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local cs = PlayerResource:GetConnectionState(playerId)
|
||
____print(
|
||
nil,
|
||
((((("[GameStatsTracker] ⚠️ Игрок " .. playerName) .. " (SteamID: ") .. steamId) .. ") отключился (connectionState: ") .. tostring(cs)) .. ")"
|
||
)
|
||
self.playerDisconnectTime:set(playerId, currentTime)
|
||
else
|
||
local disconnectTime = self.playerDisconnectTime:get(playerId)
|
||
if currentTime - disconnectTime >= self:getDisconnectTimeoutForPlayer(playerId) then
|
||
self:handlePermanentDisconnect(playerId)
|
||
end
|
||
end
|
||
end
|
||
::__continue72::
|
||
end
|
||
for ____, playerId in __TS__Iterator(self:collectEffectivePlayersInGame()) do
|
||
self.lastKnownPlayers:add(playerId)
|
||
end
|
||
for ____, playerId in __TS__Iterator(self.playerGameData:keys()) do
|
||
self.lastKnownPlayers:add(playerId)
|
||
end
|
||
if self.isGameStarted and not self.isGameEnded and self.playerGameData.size > 0 then
|
||
if activelyConnected.size == 0 then
|
||
if self.allLeftDebounceSinceGameTime == nil then
|
||
self.allLeftDebounceSinceGameTime = currentTime
|
||
elseif currentTime - self.allLeftDebounceSinceGameTime >= ALL_LEFT_DEBOUNCE_SECONDS then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ Все игроки покинули игру (debounce " .. tostring(ALL_LEFT_DEBOUNCE_SECONDS)) .. "s). Сохраняем результаты и завершаем игру."
|
||
)
|
||
local playersToHandle = {}
|
||
for ____, playerId in __TS__Iterator(self.playerGameData:keys()) do
|
||
playersToHandle[#playersToHandle + 1] = playerId
|
||
end
|
||
for ____, playerId in ipairs(playersToHandle) do
|
||
self:handlePermanentDisconnect(playerId)
|
||
end
|
||
self.isGameEnded = true
|
||
self.allLeftDebounceSinceGameTime = nil
|
||
self:stopDisconnectCheck()
|
||
self:stopHeartbeat()
|
||
end
|
||
else
|
||
self.allLeftDebounceSinceGameTime = nil
|
||
end
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.handlePermanentDisconnect(self, playerId)
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local disconnectDuration = GameRules:GetGameTime() - (self.playerDisconnectTime:get(playerId) or self.gameStartTime)
|
||
____print(nil, ((("[GameStatsTracker] ⚠️ Игрок " .. playerName) .. " (SteamID: ") .. steamId) .. ") окончательно отключился")
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] Время отключения: " .. tostring(math.floor(disconnectDuration))) .. " секунд назад"
|
||
)
|
||
self.playerDisconnectTime:delete(playerId)
|
||
self.lastKnownPlayers:delete(playerId)
|
||
self:onPlayerDisconnect(playerId)
|
||
end
|
||
function GameStatsTracker.prototype.startGameForAllPlayers(self)
|
||
local gameState = GameRules:State_Get()
|
||
if gameState ~= 5 and not self.isGameStarted then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ Игра не в состоянии IN_PROGRESS (текущее состояние: " .. tostring(gameState)) .. ") и еще не началась, пропускаем регистрацию"
|
||
)
|
||
return
|
||
end
|
||
if gameState ~= 5 and self.isGameStarted then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ Игра началась, но состояние не IN_PROGRESS (" .. tostring(gameState)) .. "), продолжаем регистрацию"
|
||
)
|
||
end
|
||
local currentDifficulty = Difficulty.leader or "normal"
|
||
____print(nil, ("[GameStatsTracker] 📋 Начинаем регистрацию игры для всех игроков (сложность: " .. currentDifficulty) .. ")")
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] 📊 Снимок лобби: " .. tostring(#self.sessionParticipantsOrdered)) .. " слотов (только реальные игроки)"
|
||
)
|
||
local playersCount = 0
|
||
local skippedNoHero = 0
|
||
for ____, playerId in ipairs(self.sessionParticipantsOrdered) do
|
||
do
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] 🔍 Слот " .. tostring(playerId)) .. " (из снимка сессии)..."
|
||
)
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if not hero then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ У игрока " .. tostring(playerId)) .. " ещё нет героя"
|
||
)
|
||
skippedNoHero = skippedNoHero + 1
|
||
goto __continue100
|
||
end
|
||
if self:registerPlayerGame(playerId, hero) then
|
||
playersCount = playersCount + 1
|
||
end
|
||
end
|
||
::__continue100::
|
||
end
|
||
____print(nil, "[GameStatsTracker] 📊 Регистрация завершена:")
|
||
____print(
|
||
nil,
|
||
"[GameStatsTracker] ✅ Запросов на регистрацию отправлено: " .. tostring(playersCount)
|
||
)
|
||
____print(
|
||
nil,
|
||
"[GameStatsTracker] ⚠️ Пропущено (герой не готов): " .. tostring(skippedNoHero)
|
||
)
|
||
if playersCount == 0 then
|
||
____print(nil, "[GameStatsTracker] ⚠️ ВНИМАНИЕ: Не найдено ни одного игрока для регистрации!")
|
||
____print(nil, "[GameStatsTracker] Возможно, игра началась до того, как игроки выбрали героев.")
|
||
____print(nil, "[GameStatsTracker] Будем регистрировать игроков при появлении их героев.")
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.registerPlayerGame(self, playerId, hero)
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
return false
|
||
end
|
||
if not self.sessionParticipantIdSet:has(playerId) then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⏭️ registerPlayerGame: слот " .. tostring(playerId)) .. " не в снимке сессии"
|
||
)
|
||
return false
|
||
end
|
||
if self.playerGameData:has(playerId) then
|
||
return false
|
||
end
|
||
if self.sharedMatchId == nil and self.matchBootstrapPlayerId ~= nil and self.matchBootstrapPlayerId ~= playerId then
|
||
self.queuedPlayersForMatchBootstrap:add(playerId)
|
||
local queuedName = PlayerResource:GetPlayerName(playerId)
|
||
____print(nil, "[GameStatsTracker] ⏳ Ожидаем общий match_id, откладываем регистрацию игрока " .. queuedName)
|
||
return false
|
||
end
|
||
if self.registerStartInFlight:has(playerId) then
|
||
return false
|
||
end
|
||
if self.sharedMatchId == nil and self.matchBootstrapPlayerId == nil then
|
||
self.matchBootstrapPlayerId = playerId
|
||
local bootstrapName = PlayerResource:GetPlayerName(playerId)
|
||
____print(nil, "[GameStatsTracker] 🚀 Bootstrap match_id через игрока " .. bootstrapName)
|
||
end
|
||
self.registerStartInFlight:add(playerId)
|
||
self.lastKnownPlayers:add(playerId)
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local heroLevel = hero:GetLevel()
|
||
local currentDifficulty = Difficulty.leader or "normal"
|
||
____print(
|
||
nil,
|
||
(((((("[GameStatsTracker] 👤 Регистрируем игрока " .. playerName) .. " (SteamID: ") .. steamId) .. "), герой: ") .. hero:GetUnitName()) .. ", уровень: ") .. tostring(heroLevel)
|
||
)
|
||
local request = CreateHTTPRequest("POST", SERVER_CONFIG.API_URL .. "/game/start")
|
||
setApiHeadersLong(nil, request)
|
||
local dataToSend = {
|
||
steam_id = steamId,
|
||
hero = hero:GetUnitName(),
|
||
hero_level = heroLevel,
|
||
difficulty = currentDifficulty,
|
||
player_name = playerName,
|
||
match_id = self.sharedMatchId,
|
||
session_id = self.currentSessionId,
|
||
session_participants = self.sessionParticipantsSteamOrdered
|
||
}
|
||
local dsContractStart = Difficulty:getActiveDeathSentenceContractPayload()
|
||
if dsContractStart then
|
||
dataToSend.death_sentence_contract = dsContractStart
|
||
end
|
||
request:SetHTTPRequestRawPostBody(
|
||
"application/json",
|
||
encodeApiBody(nil, dataToSend)
|
||
)
|
||
request:Send(function(result)
|
||
self.registerStartInFlight:delete(playerId)
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
do
|
||
local function ____catch(e)
|
||
____print(nil, ("[GameStatsTracker] ❌ Ошибка парсинга ответа для игрока " .. playerName) .. ":", e)
|
||
if not self.playerGameData:has(playerId) then
|
||
self.lastKnownPlayers:delete(playerId)
|
||
end
|
||
self:onRegisterStartFinished(playerId)
|
||
end
|
||
local ____try, ____hasReturned = pcall(function()
|
||
local responseData = {json.decode(result.Body)}
|
||
local data = nil
|
||
if __TS__ArrayIsArray(responseData) and #responseData > 0 then
|
||
data = responseData[1]
|
||
elseif responseData.value ~= nil then
|
||
data = responseData.value
|
||
else
|
||
data = responseData
|
||
end
|
||
if data and data.game_id and data.match_id then
|
||
if self.sharedMatchId == nil then
|
||
self.sharedMatchId = __TS__Number(data.match_id) or nil
|
||
if self.sharedMatchId ~= nil then
|
||
____print(
|
||
nil,
|
||
"[GameStatsTracker] 🔗 Зафиксирован общий Match ID: " .. tostring(self.sharedMatchId)
|
||
)
|
||
end
|
||
end
|
||
self.playerGameData:set(playerId, {game_id = data.game_id, match_id = data.match_id})
|
||
self.lastKnownPlayers:add(playerId)
|
||
____print(nil, "[GameStatsTracker] ✅ Игра зарегистрирована для игрока " .. playerName)
|
||
____print(
|
||
nil,
|
||
(("[GameStatsTracker] Game ID: " .. tostring(data.game_id)) .. ", Match ID: ") .. tostring(data.match_id)
|
||
)
|
||
else
|
||
____print(nil, ("[GameStatsTracker] ⚠️ Неполный ответ от сервера для игрока " .. playerName) .. ":", data)
|
||
if not self.playerGameData:has(playerId) then
|
||
self.lastKnownPlayers:delete(playerId)
|
||
end
|
||
end
|
||
self:onRegisterStartFinished(playerId)
|
||
end)
|
||
if not ____try then
|
||
____catch(____hasReturned)
|
||
end
|
||
end
|
||
else
|
||
____print(
|
||
nil,
|
||
(("[GameStatsTracker] ❌ Ошибка при регистрации игры для игрока " .. playerName) .. ": StatusCode ") .. tostring(result.StatusCode)
|
||
)
|
||
if not self.playerGameData:has(playerId) then
|
||
self.lastKnownPlayers:delete(playerId)
|
||
end
|
||
self:onRegisterStartFinished(playerId)
|
||
end
|
||
end)
|
||
return true
|
||
end
|
||
function GameStatsTracker.prototype.onRegisterStartFinished(self, playerId)
|
||
if self.matchBootstrapPlayerId ~= playerId then
|
||
return
|
||
end
|
||
self.matchBootstrapPlayerId = nil
|
||
if self.queuedPlayersForMatchBootstrap.size == 0 then
|
||
return
|
||
end
|
||
local queued = __TS__ArrayFrom(self.queuedPlayersForMatchBootstrap)
|
||
self.queuedPlayersForMatchBootstrap:clear()
|
||
____print(
|
||
nil,
|
||
"[GameStatsTracker] ▶️ Продолжаем регистрацию отложенных игроков: " .. tostring(#queued)
|
||
)
|
||
for ____, queuedPlayerId in ipairs(queued) do
|
||
do
|
||
local hero = PlayerResource:GetSelectedHeroEntity(queuedPlayerId)
|
||
if not hero or not hero:IsRealHero() then
|
||
goto __continue131
|
||
end
|
||
self:registerPlayerGame(queuedPlayerId, hero)
|
||
end
|
||
::__continue131::
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.onVictory(self, callback)
|
||
if self.isGameEnded then
|
||
return
|
||
end
|
||
if not self.isGameStarted then
|
||
____print(nil, "[GameStatsTracker] onVictory: матч не был отмечен как начатый — колбэк для UI, без сохранения API")
|
||
self.isGameEnded = true
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
local duration = GameRules:GetGameTime() - self.gameStartTime
|
||
rawPrintFn(
|
||
nil,
|
||
("[GameStatsTracker] Завершение матча: result=win, duration=" .. tostring(math.floor(duration))) .. "s"
|
||
)
|
||
self:stopDisconnectCheck()
|
||
self:stopHeartbeat()
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
____print(nil, ("[GameStatsTracker] 🚫 Победа не сохраняется в статистику (" .. self.statsDisabledReason) .. ")")
|
||
self.isGameEnded = true
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
self:saveAllPlayersStats(true, callback)
|
||
self.isGameEnded = true
|
||
end
|
||
function GameStatsTracker.prototype.onDefeat(self, callback)
|
||
if self.isGameEnded then
|
||
return
|
||
end
|
||
if not self.isGameStarted then
|
||
____print(nil, "[GameStatsTracker] onDefeat: матч не был отмечен как начатый — колбэк для UI, без сохранения API")
|
||
self.isGameEnded = true
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
local duration = GameRules:GetGameTime() - self.gameStartTime
|
||
rawPrintFn(
|
||
nil,
|
||
("[GameStatsTracker] Завершение матча: result=loss, duration=" .. tostring(math.floor(duration))) .. "s"
|
||
)
|
||
self:stopDisconnectCheck()
|
||
self:stopHeartbeat()
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
____print(nil, ("[GameStatsTracker] 🚫 Поражение не сохраняется в статистику (" .. self.statsDisabledReason) .. ")")
|
||
self.isGameEnded = true
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
self:saveAllPlayersStats(false, callback)
|
||
self.isGameEnded = true
|
||
end
|
||
function GameStatsTracker.prototype.isCurrentMatchStatsDisabled(self)
|
||
return self.isStatsDisabledForCurrentMatch
|
||
end
|
||
function GameStatsTracker.prototype.shouldBlockMatchEndRewards(self)
|
||
if not ALLOW_STATS_IN_TOOLS_MODE and self:isToolsModeActive() and not ALLOW_MATCH_END_REWARDS_IN_TOOLS then
|
||
return true
|
||
end
|
||
if not ALLOW_STATS_WITH_CHEATS and self:isCheatsEnabled() and not ALLOW_MATCH_END_REWARDS_WITH_CHEATS then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
function GameStatsTracker.prototype.startHeartbeat(self)
|
||
if self.heartbeatInterval ~= nil then
|
||
Timers:RemoveTimer(self.heartbeatInterval)
|
||
end
|
||
self.heartbeatInterval = Timers:CreateTimer(
|
||
HEARTBEAT_INTERVAL_SECONDS,
|
||
function()
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return nil
|
||
end
|
||
self:sendHeartbeat()
|
||
return HEARTBEAT_INTERVAL_SECONDS
|
||
end
|
||
)
|
||
self:sendHeartbeat()
|
||
end
|
||
function GameStatsTracker.prototype.stopHeartbeat(self)
|
||
if self.heartbeatInterval ~= nil then
|
||
Timers:RemoveTimer(self.heartbeatInterval)
|
||
self.heartbeatInterval = nil
|
||
end
|
||
if self.heartbeatRetryTimer ~= nil then
|
||
Timers:RemoveTimer(self.heartbeatRetryTimer)
|
||
self.heartbeatRetryTimer = nil
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.scheduleHeartbeatRetry(self)
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return
|
||
end
|
||
if self.heartbeatRetryTimer ~= nil then
|
||
return
|
||
end
|
||
self.heartbeatRetryTimer = Timers:CreateTimer(
|
||
HEARTBEAT_RETRY_SECONDS,
|
||
function()
|
||
self.heartbeatRetryTimer = nil
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return nil
|
||
end
|
||
self:sendHeartbeat()
|
||
return nil
|
||
end
|
||
)
|
||
end
|
||
function GameStatsTracker.prototype.sendHeartbeat(self)
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return
|
||
end
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
return
|
||
end
|
||
do
|
||
local i = 0
|
||
while i < DOTA_MAX_PLAYERS do
|
||
do
|
||
local playerId = i
|
||
if not PlayerResource:IsValidPlayerID(playerId) then
|
||
goto __continue165
|
||
end
|
||
if not self:isPlayerActivelyConnected(playerId) then
|
||
goto __continue165
|
||
end
|
||
local gameData = self.playerGameData:get(playerId)
|
||
if not gameData or not gameData.game_id then
|
||
goto __continue165
|
||
end
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local requestData = {steam_id = steamId, game_id = gameData.game_id}
|
||
local request = CreateHTTPRequestScriptVM("POST", SERVER_CONFIG.API_URL .. "/game/heartbeat")
|
||
setApiHeadersLong(nil, request)
|
||
request:SetHTTPRequestRawPostBody(
|
||
"application/json",
|
||
encodeApiBody(nil, requestData)
|
||
)
|
||
request:Send(function(result)
|
||
local accepted = false
|
||
if result.StatusCode >= 200 and result.StatusCode < 300 then
|
||
do
|
||
local function ____catch(e)
|
||
____print(nil, ("[GameStatsTracker] ⚠️ Ошибка парсинга ответа heartbeat для игрока " .. playerName) .. ":", e)
|
||
end
|
||
local ____try, ____hasReturned = pcall(function()
|
||
local responseData = {json.decode(result.Body)}
|
||
local data = nil
|
||
if __TS__ArrayIsArray(responseData) and #responseData > 0 then
|
||
data = responseData[1]
|
||
elseif responseData.value ~= nil then
|
||
data = responseData.value
|
||
elseif responseData and type(responseData) == "table" then
|
||
data = responseData
|
||
end
|
||
if data and data.success then
|
||
accepted = true
|
||
____print(
|
||
nil,
|
||
((("[GameStatsTracker] 💓 Heartbeat отправлен для игрока " .. playerName) .. " (Game ID: ") .. tostring(gameData.game_id)) .. ")"
|
||
)
|
||
end
|
||
end)
|
||
if not ____try then
|
||
____catch(____hasReturned)
|
||
end
|
||
end
|
||
else
|
||
____print(
|
||
nil,
|
||
(("[GameStatsTracker] ⚠️ Ошибка отправки heartbeat для игрока " .. playerName) .. ": StatusCode ") .. tostring(result.StatusCode)
|
||
)
|
||
end
|
||
if not accepted then
|
||
self:scheduleHeartbeatRetry()
|
||
end
|
||
end)
|
||
end
|
||
::__continue165::
|
||
i = i + 1
|
||
end
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.stopDisconnectCheck(self)
|
||
if self.disconnectCheckInterval ~= nil then
|
||
Timers:RemoveTimer(self.disconnectCheckInterval)
|
||
self.disconnectCheckInterval = nil
|
||
end
|
||
self.playerDisconnectTime:clear()
|
||
end
|
||
function GameStatsTracker.prototype.onPlayerDisconnect(self, playerId)
|
||
if not self.isGameStarted or self.isGameEnded then
|
||
return
|
||
end
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
self.playerGameData:delete(playerId)
|
||
return
|
||
end
|
||
local playerName = PlayerResource:GetPlayerName(playerId)
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local gameData = self.playerGameData:get(playerId)
|
||
if not gameData or not gameData.game_id then
|
||
____print(nil, ("[GameStatsTracker] ⚠️ Игрок " .. playerName) .. " отключился до начала игры, данные не сохраняются")
|
||
self.playerGameData:delete(playerId)
|
||
return
|
||
end
|
||
____print(
|
||
nil,
|
||
((("[GameStatsTracker] 💾 Сохраняем статистику отключившегося игрока " .. playerName) .. " (Game ID: ") .. tostring(gameData.game_id)) .. ")"
|
||
)
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
local heroName = hero and hero:GetUnitName() or "unknown"
|
||
local heroLevel = hero and hero:GetLevel() or 1
|
||
local ____hero_3
|
||
if hero then
|
||
____hero_3 = hero:HasScepter()
|
||
else
|
||
____hero_3 = false
|
||
end
|
||
local hasAghs = ____hero_3
|
||
local ____hero_4
|
||
if hero then
|
||
____hero_4 = HasShard(nil, hero)
|
||
else
|
||
____hero_4 = false
|
||
end
|
||
local hasShard = ____hero_4
|
||
local duration = GameRules:GetGameTime() - self.gameStartTime
|
||
local kills = hero and PlayerResource:GetNearbyCreepDeaths(playerId) or 0
|
||
local deaths = hero and PlayerResource:GetDeaths(playerId) or 0
|
||
local netWorth = hero and PlayerResource:GetNetWorth(playerId) or 0
|
||
local combat = MatchEndCombatStats:getInstance()
|
||
local outgoingDamage = combat:getOutgoingDamageSum(playerId)
|
||
local incomingDamage = combat:getIncomingDamageSum(playerId)
|
||
local items = {}
|
||
local permanentModifiers = {}
|
||
if hero then
|
||
do
|
||
local i = 0
|
||
while i < 9 do
|
||
local item = hero:GetItemInSlot(i)
|
||
if item and item:GetAbilityName() then
|
||
items[#items + 1] = item:GetAbilityName()
|
||
end
|
||
i = i + 1
|
||
end
|
||
end
|
||
local modifierCounts = {}
|
||
do
|
||
local i = 0
|
||
while i < hero:GetModifierCount() do
|
||
local modifierName = hero:GetModifierNameByIndex(i)
|
||
if modifierName ~= nil then
|
||
local buff = hero:FindModifierByName(modifierName)
|
||
if buff then
|
||
local buffDuration = buff:GetDuration()
|
||
local remainingTime = buff:GetRemainingTime()
|
||
if buffDuration == -1 or buffDuration > 0 and remainingTime > 99999 then
|
||
modifierCounts[modifierName] = (modifierCounts[modifierName] or 0) + 1
|
||
end
|
||
end
|
||
end
|
||
i = i + 1
|
||
end
|
||
end
|
||
for modName in pairs(modifierCounts) do
|
||
permanentModifiers[#permanentModifiers + 1] = (modName .. ":") .. tostring(modifierCounts[modName])
|
||
end
|
||
end
|
||
local currentDifficulty = Difficulty.leader or "normal"
|
||
local function finishDisconnectSave()
|
||
self:saveGameResult(
|
||
steamId,
|
||
false,
|
||
duration,
|
||
kills,
|
||
deaths,
|
||
netWorth,
|
||
outgoingDamage,
|
||
incomingDamage,
|
||
heroName,
|
||
heroLevel,
|
||
items,
|
||
permanentModifiers,
|
||
hasAghs,
|
||
hasShard,
|
||
currentDifficulty,
|
||
gameData.game_id,
|
||
function()
|
||
____print(nil, ("[GameStatsTracker] ✅ Статистика отключившегося игрока " .. playerName) .. " сохранена")
|
||
self.playerGameData:delete(playerId)
|
||
end
|
||
)
|
||
end
|
||
if currentDifficulty == "death_sentence" then
|
||
Difficulty:applyDeathSentenceContractDurabilityOnLoss(finishDisconnectSave)
|
||
else
|
||
finishDisconnectSave(nil)
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.saveAllPlayersStats(self, isVictory, callback)
|
||
if self.isStatsDisabledForCurrentMatch then
|
||
____print(nil, ("[GameStatsTracker] 🚫 saveAllPlayersStats пропущен (" .. self.statsDisabledReason) .. ")")
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
local duration = GameRules:GetGameTime() - self.gameStartTime
|
||
local savedCount = 0
|
||
local resultText = isVictory and "ПОБЕДА" or "ПОРАЖЕНИЕ"
|
||
____print(nil, ("[GameStatsTracker] 💾 Сохраняем статистику для всех игроков (" .. resultText) .. ")")
|
||
local idsToSave = {}
|
||
do
|
||
local i = 0
|
||
while i < DOTA_MAX_PLAYERS do
|
||
do
|
||
local playerId = i
|
||
if not PlayerResource:IsValidPlayerID(playerId) then
|
||
goto __continue201
|
||
end
|
||
if not PlayerResource:IsValidPlayer(playerId) or PlayerResource:IsFakeClient(playerId) then
|
||
goto __continue201
|
||
end
|
||
if not self.playerGameData:has(playerId) then
|
||
goto __continue201
|
||
end
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if hero then
|
||
idsToSave[#idsToSave + 1] = playerId
|
||
end
|
||
end
|
||
::__continue201::
|
||
i = i + 1
|
||
end
|
||
end
|
||
local totalPlayers = #idsToSave
|
||
if totalPlayers == 0 then
|
||
____print(nil, "[GameStatsTracker] ⚠️ Нет активных игроков для сохранения статистики")
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
return
|
||
end
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] 📊 Сохраняем статистику для " .. tostring(totalPlayers)) .. " игроков"
|
||
)
|
||
local combat = MatchEndCombatStats:getInstance()
|
||
for ____, playerId in ipairs(idsToSave) do
|
||
do
|
||
local hero = PlayerResource:GetSelectedHeroEntity(playerId)
|
||
if not hero then
|
||
savedCount = savedCount + 1
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] ⚠️ Герой пропал для слота " .. tostring(playerId)) .. " перед save — учитываем чанк"
|
||
)
|
||
if savedCount >= totalPlayers and callback then
|
||
____print(
|
||
nil,
|
||
((("[GameStatsTracker] 🎉 Все чанки учтены (" .. tostring(savedCount)) .. "/") .. tostring(totalPlayers)) .. ")"
|
||
)
|
||
callback(nil)
|
||
end
|
||
goto __continue208
|
||
end
|
||
local steamId = tostring(PlayerResource:GetSteamAccountID(playerId))
|
||
local heroName = hero:GetUnitName()
|
||
local gameData = self.playerGameData:get(playerId)
|
||
local gameId = gameData and gameData.game_id
|
||
local kills = PlayerResource:GetNearbyCreepDeaths(playerId)
|
||
local deaths = PlayerResource:GetDeaths(playerId)
|
||
local netWorth = PlayerResource:GetNetWorth(playerId)
|
||
local outgoingDamage = combat:getOutgoingDamageSum(playerId)
|
||
local incomingDamage = combat:getIncomingDamageSum(playerId)
|
||
local heroLevel = hero:GetLevel()
|
||
local items = {}
|
||
local permanentModifiers = {}
|
||
local hasAghs = hero:HasScepter()
|
||
local hasShard = HasShard(nil, hero)
|
||
do
|
||
local slot = 0
|
||
while slot < 9 do
|
||
local item = hero:GetItemInSlot(slot)
|
||
if item and item:GetAbilityName() then
|
||
items[#items + 1] = item:GetAbilityName()
|
||
end
|
||
slot = slot + 1
|
||
end
|
||
end
|
||
local modifierCounts = {}
|
||
do
|
||
local j = 0
|
||
while j < hero:GetModifierCount() do
|
||
local modifierName = hero:GetModifierNameByIndex(j)
|
||
if modifierName ~= nil then
|
||
local buff = hero:FindModifierByName(modifierName)
|
||
if buff then
|
||
local buffDuration = buff:GetDuration()
|
||
local remainingTime = buff:GetRemainingTime()
|
||
if buffDuration == -1 or buffDuration > 0 and remainingTime > 99999 then
|
||
modifierCounts[modifierName] = (modifierCounts[modifierName] or 0) + 1
|
||
end
|
||
end
|
||
end
|
||
j = j + 1
|
||
end
|
||
end
|
||
for modName in pairs(modifierCounts) do
|
||
permanentModifiers[#permanentModifiers + 1] = (modName .. ":") .. tostring(modifierCounts[modName])
|
||
end
|
||
local currentDifficulty = Difficulty.leader or "normal"
|
||
self:saveGameResult(
|
||
steamId,
|
||
isVictory,
|
||
duration,
|
||
kills,
|
||
deaths,
|
||
netWorth,
|
||
outgoingDamage,
|
||
incomingDamage,
|
||
hero:GetUnitName(),
|
||
heroLevel,
|
||
items,
|
||
permanentModifiers,
|
||
hasAghs,
|
||
hasShard,
|
||
currentDifficulty,
|
||
gameId,
|
||
function()
|
||
savedCount = savedCount + 1
|
||
____print(
|
||
nil,
|
||
((((("[GameStatsTracker] ✅ Статистика сохранена для игрока " .. heroName) .. " (") .. tostring(savedCount)) .. "/") .. tostring(totalPlayers)) .. ")"
|
||
)
|
||
if savedCount >= totalPlayers and callback then
|
||
____print(
|
||
nil,
|
||
("[GameStatsTracker] 🎉 Все статистики сохранены! (" .. tostring(savedCount)) .. " игроков)"
|
||
)
|
||
callback(nil)
|
||
end
|
||
end
|
||
)
|
||
end
|
||
::__continue208::
|
||
end
|
||
end
|
||
function GameStatsTracker.prototype.saveGameResult(self, steamId, isVictory, duration, kills, deaths, score, outgoingDamage, incomingDamage, hero, heroLevel, items, modifiers, hasAghs, hasShard, difficulty, gameId, callback)
|
||
local dataToSend = {
|
||
steam_id = steamId,
|
||
result = isVictory and "win" or "loss",
|
||
duration = math.floor(duration),
|
||
kills = kills,
|
||
deaths = deaths,
|
||
score = score,
|
||
outgoing_damage = math.floor(outgoingDamage),
|
||
incoming_damage = math.floor(incomingDamage),
|
||
hero = hero,
|
||
hero_level = heroLevel,
|
||
items = table.concat(items, ","),
|
||
modifiers = table.concat(modifiers, ","),
|
||
aghanim_scepter = hasAghs,
|
||
aghanim_shard = hasShard,
|
||
gold_earned = score,
|
||
difficulty = difficulty,
|
||
session_id = self.currentSessionId
|
||
}
|
||
if gameId ~= nil then
|
||
dataToSend.game_id = gameId
|
||
end
|
||
local dsContractEnd = Difficulty:getActiveDeathSentenceContractPayload()
|
||
if dsContractEnd then
|
||
dataToSend.death_sentence_contract = dsContractEnd
|
||
end
|
||
local request = CreateHTTPRequest("POST", SERVER_CONFIG.API_URL .. "/game")
|
||
setApiHeadersLong(nil, request)
|
||
request:SetHTTPRequestRawPostBody(
|
||
"application/json",
|
||
encodeApiBody(nil, dataToSend)
|
||
)
|
||
request:Send(function(result)
|
||
if result.StatusCode < 200 or result.StatusCode >= 300 then
|
||
local body = result.Body ~= nil and tostring(result.Body) or ""
|
||
rawPrintFn(
|
||
nil,
|
||
(((("[GameStatsTracker] saveGameResult HTTP " .. tostring(result.StatusCode)) .. " steam_id=") .. steamId) .. " body=") .. body
|
||
)
|
||
end
|
||
if callback then
|
||
callback(nil)
|
||
end
|
||
end)
|
||
end
|
||
return ____exports
|