local ____lualib = require("lualib_bundle") local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray local __TS__NumberIsFinite = ____lualib.__TS__NumberIsFinite local ____exports = {} local ____api_helper = require("api_helper") local setApiHeaders = ____api_helper.setApiHeaders local encodeApiBody = ____api_helper.encodeApiBody local ____server_config = require("server_config") local SERVER_CONFIG = ____server_config.SERVER_CONFIG local ____contracts_registry = require("death_sentence.contracts_registry") local normalizeDeathSentenceTitleIndex = ____contracts_registry.normalizeDeathSentenceTitleIndex local normalizeDeathSentenceContractDurability = ____contracts_registry.normalizeDeathSentenceContractDurability local normalizeDeathSentenceContractDurabilityMax = ____contracts_registry.normalizeDeathSentenceContractDurabilityMax local getDeathSentenceComplicationPickCount = ____contracts_registry.getDeathSentenceComplicationPickCount local LOG_PREFIX = "[DSContractsAPI]" ____exports.DEATH_SENTENCE_CONTRACT_ROSTER_CAP = 90 --- Старые записи (armor/movespeed/full_brutality) приводим к `none` — геймплей только через усложнения-абилки. local function normalizeDeathSentenceTraitId(self, _v) return "none" end local function isValidRarity(self, v) return v == "common" or v == "rare" or v == "epic" or v == "legendary" or v == "mythic" end --- Разбор ответа GET / тела сохранённого JSON → массив экземпляров (0…CAP) или null при неверной структуре. function ____exports.parseDeathSentenceContractRosterPayload(self, raw) if not raw or type(raw) ~= "table" then return nil end local root = raw local wrap = root.death_sentence_contracts if not wrap or type(wrap) ~= "table" then return nil end local rosterRaw = wrap.roster if not __TS__ArrayIsArray(rosterRaw) then return nil end local out = {} for ____, row in ipairs(rosterRaw) do do if not row or type(row) ~= "table" then goto __continue8 end local o = row local instanceId = o.instanceId local serialNum = type(o.serial) == "number" and o.serial or tonumber(o.serial) if type(serialNum) ~= "number" or not __TS__NumberIsFinite(serialNum) then goto __continue8 end local rarity = o.rarity local rmNum = type(o.rewardMultiplier) == "number" and o.rewardMultiplier or tonumber(o.rewardMultiplier) if type(rmNum) ~= "number" or not __TS__NumberIsFinite(rmNum) then goto __continue8 end local compRaw = o.complicationIds if type(instanceId) ~= "string" or #instanceId == 0 then goto __continue8 end if not isValidRarity(nil, rarity) then goto __continue8 end local complicationIds = {} if __TS__ArrayIsArray(compRaw) then for ____, c in ipairs(compRaw) do if type(c) == "string" and #c > 0 then complicationIds[#complicationIds + 1] = c end end end local maxComp = getDeathSentenceComplicationPickCount(nil, rarity) while #complicationIds > maxComp do table.remove(complicationIds) end local titleIndex = normalizeDeathSentenceTitleIndex(nil, o.titleIndex, instanceId) local durability = normalizeDeathSentenceContractDurability(nil, o.durability, instanceId) local ____o_durabilityMax_0 = o.durabilityMax if ____o_durabilityMax_0 == nil then ____o_durabilityMax_0 = o.durability_max end local rawMax = ____o_durabilityMax_0 local durabilityMax = normalizeDeathSentenceContractDurabilityMax(nil, rawMax, durability, instanceId) local inst = { instanceId = instanceId, serial = serialNum, titleIndex = titleIndex, rarity = rarity, rewardMultiplier = rmNum, traitId = normalizeDeathSentenceTraitId(nil, o.traitId), complicationIds = complicationIds, durability = durability, durabilityMax = durabilityMax } local fav = o.favorite if fav == true or fav == 1 then inst.favorite = true elseif fav == false or fav == 0 then inst.favorite = false end local pin = o.pinned if pin == true or pin == 1 then inst.pinned = true elseif pin == false or pin == 0 then inst.pinned = false end out[#out + 1] = inst end ::__continue8:: end while #out > ____exports.DEATH_SENTENCE_CONTRACT_ROSTER_CAP do table.remove(out) end return out end local function decodeJsonBody(self, body) do local function ____catch() return true, nil end local ____try, ____hasReturned, ____returnValue = pcall(function() return true, {json.decode(body)} end) if not ____try then ____hasReturned, ____returnValue = ____catch() end if ____hasReturned then return ____returnValue end end end --- Извлечь объект из ответа API (массив из одного элемента или объект). local function unwrapApiJsonObject(self, decoded) if __TS__ArrayIsArray(decoded) and #decoded > 0 then return decoded[1] end return decoded end --- Загрузить ростер с бэка для steam_id. В колбэке roster=null при ошибке / пусто / невалидно. function ____exports.loadDeathSentenceContractsFromBackend(self, steamId, done) local url = ((SERVER_CONFIG.API_URL .. "/player/") .. steamId) .. "/death_sentence_contracts" local request = CreateHTTPRequestScriptVM("GET", url) setApiHeaders(nil, request) request:Send(function(result) if result.StatusCode < 200 or result.StatusCode >= 300 then print((((LOG_PREFIX .. " GET fail steam=") .. steamId) .. " code=") .. tostring(result.StatusCode)) done(nil, nil) return end local bodyStr = result.Body ~= nil and tostring(result.Body) or "" if #bodyStr == 0 then done(nil, nil) return end local decoded = decodeJsonBody(nil, bodyStr) local obj = unwrapApiJsonObject(nil, decoded) local roster = ____exports.parseDeathSentenceContractRosterPayload(nil, obj) if roster == nil then print((LOG_PREFIX .. " GET parse invalid steam=") .. steamId) done(nil, nil) return end print((((LOG_PREFIX .. " GET ok steam=") .. steamId) .. " count=") .. tostring(#roster)) done(nil, roster) end) end --- Сохранить ростер на бэк для одного steam_id. function ____exports.saveDeathSentenceContractsToBackend(self, steamId, roster, done) local url = ((SERVER_CONFIG.API_URL .. "/player/") .. steamId) .. "/death_sentence_contracts" local request = CreateHTTPRequestScriptVM("PUT", url) setApiHeaders(nil, request) local payload = {death_sentence_contracts = {roster = roster}} request:SetHTTPRequestRawPostBody( "application/json", encodeApiBody(nil, payload) ) request:Send(function(result) local ok = result.StatusCode >= 200 and result.StatusCode < 300 if ok then print((LOG_PREFIX .. " PUT ok steam=") .. steamId) else print((((LOG_PREFIX .. " PUT fail steam=") .. steamId) .. " code=") .. tostring(result.StatusCode)) end if done then done(nil, ok) end end) end --- Сохранить один и тот же ростер всем подключённым игрокам (одинаковая сетка лобби). function ____exports.saveDeathSentenceContractsRosterToAllConnectedPlayers(self, roster) do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not PlayerResource:IsValidPlayerID(pid) then goto __continue41 end local steamId = PlayerResource:GetSteamAccountID(pid) if not steamId or steamId == 0 then goto __continue41 end ____exports.saveDeathSentenceContractsToBackend( nil, tostring(steamId), roster ) end ::__continue41:: i = i + 1 end end end return ____exports