2b399fdb81
PR #1223 only staged the deletions of the old paths because the new top-level directories were still untracked when the commit was authored. This commit adds the actual restructured tree: SREBOT/ (existing bot), SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES, update_game_files), and TSSBOT/ (skeleton). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.4 KiB
JavaScript
136 lines
4.4 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const SEASONS_FILE = path.join(__dirname, '..', 'constants', 'seasons');
|
|
|
|
// Parse DD.MM from the "until eos" parenthetical to an end-of-day UTC unix ts
|
|
// for the season's year (inferred from the first weekN timestamp).
|
|
function _endOfDayUtc(year, day, month) {
|
|
const d = new Date(Date.UTC(year, month - 1, day, 23, 59, 59));
|
|
return Math.floor(d.getTime() / 1000);
|
|
}
|
|
|
|
// Parse the seasons file into:
|
|
// { '2026-I': {start, end, weeks}, '2026-II': {start, end, weeks}, ... }
|
|
// where `end` is the last second (UTC) of the date in the "until eos" row.
|
|
// Status is computed at access time by getSeasons(), not cached here.
|
|
function parseSeasonsFile(content) {
|
|
const lines = content.split('\n');
|
|
const seasons = {};
|
|
let current = null;
|
|
let firstTs = null;
|
|
let eosDay = null, eosMonth = null;
|
|
let weekEntries = [];
|
|
|
|
const seasonHeader = /^\s*(\d{4})-(I{1,3}|IV|VI{0,3}|IX|X)\s*$/;
|
|
const weekLine = /^(week\s+\d+|until\s+eos)\s+\((\d{2}\.\d{2})\s+[—-]\s+(\d{2}\.\d{2})\)\s+<t:(\d+):R>\s+max BR\s+(\d+(?:\.\d+)?)/i;
|
|
|
|
const commit = () => {
|
|
if (current && firstTs !== null && eosDay !== null) {
|
|
const year = parseInt(current.split('-')[0], 10);
|
|
seasons[current] = {
|
|
start: firstTs,
|
|
end: _endOfDayUtc(year, eosDay, eosMonth),
|
|
weeks: weekEntries,
|
|
};
|
|
}
|
|
current = null; firstTs = null; eosDay = null; eosMonth = null;
|
|
weekEntries = [];
|
|
};
|
|
|
|
for (const line of lines) {
|
|
const h = line.match(seasonHeader);
|
|
if (h) { commit(); current = `${h[1]}-${h[2]}`; continue; }
|
|
if (!current) continue;
|
|
const w = line.match(weekLine);
|
|
if (w) {
|
|
const kind = w[1].toLowerCase();
|
|
const startDate = w[2];
|
|
const endDate = w[3];
|
|
const ts = parseInt(w[4], 10);
|
|
const maxBr = parseFloat(w[5]);
|
|
weekEntries.push({
|
|
label: kind,
|
|
start_date: startDate,
|
|
end_date: endDate,
|
|
start_ts: ts,
|
|
max_br: maxBr,
|
|
});
|
|
if (firstTs === null) firstTs = ts;
|
|
if (kind === 'until eos') {
|
|
eosDay = parseInt(endDate.split('.')[0], 10);
|
|
eosMonth = parseInt(endDate.split('.')[1], 10);
|
|
}
|
|
}
|
|
}
|
|
commit();
|
|
return seasons;
|
|
}
|
|
|
|
// All "week N (…) <t:TS:R>" timestamps for a given season name, in order.
|
|
// Used as x-axis faint vline markers by the Python renderer.
|
|
function parseWeekBoundaries(content, seasonName) {
|
|
const lines = content.split('\n');
|
|
const out = [];
|
|
let inSeason = false;
|
|
const seasonHeader = /^\s*(\d{4})-(I{1,3}|IV|VI{0,3}|IX|X)\s*$/;
|
|
const weekLine = /<t:(\d+):R>/;
|
|
for (const line of lines) {
|
|
const h = line.match(seasonHeader);
|
|
if (h) { inSeason = `${h[1]}-${h[2]}` === seasonName; continue; }
|
|
if (!inSeason) continue;
|
|
const w = line.match(weekLine);
|
|
if (w) out.push(parseInt(w[1], 10));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
let _cached = null;
|
|
function _loadSeasons() {
|
|
if (_cached) return _cached;
|
|
const content = fs.readFileSync(SEASONS_FILE, 'utf8');
|
|
const parsed = parseSeasonsFile(content);
|
|
_cached = { content, parsed };
|
|
return _cached;
|
|
}
|
|
|
|
function getSeasons() {
|
|
const { parsed } = _loadSeasons();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const out = {};
|
|
for (const [name, range] of Object.entries(parsed)) {
|
|
out[name] = {
|
|
start: range.start,
|
|
end: range.end,
|
|
status: range.end < now ? 'completed' : 'in_progress',
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function getSeasonRange(name) {
|
|
return getSeasons()[name] || null;
|
|
}
|
|
|
|
function getSeasonDetails() {
|
|
const { parsed } = _loadSeasons();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const out = {};
|
|
for (const [name, range] of Object.entries(parsed)) {
|
|
out[name] = {
|
|
start: range.start,
|
|
end: range.end,
|
|
status: range.end < now ? 'completed' : 'in_progress',
|
|
weeks: range.weeks || [],
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function getWeekBoundaries(name) {
|
|
const { content } = _loadSeasons();
|
|
return parseWeekBoundaries(content, name);
|
|
}
|
|
|
|
module.exports = { getSeasons, getSeasonRange, getSeasonDetails, getWeekBoundaries, parseSeasonsFile, parseWeekBoundaries };
|