@diffenderfer-games/hub-contract 2.0.0-next.0 → 2.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/dist/index.d.ts +2683 -39
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +862 -18
  7. package/dist/index.js.map +1 -1
  8. package/package.json +53 -53
  9. package/src/api/analytics.ts +81 -81
  10. package/src/api/auth.ts +77 -77
  11. package/src/api/catalog-activity.ts +66 -0
  12. package/src/api/catalog.ts +177 -171
  13. package/src/api/daily.ts +122 -122
  14. package/src/api/economy/bundle.ts +69 -69
  15. package/src/api/index.ts +92 -85
  16. package/src/api/leaderboards.ts +151 -151
  17. package/src/api/messages.ts +142 -142
  18. package/src/api/mp/chat.ts +36 -36
  19. package/src/api/mp/invite-links.ts +42 -0
  20. package/src/api/mp/parties.ts +5 -0
  21. package/src/api/mp/races.ts +211 -211
  22. package/src/api/mp/rooms.ts +122 -90
  23. package/src/api/mp/snapshot.ts +17 -17
  24. package/src/api/mp.ts +135 -110
  25. package/src/api/notify.ts +114 -111
  26. package/src/api/offline.ts +45 -0
  27. package/src/api/progress/game-data.ts +189 -189
  28. package/src/api/progress.ts +59 -59
  29. package/src/api/race.ts +47 -47
  30. package/src/api/reports.ts +208 -208
  31. package/src/api/social/config.ts +47 -47
  32. package/src/api/social/endpoints.ts +179 -179
  33. package/src/api/social/people.ts +118 -118
  34. package/src/api/social/presence.ts +95 -95
  35. package/src/api/social.ts +9 -9
  36. package/src/api/tournaments/endpoints.ts +102 -0
  37. package/src/api/tournaments/play-online.ts +71 -0
  38. package/src/api/tournaments/shapes.ts +202 -0
  39. package/src/api/tournaments.ts +7 -0
  40. package/src/api/version.ts +40 -40
  41. package/src/coerce.ts +50 -50
  42. package/src/common.ts +42 -42
  43. package/src/entities.ts +62 -62
  44. package/src/errors/codes/core.ts +10 -10
  45. package/src/errors/codes/multiplayer.ts +56 -43
  46. package/src/errors/codes/play.ts +5 -5
  47. package/src/errors/codes/social.ts +31 -31
  48. package/src/errors/index.ts +88 -88
  49. package/src/index.ts +16 -16
  50. package/src/limits.ts +125 -100
  51. package/src/logic/daily.ts +54 -54
  52. package/src/logic/index.ts +8 -8
  53. package/src/logic/presence.ts +62 -62
  54. package/src/logic/time.ts +81 -81
  55. package/src/manifest/changes.ts +55 -55
  56. package/src/manifest/controls.ts +42 -42
  57. package/src/manifest/fields.ts +89 -89
  58. package/src/manifest/game.ts +139 -132
  59. package/src/manifest/index.ts +13 -12
  60. package/src/manifest/inject.ts +31 -31
  61. package/src/manifest/labels.ts +99 -0
  62. package/src/manifest/leaderboards.ts +29 -29
  63. package/src/manifest/multiplayer.ts +79 -71
  64. package/src/manifest/race.ts +93 -93
  65. package/src/middleware.ts +47 -47
  66. package/src/realtime/define.ts +52 -52
  67. package/src/realtime/envelope.ts +67 -67
  68. package/src/realtime/events/multiplayer.ts +9 -8
  69. package/src/realtime/events/parties.ts +31 -31
  70. package/src/realtime/events/races.ts +27 -27
  71. package/src/realtime/events/rooms.ts +52 -48
  72. package/src/realtime/events/social.ts +81 -81
  73. package/src/realtime/events/tournaments.ts +12 -0
  74. package/src/realtime/index.ts +24 -24
  75. package/src/realtime/ops/empty.ts +8 -8
  76. package/src/realtime/ops/hello.ts +40 -40
  77. package/src/realtime/ops/multiplayer.ts +8 -8
  78. package/src/realtime/ops/parties.ts +13 -13
  79. package/src/realtime/ops/races.ts +99 -99
  80. package/src/realtime/ops/rooms.ts +22 -8
  81. package/src/realtime/ops/social.ts +58 -58
