diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml index 4f39f625f0d..8a883545c85 100644 --- a/.github/workflows/luacheck.yml +++ b/.github/workflows/luacheck.yml @@ -35,6 +35,25 @@ jobs: check_name: 'Lint Report' annotate_only: ${{ (github.event.pull_request.head.repo.full_name != github.repository) }} + lua-import-rules: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup python + uses: actions/setup-python@v7 + with: + python-version: '3.14' + + # Fails on violations coming out of Features/ and Domain/, warns for code that has not + # been restructured yet. Those warnings become errors as each area is migrated. + - name: Check import rules + run: python scripts/check_lua_imports.py + env: + PYTHONUNBUFFERED: 1 + lua-unit-test: runs-on: ubuntu-latest diff --git a/lua/.luacheckrc b/lua/.luacheckrc index 511a6789066..5ad3f65a42d 100644 --- a/lua/.luacheckrc +++ b/lua/.luacheckrc @@ -42,4 +42,4 @@ ignore = { "212" -- unused argument } -files["lua/spec/*_spec.lua"].read_globals = {"GoldenTest", "SetActiveWiki", "allwikis"} +files["lua/spec/**/*_spec.lua"].read_globals = {"GoldenTest", "SetActiveWiki", "allwikis"} diff --git a/lua/spec/match_group_util_bracket_spec.lua b/lua/spec/domain/bracket_spec.lua similarity index 82% rename from lua/spec/match_group_util_bracket_spec.lua rename to lua/spec/domain/bracket_spec.lua index dfafb372a88..5172757e183 100644 --- a/lua/spec/match_group_util_bracket_spec.lua +++ b/lua/spec/domain/bracket_spec.lua @@ -4,22 +4,22 @@ ---@diagnostic disable: missing-fields --[[ -Tests for Module:MatchGroup/Util/Bracket, the bracket model. +Tests for Module:Domain/Bracket/Model, the bracket model. Bracket data records, topology, coordinates and the id namespace that addresses a match inside its match group. A matchlist counts as a bracket here, which is why the flat match id forms live with the bracket ones. ]] -insulate('MatchGroup/Util/Bracket', function() +insulate('Domain/Bracket/Model', function() describe('bracketDataFromRecord', function() it('returns an empty table when there is no data', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({}, BracketUtil.bracketDataFromRecord(nil)) end) it('reads matchlist bracket data', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketData = BracketUtil.bracketDataFromRecord{ type = 'matchlist', header = 'Round 1', @@ -40,12 +40,12 @@ insulate('MatchGroup/Util/Bracket', function() end) it('treats anything that is not a bracket as a matchlist', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_equal('matchlist', BracketUtil.bracketDataFromRecord{header = 'x'}.type) end) it('reads bracket bracket data', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketData = BracketUtil.bracketDataFromRecord{ type = 'bracket', header = 'Semifinals', @@ -70,7 +70,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('derives the qualification flags from the advance spots', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketData = BracketUtil.bracketDataFromRecord{ type = 'bracket', qualwin = 'true', @@ -84,7 +84,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('reads qualskip and skipround as numbers, including the true spelling', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local fromStrings = BracketUtil.bracketDataFromRecord{ type = 'bracket', qualskip = 'true', skipround = 'true', } @@ -99,7 +99,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('converts the coordinate indexes from zero based to one based', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketData = BracketUtil.bracketDataFromRecord{ type = 'bracket', coordinates = {roundIndex = 0, sectionIndex = 1, roundCount = 3, depth = 2}, @@ -115,7 +115,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('bracketDataToRecord', function() it('round trips a bracket bracket data', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local record = BracketUtil.bracketDataToRecord{ type = 'bracket', header = 'Semifinals', @@ -148,7 +148,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('computeLowerMatchIdsFromLegacy', function() it('reads toupper before tolower and skips empty ones', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({'R02-M001', 'R02-M002'}, BracketUtil.computeLowerMatchIdsFromLegacy{toupper = 'R02-M001', tolower = 'R02-M002'}) assert.are_same({'R02-M002'}, @@ -159,7 +159,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('autoAssignLowerEdges', function() it('centres the lower matches when there are fewer of them than opponents', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({{lowerMatchIndex = 1, opponentIndex = 2}}, BracketUtil.autoAssignLowerEdges(1, 3)) assert.are_same({ {lowerMatchIndex = 1, opponentIndex = 1}, @@ -168,7 +168,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('hangs the excess lower matches off the last opponent', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({ {lowerMatchIndex = 1, opponentIndex = 1}, {lowerMatchIndex = 2, opponentIndex = 2}, @@ -177,14 +177,14 @@ insulate('MatchGroup/Util/Bracket', function() end) it('returns nothing when there are no lower matches', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({}, BracketUtil.autoAssignLowerEdges(0, 2)) end) end) describe('computeAdvanceSpots', function() it('advances the winner to the upper match', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same( {{bg = 'up', type = 'advance', matchId = 'R02-M001'}}, BracketUtil.computeAdvanceSpots{upperMatchId = 'R02-M001'} @@ -192,7 +192,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('lets winnerto and loserto override the upper match', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local spots = BracketUtil.computeAdvanceSpots{ upperMatchId = 'R02-M001', winnerto = 'R02-M009', @@ -203,7 +203,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('marks qualification spots while keeping the target match', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local spots = BracketUtil.computeAdvanceSpots{ upperMatchId = 'R02-M001', qualwin = 'true', @@ -214,21 +214,21 @@ insulate('MatchGroup/Util/Bracket', function() end) it('returns nothing for a match that goes nowhere', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({}, BracketUtil.computeAdvanceSpots{}) end) end) describe('populateAdvanceSpots', function() it('does nothing for an empty bracket', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.is_true(pcall(function() BracketUtil.populateAdvanceSpots{matches = {}} end)) end) it('sends the semifinal losers to the third place match', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketDatasById = { ['R01-M001'] = {advanceSpots = {}}, ['R02-M001'] = {advanceSpots = {}, lowerMatchIds = {'R01-M001'}, thirdPlaceMatchId = 'RxMTP'}, @@ -246,7 +246,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('applies the pbg overrides from extradata', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketData = {advanceSpots = {}, lowerMatchIds = {}} BracketUtil.populateAdvanceSpots{ matches = {{extradata = {pbg1 = 'down', pbg2 = 'stay'}, bracketData = bracketData}}, @@ -260,9 +260,28 @@ insulate('MatchGroup/Util/Bracket', function() end) end) + describe('resetMatch', function() + it('finds the match that replays a grand final', function() + local BracketUtil = require('Module:Domain/Bracket/Model') + local reset = {matchId = 'R03-M001'} + assert.are_equal(reset, BracketUtil.resetMatch( + {['R03-M001'] = reset}, {bracketResetMatchId = 'R03-M001'})) + end) + + it('finds nothing for a match that has no reset', function() + local BracketUtil = require('Module:Domain/Bracket/Model') + assert.is_nil(BracketUtil.resetMatch({['R03-M001'] = {}}, {})) + end) + + it('finds nothing when the reset match is not in the bracket', function() + local BracketUtil = require('Module:Domain/Bracket/Model') + assert.is_nil(BracketUtil.resetMatch({}, {bracketResetMatchId = 'R03-M001'})) + end) + end) + describe('computeRootMatchIds', function() it('takes the matches that have no upper match, in coordinate order', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local rootMatchIds = BracketUtil.computeRootMatchIds{ ['R01-M001'] = {upperMatchId = 'R02-M001'}, ['R02-M001'] = {coordinates = {rootIndex = 2}}, @@ -272,7 +291,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('never treats a bracket reset match as a root', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_same({'R01-M001'}, BracketUtil.computeRootMatchIds{ ['R01-M001'] = {coordinates = {rootIndex = 1}}, ['abcdefghij_RxMBR'] = {coordinates = {rootIndex = 2}}, @@ -282,7 +301,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('backfillUpperMatchIds', function() it('points every lower match at the match above it', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketDatasById = { ['R01-M001'] = {lowerMatchIds = {}}, ['R01-M002'] = {lowerMatchIds = {}}, @@ -298,7 +317,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('index table conversion', function() it('shifts only the keys ending in Index', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local coordinates = {roundIndex = 0, sectionIndex = 2, roundCount = 4, matchId = 'R01-M001'} local oneBased = BracketUtil.indexTableFromRecord(coordinates) @@ -309,7 +328,7 @@ insulate('MatchGroup/Util/Bracket', function() describe('sectionIndexToString', function() it('names the first, last and middle sections', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_equal('upper', BracketUtil.sectionIndexToString(1, 3)) assert.are_equal('mid', BracketUtil.sectionIndexToString(2, 3)) assert.are_equal('lower', BracketUtil.sectionIndexToString(3, 3)) @@ -321,20 +340,20 @@ insulate('MatchGroup/Util/Bracket', function() -- match ids address a match inside its match group, so they live with the bracket half describe('match ids', function() it('splits a match id into bracket id and base match id', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') local bracketId, baseMatchId = BracketUtil.splitMatchId('h5HXaqbSVP_R02-M002') assert.are_equal('h5HXaqbSVP', bracketId) assert.are_equal('R02-M002', baseMatchId) end) it('returns nothing for a match id it cannot split', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.is_nil(BracketUtil.splitMatchId('R02-M002')) assert.is_nil((BracketUtil.splitMatchId(''))) end) it('converts between the padded and short match id forms', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_equal('R1M3', BracketUtil.matchIdToKey('R01-M003')) assert.are_equal('R12M345', BracketUtil.matchIdToKey('R12-M345')) assert.are_equal('R01-M003', BracketUtil.matchIdFromKey('R1M3')) @@ -342,7 +361,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('leaves the reset and third place placeholders alone', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_equal('RxMBR', BracketUtil.matchIdToKey('RxMBR')) assert.are_equal('RxMTP', BracketUtil.matchIdToKey('RxMTP')) assert.are_equal('RxMBR', BracketUtil.matchIdFromKey('RxMBR')) @@ -350,7 +369,7 @@ insulate('MatchGroup/Util/Bracket', function() end) it('pads a matchlist key to four digits', function() - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local BracketUtil = require('Module:Domain/Bracket/Model') assert.are_equal('0003', BracketUtil.matchIdFromKey('3')) end) diff --git a/lua/spec/match_group_util_match_spec.lua b/lua/spec/domain/match_spec.lua similarity index 81% rename from lua/spec/match_group_util_match_spec.lua rename to lua/spec/domain/match_spec.lua index 6f7c45874ad..e677379bdf9 100644 --- a/lua/spec/match_group_util_match_spec.lua +++ b/lua/spec/domain/match_spec.lua @@ -4,7 +4,7 @@ ---@diagnostic disable: missing-fields --[[ -Tests for Module:MatchGroup/Util/Match, the match model: reading match records into matches, +Tests for Module:Domain/Match/Model, the match model: reading match records into matches, opponents, games and players. ]] @@ -22,10 +22,10 @@ local function gameRecord(record) return record end -insulate('MatchGroup/Util/Match', function() +insulate('Domain/Match/Model', function() describe('matchFromRecord', function() it('reads a full bracket match record', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ match2id = 'abcdefghij_R01-M001', bestof = '3', @@ -60,7 +60,7 @@ insulate('MatchGroup/Util/Match', function() end) it('lifts comment, timestamp and timezone out of extradata', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -77,7 +77,7 @@ insulate('MatchGroup/Util/Match', function() end) it('fills in the defaults of a minimal record', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -98,7 +98,7 @@ insulate('MatchGroup/Util/Match', function() end) it('lower cases the walkover and blanks out empty strings', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -117,7 +117,7 @@ insulate('MatchGroup/Util/Match', function() end) it('parses json encoded links, stream and bracket data', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -134,7 +134,7 @@ insulate('MatchGroup/Util/Match', function() end) it('auto assigns lower edges for bracket matches that have none', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -159,7 +159,7 @@ insulate('MatchGroup/Util/Match', function() ---@param matchOverrides table? ---@return standardOpponent local function readOpponent(opponent, matchOverrides) - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local record = matchRecord(require('Module:Table').merge( {match2opponents = {opponent}, match2games = {}}, matchOverrides or {} @@ -212,7 +212,7 @@ insulate('MatchGroup/Util/Match', function() describe('createOpponent', function() it('fills in the defaults of a bare opponent', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_same({ extradata = {}, players = {}, @@ -221,7 +221,7 @@ insulate('MatchGroup/Util/Match', function() end) it('passes the given fields through', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local opponent = MatchUtil.createOpponent{name = 'A', score = 2, type = 'team', template = 'a'} assert.are_equal('A', opponent.name) assert.are_equal(2, opponent.score) @@ -232,7 +232,7 @@ insulate('MatchGroup/Util/Match', function() describe('playerFromRecord', function() it('reads a player and pulls the team out of extradata', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local player = MatchUtil.playerFromRecord{ name = 'Player_A', displayname = 'pA', @@ -248,14 +248,14 @@ insulate('MatchGroup/Util/Match', function() end) it('blanks out an empty flag', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.is_nil(MatchUtil.playerFromRecord{name = 'A', flag = ''}.flag) end) end) describe('gameFromRecord', function() it('reads a game and lifts its display fields out of extradata', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local game = MatchUtil.gameFromRecord(gameRecord{ map = 'Map1', winner = '1', @@ -290,14 +290,14 @@ insulate('MatchGroup/Util/Match', function() end) it('defaults the scores to an empty list', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_same({}, MatchUtil.gameFromRecord(gameRecord{map = 'Map1'}, 2).scores) end) end) describe('record extradata', function() it('parses json encoded extradata', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', @@ -310,7 +310,7 @@ insulate('MatchGroup/Util/Match', function() it('does not mutate the extradata of the record it was given', function() -- the model lifts comment/timestamp/timezoneid out of extradata, so it has to work on a copy - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local extradata = {comment = 'a comment', timestamp = 1641384000, keepme = 'yes'} local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', @@ -326,7 +326,7 @@ insulate('MatchGroup/Util/Match', function() end) it('defaults to an empty table when the record has none', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local match = MatchUtil.matchFromRecord(matchRecord{ date = '2022-01-05', mode = 'team', match2opponents = {}, match2games = {}, }) @@ -336,7 +336,7 @@ insulate('MatchGroup/Util/Match', function() describe('groupBySubgroup', function() it('groups consecutive games with the same subgroup and takes their headers', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local submatches = MatchUtil.groupBySubgroup{ extradata = {subgroup1header = 'First', subgroup2header = 'Second'}, games = { @@ -355,7 +355,7 @@ insulate('MatchGroup/Util/Match', function() end) it('numbers the submatches by position, not by the subgroup value', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local submatches = MatchUtil.groupBySubgroup{ extradata = {}, games = {{map = 'M1', subgroup = 5}, {map = 'M2', subgroup = 9}}, @@ -366,7 +366,7 @@ insulate('MatchGroup/Util/Match', function() end) it('starts a new submatch every time the subgroup changes back', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') local submatches = MatchUtil.groupBySubgroup{ extradata = {}, games = {{subgroup = 1}, {subgroup = 2}, {subgroup = 1}}, @@ -375,32 +375,32 @@ insulate('MatchGroup/Util/Match', function() end) it('returns nothing for a match without games', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_same({}, MatchUtil.groupBySubgroup{extradata = {}, games = {}}) end) end) describe('computeMatchPhase', function() it('is finished once there is a winner or the finished flag is set', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('finished', MatchUtil.computeMatchPhase{winner = 1, date = '2099-01-01'}) assert.are_equal('finished', MatchUtil.computeMatchPhase{finished = true, date = '2099-01-01'}) end) it('is ongoing once an exact start time has passed', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('ongoing', MatchUtil.computeMatchPhase{date = '2020-01-01 12:00:00', dateIsExact = true}) end) it('is upcoming before the start time', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('upcoming', MatchUtil.computeMatchPhase{date = '2099-01-01 12:00:00', dateIsExact = true}) end) it('is upcoming when the date is explicitly not exact', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('upcoming', MatchUtil.computeMatchPhase{date = '2020-01-01 12:00:00', dateexact = '0'}) end) @@ -408,18 +408,18 @@ insulate('MatchGroup/Util/Match', function() it('ignores a dateIsExact of false, because it is read with an or', function() -- `match.dateIsExact or match.dateexact` turns a false into a nil, which is not the same as -- an explicit false, so a past match with dateIsExact = false still counts as ongoing - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('ongoing', MatchUtil.computeMatchPhase{date = '2020-01-01 12:00:00', dateIsExact = false}) end) it('is upcoming when there is no usable date', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('upcoming', MatchUtil.computeMatchPhase{}) end) it('accepts a raw record as well as a model, via dateexact and timestamp', function() - local MatchUtil = require('Module:MatchGroup/Util/Match') + local MatchUtil = require('Module:Domain/Match/Model') assert.are_equal('ongoing', MatchUtil.computeMatchPhase{dateexact = '1', timestamp = 1577880000}) end) @@ -427,18 +427,15 @@ insulate('MatchGroup/Util/Match', function() end) --[[ -Reading map scores onto the opponent instead of the match score is per wiki configuration, -so it needs a wiki that turns it on. +Reading map scores onto the opponent instead of the match score is a per wiki rule, which the +model takes as a parameter. No wiki has to be stood up to test it. ]] -insulate('MatchGroup/Util/Match on a wiki with game scores in a best of one', function() - setup(function() SetActiveWiki('counterstrike') end) - teardown(function() SetActiveWiki() end) - +insulate('Domain/Match/Model with game scores in a best of one', function() ---@param record table ---@return standardOpponent local function readFirstOpponent(record) - local MatchUtil = require('Module:MatchGroup/Util/Match') - return MatchUtil.opponentFromRecord(record, record.match2opponents[1], 1) + local MatchUtil = require('Module:Domain/Match/Model') + return MatchUtil.opponentFromRecord(record, record.match2opponents[1], 1, {gameScoresIfBo1 = true}) end it('shows the map score of a best of one', function() @@ -481,3 +478,41 @@ insulate('MatchGroup/Util/Match on a wiki with game scores in a best of one', fu assert.is_nil(opponent.scoreDisplay) end) end) + +--[[ +The default rules come from Info.config, which is the path production takes, so this one does need +a wiki that turns the rule on. +]] +insulate('Domain/Match/Model reading the per wiki defaults', function() + setup(function() SetActiveWiki('counterstrike') end) + teardown(function() SetActiveWiki() end) + + ---@param options table? + ---@return standardOpponent + local function readFirstOpponent(options) + local MatchUtil = require('Module:Domain/Match/Model') + local record = matchRecord{ + bestof = '1', + match2opponents = { + {name = 'A', score = '1', status = 'S', match2players = {}}, + {name = 'B', score = '0', status = 'S', match2players = {}}, + }, + match2games = {{opponents = {{score = '16', status = 'S'}, {score = '14', status = 'S'}}}}, + } + return MatchUtil.opponentFromRecord(record, record.match2opponents[1], 1, options) + end + + it('applies the configured rule when the caller passes no options', function() + assert.are_equal(16, readFirstOpponent().scoreDisplay) + end) + + it('keeps the configured rule when the caller overrides nothing', function() + -- the options are merged under the defaults rather than replacing them, so a caller + -- overriding one rule does not silently lose the others + assert.are_equal(16, readFirstOpponent({}).scoreDisplay) + end) + + it('lets the caller turn the rule off', function() + assert.is_nil(readFirstOpponent({gameScoresIfBo1 = false}).scoreDisplay) + end) +end) diff --git a/lua/spec/domain/team_history_spec.lua b/lua/spec/domain/team_history_spec.lua new file mode 100644 index 00000000000..fd33d45beab --- /dev/null +++ b/lua/spec/domain/team_history_spec.lua @@ -0,0 +1,228 @@ +--- Triple Comment to Enable our LLS Plugin +--[[ +Tests for Module:Domain/TeamHistory/Model: reading transfer records into one team's history of a person. +]] + +local TeamHistory = require('Module:Domain/TeamHistory/Model') +local Table = require('Module:Table') + +local TransferType = TeamHistory.TransferType + +local TEAM = 'mouz 2021' +local OTHER_TEAM = 'team liquid 2024' +local TEAMS = {'mousesports orig', TEAM} + +---Builds a transfer record. `extradata` is merged rather than replaced. +---@param props table? +---@return table +local function transfer(props) + props = Table.copy(props or {}) + local extradata = Table.extract(props, 'extradata') or {} + local record = Table.merge({ + player = 'Alice', + nationality = 'se', + date = '2022-01-01', + fromteamtemplate = '', + toteamtemplate = '', + role1 = '', + role2 = '', + wholeteam = 0, + reference = {}, + }, props) + record.extradata = Table.merge({displayname = record.player}, extradata) + return record +end + +---Reads a transfer that is expected to be relevant to the team. +---@param record table +---@return TeamHistoryEntry +local function fromTransfer(record) + local result = TeamHistory.fromTransfer(record, TEAMS) + assert(result, 'expected the transfer to be relevant to the team') + return result +end + +describe('Team history', function() + + describe('fromTransfer', function() + it('reads a transfer into the team as a join', function() + local result = fromTransfer(transfer{ + toteamtemplate = TEAM, + role2 = 'Standin', + nationality = 'se', + extradata = {displayname = 'AliceTag', displaydate = '2022-01-??', position = 'Mid'}, + }) + + assert.are_equal(TransferType.JOIN, result.type) + assert.are_equal('Alice', result.pagename) + assert.are_equal('AliceTag', result.displayname) + assert.are_equal('se', result.flag) + assert.are_equal('2022-01-01', result.date) + assert.are_equal('2022-01-??', result.dateDisplay) + assert.are_equal('Mid', result.position) + assert.are_equal(TEAM, result.toTeam) + assert.are_equal('Standin', result.toRole) + assert.is_nil(result.fromTeam) + assert.is_nil(result.fromRole) + assert.is_false(result.wholeTeam) + end) + + it('reads a transfer away from the team as a leave', function() + local result = fromTransfer(transfer{ + fromteamtemplate = TEAM, + role1 = 'Standin', + toteamtemplate = OTHER_TEAM, + role2 = 'Coach', + }) + + assert.are_equal(TransferType.LEAVE, result.type) + assert.are_equal(TEAM, result.fromTeam) + assert.are_equal('Standin', result.fromRole) + -- the team the person went to is carried along so it can be shown as the next team + assert.are_equal(OTHER_TEAM, result.toTeam) + assert.are_equal('Coach', result.toRole) + end) + + it('reads a transfer within the team as a change', function() + local result = fromTransfer(transfer{ + fromteamtemplate = TEAM, + role1 = 'Standin', + toteamtemplate = TEAM, + role2 = 'Captain', + }) + + assert.are_equal(TransferType.CHANGE, result.type) + assert.are_equal('Standin', result.fromRole) + assert.are_equal('Captain', result.toRole) + end) + + it('matches any of the historical templates of the team', function() + local result = fromTransfer(transfer{toteamtemplate = 'mousesports orig'}) + assert.are_equal(TransferType.JOIN, result.type) + assert.are_equal('mousesports orig', result.toTeam) + end) + + it('follows the team through the secondary team fields', function() + local result = fromTransfer(transfer{ + toteamtemplate = OTHER_TEAM, + role2 = 'Coach', + extradata = {toteamsectemplate = TEAM, role2sec = 'Standin'}, + }) + + assert.are_equal(TransferType.JOIN, result.type) + assert.are_equal(TEAM, result.toTeam) + -- the role on the secondary team is the relevant one + assert.are_equal('Standin', result.toRole) + end) + + it('prefers the main team over the secondary one', function() + local result = fromTransfer(transfer{ + toteamtemplate = TEAM, + role2 = 'Captain', + extradata = {toteamsectemplate = 'mousesports orig', role2sec = 'Standin'}, + }) + assert.are_equal('Captain', result.toRole) + end) + + it('carries the next team from the secondary field when the person left it', function() + local result = fromTransfer(transfer{ + fromteamtemplate = OTHER_TEAM, + toteamtemplate = 'team liquid 2023', + role2 = 'Coach', + extradata = { + fromteamsectemplate = TEAM, + toteamsectemplate = 'team liquid 2023', + role1sec = 'Standin', + role2sec = 'Captain', + }, + }) + + assert.are_equal(TransferType.LEAVE, result.type) + assert.are_equal(TEAM, result.fromTeam) + assert.are_equal('Standin', result.fromRole) + -- the next team is taken from the secondary side too, not from the main one + assert.are_equal('team liquid 2023', result.toTeam) + assert.are_equal('Captain', result.toRole) + end) + + it('carries nothing along when the person left the team for nowhere', function() + local result = fromTransfer(transfer{ + fromteamtemplate = OTHER_TEAM, + toteamtemplate = '', + extradata = {fromteamsectemplate = TEAM, toteamsectemplate = '', role1sec = 'Standin'}, + }) + + assert.are_equal(TransferType.LEAVE, result.type) + assert.is_nil(result.toTeam) + assert.is_nil(result.toRole) + end) + + it('skips a transfer that changes nothing for the team', function() + -- the person moved between two other teams while keeping the same role here + assert.is_nil(TeamHistory.fromTransfer(transfer{ + fromteamtemplate = OTHER_TEAM, + toteamtemplate = 'team liquid 2023', + extradata = { + fromteamsectemplate = TEAM, + toteamsectemplate = TEAM, + role1sec = 'Standin', + role2sec = 'Standin', + }, + }, TEAMS)) + end) + + it('keeps a transfer that only changes the role within the team', function() + local result = fromTransfer(transfer{ + fromteamtemplate = TEAM, + role1 = 'Standin', + toteamtemplate = TEAM, + role2 = 'Captain', + }) + assert.is_truthy(result) + end) + + it('skips a transfer that does not involve the team at all', function() + assert.is_nil(TeamHistory.fromTransfer(transfer{ + fromteamtemplate = OTHER_TEAM, + toteamtemplate = OTHER_TEAM, + }, TEAMS)) + end) + + it('reads the whole team flag', function() + assert.is_true(fromTransfer(transfer{toteamtemplate = TEAM, wholeteam = 1}).wholeTeam) + end) + + it('copes with a record that has no extradata', function() + local record = transfer{toteamtemplate = TEAM} + record.extradata = nil + assert.are_equal(TransferType.JOIN, fromTransfer(record).type) + end) + end) + + describe('fromTransfers', function() + it('groups the entries per person', function() + local history = TeamHistory.fromTransfers({ + transfer{player = 'Alice', toteamtemplate = TEAM, date = '2021-01-01'}, + transfer{player = 'Bob', toteamtemplate = TEAM, date = '2021-02-01'}, + transfer{player = 'Alice', fromteamtemplate = TEAM, date = '2022-01-01'}, + }, TEAMS) + + assert.are_equal(2, #history.Alice) + assert.are_equal(1, #history.Bob) + assert.are_equal('2021-01-01', history.Alice[1].date) + assert.are_equal('2022-01-01', history.Alice[2].date) + end) + + it('leaves out a person whose every transfer was skipped', function() + local history = TeamHistory.fromTransfers({ + transfer{player = 'Alice', fromteamtemplate = OTHER_TEAM, toteamtemplate = OTHER_TEAM}, + }, TEAMS) + + assert.are_same({}, history) + end) + + it('returns an empty table when there are no transfers', function() + assert.are_same({}, TeamHistory.fromTransfers({}, TEAMS)) + end) + end) +end) diff --git a/lua/spec/match_group_util_spec.lua b/lua/spec/match_group_util_spec.lua index 66909ea2625..9b8b5ef568a 100644 --- a/lua/spec/match_group_util_spec.lua +++ b/lua/spec/match_group_util_spec.lua @@ -170,7 +170,7 @@ insulate('MatchGroup/Util', function() record('resetbracket_RxMBR', {type = 'bracket'}), }) - local finals, resetMatch = MatchGroupUtil.fetchMatchForBracketDisplay( + local finals, resetMatch = MatchGroupUtil.fetchMatchWithBracketReset( 'resetbracket', 'resetbracket_R01-M001') assert.is_truthy(finals) @@ -194,8 +194,8 @@ insulate('MatchGroup/Util', function() describe('re-exports', function() it('points every match and bracket model member at its implementation', function() local MatchGroupUtil = require('Module:MatchGroup/Util') - local MatchUtil = require('Module:MatchGroup/Util/Match') - local BracketUtil = require('Module:MatchGroup/Util/Bracket') + local MatchUtil = require('Module:Domain/Match/Model') + local BracketUtil = require('Module:Domain/Bracket/Model') local Types = require('Module:MatchGroup/Util/Types') assert.are_equal(Types, MatchGroupUtil.types) diff --git a/lua/spec/squad_auto_spec.lua b/lua/spec/squad_auto_spec.lua index 31aeec4f081..9e8d2da16b5 100644 --- a/lua/spec/squad_auto_spec.lua +++ b/lua/spec/squad_auto_spec.lua @@ -74,7 +74,7 @@ end ---@field categories string[] ---Runs the auto squad table and collects everything it stored, queried and rendered. ----@param args table arguments for Module:Squad/Auto +---@param args table arguments for Module:Features/Squad/Auto ---@param fixtures {transfers: table[]?, person: table?, nextTeam: table?}? ---@return SquadAutoTestResult local function runAuto(args, fixtures) diff --git a/lua/spec/squad_history_spec.lua b/lua/spec/squad_history_spec.lua index 975ec6bbe7e..c6d06b39c08 100644 --- a/lua/spec/squad_history_spec.lua +++ b/lua/spec/squad_history_spec.lua @@ -11,13 +11,13 @@ The end to end behaviour of the same logic stays covered by squad_auto_spec.lua. local SquadHistory = require('Module:Features/Squad/Lib/History') local SquadTypes = require('Module:Features/Squad/Types') +local TeamHistory = require('Module:Domain/TeamHistory/Model') local Table = require('Module:Table') -local TransferType = SquadTypes.TransferType +local TransferType = TeamHistory.TransferType local SquadStatus = SquadTypes.SquadStatus local TEAM = 'mouz 2021' -local OTHER_TEAM = 'team liquid 2024' local TEAMS = {'mousesports orig', TEAM} ---Builds a transfer record. `extradata` is merged rather than replaced. @@ -41,15 +41,6 @@ local function transfer(props) return record end ----Reads a transfer that is expected to be relevant to the team. ----@param record table ----@return TeamHistoryEntry -local function fromTransfer(record) - local result = SquadHistory.fromTransfer(record, TEAMS) - assert(result, 'expected the transfer to be relevant to the team') - return result -end - ---@param props table? ---@return TeamHistoryEntry local function entry(props) @@ -103,7 +94,7 @@ end ---@param ... table ---@return TeamHistoryEntry[] local function historyOf(...) - return SquadHistory.fromTransfers({...}, TEAMS).Alice or {} + return TeamHistory.fromTransfers({...}, TEAMS).Alice or {} end ---The selection a squad table of this status makes, where a row is expected. @@ -133,187 +124,7 @@ local function dateOf(historyEntry) end describe('Squad history', function() - describe('fromTransfer', function() - it('reads a transfer into the team as a join', function() - local result = fromTransfer(transfer{ - toteamtemplate = TEAM, - role2 = 'Standin', - nationality = 'se', - extradata = {displayname = 'AliceTag', displaydate = '2022-01-??', position = 'Mid'}, - }) - - assert.are_equal(TransferType.JOIN, result.type) - assert.are_equal('Alice', result.pagename) - assert.are_equal('AliceTag', result.displayname) - assert.are_equal('se', result.flag) - assert.are_equal('2022-01-01', result.date) - assert.are_equal('2022-01-??', result.dateDisplay) - assert.are_equal('Mid', result.position) - assert.are_equal(TEAM, result.toTeam) - assert.are_equal('Standin', result.toRole) - assert.is_nil(result.fromTeam) - assert.is_nil(result.fromRole) - assert.is_false(result.wholeTeam) - end) - - it('reads a transfer away from the team as a leave', function() - local result = fromTransfer(transfer{ - fromteamtemplate = TEAM, - role1 = 'Standin', - toteamtemplate = OTHER_TEAM, - role2 = 'Coach', - }) - - assert.are_equal(TransferType.LEAVE, result.type) - assert.are_equal(TEAM, result.fromTeam) - assert.are_equal('Standin', result.fromRole) - -- the team the person went to is carried along so it can be shown as the next team - assert.are_equal(OTHER_TEAM, result.toTeam) - assert.are_equal('Coach', result.toRole) - end) - - it('reads a transfer within the team as a change', function() - local result = fromTransfer(transfer{ - fromteamtemplate = TEAM, - role1 = 'Standin', - toteamtemplate = TEAM, - role2 = 'Captain', - }) - - assert.are_equal(TransferType.CHANGE, result.type) - assert.are_equal('Standin', result.fromRole) - assert.are_equal('Captain', result.toRole) - end) - - it('matches any of the historical templates of the team', function() - local result = fromTransfer(transfer{toteamtemplate = 'mousesports orig'}) - assert.are_equal(TransferType.JOIN, result.type) - assert.are_equal('mousesports orig', result.toTeam) - end) - - it('follows the team through the secondary team fields', function() - local result = fromTransfer(transfer{ - toteamtemplate = OTHER_TEAM, - role2 = 'Coach', - extradata = {toteamsectemplate = TEAM, role2sec = 'Standin'}, - }) - - assert.are_equal(TransferType.JOIN, result.type) - assert.are_equal(TEAM, result.toTeam) - -- the role on the secondary team is the relevant one - assert.are_equal('Standin', result.toRole) - end) - - it('prefers the main team over the secondary one', function() - local result = fromTransfer(transfer{ - toteamtemplate = TEAM, - role2 = 'Captain', - extradata = {toteamsectemplate = 'mousesports orig', role2sec = 'Standin'}, - }) - assert.are_equal('Captain', result.toRole) - end) - - it('carries the next team from the secondary field when the person left it', function() - local result = fromTransfer(transfer{ - fromteamtemplate = OTHER_TEAM, - toteamtemplate = 'team liquid 2023', - role2 = 'Coach', - extradata = { - fromteamsectemplate = TEAM, - toteamsectemplate = 'team liquid 2023', - role1sec = 'Standin', - role2sec = 'Captain', - }, - }) - - assert.are_equal(TransferType.LEAVE, result.type) - assert.are_equal(TEAM, result.fromTeam) - assert.are_equal('Standin', result.fromRole) - -- the next team is taken from the secondary side too, not from the main one - assert.are_equal('team liquid 2023', result.toTeam) - assert.are_equal('Captain', result.toRole) - end) - - it('carries nothing along when the person left the team for nowhere', function() - local result = fromTransfer(transfer{ - fromteamtemplate = OTHER_TEAM, - toteamtemplate = '', - extradata = {fromteamsectemplate = TEAM, toteamsectemplate = '', role1sec = 'Standin'}, - }) - - assert.are_equal(TransferType.LEAVE, result.type) - assert.is_nil(result.toTeam) - assert.is_nil(result.toRole) - end) - it('skips a transfer that changes nothing for the team', function() - -- the person moved between two other teams while keeping the same role here - assert.is_nil(SquadHistory.fromTransfer(transfer{ - fromteamtemplate = OTHER_TEAM, - toteamtemplate = 'team liquid 2023', - extradata = { - fromteamsectemplate = TEAM, - toteamsectemplate = TEAM, - role1sec = 'Standin', - role2sec = 'Standin', - }, - }, TEAMS)) - end) - - it('keeps a transfer that only changes the role within the team', function() - local result = fromTransfer(transfer{ - fromteamtemplate = TEAM, - role1 = 'Standin', - toteamtemplate = TEAM, - role2 = 'Captain', - }) - assert.is_truthy(result) - end) - - it('skips a transfer that does not involve the team at all', function() - assert.is_nil(SquadHistory.fromTransfer(transfer{ - fromteamtemplate = OTHER_TEAM, - toteamtemplate = OTHER_TEAM, - }, TEAMS)) - end) - - it('reads the whole team flag', function() - assert.is_true(fromTransfer(transfer{toteamtemplate = TEAM, wholeteam = 1}).wholeTeam) - end) - - it('copes with a record that has no extradata', function() - local record = transfer{toteamtemplate = TEAM} - record.extradata = nil - assert.are_equal(TransferType.JOIN, fromTransfer(record).type) - end) - end) - - describe('fromTransfers', function() - it('groups the entries per person', function() - local history = SquadHistory.fromTransfers({ - transfer{player = 'Alice', toteamtemplate = TEAM, date = '2021-01-01'}, - transfer{player = 'Bob', toteamtemplate = TEAM, date = '2021-02-01'}, - transfer{player = 'Alice', fromteamtemplate = TEAM, date = '2022-01-01'}, - }, TEAMS) - - assert.are_equal(2, #history.Alice) - assert.are_equal(1, #history.Bob) - assert.are_equal('2021-01-01', history.Alice[1].date) - assert.are_equal('2022-01-01', history.Alice[2].date) - end) - - it('leaves out a person whose every transfer was skipped', function() - local history = SquadHistory.fromTransfers({ - transfer{player = 'Alice', fromteamtemplate = OTHER_TEAM, toteamtemplate = OTHER_TEAM}, - }, TEAMS) - - assert.are_same({}, history) - end) - - it('returns an empty table when there are no transfers', function() - assert.are_same({}, SquadHistory.fromTransfers({}, TEAMS)) - end) - end) describe('an active squad', function() it('shows someone whose latest transfer put them on the team', function() diff --git a/lua/wikis/ageofempires/MatchSummary/Ffa.lua b/lua/wikis/ageofempires/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/ageofempires/MatchSummary/Ffa.lua +++ b/lua/wikis/ageofempires/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/apexlegends/MatchSummary/Ffa.lua b/lua/wikis/apexlegends/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/apexlegends/MatchSummary/Ffa.lua +++ b/lua/wikis/apexlegends/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/arenafps/MatchSummary/Ffa.lua b/lua/wikis/arenafps/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/arenafps/MatchSummary/Ffa.lua +++ b/lua/wikis/arenafps/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/autochess/MatchSummary/Ffa.lua b/lua/wikis/autochess/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/autochess/MatchSummary/Ffa.lua +++ b/lua/wikis/autochess/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/callofduty/MatchSummary/Ffa.lua b/lua/wikis/callofduty/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/callofduty/MatchSummary/Ffa.lua +++ b/lua/wikis/callofduty/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/commons/Bracket/Template.lua b/lua/wikis/commons/Bracket/Template.lua index 5f3e41dc24f..fe03a925eea 100644 --- a/lua/wikis/commons/Bracket/Template.lua +++ b/lua/wikis/commons/Bracket/Template.lua @@ -14,7 +14,7 @@ local Template = Lua.import('Module:Template') local BracketDisplay = Lua.import('Module:MatchGroup/Display/Bracket') local Match = Lua.import('Module:Match') -local MatchGroupCoordinates = Lua.import('Module:MatchGroup/Coordinates') +local MatchGroupCoordinates = Lua.import('Module:Domain/Bracket/Coordinates') local MatchGroupUtil = Lua.import('Module:MatchGroup/Util/Custom') local Opponent = Lua.import('Module:Opponent') @@ -168,7 +168,7 @@ end BracketTemplate.perfConfig = { locations = { 'Module:Bracket/Template|*', - 'Module:MatchGroup/Coordinates|*', + 'Module:Domain/Bracket/Coordinates|*', 'Module:MatchGroup/Display/Bracket|*', } } diff --git a/lua/wikis/commons/MatchGroup/Coordinates.lua b/lua/wikis/commons/Domain/Bracket/Coordinates.lua similarity index 99% rename from lua/wikis/commons/MatchGroup/Coordinates.lua rename to lua/wikis/commons/Domain/Bracket/Coordinates.lua index aabea647eb8..312da22861e 100644 --- a/lua/wikis/commons/MatchGroup/Coordinates.lua +++ b/lua/wikis/commons/Domain/Bracket/Coordinates.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- page=Module:MatchGroup/Coordinates +-- page=Module:Domain/Bracket/Coordinates -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- diff --git a/lua/wikis/commons/MatchGroup/Util/Bracket.lua b/lua/wikis/commons/Domain/Bracket/Model.lua similarity index 78% rename from lua/wikis/commons/MatchGroup/Util/Bracket.lua rename to lua/wikis/commons/Domain/Bracket/Model.lua index a02028525ea..294a65a124d 100644 --- a/lua/wikis/commons/MatchGroup/Util/Bracket.lua +++ b/lua/wikis/commons/Domain/Bracket/Model.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- page=Module:MatchGroup/Util/Bracket +-- page=Module:Domain/Bracket/Model -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -12,7 +12,7 @@ local Logic = Lua.import('Module:Logic') local String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') -local MatchGroupCoordinates = Lua.import('Module:MatchGroup/Coordinates') +local MatchGroupCoordinates = Lua.import('Module:Domain/Bracket/Coordinates') local nilIfEmpty = String.nilIfEmpty @@ -24,6 +24,80 @@ namespace that addresses a match within its group. A matchlist counts as a bracket here: it is the flat case of the same structure, which is why the matchlist id form lives alongside the bracket ones. ]] +---@class MatchGroupUtilLowerEdge +---@field lowerMatchIndex number +---@field opponentIndex number + +---@alias AdvanceBg 'up'|'stayup'|'stay'|'staydown'|'down' + +---@class MatchGroupUtilAdvanceSpot +---@field bg AdvanceBg +---@field matchId string? +---@field type string? + +---@class MatchGroupUtilBracketBracketData +---@field coordinates MatchGroupUtilMatchCoordinates +---@field advanceSpots MatchGroupUtilAdvanceSpot[] +---@field bracketResetMatchId string? +---@field bracketType string? +---@field header string? +---@field inheritedHeader string? +---@field lowerEdges MatchGroupUtilLowerEdge[]? +---@field lowerMatchIds string[] +---@field qualLose boolean? +---@field qualLoseLiteral string? +---@field qualSkip number? +---@field qualWin boolean? +---@field qualWinLiteral string? +---@field skipRound number? +---@field thirdPlaceMatchId string? +---@field title string? +---@field type 'bracket' +---@field upperMatchId string? +---@field matchId string? +---@field matchPage string? +---@field qualifiedHeader string? + +---@class MatchGroupUtilMatchCoordinates +---@field depth integer +---@field depthCount integer +---@field matchIndexInRound integer +---@field rootIndex integer +---@field roundCount integer +---@field roundIndex integer +---@field sectionCount integer +---@field sectionIndex integer +---@field semanticDepth integer +---@field semanticRoundIndex integer + +---@class MatchGroupUtilMatchlistBracketData +---@field header string? +---@field title string? +---@field dateHeader boolean? +---@field type 'matchlist' +---@field matchIndex string? +---@field matchPage string? + +---@alias MatchGroupUtilBracketData MatchGroupUtilMatchlistBracketData|MatchGroupUtilBracketBracketData + +---@class MatchGroupUtilMatchlist +---@field bracketDatasById table +---@field matches MatchGroupUtilMatch[] +---@field matchesById table +---@field type 'matchlist' + +---@class MatchGroupUtilBracket +---@field bracketDatasById table +---@field coordinatesByMatchId table +---@field matches MatchGroupUtilMatch[] +---@field matchesById table +---@field rootMatchIds string[] +---@field rounds string[][] +---@field sections string[][] +---@field type 'bracket' + +---@alias MatchGroupUtilMatchGroup MatchGroupUtilBracket|MatchGroupUtilMatchlist + local BracketUtil = {} ---@param data table? @@ -312,4 +386,12 @@ function BracketUtil.splitMatchId(matchId) return matchId:match('^(.-)_([%w-]+)$') end +---The match that replays this one when the lower bracket team wins the grand final. +---@param matchesById table +---@param bracketData MatchGroupUtilBracketData +---@return MatchGroupUtilMatch? +function BracketUtil.resetMatch(matchesById, bracketData) + return bracketData.bracketResetMatchId and matchesById[bracketData.bracketResetMatchId] or nil +end + return BracketUtil diff --git a/lua/wikis/commons/MatchGroup/Util/Match.lua b/lua/wikis/commons/Domain/Match/Model.lua similarity index 65% rename from lua/wikis/commons/MatchGroup/Util/Match.lua rename to lua/wikis/commons/Domain/Match/Model.lua index 462e70c8f51..01107381915 100644 --- a/lua/wikis/commons/MatchGroup/Util/Match.lua +++ b/lua/wikis/commons/Domain/Match/Model.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- page=Module:MatchGroup/Util/Match +-- page=Module:Domain/Match/Model -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -10,15 +10,14 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Date = Lua.import('Module:Date/Ext') local Faction = Lua.import('Module:Faction') -local FnUtil = Lua.import('Module:FnUtil') local Info = Lua.import('Module:Info', {loadData = true}) local Json = Lua.import('Module:Json') local Logic = Lua.import('Module:Logic') -local Opponent = Lua.import('Module:Opponent/Custom') +local Opponent = Lua.import('Module:Opponent') local String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') -local BracketUtil = Lua.import('Module:MatchGroup/Util/Bracket') +local BracketUtil = Lua.import('Module:Domain/Bracket/Model') local NOW = Date.getCurrentTimestamp() @@ -27,11 +26,147 @@ local nilIfEmpty = String.nilIfEmpty --[[ The match model: reading match records into matches, opponents, games and players, and the match ids that address them. -Reading a match record does mean interpreting the bracket data it carries, -which is why this imports Module:MatchGroup/Util/Bracket. +Reading a match record does mean interpreting the bracket data it carries, which is why this +imports Module:Domain/Bracket/Model. + +Per wiki rules come from Info.config as defaults, so that any feature can read a match correctly +without going through another feature. Every such rule is overridable by a parameter, which is what +keeps this testable without standing a wiki up. ]] +---@class standardPlayer +---@field displayName string? +---@field flag string? +---@field pageName string? +---@field team string? +---@field extradata table? +---@field pageIsResolved boolean? +---@field faction string? +---@field roles string[]? +---@field apiId string? + +---@class standardOpponent +---@field advanceBg string? +---@field advances boolean? +---@field icon string? +---@field icondark string? +---@field name string? +---@field placement number? +---@field placement2 number? +---@field players standardPlayer[]? +---@field score number? +---@field scoreDisplay number? +---@field score2 number? +---@field status string? +---@field status2 string? +---@field template string? +---@field type OpponentType +---@field team string? +---@field extradata table + +---@class GameOpponent +---@field name string? +---@field players standardPlayer[] +---@field template string? +---@field type string + +---@alias MatchStatus 'notplayed'|''|nil + +---@class MatchGroupUtilGame +---@field comment string? +---@field date string? +---@field dateIsExact boolean +---@field game string? +---@field header string? +---@field length string|number? +---@field map string? +---@field mapDisplayName string? +---@field mode string? +---@field opponents {players: table[], score: number?, status: string?}[] +---@field patch string? +---@field resultType string? +---@field scores number[] +---@field subgroup number? +---@field type string? +---@field vod string? +---@field winner integer? +---@field status string? +---@field walkover string? +---@field extradata table? +---@field timestamp number +---@field timezoneId string? + +---@class MatchGroupUtilMatch +---@field bracketData MatchGroupUtilBracketData +---@field comment string? +---@field date string +---@field dateIsExact boolean +---@field finished boolean +---@field game string? +---@field games MatchGroupUtilGame[] +---@field icon string? +---@field iconDark string? +---@field links table +---@field liquipediatier string? # TODO: camelCase +---@field liquipediatiertype string? # TODO: camelCase +---@field matchId string? +---@field mode string? +---@field opponents standardOpponent[] +---@field pageName string? +---@field parent string? +---@field patch string? +---@field phase 'upcoming'|'ongoing'|'finished' +---@field publisherTier string? +---@field resultType string? +---@field section string? +---@field series string? +---@field shortname string? +---@field status MatchStatus +---@field stream table +---@field submatches MatchGroupUtilSubgroup[] +---@field tickername string? +---@field tournament string? +---@field type string? +---@field vod string? +---@field walkover string? +---@field winner number? +---@field extradata table? +---@field timestamp number +---@field timezoneId string? +---@field bestof number? + +---@class MatchGroupUtilSubgroup +---@field games MatchGroupUtilGame[] +---@field subgroup number +---@field header string? + +---@class FFAMatchGroupUtilMatch: MatchGroupUtilMatch +---@field games FFAMatchGroupUtilGame[] + +---@class FFAMatchGroupUtilGame: MatchGroupUtilGame +---@field stream table + +--- The per wiki rules that reading a match record depends on. Passed in rather than read from +--- Info.config, so that the match model stays wiki agnostic and testable. +---@class MatchReadOptions +---@field gameScoresIfBo1 boolean? show the map score rather than the match score in a best of one + +--- The subset of a match or game record that is enough to work out its phase. +---@class PartialMatchGameRecord +---@field date string +---@field dateexact boolean|string|nil # records carry '0'/'1' +---@field timestamp number? +---@field finished boolean? +---@field winner integer? + local MatchUtil = {} +---The per wiki rules a match record is read under. Merged under whatever the caller passes, so +---overriding one rule still leaves the rest at their configured values. +---@return MatchReadOptions +local function defaultOptions() + return {gameScoresIfBo1 = Info.config.match2.gameScoresIfBo1} +end + ---Parse extradata as a JSON string if read from page variables. Otherwise create a copy if fetched from lpdb. ---The returned extradata table can then be mutated without altering the source. ---@param recordExtradata table|string? @@ -50,10 +185,14 @@ end ---This is the implementation used on wikis by default. Wikis may specify a different conversion by setting ---WikiSpecific.matchFromRecord. Refer to the starcraft2 wiki as an example. ---@param record match2 +---@param options MatchReadOptions? overrides the per wiki defaults ---@return MatchGroupUtilMatch -function MatchUtil.matchFromRecord(record) +function MatchUtil.matchFromRecord(record, options) local extradata = parseOrCopyExtradata(record.extradata) - local opponents = Array.map(record.match2opponents, FnUtil.curry(MatchUtil.opponentFromRecord, record)) + options = Table.merge(defaultOptions(), options) + local opponents = Array.map(record.match2opponents, function(opponentRecord, opponentIndex) + return MatchUtil.opponentFromRecord(record, opponentRecord, opponentIndex, options) + end) local games = Array.map(record.match2games, function(game) return MatchUtil.gameFromRecord(game, #opponents) end) local bracketData = BracketUtil.bracketDataFromRecord(Json.parseIfString(record.match2bracketdata)) if bracketData.type == 'bracket' then @@ -110,8 +249,10 @@ end ---@param matchRecord match2 ---@param record match2opponent ---@param opponentIndex integer +---@param options MatchReadOptions? ---@return standardOpponent -function MatchUtil.opponentFromRecord(matchRecord, record, opponentIndex) +function MatchUtil.opponentFromRecord(matchRecord, record, opponentIndex, options) + options = Table.merge(defaultOptions(), options) local extradata = parseOrCopyExtradata(record.extradata) local score = record.score local status = record.status @@ -120,7 +261,7 @@ function MatchUtil.opponentFromRecord(matchRecord, record, opponentIndex) local hasOnlyScores = Array.all(matchRecord.match2opponents, function(opponent) return opponent.status == 'S' end) local scoreDisplay = nil - if bestof == 1 and Info.config.match2.gameScoresIfBo1 and game1 and hasOnlyScores then + if bestof == 1 and options.gameScoresIfBo1 and game1 and hasOnlyScores then local mapOpponent = (game1.opponents or {})[opponentIndex] or {} scoreDisplay = tonumber(mapOpponent.score) status = mapOpponent.status diff --git a/lua/wikis/commons/Domain/Match/Store.lua b/lua/wikis/commons/Domain/Match/Store.lua new file mode 100644 index 00000000000..98448229b26 --- /dev/null +++ b/lua/wikis/commons/Domain/Match/Store.lua @@ -0,0 +1,56 @@ +--- +-- @Liquipedia +-- page=Module:Domain/Match/Store +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local Json = Lua.import('Module:Json') +local Operator = Lua.import('Module:Operator') +local Variables = Lua.import('Module:Variables') + +--[[ +Where match records come from. Every read of the match2 table belongs here, so that features can +ask the match domain for matches instead of writing their own queries. + +Returns raw records; turning them into matches is Module:Domain/Match/Model. +]] +local MatchStore = {} + +---Fetches all match ids of matches that satisfy the supplied condition +---@param props {conditions: string|AbstractConditionNode, limit: string|integer?, order: string?} +---@return string[] +function MatchStore.fetchMatchIds(props) + ---@type string[] + return Array.map(mw.ext.LiquipediaDB.lpdb('match2', { + limit = tonumber(props.limit) or 1000, + query = 'match2id', + conditions = tostring(props.conditions), + order = props.order + }), Operator.property('match2id')) +end + +---Fetches all matches in a matchlist or bracket. Tries to read from page variables before fetching from LPDB. +---Returns a list of records ordered lexicographically by matchId. +---@param bracketId string +---@return table[] +function MatchStore.fetchMatchRecords(bracketId) + local varData = Variables.varDefault('match2bracket_' .. bracketId) + if varData then + return (Json.parse(varData)) + end + + return mw.ext.LiquipediaDB.lpdb( + 'match2', + { + conditions = '([[namespace::0]] or [[namespace::>0]]) AND [[match2bracketid::' .. bracketId .. ']]', + order = 'match2id ASC', + limit = 5000, + } + ) +end + +return MatchStore diff --git a/lua/wikis/commons/Domain/TeamHistory/Model.lua b/lua/wikis/commons/Domain/TeamHistory/Model.lua new file mode 100644 index 00000000000..76b36f5c632 --- /dev/null +++ b/lua/wikis/commons/Domain/TeamHistory/Model.lua @@ -0,0 +1,205 @@ +--- +-- @Liquipedia +-- page=Module:Domain/TeamHistory/Model +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Array = Lua.import('Module:Array') +local FnUtil = Lua.import('Module:FnUtil') +local Logic = Lua.import('Module:Logic') +local Operator = Lua.import('Module:Operator') + +--[[ +Answers "was this person on the team, and when" by reading transfer records from the point of view +of one team. + +Pure: it neither queries nor renders anything, and it knows nothing about how any feature displays +the result. +]] +local TeamHistory = {} + +---@enum TransferType +TeamHistory.TransferType = { + LEAVE = 'LEAVE', + JOIN = 'JOIN', + CHANGE = 'CHANGE', +} + +---@enum TransferSide +TeamHistory.TransferSide = { + FROM = 'from', + TO = 'to', +} + +--- One transfer, seen from the point of view of one team. +---@class (exact) TeamHistoryEntry +---@field pagename string +---@field displayname string +---@field flag string +---@field date string +---@field dateDisplay string? +---@field type TransferType +---@field references table +---@field wholeTeam boolean +---@field position string? +---@field fromTeam string? +---@field fromRole string? +---@field toTeam string? +---@field toRole string? +---@field faction string? + +local TransferType = TeamHistory.TransferType +local Side = TeamHistory.TransferSide + +---Checks whether a given team is one of the teams the history is built for +---@param teams string[] +---@param team string? +---@return boolean +local function isCurrentTeam(teams, team) + if not team then + return false + end + return Array.any(teams, FnUtil.curry(Operator.eq, team)) +end + +---Finds the team on one side of a transfer, if it is one of the teams the history is built for +---@param side TransferSide +---@param transfer transfer +---@param teams string[] +---@return string? team +---@return boolean isMain whether the team was found in the main rather than the secondary field +local function parseRelevantTeam(side, transfer, teams) + local mainTeam = transfer[side .. 'teamtemplate'] + if mainTeam and isCurrentTeam(teams, mainTeam) then + return mainTeam, true + end + + local secondaryTeam = transfer.extradata[side .. 'teamsectemplate'] + if secondaryTeam and isCurrentTeam(teams, secondaryTeam) then + return secondaryTeam, false + end + + return nil, false +end + +---Maps a transfer to a transfertype, with regards to the current team. +---@param relevantFromTeam string? +---@param relevantToTeam string? +---@return TransferType +local function getTransferType(relevantFromTeam, relevantToTeam) + if relevantFromTeam then + if relevantToTeam then + return TransferType.CHANGE + end + return TransferType.LEAVE + end + return TransferType.JOIN +end + +---Parses the relevant role for the current team from a transfer +---@param side TransferSide +---@param transfer transfer +---@param team string? +---@param isMain boolean +---@return string? +local function parseRelevantRole(side, transfer, team, isMain) + if not team then + return nil + end + + if isMain then + return side == Side.FROM and transfer.role1 or transfer.role2 + else + return side == Side.FROM and transfer.extradata.role1sec or transfer.extradata.role2sec + end +end + +---Reads a single transfer record from the point of view of a team. +---Returns nothing when the transfer does not change anything for that team. +---@param record transfer +---@param teams string[] every team template that counts as the team +---@return TeamHistoryEntry? +function TeamHistory.fromTransfer(record, teams) + record.extradata = record.extradata or {} + + local relevantFromTeam, isFromMain = parseRelevantTeam(Side.FROM, record, teams) + local relevantToTeam, isToMain = parseRelevantTeam(Side.TO, record, teams) + local transferType = getTransferType(relevantFromTeam, relevantToTeam) + + local fromRole = parseRelevantRole(Side.FROM, record, relevantFromTeam, isFromMain) + local toRole = parseRelevantRole(Side.TO, record, relevantToTeam, isToMain) + + -- For leave transfers: Pass on new team for display as next team + if transferType == TransferType.LEAVE and Logic.isEmpty(relevantToTeam) then + if isFromMain then + relevantToTeam = Logic.nilIfEmpty(record.toteamtemplate) + toRole = Logic.nilIfEmpty(record.role2) + else + relevantToTeam = Logic.nilIfEmpty(record.extradata.toteamsectemplate) + toRole = Logic.nilIfEmpty(record.extradata.role2sec) + end + end + + ---@type TeamHistoryEntry + local entry = { + type = transferType, + + -- Person related information + pagename = record.player, + displayname = record.extradata.displayname, + flag = record.nationality, + + -- Date and references + date = record.date, + dateDisplay = record.extradata.displaydate, + references = record.reference, + + -- Roles + fromRole = fromRole, + toRole = toRole, + + fromTeam = relevantFromTeam, + toTeam = relevantToTeam, + + -- Other + wholeTeam = Logic.readBool(record.wholeteam), + position = record.extradata.position, + faction = record.extradata.faction + } + + -- Skip this transfer if there is no relevant change, i.e. the role in this team didn't change + -- E.g. this is grabbed by secondary team, but only main team changed + if relevantFromTeam == relevantToTeam + and entry.fromRole == entry.toRole then + return nil + end + + return entry +end + +---Groups transfer records into a team history per person. +---A person whose every transfer is irrelevant to the team never enters the history at all. +---@param records transfer[] ordered by date ascending +---@param teams string[] every team template that counts as the team +---@return table +function TeamHistory.fromTransfers(records, teams) + ---@type table + local playersTeamHistory = {} + + Array.forEach(records, function(record) + local entry = TeamHistory.fromTransfer(record, teams) + if not entry then + return + end + + playersTeamHistory[record.player] = playersTeamHistory[record.player] or {} + table.insert(playersTeamHistory[record.player], entry) + end) + + return playersTeamHistory +end + +return TeamHistory diff --git a/lua/wikis/commons/Features/Squad/Api/TransferHistory.lua b/lua/wikis/commons/Domain/TeamHistory/Store.lua similarity index 82% rename from lua/wikis/commons/Features/Squad/Api/TransferHistory.lua rename to lua/wikis/commons/Domain/TeamHistory/Store.lua index ddc32e5731f..b832ca012f8 100644 --- a/lua/wikis/commons/Features/Squad/Api/TransferHistory.lua +++ b/lua/wikis/commons/Domain/TeamHistory/Store.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- page=Module:Features/Squad/Api/TransferHistory +-- page=Module:Domain/TeamHistory/Store -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -13,7 +13,7 @@ local Json = Lua.import('Module:Json') local Lpdb = Lua.import('Module:Lpdb') local PageVariableNamespace = Lua.import('Module:PageVariableNamespace') -local SquadHistory = Lua.import('Module:Features/Squad/Lib/History') +local TeamHistory = Lua.import('Module:Domain/TeamHistory/Model') local BooleanOperator = Condition.BooleanOperator local Comparator = Condition.Comparator @@ -23,15 +23,18 @@ local pageVars = PageVariableNamespace() local QUERY_LIMIT = 5000 local QUERY_ORDER = 'date asc, objectname desc' ---- Every transfer read the squad tables do. -local SquadTransferHistory = {} +--[[ +Where a team's transfer history comes from. Every read of the transfer table for a team belongs +here, so that any feature can ask for a team history instead of writing its own query. +]] +local TeamHistoryStore = {} ---Builds the conditions to fetch all transfers related ---to the given team, respecting historical templates. ---@private ---@param teams string[] ---@return string -function SquadTransferHistory._buildConditions(teams) +function TeamHistoryStore._buildConditions(teams) local conditions = Condition.Tree(BooleanOperator.any) Array.forEach(teams, function (templatename) conditions:add{ @@ -50,7 +53,7 @@ end ---@param team string the team the squad table is for, used as the cache key ---@param teams string[] every team template that counts as that team ---@return table -function SquadTransferHistory.forTeam(team, teams) +function TeamHistoryStore.forTeam(team, teams) local teamHistoryKey = team .. '_all_transfers' ---@type table? @@ -63,7 +66,7 @@ function SquadTransferHistory.forTeam(team, teams) Lpdb.executeMassQuery( 'transfer', { - conditions = SquadTransferHistory._buildConditions(teams), + conditions = TeamHistoryStore._buildConditions(teams), order = QUERY_ORDER, limit = QUERY_LIMIT }, @@ -72,7 +75,7 @@ function SquadTransferHistory.forTeam(team, teams) end ) - local playersTeamHistory = SquadHistory.fromTransfers(records, teams) + local playersTeamHistory = TeamHistory.fromTransfers(records, teams) pageVars:set(teamHistoryKey, Json.stringify(playersTeamHistory)) return playersTeamHistory @@ -84,7 +87,7 @@ end ---@return string? newTeam ---@return string? newRole ---@return string? newDate -function SquadTransferHistory.fetchNextTeam(pagename, date) +function TeamHistoryStore.fetchNextTeam(pagename, date) local conditions = Condition.Tree(BooleanOperator.all) :add{ Condition.Util.anyOf(Condition.ColumnName('player'), {pagename, (string.gsub(pagename, ' ', '_'))}), @@ -102,4 +105,4 @@ function SquadTransferHistory.fetchNextTeam(pagename, date) return transfer.toteamtemplate, transfer.role2, transfer.date end -return SquadTransferHistory +return TeamHistoryStore diff --git a/lua/wikis/commons/Features/Squad/Auto.lua b/lua/wikis/commons/Features/Squad/Auto.lua index c5cf89a2fb7..77ea8ca022d 100644 --- a/lua/wikis/commons/Features/Squad/Auto.lua +++ b/lua/wikis/commons/Features/Squad/Auto.lua @@ -24,7 +24,7 @@ local TransferRefs = Lua.import('Module:Transfer/References') local SquadTypes = Lua.import('Module:Features/Squad/Types') local SquadHistory = Lua.import('Module:Features/Squad/Lib/History') -local SquadTransferHistory = Lua.import('Module:Features/Squad/Api/TransferHistory') +local TeamHistoryStore = Lua.import('Module:Domain/TeamHistory/Store') local SquadCustom = Lua.import('Module:Features/Squad/Custom') local INVALID_HISTORY_CATEGORY = 'SquadAuto with invalid player history' @@ -78,7 +78,7 @@ end ---Handles all necessary steps to fetch and sort data function SquadAuto:build() self:_parseConfig() - self.playersTeamHistory = SquadTransferHistory.forTeam(self.config.team, self.config.teams) + self.playersTeamHistory = TeamHistoryStore.forTeam(self.config.team, self.config.teams) local entries = self:_selectEntries() Array.forEach(entries, FnUtil.curry(SquadAuto._enrichEntry, self)) return entries @@ -403,7 +403,7 @@ function SquadAuto:_mapToSquadPerson(stint) -- On leave: Fetch the next team a person joined if Logic.isNotEmpty(leaveEntry) and Logic.isEmpty(entry.newteam) then - local newTeam, newRole, newDate = SquadTransferHistory.fetchNextTeam(joinEntry.pagename, leaveEntry.date) + local newTeam, newRole, newDate = TeamHistoryStore.fetchNextTeam(joinEntry.pagename, leaveEntry.date) if newTeam then entry.newteam = newTeam entry.newteamrole = newRole diff --git a/lua/wikis/commons/Features/Squad/Lib/History.lua b/lua/wikis/commons/Features/Squad/Lib/History.lua index 1ea60625987..c822a9c7798 100644 --- a/lua/wikis/commons/Features/Squad/Lib/History.lua +++ b/lua/wikis/commons/Features/Squad/Lib/History.lua @@ -8,168 +8,17 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') -local FnUtil = Lua.import('Module:FnUtil') -local Logic = Lua.import('Module:Logic') -local Operator = Lua.import('Module:Operator') local SquadTypes = Lua.import('Module:Features/Squad/Types') +local TeamHistory = Lua.import('Module:Domain/TeamHistory/Model') -local TransferType = SquadTypes.TransferType -local Side = SquadTypes.TransferSide +local TransferType = TeamHistory.TransferType local ROLE_INACTIVE = SquadTypes.ROLE_INACTIVE --- Answers "was this person on the team, and when" from transfer records. --- Pure: it neither queries nor renders anything, it only reads records and returns history. local SquadHistory = {} ----Checks whether a given team is one of the teams the history is built for ----@param teams string[] ----@param team string? ----@return boolean -local function isCurrentTeam(teams, team) - if not team then - return false - end - return Array.any(teams, FnUtil.curry(Operator.eq, team)) -end - ----Finds the team on one side of a transfer, if it is one of the teams the history is built for ----@param side TransferSide ----@param transfer transfer ----@param teams string[] ----@return string? team ----@return boolean isMain whether the team was found in the main rather than the secondary field -local function parseRelevantTeam(side, transfer, teams) - local mainTeam = transfer[side .. 'teamtemplate'] - if mainTeam and isCurrentTeam(teams, mainTeam) then - return mainTeam, true - end - - local secondaryTeam = transfer.extradata[side .. 'teamsectemplate'] - if secondaryTeam and isCurrentTeam(teams, secondaryTeam) then - return secondaryTeam, false - end - - return nil, false -end - ----Maps a transfer to a transfertype, with regards to the current team. ----@param relevantFromTeam string? ----@param relevantToTeam string? ----@return TransferType -local function getTransferType(relevantFromTeam, relevantToTeam) - if relevantFromTeam then - if relevantToTeam then - return TransferType.CHANGE - end - return TransferType.LEAVE - end - return TransferType.JOIN -end - ----Parses the relevant role for the current team from a transfer ----@param side TransferSide ----@param transfer transfer ----@param team string? ----@param isMain boolean ----@return string? -local function parseRelevantRole(side, transfer, team, isMain) - if not team then - return nil - end - - if isMain then - return side == Side.FROM and transfer.role1 or transfer.role2 - else - return side == Side.FROM and transfer.extradata.role1sec or transfer.extradata.role2sec - end -end - ----Reads a single transfer record from the point of view of a team. ----Returns nothing when the transfer does not change anything for that team. ----@param record transfer ----@param teams string[] every team template that counts as the team ----@return TeamHistoryEntry? -function SquadHistory.fromTransfer(record, teams) - record.extradata = record.extradata or {} - - local relevantFromTeam, isFromMain = parseRelevantTeam(Side.FROM, record, teams) - local relevantToTeam, isToMain = parseRelevantTeam(Side.TO, record, teams) - local transferType = getTransferType(relevantFromTeam, relevantToTeam) - - local fromRole = parseRelevantRole(Side.FROM, record, relevantFromTeam, isFromMain) - local toRole = parseRelevantRole(Side.TO, record, relevantToTeam, isToMain) - - -- For leave transfers: Pass on new team for display as next team - if transferType == TransferType.LEAVE and Logic.isEmpty(relevantToTeam) then - if isFromMain then - relevantToTeam = Logic.nilIfEmpty(record.toteamtemplate) - toRole = Logic.nilIfEmpty(record.role2) - else - relevantToTeam = Logic.nilIfEmpty(record.extradata.toteamsectemplate) - toRole = Logic.nilIfEmpty(record.extradata.role2sec) - end - end - - ---@type TeamHistoryEntry - local entry = { - type = transferType, - - -- Person related information - pagename = record.player, - displayname = record.extradata.displayname, - flag = record.nationality, - - -- Date and references - date = record.date, - dateDisplay = record.extradata.displaydate, - references = record.reference, - - -- Roles - fromRole = fromRole, - toRole = toRole, - - fromTeam = relevantFromTeam, - toTeam = relevantToTeam, - - -- Other - wholeTeam = Logic.readBool(record.wholeteam), - position = record.extradata.position, - faction = record.extradata.faction - } - - -- Skip this transfer if there is no relevant change, i.e. the role in this team didn't change - -- E.g. this is grabbed by secondary team, but only main team changed - if relevantFromTeam == relevantToTeam - and entry.fromRole == entry.toRole then - return nil - end - - return entry -end - ----Groups transfer records into a team history per person. ----A person whose every transfer is irrelevant to the team never enters the history at all. ----@param records transfer[] ordered by date ascending ----@param teams string[] every team template that counts as the team ----@return table -function SquadHistory.fromTransfers(records, teams) - ---@type table - local playersTeamHistory = {} - - Array.forEach(records, function(record) - local entry = SquadHistory.fromTransfer(record, teams) - if not entry then - return - end - - playersTeamHistory[record.player] = playersTeamHistory[record.player] or {} - table.insert(playersTeamHistory[record.player], entry) - end) - - return playersTeamHistory -end - ---Walks a person's team history as join -> [inactive] -> leave and returns the stints it found. ---@private ---@param entries TeamHistoryEntry[] diff --git a/lua/wikis/commons/Features/Squad/Types.lua b/lua/wikis/commons/Features/Squad/Types.lua index 084449c2522..33c308f6952 100644 --- a/lua/wikis/commons/Features/Squad/Types.lua +++ b/lua/wikis/commons/Features/Squad/Types.lua @@ -36,23 +36,6 @@ ---@field leavedateref table? ---@field inactivedateref table? ---- One transfer, seen from the point of view of one team. ----@class (exact) TeamHistoryEntry ----@field pagename string ----@field displayname string ----@field flag string ----@field date string ----@field dateDisplay string? ----@field type TransferType ----@field references table ----@field wholeTeam boolean ----@field position string? ----@field fromTeam string? ----@field fromRole string? ----@field toTeam string? ----@field toRole string? ----@field faction string? - --- The transfers behind a single squad row: what put a person on the team, what took them off the --- active squad, and what ended their time there. A person can have several of these on one team, --- and `inactiveEntry` is the transfer that made them inactive, not the last one before leaving. @@ -73,19 +56,6 @@ local SquadTypes = {} ----@enum TransferType -SquadTypes.TransferType = { - LEAVE = 'LEAVE', - JOIN = 'JOIN', - CHANGE = 'CHANGE', -} - ----@enum TransferSide -SquadTypes.TransferSide = { - FROM = 'from', - TO = 'to', -} - --- The role a transfer sets to take someone off the active squad. SquadTypes.ROLE_INACTIVE = 'Inactive' diff --git a/lua/wikis/commons/GameSummary.lua b/lua/wikis/commons/GameSummary.lua index df48b60cc38..e691dec070b 100644 --- a/lua/wikis/commons/GameSummary.lua +++ b/lua/wikis/commons/GameSummary.lua @@ -17,7 +17,7 @@ local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') ---@param props {bracketId: string, matchId: string, gameIdx: integer} ---@return Html function CustomGameSummary.getGameByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch local game = match.games[props.gameIdx] diff --git a/lua/wikis/commons/GameSummary/Starcraft.lua b/lua/wikis/commons/GameSummary/Starcraft.lua index 635dba593f1..3c45fa808a6 100644 --- a/lua/wikis/commons/GameSummary/Starcraft.lua +++ b/lua/wikis/commons/GameSummary/Starcraft.lua @@ -19,7 +19,7 @@ local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') ---@param props {bracketId: string, matchId: string, gameIdx: integer} ---@return Html function CustomGameSummary.getGameByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) --[[ + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) --[[ @as FFAMatchGroupUtilMatch]] local game = match.games[props.gameIdx] diff --git a/lua/wikis/commons/MatchGroup/Display/Bracket.lua b/lua/wikis/commons/MatchGroup/Display/Bracket.lua index e5b035fc382..4131189f2e7 100644 --- a/lua/wikis/commons/MatchGroup/Display/Bracket.lua +++ b/lua/wikis/commons/MatchGroup/Display/Bracket.lua @@ -16,6 +16,7 @@ local Opponent = Lua.import('Module:Opponent/Custom') local StringUtils = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') +local BracketUtil = Lua.import('Module:Domain/Bracket/Model') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchGroupUtil = Lua.import('Module:MatchGroup/Util/Custom') local WikiSpecific = Lua.import('Module:Brkts/WikiSpecific') @@ -356,7 +357,7 @@ function BracketDisplay.computeHeaderRows(bracket, config) local coords = bracket.coordinatesByMatchId[matchId] if bracketData.header then local headerRow = getHeaderRow(matchId) - local brMatch = bracketData.bracketResetMatchId and bracket.matchesById[bracketData.bracketResetMatchId] + local brMatch = BracketUtil.resetMatch(bracket.matchesById, bracketData) headerRow[coords.roundIndex] = { hasBrMatch = brMatch and true or false, header = bracketData.header, @@ -455,8 +456,7 @@ function BracketDisplay.NodeBody(props) -- Include results from bracketResetMatch ---@type MatchGroupUtilMatch? - local bracketResetMatch = match.bracketData.bracketResetMatchId - and props.matchesById[match.bracketData.bracketResetMatchId] + local bracketResetMatch = BracketUtil.resetMatch(props.matchesById, match.bracketData) if bracketResetMatch then match = Logic.wrapTryOrLog(DisplayHelper.mergeBracketResetMatch)(match, bracketResetMatch) end diff --git a/lua/wikis/commons/MatchGroup/Display/Horizontallist.lua b/lua/wikis/commons/MatchGroup/Display/Horizontallist.lua index d1d44d54c39..6b1294487b2 100644 --- a/lua/wikis/commons/MatchGroup/Display/Horizontallist.lua +++ b/lua/wikis/commons/MatchGroup/Display/Horizontallist.lua @@ -88,7 +88,7 @@ function HorizontallistDisplay.Bracket(props) classes = {'navigation-tabs__list'}, attributes = {role = 'tablist'}, children = Array.map(HorizontallistDisplay.computeHeaders(sortedBracket), function (header, index) - local attachedMatch = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, sortedBracket[index][1]) + local attachedMatch = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, sortedBracket[index][1]) local _, matchId = MatchGroupUtil.splitMatchId(attachedMatch.matchId) ---@cast matchId -nil --- If it's a matchList, then matchId is valid as is (also is numeric), otherwise we need to convert it to a key @@ -137,7 +137,7 @@ function HorizontallistDisplay.findMatchClosestInTime(bracketId, bracket) local liveGames = {} ---@type {matchIdx: integer, distanceToNow: integer}[] local otherGames = {} ---@type {matchIdx: integer, distanceToNow: integer}[] for matchIdx, matchInfo in ipairs(bracket) do - local match = MatchGroupUtil.fetchMatchForBracketDisplay(bracketId, matchInfo[1]) + local match = MatchGroupUtil.fetchMatchWithBracketReset(bracketId, matchInfo[1]) for _, game in ipairs(match.games) do local tblToInsertInto = MatchGroupUtil.computeMatchPhase(game) == 'live' and liveGames or otherGames local ts = Date.readTimestampOrNil(game.date) diff --git a/lua/wikis/commons/MatchGroup/Display/SingleGame.lua b/lua/wikis/commons/MatchGroup/Display/SingleGame.lua index 3ce29d5863b..8dbb946af34 100644 --- a/lua/wikis/commons/MatchGroup/Display/SingleGame.lua +++ b/lua/wikis/commons/MatchGroup/Display/SingleGame.lua @@ -35,7 +35,7 @@ function SingleGameDisplay.SingleGameContainer(props) assert(bracketId, 'Missing or invalid matchId') assert(props.gameIdx, 'Missing gameIdx') - local match = MatchGroupUtil.fetchMatchForBracketDisplay(bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(bracketId, props.matchId) if not match then return Html.Fragment{} diff --git a/lua/wikis/commons/MatchGroup/Display/SingleMatch.lua b/lua/wikis/commons/MatchGroup/Display/SingleMatch.lua index 84437f2b7fa..294336346b6 100644 --- a/lua/wikis/commons/MatchGroup/Display/SingleMatch.lua +++ b/lua/wikis/commons/MatchGroup/Display/SingleMatch.lua @@ -35,7 +35,7 @@ function SingleMatchDisplay.SingleMatchContainer(props) assert(bracketId, 'Missing or invalid matchId') - local match = MatchGroupUtil.fetchMatchForBracketDisplay(bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(bracketId, props.matchId) return match and SingleMatchDisplay.SingleMatch({ config = props.config, diff --git a/lua/wikis/commons/MatchGroup/Util.lua b/lua/wikis/commons/MatchGroup/Util.lua index f6cb820275e..037e80e1dff 100644 --- a/lua/wikis/commons/MatchGroup/Util.lua +++ b/lua/wikis/commons/MatchGroup/Util.lua @@ -9,23 +9,21 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local FnUtil = Lua.import('Module:FnUtil') -local Json = Lua.import('Module:Json') -local Operator = Lua.import('Module:Operator') local Table = Lua.import('Module:Table') -local Variables = Lua.import('Module:Variables') -local MatchGroupCoordinates = Lua.import('Module:MatchGroup/Coordinates') +local MatchGroupCoordinates = Lua.import('Module:Domain/Bracket/Coordinates') local WikiSpecific = Lua.import('Module:Brkts/WikiSpecific') -local BracketUtil = Lua.import('Module:MatchGroup/Util/Bracket') -local MatchUtil = Lua.import('Module:MatchGroup/Util/Match') +local BracketUtil = Lua.import('Module:Domain/Bracket/Model') +local MatchStore = Lua.import('Module:Domain/Match/Store') +local MatchUtil = Lua.import('Module:Domain/Match/Model') local Types = Lua.import('Module:MatchGroup/Util/Types') --[[ Fetches match records and assembles them into matchlists and brackets. -The pieces it assembles from live elsewhere: the match model in Module:MatchGroup/Util/Match, the -bracket model in Module:MatchGroup/Util/Bracket, and the shapes of both in +The pieces it assembles from live elsewhere: the match model in Module:Domain/Match/Model, the +bracket model in Module:Domain/Bracket/Model, and the shapes of both in Module:MatchGroup/Util/Types. Display related functions go in Module:MatchGroup/Display/Helper. Both models are also re-exported here, as a complete mirror: every member of either model has a @@ -38,7 +36,11 @@ over, and nothing that is not a model member should be added to it. ---@class MatchGroupUtil local MatchGroupUtil = {types = Types} ---- Re-exported from Module:MatchGroup/Util/Match. Prefer importing that module directly. +--- Re-exported from Module:Domain/Match/Store. Prefer importing that module directly. +MatchGroupUtil.fetchMatchIds = MatchStore.fetchMatchIds +MatchGroupUtil.fetchMatchRecords = MatchStore.fetchMatchRecords + +--- Re-exported from Module:Domain/Match/Model. Prefer importing that module directly. MatchGroupUtil.matchFromRecord = MatchUtil.matchFromRecord MatchGroupUtil.opponentFromRecord = MatchUtil.opponentFromRecord MatchGroupUtil.createOpponent = MatchUtil.createOpponent @@ -47,7 +49,7 @@ MatchGroupUtil.gameFromRecord = MatchUtil.gameFromRecord MatchGroupUtil.groupBySubgroup = MatchUtil.groupBySubgroup MatchGroupUtil.computeMatchPhase = MatchUtil.computeMatchPhase ---- Re-exported from Module:MatchGroup/Util/Bracket. Prefer importing that module directly. +--- Re-exported from Module:Domain/Bracket/Model. Prefer importing that module directly. MatchGroupUtil.splitMatchId = BracketUtil.splitMatchId MatchGroupUtil.matchIdToKey = BracketUtil.matchIdToKey MatchGroupUtil.matchIdFromKey = BracketUtil.matchIdFromKey @@ -57,6 +59,7 @@ MatchGroupUtil.computeLowerMatchIdsFromLegacy = BracketUtil.computeLowerMatchIds MatchGroupUtil.autoAssignLowerEdges = BracketUtil.autoAssignLowerEdges MatchGroupUtil.computeAdvanceSpots = BracketUtil.computeAdvanceSpots MatchGroupUtil.populateAdvanceSpots = BracketUtil.populateAdvanceSpots +MatchGroupUtil.resetMatch = BracketUtil.resetMatch MatchGroupUtil.computeRootMatchIds = BracketUtil.computeRootMatchIds MatchGroupUtil.backfillUpperMatchIds = BracketUtil.backfillUpperMatchIds MatchGroupUtil.backfillCoordinates = BracketUtil.backfillCoordinates @@ -64,39 +67,6 @@ MatchGroupUtil.indexTableFromRecord = BracketUtil.indexTableFromRecord MatchGroupUtil.indexTableToRecord = BracketUtil.indexTableToRecord MatchGroupUtil.sectionIndexToString = BracketUtil.sectionIndexToString ----Fetches all match ids of matches that satisfy the supplied condition ----@param props {conditions: string|AbstractConditionNode, limit: string|integer?, order: string?} ----@return string[] -function MatchGroupUtil.fetchMatchIds(props) - ---@type string[] - return Array.map(mw.ext.LiquipediaDB.lpdb('match2', { - limit = tonumber(props.limit) or 1000, - query = 'match2id', - conditions = tostring(props.conditions), - order = props.order - }), Operator.property('match2id')) -end - ----Fetches all matches in a matchlist or bracket. Tries to read from page variables before fetching from LPDB. ----Returns a list of records ordered lexicographically by matchId. ----@param bracketId string ----@return table[] -function MatchGroupUtil.fetchMatchRecords(bracketId) - local varData = Variables.varDefault('match2bracket_' .. bracketId) - if varData then - return (Json.parse(varData)) - end - - return mw.ext.LiquipediaDB.lpdb( - 'match2', - { - conditions = '([[namespace::0]] or [[namespace::>0]]) AND [[match2bracketid::' .. bracketId .. ']]', - order = 'match2id ASC', - limit = 5000, - } - ) -end - MatchGroupUtil.fetchMatchGroup = FnUtil.memoize(function(bracketId) local matchRecords = MatchGroupUtil.fetchMatchRecords(bracketId) return MatchGroupUtil.makeMatchGroup(matchRecords) @@ -176,18 +146,16 @@ function MatchGroupUtil.fetchMatches(bracketId) return MatchGroupUtil.fetchMatchGroup(bracketId).matches end ----Returns a match struct for use in a bracket display or match summary popup. The bracket display and match summary ----popup expects that the finals match also include results from the bracket reset match. +---Returns a match together with its bracket reset match, if it has one. Callers that display the +---pair as a single match merge them with Module:MatchGroup/Display/Helper.mergeBracketResetMatch. ---@param bracketId string ---@param matchId string ---@return MatchGroupUtilMatch, MatchGroupUtilMatch? -function MatchGroupUtil.fetchMatchForBracketDisplay(bracketId, matchId) +function MatchGroupUtil.fetchMatchWithBracketReset(bracketId, matchId) local bracket = MatchGroupUtil.fetchMatchGroup(bracketId) local match = bracket.matchesById[matchId] - local bracketResetMatch = match - and match.bracketData.bracketResetMatchId - and bracket.matchesById[match.bracketData.bracketResetMatchId] + local bracketResetMatch = match and BracketUtil.resetMatch(bracket.matchesById, match.bracketData) return match, bracketResetMatch end diff --git a/lua/wikis/commons/MatchGroup/Util/Types.lua b/lua/wikis/commons/MatchGroup/Util/Types.lua index a6aeb1c327b..6a3cc1fb338 100644 --- a/lua/wikis/commons/MatchGroup/Util/Types.lua +++ b/lua/wikis/commons/MatchGroup/Util/Types.lua @@ -10,25 +10,19 @@ local Lua = require('Module:Lua') local TypeUtil = Lua.import('Module:TypeUtil') --[[ -The shapes of the match group model: matches, opponents, games, players, bracket data and the -match groups that hold them. +Deprecated. The runtime TypeUtil description of the match group model, reachable as +MatchGroupUtil.types and extended by a couple of per wiki modules. + +TypeUtil is on its way out, and the shapes themselves are documented as annotations on +Module:Domain/Match/Model and Module:Domain/Bracket/Model. Nothing new should be added here. ]] local Types = {} ----@class MatchGroupUtilLowerEdge ----@field lowerMatchIndex number ----@field opponentIndex number - Types.LowerEdge = TypeUtil.struct({ lowerMatchIndex = 'number', opponentIndex = 'number', }) ----@alias AdvanceBg 'up'|'stayup'|'stay'|'staydown'|'down' Types.AdvanceBg = TypeUtil.literalUnion('up', 'stayup', 'stay', 'staydown', 'down') ----@class MatchGroupUtilAdvanceSpot ----@field bg AdvanceBg ----@field matchId string? ----@field type string? Types.AdvanceSpot = TypeUtil.struct({ bg = Types.AdvanceBg, @@ -36,29 +30,6 @@ Types.AdvanceSpot = TypeUtil.struct({ type = TypeUtil.literalUnion('advance', 'custom', 'qualify'), }) ----@class MatchGroupUtilBracketBracketData ----@field coordinates MatchGroupUtilMatchCoordinates ----@field advanceSpots MatchGroupUtilAdvanceSpot[] ----@field bracketResetMatchId string? ----@field bracketType string? ----@field header string? ----@field inheritedHeader string? ----@field lowerEdges MatchGroupUtilLowerEdge[]? ----@field lowerMatchIds string[] ----@field qualLose boolean? ----@field qualLoseLiteral string? ----@field qualSkip number? ----@field qualWin boolean? ----@field qualWinLiteral string? ----@field skipRound number? ----@field thirdPlaceMatchId string? ----@field title string? ----@field type 'bracket' ----@field upperMatchId string? ----@field matchId string? ----@field matchPage string? ----@field qualifiedHeader string? - Types.BracketBracketData = TypeUtil.struct({ advanceSpots = TypeUtil.array(Types.AdvanceSpot), bracketResetMatchId = 'string?', @@ -79,17 +50,6 @@ Types.BracketBracketData = TypeUtil.struct({ type = TypeUtil.literal('bracket'), upperMatchId = 'string?', }) ----@class MatchGroupUtilMatchCoordinates ----@field depth integer ----@field depthCount integer ----@field matchIndexInRound integer ----@field rootIndex integer ----@field roundCount integer ----@field roundIndex integer ----@field sectionCount integer ----@field sectionIndex integer ----@field semanticDepth integer ----@field semanticRoundIndex integer Types.MatchCoordinates = TypeUtil.struct({ depth = 'integer', @@ -103,13 +63,6 @@ Types.MatchCoordinates = TypeUtil.struct({ semanticDepth = 'integer', semanticRoundIndex = 'integer', }) ----@class MatchGroupUtilMatchlistBracketData ----@field header string? ----@field title string? ----@field dateHeader boolean? ----@field type 'matchlist' ----@field matchIndex string? ----@field matchPage string? Types.MatchlistBracketData = TypeUtil.struct({ header = 'string?', @@ -117,23 +70,11 @@ Types.MatchlistBracketData = TypeUtil.struct({ dateHeader = 'boolean?', type = TypeUtil.literal('matchlist'), }) ----@alias MatchGroupUtilBracketData MatchGroupUtilMatchlistBracketData|MatchGroupUtilBracketBracketData Types.BracketData = TypeUtil.union( Types.MatchlistBracketData, Types.BracketBracketData ) ----@class standardPlayer ----@field displayName string? ----@field flag string? ----@field pageName string? ----@field team string? ----@field extradata table? ----@field pageIsResolved boolean? ----@field faction string? ----@field roles string[]? ----@field apiId string? - Types.Player = TypeUtil.struct({ displayName = 'string?', flag = 'string?', @@ -144,25 +85,6 @@ Types.Player = TypeUtil.struct({ faction = 'string?', }) ----@class standardOpponent ----@field advanceBg string? ----@field advances boolean? ----@field icon string? ----@field icondark string? ----@field name string? ----@field placement number? ----@field placement2 number? ----@field players standardPlayer[]? ----@field score number? ----@field scoreDisplay number? ----@field score2 number? ----@field status string? ----@field status2 string? ----@field template string? ----@field type OpponentType ----@field team string? ----@field extradata table - Types.Opponent = TypeUtil.struct({ advanceBg = 'string?', advances = 'boolean?', @@ -180,12 +102,6 @@ Types.Opponent = TypeUtil.struct({ extradata = 'table', }) ----@class GameOpponent ----@field name string? ----@field players standardPlayer[] ----@field template string? ----@field type string - Types.GameOpponent = TypeUtil.struct({ name = 'string?', players = TypeUtil.optional(TypeUtil.array(Types.Player)), @@ -193,33 +109,8 @@ Types.GameOpponent = TypeUtil.struct({ type = 'string', }) ----@alias MatchStatus 'notplayed'|''|nil Types.Status = TypeUtil.optional(TypeUtil.literalUnion('notplayed', '')) ----@class MatchGroupUtilGame ----@field comment string? ----@field date string? ----@field dateIsExact boolean ----@field game string? ----@field header string? ----@field length string|number? ----@field map string? ----@field mapDisplayName string? ----@field mode string? ----@field opponents {players: table[], score: number?, status: string?}[] ----@field patch string? ----@field resultType string? ----@field scores number[] ----@field subgroup number? ----@field type string? ----@field vod string? ----@field winner integer? ----@field status string? ----@field walkover string? ----@field extradata table? ----@field timestamp number ----@field timezoneId string? - Types.Game = TypeUtil.struct({ comment = 'string?', date = 'string?', @@ -239,45 +130,6 @@ Types.Game = TypeUtil.struct({ extradata = 'table?', }) ----@class MatchGroupUtilMatch ----@field bracketData MatchGroupUtilBracketData ----@field comment string? ----@field date string ----@field dateIsExact boolean ----@field finished boolean ----@field game string? ----@field games MatchGroupUtilGame[] ----@field icon string? ----@field iconDark string? ----@field links table ----@field liquipediatier string? # TODO: camelCase ----@field liquipediatiertype string? # TODO: camelCase ----@field matchId string? ----@field mode string? ----@field opponents standardOpponent[] ----@field pageName string? ----@field parent string? ----@field patch string? ----@field phase 'upcoming'|'ongoing'|'finished' ----@field publisherTier string? ----@field resultType string? ----@field section string? ----@field series string? ----@field shortname string? ----@field status MatchStatus ----@field stream table ----@field submatches MatchGroupUtilSubgroup[] ----@field tickername string? ----@field tournament string? ----@field type string? ----@field vod string? ----@field walkover string? ----@field winner number? ----@field extradata table? ----@field timestamp number ----@field timezoneId string? ----@field bestof number? - Types.Match = TypeUtil.struct({ bracketData = Types.BracketData, comment = 'string?', @@ -312,23 +164,6 @@ Types.Match = TypeUtil.struct({ extradata = 'table?', }) ----@class MatchGroupUtilSubgroup ----@field games MatchGroupUtilGame[] ----@field subgroup number ----@field header string? - ----@class FFAMatchGroupUtilMatch: MatchGroupUtilMatch ----@field games FFAMatchGroupUtilGame[] - ----@class FFAMatchGroupUtilGame: MatchGroupUtilGame ----@field stream table - ----@class MatchGroupUtilMatchlist ----@field bracketDatasById table ----@field matches MatchGroupUtilMatch[] ----@field matchesById table ----@field type 'matchlist' - Types.Matchlist = TypeUtil.struct({ bracketDatasById = TypeUtil.table('string', Types.BracketData), matches = TypeUtil.array(Types.Match), @@ -336,16 +171,6 @@ Types.Matchlist = TypeUtil.struct({ type = TypeUtil.literal('matchlist'), }) ----@class MatchGroupUtilBracket ----@field bracketDatasById table ----@field coordinatesByMatchId table ----@field matches MatchGroupUtilMatch[] ----@field matchesById table ----@field rootMatchIds string[] ----@field rounds string[][] ----@field sections string[][] ----@field type 'bracket' - Types.Bracket = TypeUtil.struct({ bracketDatasById = TypeUtil.table('string', Types.BracketData), coordinatesByMatchId = TypeUtil.table('string', Types.MatchCoordinates), @@ -357,18 +182,9 @@ Types.Bracket = TypeUtil.struct({ type = TypeUtil.literal('bracket'), }) ----@alias MatchGroupUtilMatchGroup MatchGroupUtilBracket|MatchGroupUtilMatchlist Types.MatchGroup = TypeUtil.union( Types.Matchlist, Types.Bracket ) ---- The subset of a match or game record that is enough to work out its phase. ----@class PartialMatchGameRecord ----@field date string ----@field dateexact boolean|string|nil # records carry '0'/'1' ----@field timestamp number? ----@field finished boolean? ----@field winner integer? - return Types diff --git a/lua/wikis/commons/MatchSummary/Base.lua b/lua/wikis/commons/MatchSummary/Base.lua index 5dad1d2b8f2..d17e9f371ae 100644 --- a/lua/wikis/commons/MatchSummary/Base.lua +++ b/lua/wikis/commons/MatchSummary/Base.lua @@ -203,7 +203,7 @@ function MatchSummary.defaultGetByMatchId(CustomMatchSummary, args, options) options = options or {} - local match, bracketResetMatch = MatchGroupUtil.fetchMatchForBracketDisplay( + local match, bracketResetMatch = MatchGroupUtil.fetchMatchWithBracketReset( args.bracketId, args.matchId) ---@type (fun(match: MatchGroupUtilMatch):string?)|string|integer? diff --git a/lua/wikis/commons/MatchSummary/Starcraft/Ffa.lua b/lua/wikis/commons/MatchSummary/Starcraft/Ffa.lua index 5b8134e9dbd..28cd1119dad 100644 --- a/lua/wikis/commons/MatchSummary/Starcraft/Ffa.lua +++ b/lua/wikis/commons/MatchSummary/Starcraft/Ffa.lua @@ -27,7 +27,7 @@ local Parser = {} ---@param props {bracketId: string, matchId: string} ---@return Renderable function StarcraftMatchSummaryFfa.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch BaseMatchSummary.updateMatchOpponents(match) diff --git a/lua/wikis/commons/PrizePool/Import.lua b/lua/wikis/commons/PrizePool/Import.lua index 538d335253a..ebd1e4d1574 100644 --- a/lua/wikis/commons/PrizePool/Import.lua +++ b/lua/wikis/commons/PrizePool/Import.lua @@ -17,7 +17,7 @@ local String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') local TournamentStructure = Lua.import('Module:TournamentStructure') -local MatchGroupCoordinates = Lua.import('Module:MatchGroup/Coordinates') +local MatchGroupCoordinates = Lua.import('Module:Domain/Bracket/Coordinates') local MatchGroupUtil = Lua.import('Module:MatchGroup/Util/Custom') local Placement = Lua.import('Module:PrizePool/Placement') diff --git a/lua/wikis/counterstrike/MatchSummary/Ffa.lua b/lua/wikis/counterstrike/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/counterstrike/MatchSummary/Ffa.lua +++ b/lua/wikis/counterstrike/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/deltaforce/MatchSummary/Ffa.lua b/lua/wikis/deltaforce/MatchSummary/Ffa.lua index 7d5f0dfc0cf..c3d4de6643c 100644 --- a/lua/wikis/deltaforce/MatchSummary/Ffa.lua +++ b/lua/wikis/deltaforce/MatchSummary/Ffa.lua @@ -20,7 +20,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@return Widget function CustomMatchSummary.getByMatchId(props) ---@class FFAMatchGroupUtilMatch - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/fortnite/MatchSummary/Ffa.lua b/lua/wikis/fortnite/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/fortnite/MatchSummary/Ffa.lua +++ b/lua/wikis/fortnite/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/freefire/MatchSummary/Ffa.lua b/lua/wikis/freefire/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/freefire/MatchSummary/Ffa.lua +++ b/lua/wikis/freefire/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/geoguessr/GameSummary.lua b/lua/wikis/geoguessr/GameSummary.lua index 3d6d52a038a..98a80b3f0e5 100644 --- a/lua/wikis/geoguessr/GameSummary.lua +++ b/lua/wikis/geoguessr/GameSummary.lua @@ -19,8 +19,8 @@ local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') ---@param props {bracketId: string, matchId: string, gameIdx: integer} ---@return Html function CustomGameSummary.getGameByMatchId(props) - ---@type FFAMatchGroupUtilMatch - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) + ---@cast match FFAMatchGroupUtilMatch local game = match.games[props.gameIdx] assert(game, 'Error Game ID ' .. tostring(props.gameIdx) .. ' not found') diff --git a/lua/wikis/geoguessr/MatchSummary/Ffa.lua b/lua/wikis/geoguessr/MatchSummary/Ffa.lua index 1ca06ea9190..5f58cb4dd97 100644 --- a/lua/wikis/geoguessr/MatchSummary/Ffa.lua +++ b/lua/wikis/geoguessr/MatchSummary/Ffa.lua @@ -23,7 +23,7 @@ local Parser = {} ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/halo/MatchSummary/Ffa.lua b/lua/wikis/halo/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/halo/MatchSummary/Ffa.lua +++ b/lua/wikis/halo/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/hearthstone/MatchSummary/Ffa.lua b/lua/wikis/hearthstone/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/hearthstone/MatchSummary/Ffa.lua +++ b/lua/wikis/hearthstone/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/lab/MatchSummary/Ffa.lua b/lua/wikis/lab/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/lab/MatchSummary/Ffa.lua +++ b/lua/wikis/lab/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/naraka/MatchSummary/Ffa.lua b/lua/wikis/naraka/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/naraka/MatchSummary/Ffa.lua +++ b/lua/wikis/naraka/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/pubg/MatchSummary/Ffa.lua b/lua/wikis/pubg/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/pubg/MatchSummary/Ffa.lua +++ b/lua/wikis/pubg/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/pubgmobile/MatchSummary/Ffa.lua b/lua/wikis/pubgmobile/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/pubgmobile/MatchSummary/Ffa.lua +++ b/lua/wikis/pubgmobile/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/tarkovarena/MatchSummary/Ffa.lua b/lua/wikis/tarkovarena/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/tarkovarena/MatchSummary/Ffa.lua +++ b/lua/wikis/tarkovarena/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/tft/MatchSummary/Ffa.lua b/lua/wikis/tft/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/tft/MatchSummary/Ffa.lua +++ b/lua/wikis/tft/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/thefinals/GameSummary.lua b/lua/wikis/thefinals/GameSummary.lua index 340454f1665..9797fc77925 100644 --- a/lua/wikis/thefinals/GameSummary.lua +++ b/lua/wikis/thefinals/GameSummary.lua @@ -19,7 +19,7 @@ local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') ---@param props {bracketId: string, matchId: string, gameIdx: integer} ---@return Html function CustomGameSummary.getGameByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) --[[ + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) --[[ @as FFAMatchGroupUtilMatch]] local game = match.games[props.gameIdx] diff --git a/lua/wikis/thefinals/MatchSummary/Ffa.lua b/lua/wikis/thefinals/MatchSummary/Ffa.lua index cad2351421f..6f3e7087e23 100644 --- a/lua/wikis/thefinals/MatchSummary/Ffa.lua +++ b/lua/wikis/thefinals/MatchSummary/Ffa.lua @@ -23,7 +23,7 @@ local Parser = {} ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/trackmania/MatchSummary/Ffa.lua b/lua/wikis/trackmania/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/trackmania/MatchSummary/Ffa.lua +++ b/lua/wikis/trackmania/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/underlords/MatchSummary/Ffa.lua b/lua/wikis/underlords/MatchSummary/Ffa.lua index e4e4830081f..aad3b1ef729 100644 --- a/lua/wikis/underlords/MatchSummary/Ffa.lua +++ b/lua/wikis/underlords/MatchSummary/Ffa.lua @@ -19,7 +19,7 @@ local WidgetUtil = Lua.import('Module:Widget/Util') ---@param props {bracketId: string, matchId: string} ---@return Renderable function CustomMatchSummary.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch SummaryHelper.updateMatchOpponents(match) local scoringData = SummaryHelper.createScoringData(match) diff --git a/lua/wikis/warcraft/GameSummary.lua b/lua/wikis/warcraft/GameSummary.lua index dcf7020596e..356d408abbb 100644 --- a/lua/wikis/warcraft/GameSummary.lua +++ b/lua/wikis/warcraft/GameSummary.lua @@ -22,7 +22,7 @@ local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') ---@param props {bracketId: string, matchId: string, gameIdx: integer} ---@return Html function CustomGameSummary.getGameByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) --[[ + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) --[[ @as FFAMatchGroupUtilMatch]] local game = match.games[props.gameIdx] diff --git a/lua/wikis/warcraft/MatchSummary/Ffa.lua b/lua/wikis/warcraft/MatchSummary/Ffa.lua index e02f6da676c..64e8e583231 100644 --- a/lua/wikis/warcraft/MatchSummary/Ffa.lua +++ b/lua/wikis/warcraft/MatchSummary/Ffa.lua @@ -27,7 +27,7 @@ local Parser = {} ---@param props {bracketId: string, matchId: string} ---@return Renderable function WarcraftMatchSummaryFfa.getByMatchId(props) - local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + local match = MatchGroupUtil.fetchMatchWithBracketReset(props.bracketId, props.matchId) ---@cast match FFAMatchGroupUtilMatch BaseMatchSummary.updateMatchOpponents(match) diff --git a/scripts/check_lua_imports.py b/scripts/check_lua_imports.py new file mode 100644 index 00000000000..8be3873d2a8 --- /dev/null +++ b/scripts/check_lua_imports.py @@ -0,0 +1,183 @@ +"""Check the layering rules from the bulletproof-lua RFC. + +Three layers, and imports only point down: features -> domain -> shared. Within a feature they +point down too: an entry point may reach the controller, the controller may reach +Components/Api/Lib, and those may reach Types. + +Only the parts of the tree that have been moved onto the structure are classified. Everything +that has not been restructured yet is "unclassified", and is only used to detect reaching into a +feature's internals from outside; it is not itself held to the layer rules. + +Enforcement is graduated. Code that has been moved onto the structure is held to the rules and +fails the build, because a regression there is a regression in work already done. Unclassified code +only warns, because it has not been migrated yet; those warnings become errors as each area moves. + +On GitHub Actions each violation is printed as a workflow command so it lands on the diff of the +offending line. Run anywhere else it is plain text on stderr instead. +""" + +from __future__ import annotations + +import argparse +import os +import pathlib +import re +import sys +from typing import NamedTuple + +LUA_ROOT = pathlib.Path("lua/wikis") +IMPORT_RE = re.compile( + r"""(?:Lua\.import|Lua\.requireIfExists|require)\(\s*['"]Module:([^'"]+)['"]""" +) + +# Feature tiers, lowest first. Modules directly in the feature folder are entry points. +TIERS = {"Types": 0, "Lib": 1, "Api": 1, "Components": 1, "Controller": 2} +ENTRY_TIER = 3 + +# Domain holds the site's entities and their rules. It may read per wiki *config* (Info.config is +# data, and every rule read from it must be overridable by a parameter so the entity stays +# testable), but it may not render, and it may not reach for per wiki *code*: that is a feature +# concern and it cannot be substituted in a test. +DOMAIN_FORBIDDEN_ROOTS = {"Widget": "renders"} +DOMAIN_FORBIDDEN_PATTERNS = { + "Brkts/WikiSpecific": "reaches for per wiki code", + "/Custom": "reaches for per wiki code", +} + + +class Violation(NamedTuple): + """One broken rule, located at the import that broke it.""" + + path: pathlib.Path + line: int + column: int + message: str + + +class Module: + """A lua module, classified by where it sits in the layering.""" + + def __init__(self, path: pathlib.Path): + self.path = path + parts = path.relative_to(LUA_ROOT).with_suffix("").parts + self.name = "/".join(parts[1:]) + self.wiki = parts[0] + self.layer, self.feature, self.tier, self.group = classify("/".join(parts[1:])) + + def imports(self) -> list[tuple[str, int, int]]: + """Every imported module name, with the line and column it is imported on.""" + text = self.path.read_text(encoding="utf-8") + found = [] + for match in IMPORT_RE.finditer(text): + line = text.count("\n", 0, match.start()) + 1 + column = match.start() - text.rfind("\n", 0, match.start()) + found.append((match.group(1), line, column)) + return found + + +def classify(name: str) -> tuple[str, str | None, int | None, str | None]: + """Classify a module name into layer, feature, tier and tier folder.""" + parts = name.split("/") + if parts[0] == "Features" and len(parts) > 1: + rest = parts[2:] + # Types.lua and Controller.lua are tiers that sit directly in the feature folder; + # anything else there (Custom.lua, Auto.lua) is an entry point. + group = rest[0] if rest and rest[0] in TIERS else None + tier = TIERS[group] if group else ENTRY_TIER + return "feature", parts[1], tier, group + if parts[0] == "Domain": + return "domain", None, None, None + return "unclassified", None, None, None + + +def violations(module: Module) -> list[Violation]: + found = [] + for name, line, column in module.imports(): + layer, feature, tier, group = classify(name) + + def report(message: str) -> None: + found.append(Violation(module.path, line, column, message)) + + if layer == "feature" and feature != module.feature and tier != ENTRY_TIER: + report(f"reaches into {feature}'s internals: Module:{name}") + + if ( + layer == "feature" + and feature != module.feature + and module.layer == "feature" + ): + report(f"imports another feature: Module:{name}") + + if layer == "feature" and module.layer == "domain": + report(f"domain must not import a feature: Module:{name}") + + if module.layer == "domain": + reason = DOMAIN_FORBIDDEN_ROOTS.get(name.split("/")[0]) + for pattern, why in DOMAIN_FORBIDDEN_PATTERNS.items(): + if pattern in name: + reason = why + if reason: + report(f"domain {reason}: Module:{name}") + + if ( + layer == "feature" + and feature == module.feature + and None not in (tier, module.tier) + ): + # Peers inside one tier folder may compose each other; crossing between them may not. + if tier > module.tier: + report(f"imports upward within {feature}: Module:{name}") + elif tier == module.tier and group != module.group: + report(f"imports sideways within {feature}: Module:{name}") + + return found + + +def report_github(level: str, violation: Violation) -> None: + """Print a workflow command, so the violation is annotated on the line that caused it.""" + # Workflow commands are read off stdout, and the data in them has to be escaped. + message = violation.message + for char, replacement in (("%", "%25"), ("\r", "%0D"), ("\n", "%0A")): + message = message.replace(char, replacement) + print( + f"::{level} file={violation.path}," + f"line={violation.line},col={violation.column}::{message}" + ) + + +def report_plain(level: str, violation: Violation) -> None: + print( + f"{level}: {violation.path}:{violation.line}: {violation.message}", + file=sys.stderr, + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.parse_args() + + errors: list[Violation] = [] + warnings: list[Violation] = [] + for path in sorted(LUA_ROOT.rglob("*.lua")): + module = Module(path) + # A violation coming out of restructured code breaks work already done, so it fails. + bucket = warnings if module.layer == "unclassified" else errors + bucket.extend(violations(module)) + + report = ( + report_github if os.environ.get("GITHUB_ACTIONS") == "true" else report_plain + ) + for level, found in (("error", errors), ("warning", warnings)): + for violation in found: + report(level, violation) + + if not errors and not warnings: + print("No import rule violations.") + else: + print(f"\n{len(errors)} error(s), {len(warnings)} warning(s).") + + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main())