diff --git a/app/db/tables.ts b/app/db/tables.ts index eeb5d6588..af6396c72 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -496,6 +496,7 @@ export interface User { twitter: string | null; vc: Generated; youtubeId: string | null; + plusSkippedForSeasonNth: number | null; } export interface UserResultHighlight { diff --git a/app/features/admin/routes/admin.test.ts b/app/features/admin/routes/admin.test.ts index a14e11f16..a4d148bd1 100644 --- a/app/features/admin/routes/admin.test.ts +++ b/app/features/admin/routes/admin.test.ts @@ -140,6 +140,45 @@ PlusVoting( }, ); +PlusVoting( + "skips users from leaderboard with the skip flag for the season", + async () => { + MockDate.set(new Date("2023-11-29T00:00:00.000Z")); + + await Test.database.insertUsers(11); + await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1)); + + await db + .updateTable("User") + .set({ plusSkippedForSeasonNth: 1 }) + .where("User.id", "=", 1) + .execute(); + + await adminAction({ _action: "REFRESH" }, { user: "admin" }); + + assert.equal(await countPlusTierMembers(1), 10); + assert.equal(await countPlusTierMembers(2), 0); + }, +); + +PlusVoting("plus server skip flag ignored if for past season", async () => { + MockDate.set(new Date("2023-11-29T00:00:00.000Z")); + + await Test.database.insertUsers(11); + await createLeaderboard(Array.from({ length: 11 }).map((_, i) => i + 1)); + + await db + .updateTable("User") + .set({ plusSkippedForSeasonNth: 0 }) + .where("User.id", "=", 1) + .execute(); + + await adminAction({ _action: "REFRESH" }, { user: "admin" }); + + assert.equal(await countPlusTierMembers(1), 10); + assert.equal(await countPlusTierMembers(2), 1); +}); + PlusVoting("ignores leaderboard while season is ongoing", async () => { MockDate.set(new Date("2024-02-15T00:00:00.000Z")); diff --git a/app/features/leaderboards/core/leaderboards.server.ts b/app/features/leaderboards/core/leaderboards.server.ts index fbfc8bd11..f05b491d7 100644 --- a/app/features/leaderboards/core/leaderboards.server.ts +++ b/app/features/leaderboards/core/leaderboards.server.ts @@ -4,6 +4,7 @@ import type { SeasonPopularUsersWeapon } from "../queries/seasonPopularUsersWeap import type { MainWeaponId } from "~/modules/in-game-lists"; import { weaponCategories } from "~/modules/in-game-lists"; import { seasonHasTopTen } from "../leaderboards-utils"; +import { currentOrPreviousSeason } from "~/features/mmr/season"; export function addTiers(entries: UserSPLeaderboardItem[], season: number) { const tiers = freshUserSkills(season); @@ -53,6 +54,12 @@ export function addPendingPlusTiers( if (!highestPlusTierWithSpace) break; if (entry.plusTier && entry.plusTier <= highestPlusTierWithSpace) continue; + if ( + entry.plusSkippedForSeasonNth === currentOrPreviousSeason(new Date())?.nth + ) { + entry.plusSkippedForSeasonNth = null; + continue; + } entry.pendingPlusTier = highestPlusTierWithSpace; const key = `+${highestPlusTierWithSpace}` as const; diff --git a/app/features/leaderboards/queries/userSPLeaderboard.server.ts b/app/features/leaderboards/queries/userSPLeaderboard.server.ts index 828a3c686..d89cdf783 100644 --- a/app/features/leaderboards/queries/userSPLeaderboard.server.ts +++ b/app/features/leaderboards/queries/userSPLeaderboard.server.ts @@ -15,6 +15,7 @@ const stm = sql.prepare(/* sql */ ` "User"."discordAvatar", "User"."discordId", "User"."customUrl", + "User"."plusSkippedForSeasonNth", "PlusTier"."tier" as "plusTier", rank () over ( order by "Skill"."Ordinal" desc @@ -48,6 +49,7 @@ export interface UserSPLeaderboardItem { discordId: User["discordId"]; customUrl: User["customUrl"]; plusTier?: PlusTier["tier"]; + plusSkippedForSeasonNth: number | null; /** Plus tier player is on track to join */ pendingPlusTier?: PlusTier["tier"]; placementRank: number; diff --git a/db-test.sqlite3 b/db-test.sqlite3 index e10026d0e..9948065ce 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/migrations/042-plus-skipped.js b/migrations/042-plus-skipped.js new file mode 100644 index 000000000..21b0891c3 --- /dev/null +++ b/migrations/042-plus-skipped.js @@ -0,0 +1,5 @@ +module.exports.up = function (db) { + db.prepare( + /* sql */ `alter table "User" add "plusSkippedForSeasonNth" integer`, + ).run(); +}; diff --git a/package.json b/package.json index ce9db5ab9..cf4d61789 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "delete-user": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/delete-user.ts", "ban-user": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/ban-user.ts", "delete-skill": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/delete-skill.ts", + "skip-plus": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/skip-plus.ts", "season-initial-powers": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/season-initial-powers.ts", "refresh-prod-db": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/refresh-prod-db.ts && npm run migrate up", "lint:ts": "eslint . --ext .ts,.tsx", diff --git a/scripts/skip-plus.ts b/scripts/skip-plus.ts new file mode 100644 index 000000000..ae3b9d9a2 --- /dev/null +++ b/scripts/skip-plus.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-console */ +import "dotenv/config"; +import invariant from "tiny-invariant"; +import { sql } from "~/db/sql"; +import { currentOrPreviousSeason } from "~/features/mmr/season"; + +const discordId = process.argv[2]?.trim(); + +invariant(discordId, "discord id is required (argument 1)"); + +const currentSeasonNth = currentOrPreviousSeason(new Date())?.nth; + +invariant(currentSeasonNth, "current season nth is required"); + +sql + .prepare( + 'update "User" set plusSkippedForSeasonNth = @plusSkippedForSeasonNth where discordId = @discordId', + ) + .run({ discordId, plusSkippedForSeasonNth: currentSeasonNth }); + +console.log( + `Plus Server admission will be skipped for Discord ID: ${discordId} (season ${currentSeasonNth})`, +);