mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-27 21:58:08 -05:00
Simplify comments
This commit is contained in:
@@ -60,10 +60,7 @@ export interface BenchmarkCase {
|
||||
run: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full benchmark case registry from resolved fixtures. Cases whose
|
||||
* fixture resolved to null are returned in `skipped` instead.
|
||||
*/
|
||||
/** Builds the benchmark case registry from resolved fixtures; cases whose fixture resolved to null go to `skipped`. */
|
||||
export function buildCases(fx: Fixtures): {
|
||||
cases: BenchmarkCase[];
|
||||
skipped: string[];
|
||||
@@ -87,7 +84,6 @@ export function buildCases(fx: Fixtures): {
|
||||
cases.push({ name, run: async () => run() });
|
||||
}
|
||||
|
||||
// AdminRepository
|
||||
addStatic("AdminRepository.findAllBannedUsers", () =>
|
||||
AdminRepository.findAllBannedUsers(),
|
||||
);
|
||||
@@ -95,7 +91,6 @@ export function buildCases(fx: Fixtures): {
|
||||
AdminRepository.findModNoteById(id),
|
||||
);
|
||||
|
||||
// ExternalStreamRepository
|
||||
addStatic("ExternalStreamRepository.findAll", () =>
|
||||
ExternalStreamRepository.findAll(),
|
||||
);
|
||||
@@ -103,7 +98,6 @@ export function buildCases(fx: Fixtures): {
|
||||
ExternalStreamRepository.findAllForSidebar(),
|
||||
);
|
||||
|
||||
// ApiRepository
|
||||
add("ApiRepository.findTokenByUserId", fx.apiTokenUserId, (userId) =>
|
||||
ApiRepository.findTokenByUserId(userId, "read"),
|
||||
);
|
||||
@@ -111,7 +105,6 @@ export function buildCases(fx: Fixtures): {
|
||||
ApiRepository.findAllApiTokens(),
|
||||
);
|
||||
|
||||
// ArtRepository
|
||||
addStatic("ArtRepository.findShowcaseArts", () =>
|
||||
ArtRepository.findShowcaseArts(),
|
||||
);
|
||||
@@ -129,7 +122,6 @@ export function buildCases(fx: Fixtures): {
|
||||
ArtRepository.findById(artId),
|
||||
);
|
||||
|
||||
// AssociationRepository
|
||||
add("AssociationRepository.findById", fx.heavyAssociation, (association) =>
|
||||
AssociationRepository.findById(association.id, { withMembers: true }),
|
||||
);
|
||||
@@ -155,12 +147,10 @@ export function buildCases(fx: Fixtures): {
|
||||
(association) => AssociationRepository.findInviteCodeById(association.id),
|
||||
);
|
||||
|
||||
// LogInLinkRepository
|
||||
add("LogInLinkRepository.findValidByCode", fx.logInLinkCode, (code) =>
|
||||
LogInLinkRepository.findValidByCode(code),
|
||||
);
|
||||
|
||||
// AvailabilityRepository
|
||||
add(
|
||||
"AvailabilityRepository.findAllWeeksByUserIds",
|
||||
both(fx.manyUserIds, fx.availabilityWindow),
|
||||
@@ -220,7 +210,6 @@ export function buildCases(fx: Fixtures): {
|
||||
AvailabilityRepository.findTeamEventById(id),
|
||||
);
|
||||
|
||||
// BadgeRepository
|
||||
addStatic("BadgeRepository.findAll", () => BadgeRepository.findAll());
|
||||
add("BadgeRepository.findById", fx.heavyBadgeId, (badgeId) =>
|
||||
BadgeRepository.findById(badgeId),
|
||||
@@ -238,7 +227,6 @@ export function buildCases(fx: Fixtures): {
|
||||
BadgeRepository.findByAuthorUserId(userId),
|
||||
);
|
||||
|
||||
// BuildRepository
|
||||
add("BuildRepository.findAllByUserId", fx.heavyBuildUserId, (userId) =>
|
||||
BuildRepository.findAllByUserId(userId, {
|
||||
showPrivate: true,
|
||||
@@ -269,7 +257,6 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// CalendarRepository
|
||||
add(
|
||||
"CalendarRepository.findAllBetweenTwoTimestamps",
|
||||
fx.calendarWindow,
|
||||
@@ -297,7 +284,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(eventId) => CalendarRepository.findTopThreeResultsByEventIds([eventId]),
|
||||
);
|
||||
|
||||
// ChatRepository
|
||||
add("ChatRepository.findAllRoomsByIds", fx.heavyChatUsers, (chatUsers) =>
|
||||
ChatRepository.findAllRoomsByIds(chatUsers.busiest.openRoomIds),
|
||||
);
|
||||
@@ -329,7 +315,6 @@ export function buildCases(fx: Fixtures): {
|
||||
),
|
||||
);
|
||||
|
||||
// ChatRoomResolver
|
||||
// a tournament match room is the costliest to resolve: its owner join carries
|
||||
// the team members and the tournament's organizer permissions on top
|
||||
add(
|
||||
@@ -341,7 +326,6 @@ export function buildCases(fx: Fixtures): {
|
||||
ChatRoomResolver.findAllByUserId(chatUsers.busiest.id),
|
||||
);
|
||||
|
||||
// FriendRepository
|
||||
add("FriendRepository.findByUserIdWithActivity", fx.heavyFriendPair, (pair) =>
|
||||
FriendRepository.findByUserIdWithActivity(pair.userId),
|
||||
);
|
||||
@@ -391,7 +375,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(request) => FriendRepository.findFriendRequestByIdAndReceiver(request),
|
||||
);
|
||||
|
||||
// ImageRepository
|
||||
add("ImageRepository.findById", fx.imageId, (imageId) =>
|
||||
ImageRepository.findById(imageId),
|
||||
);
|
||||
@@ -410,7 +393,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(userId) => ImageRepository.countUnvalidatedBySubmitterUserId(userId),
|
||||
);
|
||||
|
||||
// LeaderboardRepository
|
||||
add("LeaderboardRepository.findTeamLeaderboardBySeason", fx.sq, (sq) =>
|
||||
LeaderboardRepository.findTeamLeaderboardBySeason({
|
||||
season: sq.season,
|
||||
@@ -441,7 +423,6 @@ export function buildCases(fx: Fixtures): {
|
||||
LeaderboardRepository.findSeasonPopularUsersWeapon(sq.season),
|
||||
);
|
||||
|
||||
// LFGRepository
|
||||
addStatic("LFGRepository.findAllPosts.anon", () =>
|
||||
LFGRepository.findAllPosts(),
|
||||
);
|
||||
@@ -452,17 +433,14 @@ export function buildCases(fx: Fixtures): {
|
||||
LFGRepository.findByAuthorUserId(authorId),
|
||||
);
|
||||
|
||||
// LiveStreamRepository
|
||||
addStatic("LiveStreamRepository.findXRankStreams", () =>
|
||||
LiveStreamRepository.findXRankStreams(),
|
||||
);
|
||||
|
||||
// MatchProfileRepository
|
||||
add("MatchProfileRepository.findSettingsByUserId", fx.heavyUser, (user) =>
|
||||
MatchProfileRepository.findSettingsByUserId(user.id),
|
||||
);
|
||||
|
||||
// SkillRepository (app/features/mmr)
|
||||
add("SkillRepository.findCurrentUserSkills", fx.skillBatch, (skillBatch) =>
|
||||
SkillRepository.findCurrentUserSkills({
|
||||
season: skillBatch.season,
|
||||
@@ -497,7 +475,6 @@ export function buildCases(fx: Fixtures): {
|
||||
SkillRepository.findSeasonActiveDaysByUserId(sq),
|
||||
);
|
||||
|
||||
// NotificationRepository
|
||||
add("NotificationRepository.findByUserId", fx.notification, (notification) =>
|
||||
NotificationRepository.findByUserId(notification.userId, { limit: 50 }),
|
||||
);
|
||||
@@ -510,7 +487,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(userIds) => NotificationRepository.findAllSubscriptionsByUserIds(userIds),
|
||||
);
|
||||
|
||||
// PlusSuggestionRepository
|
||||
add(
|
||||
"PlusSuggestionRepository.findAllByMonth",
|
||||
fx.plusSuggestionMonthYear,
|
||||
@@ -530,7 +506,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(args) => PlusSuggestionRepository.findMonthSummary(args),
|
||||
);
|
||||
|
||||
// PlusVotingRepository
|
||||
addStatic("PlusVotingRepository.findAllPlusTiersFromLatestVoting", () =>
|
||||
PlusVotingRepository.findAllPlusTiersFromLatestVoting(),
|
||||
);
|
||||
@@ -550,7 +525,6 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// ScannerIngestRepository
|
||||
add(
|
||||
"ScannerIngestRepository.gamesPlayedByUserInTournament",
|
||||
fx.scannerIngest,
|
||||
@@ -637,7 +611,6 @@ export function buildCases(fx: Fixtures): {
|
||||
ScannerIngestRepository.gamesInTournamentMatch(tournamentMatchId),
|
||||
);
|
||||
|
||||
// ScrimMapListRepository
|
||||
add(
|
||||
"ScrimMapListRepository.findMapListsByScrimPostId",
|
||||
fx.heavyScrimPostId,
|
||||
@@ -645,14 +618,12 @@ export function buildCases(fx: Fixtures): {
|
||||
ScrimMapListRepository.findMapListsByScrimPostId(scrimPostId),
|
||||
);
|
||||
|
||||
// ScrimMapRepository
|
||||
add(
|
||||
"ScrimMapRepository.findMapsByScrimPostId",
|
||||
fx.heavyScrimPostId,
|
||||
(scrimPostId) => ScrimMapRepository.findMapsByScrimPostId(scrimPostId),
|
||||
);
|
||||
|
||||
// ScrimPostRepository
|
||||
add(
|
||||
"ScrimPostRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SCRIM ?? null,
|
||||
@@ -699,14 +670,12 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// GroupMatchContinueVoteRepository
|
||||
add(
|
||||
"GroupMatchContinueVoteRepository.findAllByGroupIds",
|
||||
fx.heavyGroupIds,
|
||||
(groupIds) => GroupMatchContinueVoteRepository.findAllByGroupIds(groupIds),
|
||||
);
|
||||
|
||||
// PlayerStatRepository
|
||||
add("PlayerStatRepository.findSeasonMapWinrateByUserId", fx.sq, (sq) =>
|
||||
PlayerStatRepository.findSeasonMapWinrateByUserId(sq),
|
||||
);
|
||||
@@ -732,7 +701,6 @@ export function buildCases(fx: Fixtures): {
|
||||
PlayerStatRepository.findSeasonTournamentRunsByUserId(sq),
|
||||
);
|
||||
|
||||
// ReportedWeaponRepository
|
||||
add(
|
||||
"ReportedWeaponRepository.findByMatchId",
|
||||
fx.heavyGroupMatchId,
|
||||
@@ -760,7 +728,6 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// SQMatchRepository
|
||||
add(
|
||||
"SQMatchRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SQ_MATCH ?? null,
|
||||
@@ -793,12 +760,11 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// QStreamsRepository
|
||||
addStatic("QStreamsRepository.findAllActiveMatchPlayers", () =>
|
||||
QStreamsRepository.findAllActiveMatchPlayers(),
|
||||
);
|
||||
|
||||
// PrivateUserNoteRepository (relies on the benchmark's actor context)
|
||||
// relies on the benchmark's actor context
|
||||
addStatic("PrivateUserNoteRepository.findAllOwn.all", () =>
|
||||
PrivateUserNoteRepository.findAllOwn(),
|
||||
);
|
||||
@@ -808,7 +774,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(userIds) => PrivateUserNoteRepository.findAllOwn(userIds),
|
||||
);
|
||||
|
||||
// SQGroupRepository
|
||||
add(
|
||||
"SQGroupRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.SQ_GROUP ?? null,
|
||||
@@ -858,12 +823,10 @@ export function buildCases(fx: Fixtures): {
|
||||
SQGroupRepository.findRecentlyFinishedMatches(),
|
||||
);
|
||||
|
||||
// SplatoonRotationRepository
|
||||
addStatic("SplatoonRotationRepository.findAll", () =>
|
||||
SplatoonRotationRepository.findAll(),
|
||||
);
|
||||
|
||||
// TeamRepository
|
||||
addStatic("TeamRepository.searchByName", () =>
|
||||
TeamRepository.searchByName(SEARCH_QUERY),
|
||||
);
|
||||
@@ -888,7 +851,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TeamRepository.findAllByMemberUserId(team.memberUserId),
|
||||
);
|
||||
|
||||
// XRankPlacementRepository
|
||||
add("XRankPlacementRepository.isPlayerLinkedByUserId", fx.xrank, (xrank) =>
|
||||
XRankPlacementRepository.isPlayerLinkedByUserId(xrank.userId),
|
||||
);
|
||||
@@ -919,14 +881,12 @@ export function buildCases(fx: Fixtures): {
|
||||
XRankPlacementRepository.findPeaksByUserId(xrank.userId, "both"),
|
||||
);
|
||||
|
||||
// BracketRepository
|
||||
add(
|
||||
"BracketRepository.findByTournamentId",
|
||||
fx.heaviestBracketTournamentId,
|
||||
(tournamentId) => BracketRepository.findByTournamentId(tournamentId),
|
||||
);
|
||||
|
||||
// TournamentMatchVodRepository
|
||||
add(
|
||||
"TournamentMatchVodRepository.findVodsByTournamentId",
|
||||
fx.heavyTournamentId,
|
||||
@@ -957,7 +917,6 @@ export function buildCases(fx: Fixtures): {
|
||||
),
|
||||
);
|
||||
|
||||
// TournamentLFGRepository
|
||||
add(
|
||||
"TournamentLFGRepository.findLookingTeamsByTournamentId",
|
||||
fx.lfgTournament,
|
||||
@@ -977,7 +936,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TournamentLFGRepository.findAllSubsByTournamentId(lfg.tournamentId),
|
||||
);
|
||||
|
||||
// TournamentMatchRepository
|
||||
add(
|
||||
"TournamentMatchRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.TOURNAMENT_MATCH ?? null,
|
||||
@@ -1019,7 +977,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TournamentMatchRepository.findByTournamentTeamId(tournamentTeamId),
|
||||
);
|
||||
|
||||
// TournamentOrganizationRepository
|
||||
add("TournamentOrganizationRepository.findBySlug", fx.heavyOrg, (org) =>
|
||||
TournamentOrganizationRepository.findBySlug(org.slug),
|
||||
);
|
||||
@@ -1109,7 +1066,6 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// SavedCalendarEventRepository
|
||||
add(
|
||||
"SavedCalendarEventRepository.isSaved",
|
||||
both(fx.heavyUser, fx.heavyTournamentId),
|
||||
@@ -1128,7 +1084,6 @@ export function buildCases(fx: Fixtures): {
|
||||
(user) => SavedCalendarEventRepository.findAllUpcomingByUserId(user.id),
|
||||
);
|
||||
|
||||
// TournamentAuditLogRepository
|
||||
add(
|
||||
"TournamentAuditLogRepository.findByTournamentId",
|
||||
fx.auditTournamentId,
|
||||
@@ -1152,7 +1107,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TournamentAuditLogRepository.findTeamsByTournamentId(tournamentId),
|
||||
);
|
||||
|
||||
// TournamentRepository
|
||||
add(
|
||||
"TournamentRepository.findOrganizerPermissionsByTournamentIds",
|
||||
fx.recentTournamentIds,
|
||||
@@ -1269,7 +1223,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TournamentRepository.findRunningTournamentIds(),
|
||||
);
|
||||
|
||||
// TournamentTeamRepository
|
||||
add(
|
||||
"TournamentTeamRepository.findAllByChatRoomIds",
|
||||
fx.openChatRoomIdsByType?.TOURNAMENT_TEAM ?? null,
|
||||
@@ -1325,7 +1278,6 @@ export function buildCases(fx: Fixtures): {
|
||||
}),
|
||||
);
|
||||
|
||||
// TrophyRepository
|
||||
addStatic("TrophyRepository.all", () => TrophyRepository.all());
|
||||
add("TrophyRepository.findById", fx.trophy, (trophy) =>
|
||||
TrophyRepository.findById(trophy.heavyTrophyId),
|
||||
@@ -1340,7 +1292,6 @@ export function buildCases(fx: Fixtures): {
|
||||
TrophyRepository.findWinsByOwner(trophy.wins),
|
||||
);
|
||||
|
||||
// UserCardRepository
|
||||
add("UserCardRepository.findAllByUserIds", fx.manyUserIds, (userIds) =>
|
||||
UserCardRepository.findAllByUserIds({
|
||||
userIds,
|
||||
@@ -1358,7 +1309,6 @@ export function buildCases(fx: Fixtures): {
|
||||
UserCardRepository.findVerifiedXpByUserId(user.id, null),
|
||||
);
|
||||
|
||||
// UserRepository
|
||||
add("UserRepository.findIdByIdentifier", fx.heavyUser, (user) =>
|
||||
UserRepository.findIdByIdentifier(user.identifier),
|
||||
);
|
||||
@@ -1463,7 +1413,6 @@ export function buildCases(fx: Fixtures): {
|
||||
UserRepository.findWeaponPoolByUserId(user.id),
|
||||
);
|
||||
|
||||
// VodRepository
|
||||
add("VodRepository.findByUserId", fx.vod, (vod) =>
|
||||
VodRepository.findByUserId(vod.userId),
|
||||
);
|
||||
|
||||
@@ -137,10 +137,8 @@ export interface Fixtures {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves "worst-case" benchmark arguments by sampling the heaviest rows from
|
||||
* the database (user with most tournament results, tournament with most teams
|
||||
* and so on). Fields resolve to null when the source table is empty which
|
||||
* causes the dependent benchmark cases to be skipped.
|
||||
* Resolves worst-case benchmark arguments by sampling the heaviest rows (user with most
|
||||
* results, tournament with most teams...). Fields resolve to null when the source table is empty, skipping their cases.
|
||||
*/
|
||||
export async function resolveFixtures(): Promise<Fixtures> {
|
||||
const heavyUser = await resolveHeavyUser();
|
||||
|
||||
@@ -12,10 +12,8 @@ const dontWrite = process.argv.includes(NO_WRITE_KEY);
|
||||
|
||||
const KNOWN_SUFFIXES = ["_zero", "_one", "_two", "_few", "_many", "_other"];
|
||||
|
||||
// `_zero` is an i18next `count === 0` override, not a CLDR plural category. It
|
||||
// may legitimately sit alongside the bare singular key that single-plural
|
||||
// languages (zh, ja, ko) use after `i18n:sync` collapses their plural forms, so
|
||||
// unlike the real plural suffixes it never counts as a with/without-suffix clash.
|
||||
// `_zero` is an i18next `count === 0` override, not a CLDR category: it legitimately sits beside the
|
||||
// bare singular key single-plural languages (zh, ja, ko) use, so it never counts as a suffix clash
|
||||
const ZERO_SUFFIX = "_zero";
|
||||
const PLURAL_SUFFIXES = KNOWN_SUFFIXES.filter((sfx) => sfx !== ZERO_SUFFIX);
|
||||
|
||||
@@ -188,7 +186,7 @@ function validateNoDuplicateKeys({
|
||||
}
|
||||
}
|
||||
|
||||
// get keys while respecting different plural/context key suffixes in different languages.
|
||||
// keys with their language-specific plural/context suffixes stripped
|
||||
function getKeysWithoutSuffix(
|
||||
translations: Record<string, string>,
|
||||
lang: string,
|
||||
|
||||
@@ -9,31 +9,22 @@ const __dirname = path.dirname(__filename);
|
||||
const LOCALES_PATH = path.join(__dirname, "..", "locales");
|
||||
const PRIMARY_LANGUAGE = "en";
|
||||
|
||||
// When passed, no files are written. Instead the script exits non-zero if any
|
||||
// file would be collapsed, so CI can fail a PR that committed suffixed plural
|
||||
// keys in a single-plural language without running `pnpm run i18n:sync`.
|
||||
// writes nothing and exits non-zero if any file would be collapsed, so CI can
|
||||
// fail a PR that committed suffixed plural keys without running `pnpm run i18n:sync`
|
||||
const CHECK_KEY = "--check";
|
||||
const checkOnly = process.argv.includes(CHECK_KEY);
|
||||
|
||||
// `_zero` is intentionally excluded: it is not a CLDR plural category but an
|
||||
// i18next special-case override for `count === 0`. `i18next-locales-sync` treats
|
||||
// e.g. `foo_zero` as a plain key (English has no `_zero` plural form), so it is
|
||||
// preserved across every language and must not be collapsed into `foo`.
|
||||
// `_zero` is excluded: an i18next `count === 0` override rather than a CLDR category, which
|
||||
// `i18next-locales-sync` treats as a plain key preserved across every language
|
||||
const COLLAPSIBLE_PLURAL_SUFFIXES = ["_one", "_two", "_few", "_many", "_other"];
|
||||
|
||||
// The plural form whose value we keep when collapsing. `_other` is preferred
|
||||
// because it is the form i18next resolves at runtime for these languages and it
|
||||
// typically carries the `{{count}}` interpolation. The remaining forms are only
|
||||
// used as a fallback when `_other` has no translated value.
|
||||
// the form i18next resolves at runtime for these languages, typically carrying `{{count}}`;
|
||||
// the other forms are only a fallback when it has no translated value
|
||||
const PREFERRED_SUFFIX = "_other";
|
||||
|
||||
// `i18next-locales-sync` stores plural keys for languages whose CLDR cardinal
|
||||
// rule has a single category ("other" only, e.g. zh, ja, ko) under the bare
|
||||
// singular key instead of suffixed `_one`/`_other` keys. When a translator adds
|
||||
// a value under a suffixed key for one of these languages, the sync tool can't
|
||||
// find the singular key it expects and silently drops the translation. We run
|
||||
// this before the sync to collapse those suffixed keys into the singular key,
|
||||
// preserving the (non-empty) value so it survives the sync.
|
||||
// `i18next-locales-sync` keys single-category languages (zh, ja, ko) by the bare singular key;
|
||||
// a translation a translator added under a suffixed key is silently dropped by the sync unless
|
||||
// collapsed into the singular key first, which is what this script does
|
||||
const languages = fs
|
||||
.readdirSync(LOCALES_PATH)
|
||||
.filter((lang) => lang !== PRIMARY_LANGUAGE && !lang.startsWith("."));
|
||||
@@ -103,8 +94,7 @@ function collapsePluralKeys(content: Record<string, string>) {
|
||||
collapsedBaseKeys.add(baseKey);
|
||||
const isPreferred = suffix === PREFERRED_SUFFIX;
|
||||
|
||||
// prefer the `_other` value (it carries {{count}}); otherwise keep the
|
||||
// first non-empty value found across the remaining plural forms
|
||||
// `_other` wins; otherwise the first non-empty value of the remaining forms
|
||||
const shouldReplace =
|
||||
!(baseKey in collapsed) ||
|
||||
(!collapsed[baseKey] && !!value) ||
|
||||
|
||||
@@ -695,20 +695,16 @@ function parametersToSubWeaponResult(
|
||||
};
|
||||
}
|
||||
|
||||
// Thrown targeting specials (Booyah Bomb, Super Chump, Triple Inkstrike, Ultra Stamp) are
|
||||
// player-aimed and have no throw-distance param in the game data, so their throw (fly) range is
|
||||
// approximated with a constant. Kept as named per-special constants so each can be tuned later;
|
||||
// for now they all reuse Booyah Bomb's value.
|
||||
// Thrown targeting specials (Booyah Bomb, Super Chump, Triple Inkstrike, Ultra Stamp) are player-aimed
|
||||
// with no throw-distance param, so their fly range is a per-special constant, all Booyah Bomb's value for now.
|
||||
const BOOYAH_BOMB_FLY_DISTANCE = 27;
|
||||
const SUPER_CHUMP_FLY_DISTANCE = 30;
|
||||
const TRIPLE_INKSTRIKE_FLY_DISTANCE = 28;
|
||||
const ULTRA_STAMP_FLY_DISTANCE = 24;
|
||||
|
||||
// Range circle data for the map planner. Projectile / thrown specials store the distance the shot
|
||||
// travels plus its blast; a few store a single effect radius. Specials whose reach is global
|
||||
// (Tenta Missiles, Killer Wail), placed or utility (Big Bubbler, Tacticooler, Ink Storm, Ink Vac),
|
||||
// or otherwise has no meaningful circle (Kraken Royale, Splattercolor Screen) are left out.
|
||||
// See app/features/comp-analyzer/core/special-weapon-range.ts.
|
||||
// Range circles for the map planner (see app/features/comp-analyzer/core/special-weapon-range.ts):
|
||||
// travel distance plus blast, or a single effect radius. Global (Tenta Missiles, Killer Wail), placed/utility
|
||||
// (Big Bubbler, Tacticooler, Ink Storm, Ink Vac) and circle-less (Kraken Royale, Splattercolor Screen) specials are left out.
|
||||
function specialWeaponRangeParams(
|
||||
specialWeapon: SpecialWeapon,
|
||||
rawParams: any,
|
||||
@@ -785,8 +781,7 @@ function specialWeaponRangeParams(
|
||||
}
|
||||
}
|
||||
|
||||
// some specials lack damage values in the params
|
||||
// so they are instead hardcoded here as a workaround
|
||||
// some specials lack damage values in the params, so they are hardcoded here
|
||||
function parametersToSpecialWeaponResult(params: any) {
|
||||
const result: any = {};
|
||||
|
||||
|
||||
@@ -114,9 +114,8 @@ const specialIdsForRow = (rowKey: string) =>
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolves the per-target damage rate of every damage rate info row in a single config dump.
|
||||
* Only the PvP-relevant receivers are kept and the synthetic launched/Recycled Brella canopy
|
||||
* targets are derived the same way the live object damage calculator expects them.
|
||||
* Per-target damage rates of every damage rate info row in one config dump, PvP-relevant receivers
|
||||
* only, with the synthetic launched/Recycled Brella canopy targets derived as the calculator expects.
|
||||
*/
|
||||
const damageRatesByRow = (
|
||||
config: DamageRateConfig,
|
||||
@@ -190,9 +189,8 @@ function versionDirToDisplay(version: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the per-row, per-target damage rate history across every versioned config dump and
|
||||
* writes it for the params page to surface in its patch history. Only PvP-relevant rows and
|
||||
* only targets whose rate actually changed at some point are kept, so the output stays small.
|
||||
* Writes the per-row, per-target damage rate history across every versioned dump for the params
|
||||
* page's patch history. Only PvP-relevant rows and targets whose rate ever changed are kept.
|
||||
*/
|
||||
function writeDamageRateHistory() {
|
||||
const versionDirs = fs
|
||||
|
||||
@@ -15,23 +15,17 @@ const TEST_DB_PATH = path.join(ROOT_DIR, "db-test.sqlite3");
|
||||
export function setup() {
|
||||
ensureMigratedDb(TEST_DB_PATH);
|
||||
|
||||
// Test workers only ever read this file, to copy its schema into their own
|
||||
// in-memory database. Taking it out of WAL drops the -wal/-shm sidecars, so
|
||||
// those concurrent opens need no write access and cannot contend.
|
||||
// test workers only read this file to copy its schema; out of WAL there are no
|
||||
// -wal/-shm sidecars, so their concurrent opens need no write access
|
||||
const db = new DatabaseSync(TEST_DB_PATH);
|
||||
db.exec("PRAGMA journal_mode = DELETE");
|
||||
db.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the SQLite file at `dbPath` has every migration applied: creates it
|
||||
* if missing, applies pending migrations, and rebuilds it from scratch if an
|
||||
* already applied migration no longer exists on disk or its contents changed
|
||||
* since it was applied.
|
||||
*
|
||||
* Rebuilding on changed contents matters because a branch edits the migration
|
||||
* it added rather than stacking a new one, so the file name kysely tracks stays
|
||||
* the same while the schema it produces does not.
|
||||
* Creates `dbPath` if missing, applies pending migrations, and rebuilds it from scratch if an
|
||||
* applied migration disappeared or changed contents: a branch edits the migration it added
|
||||
* rather than stacking a new one, so the file name kysely tracks stays the same while the schema does not.
|
||||
*/
|
||||
export function ensureMigratedDb(dbPath: string) {
|
||||
const resolvedPath = path.resolve(ROOT_DIR, dbPath);
|
||||
|
||||
@@ -120,10 +120,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The screenshotted page is whatever dev server holds the port, which can be another
|
||||
* checkout of the repo whose entries are not the ones being shipped.
|
||||
*/
|
||||
/** The dev server holding the port can be another checkout whose entries are not the ones being shipped. */
|
||||
async function assertSameCheckout(page: Page) {
|
||||
const marker = page.locator("[data-changelog-head]");
|
||||
const pageHead =
|
||||
|
||||
@@ -37,8 +37,7 @@ async function main() {
|
||||
});
|
||||
|
||||
await page.goto(OG_IMAGES_PAGE_URL, { waitUntil: "networkidle" });
|
||||
// the page previews the canvases scaled down and inside the site layout, the
|
||||
// one being captured is blown up to full size and lifted on top of everything
|
||||
// the page previews the canvases scaled down; the captured one is blown up to full size on top of everything
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
* { --og-scale: 1 !important }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// calculates org stats for the last 6 finished months
|
||||
// used to decide which orgs are considered "established"
|
||||
// you need at least 150 average monthly active players to be considered established
|
||||
// if you drop below 100 you lose it
|
||||
// org stats for the last 6 finished months, deciding which orgs are "established":
|
||||
// 150 average monthly active players to gain it, dropping below 100 loses it
|
||||
|
||||
import { db } from "~/db/sql";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
@@ -18,22 +18,18 @@ function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// delete db-prod.sqlite3-shm file if exists
|
||||
if (fs.existsSync(dbProdShmPath)) {
|
||||
fs.unlinkSync(dbProdShmPath);
|
||||
}
|
||||
|
||||
// delete db-prod.sqlite3-wal file if exists
|
||||
if (fs.existsSync(dbProdWalPath)) {
|
||||
fs.unlinkSync(dbProdWalPath);
|
||||
}
|
||||
|
||||
// delete db-prod.sqlite3 if exists
|
||||
if (fs.existsSync(dbProdPath)) {
|
||||
fs.unlinkSync(dbProdPath);
|
||||
}
|
||||
|
||||
// copy db-copy.sqlite3 to db-prod.sqlite3
|
||||
fs.copyFileSync(dbCopyPath, dbProdPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Bootstrap glyph atlases from the labeled reference fixtures: slices glyph
|
||||
* templates straight out of the reference frames, since the same scoreboard
|
||||
* captured through different pipelines (OBS virtual camera 720p, game
|
||||
* capture 1080p) yields subtly different pixels — every source contributes
|
||||
* its own crop per character and recognition takes the best-scoring one.
|
||||
* build-glyph-atlas.ts then fills in the rest of the charset from the
|
||||
* fonts, preserving these fixture-tagged glyphs.
|
||||
* Bootstraps glyph atlases from labeled reference fixtures: slices templates
|
||||
* straight out of the frames, since the same scoreboard captured through
|
||||
* different pipelines (OBS virtual camera 720p, game capture 1080p) yields
|
||||
* subtly different pixels — every source contributes its own crop per char and
|
||||
* recognition takes the best-scoring one. build-glyph-atlas.ts then fills the
|
||||
* rest of the charset from the fonts, preserving these fixture-tagged glyphs.
|
||||
*
|
||||
* Usage: pnpm scanner:bootstrap-atlas
|
||||
* Writes SCANNER_ASSETS_DIR/glyphs/scoreboard-{names,paint-digits,stat-digits}.{png,json}
|
||||
* Writes SCANNER_ASSETS_DIR/glyphs/*.{png,json}
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -61,9 +60,8 @@ interface Source {
|
||||
stats: string[][];
|
||||
teamScores: string[];
|
||||
/**
|
||||
* Split hints are absolute x positions where merged segments must be cut
|
||||
* (keyed by row index) — e.g. the "Te" in Teddy renders as one segment,
|
||||
* with the exact boundary shifting a couple of pixels between sources.
|
||||
* absolute x positions where merged segments must be cut, keyed by row —
|
||||
* e.g. "Te" in Teddy renders as one segment, boundary shifting a few px per source
|
||||
*/
|
||||
nameSplitHints: Record<number, number[]>;
|
||||
/** omit to skip header harvest for this source */
|
||||
@@ -94,10 +92,9 @@ const REFERENCE = {
|
||||
["05", "02", "04"],
|
||||
],
|
||||
/**
|
||||
* Team totals render with an outline on the colored team box, so they get
|
||||
* their own atlas rather than reusing paint digits. This fixture only
|
||||
* contributes '5' and '0' — later fixtures extend the set (harvest keeps
|
||||
* the first instance per char, so rerunning with more fixtures is additive).
|
||||
* Team totals render outlined on the colored team box, so they get their own
|
||||
* atlas. This fixture only contributes '5' and '0'; harvest keeps the first
|
||||
* instance per char, so rerunning with more fixtures is additive.
|
||||
*/
|
||||
teamScores: ["500", "0"],
|
||||
header: {
|
||||
@@ -154,11 +151,10 @@ const SOURCES: Source[] = [
|
||||
nameSplitHints: {},
|
||||
},
|
||||
{
|
||||
// This capture renders names slightly smaller than the reference: its
|
||||
// 'T' (9x15) loses to the taller reference crops, and its baseline dots
|
||||
// are pure homoglyphs the font templates can't split — '.' renders 4px,
|
||||
// '・' 5px, so exact crops separate them via the ink-coverage penalty.
|
||||
// Only the dot/T rows are labeled; the rest add nothing new.
|
||||
// Renders names slightly smaller than the reference: its 'T' (9x15) loses
|
||||
// to the taller reference crops, and its baseline dots are homoglyphs the
|
||||
// font templates can't split ('.' 4px, '・' 5px; exact crops separate them
|
||||
// via the ink-coverage penalty). Only the dot/T rows are labeled.
|
||||
frame: "scoreboard/robot/frame.png",
|
||||
names: ["R.O.B.O.T", "", "", "", "", "Rαι×ι..・", "", ""],
|
||||
paints: ["", "", "", "", "", "", "", ""],
|
||||
@@ -239,10 +235,9 @@ async function loadGreen(framePath: string): Promise<Mat> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Death splash-tag name band, prepared the way the detector reads it
|
||||
* (see src/core/detectors/death/index.ts): crop the tilted tag, rotate it
|
||||
* level, crop the name band, then map each pixel to its max-channel
|
||||
* distance from the median banner color, normalized to 0-255.
|
||||
* Death splash-tag name band, prepared the way death/index.ts reads it: crop
|
||||
* the tilted tag, rotate it level, crop the name band, then map each pixel to
|
||||
* its max-channel distance from the median banner color, normalized to 0-255.
|
||||
*/
|
||||
async function loadTagBand(framePath: string): Promise<Mat> {
|
||||
const srcMat = toMat(await readImage(join(FIXTURES_DIR, framePath)));
|
||||
@@ -321,11 +316,9 @@ async function loadTagBand(framePath: string): Promise<Mat> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero out ink components touching the band border, exactly as the
|
||||
* detector does before parsing (see clearBorderBlobs in death/index.ts):
|
||||
* banner art and title-line slivers enter at the band edges, and without
|
||||
* this they join the column runs (mismatching the label) or push a crop's
|
||||
* tight box past the band edge.
|
||||
* Zeroes ink components touching the band border, as the detector does before
|
||||
* parsing (clearBorderBlobs in death/index.ts): banner art and title-line
|
||||
* slivers at the edges would join the column runs or push a tight box past it.
|
||||
*/
|
||||
function clearBorderBlobs(band: Mat, threshold: number): void {
|
||||
const bin = new cv.Mat();
|
||||
@@ -384,9 +377,8 @@ function columnRuns(
|
||||
}
|
||||
}
|
||||
if (start >= 0) runs.push({ x0: start, x1: roi.x + roi.w });
|
||||
// Merge hints bridge runs that belong to one glyph (multi-stroke kana
|
||||
// like パ segments as two strokes): a hint x inside the gap between two
|
||||
// consecutive runs joins them into a single crop.
|
||||
// Merge hints bridge runs belonging to one glyph (multi-stroke kana like パ
|
||||
// segment as two strokes): a hint x inside the gap joins the two runs.
|
||||
for (const hint of mergeHints) {
|
||||
const i = runs.findIndex(
|
||||
(r, idx) =>
|
||||
@@ -460,10 +452,9 @@ function cropGlyph(
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a labeled text ROI into per-char crops. A Map collector keeps the
|
||||
* Slices a labeled text ROI into per-char crops. A Map collector keeps the
|
||||
* first instance per char (digits render identically everywhere); an array
|
||||
* collector keeps every instance — letters land on different subpixel
|
||||
* phases, so each occurrence is a distinct, equally-authoritative exemplar.
|
||||
* collector keeps every instance (letters land on different subpixel phases).
|
||||
*/
|
||||
function harvest(
|
||||
roi: Roi,
|
||||
@@ -621,8 +612,7 @@ for (const source of SOURCES) {
|
||||
}
|
||||
|
||||
// Replay-browser fixtures: the code line renders in FOT-RowdyStd, which no
|
||||
// live-scoreboard atlas covers. Letters land on different subpixel phases,
|
||||
// so keep every occurrence (array collector), like names.
|
||||
// live-scoreboard atlas covers. Every occurrence kept (array), like names.
|
||||
const REPLAY_SOURCES: { frame: string; code: string }[] = [
|
||||
{
|
||||
frame:
|
||||
@@ -639,10 +629,9 @@ const REPLAY_SOURCES: { frame: string; code: string }[] = [
|
||||
"scoreboard-battle-log-replay/private-battle-crableg-capital/frame.png",
|
||||
code: "R1V4-PAHW-GGM2-PD9S",
|
||||
},
|
||||
// Heavily compressed stream captures: font-rendered templates lose to
|
||||
// fixture crops of lookalikes on these (E beat a real F by 0.1+), so the
|
||||
// chars they cover (8 B F J N T U 5 among them) need crops at this
|
||||
// fidelity. brinewater-1411/marlin stay out as generalization checks.
|
||||
// Heavily compressed stream captures: font-rendered templates lose to fixture
|
||||
// crops of lookalikes here (E beat a real F by 0.1+), so the chars they cover
|
||||
// need crops at this fidelity. brinewater-1411/marlin stay out as generalization checks.
|
||||
{
|
||||
frame:
|
||||
"scoreboard-battle-log-replay/x-battle-rainmaker-brinewater-1404/frame.png",
|
||||
@@ -671,9 +660,8 @@ for (const source of REPLAY_SOURCES) {
|
||||
}
|
||||
|
||||
// Death splash-tag names (BlitzBold + Rowdy kana at ~46px). The atlas is
|
||||
// nominal 42 with the detector upscaling to 46 at load (see the builder
|
||||
// comment in scripts/scanner/build-glyph-atlas.ts), so native-size crops shrink to
|
||||
// 42 here and come back to native after the load-time upscale.
|
||||
// nominal 42 with the detector upscaling to 46 at load (see build-glyph-atlas.ts),
|
||||
// so native-size crops shrink to 42 here.
|
||||
const TAG_BIN_THRESHOLD = 160; // TAG_NAME_BIN_THRESHOLD in death/index.ts
|
||||
const TAG_ATLAS_HEIGHT = 42;
|
||||
const DEATH_TAG_SOURCES: {
|
||||
@@ -725,10 +713,9 @@ for (const source of DEATH_TAG_SOURCES) {
|
||||
}
|
||||
}
|
||||
|
||||
// JA death-message lines (condensed Kurokane/Rowdy blend at ~40px; the
|
||||
// atlas is native-size, matched unscaled by the detector's JA read path).
|
||||
// ROIs are fixture-specific tight boxes around each line so scene ink
|
||||
// outside the burst stays out of the runs.
|
||||
// JA death-message lines (condensed Kurokane/Rowdy blend at ~40px; native-size
|
||||
// atlas, matched unscaled by the JA read path). ROIs are fixture-specific tight
|
||||
// boxes around each line so scene ink outside the burst stays out of the runs.
|
||||
const DEATH_JA_BIN_THRESHOLD = 190; // SPLAT_TEXT_BIN_THRESHOLD in death/rois.ts
|
||||
const DEATH_JA_SOURCES: {
|
||||
frame: string;
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Build glyph atlases by rendering the Splatoon fonts at the exact
|
||||
* scoreboard render sizes, calibrated against the reference fixture:
|
||||
* Builds glyph atlases by rendering the Splatoon fonts at the exact scoreboard
|
||||
* render sizes, calibrated against the reference fixture:
|
||||
*
|
||||
* paint digits BlitzMain 34px (tight '0' height ~29px at 1080p)
|
||||
* stat digits BlitzMain 20px (~17px)
|
||||
* team digits BlitzBold 36px (~28px — team totals use the bold face,
|
||||
* which is why they can't reuse paint glyphs)
|
||||
* names BlitzMain 20px (cap height ~17px), full charset:
|
||||
* ASCII + Latin-1 + kana + common symbols (~330 glyphs)
|
||||
* team digits BlitzBold 36px (~28px — bold face, so paint glyphs can't be reused)
|
||||
* names BlitzMain 20px (cap height ~17px), ASCII + Latin-1 + kana + symbols
|
||||
*
|
||||
* The fonts ship with the game and are not committed — drop them into
|
||||
* assets/fonts/ (see README). Fixture-crop bootstrapping
|
||||
* (scripts/scanner/bootstrap-atlas-from-fixture.ts) remains as a cross-check and as
|
||||
* the fallback when the fonts are unavailable.
|
||||
* assets/fonts/ (see README); bootstrap-atlas-from-fixture.ts is the fallback.
|
||||
*
|
||||
* Usage: pnpm scanner:build-glyph-atlas
|
||||
* Writes SCANNER_ASSETS_DIR/glyphs/scoreboard-*.{png,json}
|
||||
@@ -39,11 +35,9 @@ import { readImage, writePng } from "../../app/features/scanner/node/image-io";
|
||||
import { readFontCoverage } from "./otf-cmap";
|
||||
|
||||
/**
|
||||
* Atlases are hybrids: glyphs harvested from labeled fixtures
|
||||
* (scripts/scanner/bootstrap-atlas-from-fixture.ts, tagged source:"fixture") are exact
|
||||
* in-game pixels and are preserved across rebuilds; font-rendered glyphs
|
||||
* fill in the rest of the charset. Recognition takes the best-scoring glyph,
|
||||
* so fixture crops dominate wherever they exist.
|
||||
* Atlases are hybrids: fixture-harvested glyphs (bootstrap-atlas-from-fixture.ts,
|
||||
* source:"fixture") are exact in-game pixels preserved across rebuilds;
|
||||
* font-rendered glyphs fill the rest of the charset and lose ties to them.
|
||||
*/
|
||||
|
||||
const FONTS_DIR = new URL("../../assets/fonts", import.meta.url).pathname;
|
||||
@@ -59,11 +53,9 @@ const FONT_FILES = {
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Which codepoints each font actually maps: canvas silently substitutes a
|
||||
* system font for the rest (the Blitz cuts have no kanji/hangul/hanzi), so
|
||||
* charsets are filtered through this before rendering — the CJK languages'
|
||||
* ideographs drop out of the localized charsets instead of baking
|
||||
* wrong-font glyphs into the atlases.
|
||||
* Which codepoints each font maps: canvas silently substitutes a system font
|
||||
* for the rest (the Blitz cuts have no kanji/hangul/hanzi), so charsets are
|
||||
* filtered through this so no wrong-font glyphs get baked into the atlases.
|
||||
*/
|
||||
const fontCoverage: Record<string, (codepoint: number) => boolean> = {};
|
||||
|
||||
@@ -100,35 +92,31 @@ function nameCharset(): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Greek letters players stylize scoreboard names with, added per attested
|
||||
* fixture need only ("Rιppιng_H"): most of the block are homoglyphs of latin
|
||||
* or kana at capture fidelity (η~n, ε~c, Γ~か strokes...) and displace
|
||||
* correct matches on ranking noise, so it is not included wholesale — and
|
||||
* even a few extra narrow glyphs shift an atlas's median width enough to
|
||||
* change wide-segment splitting, so it stays out of the death-tag charset
|
||||
* until a death fixture attests it.
|
||||
* Greek letters players stylize names with, added per attested fixture only
|
||||
* ("Rιppιng_H"): most of the block are homoglyphs of latin/kana at capture
|
||||
* fidelity (η~n, ε~c) and displace correct matches on ranking noise. Even a
|
||||
* few narrow glyphs shift an atlas's median width enough to change
|
||||
* wide-segment splitting, so it stays out of the death-tag charset until attested.
|
||||
*/
|
||||
const NAME_GREEK = "ια"; // ι: "Rιppιng_H", α: "◇Dαrz™" (special-symbols fixture)
|
||||
|
||||
/**
|
||||
* The rest of the in-game name editor's symbol pickers (sendou.ink's
|
||||
* IN_GAME_NAME_CHARACTER_CATEGORIES: "symbols" + "cjk-symbols"), minus what
|
||||
* nameCharset() already carries via ASCII/Latin-1/kana and minus chars the
|
||||
* Blitz cmap doesn't map (ˊˋ𝑓⁀⚪⚫◻◼⍑ — canvas would render a system-font
|
||||
* substitute; nameSymbols() re-checks at build time). "•" stays out too:
|
||||
* BlitzMain's own bullet is a 4px dot, the on-screen full-size circle comes
|
||||
* from "●" via RENDER_ALIASES. The tilde is the fullwidth "~" (U+FF5E) only
|
||||
* — the wave dash "〜" (U+301C) is a pixel-identical homoglyph that would
|
||||
* duel it on ranking noise, and FF5E is the form the fixture labels attest.
|
||||
* Like NAME_GREEK, scoreboard-names only (not death-tag) until attested.
|
||||
* IN_GAME_NAME_CHARACTER_CATEGORIES "symbols" + "cjk-symbols"), minus what
|
||||
* nameCharset() already carries and chars the Blitz cmap doesn't map
|
||||
* (ˊˋ𝑓⁀⚪⚫◻◼⍑; nameSymbols() re-checks at build time). "•" stays out: BlitzMain's
|
||||
* bullet is a 4px dot, the on-screen circle comes from "●" via RENDER_ALIASES.
|
||||
* Only the fullwidth "~" (U+FF5E), the form fixture labels attest — the wave
|
||||
* dash "〜" (U+301C) is a pixel-identical homoglyph that would duel it.
|
||||
* Scoreboard-names only (not death-tag) until attested, like NAME_GREEK.
|
||||
*/
|
||||
const NAME_SYMBOLS =
|
||||
"′‘’‚‛…″“”„←→↑↓⇒⇔˜€∞√∀⊂⊃∴∵∂№♭♀♂◎◇◆△▲▽▼†※™" + "『』【】〈〉《》〔〕々〆〇〃~";
|
||||
|
||||
/**
|
||||
* Render the key char's glyph but emit it as the value char: in-game names
|
||||
* show "•" as the full-size filled circle (BlitzMain's own "•" is a 4px dot
|
||||
* that never appears on screen), and fixture labels write it as "•".
|
||||
* Render the key char but emit it as the value: in-game names show "•" as a
|
||||
* full-size filled circle (BlitzMain's own "•" is a 4px dot never seen on
|
||||
* screen) and fixture labels write it as "•".
|
||||
*/
|
||||
const RENDER_ALIASES: Record<string, string> = { "●": "•" };
|
||||
|
||||
@@ -163,9 +151,8 @@ async function readFixtureGlyphs(name: string): Promise<GlyphBitmap[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one glyph and tight-crop it via the alpha channel. xScale < 1
|
||||
* condenses the glyph horizontally (the JA death message renders its font
|
||||
* squeezed to ~3/4 width in-game).
|
||||
* Renders one glyph and tight-crops it via the alpha channel. xScale < 1
|
||||
* condenses horizontally (the JA death message renders squeezed to ~3/4 width).
|
||||
*/
|
||||
function renderGlyph(
|
||||
family: string,
|
||||
@@ -331,15 +318,12 @@ await build("scoreboard-names", 17, [
|
||||
},
|
||||
]);
|
||||
/**
|
||||
* Localized closed-set charsets (all 14 game languages; the canonical
|
||||
* English strings are included by construction), restricted to the Latin
|
||||
* scripts (< U+0250) the fixtures attest: like the Greek block in the name
|
||||
* charset, wholesale Cyrillic/kana/ideograph glyphs displace Latin matches
|
||||
* on ranking noise (adding them regressed the English fixtures), so the
|
||||
* non-Latin languages' entries stay in the closed sets — ready to snap —
|
||||
* but their glyphs wait for fixtures to tune against. Chars the font
|
||||
* doesn't map are dropped too: canvas would silently render a system-font
|
||||
* substitute into the atlas.
|
||||
* Localized closed-set charsets (all 14 game languages), restricted to the
|
||||
* Latin scripts (< U+0250) the fixtures attest: wholesale Cyrillic/kana/
|
||||
* ideograph glyphs displace Latin matches on ranking noise (adding them
|
||||
* regressed the English fixtures), so non-Latin entries stay in the closed
|
||||
* sets but their glyphs wait for fixtures. Chars the font doesn't map are
|
||||
* dropped too (canvas would render a system-font substitute).
|
||||
*/
|
||||
function localizedChars(texts: readonly string[], family: string): string[] {
|
||||
const covers = fontCoverage[family]!;
|
||||
@@ -391,12 +375,10 @@ await build("scoreboard-replay-result", 30, [
|
||||
chars: localizedChars(resultTexts, "Rowdy"),
|
||||
},
|
||||
]);
|
||||
// map-start intro splash: the big mode title on the center splat is
|
||||
// BlitzBold (~76px tight caps at 1080p; a px sweep against the fixture reads
|
||||
// best at 99-101), the stage name bottom-right is BlitzMain (~40px tight;
|
||||
// 46-48px render). The constant "MODE" label ("Kampfart", ...) is BlitzMain
|
||||
// too and is read with the stage atlas rescaled to its ~48px height, so its
|
||||
// chars ride along.
|
||||
// map-start intro splash: the mode title on the center splat is BlitzBold
|
||||
// (~76px tight caps at 1080p; a px sweep reads best at 99-101), the stage name
|
||||
// bottom-right is BlitzMain (~40px tight; 46-48px render). The constant "MODE"
|
||||
// label is BlitzMain too, read with the stage atlas rescaled to ~48px.
|
||||
await build("map-start-mode", 76, [
|
||||
{
|
||||
family: "BlitzBold",
|
||||
@@ -411,11 +393,9 @@ await build("map-start-stage", 40, [
|
||||
chars: localizedChars([...stageTexts, ...ALL_MODE_LABELS], "BlitzMain"),
|
||||
},
|
||||
]);
|
||||
// death screen: the localized "Splatted by <weapon>!" burst message (tight
|
||||
// caps ~28px; the face reads between the two Blitz cuts at capture
|
||||
// fidelity, so carry both and let recognition take the max) and the
|
||||
// splash-tag name, which is BlitzBold for latin but the angular Rowdy face
|
||||
// for kana
|
||||
// death screen: the localized "Splatted by <weapon>!" burst (tight caps ~28px;
|
||||
// the face reads between the two Blitz cuts at capture fidelity, so carry both)
|
||||
// and the splash-tag name: BlitzBold for latin, the angular Rowdy face for kana
|
||||
const deathWeaponTexts = [
|
||||
...ALL_WEAPON_ENTRIES.map((e) => e.name),
|
||||
...Object.values(LOCALIZED_WEAPON_NAMES).flatMap((names) =>
|
||||
@@ -439,15 +419,12 @@ await build("death-weapon", 34, [
|
||||
chars: localizedChars(deathWeaponTexts, "BlitzBold"),
|
||||
},
|
||||
]);
|
||||
// JA death message, a separate atlas read only by the JA line ROIs: mixing
|
||||
// kana into the Latin set would shift its median width (breaking wide-
|
||||
// segment splitting) and displace Latin matches on ranking noise. The
|
||||
// in-game JP face renders horizontally condensed and sits between the two
|
||||
// FOT cuts at capture fidelity — ス/ク read as Kurokane, で as Rowdy — so
|
||||
// carry both, at the (px, xScale) pairs that peaked in an NCC sweep against
|
||||
// the classic-squiffer-jp fixture. Latin/digit chars inside JP weapon names
|
||||
// (LACT-450, .52ガロン) ride along in both faces. Attested-fixture rule as
|
||||
// everywhere: KO/ZH names stay in the closed sets without an atlas.
|
||||
// JA death message, a separate atlas read only by the JA line ROIs: mixing kana
|
||||
// into the Latin set would shift its median width (breaking wide-segment
|
||||
// splitting) and displace Latin matches. The in-game JP face renders condensed
|
||||
// and sits between the two FOT cuts (ス/ク read as Kurokane, で as Rowdy), so
|
||||
// carry both at the (px, xScale) pairs that peaked in an NCC sweep against the
|
||||
// classic-squiffer-jp fixture. KO/ZH names stay in the closed sets without an atlas.
|
||||
const deathJaTexts = [
|
||||
...(LOCALIZED_WEAPON_NAMES.JPja ?? []).map((n) => n.text),
|
||||
...DEATH_MESSAGE_TEMPLATES.filter((t) =>
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Generate the localized closed sets from the splat3 repo's language dumps
|
||||
* (https://github.com/Leanny/splat3, data/language/<Lang>_full.json), so
|
||||
* ingestion works no matter which language the player runs the game in.
|
||||
* Detectors OCR whatever is on screen, snap it against every language's
|
||||
* entries, and always emit the sendou.ink id (canonical English text
|
||||
* exists only inside the snap tables).
|
||||
* Generates the localized closed sets from the splat3 repo's language dumps
|
||||
* (https://github.com/Leanny/splat3, data/language/<Lang>_full.json) so
|
||||
* ingestion works in any game language: detectors OCR what is on screen, snap
|
||||
* it against every language's entries, and emit the sendou.ink id.
|
||||
*
|
||||
* Sources per language:
|
||||
* CommonMsg/VS/VSRuleName modes (+ the _2L two-line intro-splash
|
||||
* wrap variants, e.g. "Muschel-\nchaos")
|
||||
* CommonMsg/VS/VSRuleName modes (+ _2L two-line intro-splash wraps, "Muschel-\nchaos")
|
||||
* CommonMsg/VS/VSStageName stages (keyed via the USen values)
|
||||
* CommonMsg/MatchMode lobby tags (XMatch / Private)
|
||||
* LayoutMsg/Lobby_MenuMode_00 the intro splash's "MODE" label
|
||||
* LayoutMsg/Mng_Result_00 replay-browser VICTORY / DEFEAT tags
|
||||
* LayoutMsg/VS_Beaten_00 (999) the death-burst message; the weapon
|
||||
* placeholder sits on line 1 or 2
|
||||
* depending on language, so this becomes
|
||||
* a per-language template
|
||||
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to the canonical
|
||||
* entries via their USen value
|
||||
* LayoutMsg/VS_Beaten_00 (999) death-burst message; the weapon placeholder sits on
|
||||
* line 1 or 2 by language, so it becomes a per-language template
|
||||
* CommonMsg/Weapon/WeaponName_* weapon names, mapped to canonical entries via USen
|
||||
*
|
||||
* Usage: pnpm scanner:build-localized-entries [path-to-splat3]
|
||||
* Writes app/features/scanner/core/localized-entries.ts
|
||||
@@ -119,8 +113,6 @@ if (!languages.includes(CANONICAL_LANG)) {
|
||||
const dumps = new Map<string, LangDump>(languages.map((l) => [l, loadLang(l)]));
|
||||
const usen = dumps.get(CANONICAL_LANG)!;
|
||||
|
||||
// ---- validate the canonical sets against USen ------------------------------
|
||||
|
||||
for (const [key, mode] of Object.entries(RULE_KEYS)) {
|
||||
const value = clean(usen["CommonMsg/VS/VSRuleName"]![key]!);
|
||||
const expected = misc[`MODE_LONG_${mode}`]!;
|
||||
@@ -152,8 +144,6 @@ for (const [name, stageId] of stageIdByEnglishName) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- per-language closed sets ----------------------------------------------
|
||||
|
||||
interface LocalizedLobby {
|
||||
text: string;
|
||||
lobby: ScannerLobby;
|
||||
@@ -233,8 +223,6 @@ for (const category of ["lobbies", "modes", "modeWraps", "stages"] as const) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- death message templates -----------------------------------------------
|
||||
|
||||
const PLACEHOLDER = /\[group=[^\]]*\]/;
|
||||
/** stands in for the weapon placeholder while splitting the message */
|
||||
const SENTINEL = "\u0000";
|
||||
@@ -282,8 +270,6 @@ for (const lang of languages) {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- localized weapon names --------------------------------------------------
|
||||
|
||||
const WEAPON_MSGS = [
|
||||
"CommonMsg/Weapon/WeaponName_Main",
|
||||
"CommonMsg/Weapon/WeaponName_Sub",
|
||||
@@ -328,8 +314,6 @@ for (const lang of languages) {
|
||||
if (entries.length > 0) localizedWeaponNames[lang] = entries;
|
||||
}
|
||||
|
||||
// ---- emit --------------------------------------------------------------------
|
||||
|
||||
const banner = (extra: string) =>
|
||||
`/**
|
||||
* GENERATED by scripts/scanner/build-localized-entries.ts from the splat3 repo's
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Build the planner signature atlas from the assets repo's planner renders.
|
||||
*
|
||||
* The sendou-ink/assets checkout's assets/planner-maps/ holds the full
|
||||
* planner PNGs (~340MB), named "<stageId>-<MODE>-<TYPE>.png" (MODE in
|
||||
* CB/RM/SZ/TC/TW; TYPE in OVER/MINI/ITEMS). This tool reduces each
|
||||
* PLANNER_TYPE render to the ink-invariant structural signature
|
||||
* (app/features/scanner/core/detectors/minimap/stage.ts) and packs all of them,
|
||||
* quantized to uint8, into a single grayscale atlas PNG plus a manifest
|
||||
* (keys "<stageId>-<MODE>") — a few hundred KB the minimap detector loads
|
||||
* to identify the stage. Output goes to the assets checkout
|
||||
* (SCANNER_ASSETS_DIR/planner); shipping a regen means pushing the assets repo.
|
||||
* Builds the planner signature atlas from the assets repo's planner renders:
|
||||
* assets/planner-maps/ in the sendou-ink/assets checkout holds the full PNGs
|
||||
* (~340MB) named "<stageId>-<MODE>-<TYPE>.png" (MODE in CB/RM/SZ/TC/TW; TYPE
|
||||
* in OVER/MINI/ITEMS). Each PLANNER_TYPE render is reduced to the
|
||||
* ink-invariant structural signature (core/detectors/minimap/stage.ts) and
|
||||
* packed, quantized to uint8, into one grayscale atlas PNG plus a manifest
|
||||
* (keys "<stageId>-<MODE>"). Output goes to SCANNER_ASSETS_DIR/planner;
|
||||
* shipping a regen means pushing the assets repo.
|
||||
*
|
||||
* pnpm scanner:build-planner-signatures
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Minimal OpenType cmap reader: which codepoints does a font actually map?
|
||||
*
|
||||
* Minimal OpenType cmap reader: which codepoints a font actually maps.
|
||||
* @napi-rs/canvas silently falls back to a system font for characters the
|
||||
* requested family lacks (the Blitz cuts carry Latin/Greek/Cyrillic/kana
|
||||
* but no kanji, hangul or hanzi), which would bake wrong-font glyphs into
|
||||
* the atlases. The builder filters every charset through this before
|
||||
* rendering. Supports the two subtable formats the game fonts use:
|
||||
* format 4 (BMP segments) and format 12 (grouped full-range).
|
||||
* family lacks (the Blitz cuts have no kanji, hangul or hanzi), which would
|
||||
* bake wrong-font glyphs into the atlases. Supports format 4 (BMP segments)
|
||||
* and format 12 (grouped full-range), the two the game fonts use.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* CLI harness: replay a directory of extracted VoD frames through the full
|
||||
* detector registry driven by a DetectorScheduler, mirroring the analyzer
|
||||
* worker's chunk scan — to reproduce scheduling-dependent misses offline.
|
||||
* This is the tool for "the browser scan missed an event that a fixture
|
||||
* CLI harness: replays a directory of extracted VoD frames through the full
|
||||
* detector registry driven by a DetectorScheduler, mirroring the worker's
|
||||
* chunk scan — the tool for "the browser scan missed an event that a fixture
|
||||
* parses fine": extract the surrounding footage with
|
||||
* ffmpeg -ss <startT> -i vod.mkv -t 30 -vf fps=6 frames/f%04d.png
|
||||
* then replay it and watch which checks the scheduler ran and what they saw.
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Accuracy report across all fixtures — separate from pass/fail testing.
|
||||
* Runs each detector over its own fixture directory and prints aggregate
|
||||
* per-field accuracy plus character error rate (CER) for names, the metric
|
||||
* that drives glyph atlas expansion.
|
||||
*
|
||||
* Usage: pnpm scanner:report
|
||||
* Accuracy report across all fixtures, separate from pass/fail testing:
|
||||
* aggregate per-field accuracy plus character error rate (CER) for names,
|
||||
* the metric that drives glyph atlas expansion. Usage: pnpm scanner:report
|
||||
*/
|
||||
import { loadOpenCV } from "../../app/features/scanner/core/cv";
|
||||
import {
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* CLI equivalent of the VoD tab: scan a video file with the full detector
|
||||
* registry and write the same events CSV the tab's Export menu downloads.
|
||||
* ffmpeg decodes the video to raw RGBA frames piped through the
|
||||
* DetectorScheduler + detectors, and every parse event goes through a
|
||||
* TimelineBuilder with the tab's default merge/confidence options — so the
|
||||
* CSV matches a browser scan of the same footage (minus the calm-stretch
|
||||
* keyframe skimming, which only affects speed, not results).
|
||||
* CLI equivalent of the VoD tab: scans a video with the full detector registry
|
||||
* and writes the same events CSV the tab's Export menu downloads. ffmpeg
|
||||
* decodes to raw RGBA frames piped through the DetectorScheduler + detectors
|
||||
* and a TimelineBuilder with the tab's default options, so the CSV matches a
|
||||
* browser scan (minus keyframe skimming, which only affects speed).
|
||||
*
|
||||
* Requires ffmpeg (and ffprobe for the progress percentage) on PATH.
|
||||
*
|
||||
* Usage: pnpm scanner:scan-vod <video> [--fps 8] [--start T] [--duration S] [--out file.csv] [--telemetry]
|
||||
* --telemetry prints the VoD tab's ?telemetry=true scan counters after the
|
||||
* run (per-detector gate/parse time, scheduling savings).
|
||||
* --telemetry prints the VoD tab's ?telemetry=true scan counters after the run.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
|
||||
/**
|
||||
* Audit death/special detection against scoreboard truth, from an events CSV
|
||||
* downloaded from the scanner UI (Live/VoD tab → Download CSV). The CSV rows
|
||||
* are parsed back into DetectedEvents, run through the real match builder,
|
||||
* and rendered into the same status spans the timeline would draw — then
|
||||
* each player's span counts are diffed against the scoreboard's D/S numbers,
|
||||
* which are near-always correct. A special span ending in a death (or held
|
||||
* at the final whistle) is a legit non-use and does not count toward S.
|
||||
* Audits death/special detection against scoreboard truth, from an events CSV
|
||||
* downloaded from the scanner UI. Rows are parsed back into DetectedEvents,
|
||||
* run through the real match builder and rendered into the timeline's status
|
||||
* spans; each player's span counts are diffed against the scoreboard's D/S
|
||||
* numbers. A special span ending in a death (or held at the final whistle) is
|
||||
* a legit non-use and does not count toward S.
|
||||
*
|
||||
* The CSV is lossy (no ink colors, only top-1 strip-weapon candidates), so
|
||||
* cast-footage side orientation and slot→row assignment can degrade to
|
||||
* their fallbacks; the output flags when a mismatch looks like a
|
||||
* slot-mapping artifact rather than a detection error.
|
||||
*
|
||||
* Output is structured for triage: every discrepancy lists the exact
|
||||
* read timestamps most likely to yield a new failing fixture.
|
||||
* cast-footage side orientation and slot→row assignment can degrade to their
|
||||
* fallbacks; the output flags mismatches that look like slot-mapping artifacts.
|
||||
* Every discrepancy lists the read timestamps most likely to yield a fixture.
|
||||
*
|
||||
* Usage: pnpm scanner:status-audit <events.csv> [--all]
|
||||
*/
|
||||
@@ -182,9 +178,6 @@ for (const [index, builtMatch] of built.entries()) {
|
||||
printCandidates();
|
||||
printGuidance();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CSV → DetectedEvent reconstruction
|
||||
|
||||
function parseCsv(text: string): string[][] {
|
||||
const result: string[][] = [];
|
||||
let row: string[] = [];
|
||||
@@ -451,9 +444,6 @@ function parseScoreboardRow(row: string[]): ScoreboardData & {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analysis
|
||||
|
||||
interface FixtureCandidate {
|
||||
score: number;
|
||||
t: number;
|
||||
@@ -471,10 +461,9 @@ interface StatusSpan extends TimeWindow {
|
||||
/** sample timestamps that read the flag true inside the span */
|
||||
confirmingReads: number[];
|
||||
/**
|
||||
* widest the true state could really have held: from the last false read
|
||||
* before the span to the false read that closed it (the builder's
|
||||
* flank-to-flank measure); the rendered span bounds where a flank is
|
||||
* unobserved (series edge or gap-split)
|
||||
* widest the true state could have held: from the last false read before the
|
||||
* span to the false read that closed it (the builder's flank-to-flank measure);
|
||||
* the rendered span bounds where a flank is unobserved
|
||||
*/
|
||||
maxPossibleSeconds: number;
|
||||
}
|
||||
@@ -587,9 +576,6 @@ function readsInWindow(
|
||||
.map((event) => Math.round(event.t * 100) / 100);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output
|
||||
|
||||
function ts(t: number): string {
|
||||
return `t=${Math.round(t * 10) / 10} (${formatTime(t)})`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Config for running the scanner scripts with vite-node. Deliberately minimal:
|
||||
* the root vite.config.ts pre-bundles @techstark/opencv-js for the browser
|
||||
* worker, and vite-node would resolve that browser prebundle (which
|
||||
* crashes on __dirname in Node). Without the include, vite-node
|
||||
* externalizes the dep to a plain require of the (patched) CJS bundle.
|
||||
* vite-node config for the scanner scripts. The root vite.config.ts
|
||||
* pre-bundles @techstark/opencv-js for the browser worker, and vite-node would
|
||||
* resolve that prebundle (which crashes on __dirname in Node); without the
|
||||
* include it externalizes the dep to a plain require of the patched CJS bundle.
|
||||
*/
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ import { seedImages } from "./seed-images";
|
||||
async function main() {
|
||||
const dbEmpty = !(await db.selectFrom("User").selectAll().executeTakeFirst());
|
||||
|
||||
// Run migration and seed if db.sqlite3 doesn't exist
|
||||
if (dbEmpty) {
|
||||
logger.info("🌱 Seeding database...");
|
||||
try {
|
||||
// Dynamically imported so we skip transforming the large
|
||||
// seed import graph if the database is already seeded
|
||||
// dynamic so the large seed import graph is not transformed when already seeded
|
||||
const { seed } = await import("~/db/seed");
|
||||
await seed();
|
||||
logger.info("Database seeded successfully");
|
||||
@@ -23,7 +21,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Seed images to Minio
|
||||
logger.info("🖼️ Seeding images to Minio...");
|
||||
try {
|
||||
await seedImages();
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
/**
|
||||
* Hand-maintained types for the [Leanny/splat3](https://github.com/Leanny/splat3) game data dump.
|
||||
*
|
||||
* The dump itself is not checked in (see `.gitignore`), so these types are declared here rather
|
||||
* than inferred from the JSON files. That keeps `pnpm run typecheck:scripts` passing whether or
|
||||
* not the dump is currently sitting in `scripts/dicts/splat3`.
|
||||
*
|
||||
* Only the fields the scripts actually read are declared for the deeply nested parameter dumps.
|
||||
* When a monthly update changes the dump's shape, update these types alongside the scripts.
|
||||
* Hand-maintained types for the [Leanny/splat3](https://github.com/Leanny/splat3) game data dump,
|
||||
* declared rather than inferred so `pnpm run typecheck:scripts` passes without the gitignored dump in
|
||||
* `scripts/dicts/splat3`. Only the fields the scripts read are declared; update alongside the scripts.
|
||||
*/
|
||||
|
||||
/** `data/language/<Lang>.json` — message category -> message key -> localized (URI encoded) text. */
|
||||
|
||||
@@ -10,7 +10,6 @@ invariant(discordId !== discordId2, "discord ids must be different");
|
||||
|
||||
const tempDiscordId = "temp-discord-id";
|
||||
|
||||
// swap user discordIds
|
||||
await db.transaction().execute(async (trx) => {
|
||||
const swap = (from: string, to: string) =>
|
||||
trx
|
||||
|
||||
@@ -16,7 +16,6 @@ async function main() {
|
||||
let deleted = 0;
|
||||
let updated = 0;
|
||||
|
||||
// update existing
|
||||
for (const existingBadge of await homemadeBadgesInDb()) {
|
||||
const badge = homemadeBadges[existingBadge.code];
|
||||
|
||||
@@ -51,7 +50,6 @@ async function main() {
|
||||
|
||||
let added = 0;
|
||||
|
||||
// add new
|
||||
for (const [fileName, badge] of Object.entries(homemadeBadges)) {
|
||||
const existing = homemadeAfterUpdates.find(
|
||||
(existingBadge) => fileName === existingBadge.code,
|
||||
|
||||
Reference in New Issue
Block a user