Skip to content

Commit

Permalink
Create participant table Component (#3346)
Browse files Browse the repository at this point in the history
* low hanging fruits

* Add input reading and storage
- todo: import functionality
- todo: display (incl. filter for notable)

* commons display

* manual faction count input reading (sc2)

* add sc2 display

* Add note and dq support - move opp sorting into readSection

* adjust lpdb instead of only add extradata

* kick unused

* typos

* add importFromMatchGroupSpec support

* shut up linter

* typo and shut up anno warnings

* typo

* another typo

* fix some issues

* disable race icons for players in sc special version

* adjust display

* adjust default width

* remove redundant

* fix count calculations (ignore dq players for them)

* return `self` instead of `ParticipantTable`

* improve starcraft custom

* fix unknown column issues

* Update components/participant_table/commons/participant_table_import.lua

Co-authored-by: Martin B <[email protected]>

* Update components/participant_table/commons/starcraft_starcraft2/participant_table_starcraft.lua

* order params

* Update components/participant_table/commons/starcraft_starcraft2/participant_table_starcraft.lua

* Update participant_table_import.lua

* enable storage on sc(2) without date condition

* Update components/participant_table/commons/participant_table_base.lua

* kick `/dev`

---------

Co-authored-by: Martin B <[email protected]>
  • Loading branch information
hjpalpha and iMarbot authored Oct 13, 2023
1 parent 60e9be0 commit c45d6ee
Show file tree
Hide file tree
Showing 6 changed files with 813 additions and 0 deletions.
398 changes: 398 additions & 0 deletions components/participant_table/commons/participant_table_base.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
---
-- @Liquipedia
-- wiki=commons
-- page=Module:ParticipantTable/Base
--
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

--can not name it `Module:ParticipantTable` due to that already existing on some wikis

local Arguments = require('Module:Arguments')
local Array = require('Module:Array')
local Class = require('Module:Class')
local DateExt = require('Module:Date/Ext')
local Json = require('Module:Json')
local Logic = require('Module:Logic')
local Lua = require('Module:Lua')
local Namespace = require('Module:Namespace')
local PageVariableNamespace = require('Module:PageVariableNamespace')
local String = require('Module:StringUtils')
local Table = require('Module:Table')
local Template = require('Module:Template')
local Variables = require('Module:Variables')

local OpponentLibraries = require('Module:OpponentLibraries')
local Opponent = OpponentLibraries.Opponent
local OpponentDisplay = OpponentLibraries.OpponentDisplay

local Import = Lua.import('Module:ParticipantTable/Import', {requireDevIfEnabled = true})
local PlayerExt = Lua.import('Module:Player/Ext', {requireDevIfEnabled = true})
local TournamentStructure = Lua.import('Module:TournamentStructure', {requireDevIfEnabled = true})

local pageVars = PageVariableNamespace('ParticipantTable')

---@class ParticipantTable
---@operator call(Frame): ParticipantTable
---@field args table
---@field config ParticipantTableConfig
---@field display Html?
---@field sections ParticipantTableSection[]
local ParticipantTable = Class.new(
function(self, frame)
self.args = Arguments.getArgs(frame)
end)

---@param frame Frame
---@return Html?
function ParticipantTable.run(frame)
return ParticipantTable(frame):read():store():create()
end

---@return self
function ParticipantTable:read()
self.config = self.readConfig(self.args)
self:readSections()

return self
end

---@class ParticipantTableConfig
---@field lpdbPrefix string
---@field noStorage boolean
---@field matchGroupSpec table?
---@field syncPlayers boolean
---@field showCountBySection boolean
---@field onlyNotable boolean
---@field count number?
---@field colSpan number
---@field resolveDate string
---@field sortPlayers boolean sort players within an opponent
---@field showTeams boolean
---@field title string?
---@field importOnlyQualified boolean?
---@field display boolean
---@field columnWidth number? width of the column in px

---@param args table
---@param parentConfig ParticipantTableConfig?
---@return ParticipantTableConfig
function ParticipantTable.readConfig(args, parentConfig)
parentConfig = parentConfig or {}
local config = {
lpdbPrefix = args.lpdbPrefix or parentConfig.lpdbPrefix or Variables.varDefault('lpdbPrefix') or '',
noStorage = Logic.readBool(args.noStorage or parentConfig.noStorage or
Variables.varDefault('disable_LPDB_storage') or not Namespace.isMain()),
matchGroupSpec = TournamentStructure.readMatchGroupsSpec(args),
syncPlayers = Logic.nilOr(Logic.readBoolOrNil(args.syncPlayers), parentConfig.syncPlayers, true),
showCountBySection = Logic.readBool(args.showCountBySection or parentConfig.showCountBySection),
count = tonumber(args.count),
colSpan = tonumber(args.colspan) or parentConfig.colSpan or 4,
onlyNotable = Logic.readBool(args.onlyNotable or parentConfig.onlyNotable),
resolveDate = args.date or parentConfig.resolveDate or DateExt.getContextualDate(),
sortPlayers = Logic.readBool(args.sortPlayers or parentConfig.sortPlayers),
showTeams = not Logic.readBool(args.disable_teams),
title = args.title,
importOnlyQualified = Logic.readBool(args.onlyQualified),
display = not Logic.readBool(args.hidden)
}

config.columnWidth = tonumber(args.entrywidth) or config.showTeams and 212 or 156

return config
end

function ParticipantTable:readSections()
self.sections = {}
Array.forEach(self:fetchSectionsArgs(), function(sectionArgs)
self:readSection(sectionArgs)
end)
end

---@return table[]
function ParticipantTable:fetchSectionsArgs()
local args = self.args

pageVars:set('stashArgs', '1')

-- make sure that all sections stashArgs
for _, potentialSection in pairs(args) do
ParticipantTable._stashArgs(potentialSection)
end

-- retrieve sectionsArgs
local sectionsArgs = Template.retrieveReturnValues('ParticipantTable')
pageVars:delete('stashArgs')

--case no sections: use whole table as first section
if Table.isEmpty(sectionsArgs) then
return {args}
end

return sectionsArgs
end

---access the args so it stashes
---@param potentialSection string
---@return string
function ParticipantTable._stashArgs(potentialSection)
return potentialSection
end


---@class ParticipantTableSection
---@field config ParticipantTableConfig
---@field entries ParticipantTableEntry

---@param args table
function ParticipantTable:readSection(args)
local config = self.readConfig(args, self.config)
local section = {config = config}

local keys = Array.filter(Array.extractKeys(args), function(key)
return String.contains(key, '^%d+$') or
String.contains(key, '^p%d+$') or
String.contains(key, '^player%d+$')
end)

local entriesByName = {}

--need custom sort so it doesn't compare number with string
local sortKeys = function(tbl, key1, key2) return tostring(key1) < tostring(key2) end

for _, key in Table.iter.spairs(keys, sortKeys) do
local entry = self:readEntry(args, key, config)
if entriesByName[entry.name] then
error('Duplicate Input "|' .. key .. '=' .. args[key] .. '"')
end

entriesByName[entry.name] = entry
end

section.entries = Array.map(Import.importFromMatchGroupSpec(config, entriesByName), function(entry)
entry.opponent = Opponent.resolve(entry.opponent, config.resolveDate, {syncPlayer = config.syncPlayers})
return entry
end)

Array.sortInPlaceBy(section.entries, function(entry) return entry.name:lower() end)

table.insert(self.sections, section)
end

---@class ParticipantTableEntry
---@field opponent standardOpponent
---@field name string
---@field note string?
---@field dq boolean

---@param sectionArgs table
---@param key string|number
---@param config any
---@return ParticipantTableEntry
function ParticipantTable:readEntry(sectionArgs, key, config)
--if not a json assume it is a solo opponent
local opponentArgs = Json.parseIfTable(sectionArgs[key]) or Logic.isNumeric(key) and {
type = Opponent.solo,
name = sectionArgs[key],
} or {
type = Opponent.solo,
name = sectionArgs[key],
link = sectionArgs[key .. 'link'],
flag = sectionArgs[key .. 'flag'],
team = sectionArgs[key .. 'team'],
}

assert(Opponent.isType(opponentArgs.type) and opponentArgs.type ~= Opponent.team,
'Missing or unsupported opponent type for "' .. sectionArgs[key] .. '"')

local opponent = Opponent.readOpponentArgs(opponentArgs)

if config.sortPlayers and opponent.players then
table.sort(opponent.players, function (player1, player2)
local name1 = (player1.displayName or player1.name):lower()
local name2 = (player2.displayName or player2.name):lower()
return name1 < name2
end)
end

return {
dq = Logic.readBool(opponentArgs.dq or sectionArgs[key .. 'dq']),
note = opponentArgs.note or sectionArgs[key .. 'note'],
opponent = opponent,
name = Opponent.toName(opponent),
}
end

---@return self
function ParticipantTable:store()
if self.config.noStorage then return self end

local lpdbTournamentData = {
tournament = Variables.varDefault('tournament_name'),
parent = Variables.varDefault('tournament_parent'),
series = Variables.varDefault('tournament_series'),
startdate = Variables.varDefault('tournament_startdate'),
mode = Variables.varDefault('tournament_mode'),
type = Variables.varDefault('tournament_type'),
liquipediatier = Variables.varDefault('tournament_liquipediatier'),
liquipediatiertype = Variables.varDefault('tournament_liquipediatiertype'),
publishertier = Variables.varDefault('tournament_publishertier'),
icon = Variables.varDefault('tournament_icon'),
icondark = Variables.varDefault('tournament_icondark'),
game = Variables.varDefault('tournament_game'),
prizepoolindex = tonumber(Variables.varDefault('prizepool_index')) or 0,
}

local placements = self:getPlacements()

Array.forEach(self.sections, function(section) Array.forEach(section.entries, function(entry)
local lpdbData = Opponent.toLpdbStruct(entry.opponent)

if placements[lpdbData.opponentname] or section.config.noStorage or
Opponent.isTbd(entry.opponent) or Opponent.isEmpty(entry.opponent) then return end


lpdbData = Table.merge(lpdbTournamentData, lpdbData, {date = section.config.resolveDate, extradata = {}})

ParticipantTable:adjustLpdbData(lpdbData, entry, section.config)

mw.ext.LiquipediaDB.lpdb_placement(self:objectName(lpdbData), Json.stringifySubTables(lpdbData))
end) end)

return self
end

---Get placements already set on the page from prize pools
---@return table<string, true>
function ParticipantTable:getPlacements()
local placements = {}
Array.forEach(mw.ext.LiquipediaDB.lpdb('placement', {
limit = 5000,
conditions = '[[placement::!]] AND [[pagename::' .. string.gsub(mw.title.getCurrentTitle().text, ' ', '_') .. ']]',
}), function(placement) placements[placement.opponentname] = true end)

return placements
end

---@param lpdbData table
---@return string
function ParticipantTable:objectName(lpdbData)
return 'ranking_' .. self.config.lpdbPrefix .. '_' .. lpdbData.prizepoolindex .. '_' .. lpdbData.opponentname
end

---@param lpdbData table
---@param entry ParticipantTableEntry
---@param config ParticipantTableConfig
function ParticipantTable:adjustLpdbData(lpdbData, entry, config)
end

---@return Html?
function ParticipantTable:create()
local config = self.config

if not config.display then return end

self.display = mw.html.create('div')
:addClass('participantTable')
:css('max-width', '100%!important')
:css('width', (config.colSpan * config.columnWidth + 1) .. 'px')
:node(mw.html.create('div'):addClass('participantTable-title'):wikitext(config.title or 'Participants'))

Array.forEach(self.sections, function(section) self:displaySection(section) end)

return self.display
end

---@return Html
function ParticipantTable.newSectionNode()
return mw.html.create('div'):addClass('participantTable-row')
end

---@param section ParticipantTableSection
function ParticipantTable:displaySection(section)
local entries = section.config.onlyNotable and self.filterOnlyNotables(section.entries) or section.entries

local sectionEntryCount = #Array.filter(entries, function(entry) return not entry.dq end)

self.display:node(self.newSectionNode():node(self.sectionTitle(section, sectionEntryCount)))

if Table.isEmpty(section.entries) then
self.display:node(self.newSectionNode():node(self:tbd()))
return
end

local sectionNode = ParticipantTable.newSectionNode()

Array.forEach(entries, function(entry, entryIndex)
sectionNode:node(self:displayEntry(entry):css('width', self.config.columnWidth .. 'px'))
end)

local nextColumn = (#entries % self.config.colSpan) + 1

Array.forEach(Array.range(nextColumn, self.config.colSpan),
function() sectionNode:node(self:empty()) end)

self.display:node(sectionNode)
end

---@return Html
function ParticipantTable:tbd()
return mw.html.create('div')
:addClass('participantTable-tbd')
:wikitext('To be determined')
end

---@return Html
function ParticipantTable:empty()
return mw.html.create('div'):addClass('participantTable-entry participantTable-empty')
end

---@param section ParticipantTableSection
---@param amountOfEntries number
---@return Html?
function ParticipantTable.sectionTitle(section, amountOfEntries)
if Logic.isEmpty(section.config.title) then return end

local title = mw.html.create('div'):addClass('participantTable-title'):wikitext(section.config.title)

if not section.config.showCountBySection then return title end

return title:tag('i'):wikitext(' (' .. (section.config.count or amountOfEntries) .. ')'):done()
end

---@param entry ParticipantTableEntry
---@param additionalProps table?
---@return Html
function ParticipantTable:displayEntry(entry, additionalProps)
additionalProps = additionalProps or {}

return mw.html.create('div')
:addClass('participantTable-entry brkts-opponent-hover')
:attr('aria-label', entry.name)
:node(OpponentDisplay.BlockOpponent(Table.merge(additionalProps, {
dq = entry.dq,
note = entry.note,
showPlayerTeam = self.config.showTeams,
opponent = entry.opponent,
})))
end

---@param entries ParticipantTableEntry[]
---@return ParticipantTableEntry[]
function ParticipantTable.filterOnlyNotables(entries)
return Array.filter(entries, function(entry) return ParticipantTable.isNotable(entry) end)
end

---@param entry ParticipantTableEntry
---@return boolean
function ParticipantTable.isNotable(entry)
return Array.any(entry.opponent.players or {}, ParticipantTable.isNotablePlayer)
end

---@param player standardPlayer
---@return boolean
function ParticipantTable.isNotablePlayer(player)
return Logic.isNotEmpty(player.pageName) and PlayerExt.fetchPlayerFlag(player.pageName) ~= nil
end

return ParticipantTable
Loading

0 comments on commit c45d6ee

Please sign in to comment.