local ____lualib = require("lualib_bundle") local __TS__Class = ____lualib.__TS__Class local Map = ____lualib.Map local __TS__New = ____lualib.__TS__New local __TS__ArrayMap = ____lualib.__TS__ArrayMap local __TS__ObjectKeys = ____lualib.__TS__ObjectKeys local __TS__ArrayFind = ____lualib.__TS__ArrayFind local __TS__ArraySome = ____lualib.__TS__ArraySome local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter local ____exports = {} local ____contract_steam_compare = require("contracts.contract_steam_compare") local compareSteamIdDecimalString = ____contract_steam_compare.compareSteamIdDecimalString local ____contract_backend_adapter = require("contracts.contract_backend_adapter") local clampContractMultiplierFromBackend = ____contract_backend_adapter.clampContractMultiplierFromBackend local contractAdapterFinalizeContractVoting = ____contract_backend_adapter.contractAdapterFinalizeContractVoting local contractAdapterGetPlayerContracts = ____contract_backend_adapter.contractAdapterGetPlayerContracts local contractAdapterLinkSessionToMatch = ____contract_backend_adapter.contractAdapterLinkSessionToMatch local contractAdapterNominate = ____contract_backend_adapter.contractAdapterNominate local contractAdapterSaveDroppedContract = ____contract_backend_adapter.contractAdapterSaveDroppedContract local contractAdapterVote = ____contract_backend_adapter.contractAdapterVote local requestIdDrop = ____contract_backend_adapter.requestIdDrop local requestIdFinalize = ____contract_backend_adapter.requestIdFinalize local requestIdLinkMatch = ____contract_backend_adapter.requestIdLinkMatch local requestIdNominate = ____contract_backend_adapter.requestIdNominate local requestIdVote = ____contract_backend_adapter.requestIdVote local ____contract_drop_config = require("contracts.contract_drop_config") local rollContractDropTier = ____contract_drop_config.rollContractDropTier local ____contract_generator = require("contracts.contract_generator") local generateContractDraft = ____contract_generator.generateContractDraft local NET_TABLE = "contract_match" local NET_KEY = "state" local FINALIZE_TIMEOUT_SEC = 10 ____exports.ContractMatchManager = __TS__Class() local ContractMatchManager = ____exports.ContractMatchManager ContractMatchManager.name = "ContractMatchManager" ContractMatchManager.____file_path = "scripts/vscripts/contracts/contract_match_manager.lua" function ContractMatchManager.prototype.____constructor(self) self.listenersRegistered = false self.inventoryBySteam = __TS__New(Map) self.candidates = {} self.votesByVoterSteam = {} self.nominateOrderSeq = 0 self.publicState = { contract_session_id = "", phase = "voting", candidates = {}, vote_counts = {}, active_contract = nil, contract_multiplier = 1 } self.confirmedActiveContract = nil self.confirmedMultiplier = 1 self.finalizeResolved = false self.finalizeAttemptSerial = 0 self.activeFinalizeAttempt = 0 self.linkMatchDone = false self.dropRollIndex = 0 end function ContractMatchManager.getInstance(self) if not ____exports.ContractMatchManager.instance then ____exports.ContractMatchManager.instance = __TS__New(____exports.ContractMatchManager) end return ____exports.ContractMatchManager.instance end function ContractMatchManager.prototype.init(self) if self.listenersRegistered or type(CustomGameEventManager) == "nil" then return end CustomGameEventManager:RegisterListener( "invasion_contracts_request", function(_src, data) local pid = data.PlayerID self:onContractsRequest(pid) end ) CustomGameEventManager:RegisterListener( "invasion_contract_nominate", function(_src, data) local pid = data.PlayerID local ____tostring_1 = tostring local ____data_contract_instance_id_0 = data.contract_instance_id if ____data_contract_instance_id_0 == nil then ____data_contract_instance_id_0 = "" end local cid = ____tostring_1(____data_contract_instance_id_0) self:onNominate(pid, cid) end ) CustomGameEventManager:RegisterListener( "invasion_contract_vote", function(_src, data) local pid = data.PlayerID local ____tostring_3 = tostring local ____data_contract_instance_id_2 = data.contract_instance_id if ____data_contract_instance_id_2 == nil then ____data_contract_instance_id_2 = "" end local cid = ____tostring_3(____data_contract_instance_id_2) self:onVote(pid, cid) end ) self.listenersRegistered = true self:syncNetTable() end function ContractMatchManager.prototype.getConfirmedContractMultiplier(self) return self.confirmedMultiplier end function ContractMatchManager.prototype.getConfirmedActiveContract(self) return self.confirmedActiveContract end function ContractMatchManager.prototype.tryLinkSessionToBackendMatch(self, backendMatchId) local sid = self:getOrCreateSessionId() local mid = backendMatchId if not mid or self.linkMatchDone then return end local rid = requestIdLinkMatch(nil, sid, mid) contractAdapterLinkSessionToMatch( nil, sid, rid, mid, function(____, ok) if ok then self.linkMatchDone = true print((("[ContractMatchManager] link-match ok session=" .. sid) .. " match=") .. mid) end end ) end function ContractMatchManager.prototype.beginFinalizeForDifficulty(self, leader, backendMatchId, onWorldApplyReady) if leader ~= "impossible" then self.confirmedActiveContract = nil self.confirmedMultiplier = 1 self.publicState.phase = "locked_without_contract" self.publicState.active_contract = nil self.publicState.contract_multiplier = 1 self.publicState.public_error_code = nil self:syncNetTable() onWorldApplyReady(nil) return end self.finalizeAttemptSerial = self.finalizeAttemptSerial + 1 local attempt = self.finalizeAttemptSerial self.activeFinalizeAttempt = attempt self.finalizeResolved = false self.publicState.phase = "finalizing" self.publicState.public_error_code = nil self:syncNetTable() Timers:CreateTimer( FINALIZE_TIMEOUT_SEC, function() if self.finalizeResolved then return nil end if self.activeFinalizeAttempt ~= attempt then return nil end if self.publicState.phase ~= "finalizing" then return nil end print("[ContractMatchManager] finalize timeout attempt=" .. tostring(attempt)) self:applyFinalizeFailure(attempt, "contracts_finalize_failed", onWorldApplyReady) return nil end ) local sid = self:getOrCreateSessionId() local matchId = backendMatchId or nil local localWinner = self:computeLocalWinnerContractId() local candSnap = __TS__ArrayMap( self.candidates, function(____, c) return {contract_instance_id = c.contract_instance_id, owner_steam_id = c.owner_steam_id, nominated_at = c.nominated_at, nominated_order = c.nominated_order} end ) local voteSnap = {} for ____, voter in ipairs(__TS__ObjectKeys(self.votesByVoterSteam)) do local cid = self.votesByVoterSteam[voter] if cid ~= nil and cid ~= "" then voteSnap[#voteSnap + 1] = {voter_steam_id = voter, contract_instance_id = cid} end end local rid = requestIdFinalize(nil, sid) contractAdapterFinalizeContractVoting( nil, sid, rid, matchId, localWinner, candSnap, voteSnap, function(____, resp, httpOk) if self.finalizeResolved then print("[ContractMatchManager] finalize HTTP late ignored attempt=" .. tostring(attempt)) return end if self.activeFinalizeAttempt ~= attempt then print("[ContractMatchManager] finalize HTTP stale attempt=" .. tostring(attempt)) return end if self.publicState.phase ~= "finalizing" then print("[ContractMatchManager] finalize HTTP wrong phase") return end if not httpOk or not resp or not resp.ok then print("[ContractMatchManager] finalize HTTP/body fail attempt=" .. tostring(attempt)) self:applyFinalizeFailure(attempt, "contracts_finalize_failed", onWorldApplyReady) return end local mult = clampContractMultiplierFromBackend(nil, resp.contract_multiplier) if mult == nil then print("[ContractMatchManager] finalize invalid multiplier raw=" .. tostring(resp.contract_multiplier)) self:applyFinalizeFailure(attempt, "contracts_finalize_failed", onWorldApplyReady) return end self.finalizeResolved = true self.confirmedMultiplier = mult self.confirmedActiveContract = resp.active_contract or nil self.publicState.active_contract = self.confirmedActiveContract self.publicState.contract_multiplier = mult self.publicState.phase = self.confirmedActiveContract and "locked" or "locked_without_contract" self.publicState.public_error_code = nil self:syncNetTable() onWorldApplyReady(nil) end ) end function ContractMatchManager.prototype.processImpossibleVictoryDrops(self, allowPersistedDrop) if not allowPersistedDrop then print("[ContractMatchManager] drop skipped: match end rewards / stats blocked") return end do local i = 0 while i < DOTA_MAX_PLAYERS do do local pid = i if not PlayerResource:IsValidPlayerID(pid) or not PlayerResource:IsValidPlayer(pid) or PlayerResource:IsFakeClient(pid) then goto __continue34 end local steam = tostring(PlayerResource:GetSteamAccountID(pid)) local tier = rollContractDropTier(nil) if tier <= 0 then goto __continue34 end self.dropRollIndex = self.dropRollIndex + 1 local draftId = (((("draft_" .. self:getOrCreateSessionId()) .. "_") .. steam) .. "_") .. tostring(self.dropRollIndex) local draft = generateContractDraft(nil, steam, tier, draftId) local rid = requestIdDrop( nil, self:getOrCreateSessionId(), steam, self.dropRollIndex ) contractAdapterSaveDroppedContract( nil, steam, rid, draft, function(____, canonical, ok) if not ok or not canonical then print("[ContractMatchManager] drop save failed steam=" .. steam) return end if type(CustomGameEventManager) ~= "nil" then local pl = PlayerResource:GetPlayer(pid) if pl then CustomGameEventManager:Send_ServerToPlayer(pl, "invasion_contract_drop_result", {ok = true, contract = canonical}) end end end ) end ::__continue34:: i = i + 1 end end end function ContractMatchManager.prototype.getOrCreateSessionId(self) if not self.sessionId then local t = math.floor(GameRules:GetGameTime() * 1000) self.sessionId = (("cs_" .. tostring(t)) .. "_") .. tostring(RandomInt(100000, 999999)) self.publicState.contract_session_id = self.sessionId end return self.sessionId end function ContractMatchManager.prototype.onContractsRequest(self, playerId) if not PlayerResource:IsValidPlayerID(playerId) or not PlayerResource:IsValidPlayer(playerId) then return end local steam = tostring(PlayerResource:GetSteamAccountID(playerId)) contractAdapterGetPlayerContracts( nil, steam, function(____, list, err) if list == nil then self:sendInventory(playerId, nil, "contracts_load_failed", true) return end self.inventoryBySteam:set(steam, list) self:sendInventory(playerId, list, nil, true) end ) end function ContractMatchManager.prototype.sendInventory(self, playerId, contracts, errorCode, done) if type(CustomGameEventManager) == "nil" then return end local payload = {contracts = contracts, loading_done = done} if errorCode then payload.error_code = errorCode end local pl = PlayerResource:GetPlayer(playerId) if pl then CustomGameEventManager:Send_ServerToPlayer(pl, "invasion_contracts_inventory", payload) end end function ContractMatchManager.prototype.onNominate(self, playerId, contractInstanceId) if self.publicState.phase ~= "voting" then print("[ContractMatchManager] nominate rejected phase=" .. self.publicState.phase) return end if not contractInstanceId or #contractInstanceId == 0 then return end if not PlayerResource:IsValidPlayerID(playerId) or not PlayerResource:IsValidPlayer(playerId) then return end local steam = tostring(PlayerResource:GetSteamAccountID(playerId)) local inv = self.inventoryBySteam:get(steam) local ____opt_4 = inv local found = ____opt_4 and __TS__ArrayFind( inv, function(____, c) return c.contract_instance_id == contractInstanceId end ) if not found or found.is_broken or found.owner_steam_id ~= steam then self:syncNetTableErrorFlash("contracts_invalid_contract") return end local sid = self:getOrCreateSessionId() local rid = requestIdNominate(nil, sid, steam, contractInstanceId) contractAdapterNominate( nil, sid, rid, steam, contractInstanceId, function(____, ok) if not ok then print((("[ContractMatchManager] nominate HTTP fail steam=" .. steam) .. " id=") .. contractInstanceId) self:syncNetTableErrorFlash("contracts_invalid_contract") return end self:removeCandidateByOwner(steam) self.nominateOrderSeq = self.nominateOrderSeq + 1 local pub = { contract_instance_id = contractInstanceId, owner_steam_id = steam, nominated_at = GameRules:GetGameTime(), nominated_order = self.nominateOrderSeq, name = found.name, tier = found.tier } local ____self_candidates_6 = self.candidates ____self_candidates_6[#____self_candidates_6 + 1] = pub self.votesByVoterSteam[steam] = contractInstanceId self:rebuildVoteCounts() self:syncNetTable() end ) end function ContractMatchManager.prototype.onVote(self, playerId, contractInstanceId) if self.publicState.phase ~= "voting" then print("[ContractMatchManager] vote rejected phase=" .. self.publicState.phase) return end if not contractInstanceId then return end if not PlayerResource:IsValidPlayerID(playerId) or not PlayerResource:IsValidPlayer(playerId) then return end local voter = tostring(PlayerResource:GetSteamAccountID(playerId)) local exists = __TS__ArraySome( self.candidates, function(____, c) return c.contract_instance_id == contractInstanceId end ) if not exists then self:syncNetTableErrorFlash("contracts_vote_rejected") return end local sid = self:getOrCreateSessionId() local rid = requestIdVote(nil, sid, voter, contractInstanceId) contractAdapterVote( nil, sid, rid, voter, contractInstanceId, function(____, ok) if not ok then print("[ContractMatchManager] vote HTTP fail voter=" .. voter) self:syncNetTableErrorFlash("contracts_vote_rejected") return end self.votesByVoterSteam[voter] = contractInstanceId self:rebuildVoteCounts() self:syncNetTable() end ) end function ContractMatchManager.prototype.removeCandidateByOwner(self, steam) self.candidates = __TS__ArrayFilter( self.candidates, function(____, c) return c.owner_steam_id ~= steam end ) end function ContractMatchManager.prototype.rebuildVoteCounts(self) local counts = {} for ____, voter in ipairs(__TS__ObjectKeys(self.votesByVoterSteam)) do local cid = self.votesByVoterSteam[voter] if cid ~= nil and cid ~= "" then counts[cid] = (counts[cid] or 0) + 1 end end self.publicState.vote_counts = counts self.publicState.candidates = self.candidates end function ContractMatchManager.prototype.computeLocalWinnerContractId(self) if #self.candidates == 0 then return nil end local counts = {} for ____, voter in ipairs(__TS__ObjectKeys(self.votesByVoterSteam)) do local cid = self.votesByVoterSteam[voter] if cid ~= nil and cid ~= "" then counts[cid] = (counts[cid] or 0) + 1 end end local totalVotes = 0 for ____, k in ipairs(__TS__ObjectKeys(counts)) do totalVotes = totalVotes + (counts[k] or 0) end if totalVotes == 0 then return nil end local bestIds = {} local bestVotes = -1 for ____, cid in ipairs(__TS__ObjectKeys(counts)) do local n = counts[cid] or 0 if n > bestVotes then bestVotes = n bestIds = {cid} elseif n == bestVotes then bestIds[#bestIds + 1] = cid end end if #bestIds == 1 then return bestIds[1] end return self:pickTieBreakContractId(bestIds) end function ContractMatchManager.prototype.pickTieBreakContractId(self, ids) local byId = {} for ____, c in ipairs(self.candidates) do byId[c.contract_instance_id] = c end local best = ids[1] do local i = 1 while i < #ids do local cur = ids[i + 1] if self:compareCandidatesForTie(byId[cur], byId[best], cur, best) < 0 then best = cur end i = i + 1 end end return best end function ContractMatchManager.prototype.compareCandidatesForTie(self, ca, cb, idA, idB) if not ca or not cb then return 0 end if ca.nominated_order ~= cb.nominated_order then return ca.nominated_order < cb.nominated_order and -1 or 1 end local steamCmp = compareSteamIdDecimalString(nil, ca.owner_steam_id, cb.owner_steam_id) if steamCmp ~= 0 then return steamCmp end if idA < idB then return -1 end if idA > idB then return 1 end return 0 end function ContractMatchManager.prototype.applyFinalizeFailure(self, attempt, code, onWorldApplyReady) if self.finalizeResolved then return end if self.activeFinalizeAttempt ~= attempt then return end if self.activeFinalizeAttempt ~= attempt then return end self.finalizeResolved = true self.confirmedActiveContract = nil self.confirmedMultiplier = 1 self.publicState.active_contract = nil self.publicState.contract_multiplier = 1 self.publicState.phase = "finalize_error" self.publicState.public_error_code = code self:syncNetTable() onWorldApplyReady(nil) end function ContractMatchManager.prototype.syncNetTableErrorFlash(self, code) self.publicState.public_error_code = code self:syncNetTable() Timers:CreateTimer( 0.1, function() self.publicState.public_error_code = nil self:syncNetTable() return nil end ) end function ContractMatchManager.prototype.syncNetTable(self) if type(CustomNetTables) == "nil" then return end self.publicState.candidates = self.candidates self:rebuildVoteCounts() if not self.publicState.contract_session_id and self.sessionId then self.publicState.contract_session_id = self.sessionId end CustomNetTables:SetTableValue(NET_TABLE, NET_KEY, self.publicState) end return ____exports