diff --git a/components/match2/commons/match_group_input_util.lua b/components/match2/commons/match_group_input_util.lua index f68dde8b47f..ea9ed598282 100644 --- a/components/match2/commons/match_group_input_util.lua +++ b/components/match2/commons/match_group_input_util.lua @@ -30,13 +30,6 @@ local globalVars = PageVariableNamespace{cached = true} local MatchGroupInputUtil = {} -local DEFAULT_ALLOWED_VETOES = { - 'decider', - 'pick', - 'ban', - 'defaultban', -} - local NOT_PLAYED_INPUTS = { 'skip', 'np', @@ -44,6 +37,13 @@ local NOT_PLAYED_INPUTS = { 'cancelled', } +MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES = { + 'decider', + 'pick', + 'ban', + 'defaultban', +} + MatchGroupInputUtil.STATUS_INPUTS = { DEFAULT_WIN = 'W', DEFAULT_LOSS = 'L', @@ -686,7 +686,7 @@ end function MatchGroupInputUtil.getMapVeto(match, allowedVetoes) if not match.mapveto then return nil end - allowedVetoes = allowedVetoes or DEFAULT_ALLOWED_VETOES + allowedVetoes = allowedVetoes or MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES match.mapveto = Json.parseIfString(match.mapveto) diff --git a/components/match2/wikis/arenafps/match_group_input_custom.lua b/components/match2/wikis/arenafps/match_group_input_custom.lua index c88cca037cd..d69b73a7cb7 100644 --- a/components/match2/wikis/arenafps/match_group_input_custom.lua +++ b/components/match2/wikis/arenafps/match_group_input_custom.lua @@ -7,30 +7,22 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local MATCH_BYE = 'bye' - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'Duel' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -39,349 +31,145 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getTournamentVars(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + 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 - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + MatchFunctions.getTournamentVars(match) - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner + return match end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'Duel')) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end - if match.quakehistory then links.quakehistory = 'http://www.quakehistory.com/en/matches/' .. match.quakehistory end - if match.dbstats then links.dbstats = 'https://quakelife.ru/diabotical/stats/matches/?matches=' .. match.dbstats end - if match.qrindr then links.qrindr = 'https://qrindr.com/match/' .. match.qrindr end - if match.esl then links.esl = 'https://play.eslgaming.com/match/' .. match.esl end - if match.stats then links.stats = match.stats end + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - match.finished = Array.any(opponents, function(opponent) - return (tonumber(opponent.score) or 0) >= firstTo - end) - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) +end - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - return match end --- Get Playerdata for non-team opponents ----@param player table ----@return boolean -function CustomMatchGroupInput._playerIsBye(player) - return (player.name or ''):lower() == MATCH_BYE or (player.displayname or ''):lower() == MATCH_BYE +---@param match table +---@return table +function MatchFunctions.getLinks(match) + return { + preview = match.preview, + quakehistory = match.quakehistory and ('http://www.quakehistory.com/en/matches/' .. match.quakehistory) or nil, + dbstats = match.dbstats and ('https://quakelife.ru/diabotical/stats/matches/?matches=' .. match.dbstats) or nil, + qrindr = match.qrindr and ('https://qrindr.com/match/' .. match.qrindr) or nil, + esl = match.esl and ('https://play.eslgaming.com/match/' .. match.esl) or nil, + stats = match.stats, + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - 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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map -end - ----@param map table ----@return table -function mapFunctions.getTournamentVars(map) - map.mode = Logic.emptyOr(map.mode, Variables.varDefault('tournament_mode', 'team')) - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/arenafps/match_summary.lua b/components/match2/wikis/arenafps/match_summary.lua index 1f591585b4c..bfdaa81fa54 100644 --- a/components/match2/wikis/arenafps/match_summary.lua +++ b/components/match2/wikis/arenafps/match_summary.lua @@ -123,12 +123,12 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/battlerite/match_group_input_custom.lua b/components/match2/wikis/battlerite/match_group_input_custom.lua index f62ac04d8ca..3d2aa7abe62 100644 --- a/components/match2/wikis/battlerite/match_group_input_custom.lua +++ b/components/match2/wikis/battlerite/match_group_input_custom.lua @@ -6,43 +6,26 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Logic = require('Module:Logic') +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') -local Table = require('Module:Table') -local Variables = require('Module:Variables') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') +local Table = require('Module:Table') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 15 -local NO_SCORE = -99 -local DUMMY_MAP = 'default' -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DUMMY_MAP = 'default' -- Is set in Template:Map when |map= is empty. +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + pagifyPlayerNames = true, + maxNumPlayers = 15, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -51,378 +34,145 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getScoreFromMapWinners(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getLinks(match) - match = matchFunctions.getExtraData(match) - - -- Adjust map data, especially set participants data - match = matchFunctions.adjustMapData(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) ----@param match table ----@return table -function matchFunctions.adjustMapData(match) - for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do - match[key] = mapFunctions.getExtraData(map) - end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - return match -end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end + 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 - -- set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + MatchGroupInputUtil.getCommonTournamentVars(match) - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 + return match end --- Check if any opponent has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local setScores = false - - -- If the match has started, we want to use the automatic calculations - if match.dateexact and match.timestamp <= NOW then - setScores = true - end +-- Template:Map sets a default map name so we can count the number of maps. +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) +end - local mapIndex = 1 - while match['map'..mapIndex] do - local winner = tonumber(match['map'..mapIndex].winner) - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - setScores = true - newScores[winner] = (newScores[winner] or 0) + 1 +---@param match table +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - mapIndex = mapIndex + 1 - end - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and setScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getLinks(match) - match.links = {} - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif opponent.type ~= Opponent.solo and opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end - - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if - not Logic.isEmpty(match.winner) or - Logic.readBool(match.finished) or - CustomMatchGroupInput.placementCheckSpecialStatus(opponents) - then - match.finished = true - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - ----@param opponents table[] ----@param walkoverType string? ----@return table[] -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index, _ in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType - end - return opponents end -- -- map related functions -- --- Parse extradata information +-- Check if a map should be discarded due to being redundant +-- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ----@return table -function mapFunctions.getExtraData(map) - map.extradata.comment = map.comment - - return map +---@return boolean +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end --- Calculate Score and Winner of the map ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = STATUS_SCORE - score = tonumber(score) - map['score' .. scoreIndex] = score - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map +function MapFunctions.getExtraData(map, opponentCount) + return { + comment = map.comment, + } end return CustomMatchGroupInput diff --git a/components/match2/wikis/clashofclans/match_group_input_custom.lua b/components/match2/wikis/clashofclans/match_group_input_custom.lua index efc8a269b54..7515b0c17c8 100644 --- a/components/match2/wikis/clashofclans/match_group_input_custom.lua +++ b/components/match2/wikis/clashofclans/match_group_input_custom.lua @@ -7,432 +7,192 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local MathUtil = require('Module:MathUtil') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_PLAYERS = 10 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + pagifyPlayerNames = true, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup +---@param match table +---@param options table? +---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - CustomMatchGroupInput._underScoreAdjusts(match) + local games = MatchFunctions.extractMaps(match, #opponents) - return match -end - -function CustomMatchGroupInput._underScoreAdjusts(match) - local fixUnderscore = function(page) - return page and page:gsub(' ', '_') or page - end - - for opponentKey, opponent in Table.iter.pairsByPrefix(match, 'opponent') do - opponent.name = fixUnderscore(opponent.name) - - for _, player in Table.iter.pairsByPrefix(match, opponentKey .. '_p') do - player.name = fixUnderscore(player.name) - end - end -end - --- called from Module:Match/Subobjects -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - map.map = nil - - return map -end - -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if CustomMatchGroupInput.isDraw(indexedScores, tonumber(data.winner)) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = CustomMatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if CustomMatchGroupInput.placementCheckFF(indexedScores) then - data.walkover = 'ff' - elseif CustomMatchGroupInput.placementCheckDQ(indexedScores) then - data.walkover = 'dq' - elseif CustomMatchGroupInput.placementCheckWL(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, tonumber(data.winner), nil, data.finished) - data.winner = tonumber(data.winner) or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end - - return data, indexedScores -end - ----@param indexedScores table[] ----@param winner integer? ----@return boolean -function CustomMatchGroupInput.isDraw(indexedScores, winner) - if winner == 0 then return true end - if winner then return false end - return MatchGroupInput.isDraw(indexedScores) -end - -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' or winner then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local last = {score = NO_SCORE, placement = NO_SCORE} - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if last.score == score and last.time == opp.time and last.percentage == opp.percentage then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or last.placement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - last = { - score = score or NO_SCORE, - placement = counter, - time = opp.time, - percentage = opp.percentage, - } - end - end - end - - return opponents, winner -end - -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local score1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local score2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE + match.bestof = MatchFunctions.getBestOf(match.bestof) - if score1 ~= score2 then - return score1 > score2 - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - local percentage1 = tbl[key1].percentage - local percentage2 = tbl[key2].percentage + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - if percentage1 ~= percentage2 then - return percentage1 > percentage2 + 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 - local time1 = tbl[key1].time - local time2 = tbl[key2].time - - if time1 == time2 or time2 and not time1 then - return false - elseif not time2 then - return true - end + MatchFunctions.getTournamentVars(match) - return time1 < time2 -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) --- Check if any team has a none-standard status -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status ~= 'S' end) -end + match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) --- function to check for forfeits -function CustomMatchGroupInput.placementCheckFF(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'FF' end) -end + match.games = games + match.opponents = opponents --- function to check for DQ's -function CustomMatchGroupInput.placementCheckDQ(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'DQ' end) -end + match.extradata = MatchFunctions.getExtraData(match) --- function to check for W/L -function CustomMatchGroupInput.placementCheckWL(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'L' end) + return match end --- Get the winner when resulttype=default -function CustomMatchGroupInput.getDefaultWinner(tbl) - for index, scoreInfo in pairs(tbl) do - if scoreInfo.status == 'W' then - return index - end - end - return -1 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break - end - end - - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end - end - - return match -end - -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - - return match -end -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mvpteam = match.mvpteam or match.winner, +---@param match table +---@return table +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), } - - return match end --- Parse MVP input -function matchFunctions.getMVP(match) - if not match.mvp then return {} end - local mvppoints = match.mvppoints or 1 - - -- Split the input - local players = mw.text.split(match.mvp, ',') - - -- Trim the input - for index, player in pairs(players) do - players[index] = mw.text.trim(player) - end - - return {players = players, points = mvppoints} -end - -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = matchFunctions.getTeamPlayers(match, opponentIndex, opponent.name) - end +---@param match table +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.map = nil + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return { + score = score, + status = status, + time = map.extradata.times[opponentIndex], + percentage = map.extradata.percentages[opponentIndex] or 0 + } + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof / 2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end - end + table.insert(maps, map) + match[key] = nil end - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end + return maps +end - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF +end - return match +---@param match table +---@return table +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end --- Get Playerdata from Vars (get's set in TeamCards) for team opponents -function matchFunctions.getTeamPlayers(match, opponentIndex, teamName) - -- let's make sure we don't leave any gaps. - match['opponent' .. opponentIndex].match2players = {} - for playerIndex = 1, MAX_NUM_PLAYERS do - -- parse player - local player = Json.parseIfString(match['opponent' .. opponentIndex .. '_p' .. playerIndex]) or {} - player.name = player.name or Variables.varDefault(teamName .. '_p' .. playerIndex) - player.flag = player.flag or Variables.varDefault(teamName .. '_p' .. playerIndex .. 'flag') - player.displayname = player.displayname or Variables.varDefault(teamName .. '_p' .. playerIndex .. 'dn') - if not Table.isEmpty(player) then - table.insert(match['opponent' .. opponentIndex].match2players, player) - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end +end - return match +---@param match table +---@return table +function MatchFunctions.getLinks(match) + return {} end -- -- map related functions -- --- Parse extradata information -function mapFunctions.getExtraData(map) - map.extradata = { +---@param map table +---@return table +function MapFunctions.getExtraData(map) + return { comment = map.comment, - times = mapFunctions.readTimes(map), - percentages = mapFunctions.readPercentages(map), + times = MapFunctions.readTimes(map), + percentages = MapFunctions.readPercentages(map), } - return map end -function mapFunctions.readPercentages(map) +---@param map table +---@return table +function MapFunctions.readPercentages(map) local percentages = {} for _, percentage in Table.iter.pairsByPrefix(map, 'percent') do @@ -442,7 +202,9 @@ function mapFunctions.readPercentages(map) return percentages end -function mapFunctions.readTimes(map) +---@param map table +---@return table +function MapFunctions.readTimes(map) local timesInSeconds = {} for _, timeInput in Table.iter.pairsByPrefix(map, 'time') do @@ -460,34 +222,4 @@ function mapFunctions.readTimes(map) return timesInSeconds end --- Calculate Score and Winner of the map -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = tonumber(map['score' .. scoreIndex]) or map['score' .. scoreIndex] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = tonumber(score) - obj.time = map.extradata.times[scoreIndex] - obj.percentage = map.extradata.percentages[scoreIndex] or 0 - 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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map -end - return CustomMatchGroupInput diff --git a/components/match2/wikis/clashofclans/match_summary.lua b/components/match2/wikis/clashofclans/match_summary.lua index 41e6c3a14a3..46b7527bf17 100644 --- a/components/match2/wikis/clashofclans/match_summary.lua +++ b/components/match2/wikis/clashofclans/match_summary.lua @@ -94,10 +94,15 @@ function CustomMatchSummary.createBody(match) end function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' return mw.html.create('div') :css('width', '16px') - :wikitext(score) + :wikitext(DisplayHelper.MapScore( + game.scores[opponentIndex], + opponentIndex, + game.resultType, + game.walkover, + game.winner + )) end function CustomMatchSummary._percentage(game, opponentIndex) diff --git a/components/match2/wikis/criticalops/match_group_input_custom.lua b/components/match2/wikis/criticalops/match_group_input_custom.lua index 6ecae1a8ddb..8200bd3e642 100644 --- a/components/match2/wikis/criticalops/match_group_input_custom.lua +++ b/components/match2/wikis/criticalops/match_group_input_custom.lua @@ -6,369 +6,164 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') -local MathUtil = require('Module:MathUtil') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local MathUtil = require('Module:MathUtil') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local DEFAULT_MODE = 'team' +local DUMMY_MAP = 'null' -- Is set in Template:Map when |map= is empty. local SIDE_DEF = 'ct' local SIDE_ATK = 't' -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local NOT_PLAYED_MATCH_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local NOT_PLAYED_RESULT_TYPE = 'np' -local DRAW_RESULT_TYPE = 'draw' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local NOT_PLAYED_SCORE = -1 -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local DEFAULT_RESULT_TYPE = 'default' -local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.removeUnsetMaps(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getExtraData(match) +function CustomMatchGroupInput.processMatch(match, options) + match.finished = Logic.nilIfEmpty(match.finished) or match.status - return match -end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return map -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + 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 - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + MatchFunctions.getTournamentVars(match) --- function to check for draws ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckDraw(tbl) - if #tbl < MAX_NUM_OPPONENTS then - return false - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return MatchGroupInput.isDraw(tbl) -end + match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner = tonumber(data.winner) - if Logic.readBool(data.finished) then - if CustomMatchGroupInput.placementCheckDraw(indexedScores) then - data.winner = 0 - data.resulttype = DRAW_RESULT_TYPE - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 1) - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = STATUS_FORFEIT - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = STATUS_DISQUALIFIED - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = STATUS_DEFAULT_LOSS - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - elseif CustomMatchGroupInput.placementCheckScoresSet(indexedScores) then - --C-OPS only has exactly 2 opponents, neither more or less - if #indexedScores == MAX_NUM_OPPONENTS then - if tonumber(indexedScores[1].score) > tonumber(indexedScores[2].score) then - data.winner = 1 - else - data.winner = 2 - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - end - end - --If a manual winner is set use it - if winner and data.resulttype ~= DEFAULT_RESULT_TYPE then - if winner == 0 then - data.resulttype = DRAW_RESULT_TYPE - else - data.resulttype = nil - end - data.winner = winner - indexedScores = MatchGroupInput.setPlacement(indexedScores, winner, 1, 2) - end - end - return data, indexedScores -end + match.games = games + match.opponents = opponents + match.extradata = MatchFunctions.getExtraData(match) --- Check if any team has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) + return match end ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckScoresSet(tbl) - return Table.all(tbl, function (_, scoreinfo) return scoreinfo.status == STATUS_SCORE end) -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - local mapCount = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - mapCount = mapIndex - end - match.bestof = mapCount - return match -end - -- Template:Map sets a default map name so we can count the number of maps. --- These maps however shouldn't be stored in lpdb, nor displayed --- The discardMap function will check if a map should be removed --- Remove all maps that should be removed. ----@param match table ----@return table -function matchFunctions.removeUnsetMaps(match) - for mapKey, map in Table.iter.pairsByPrefix(match, 'map') do - if map.map == DUMMY_MAP_NAME then - match[mapKey] = nil - end - end - return match +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - -- For best of 1, display the results of the single map - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - local newScores = {} - local foundScores = false - if match.bestof == 1 then - if match.map1 then - newScores = match.map1.scores - foundScores = true - end - else -- For best of >1, disply the map wins - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - foundScores = true - -- Only two opponents in C-OPS - if winner and winner > 0 and winner <= 2 then - newScores[winner] = (newScores[winner] or 0) + 1 - end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end + + table.insert(maps, map) + match[key] = nil end - if not opponent1.score and foundScores then - opponent1.score = newScores[1] or 0 - end - if not opponent2.score and foundScores then - opponent2.score = newScores[2] or 0 - end - match.opponent1 = opponent1 - match.opponent2 = opponent2 - return match -end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - return match +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end ----@param match table ----@return string? -function matchFunctions.getMatchStatus(match) - if match.resulttype == NOT_PLAYED_RESULT_TYPE then - return match.status - else - return nil +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - status = matchFunctions.getMatchStatus(match), - } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = matchFunctions.getPlayers(match, opponentIndex, opponent.name) - end - end - end - - -- Handle tournament status for unfinished matches - if (not Logic.readBool(match.finished)) and Logic.isNotEmpty(match.status) then - match.finished = match.status - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, match.finished) then - match.resulttype = NOT_PLAYED_MATCH_STATUSES - match.status = match.finished - match.finished = false - match.dateexact = false - else - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getLinks(match) + return {} end --- Get Playerdata from Vars (get's set in TeamCards) ---@param match table ----@param opponentIndex integer ----@param teamName string ---@return table -function matchFunctions.getPlayers(match, opponentIndex, teamName) - -- match._storePlayers will break after the first empty player. let's make sure we don't leave any gaps. - local count = 1 - for playerIndex = 1, MAX_NUM_PLAYERS do - -- parse player - local player = match['opponent' .. opponentIndex .. '_p' .. playerIndex] or {} - player = Json.parseIfString(player) - local playerPrefix = teamName .. '_p' .. playerIndex - player.name = player.name or Variables.varDefault(playerPrefix) - player.flag = player.flag or Variables.varDefault(playerPrefix .. 'flag') - player.displayname = player.displayname or Variables.varDefault(playerPrefix .. 'dn') - if not Table.isEmpty(player) then - match['opponent' .. opponentIndex .. '_p' .. count] = player - count = count + 1 - end - end - return match +function MatchFunctions.getExtraData(match) + return { + mapveto = MatchGroupInputUtil.getMapVeto(match), + status = match.resulttype == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED and match.status or nil, + } end -- @@ -379,104 +174,82 @@ end -- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ---@return boolean -function mapFunctions.discardMap(map) - return map.map == DUMMY_MAP_NAME +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end --- Parse extradata information ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { - comment = map.comment, - } - return map +function MapFunctions.getExtraData(map) + local extradata = MapFunctions.getSideData(map) + + return Table.merge(extradata, {comment = map.comment}) end ---@param map table ---@return table -function mapFunctions._getHalfScores(map) - map.extradata.t1sides = {} - map.extradata.t2sides = {} - map.extradata.t1halfs = {} - map.extradata.t2halfs = {} - - local key = '' - local overtimes = 0 - - local function getOppositeSide(side) - return side == SIDE_DEF and SIDE_ATK or SIDE_DEF - end - - while true do - local t1Side = map[key .. 't1firstside'] - if Logic.isEmpty(t1Side) or (t1Side ~= SIDE_DEF and t1Side ~= SIDE_ATK) then - break - end - local t2Side = getOppositeSide(t1Side) - - -- Iterate over two Halfs (In regular time a half is 15 rounds, after that sides switch) - for _ = 1, 2, 1 do - if(map[key .. 't1' .. t1Side] and map[key .. 't2' .. t2Side]) then - table.insert(map.extradata.t1sides, t1Side) - table.insert(map.extradata.t2sides, t2Side) - table.insert(map.extradata.t1halfs, tonumber(map[key .. 't1' .. t1Side]) or 0) - table.insert(map.extradata.t2halfs, tonumber(map[key .. 't2' .. t2Side]) or 0) - map[key .. 't1' .. t1Side] = nil - map[key .. 't2' .. t2Side] = nil - -- second half (sides switch) - t1Side, t2Side = t2Side, t1Side - end - end - - overtimes = overtimes + 1 - key = 'o' .. overtimes - end - - return map +function MapFunctions.getSideData(map) + ---@param sideInput string + ---@return boolean + local isValidSide = function(sideInput) + return Logic.isNotEmpty(sideInput) and (sideInput == SIDE_DEF or sideInput == SIDE_ATK) + end + + ---@param prefix string + ---@param t1Side string + ---@param t2Side string + ---@return {t1Side: string, t2Side: string, t1Half: integer, t2Half: integer}? + local getDataFor = function(prefix, t1Side, t2Side) + local half1 = tonumber(map[prefix .. 't1' .. t1Side]) + local half2 = tonumber(map[prefix .. 't2' .. t2Side]) + if not half1 or not half2 then return end + return { + t1Side = t1Side, + t2Side = t2Side, + t1Half = half1, + t2Half = half2, + } + end + + ---@param prefix string + ---@return {t1Side: string, t2Side: string, t1Half: integer, t2Half: integer}[]? + local getSideData = function(prefix) + local t1Side = map[prefix .. 't1firstside'] + if not isValidSide(t1Side) then return end + local t2Side = t1Side == SIDE_DEF and SIDE_ATK or SIDE_DEF + + return Array.append({}, + getDataFor(prefix, t1Side, t2Side), + getDataFor(prefix, t2Side, t1Side) + ) + end + + local sideData = getSideData('') or {} + + Array.extendWith(sideData, Array.mapIndexes(function(overtimeIndex) + return getSideData('o' .. overtimeIndex) + end)) + + return { + t1sides = Array.map(sideData, Operator.t1Side), + t2sides = Array.map(sideData, Operator.t2Side), + t1halfs = Array.map(sideData, Operator.t1Half), + t2halfs = Array.map(sideData, Operator.t2Half), + } end --- Calculate Score and Winner of the map --- Use the half information if available ---@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - - map = mapFunctions._getHalfScores(map) - - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score - if Table.includes(ALLOWED_STATUSES, map['score' .. scoreIndex]) then - score = map['score' .. scoreIndex] - elseif Logic.isNotEmpty(map.extradata['t' .. scoreIndex .. 'halfs']) then - score = MathUtil.sum(map.extradata['t' .. scoreIndex .. 'halfs']) - else - score = tonumber(map['score' .. scoreIndex]) - end - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = STATUS_SCORE - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - map.scores[scoreIndex] = score - indexedScores[scoreIndex] = obj +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(map) + local sideData = MapFunctions.getSideData(map) + return function(opponentIndex) + local partialScores = sideData['t' .. opponentIndex .. 'halfs'] + if Logic.isEmpty(partialScores) then + return end - end - if Table.includes(NOT_PLAYED_MATCH_STATUSES, map.finished) then - map.resulttype = NOT_PLAYED_RESULT_TYPE - else - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) + return MathUtil.sum(partialScores) end - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/criticalops/match_summary.lua b/components/match2/wikis/criticalops/match_summary.lua index 3ce30aed072..61031084aa5 100644 --- a/components/match2/wikis/criticalops/match_summary.lua +++ b/components/match2/wikis/criticalops/match_summary.lua @@ -318,8 +318,8 @@ function CustomMatchSummary._createMap(game) local team2Score = Score('rtl'):setRight() -- Teams map score - team1Score:setMapScore(game.scores[1]) - team2Score:setMapScore(game.scores[2]) + team1Score:setMapScore(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) + team2Score:setMapScore(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) local t1sides = extradata['t1sides'] or {} local t2sides = extradata['t2sides'] or {} diff --git a/components/match2/wikis/crossfire/match_group_input_custom.lua b/components/match2/wikis/crossfire/match_group_input_custom.lua index 969b59e804a..04ce90eb7cb 100644 --- a/components/match2/wikis/crossfire/match_group_input_custom.lua +++ b/components/match2/wikis/crossfire/match_group_input_custom.lua @@ -7,364 +7,180 @@ -- local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + pagifyPlayerNames = true, + maxNumPlayers = 5 +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + 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 - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + MatchFunctions.getTournamentVars(match) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return data, indexedScores -end + match.games = games + match.opponents = opponents ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = -99 - local lastPlacement = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or -99 - end - end - end + match.extradata = MatchFunctions.getExtraData(match) - return opponents, winner + return match end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - match = MatchGroupInput.getCommonTournamentVars(match) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) +end - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - casters = MatchGroupInput.readCasters(match, {noSort = true}) - } - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - maxNumPlayers = 5, resolveRedirect = true, applyUnderScores = true - }) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - match.finished = Logic.readBool(match.finished) - or isScoreSet and ( - Array.any(opponents, function(opponent) return (tonumber(opponent.score) or 0) > match.bestof/2 end) - or Array.all(opponents, function(opponent) return (tonumber(opponent.score) or 0) == match.bestof/2 end) - ) - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/crossfire/match_summary.lua b/components/match2/wikis/crossfire/match_summary.lua index 116d75de43d..15839bd1ce0 100644 --- a/components/match2/wikis/crossfire/match_summary.lua +++ b/components/match2/wikis/crossfire/match_summary.lua @@ -85,14 +85,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] - return mw.html.create('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -121,11 +113,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) row:addElement(leftNode) diff --git a/components/match2/wikis/halo/match_group_input_custom.lua b/components/match2/wikis/halo/match_group_input_custom.lua index 9b842b0b729..4704983e743 100644 --- a/components/match2/wikis/halo/match_group_input_custom.lua +++ b/components/match2/wikis/halo/match_group_input_custom.lua @@ -6,30 +6,23 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local MATCH_BYE = {'bye', 'BYE'} -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -38,373 +31,153 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local games = MatchFunctions.extractMaps(match, #opponents) - return map -end + match.bestof = MatchFunctions.getBestOf(match.bestof) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + 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 - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + MatchFunctions.getTournamentVars(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.games = games + match.opponents = opponents - return data, indexedScores -end - ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.extradata = MatchFunctions.getExtraData(match) - return opponents, winner + return match end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - match.links = {} - local links = match.links - if match.faceit then links.faceit = 'https://www.faceit.com/en/halo_infinite/room/' .. match.faceit end - if match.halodatahive then links.halodatahive = 'https://halodatahive.com/Series/Summary/' .. match.halodatahive end - if match.stats then links.stats = match.stats end + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - casters = MatchGroupInput.readCasters(match), - } - return match +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match end --- Get Playerdata for non-team opponents ---@param match table ----@param opponentType OpponentType ----@param opponentIndex integer ----@return table[] -function matchFunctions.getPlayers(match, opponentType, opponentIndex) - local players = {} - for playerIndex = 1, Opponent.partySize(opponentType) do - -- parse player - local player = Json.parseIfString(match['opponent' .. opponentIndex .. '_p' .. playerIndex]) or {} - player.name = player.name or 'TBD' - player.flag = player.flag - player.displayname = player.displayname or player.name - if Table.isNotEmpty(player) then - table.insert(players, player) - end - end - - return players +---@return table +function MatchFunctions.getLinks(match) + return { + faceit = match.faceit and ('https://www.faceit.com/en/halo_infinite/room/' .. match.faceit) or nil, + halodatahive = match.halodatahive and ('https://halodatahive.com/Series/Summary/' .. match.halodatahive) or nil, + stats = match.stats, + } end ----@param player table ----@return boolean -function CustomMatchGroupInput._playerIsBye(player) - return (player.name or ''):lower() == MATCH_BYE or (player.displayname or ''):lower() == MATCH_BYE +---@param match table +---@return table +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - 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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/halo/match_summary.lua b/components/match2/wikis/halo/match_summary.lua index 2b587156380..d1521916fd3 100644 --- a/components/match2/wikis/halo/match_summary.lua +++ b/components/match2/wikis/halo/match_summary.lua @@ -119,14 +119,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' - return mw.html.create('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -155,11 +147,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._addCheckmark(game.winner == 1)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._addCheckmark(game.winner == 2)) row:addElement(leftNode) diff --git a/components/match2/wikis/osu/match_group_input_custom.lua b/components/match2/wikis/osu/match_group_input_custom.lua index a253ead4ce5..ab93687078b 100644 --- a/components/match2/wikis/osu/match_group_input_custom.lua +++ b/components/match2/wikis/osu/match_group_input_custom.lua @@ -6,367 +6,187 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_VETOES = {'decider', 'pick', 'ban', 'defaultban', 'protect'} -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 20 +local ALLOWED_VETOES = Array.append(MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES, 'protect') local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + 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 - return match -end + MatchFunctions.getTournamentVars(match) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return map -end + match.games = games + match.opponents = opponents ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + match.extradata = MatchFunctions.getExtraData(match) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + return match +end - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end +CustomMatchGroupInput.processMap = FnUtil.identity - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end +-- +-- match related functions +-- ----@param data table ----@param indexedScores table[] ----@return table +---@param match table +---@param opponentCount integer ---@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local percentageScore = (map['score' .. opponentIndex] or ''):match('(%d+)%%') + if percentageScore then + return {score = map['score' .. opponentIndex], status = MatchGroupInputUtil.STATUS.SCORE} end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner + local scoreInput = string.gsub(map['score' .. opponentIndex] or '', ',', '') + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = scoreInput, + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - --set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true + table.insert(maps, map) + match[key] = nil end - return data, indexedScores + return maps end ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] +---@param bestofInput string|integer? ---@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local temporaryScore - local temporaryPlace = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) or '' - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if temporaryScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or temporaryPlace - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - temporaryPlace = counter - temporaryScore = score - end - end - end +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - return opponents, winner -end + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end --- --- match related functions --- - ---@param match table ---@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break - end - end - - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return match end ---@param match table ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) +function MatchFunctions.getLinks(match) + local links = {preview = match.preview} - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end for key, linkPart in Table.iter.pairsByPrefix(match, 'mplink', {requireIndex = false}) do links[key] = 'https://osu.ppy.sh/community/matches/' .. linkPart end - return match + return links end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mapveto = MatchGroupInput.getMapVeto(match, ALLOWED_VETOES), - casters = MatchGroupInput.readCasters(match), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + mapveto = MatchGroupInputUtil.getMapVeto(match, ALLOWED_VETOES), + casters = MatchGroupInputUtil.readCasters(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match end -- -- map related functions -- --- Parse extradata information ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/osu/match_summary.lua b/components/match2/wikis/osu/match_summary.lua index b32825c3e17..865b0711849 100644 --- a/components/match2/wikis/osu/match_summary.lua +++ b/components/match2/wikis/osu/match_summary.lua @@ -247,12 +247,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - return mw.html.create('div'):wikitext(game.scores[opponentIndex]) -end ---@param game MatchGroupUtilGame ---@return MatchSummaryRow @@ -279,15 +273,24 @@ function CustomMatchSummary._createMapRow(game) centerNode:addClass('brkts-popup-spaced-map-skip') end + ---@param score integer|string|nil + ---@return integer|string|nil + local displayNumericScore = function(score) + if not Logic.isNumeric(score) then + return score + end + return mw.getContentLanguage():formatNum(score --[[@as integer]]) + end + local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(displayNumericScore(game.scores[1]), 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(displayNumericScore(game.scores[2]), 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/smite/match_group_input_custom.lua b/components/match2/wikis/smite/match_group_input_custom.lua index 4bbfa2eaf3e..db8b241609a 100644 --- a/components/match2/wikis/smite/match_group_input_custom.lua +++ b/components/match2/wikis/smite/match_group_input_custom.lua @@ -7,416 +7,232 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') local FnUtil = require('Module:FnUtil') local GodNames = mw.loadData('Module:GodNames') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 15 -local MAX_NUM_GAMES = 7 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_BESTOF = 3 local DEFAULT_MODE = 'team' -local NO_SCORE = -99 -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 -local SECONDS_UNTIL_FINISHED_EXACT = 30800 -local SECONDS_UNTIL_FINISHED_NOT_EXACT = 86400 local DUMMY_MAP = 'default' - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MAX_NUM_PLAYERS = 15 +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + pagifyPlayerNames = true, + maxNumPlayers = MAX_NUM_PLAYERS, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getPicksAndBans(map) - map = mapFunctions.getAdditionalExtraData(map) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - return map -end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchFunctions.getBestOf(match.bestof) ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + 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 - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + MatchFunctions.getTournamentVars(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, STATUS_DRAW) - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.hasDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - -- set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return data, indexedScores -end + match.games = games + match.opponents = opponents ----@param opponents table[] ----@param winner nil ----@param specialType nil ----@param finished nil ----@return table[], nil -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == STATUS_DRAW then - for key in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and String.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.extradata = MatchFunctions.getExtraData(match) - return opponents, winner + return match end ---- @param tbl table ---- @param key1 string ---- @param key2 string ---- @return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = #Array.filter(Array.range(1, MAX_NUM_GAMES), function(idx) return match['map'.. idx] end) - return match -end --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local setScores = false - - -- If the match has started, we want to use the automatic calculations - if match.dateexact then - if match.timestamp <= NOW then - setScores = true - end - end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - setScores = true - newScores[winner] = (newScores[winner] or 0) + 1 - end + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + table.insert(maps, MapFunctions.readMap(map, opponentCount)) + match[key] = nil end - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and setScores then - match['opponent' .. index].score = newScores[index] or 0 - end + return maps +end + +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) + + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) +end + +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = { +function MatchFunctions.getLinks(match) + return { stats = match.stats, - smiteesports = match.smiteesports - and ('https://www.smiteesports.com/matches/' .. match.smiteesports) or nil, + smiteesports = match.smiteesports and ('https://www.smiteesports.com/matches/' .. match.smiteesports) or nil, } - return match end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif opponent.type ~= Opponent.solo and opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end +function MatchFunctions.getExtraData(match) + return { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + } +end - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end +-- +-- map related functions +-- - -- see if match should actually be finished if score is set - if not Logic.readBool(match.finished) then - matchFunctions._finishMatch(match, opponents, isScoreSet) - end +---@param map table +---@param opponentCount integer +---@return table? +function MapFunctions.readMap(map, opponentCount) + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] - -- apply placements and winner if finshed - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) + if not MapFunctions.keepMap(map) then + map.map = nil end - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent + if Logic.isDeepEmpty(map) then + return nil end - return match -end ----@param match table ----@param opponents table ----@param isScoreSet boolean ----@return table -function matchFunctions._finishMatch(match, opponents, isScoreSet) - -- If a winner has been set - if Logic.isNotEmpty(match.winner) then - match.finished = true - end + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) - -- If special status has been applied to a team - if MatchGroupInput.hasSpecialStatus(opponents) then - match.finished = true - end + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) - -- see if match should actually be finished if bestof limit was reached - match.finished = Logic.readBool(match.finished) - or isScoreSet and ( - Array.any(opponents, function(opponent) return (tonumber(opponent.score) or 0) > match.bestof/2 end) - or Array.all(opponents, function(opponent) return (tonumber(opponent.score) or 0) == match.bestof/2 end) - ) - - -- If enough time has passed since match started, it should be marked as finished - if isScoreSet and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and SECONDS_UNTIL_FINISHED_EXACT - or SECONDS_UNTIL_FINISHED_NOT_EXACT - if match.timestamp + threshold < NOW then - match.finished = true - end + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - return match + return map end ----@param opponents table ----@param walkoverType string ----@return table -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType +-- Check if a map should be discarded due to being redundant +-- DUMMY_MAP_NAME needs the match the default value in Template:Map +---@param map table +---@return boolean +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP +end + +---@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 - return opponents -end - ----@param match table ----@return table -function matchFunctions.getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match, {noSort = true}), - } - return match end --- --- map related functions --- - --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getAdditionalExtraData(map) - map.extradata.comment = map.comment - map.extradata.team1side = string.lower(map.team1side or '') - map.extradata.team2side = string.lower(map.team2side or '') - - return map +function MapFunctions.getExtraData(map, opponentCount) + return Table.merge({ + comment = map.comment, + team1side = string.lower(map.team1side or ''), + team2side = string.lower(map.team2side or ''), + }, MapFunctions.getPicksAndBans(map, opponentCount)) end ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getPicksAndBans(map) +function MapFunctions.getPicksAndBans(map, opponentCount) local godData = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, GodNames) - for opponentIndex = 1, MAX_NUM_OPPONENTS do + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, GodNames) + for opponentIndex = 1, opponentCount do for playerIndex = 1, MAX_NUM_PLAYERS do local god = map['t' .. opponentIndex .. 'g' .. playerIndex] godData['team' .. opponentIndex .. 'god' .. playerIndex] = getCharacterName(god) @@ -425,20 +241,8 @@ function mapFunctions.getPicksAndBans(map) godData['team' .. opponentIndex .. 'ban' .. playerIndex] = getCharacterName(ban) end end - map.extradata = godData - return map -end --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - if Logic.isNumeric(map.winner) then - map.winner = tonumber(map.winner) - map.finished = true - end - - return map + return godData end return CustomMatchGroupInput diff --git a/components/match2/wikis/splitgate/match_group_input_custom.lua b/components/match2/wikis/splitgate/match_group_input_custom.lua index 26c3636bb16..5853fb78fe2 100644 --- a/components/match2/wikis/splitgate/match_group_input_custom.lua +++ b/components/match2/wikis/splitgate/match_group_input_custom.lua @@ -6,28 +6,23 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_VODGAMES = 9 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -36,388 +31,151 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local games = MatchFunctions.extractMaps(match, #opponents) - return map -end + match.bestof = MatchFunctions.getBestOf(match.bestof) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + 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 - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + MatchFunctions.getTournamentVars(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - data.finished == 'skip' or - data.finished == 'np' or - data.finished == 'cancelled' or - data.finished == 'canceled' or - data.winner == 'skip' or - data.winner == 'np' or - data.winner == 'cancelled' or - data.winner == 'canceled' - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.games = games + match.opponents = opponents - return data, indexedScores -end + match.extradata = MatchFunctions.getExtraData(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end - - return opponents, winner + return match end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end +CustomMatchGroupInput.processMap = FnUtil.identity -- -- match related functions -- ----@param match any ----@return any -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - return MatchGroupInput.getCommonTournamentVars(match) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end + + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.esl then links.esl = 'https://play.eslgaming.com/match/' .. match.esl end - if match.stats then links.stats = match.stats end - - -- apply vodgames - for index = 1, MAX_NUM_VODGAMES do - local vodgame = match['vodgame' .. index] - if not Logic.isEmpty(vodgame) then - local map = match['map' .. index] or {} - map.vod = map.vod or vodgame - match['map' .. index] = map - end +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) +end + +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - return match end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = matchFunctions.getMapVeto(match), - mvp = MatchGroupInput.readMvp(match), - isconverted = 0 +function MatchFunctions.getLinks(match) + return { + esl = match.esl and ('https://play.eslgaming.com/match/' .. match.esl) or nil, + stats = match.stats, } - return match -end - --- Parse the mapVeto input ----@param match table ----@return table[] -function matchFunctions.getMapVeto(match) - if not match.vetoes then return {} end - - local vetoes = mw.text.split(match.vetoes or '', ',') - match.vetoes = nil - local vetoesBy = mw.text.split(match.vetoesBy or '', ',') - match.vetoesBy = nil - local index = 1 - local currentVetoMap = mw.text.trim(vetoes[1]) - local vetoData = {} - - while not String.isEmpty(currentVetoMap) do - local by = tonumber(mw.text.trim(vetoesBy[index]) or '') - vetoData[index] = { map = currentVetoMap, by = by } - index = index + 1 - currentVetoMap = mw.text.trim(vetoes[index] or '') - end - - return vetoData end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == 'team' and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - 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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/splitgate/match_summary.lua b/components/match2/wikis/splitgate/match_summary.lua index e96b1a4db60..1ee62ce7905 100644 --- a/components/match2/wikis/splitgate/match_summary.lua +++ b/components/match2/wikis/splitgate/match_summary.lua @@ -61,14 +61,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' - return htmlCreate('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -97,11 +89,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = htmlCreate('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, 'check')) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = htmlCreate('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, 'check')) row:addElement(leftNode) diff --git a/components/match2/wikis/worldoftanks/match_group_input_custom.lua b/components/match2/wikis/worldoftanks/match_group_input_custom.lua index b7cdcf5c402..5366fcdcbe4 100644 --- a/components/match2/wikis/worldoftanks/match_group_input_custom.lua +++ b/components/match2/wikis/worldoftanks/match_group_input_custom.lua @@ -6,364 +6,177 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_VETOES = {'decider', 'pick', 'ban', 'defaultban', 'protect'} -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 20 +local ALLOWED_VETOES = Array.append(MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES, 'protect') local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + 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 - return match -end + MatchFunctions.getTournamentVars(match) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return map -end + match.games = games + match.opponents = opponents ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + match.extradata = MatchFunctions.getExtraData(match) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + return match +end - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end +CustomMatchGroupInput.processMap = FnUtil.identity - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end +-- +-- match related functions +-- ----@param data table ----@param indexedScores table[] ----@return table +---@param match table +---@param opponentCount integer ---@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - --set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true + table.insert(maps, map) + match[key] = nil end - return data, indexedScores + return maps end ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] +---@param bestofInput string|integer? ---@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local temporaryScore - local temporaryPlace = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) or '' - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if temporaryScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or temporaryPlace - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - temporaryPlace = counter - temporaryScore = score - end - end - end +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - return opponents, winner -end + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end --- --- match related functions --- - ---@param match table ---@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end - end - - return match end ---@param match table ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end - - return match -end - ----@param match table ----@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mapveto = MatchGroupInput.getMapVeto(match, ALLOWED_VETOES), - casters = MatchGroupInput.readCasters(match), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + mapveto = MatchGroupInputUtil.getMapVeto(match, ALLOWED_VETOES), + casters = MatchGroupInputUtil.readCasters(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.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 - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/worldoftanks/match_summary.lua b/components/match2/wikis/worldoftanks/match_summary.lua index 3cf1e6d8cd0..b9612618fcd 100644 --- a/components/match2/wikis/worldoftanks/match_summary.lua +++ b/components/match2/wikis/worldoftanks/match_summary.lua @@ -243,12 +243,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - return mw.html.create('div'):wikitext(game.scores[opponentIndex]) -end ---@param game MatchGroupUtilGame ---@return MatchSummaryRow @@ -278,12 +272,12 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/zula/match_group_input_custom.lua b/components/match2/wikis/zula/match_group_input_custom.lua index eb236eb148f..f9e3a7a4d1b 100644 --- a/components/match2/wikis/zula/match_group_input_custom.lua +++ b/components/match2/wikis/zula/match_group_input_custom.lua @@ -6,338 +6,159 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') -local MathUtil = require('Module:MathUtil') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') - -local SIDE_DEF = 'ct' -local SIDE_ATK = 't' -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local NOT_PLAYED_MATCH_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local NOT_PLAYED_RESULT_TYPE = 'np' -local DRAW_RESULT_TYPE = 'draw' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local NOT_PLAYED_SCORE = -1 -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_RESULT_TYPE = 'default' -local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_MODE = 'team' +local DUMMY_MAP = 'null' -- Is set in Template:Map when |map= is empty. -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.removeUnsetMaps(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getExtraData(match) - - return match -end +function CustomMatchGroupInput.processMatch(match, options) + match.finished = Logic.nilIfEmpty(match.finished) or match.status --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return map -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + 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) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) --- function to check for draws ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckDraw(tbl) - if #tbl < MAX_NUM_OPPONENTS then - return false + 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 - return MatchGroupInput.isDraw(tbl) -end + MatchFunctions.getTournamentVars(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner = tonumber(data.winner) - if Logic.readBool(data.finished) then - if CustomMatchGroupInput.placementCheckDraw(indexedScores) then - data.winner = 0 - data.resulttype = DRAW_RESULT_TYPE - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 1) - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = STATUS_FORFEIT - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = STATUS_DISQUALIFIED - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = STATUS_DEFAULT_LOSS - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - elseif CustomMatchGroupInput.placementCheckScoresSet(indexedScores) then - --C-OPS only has exactly 2 opponents, neither more or less - if #indexedScores == MAX_NUM_OPPONENTS then - if tonumber(indexedScores[1].score) > tonumber(indexedScores[2].score) then - data.winner = 1 - else - data.winner = 2 - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - end - end - --If a manual winner is set use it - if winner and data.resulttype ~= DEFAULT_RESULT_TYPE then - if winner == 0 then - data.resulttype = DRAW_RESULT_TYPE - else - data.resulttype = nil - end - data.winner = winner - indexedScores = MatchGroupInput.setPlacement(indexedScores, winner, 1, 2) - end - end - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) + match.games = games + match.opponents = opponents --- Check if any team has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckScoresSet(tbl) - return Table.all(tbl, function (_, scoreinfo) return scoreinfo.status == STATUS_SCORE end) + return match end +CustomMatchGroupInput.processMap = FnUtil.identity + -- -- match related functions -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - local mapCount = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - mapCount = mapIndex - end - match.bestof = mapCount - return match -end - --- Template:Map sets a default map name so we can count the number of maps. --- These maps however shouldn't be stored in lpdb, nor displayed --- The discardMap function will check if a map should be removed --- Remove all maps that should be removed. ----@param match table ----@return table -function matchFunctions.removeUnsetMaps(match) - for mapKey, map in Table.iter.pairsByPrefix(match, 'map') do - if map.map == DUMMY_MAP_NAME then - match[mapKey] = nil +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - return match -end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - -- For best of 1, display the results of the single map - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - local newScores = {} - local foundScores = false - if match.bestof == 1 then - if match.map1 then - newScores = match.map1.scores - foundScores = true - end - else -- For best of >1, disply the map wins - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - foundScores = true - -- Only two opponents in C-OPS - if winner and winner > 0 and winner <= 2 then - newScores[winner] = (newScores[winner] or 0) + 1 - end - end - end - if not opponent1.score and foundScores then - opponent1.score = newScores[1] or 0 - end - if not opponent2.score and foundScores then - opponent2.score = newScores[2] or 0 + table.insert(maps, map) + match[key] = nil end - match.opponent1 = opponent1 - match.opponent2 = opponent2 - return match + + return maps end ---@param match table ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInput.getCommonTournamentVars(match) +function MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + return MatchGroupInputUtil.getCommonTournamentVars(match) end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - return match +-- Template:Map sets a default map name so we can count the number of maps. +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) end ----@param match table ----@return string? -function matchFunctions.getMatchStatus(match) - if match.resulttype == NOT_PLAYED_RESULT_TYPE then - return match.status - else - return nil +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - status = matchFunctions.getMatchStatus(match), - } - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - opponents[opponentIndex] = opponent - end - end - - -- Handle tournament status for unfinished matches - if (not Logic.readBool(match.finished)) and Logic.isNotEmpty(match.status) then - match.finished = match.status - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, match.finished) then - match.resulttype = NOT_PLAYED_MATCH_STATUSES - match.status = match.finished - match.finished = false - match.dateexact = false - else - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + status = match.resulttype == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED and match.status or nil, + mapveto = MatchGroupInputUtil.getMapVeto(match), + } end -- @@ -348,104 +169,15 @@ end -- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ---@return boolean -function mapFunctions.discardMap(map) - return map.map == DUMMY_MAP_NAME -end - --- Parse extradata information ----@param map table ----@return table -function mapFunctions.getExtraData(map) - map.extradata = { - comment = map.comment, - } - return map +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions._getHalfScores(map) - map.extradata.t1sides = {} - map.extradata.t2sides = {} - map.extradata.t1halfs = {} - map.extradata.t2halfs = {} - - local key = '' - local overtimes = 0 - - local function getOppositeSide(side) - return side == SIDE_DEF and SIDE_ATK or SIDE_DEF - end - - while true do - local t1Side = map[key .. 't1firstside'] - if Logic.isEmpty(t1Side) or (t1Side ~= SIDE_DEF and t1Side ~= SIDE_ATK) then - break - end - local t2Side = getOppositeSide(t1Side) - - -- Iterate over two Halfs (In regular time a half is 15 rounds, after that sides switch) - for _ = 1, 2, 1 do - if(map[key .. 't1' .. t1Side] and map[key .. 't2' .. t2Side]) then - table.insert(map.extradata.t1sides, t1Side) - table.insert(map.extradata.t2sides, t2Side) - table.insert(map.extradata.t1halfs, tonumber(map[key .. 't1' .. t1Side]) or 0) - table.insert(map.extradata.t2halfs, tonumber(map[key .. 't2' .. t2Side]) or 0) - map[key .. 't1' .. t1Side] = nil - map[key .. 't2' .. t2Side] = nil - -- second half (sides switch) - t1Side, t2Side = t2Side, t1Side - end - end - - overtimes = overtimes + 1 - key = 'o' .. overtimes - end - - return map -end - --- Calculate Score and Winner of the map --- Use the half information if available ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - - map = mapFunctions._getHalfScores(map) - - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score - if Table.includes(ALLOWED_STATUSES, map['score' .. scoreIndex]) then - score = map['score' .. scoreIndex] - elseif Logic.isNotEmpty(map.extradata['t' .. scoreIndex .. 'halfs']) then - score = MathUtil.sum(map.extradata['t' .. scoreIndex .. 'halfs']) - else - score = tonumber(map['score' .. scoreIndex]) - end - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = STATUS_SCORE - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - map.scores[scoreIndex] = score - indexedScores[scoreIndex] = obj - end - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, map.finished) then - map.resulttype = NOT_PLAYED_RESULT_TYPE - else - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - end - - return map +function MapFunctions.getExtraData(map, opponentCount) + return {comment = map.comment} end return CustomMatchGroupInput diff --git a/components/match2/wikis/zula/match_summary.lua b/components/match2/wikis/zula/match_summary.lua index fdc1ae6d9ee..d098273ebf3 100644 --- a/components/match2/wikis/zula/match_summary.lua +++ b/components/match2/wikis/zula/match_summary.lua @@ -318,8 +318,8 @@ function CustomMatchSummary._createMap(game) local team2Score = Score('rtl'):setRight() -- Teams map score - team1Score:setMapScore(game.scores[1]) - team2Score:setMapScore(game.scores[2]) + team1Score:setMapScore(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) + team2Score:setMapScore(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) local t1sides = extradata['t1sides'] or {} local t2sides = extradata['t2sides'] or {}