diff --git a/components/match2/wikis/stormgate/match_group_input_custom.lua b/components/match2/wikis/stormgate/match_group_input_custom.lua index da7408e805a..54a3a53ea22 100644 --- a/components/match2/wikis/stormgate/match_group_input_custom.lua +++ b/components/match2/wikis/stormgate/match_group_input_custom.lua @@ -10,690 +10,443 @@ local Array = require('Module:Array') local Faction = require('Module:Faction') local Flags = require('Module:Flags') local HeroData = mw.loadData('Module:HeroData') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent local Streams = Lua.import('Module:Links/Stream') -local OpponentLibrary = require('Module:OpponentLibraries') -local Opponent = OpponentLibrary.Opponent - -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local DEFAULT_WIN_STATUS = 'W' -local SCORE_STATUS = 'S' -local ALLOWED_STATUSES = Array.append(DEFAULT_LOSS_STATUSES, DEFAULT_WIN_STATUS) -local RESULT_TYPE_DEFAULT = 'default' -local RESULT_TYPE_NOT_PLAYED = 'np' -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_BEST_OF = 99 -local MODE_MIXED = 'mixed' -local TBD = 'tbd' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} +local TBD = 'TBD' local DEFAULT_HERO_FACTION = HeroData.default.faction -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MODE_MIXED = 'mixed' + +---@class StormgateParticipant +---@field player string +---@field faction string? +---@field heroes string[]? +---@field position integer? +---@field flag string? +---@field random boolean? local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} ---- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) assert(not Logic.readBool(match.ffa), 'FFA is not yet supported in stormgate match2') - Table.mergeInto( - match, - CustomMatchGroupInput._readDate(match) - ) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._adjustData(match) - CustomMatchGroupInput._updateFinished(match) - match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getExtraData(match) - - return match -end + Table.mergeInto(match, MatchFunctions.readDate(match)) ----@param matchArgs table ----@return table -function CustomMatchGroupInput._readDate(matchArgs) - local dateProps = MatchGroupInput.readDate(matchArgs.date, { - 'matchDate', - 'tournament_startdate', - 'tournament_enddate' - }) - - if dateProps.dateexact then - Variables.varDefine('matchDate', dateProps.date) - end - - return dateProps -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) ----@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) - if match.finished then - return - end + Array.forEach(opponents, function(opponent) + opponent.extradata = opponent.extradata or {} + Table.mergeInto(opponent.extradata, MatchFunctions.getOpponentExtradata(opponent)) + -- make sure match2players is not nil to avoid indexing nil + opponent.match2players = opponent.match2players or {} + Array.forEach(opponent.match2players, function(player) + player.extradata = player.extradata or {} + player.extradata.faction = MatchFunctions.getPlayerFaction(player) + end) + end) - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < NOW -end + local games = MatchFunctions.extractMaps(match, opponents) ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match.cancelled = Logic.emptyOr(match.cancelled, Variables.varDefault('tournament_cancelled', 'false')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - match.bestof = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('match_bestof'))) - Variables.varDefine('match_bestof', match.bestof) + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, opponents) + or nil - MatchGroupInput.getCommonTournamentVars(match) -end + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) ----@param match table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match), - ffa = 'false', - } + match.mode = MatchFunctions.getMode(opponents) - for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do - match.extradata[prefix] = mapVeto and mw.ext.TeamLiquidIntegration.resolve_redirect(mapVeto) or nil - match.extradata[prefix .. 'by'] = match[prefix .. 'by'] - match.extradata[prefix .. 'displayname'] = match[prefix .. 'displayName'] + match.bestof = MatchFunctions.getBestOf(match.bestof) + local cancelled = Logic.readBool(Logic.emptyOr(match.cancelled, Variables.varDefault('tournament_cancelled'))) + if cancelled then + match.finished = match.finished or 'skip' end - Table.mergeInto(match.extradata, Table.filterByKey(match, function(key, value) - return key:match('subgroup%d+header') end)) -end + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) ----@param match table -function CustomMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores + set mode - CustomMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - subGroupIndex = CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - CustomMatchGroupInput._matchWinnerProcessing(match) -end - ----@param match table -function CustomMatchGroupInput._matchWinnerProcessing(match) - local bestof = match.bestof or DEFAULT_BEST_OF - local numberofOpponents = 0 - - Array.map(Array.range(1, MAX_NUM_OPPONENTS),function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - - if Logic.isEmpty(opponent) then return end - - numberofOpponents = numberofOpponents + 1 - - if Table.includes(ALLOWED_STATUSES, string.upper(opponent.score or '')) then - opponent.status = string.upper(opponent.score) - match.resulttype = RESULT_TYPE_DEFAULT - match.finished = true - opponent.score = -1 - - if opponent.status == DEFAULT_WIN_STATUS then - match.winner = opponentIndex - else - match.walkover = opponent.status - end - else - opponent.status = SCORE_STATUS - opponent.score = tonumber(opponent.score) or tonumber(opponent.sumscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - end - end - - if Logic.readBool(match.cancelled) then - match.finished = true - if String.isEmpty(match.resulttype) and Logic.isEmpty(opponent.score) then - match.resulttype = RESULT_TYPE_NOT_PLAYED - opponent.score = opponent.score or -1 - end - end - - -- to not break the loop - return true - end) - - CustomMatchGroupInput._determineWinnerIfMissing(match) + MatchGroupInputUtil.getCommonTournamentVars(match) - for opponentIndex = 1, numberofOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end + match.stream = Streams.processStreams(match) + match.vod = Logic.nilIfEmpty(match.vod) + match.extradata = MatchFunctions.getExtraData(match, #games) - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end -end - ----@param match table ----@return table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if Logic.readBool(match.finished) and Logic.isEmpty(match.winner) then - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end - end + match.games = games + match.opponents = opponents return match end ---OpponentInput functions - ----@param match table ----@return table -function CustomMatchGroupInput._opponentInput(match) - local opponentTypes = {} - - for opponentKey, opponent, opponentIndex in Table.iter.pairsByPrefix(match, 'opponent') do - opponent = Json.parseIfString(opponent) - - --Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - -- Opponent processing (first part) - -- Sort out extradata - opponent.extradata = { - advantage = opponent.advantage, - penalty = opponent.penalty, - score2 = opponent.score2, - } - - local partySize = Opponent.partySize(opponent.type) - if partySize then - opponent = CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - elseif opponent.type == Opponent.team then - opponent = CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, match.date) - opponent = CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - elseif opponent.type == Opponent.literal then - opponent = CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - else - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - --set initial opponent sumscore - opponent.sumscore = tonumber(opponent.extradata.advantage) or (-1 * (tonumber(opponent.extradata.penalty) or 0)) - - table.insert(opponentTypes, opponent.type) +---@param matchArgs table +---@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} +function MatchFunctions.readDate(matchArgs) + local dateProps = MatchGroupInputUtil.readDate(matchArgs.date, { + 'matchDate', + 'tournament_startdate', + 'tournament_enddate' + }) - match[opponentKey] = opponent + if dateProps.dateexact then + Variables.varDefine('matchDate', dateProps.date) end - assert(#opponentTypes <= MAX_NUM_OPPONENTS, 'Too many opponents') - - match.mode = Array.all(opponentTypes, function(opponentType) return opponentType == opponentTypes[1] end) - and opponentTypes[1] or MODE_MIXED - - match.isTeamMatch = Array.any(opponentTypes, function(opponentType) return opponentType == Opponent.team end) - - return match + return dateProps end ----reads the players of a team from input and wiki variables ---@param match table ----@param opponentIndex integer ----@param opponent table ----@return table -function CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - local players = {} +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + local subGroup = 0 + for mapKey, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local map + map, subGroup = MapFunctions.readMap(mapInput, subGroup, #opponents) - local teamName = opponent.name + map.participants = MapFunctions.getParticipants(mapInput, opponents) - local insertIntoPlayers = function(player) - if type(player) ~= 'table' or Logic.isEmpty(player) or Logic.isEmpty(player.name) then - return - end - - player.name = mw.ext.TeamLiquidIntegration.resolve_redirect(player.name):gsub(' ', '_') - player.flag = Flags.CountryName(player.flag) - player.displayname = Logic.emptyOr(player.displayname, player.displayName) - player.extradata = {faction = Faction.read(player.faction)} + map.mode = MapFunctions.getMode(mapInput, map.participants, opponents) - players[player.name] = players[player.name] or {} - Table.deepMergeInto(players[player.name], player) - end + Table.mergeInto(map.extradata, MapFunctions.getAdditionalExtraData(map, map.participants)) - local playerIndex = 1 - local varPrefix = teamName .. '_p' .. playerIndex - local name = Variables.varDefault(varPrefix) - while name do - insertIntoPlayers{ - name = name, - displayName = Variables.varDefault(varPrefix .. 'dn'), - faction = Variables.varDefault(varPrefix .. 'faction'), - flag = Variables.varDefault(varPrefix .. 'flag'), - } - playerIndex = playerIndex + 1 - varPrefix = teamName .. '_p' .. playerIndex - name = Variables.varDefault(varPrefix) - end + map.vod = Logic.emptyOr(mapInput.vod, match['vodgame' .. mapIndex]) - --players from manual input as `opponnetX_pY` - for _, player in Table.iter.pairsByPrefix(match, 'opponent' .. opponentIndex .. '_p') do - insertIntoPlayers(Json.parseIfString(player)) + table.insert(maps, map) + match[mapKey] = nil end - opponent.match2players = Array.extractValues(players) - --set default faction for unset factions - Array.forEach(opponent.match2players, function(player) - player.extradata.faction = player.extradata.faction or Faction.defaultFaction - end) - - return opponent + return maps end ----@param opponent table ----@return table -function CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - local faction = opponent.faction - local flag = opponent.flag - local name = opponent.name or opponent[1] - local extradata = opponent.extradata - - local players = {} - if String.isNotEmpty(faction) or String.isNotEmpty(flag) then - players[1] = { - displayname = name, - name = TBD:upper(), - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - extradata.hasFactionOrFlag = true +---@param maps table[] +---@param opponents table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, opponents) + return function(opponentIndex) + local calculatedScore = MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + if not calculatedScore then return end + local opponent = opponents[opponentIndex] + return calculatedScore + (opponent.extradata.advantage or 0) - (opponent.extradata.penalty or 0) end - - return { - type = opponent.type, - name = name, - score = opponent.score, - extradata = extradata, - match2players = players - } end ---@param opponent table ----@param partySize integer ---@return table -function CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - local players = {} - local links = {} - - for playerIndex = 1, partySize do - local name = Logic.emptyOr(opponent['p' .. playerIndex], opponent[playerIndex]) or '' - local link = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent['p' .. playerIndex .. 'link'], - Variables.varDefault(name .. '_page') - ) or name):gsub(' ', '_') - table.insert(links, link) - - table.insert(players, { - displayname = name, - name = link, - flag = Flags.CountryName(Logic.emptyOr( - opponent['p' .. playerIndex .. 'flag'], - Variables.varDefault(name .. '_flag') - )), - extradata = {faction = Faction.read(Logic.emptyOr( - opponent['p' .. playerIndex .. 'faction'], - Variables.varDefault(name .. '_faction') - )) or Faction.defaultFaction} - }) - end - - table.sort(links) - +function MatchFunctions.getOpponentExtradata(opponent) return { - type = opponent.type, - name = table.concat(links, ' / '), - score = opponent.score, - extradata = opponent.extradata, - match2players = players + advantage = tonumber(opponent.advantage), + penalty = tonumber(opponent.penalty), } end ----@param opponent table ----@param date string ----@return table -function CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, date) - local template = string.lower(Logic.emptyOr(opponent.template, opponent[1], '')--[[@as string]]):gsub('_', ' ') - - if String.isEmpty(template) or template == 'noteam' then - opponent = Table.merge(opponent, Opponent.blank(Opponent.team)) - opponent.name = Opponent.toName(opponent) - return opponent - end +---@param player table +---@return string +function MatchFunctions.getPlayerFaction(player) + return Faction.read(player.extradata.faction) or Faction.defaultFaction +end - assert(mw.ext.TeamTemplate.teamexists(template), 'Missing team template "' .. template .. '"') +---@param opponents {type: OpponentType} +---@return string +function MatchFunctions.getMode(opponents) + local opponentTypes = Array.map(opponents, Operator.property('type')) + return #Array.unique(opponentTypes) == 1 and opponentTypes[1] or MODE_MIXED +end - local templateData = mw.ext.TeamTemplate.raw(template, date) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('match_bestof')) - opponent.icon = templateData.image - opponent.icondark = Logic.emptyOr(templateData.imagedark, templateData.image) - opponent.name = templateData.page:gsub(' ', '_') - opponent.template = templateData.templatename or template + if bestof then + Variables.varDefine('match_bestof', bestof) + end - return opponent + return bestof end ---MapInput functions - ---@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return integer -function CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or '') - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - header = map.header, +---@param numberOfGames integer +---@return table +function MatchFunctions.getExtraData(match, numberOfGames) + local extradata = { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - CustomMatchGroupInput.ProcessPlayerMapData(map, match, 2) - - --adjust sumscore for winner opponent - if (tonumber(map.winner) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - -- handle subgroup stuff if team match - if match.isTeamMatch then - map.subgroup = tonumber(map.subgroup) or (subGroupIndex + 1) - subGroupIndex = map.subgroup + for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do + extradata[prefix] = mapVeto and mw.ext.TeamLiquidIntegration.resolve_redirect(mapVeto) or nil + extradata[prefix .. 'by'] = match[prefix .. 'by'] + extradata[prefix .. 'displayname'] = match[prefix .. 'displayName'] end - match['map' .. mapIndex] = map + Table.mergeInto(extradata, Table.filterByKey(match, function(key) return key:match('subgroup%d+header') end)) - return subGroupIndex + return extradata end ----@param map table +---@param mapInput table +---@param subGroup integer +---@param opponentCount integer ---@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end +---@return integer +function MapFunctions.readMap(mapInput, subGroup, opponentCount) + subGroup = tonumber(mapInput.subgroup) or (subGroup + 1) + + local mapName = mapInput.map + if mapName and mapName:upper() ~= TBD then + mapName = mw.ext.TeamLiquidIntegration.resolve_redirect(mapName) + elseif mapName then + mapName = TBD end - local winner = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - winner = walkoverInput - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - map.winner = winner + local map = { + map = mapName, + subgroup = subGroup, + extradata = { + comment = mapInput.comment, + header = mapInput.header, + } + } - return map - end + map.finished = MatchGroupInputUtil.mapIsFinished(mapInput) + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = mapInput.walkover, + winner = mapInput.winner, + opponentIndex = opponentIndex, + score = mapInput['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(mapInput.winner, map.finished)) + return {score = score, status = status} + end) - if hasManualScores then - map.winner = winner or CustomMatchGroupInput._getWinner(indexedScores) + map.scores = Array.map(opponentInfo, Operator.property('score')) - return map + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(mapInput.winner, mapInput.finished, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, mapInput.winner, opponentInfo) end - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = RESULT_TYPE_NOT_PLAYED - elseif winner == 1 then - map.scores = {1, 0} - elseif winner == 2 then - map.scores = {0, 1} - elseif winner == 0 or map.winner == 'draw' then - map.scores = {0.5, 0.5} - map.resulttype = 'draw' - end - - map.winner = winner + return map, subGroup +end - return map +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + return winner == opponentIndex and 1 or 0 + end end ----@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.ProcessPlayerMapData(map, match, numberOfOpponents) +---@param mapInput table +---@param opponents table[] +---@return table +function MapFunctions.getParticipants(mapInput, opponents) local participants = {} - local modeParts = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - local partySize = Opponent.partySize(opponent.type) - local players = opponent.match2players - if partySize then - table.insert(modeParts, partySize) - CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) + Array.forEach(opponents, function(opponent, opponentIndex) + if opponent.type == Opponent.literal then + return elseif opponent.type == Opponent.team then - table.insert(modeParts, CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants)) - elseif opponent.type == Opponent.literal then - table.insert(modeParts, 'literal') + Table.mergeInto(participants, MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex)) + return end - end + Table.mergeInto(participants, MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex)) + end) - map.mode = table.concat(modeParts, 'v') - map.participants = participants + return participants +end - if numberOfOpponents ~= MAX_NUM_OPPONENTS or map.mode ~= '1v1' then - return - end +---@param mapInput table +---@param opponent table +---@param opponentIndex integer +---@return table +function MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return Logic.nilIfEmpty(mapInput['t' .. opponentIndex .. 'p' .. playerIndex]) + end) - local opponentFactions, playerNameArray, heroesData - = CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - map.extradata = Table.merge(map.extradata, heroesData) - if tonumber(map.winner) == 1 then - map.extradata.winnerfaction = opponentFactions[1] - map.extradata.loserfaction = opponentFactions[2] - elseif tonumber(map.winner) == 2 then - map.extradata.winnerfaction = opponentFactions[2] - map.extradata.loserfaction = opponentFactions[1] - end - map.extradata.opponent1 = playerNameArray[1] - map.extradata.opponent2 = playerNameArray[2] -end + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']) or Variables.varDefault(mapInput[prefix] .. '_page'), + } + end, + function(playerIndex, playerIdData, playerInputData) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + local faction = Faction.read(mapInput[prefix .. 'faction']) + or (playerIdData.extradata or {}).faction or Faction.defaultFaction + local link = playerIdData.name or playerInputData.link or playerInputData.name:gsub(' ', '_') + return { + faction = faction, + player = link, + flag = Flags.CountryName(playerIdData.flag), + position = playerIndex, + random = Logic.readBool(mapInput[prefix .. 'random']), + heroes = MapFunctions.readHeroes( + mapInput[prefix .. 'heroes'], + faction, + link, + Logic.readBool(mapInput[prefix .. 'noheroescheck']) + ), + } + end + ) ----@param participants table ----@return table ----@return table ----@return table -function CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - local opponentFactions, playerNameArray, heroesData = {}, {}, {} - for participantKey, participantData in pairs(participants) do - local opponentIndex = tonumber(string.sub(participantKey, 1, 1)) - -- opponentIndex can not be nil due to the format of the participants keys - ---@cast opponentIndex -nil - opponentFactions[opponentIndex] = participantData.faction - playerNameArray[opponentIndex] = participantData.player - Array.forEach(participantData.heroes or {}, function(hero, heroIndex) - heroesData['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero - end) - end + Array.forEach(unattachedParticipants, function(participant) + local name = mapInput['t' .. opponentIndex .. 'p' .. participant.position] + local nameUpper = name:upper() + local isTBD = nameUpper == TBD + + table.insert(opponent.match2players, { + name = isTBD and TBD or participant.player, + displayname = isTBD and TBD or name, + flag = participant.flag, + extradata = {faction = participant.faction}, + }) + participants[#opponent.match2players] = participant + end) - return opponentFactions, playerNameArray, heroesData + return Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex)) end ----@param players table[] ----@param map table +---@param mapInput table +---@param opponent table ---@param opponentIndex integer ----@param participants table ----@return table -function CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) +---@return table +function MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex) + local players = opponent.match2players + + -- resolve the aliases in case they are used local prefix = 't' .. opponentIndex .. 'p' - for playerIndex, player in pairs(players) do - local faction = Logic.emptyOr( - map[prefix .. playerIndex .. 'faction'], - player.extradata.faction, - Faction.defaultFaction - ) - faction = Faction.read(faction) + local participants = {} + + Array.forEach(players, function(player, playerIndex) + local faction = Faction.read(mapInput['t' .. opponentIndex .. 'p' .. playerIndex .. 'faction']) + or player.extradata.faction participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, + faction = Faction.read(faction or player.extradata.faction), player = player.name, - heroes = CustomMatchGroupInput._readHeroes( - map[prefix .. playerIndex .. 'heroes'], + heroes = MapFunctions.readHeroes( + mapInput[prefix .. playerIndex .. 'heroes'], faction, player.name, - Logic.readBool(map[prefix .. playerIndex .. 'noheroescheck']) + Logic.readBool(mapInput[prefix .. playerIndex .. 'noheroescheck']) ), } - end + end) return participants end ----@param players table[] ----@param map table ----@param opponentIndex integer ----@param participants table ----@return integer -function CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants) - local amountOfTbds = 0 - local playerData = {} - - local numberOfPlayers = 0 - for prefix, playerInput, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - numberOfPlayers = numberOfPlayers + 1 - if playerInput:lower() == TBD then - amountOfTbds = amountOfTbds + 1 - else - local link = Logic.emptyOr(map[prefix .. 'link'], Variables.varDefault(playerInput .. '_page')) or playerInput - link = mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_') - - playerData[link] = { - faction = Faction.read(map[prefix .. 'faction']), - position = playerIndex, - heroes = map[prefix .. 'heroes'], - heroesCheckDisabled = Logic.readBool(map[prefix .. 'noheroescheck']), - playedRandom = Logic.readBool(map[prefix .. 'random']), - displayName = playerInput, - } - end +---@param mapInput table # the input data +---@param participants table +---@param opponents table[] +---@return string +function MapFunctions.getMode(mapInput, participants, opponents) + -- assume we have a min of 2 opponents in a game + local playerCounts = {0, 0} + for key in pairs(participants) do + local parsedOpponentIndex = key:match('(%d+)_%d+') + local opponetIndex = tonumber(parsedOpponentIndex) --[[@as integer]] + playerCounts[opponetIndex] = (playerCounts[opponetIndex] or 0) + 1 end - local addToParticipants = function(currentPlayer, player, playerIndex) - local faction = currentPlayer.faction or (player.extradata or {}).faction or Faction.defaultFaction + local modeParts = Array.map(playerCounts, function(count, opponentIndex) + if count == 0 then + return Opponent.literal + end - participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, - player = player.name, - position = currentPlayer.position, - flag = Flags.CountryName(player.flag), - heroes = CustomMatchGroupInput._readHeroes( - currentPlayer.heroes, - faction, - player.name, - currentPlayer.heroesCheckDisabled - ), - random = currentPlayer.playedRandom, - } + return count + end) + + return table.concat(modeParts, 'v') +end + +---@param map table +---@param participants table +---@return table +function MapFunctions.getAdditionalExtraData(map, participants) + if map.mode ~= '1v1' then return {} end + + local extradata = MapFunctions.getHeroesExtradata(participants) + + local players = {} + for _, player in Table.iter.spairs(participants) do + table.insert(players, player) end - Array.forEach(players, function(player, playerIndex) - local currentPlayer = playerData[player.name] - if not currentPlayer then return end + extradata.opponent1 = players[1].player + extradata.opponent2 = players[2].player - addToParticipants(currentPlayer, player, playerIndex) - playerData[player.name] = nil - end) + if map.winner ~= 1 and map.winner ~= 2 then + return extradata + end + local loser = 3 - map.winner - -- if we have players not already in the match2players insert them - -- this is to break conditional data loops between match2 and teamCard/HDB - Table.iter.forEachPair(playerData, function(playerLink, player) - local faction = player.faction or Faction.defaultFaction - table.insert(players, { - name = playerLink, - displayname = player.displayName, - extradata = {faction = faction}, - }) - addToParticipants(player, players[#players], #players) - numberOfPlayers = numberOfPlayers + 1 - end) + extradata.winnerfaction = players[map.winner].faction + extradata.loserfaction = players[loser].faction - Array.forEach(Array.range(1, amountOfTbds), function(tbdIndex) - participants[opponentIndex .. '_' .. (#players + tbdIndex)] = { - faction = Faction.defaultFaction, - player = TBD:upper(), - } - end) + return extradata +end - map.participants = participants +--- additionally store heroes in extradata so we can condition on them +---@param participants table +---@return table +function MapFunctions.getHeroesExtradata(participants) + local extradata = {} + for participantKey, participant in Table.iter.spairs(participants) do + local opponentIndex = string.match(participantKey, '^(%d+)_') + Array.forEach(participant.heroes or {}, function(hero, heroIndex) + extradata['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero + end) + end - return numberOfPlayers + return extradata end ---@param heroesInput string? @@ -701,7 +454,7 @@ end ---@param playerName string ---@param ignoreFactionHeroCheck boolean ---@return string[]? -function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) +function MapFunctions.readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) if String.isEmpty(heroesInput) then return end @@ -715,38 +468,11 @@ function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ign local isCoreFaction = Table.includes(Faction.coreFactions, faction) assert(ignoreFactionHeroCheck or not isCoreFaction or faction == heroData.faction or heroData.faction == DEFAULT_HERO_FACTION, - 'Invalid hero input "' .. hero .. '" for faction "' - .. Faction.toName(faction) .. '" of player "' .. playerName .. '"') + 'Invalid hero input "' .. hero .. '" for faction "' .. Faction.toName(faction) + .. '" of player "' .. playerName .. '"') return heroData.name end) end ----@param indexedScores table ----@return integer? -function CustomMatchGroupInput._getWinner(indexedScores) - table.sort(indexedScores, CustomMatchGroupInput._mapWinnerSortFunction) - - return indexedScores[1].index -end - ----@param opponent1 table ----@param opponent2 table ----@return boolean -function CustomMatchGroupInput._mapWinnerSortFunction(opponent1, opponent2) - local opponent1Norm = opponent1.status == SCORE_STATUS - local opponent2Norm = opponent2.status == SCORE_STATUS - - if opponent1Norm and opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - elseif opponent1Norm then return true - elseif opponent2Norm then return false - elseif opponent1.status == DEFAULT_WIN_STATUS then return true - elseif Table.includes(ALLOWED_STATUSES, opponent1.status) then return false - elseif opponent2.status == DEFAULT_WIN_STATUS then return false - elseif Table.includes(ALLOWED_STATUSES, opponent2.status) then return true - else return true - end -end - return CustomMatchGroupInput diff --git a/components/match2/wikis/stormgate/match_summary.lua b/components/match2/wikis/stormgate/match_summary.lua index fb59edff5d7..3ae5ba25147 100644 --- a/components/match2/wikis/stormgate/match_summary.lua +++ b/components/match2/wikis/stormgate/match_summary.lua @@ -366,17 +366,21 @@ function CustomMatchSummary._submatchHeader(submatch) } end + ---@param opponentIndex any + ---@return Html local createScore = function(opponentIndex) - local isWinner = opponentIndex == submatch.winner + local isWinner = opponentIndex == submatch.winner or submatch.resultType == 'draw' if submatch.resultType == 'default' then return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = isWinner and 'W' or submatch.walkover, + scoreText = isWinner and 'W' or string.upper(submatch.walkover), } end + + local score = submatch.resultType ~= 'np' and (submatch.scores or {})[opponentIndex] or nil return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = (submatch.scores or {})[opponentIndex] or '', + scoreText = score, } end diff --git a/components/opponent/wikis/stormgate/player_ext_custom.lua b/components/opponent/wikis/stormgate/player_ext_custom.lua index 2f3825d4090..e928a33e5c6 100644 --- a/components/opponent/wikis/stormgate/player_ext_custom.lua +++ b/components/opponent/wikis/stormgate/player_ext_custom.lua @@ -52,7 +52,11 @@ end) function CustomPlayerExt.fetchPlayerFaction(resolvedPageName, date) local lpdbPlayer = CustomPlayerExt.fetchPlayer(resolvedPageName) if lpdbPlayer and lpdbPlayer.factionHistory then - date = date or DateExt.getContextualDateOrNow() + local timestamp = DateExt.readTimestamp(date or DateExt.getContextualDateOrNow()) + ---@cast timestamp -nil + -- convert date to iso format to match the dates retrieved from the data points + -- need the time too so the below check remains the same as before + date = DateExt.formatTimestamp('Y-m-d H:i:s', timestamp) local entry = Array.find(lpdbPlayer.factionHistory, function(entry) return date <= entry.endDate end) return entry and Faction.read(entry.faction) else