Files
Dota-Zombie-Invasion/scripts/vscripts/contracts/contract_match_manager.lua
T
2026-05-29 15:11:31 +07:00

542 lines
20 KiB
Lua

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