@@ -1,88 +1,88 @@
1
- /**
2
- * The error model (plan §5.3). Every failed REST call and every failed socket
3
- * ack carries an {@link ErrorBody}. `error` is always safe to show a player.
4
- * Public codes are listed per area in `codes/`; admin-only codes live in the
5
- * server-only internal contract.
6
- *
7
- * Each schema here also has a named `…Schema` interface. Endpoints reference
8
- * these schemas hundreds of times; naming their types lets the published
9
- * `.d.ts` refer to them instead of printing every code at every use.
10
- */
11
- import { z } from 'zod';
12
- import { Millis } from '../common';
13
- import { CORE_CODES } from './codes/core';
14
- import { MULTIPLAYER_CODES } from './codes/multiplayer';
15
- import { PLAY_CODES } from './codes/play';
16
- import { SOCIAL_CODES } from './codes/social';
17
-
18
- const errorCode = z.enum([...CORE_CODES, ...PLAY_CODES, ...SOCIAL_CODES, ...MULTIPLAYER_CODES]);
19
-
20
- /** The type of the {@link ErrorCode} schema. */
21
- export interface ErrorCodeSchema extends Readonly<typeof errorCode> {}
22
-
23
- /** Every public error code. */
24
- export const ErrorCode: ErrorCodeSchema = errorCode;
25
-
26
- /** A public error code. */
27
- export type ErrorCode = z.infer<ErrorCodeSchema>;
28
-
29
- const rejectReason = z.enum([
30
- 'link', 'personal_info', 'contact', 'language', 'slur', 'sexual', 'grown_up', 'unkind',
31
- 'threat', 'drugs', 'spam', 'shouting', 'too_long', 'empty', 'chars', 'reserved',
32
- 'rate_limited', 'muted', 'check_unavailable', 'chat_off', 'not_friends', 'suspended',
33
- 'quick_only', 'dms_closed', 'other',
34
- ]);
35
-
36
- /** The type of the {@link RejectReason} schema. */
37
- export interface RejectReasonSchema extends Readonly<typeof rejectReason> {}
38
-
39
- /**
40
- * Why text was rejected or a chat action was refused. Coarse on purpose: a
41
- * reason names a category, never the word or list that matched.
42
- */
43
- export const RejectReason: RejectReasonSchema = rejectReason;
44
-
45
- /** A content-reject reason. */
46
- export type RejectReason = z.infer<RejectReasonSchema>;
47
-
48
- const errorBody = z.object({
49
- /** A sentence that is always safe to show the player. */
50
- error: z.string(),
51
- /** What went wrong, for code to branch on. */
52
- code: ErrorCode.optional(),
53
- /** Content rejects and suspensions: the category. */
54
- reason: RejectReason.optional(),
55
- /** A kid-friendly suggestion, e.g. "try saying it another way". */
56
- hint: z.string().optional(),
57
- /** Rate limits: how long to wait, in ms (0 when not limited). */
58
- retryAfterMs: z.number().int().nonnegative().optional(),
59
- /** Suspensions and mutes: when they end (epoch ms). */
60
- until: Millis.optional(),
61
- });
62
-
63
- /** The type of the {@link ErrorBody} schema. */
64
- export interface ErrorBodySchema extends Readonly<typeof errorBody> {}
65
-
66
- /** The body of every error response and failed socket ack. */
67
- export const ErrorBody: ErrorBodySchema = errorBody;
68
-
69
- /** An error body. */
70
- export type ErrorBody = z.infer<ErrorBodySchema>;
71
-
72
- const contentRejected = errorBody.extend({
73
- code: z.literal('content_rejected'),
74
- reason: RejectReason,
75
- hint: z.string(),
76
- });
77
-
78
- /** The type of the {@link ContentRejected} schema. */
79
- export interface ContentRejectedSchema extends Readonly<typeof contentRejected> {}
80
-
81
- /**
82
- * The 422 content-rejected body (multiplayer.md §4.6), shared by DMs, room
83
- * chat, suggestions, names and trade notes.
84
- */
85
- export const ContentRejected: ContentRejectedSchema = contentRejected;
86
-
87
- /** A content-rejected body. */
88
- export type ContentRejected = z.infer<ContentRejectedSchema>;
1
+ /**
2
+ * The error model (plan §5.3). Every failed REST call and every failed socket
3
+ * ack carries an {@link ErrorBody}. `error` is always safe to show a player.
4
+ * Public codes are listed per area in `codes/`; admin-only codes live in the
5
+ * server-only internal contract.
6
+ *
7
+ * Each schema here also has a named `…Schema` interface. Endpoints reference
8
+ * these schemas hundreds of times; naming their types lets the published
9
+ * `.d.ts` refer to them instead of printing every code at every use.
10
+ */
11
+ import { z } from 'zod';
12
+ import { Millis } from '../common';
13
+ import { CORE_CODES } from './codes/core';
14
+ import { MULTIPLAYER_CODES } from './codes/multiplayer';
15
+ import { PLAY_CODES } from './codes/play';
16
+ import { SOCIAL_CODES } from './codes/social';
17
+
18
+ const errorCode = z.enum([...CORE_CODES, ...PLAY_CODES, ...SOCIAL_CODES, ...MULTIPLAYER_CODES]);
19
+
20
+ /** The type of the {@link ErrorCode} schema. */
21
+ export interface ErrorCodeSchema extends Readonly<typeof errorCode> {}
22
+
23
+ /** Every public error code. */
24
+ export const ErrorCode: ErrorCodeSchema = errorCode;
25
+
26
+ /** A public error code. */
27
+ export type ErrorCode = z.infer<ErrorCodeSchema>;
28
+
29
+ const rejectReason = z.enum([
30
+ 'link', 'personal_info', 'contact', 'language', 'slur', 'sexual', 'grown_up', 'unkind',
31
+ 'threat', 'drugs', 'spam', 'shouting', 'too_long', 'empty', 'chars', 'reserved',
32
+ 'rate_limited', 'muted', 'check_unavailable', 'chat_off', 'not_friends', 'suspended',
33
+ 'quick_only', 'dms_closed', 'other',
34
+ ]);
35
+
36
+ /** The type of the {@link RejectReason} schema. */
37
+ export interface RejectReasonSchema extends Readonly<typeof rejectReason> {}
38
+
39
+ /**
40
+ * Why text was rejected or a chat action was refused. Coarse on purpose: a
41
+ * reason names a category, never the word or list that matched.
42
+ */
43
+ export const RejectReason: RejectReasonSchema = rejectReason;
44
+
45
+ /** A content-reject reason. */
46
+ export type RejectReason = z.infer<RejectReasonSchema>;
47
+
48
+ const errorBody = z.object({
49
+ /** A sentence that is always safe to show the player. */
50
+ error: z.string(),
51
+ /** What went wrong, for code to branch on. */
52
+ code: ErrorCode.optional(),
53
+ /** Content rejects and suspensions: the category. */
54
+ reason: RejectReason.optional(),
55
+ /** A kid-friendly suggestion, e.g. "try saying it another way". */
56
+ hint: z.string().optional(),
57
+ /** Rate limits: how long to wait, in ms (0 when not limited). */
58
+ retryAfterMs: z.number().int().nonnegative().optional(),
59
+ /** Suspensions and mutes: when they end (epoch ms). */
60
+ until: Millis.optional(),
61
+ });
62
+
63
+ /** The type of the {@link ErrorBody} schema. */
64
+ export interface ErrorBodySchema extends Readonly<typeof errorBody> {}
65
+
66
+ /** The body of every error response and failed socket ack. */
67
+ export const ErrorBody: ErrorBodySchema = errorBody;
68
+
69
+ /** An error body. */
70
+ export type ErrorBody = z.infer<ErrorBodySchema>;
71
+
72
+ const contentRejected = errorBody.extend({
73
+ code: z.literal('content_rejected'),
74
+ reason: RejectReason,
75
+ hint: z.string(),
76
+ });
77
+
78
+ /** The type of the {@link ContentRejected} schema. */
79
+ export interface ContentRejectedSchema extends Readonly<typeof contentRejected> {}
80
+
81
+ /**
82
+ * The 422 content-rejected body (multiplayer.md §4.6), shared by DMs, room
83
+ * chat, suggestions, names and trade notes.
84
+ */
85
+ export const ContentRejected: ContentRejectedSchema = contentRejected;
86
+
87
+ /** A content-rejected body. */
88
+ export type ContentRejected = z.infer<ContentRejectedSchema>;
package/src/index.ts CHANGED
@@ -1,16 +1,16 @@
1
- /**
2
- * The diffenderfer.games hub wire contract: zod schemas and types for the
3
- * public REST API, the `/_ws` socket envelope, ops and events, error bodies
4
- * and the `game.*` manifest.
5
- *
6
- * @packageDocumentation
7
- */
8
- export * from './common';
9
- export * from './entities';
10
- export * from './errors';
11
- export * from './middleware';
12
- export * from './api';
13
- export * from './realtime';
14
- export * from './manifest';
15
- export * from './limits';
16
- export * from './logic';
1
+ /**
2
+ * The diffenderfer.games hub wire contract: zod schemas and types for the
3
+ * public REST API, the `/_ws` socket envelope, ops and events, error bodies
4
+ * and the `game.*` manifest.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export * from './common';
9
+ export * from './entities';
10
+ export * from './errors';
11
+ export * from './middleware';
12
+ export * from './api';
13
+ export * from './realtime';
14
+ export * from './manifest';
15
+ export * from './limits';
16
+ export * from './logic';
package/src/limits.ts CHANGED
@@ -1,100 +1,125 @@
1
- /**
2
- * Limits a UI has to know about: text lengths, name rules, seat counts and
3
- * the rules for a game's `package.json` `game.*` block. Each number is
4
- * defined once here; the schemas and the server import it.
5
- *
6
- * Rate thresholds, caps and timers are server-only and not part of this package.
7
- */
8
- import { z } from 'zod';
9
-
10
- /** Account name and password rules (sign-up, claim, rename). */
11
- export const ACCOUNT_LIMITS = {
12
- /** Shortest username, after trimming. */
13
- usernameMin: 2,
14
- /** Longest username, after trimming. */
15
- usernameMax: 24,
16
- /** Shortest password. */
17
- passwordMin: 6,
18
- /** How many built-in avatars there are (`a0` … `a23`). */
19
- avatarCount: 24,
20
- } as const;
21
-
22
- /** The characters a username may use: letters, digits, `_`, `-` and spaces. */
23
- export const USERNAME_PATTERN = /^[A-Za-z0-9_\- ]+$/;
24
-
25
- /**
26
- * The text limits every social UI renders from. The hub sends the live values
27
- * in `GET /social/config` and the socket `hello` (`limits`); the server can
28
- * change them (`HUB_LIMITS_JSON`), so a UI should prefer the sent values.
29
- */
30
- export const UiLimits = z.object({
31
- /** Longest direct message, in characters. */
32
- dmMaxChars: z.number().int().positive(),
33
- /** Longest room or party chat line, in characters. */
34
- roomChatMaxChars: z.number().int().positive(),
35
- /** Longest suggestion title, in characters. */
36
- suggestionTitleMax: z.number().int().positive(),
37
- /** Longest suggestion body, in characters. */
38
- suggestionBodyMax: z.number().int().positive(),
39
- });
40
-
41
- /** The text limits every social UI renders from. */
42
- export type UiLimits = z.infer<typeof UiLimits>;
43
-
44
- /** Today's default {@link UiLimits}, used until the hub sends its own. */
45
- export const UI_LIMIT_DEFAULTS = {
46
- dmMaxChars: 200,
47
- roomChatMaxChars: 120,
48
- suggestionTitleMax: 60,
49
- suggestionBodyMax: 500,
50
- } as const satisfies UiLimits;
51
-
52
- /** Seats in hub rooms and parties. */
53
- export const SEAT_LIMITS = {
54
- /** Most players in one hub room, and the top of any `players` range. */
55
- maxSeats: 8,
56
- /** Most members in one party. */
57
- partyMax: 8,
58
- } as const;
59
-
60
- /** What a racer's live stats (`hub.race.status(stats)`) may hold. */
61
- export const RACE_STATS_LIMITS = {
62
- /** Most stat keys in one update. */
63
- maxKeys: 12,
64
- /** Largest update, in bytes of JSON. */
65
- maxBytes: 600,
66
- } as const;
67
-
68
- /** Limits on the `package.json` `game.*` block (see the manifest schemas). */
69
- export const MANIFEST_LIMITS = {
70
- /** Most `game.changes` entries the hub reads. */
71
- changesMax: 200,
72
- /** Longest `game.changes` entry, in characters. */
73
- changeTextMax: 280,
74
- /** Longest leaderboard title, in characters. */
75
- leaderboardTitleMax: 80,
76
- /** Furthest the menu button may be pushed from its corner, in px. */
77
- menuOffsetMax: 600,
78
- /** Longest lobby URL suffix, in characters. */
79
- lobbyMax: 200,
80
- /** Most `game.multiplayer.modes`. */
81
- modesMax: 16,
82
- /** Longest mode title, in characters. */
83
- modeTitleMax: 40,
84
- /** Most `game.multiplayer.quickChat` phrases. */
85
- quickChatMax: 24,
86
- /** Longest quick-chat phrase, in characters. */
87
- quickChatTextMax: 40,
88
- /** Most `game.multiplayer.chatAllow` words. */
89
- chatAllowMax: 20,
90
- /** Most `game.race.params`. */
91
- raceParamsMax: 6,
92
- /** Most options in one race param. */
93
- raceOptionsMax: 12,
94
- /** Most `game.race.stats` (the live HUD shows the first 3). */
95
- raceStatsMax: 8,
96
- /** Longest race param, option or stat title, in characters. */
97
- raceTitleMax: 24,
98
- /** Largest `game.race.minMs`, in ms (10 minutes). */
99
- raceMinMsMax: 600_000,
100
- } as const;
1
+ /**
2
+ * Limits a UI has to know about: text lengths, name rules, seat counts and
3
+ * the rules for a game's `package.json` `game.*` block. Each number is
4
+ * defined once here; the schemas and the server import it.
5
+ *
6
+ * Rate thresholds, caps and timers are server-only and not part of this package.
7
+ */
8
+ import { z } from 'zod';
9
+
10
+ /** Account name and password rules (sign-up, claim, rename). */
11
+ export const ACCOUNT_LIMITS = {
12
+ /** Shortest username, after trimming. */
13
+ usernameMin: 2,
14
+ /** Longest username, after trimming. */
15
+ usernameMax: 24,
16
+ /** Shortest password. */
17
+ passwordMin: 6,
18
+ /** How many built-in avatars there are (`a0` … `a23`). */
19
+ avatarCount: 24,
20
+ } as const;
21
+
22
+ /** The characters a username may use: letters, digits, `_`, `-` and spaces. */
23
+ export const USERNAME_PATTERN = /^[A-Za-z0-9_\- ]+$/;
24
+
25
+ /**
26
+ * The text limits every social UI renders from. The hub sends the live values
27
+ * in `GET /social/config` and the socket `hello` (`limits`); the server can
28
+ * change them (`HUB_LIMITS_JSON`), so a UI should prefer the sent values.
29
+ */
30
+ export const UiLimits = z.object({
31
+ /** Longest direct message, in characters. */
32
+ dmMaxChars: z.number().int().positive(),
33
+ /** Longest room or party chat line, in characters. */
34
+ roomChatMaxChars: z.number().int().positive(),
35
+ /** Longest suggestion title, in characters. */
36
+ suggestionTitleMax: z.number().int().positive(),
37
+ /** Longest suggestion body, in characters. */
38
+ suggestionBodyMax: z.number().int().positive(),
39
+ });
40
+
41
+ /** The text limits every social UI renders from. */
42
+ export type UiLimits = z.infer<typeof UiLimits>;
43
+
44
+ /** Today's default {@link UiLimits}, used until the hub sends its own. */
45
+ export const UI_LIMIT_DEFAULTS = {
46
+ dmMaxChars: 200,
47
+ roomChatMaxChars: 120,
48
+ suggestionTitleMax: 60,
49
+ suggestionBodyMax: 500,
50
+ } as const satisfies UiLimits;
51
+
52
+ /** Seats in hub rooms and parties. */
53
+ export const SEAT_LIMITS = {
54
+ /** Most players in one hub room, and the top of any `players` range. */
55
+ maxSeats: 8,
56
+ /** Most members in one party. */
57
+ partyMax: 8,
58
+ } as const;
59
+
60
+ /** What a racer's live stats (`hub.race.status(stats)`) may hold. */
61
+ export const RACE_STATS_LIMITS = {
62
+ /** Most stat keys in one update. */
63
+ maxKeys: 12,
64
+ /** Largest update, in bytes of JSON. */
65
+ maxBytes: 600,
66
+ } as const;
67
+
68
+ /** Limits on the `package.json` `game.*` block (see the manifest schemas). */
69
+ export const MANIFEST_LIMITS = {
70
+ /** Most `game.changes` entries the hub reads. */
71
+ changesMax: 200,
72
+ /** Longest `game.changes` entry, in characters. */
73
+ changeTextMax: 280,
74
+ /** Longest leaderboard title, in characters. */
75
+ leaderboardTitleMax: 80,
76
+ /** Furthest the menu button may be pushed from its corner, in px. */
77
+ menuOffsetMax: 600,
78
+ /** Longest lobby URL suffix, in characters. */
79
+ lobbyMax: 200,
80
+ /** Most `game.multiplayer.modes`. */
81
+ modesMax: 16,
82
+ /** Longest mode title, in characters. */
83
+ modeTitleMax: 40,
84
+ /** Most `game.multiplayer.quickChat` phrases. */
85
+ quickChatMax: 24,
86
+ /** Longest quick-chat phrase, in characters. */
87
+ quickChatTextMax: 40,
88
+ /** Most `game.multiplayer.chatAllow` words. */
89
+ chatAllowMax: 20,
90
+ /** Most `game.race.params`. */
91
+ raceParamsMax: 6,
92
+ /** Most options in one race param. */
93
+ raceOptionsMax: 12,
94
+ /** Most `game.race.stats` (the live HUD shows the first 3). */
95
+ raceStatsMax: 8,
96
+ /** Longest race param, option or stat title, in characters. */
97
+ raceTitleMax: 24,
98
+ /** Largest `game.race.minMs`, in ms (10 minutes). */
99
+ raceMinMsMax: 600_000,
100
+ } as const;
101
+
102
+ /** What a tournament may be (batch-2026-10 R15). The server enforces each one. */
103
+ export const TOURNAMENT_LIMITS = {
104
+ /** Longest title, in characters. */
105
+ titleMax: 48,
106
+ /** Fewest players. */
107
+ playersMin: 2,
108
+ /** Most players. */
109
+ playersMax: 64,
110
+ /** Most games in the set a tournament picks from. */
111
+ gamesMax: 8,
112
+ /** Lives per player: 1 single, 2 double, 3 triple elimination. */
113
+ livesMax: 3,
114
+ /** The default and the longest window to join a ready match before forfeiting, in minutes. */
115
+ readyMinutesDefault: 10,
116
+ readyMinutesMax: 60,
117
+ /** The longest "remind me before it starts", in minutes (one day). */
118
+ leadMinutesMax: 1440,
119
+ /** The default reminder for players who signed up for a scheduled tournament, in minutes. */
120
+ leadMinutesDefault: 10,
121
+ /** The furthest ahead a tournament may be scheduled, in days. */
122
+ scheduleDaysMax: 30,
123
+ /** Most friends invited in one call. */
124
+ invitesMax: 50,
125
+ } as const;
@@ -1,54 +1,54 @@
1
- /**
2
- * Daily-challenge day keys and seeds: UTC `YYYY-MM-DD` keys, the playable
3
- * window, and the deterministic per-(game, day) seed both the hub and the
4
- * game compute. Pure; the signed tokens stay server-only.
5
- */
6
-
7
- const DAY_MS = 24 * 60 * 60 * 1000;
8
- const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
9
-
10
- /** The UTC day key (`YYYY-MM-DD`) of a timestamp. */
11
- export function dayKey(ts: number): string {
12
- return new Date(ts).toISOString().slice(0, 10);
13
- }
14
-
15
- /**
16
- * The playable window, newest first: today and the `count - 1` days before it.
17
- * @param count - how many days (the back-fill window, 30 by default on the hub)
18
- */
19
- export function windowDays(now: number, count: number): string[] {
20
- return Array.from({ length: count }, (_, i) => dayKey(now - i * DAY_MS));
21
- }
22
-
23
- /**
24
- * Is `day` playable? A player's local "today" can be UTC±1, so one day into
25
- * the future is accepted, plus `backfillDays` into the past. Compared in
26
- * whole UTC days, so it is locale-independent.
27
- */
28
- export function inWindow(day: string, now: number, backfillDays: number): boolean {
29
- if (!DAY_RE.test(day)) {
30
- return false;
31
- }
32
- const dayStart = Date.parse(`${day}T00:00:00Z`);
33
- const today = Date.parse(`${dayKey(now)}T00:00:00Z`);
34
- if (Number.isNaN(dayStart) || Number.isNaN(today)) {
35
- return false;
36
- }
37
- const diff = Math.round((dayStart - today) / DAY_MS);
38
- return diff <= 1 && diff >= -backfillDays;
39
- }
40
-
41
- /**
42
- * The deterministic uint32 seed for a (game, day): FNV-1a over `game#day`.
43
- * The game derives its level or difficulty from it, so everyone plays the
44
- * same challenge.
45
- */
46
- export function seedForDay(game: string, day: string): number {
47
- let hash = 2166136261 >>> 0;
48
- const seedInput = `${game}#${day}`;
49
- for (let i = 0; i < seedInput.length; i++) {
50
- hash ^= seedInput.charCodeAt(i);
51
- hash = Math.imul(hash, 16777619);
52
- }
53
- return hash >>> 0;
54
- }
1
+ /**
2
+ * Daily-challenge day keys and seeds: UTC `YYYY-MM-DD` keys, the playable
3
+ * window, and the deterministic per-(game, day) seed both the hub and the
4
+ * game compute. Pure; the signed tokens stay server-only.
5
+ */
6
+
7
+ const DAY_MS = 24 * 60 * 60 * 1000;
8
+ const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
9
+
10
+ /** The UTC day key (`YYYY-MM-DD`) of a timestamp. */
11
+ export function dayKey(ts: number): string {
12
+ return new Date(ts).toISOString().slice(0, 10);
13
+ }
14
+
15
+ /**
16
+ * The playable window, newest first: today and the `count - 1` days before it.
17
+ * @param count - how many days (the back-fill window, 30 by default on the hub)
18
+ */
19
+ export function windowDays(now: number, count: number): string[] {
20
+ return Array.from({ length: count }, (_, i) => dayKey(now - i * DAY_MS));
21
+ }
22
+
23
+ /**
24
+ * Is `day` playable? A player's local "today" can be UTC±1, so one day into
25
+ * the future is accepted, plus `backfillDays` into the past. Compared in
26
+ * whole UTC days, so it is locale-independent.
27
+ */
28
+ export function inWindow(day: string, now: number, backfillDays: number): boolean {
29
+ if (!DAY_RE.test(day)) {
30
+ return false;
31
+ }
32
+ const dayStart = Date.parse(`${day}T00:00:00Z`);
33
+ const today = Date.parse(`${dayKey(now)}T00:00:00Z`);
34
+ if (Number.isNaN(dayStart) || Number.isNaN(today)) {
35
+ return false;
36
+ }
37
+ const diff = Math.round((dayStart - today) / DAY_MS);
38
+ return diff <= 1 && diff >= -backfillDays;
39
+ }
40
+
41
+ /**
42
+ * The deterministic uint32 seed for a (game, day): FNV-1a over `game#day`.
43
+ * The game derives its level or difficulty from it, so everyone plays the
44
+ * same challenge.
45
+ */
46
+ export function seedForDay(game: string, day: string): number {
47
+ let hash = 2166136261 >>> 0;
48
+ const seedInput = `${game}#${day}`;
49
+ for (let i = 0; i < seedInput.length; i++) {
50
+ hash ^= seedInput.charCodeAt(i);
51
+ hash = Math.imul(hash, 16777619);
52
+ }
53
+ return hash >>> 0;
54
+ }
@@ -1,8 +1,8 @@
1
- /**
2
- * Pure, browser-safe helpers every hub UI shares: time and count wording,
3
- * presence status lines, race wording, and daily day keys and seeds.
4
- */
5
- export * from './daily';
6
- export * from './presence';
7
- export * from './race';
8
- export * from './time';
1
+ /**
2
+ * Pure, browser-safe helpers every hub UI shares: time and count wording,
3
+ * presence status lines, race wording, and daily day keys and seeds.
4
+ */
5
+ export * from './daily';
6
+ export * from './presence';
7
+ export * from './race';
8
+ export * from './time';