From 72fe6b618bb3ea4d9553ab86e1a68e82666b8c87 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Tue, 13 Oct 2026 12:37:10 +0200 Subject: [PATCH] refactor(time): make resetHourLocal a per-region map A game's servers need not agree on the reset hour any more than on the offset. Reverse: 1999 and Nikke spell their 05:00 out for all three regions, so no reader's day key moves. Co-Authored-By: Claude Opus 5.5 --- docs/DATA-MODEL.md | 3 ++- src/shared/games.ts | 15 ++++++++++----- src/shared/time.ts | 11 ++++++----- test/daily.test.ts | 6 +++--- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index c101253..6bf810f 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -397,7 +397,8 @@ already logged, which is the change this table warns about below. than a bent offset. It runs a single global server on UTC-5 and resets at **05:00**, so its day rolls at 10:00 UTC. `resetOffsets` alone cannot say that: landing 10:00 UTC through the offset would mean claiming a UTC-6 server, which is a lie every other reader of `serverOffsetUtc` would inherit. So -`GameMeta.resetHourLocal` overrides `RESET_HOUR_LOCAL` per game, and is absent — meaning 04:00 — for +`GameMeta.resetHourLocal` overrides `RESET_HOUR_LOCAL` per region, like `resetOffsets` — a game's +servers need not agree on the hour any more than on the offset — and is absent — meaning 04:00 — for everything else, which is why introducing it moved no existing day key. Both facts come off the source: all 154 rows of the wiki's event list state `(UTC-5)`, and every one runs 05:00 → 04:59, an event ending one minute before the reset the next one starts on. diff --git a/src/shared/games.ts b/src/shared/games.ts index a0edd9a..a81c840 100644 --- a/src/shared/games.ts +++ b/src/shared/games.ts @@ -40,20 +40,25 @@ export interface GameMeta { */ resetOffsets?: Partial> | undefined; /** - * The hour of the server's own day this game rolls over on, when it is not - * `RESET_HOUR_LOCAL` (04:00). + * The hour of the server's own day this game rolls over on, per region, where + * it is not `RESET_HOUR_LOCAL` (04:00). * * Most gacha servers reset at 04:00 local. Reverse: 1999 resets at 05:00, and * the difference is not cosmetic: `resetOffsets` alone cannot express it, * because bending a game's stated server offset to land the right instant * would put every other reader of that offset an hour out. * + * Per region for the same reason `resetOffsets` is: a game's servers need not + * agree on the hour either. Girls' Frontline 2's Asia server rolls at 05:00 + * UTC+8 while its global server rolls at 21:00 UTC-8, and one number per game + * would have to lie about one of them. A region absent here takes 04:00. + * * Like `resetOffsets` this feeds `dayKey`, which is a **localStorage key**, so * the same warning applies — changing it for a game that already has readers * re-labels the game-day their logged ticks fall in. Absent means 04:00, which * is why adding this field moved no existing game. */ - resetHourLocal?: number | undefined; + resetHourLocal?: Partial> | undefined; } export const GAMES: Record = { @@ -86,7 +91,7 @@ export const GAMES: Record = { // the source rather than from habit: all 154 rows on the wiki's event list // state `(UTC-5)`, and every one of them starts at 05:00 and ends at 04:59 — // an event ending one minute before the reset that the next one begins on. - r1999: { id: "r1999", name: "Reverse: 1999", short: "R1999", hue: "#C9A227" , studio: "Bluepoch", dailyTasks: "Daily missions", resetOffsets: { asia: -5, america: -5, europe: -5 }, resetHourLocal: 5 }, + r1999: { id: "r1999", name: "Reverse: 1999", short: "R1999", hue: "#C9A227" , studio: "Bluepoch", dailyTasks: "Daily missions", resetOffsets: { asia: -5, america: -5, europe: -5 }, resetHourLocal: { asia: 5, america: 5, europe: 5 } }, // No `resetOffsets` and no `resetHourLocal`, for the reason p5x has none: the // source states no time of day anywhere — every date on bluearchive.wiki's // Global schedule is a bare `YYYY-MM-DD`. Blue Archive Global does run one @@ -149,7 +154,7 @@ export const GAMES: Record = { // Set in the same commit that ships the game, which costs nothing now and // could not be added later without re-labelling day keys readers had already // logged ticks under. - nikke: { id: "nikke", name: "Goddess of Victory: Nikke", short: "Nikke", hue: "#E4572E", studio: "Shift Up", dailyTasks: "Daily missions, outpost", resetOffsets: { asia: 9, america: 9, europe: 9 }, resetHourLocal: 5 }, + nikke: { id: "nikke", name: "Goddess of Victory: Nikke", short: "Nikke", hue: "#E4572E", studio: "Shift Up", dailyTasks: "Daily missions, outpost", resetOffsets: { asia: 9, america: 9, europe: 9 }, resetHourLocal: { asia: 5, america: 5, europe: 5 } }, uma: { id: "uma", name: "Umamusume: Pretty Derby", short: "Umamusume", hue: "#6FBF44", studio: "Cygames", dailyTasks: "Daily races, missions" }, // No `resetOffsets`, and the source is silent in the strongest sense: every // boundary Honkai Impact 3rd has here is a **week bucket**, and every one of diff --git a/src/shared/time.ts b/src/shared/time.ts index eb6c78d..76998f4 100644 --- a/src/shared/time.ts +++ b/src/shared/time.ts @@ -59,10 +59,11 @@ export function serverOffsetUtc(region: Region, game?: LaneId): number { } /** - * The hour of its own server day a game rolls over on. + * The hour of its own server day a game rolls over on, for the reader's region. * * Almost always `RESET_HOUR_LOCAL`. A game that resets on a different hour says - * so in `resetHourLocal` (`games.ts`) — Reverse: 1999 rolls at 05:00 — and that + * so in `resetHourLocal` (`games.ts`), per region because a game's servers need + * not agree — Reverse: 1999 rolls at 05:00 everywhere — and that * cannot be folded into `serverOffsetUtc`: shifting a game's stated offset to * land the right reset instant would misreport the server clock to everything * else that asks for it. @@ -70,9 +71,9 @@ export function serverOffsetUtc(region: Region, game?: LaneId): number { * A lane the reader invented has no server to know about and takes the default, * for the same reason `serverOffsetUtc` does. */ -export function resetHourFor(game?: LaneId): number { +export function resetHourFor(region: Region, game?: LaneId): number { const override = - game === undefined ? undefined : GAMES[game as GameId]?.resetHourLocal; + game === undefined ? undefined : GAMES[game as GameId]?.resetHourLocal?.[region]; return override ?? RESET_HOUR_LOCAL; } @@ -86,7 +87,7 @@ export function resetHourFor(game?: LaneId): number { * here as a data change, not a constant. */ export function resetShiftMs(region: Region, game?: LaneId): number { - return serverOffsetUtc(region, game) * HOUR - resetHourFor(game) * HOUR; + return serverOffsetUtc(region, game) * HOUR - resetHourFor(region, game) * HOUR; } /** diff --git a/test/daily.test.ts b/test/daily.test.ts index 44f1b71..330283c 100644 --- a/test/daily.test.ts +++ b/test/daily.test.ts @@ -393,12 +393,12 @@ describe("a game whose day rolls on a different hour", () => { test("resetHourFor defaults to 04:00 for every other game", () => { // Adding the field must not have moved a single existing reader's day key, // which is why it is an override rather than a per-game table. - expect(resetHourFor("r1999")).toBe(5); + expect(resetHourFor("europe", "r1999")).toBe(5); for (const game of ["genshin", "endfield", "arknights", undefined] as const) { - expect(resetHourFor(game)).toBe(RESET_HOUR_LOCAL); + expect(resetHourFor("europe", game)).toBe(RESET_HOUR_LOCAL); } // A lane the reader invented has no server map and takes the default too. - expect(resetHourFor("mygame:my-own-game")).toBe(RESET_HOUR_LOCAL); + expect(resetHourFor("europe", "mygame:my-own-game")).toBe(RESET_HOUR_LOCAL); }); test("an event's checklist counts days on the 05:00 clock", () => {