diff --git a/lua/wikis/commons/TeamList/Starcraft.lua b/lua/wikis/commons/TeamList/Starcraft.lua new file mode 100644 index 00000000000..57f05d89cac --- /dev/null +++ b/lua/wikis/commons/TeamList/Starcraft.lua @@ -0,0 +1,443 @@ +--- +-- @Liquipedia +-- page=Module:TeamList/Starcraft +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local Array = require('Module:Array') +local Class = require('Module:Class') +local Json = require('Module:Json') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Table = require('Module:Table') + +local TeamCard = Lua.import('Module:TeamList/Starcraft/TeamCard') +local TournamentStructure = Lua.import('Module:TournamentStructure') + +local Opponent = Lua.import('Module:Opponent/Custom') + +local TeamParticipantsController = Lua.import('Module:TeamParticipants/Controller') +local Tabs = Lua.import('Module:Tabs') + +local TeamListWrapper = {} + +---@class StarcraftTeamList +---@operator call(table): StarcraftTeamList +---@field args table +---@field config StarcraftTeamListConfig +---@field sections StarcraftTeamListSection[] +---@field root Html? +local TeamList = Class.new( + function(self, args) + self.args = args + end +) + +---@param frame Frame +---@return Renderable? +function TeamListWrapper.TemplateTeamList(frame) + local args = Arguments.getArgs(frame) + + for _, item in pairs(args) do + if item:find('<%s*br%s*/?>') then + mw.ext.TeamLiquidIntegration.add_category('TeamList with br') + end + end + + local newArgs = TeamList(args):read():map() + + if Logic.readBool(args.generate) then + TeamListWrapper.generate(newArgs) + end + + if Array.any(newArgs, function(section) + return Array.any(section, function(opp) + return Logic.isNotEmpty(opp.notes) + end) + end) then + mw.ext.TeamLiquidIntegration.add_category('TeamList with notes') + end + + if not newArgs[2] then + return TeamParticipantsController.fromTemplate(newArgs[1]) + end + + local tabArgs = {} + Array.forEach(newArgs, function(tpArgs, index) + if not tpArgs.title then + mw.ext.TeamLiquidIntegration.add_category('TeamList with missing section title') + end + tabArgs['name' .. index] = tpArgs.title + tabArgs['content' .. index] = TeamParticipantsController.fromTemplate(tpArgs) + end) + + return Tabs.dynamic(tabArgs) +end + +---@param args table[] +---@return string +function TeamListWrapper.generate(args) + if not args[2] then + return TeamListWrapper.generateSingle(args[1]) + end + + local parts = {'{{Tabs dynamic'} + Array.forEach(args, function(tpArgs, index) + table.insert(parts, '|name' .. index .. '=' .. tpArgs.title) + end) + table.insert(parts, '|This=1') + table.insert(parts, '}}') + + Array.forEach(args, function(tpArgs, index) + table.insert(parts, '{{Tabs dynamic/tab|' .. index .. '}}') + table.insert(parts, TeamListWrapper.generateSingle(tpArgs)) + end) + + table.insert(parts, '{{Tabs dynamic/end}}') + + return table.concat(parts, '\n') +end + +---@param args table +---@return string +function TeamListWrapper.generateSingle(args) + local parts = { + '{{TeamParticipants', + TeamListWrapper.generateOuterConfig(args), + } + + Array.forEach(args, function(oppArgs) + table.insert(parts, TeamListWrapper.generateOpponent(oppArgs)) + end) + table.insert(parts, '}}') + + return table.concat(parts, '\n') +end + +---@param args table +---@return string? +function TeamListWrapper.generateOuterConfig(args) + local params = { + 'showplayerinfo', + 'date', + } + + local parts = Array.map(params, function(param) + local value = args[param] + if Logic.isEmpty(value) then return end + return '|' .. param .. '=' .. value + end) + + local store = args.store == false and 'false' or 'false' + table.insert(parts, '|store=' .. store) + + return table.concat(parts) +end + + +---@param args table +---@return string +function TeamListWrapper.generateOpponent(args) + local parts = { + '\t|{{Opponent|' .. args.template, + '\t\t|import=false', + Logic.isNotEmpty(args.date) and ('\t\t|date=' .. args.date) or nil, + } + + table.insert(parts, '\t\t|players={{Persons') + Array.forEach(args.players, function(playerArgs) + table.insert(parts, TeamListWrapper.generatePlayer(playerArgs)) + end) + table.insert(parts, '\t\t}}') + + if Logic.isNotEmpty(args.notes) then + table.insert(parts, '\t\t|notes={{Json') + Array.forEach(args.notes, function(note) + table.insert(parts, '\t\t\t|' .. note) + end) + table.insert(parts, '\t\t}}') + end + + table.insert(parts, '\t}}') + + return table.concat(parts, '\n') +end + +---@param args table +---@return string +function TeamListWrapper.generatePlayer(args) + local parts = { + '\t\t\t|{{Person|' .. args.name, + } + + local add = function(param) + local value = args[param] + if Logic.isEmpty(value) then return end + table.insert(parts, '|' .. param .. '=' .. tostring(value)) + end + + local params = { + 'link', + 'flag', + 'faction', + 'team', + 'role', + 'played', + 'results', + 'status', + } + Array.forEach(params, add) + + table.insert(parts, '}}') + + return table.concat(parts) +end + +---@return self +function TeamList:read() + self.config = TeamList.readConfig(self.args) + self:readSections() + + return self +end + +---@return table[] +function TeamList:map() + return Array.map(self.sections, function(section) + local config = section.config + + local args = { + title = section.title or config.title, + showplayerinfo = config.playerInfoButton and 'true' or nil, + date = config.resolveDate, + store = not config.noStorage, + } + + if args.title and config.showCountBySection then + args.title = args.title .. ' (' .. (config.count or #section.entries) .. ')' + end + + if config.sortTeams then + Array.sortInPlaceBy(section.entries, function(entry) return entry.name:lower() end) + end + + return Table.mergeInto(args, Array.map(section.entries, TeamList.mapEntry)) + end) +end + +---@param entry StarcraftTeamCard +---@return table +function TeamList.mapEntry(entry) + local opp = entry.opponent + local notes = {opp.note} + local args = { + import = 'false', --- disallow this shit as it just fucks up things ... + players = Array.map(opp.players, function(player) + table.insert(notes, player.note) + return TeamList.mapPlayer(player) + end), + template = opp.template, + date = opp.date, + } + args.notes = notes + + if opp.dq then + mw.ext.TeamLiquidIntegration.add_category('TeamList with dq opponent') + end + + return args +end + +---@param player StarcraftTeamCardPlayer +---@return table +function TeamList.mapPlayer(player) + local args = { + name = player.displayName or player.pageName, + link = player.pageName, + flag = player.flag, + faction = player.faction, + team = player.mainTeamPage, + } + + if player.dnp then + args.played = 'false' + end + args.role = player.captain and 'Captain' or nil + + if player.dq then + args.status = 'former' + args.result = 'false' + elseif player.withdraw then + args.status = 'former' + end + + return args +end + +---@class StarcraftTeamListConfig: StarcraftTeamCardConfig +---@field showCountBySection boolean +---@field count number? +---@field title string? +---@field sortTeams boolean +---@field playerInfoButton boolean +---@field matchGroupSpec {matchGroupIds: string[], pageNames: string[]}? +---@field import boolean +---@field importOnlyQualified boolean + +---@param args table +---@param parentConfig StarcraftTeamListConfig? +---@return StarcraftTeamListConfig +function TeamList.readConfig(args, parentConfig) + parentConfig = parentConfig or {} + + local matchGroupSpec = TournamentStructure.readMatchGroupsSpec(args) + local import = matchGroupSpec ~= nil + + local config = { + --display + showCountBySection = Logic.readBool(args.showCountBySection or parentConfig.showCountBySection), + count = tonumber(args.count), + title = args.title, + sortTeams = Logic.nilOr(Logic.readBoolOrNil(args.sortTeams), parentConfig.sortTeams, true), + playerInfoButton = Logic.readBool(args.playerInfoButton), + isAdhoc = Logic.nilOr(Logic.readBoolOrNil(args.adhoc), parentConfig.isAdhoc, false), + --import + matchGroupSpec = matchGroupSpec, + import = import, + importOnlyQualified = Logic.readBool(args.onlyQualified), + autoDnp = Logic.nilOr(Logic.readBoolOrNil(args.autoDnp), import or parentConfig.import or nil), + } + + return Table.merge(TeamCard.readConfig(args, parentConfig), config) +end + +function TeamList:readSections() + self.sections = {} + + local firstSection = Json.parseIfTable(self.args[1]) + if not firstSection then + return + elseif firstSection.type ~= 'section' then + --assume no sections and treat whole list as first section + table.insert(self.sections, self:readSection(self.args)) + return + end + + Array.forEach(self.args, function(potentialSection) + local sectionArgs = Json.parseIfTable(potentialSection) + assert(sectionArgs and sectionArgs.type == 'section', 'Invalid input: "' .. potentialSection .. '" is not a section') + table.insert(self.sections, self:readSection(sectionArgs)) + end) +end + +---@class StarcraftTeamListSection +---@field config StarcraftTeamListConfig +---@field title string? +---@field entries StarcraftTeamCard[] + +---@param sectionArgs table +---@return StarcraftTeamListSection +function TeamList:readSection(sectionArgs) + local section = {config = TeamList.readConfig(sectionArgs, self.config), title = sectionArgs.title} + local entriesByName = {} + + sectionArgs = Array.extractValues(Table.filterByKey(sectionArgs, function(key) return type(key) == 'number' end)) + + Array.forEach(sectionArgs, function(teamCardArgs) + local entry = TeamCard(Table.merge({date = section.config.resolveDate}, Json.parseIfTable(teamCardArgs))) + entriesByName[entry.name] = entry + end) + + section.entries = Array.map(self:import(section.config, entriesByName), function(entry) + return entry:getConfig(section.config):sync(self.config.matchGroupSpec) + end) + + return section +end + +---@param config StarcraftTeamListConfig +---@param entriesByName table +---@return StarcraftTeamCard[] +function TeamList:import(config, entriesByName) + if not config.import then + return Array.extractValues(entriesByName) + end + + local matchRecords = TeamList._fetchMatchRecords(config.matchGroupSpec) + if Table.isEmpty(matchRecords) then + return Array.extractValues(entriesByName) + end + ---@cast matchRecords -nil + return TeamList._entriesFromMatchRecords(matchRecords, config, entriesByName) +end + +---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]} +---@return table[] +function TeamList._fetchMatchRecords(matchGroupSpec) + return mw.ext.LiquipediaDB.lpdb('match2', { + conditions = tostring(TournamentStructure.getMatch2Filter(matchGroupSpec)), + query = 'pagename, match2bracketdata, match2opponents, winner', + order = 'date asc', + limit = 5000, + }) +end + +---@param matchRecords table[] +---@param config StarcraftTeamListConfig +---@param entriesByName table +---@return StarcraftTeamCard[] +function TeamList._entriesFromMatchRecords(matchRecords, config, entriesByName) + Array.forEach(matchRecords, function(matchRecord) + Array.forEach(matchRecord.match2opponents, function(opponentRecord, opponentIndex) + if not TeamList._shouldInclude(opponentIndex, matchRecord, config.importOnlyQualified) then + return + end + + if entriesByName[opponentRecord.name] then + return + end + + entriesByName[opponentRecord.name] = TeamList._entryFromOpponentRecord(opponentRecord, config.resolveDate) + end) + end) + + return Array.extractValues(entriesByName) +end + +---@param opponentIndex integer +---@param matchRecord table +---@param importOnlyQualified boolean? +---@return boolean +function TeamList._shouldInclude(opponentIndex, matchRecord, importOnlyQualified) + local bracketData = matchRecord.match2bracketdata + return not importOnlyQualified or Logic.readBool(bracketData.quallose) or + Logic.readBool(bracketData.qualwin) and tonumber(matchRecord.winner) == opponentIndex +end + +---@param opponentRecord table +---@param resolveDate string +---@return StarcraftTeamCard? +function TeamList._entryFromOpponentRecord(opponentRecord, resolveDate) + if opponentRecord.type ~= Opponent.team or not opponentRecord.template or opponentRecord.template:lower() == 'tbd' then + return + end + + local opponentArgs = { + team = opponentRecord.template, + date = resolveDate + } + + Array.forEach(opponentRecord.match2players, function(playerRecord, playerIndex) + local prefix = 'p' .. playerIndex + opponentArgs[prefix] = playerRecord.displayname + opponentArgs[prefix .. 'link'] = playerRecord.name + opponentArgs[prefix .. 'flag'] = playerRecord.flag + opponentArgs[prefix .. 'faction'] = (playerRecord.extradata or {}).faction + end) + + return TeamCard(opponentArgs) +end + +return TeamListWrapper diff --git a/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua b/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua new file mode 100644 index 00000000000..f18e7a578c2 --- /dev/null +++ b/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua @@ -0,0 +1,306 @@ +--- +-- @Liquipedia +-- page=Module:TeamList/Starcraft/TeamCard +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local Class = Lua.import('Module:Class') +local Faction = Lua.import('Module:Faction') +local Flags = Lua.import('Module:Flags') +local FnUtil = Lua.import('Module:FnUtil') +local Json = Lua.import('Module:Json') +local Logic = Lua.import('Module:Logic') +local Lpdb = Lua.import('Module:Lpdb') +local Namespace = Lua.import('Module:Namespace') +local String = Lua.import('Module:StringUtils') +local Table = Lua.import('Module:Table') +local TeamTemplate = Lua.import('Module:TeamTemplate') +local Variables = Lua.import('Module:Variables') + +local PlayerExt = Lua.import('Module:Player/Ext') +local PlayerExtCustom = Lua.import('Module:Player/Ext/Custom') +local TournamentStructure = Lua.import('Module:TournamentStructure') + +local Opponent = Lua.import('Module:Opponent/Custom') + +-- can't use the DateExt function +-- due to the wiki vars not existing if using subst bot run +local getContextualDateOrNow = function() + local date = Variables.varDefault('tournament_enddate') + or Variables.varDefault('tournament_startdate') + + if Logic.isNotEmpty(date) then return date end + + local pageName = mw.title.getCurrentTitle().prefixedText:gsub(' ', '_') + + local data = mw.ext.LiquipediaDB.lpdb('tournament', { + conditions = '.[[pagename::' .. pageName .. ']]', + query = 'startdate, enddate', + limit = 1, + })[1] or {} + + return Logic.nilIfEmpty(data.enddate) + or Logic.nilIfEmpty(data.startdate) + or os.date('%F') --[[@as string]] +end + +---@class StarcraftTeamCard +---@operator call(table): StarcraftTeamCard +---@field args table +---@field config StarcraftTeamCardConfig +---@field opponent StarcraftTeamCardOpponent +---@field name string +---@field root Html? +local TeamCard = Class.new( + function(self, args) + self.args = args + self.opponent = self:readOpponent() + local opponentName = Opponent.toName(self.opponent) + assert(opponentName, 'Missing Team Template for "' .. (args.team or '') .. '"') + self.name = opponentName:gsub(' ', '_') + end +) + +---@class StarcraftTeamCardOpponent: StarcraftStandardOpponent +---@field players StarcraftTeamCardPlayer[] +---@field note string? +---@field dq boolean +---@field subtitle string? +---@field date string + +---@return StarcraftTeamCardOpponent +function TeamCard:readOpponent() + local args = self.args + local date = args.date or getContextualDateOrNow() + local team = (args.team or 'tbd'):lower():gsub('_', ' ') + local opponent = Opponent.resolve( + Opponent.readOpponentArgs{team, type = Opponent.team}, date + ) --[[@as StarcraftTeamCardOpponent]] + + opponent.dq = Logic.readBool(args.dq) + opponent.date = date + opponent.note = args.note + + opponent.players = Array.extractValues(Table.mapArgumentsByPrefix(args, {'p', 'player'}, function(key, index) + return self:readPlayer(key, index, date) + end)) + + if #opponent.players >= 35 then + mw.ext.TeamLiquidItegration.add_category('TeamCards with 35 players') + elseif #opponent.players >= 25 then + mw.ext.TeamLiquidIntegration.add_category('TeamCards with 25 players') + elseif #opponent.players >= 20 then + mw.ext.TeamLiquidIntegration.add_category('TeamCards with 20 players') + end + + return opponent +end + +---@class StarcraftTeamCardPlayer: StarcraftStandardPlayer +---@field ace boolean? +---@field captain boolean? +---@field dnp boolean? +---@field dq boolean? +---@field joker boolean? +---@field mainTeam string? +---@field mainTeamPage string? +---@field note boolean? +---@field tag string? +---@field tagTitle string? +---@field two boolean? +---@field withdraw boolean? + +---@param key any +---@param index integer +---@param date string +---@return StarcraftTeamCardPlayer +function TeamCard:readPlayer(key, index, date) + local args = self.args + + local getArg = function(field) + return args['p' .. index .. field] or args[field .. index] + end + + local mainTeamInput = getArg('team') + if mainTeamInput and mainTeamInput:lower() == 'noteam' then + mainTeamInput = nil + end + + local mainTeam, mainTeamPage + if mainTeamInput then + mainTeam = TeamTemplate.resolve(mainTeamInput, date) + assert(mainTeam, 'missing team template "' .. mainTeamInput .. '"') + mainTeamPage = TeamTemplate.getPageName(mainTeam) or nil + end + + return { + displayName = args[key], + flag = String.nilIfEmpty(Flags.CountryName{flag = getArg('flag')}), + pageName = getArg('link'), + faction = Faction.read(getArg('faction') or getArg('race')), + + ace = Logic.readBoolOrNil(getArg('ace')), + captain = Logic.readBoolOrNil(getArg('captain')), + dnp = Logic.readBoolOrNil(getArg('dnp')), + dq = Logic.readBoolOrNil(getArg('dq') or getArg('out')), + joker = Logic.readBoolOrNil(getArg('joker')), + mainTeam = mainTeam, + mainTeamPage = mainTeamPage, + note = getArg('note'), + tag = getArg('tag'), + tagTitle = getArg('tagTitle'), + two = Logic.readBoolOrNil(getArg('two')), + withdraw = Logic.readBoolOrNil(getArg('withdraw')), + } +end + +---@class StarcraftTeamCardConfig +---@field cardWidth string +---@field teamStyle string? +---@field showFlags boolean +---@field display boolean +---@field collapsed boolean +---@field collapsible boolean? +---@field autoDnp boolean +---@field syncPlayers boolean +---@field resolveDate string +---@field sortPlayers boolean +---@field noStorage boolean +---@field isAdhoc boolean? + +---@param parentConfig StarcraftTeamListConfig? +---@return self +function TeamCard:getConfig(parentConfig) + self.config = TeamCard.readConfig(self.args, parentConfig) + + return self +end + +---@param args table +---@param parentConfig StarcraftTeamListConfig? +---@return StarcraftTeamListConfig +function TeamCard.readConfig(args, parentConfig) + parentConfig = parentConfig or {} + + local width = tonumber(args.cardWidth or args.width) + + return { + --display + cardWidth = width and (width .. 'px') or args.cardWidth or args.width or parentConfig.cardWidth or '240px', + teamStyle = Logic.readBool(args.short) and 'short' or parentConfig.teamStyle, + showFlags = Logic.nilOr(Logic.readBoolOrNil(args.showFlags), parentConfig.showFlags, true), + display = not Logic.readBool(args.hidden), + collapsed =Logic.nilOr(Logic.readBoolOrNil(args.collapsed), not Logic.readBoolOrNil(args.uncollapsed)), + collapsible = Logic.nilOr(Logic.readBoolOrNil(args.collapsible), parentConfig.collapsible, true), + --sync + autoDnp = Logic.nilOr(Logic.readBoolOrNil(args.autoDnp), parentConfig.autoDnp, true), + syncPlayers = Logic.nilOr(Logic.readBoolOrNil(args.syncPlayers), parentConfig.syncPlayers, true), + resolveDate = args.date or parentConfig.resolveDate or getContextualDateOrNow(), + sortPlayers = Logic.nilOr(Logic.readBoolOrNil(args.sortPlayers), parentConfig.sortPlayers, true), + --storage + noStorage = Logic.readBool(args.noStorage or parentConfig.noStorage or + Lpdb.isStorageDisabled() or not Namespace.isMain()), + isAdhoc = Logic.nilOr(Logic.readBoolOrNil(args.adhoc), parentConfig.isAdhoc), + } +end + +---@param parentMatchGroupSpec {matchGroupIds: string[], pageNames: string[]}? +---@return self +function TeamCard:sync(parentMatchGroupSpec) + local config = self.config + + local players = self.opponent.players + + if Table.isEmpty(players) then + return self + end + + local date = self.opponent.date + + if config.syncPlayers then + players = Array.map(players, function(player) + player = Table.merge(player, PlayerExtCustom.syncPlayer(player, {date = date})) + player.pageName = player.pageName:gsub(' ', '_') + player.mainTeam = config.isAdhoc and PlayerExt.syncTeam(player.pageName, player.mainTeam, {}) or player.mainTeam + player.mainTeamPage = player.mainTeamPage or + player.mainTeam and TeamTemplate.getPageName(TeamTemplate.resolve(player.mainTeam, date) --[[@as string]]) or + nil + + return player + end) + end + + if config.autoDnp then + local matchGroupSpec = parentMatchGroupSpec or TournamentStructure.currentPageSpec() + players = self:dnp(players, matchGroupSpec) + end + + if config.sortPlayers then + Array.sortInPlaceBy(players, function(player) return player.displayName:lower() end) + end + + self.opponent.players = players + + return self +end + +---@param players StarcraftTeamCardPlayer[] +---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]} +---@return StarcraftTeamCardPlayer[] +function TeamCard:dnp(players, matchGroupSpec) + local dnpData = TeamCard.fetchDnp(matchGroupSpec) + + Array.forEach(players, function(player) + player.dnp = player.dnp or (dnpData[self.name] and not dnpData[self.name][player.pageName]) + end) + + return players +end + +TeamCard.fetchDnp = FnUtil.memoize(function(matchGroupSpec) + return TeamCard.fetchDnpData(matchGroupSpec) +end) + +---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]} +---@return table> +function TeamCard.fetchDnpData(matchGroupSpec) + local matchRecords = mw.ext.LiquipediaDB.lpdb('match2', { + conditions = tostring(TournamentStructure.getMatch2Filter(matchGroupSpec)), + query = 'pagename, match2bracketdata, match2opponents, winner, match2games', + order = 'date asc', + limit = 5000, + }) + + local playersByTeam = {} + Array.forEach(matchRecords, function(matchRecord) + local teams = Array.map(matchRecord.match2opponents, function(opponent, opponentIndex) + playersByTeam[opponent.name] = playersByTeam[opponent.name] or {} + return {name = opponent.name, players = Array.map(opponent.match2players, function(player) return player.name end)} + end) + + Array.forEach(matchRecord.match2games, function(game) + local gameOpponents = game.opponents + if type(gameOpponents) ~= 'table' then + gameOpponents = Json.parseIfTable(gameOpponents) or {} + end + Array.forEach(gameOpponents, function(opp, opponentIndex) + for playerIndex, player in pairs(opp.players or {}) do + if Logic.isNotEmpty(player) then + local matchPlayer = teams[opponentIndex].players[playerIndex] + if player then + playersByTeam[teams[opponentIndex].name][matchPlayer] = true + end + end + end + end) + end) + end) + + return playersByTeam +end + +return TeamCard diff --git a/lua/wikis/starcraft2/InGameRoles.lua b/lua/wikis/starcraft2/InGameRoles.lua new file mode 100644 index 00000000000..cbe3ef6b41d --- /dev/null +++ b/lua/wikis/starcraft2/InGameRoles.lua @@ -0,0 +1,13 @@ +--- +-- @Liquipedia +-- page=Module:InGameRoles +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +---@type table +local inGameRoles = { + ['captain'] = {category = 'Captain', display = 'Captain'}, +} + +return inGameRoles