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

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 (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/dist/index.d.ts +95 -95
  5. package/package.json +53 -53
  6. package/src/api/analytics.ts +81 -81
  7. package/src/api/auth.ts +77 -77
  8. package/src/api/catalog.ts +171 -171
  9. package/src/api/daily.ts +122 -122
  10. package/src/api/economy/bundle.ts +69 -69
  11. package/src/api/index.ts +85 -85
  12. package/src/api/leaderboards.ts +151 -151
  13. package/src/api/messages.ts +142 -142
  14. package/src/api/mp/chat.ts +36 -36
  15. package/src/api/mp/races.ts +211 -211
  16. package/src/api/mp/rooms.ts +90 -90
  17. package/src/api/mp/snapshot.ts +17 -17
  18. package/src/api/mp.ts +110 -110
  19. package/src/api/notify.ts +111 -111
  20. package/src/api/progress/game-data.ts +189 -189
  21. package/src/api/progress.ts +59 -59
  22. package/src/api/race.ts +47 -47
  23. package/src/api/reports.ts +208 -208
  24. package/src/api/social/config.ts +47 -47
  25. package/src/api/social/endpoints.ts +179 -179
  26. package/src/api/social/people.ts +118 -118
  27. package/src/api/social/presence.ts +95 -95
  28. package/src/api/social.ts +9 -9
  29. package/src/api/version.ts +40 -40
  30. package/src/coerce.ts +50 -50
  31. package/src/common.ts +42 -42
  32. package/src/entities.ts +62 -62
  33. package/src/errors/codes/core.ts +10 -10
  34. package/src/errors/codes/multiplayer.ts +43 -43
  35. package/src/errors/codes/play.ts +5 -5
  36. package/src/errors/codes/social.ts +31 -31
  37. package/src/errors/index.ts +88 -88
  38. package/src/index.ts +16 -16
  39. package/src/limits.ts +100 -100
  40. package/src/logic/daily.ts +54 -54
  41. package/src/logic/index.ts +8 -8
  42. package/src/logic/presence.ts +62 -62
  43. package/src/logic/time.ts +81 -81
  44. package/src/manifest/changes.ts +55 -55
  45. package/src/manifest/controls.ts +42 -42
  46. package/src/manifest/fields.ts +89 -89
  47. package/src/manifest/game.ts +132 -132
  48. package/src/manifest/index.ts +12 -12
  49. package/src/manifest/inject.ts +31 -31
  50. package/src/manifest/leaderboards.ts +29 -29
  51. package/src/manifest/multiplayer.ts +71 -71
  52. package/src/manifest/race.ts +93 -93
  53. package/src/middleware.ts +47 -47
  54. package/src/realtime/define.ts +52 -52
  55. package/src/realtime/envelope.ts +67 -67
  56. package/src/realtime/events/multiplayer.ts +8 -8
  57. package/src/realtime/events/parties.ts +31 -31
  58. package/src/realtime/events/races.ts +27 -27
  59. package/src/realtime/events/rooms.ts +48 -48
  60. package/src/realtime/events/social.ts +81 -81
  61. package/src/realtime/index.ts +24 -24
  62. package/src/realtime/ops/empty.ts +8 -8
  63. package/src/realtime/ops/hello.ts +40 -40
  64. package/src/realtime/ops/multiplayer.ts +8 -8
  65. package/src/realtime/ops/parties.ts +13 -13
  66. package/src/realtime/ops/races.ts +99 -99
  67. package/src/realtime/ops/rooms.ts +1 -1
  68. package/src/realtime/ops/social.ts +58 -58
@@ -1,211 +1,211 @@
1
- /**
2
- * Race shapes (docs/plans/race.md): params, live stats, racers and decided
3
- * results. Shared by the `race.*` socket ops and events, the race block of
4
- * a room, and `GET /race/history`. Each rule here is defined once.
5
- */
6
- import { z } from 'zod';
7
- import { HexColor, Millis, Slug, UserId } from '../../common';
8
- import { RACE_STATS_LIMITS } from '../../limits';
9
-
10
- /** A race param or stat key: a letter, then up to 23 letters, digits or `_`. */
11
- export const RaceKey = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,23}$/);
12
-
13
- /** A race param value, one of the game's declared options: a number, a boolean or a short token. */
14
- export const RaceParamValue = z.union([
15
- z.number(),
16
- z.boolean(),
17
- z.string().regex(/^[A-Za-z0-9_-]{1,24}$/),
18
- ]);
19
-
20
- /**
21
- * The creator's choices, keyed by the manifest's `race.params[].key`. The
22
- * hub keeps only declared keys and option values (anything else becomes the
23
- * param's default), so everyone gets the same puzzle.
24
- */
25
- export const RaceParams = z.record(RaceKey, RaceParamValue);
26
-
27
- /** A race's params. */
28
- export type RaceParams = z.infer<typeof RaceParams>;
29
-
30
- /** Live stat values are clamped to ±1e9. */
31
- const STAT_LIMIT = 1e9;
32
-
33
- /**
34
- * A live stat value: a finite number (clamped to ±1e9), a boolean or a
35
- * `#rrggbb` colour (lower-cased). Never text.
36
- */
37
- export const RaceStatValue = z.union([
38
- z.number().transform((n) => Math.max(-STAT_LIMIT, Math.min(STAT_LIMIT, n))),
39
- z.boolean(),
40
- HexColor.transform((c) => c.toLowerCase()),
41
- ]);
42
-
43
- /** A live stat value, as cleaned by the hub. */
44
- export type RaceStatValue = z.output<typeof RaceStatValue>;
45
-
46
- /**
47
- * A racer's live stats, keyed like the manifest's `race.stats[].key`: at
48
- * most 12 keys and 600 bytes of JSON once cleaned.
49
- */
50
- export const RaceStats = z
51
- .record(RaceKey, RaceStatValue)
52
- .refine((s) => Object.keys(s).length <= RACE_STATS_LIMITS.maxKeys, `at most ${String(RACE_STATS_LIMITS.maxKeys)} stats`)
53
- .refine((s) => JSON.stringify(s).length <= RACE_STATS_LIMITS.maxBytes, 'stats too large');
54
-
55
- /** A racer's stats. */
56
- export type RaceStats = z.infer<typeof RaceStats>;
57
-
58
- /** How far along a racer is, 0 to 1. The hub clamps values outside that range. */
59
- export const RaceProgress = z.number().transform((n) => Math.max(0, Math.min(1, n)));
60
-
61
- /** A race room's phase: waiting for racers → countdown → racing → done (→ countdown on a rematch). */
62
- export const RacePhase = z.enum(['waiting', 'countdown', 'racing', 'done']);
63
-
64
- /** A racer's state while the race runs. */
65
- export const RacerState = z.enum(['racing', 'finished', 'lost', 'forfeit', 'disconnected']);
66
-
67
- /** A value of {@link RacerState}. */
68
- export type RacerState = z.infer<typeof RacerState>;
69
-
70
- /** How a race ended: the winner finished, everyone else lost / gave up / dropped, or time ran out. */
71
- export const RaceOutcome = z.enum(['finished', 'lost', 'forfeit', 'disconnected', 'timeout']);
72
-
73
- /** A value of {@link RaceOutcome}. */
74
- export type RaceOutcome = z.infer<typeof RaceOutcome>;
75
-
76
- /** One racer's result. `beaten`: still racing when someone else won. */
77
- export const RacerResult = z.enum(['won', 'beaten', 'lost', 'forfeit', 'disconnected', 'timeout']);
78
-
79
- /** A value of {@link RacerResult}. */
80
- export type RacerResult = z.infer<typeof RacerResult>;
81
-
82
- /** Fields every live racer line has (room roster and result card). */
83
- const RacerLine = z.object({
84
- userId: UserId,
85
- /** The racer's username when the race began. */
86
- name: z.string(),
87
- /** Avatar token; never empty. */
88
- avatar: z.string(),
89
- stats: RaceStats,
90
- /** 0..1, when the game reports it. */
91
- progress: RaceProgress.optional(),
92
- });
93
-
94
- /** A racer in a race room's roster (RoomInfo.race.players). */
95
- export const Racer = RacerLine.extend({
96
- state: RacerState,
97
- /** Server-measured ms from the go to finishing or dropping out. */
98
- ms: z.number().int().nonnegative().optional(),
99
- });
100
-
101
- /** A racer in a race room. */
102
- export type Racer = z.infer<typeof Racer>;
103
-
104
- /** One racer's line in a {@link RaceResult}. */
105
- export const RaceResultPlayer = RacerLine.extend({
106
- /** 1 for the winner. */
107
- place: z.number().int().positive(),
108
- result: RacerResult,
109
- /** Server-measured ms from the go to finishing or dropping out (the race's end for those still in). */
110
- ms: z.number().int().nonnegative(),
111
- });
112
-
113
- /** Fields a decided race has both live and in the history. */
114
- const DecidedRace = z.object({
115
- game: Slug,
116
- params: RaceParams,
117
- outcome: RaceOutcome,
118
- /** Epoch ms of the go. */
119
- startedAt: Millis,
120
- /** Epoch ms. */
121
- endedAt: Millis,
122
- /** ms from the go to the decision. */
123
- durationMs: z.number().int().nonnegative(),
124
- });
125
-
126
- /** A decided race: the `race.result` event, the `race.finish` ack and RoomInfo.race.result. */
127
- export const RaceResult = DecidedRace.extend({
128
- /** The race's id in the history; `null` if recording it failed. */
129
- id: z.number().int().positive().nullable(),
130
- /** The room code. */
131
- room: z.string(),
132
- /** 1 for the room's first race, +1 per rematch. */
133
- round: z.number().int().positive(),
134
- seed: z.number().int().positive(),
135
- /** `null` when nobody won (time ran out, or everyone dropped out). */
136
- winnerId: UserId.nullable(),
137
- /** In finishing order (place 1 first). */
138
- players: z.array(RaceResultPlayer),
139
- });
140
-
141
- /** A decided race. */
142
- export type RaceResult = z.infer<typeof RaceResult>;
143
-
144
- /** The referee's view of a race room (RoomInfo.race). */
145
- export const RaceState = z.object({
146
- phase: RacePhase,
147
- /** 1 for the first race in the room, +1 per rematch; 0 while waiting for the first. */
148
- round: z.number().int().nonnegative(),
149
- params: RaceParams,
150
- /** The countdown can start once this many racers are seated. */
151
- minPlayers: z.number().int().positive(),
152
- /** Everyone racing this round (empty while waiting for the first). */
153
- players: z.array(Racer),
154
- /** Who asked for a rematch since the last race. */
155
- rematch: z.array(UserId),
156
- /** Present once the countdown starts (never before, so nobody can pre-solve). */
157
- seed: z.number().int().positive().optional(),
158
- /** Epoch ms of the go (server clock); present with `seed`. */
159
- startAt: Millis.optional(),
160
- /** Present once the round is decided. */
161
- result: RaceResult.optional(),
162
- });
163
-
164
- /** A race room's referee state. */
165
- export type RaceState = z.infer<typeof RaceState>;
166
-
167
- /** The countdown began (`race.start`): build the puzzle from `seed` + `params`. */
168
- export const RaceStart = z.object({
169
- roomId: z.string(),
170
- round: z.number().int().positive(),
171
- seed: z.number().int().positive(),
172
- params: RaceParams,
173
- /** Epoch ms of the go (server clock). */
174
- startAt: Millis,
175
- /** ms until the go. */
176
- startsIn: z.number().int().nonnegative(),
177
- /** The racers' user ids. */
178
- players: z.array(UserId),
179
- });
180
-
181
- /** A racer in a history record. Suspended opponents are `hidden`: id 0, name "A player", no avatar. */
182
- export const RaceRecordPlayer = z.object({
183
- /** 0 when hidden. */
184
- userId: z.number().int().nonnegative(),
185
- username: z.string(),
186
- /** Avatar token; `''` when hidden. */
187
- avatar: z.string(),
188
- guest: z.boolean(),
189
- place: z.number().int().positive(),
190
- result: RacerResult,
191
- /** Server-measured ms from the go to finishing or dropping out. */
192
- ms: z.number().int().nonnegative().optional(),
193
- progress: RaceProgress.optional(),
194
- stats: RaceStats,
195
- hidden: z.literal(true).optional(),
196
- });
197
-
198
- /** One past race the player was in (`GET /race/history`). */
199
- export const RaceRecord = DecidedRace.extend({
200
- id: z.number().int().positive(),
201
- gameTitle: z.string(),
202
- /** `null` when nobody won; 0 when the winner is hidden (suspended). */
203
- winnerId: z.number().int().nonnegative().nullable(),
204
- /** The signed-in player's own line (`null` only if their row is missing). */
205
- me: RaceRecordPlayer.nullable(),
206
- /** In place order. */
207
- players: z.array(RaceRecordPlayer),
208
- });
209
-
210
- /** A past race. */
211
- export type RaceRecord = z.infer<typeof RaceRecord>;
1
+ /**
2
+ * Race shapes (docs/plans/race.md): params, live stats, racers and decided
3
+ * results. Shared by the `race.*` socket ops and events, the race block of
4
+ * a room, and `GET /race/history`. Each rule here is defined once.
5
+ */
6
+ import { z } from 'zod';
7
+ import { HexColor, Millis, Slug, UserId } from '../../common';
8
+ import { RACE_STATS_LIMITS } from '../../limits';
9
+
10
+ /** A race param or stat key: a letter, then up to 23 letters, digits or `_`. */
11
+ export const RaceKey = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,23}$/);
12
+
13
+ /** A race param value, one of the game's declared options: a number, a boolean or a short token. */
14
+ export const RaceParamValue = z.union([
15
+ z.number(),
16
+ z.boolean(),
17
+ z.string().regex(/^[A-Za-z0-9_-]{1,24}$/),
18
+ ]);
19
+
20
+ /**
21
+ * The creator's choices, keyed by the manifest's `race.params[].key`. The
22
+ * hub keeps only declared keys and option values (anything else becomes the
23
+ * param's default), so everyone gets the same puzzle.
24
+ */
25
+ export const RaceParams = z.record(RaceKey, RaceParamValue);
26
+
27
+ /** A race's params. */
28
+ export type RaceParams = z.infer<typeof RaceParams>;
29
+
30
+ /** Live stat values are clamped to ±1e9. */
31
+ const STAT_LIMIT = 1e9;
32
+
33
+ /**
34
+ * A live stat value: a finite number (clamped to ±1e9), a boolean or a
35
+ * `#rrggbb` colour (lower-cased). Never text.
36
+ */
37
+ export const RaceStatValue = z.union([
38
+ z.number().transform((n) => Math.max(-STAT_LIMIT, Math.min(STAT_LIMIT, n))),
39
+ z.boolean(),
40
+ HexColor.transform((c) => c.toLowerCase()),
41
+ ]);
42
+
43
+ /** A live stat value, as cleaned by the hub. */
44
+ export type RaceStatValue = z.output<typeof RaceStatValue>;
45
+
46
+ /**
47
+ * A racer's live stats, keyed like the manifest's `race.stats[].key`: at
48
+ * most 12 keys and 600 bytes of JSON once cleaned.
49
+ */
50
+ export const RaceStats = z
51
+ .record(RaceKey, RaceStatValue)
52
+ .refine((s) => Object.keys(s).length <= RACE_STATS_LIMITS.maxKeys, `at most ${String(RACE_STATS_LIMITS.maxKeys)} stats`)
53
+ .refine((s) => JSON.stringify(s).length <= RACE_STATS_LIMITS.maxBytes, 'stats too large');
54
+
55
+ /** A racer's stats. */
56
+ export type RaceStats = z.infer<typeof RaceStats>;
57
+
58
+ /** How far along a racer is, 0 to 1. The hub clamps values outside that range. */
59
+ export const RaceProgress = z.number().transform((n) => Math.max(0, Math.min(1, n)));
60
+
61
+ /** A race room's phase: waiting for racers → countdown → racing → done (→ countdown on a rematch). */
62
+ export const RacePhase = z.enum(['waiting', 'countdown', 'racing', 'done']);
63
+
64
+ /** A racer's state while the race runs. */
65
+ export const RacerState = z.enum(['racing', 'finished', 'lost', 'forfeit', 'disconnected']);
66
+
67
+ /** A value of {@link RacerState}. */
68
+ export type RacerState = z.infer<typeof RacerState>;
69
+
70
+ /** How a race ended: the winner finished, everyone else lost / gave up / dropped, or time ran out. */
71
+ export const RaceOutcome = z.enum(['finished', 'lost', 'forfeit', 'disconnected', 'timeout']);
72
+
73
+ /** A value of {@link RaceOutcome}. */
74
+ export type RaceOutcome = z.infer<typeof RaceOutcome>;
75
+
76
+ /** One racer's result. `beaten`: still racing when someone else won. */
77
+ export const RacerResult = z.enum(['won', 'beaten', 'lost', 'forfeit', 'disconnected', 'timeout']);
78
+
79
+ /** A value of {@link RacerResult}. */
80
+ export type RacerResult = z.infer<typeof RacerResult>;
81
+
82
+ /** Fields every live racer line has (room roster and result card). */
83
+ const RacerLine = z.object({
84
+ userId: UserId,
85
+ /** The racer's username when the race began. */
86
+ name: z.string(),
87
+ /** Avatar token; never empty. */
88
+ avatar: z.string(),
89
+ stats: RaceStats,
90
+ /** 0..1, when the game reports it. */
91
+ progress: RaceProgress.optional(),
92
+ });
93
+
94
+ /** A racer in a race room's roster (RoomInfo.race.players). */
95
+ export const Racer = RacerLine.extend({
96
+ state: RacerState,
97
+ /** Server-measured ms from the go to finishing or dropping out. */
98
+ ms: z.number().int().nonnegative().optional(),
99
+ });
100
+
101
+ /** A racer in a race room. */
102
+ export type Racer = z.infer<typeof Racer>;
103
+
104
+ /** One racer's line in a {@link RaceResult}. */
105
+ export const RaceResultPlayer = RacerLine.extend({
106
+ /** 1 for the winner. */
107
+ place: z.number().int().positive(),
108
+ result: RacerResult,
109
+ /** Server-measured ms from the go to finishing or dropping out (the race's end for those still in). */
110
+ ms: z.number().int().nonnegative(),
111
+ });
112
+
113
+ /** Fields a decided race has both live and in the history. */
114
+ const DecidedRace = z.object({
115
+ game: Slug,
116
+ params: RaceParams,
117
+ outcome: RaceOutcome,
118
+ /** Epoch ms of the go. */
119
+ startedAt: Millis,
120
+ /** Epoch ms. */
121
+ endedAt: Millis,
122
+ /** ms from the go to the decision. */
123
+ durationMs: z.number().int().nonnegative(),
124
+ });
125
+
126
+ /** A decided race: the `race.result` event, the `race.finish` ack and RoomInfo.race.result. */
127
+ export const RaceResult = DecidedRace.extend({
128
+ /** The race's id in the history; `null` if recording it failed. */
129
+ id: z.number().int().positive().nullable(),
130
+ /** The room code. */
131
+ room: z.string(),
132
+ /** 1 for the room's first race, +1 per rematch. */
133
+ round: z.number().int().positive(),
134
+ seed: z.number().int().positive(),
135
+ /** `null` when nobody won (time ran out, or everyone dropped out). */
136
+ winnerId: UserId.nullable(),
137
+ /** In finishing order (place 1 first). */
138
+ players: z.array(RaceResultPlayer),
139
+ });
140
+
141
+ /** A decided race. */
142
+ export type RaceResult = z.infer<typeof RaceResult>;
143
+
144
+ /** The referee's view of a race room (RoomInfo.race). */
145
+ export const RaceState = z.object({
146
+ phase: RacePhase,
147
+ /** 1 for the first race in the room, +1 per rematch; 0 while waiting for the first. */
148
+ round: z.number().int().nonnegative(),
149
+ params: RaceParams,
150
+ /** The countdown can start once this many racers are seated. */
151
+ minPlayers: z.number().int().positive(),
152
+ /** Everyone racing this round (empty while waiting for the first). */
153
+ players: z.array(Racer),
154
+ /** Who asked for a rematch since the last race. */
155
+ rematch: z.array(UserId),
156
+ /** Present once the countdown starts (never before, so nobody can pre-solve). */
157
+ seed: z.number().int().positive().optional(),
158
+ /** Epoch ms of the go (server clock); present with `seed`. */
159
+ startAt: Millis.optional(),
160
+ /** Present once the round is decided. */
161
+ result: RaceResult.optional(),
162
+ });
163
+
164
+ /** A race room's referee state. */
165
+ export type RaceState = z.infer<typeof RaceState>;
166
+
167
+ /** The countdown began (`race.start`): build the puzzle from `seed` + `params`. */
168
+ export const RaceStart = z.object({
169
+ roomId: z.string(),
170
+ round: z.number().int().positive(),
171
+ seed: z.number().int().positive(),
172
+ params: RaceParams,
173
+ /** Epoch ms of the go (server clock). */
174
+ startAt: Millis,
175
+ /** ms until the go. */
176
+ startsIn: z.number().int().nonnegative(),
177
+ /** The racers' user ids. */
178
+ players: z.array(UserId),
179
+ });
180
+
181
+ /** A racer in a history record. Suspended opponents are `hidden`: id 0, name "A player", no avatar. */
182
+ export const RaceRecordPlayer = z.object({
183
+ /** 0 when hidden. */
184
+ userId: z.number().int().nonnegative(),
185
+ username: z.string(),
186
+ /** Avatar token; `''` when hidden. */
187
+ avatar: z.string(),
188
+ guest: z.boolean(),
189
+ place: z.number().int().positive(),
190
+ result: RacerResult,
191
+ /** Server-measured ms from the go to finishing or dropping out. */
192
+ ms: z.number().int().nonnegative().optional(),
193
+ progress: RaceProgress.optional(),
194
+ stats: RaceStats,
195
+ hidden: z.literal(true).optional(),
196
+ });
197
+
198
+ /** One past race the player was in (`GET /race/history`). */
199
+ export const RaceRecord = DecidedRace.extend({
200
+ id: z.number().int().positive(),
201
+ gameTitle: z.string(),
202
+ /** `null` when nobody won; 0 when the winner is hidden (suspended). */
203
+ winnerId: z.number().int().nonnegative().nullable(),
204
+ /** The signed-in player's own line (`null` only if their row is missing). */
205
+ me: RaceRecordPlayer.nullable(),
206
+ /** In place order. */
207
+ players: z.array(RaceRecordPlayer),
208
+ });
209
+
210
+ /** A past race. */
211
+ export type RaceRecord = z.infer<typeof RaceRecord>;
@@ -1,90 +1,90 @@
1
- /**
2
- * Hub relay rooms (multiplayer-impl.md §6): realtime multiplayer for static
3
- * games, relayed (never refereed) by the hub. A race is a room with a
4
- * `race` block on top. Shared by the `room.*` / `race.*` ops and events and
5
- * the `hello` snapshot.
6
- */
7
- import { z } from 'zod';
8
- import { Json, Slug, UserId } from '../../common';
9
- import { ChatMode } from '../../entities';
10
- import { GameMode, OpaqueId } from './parties';
11
- import { RaceState } from './races';
12
-
13
- /** A room's join code: 5 characters from `A-Z` and `2-9` (no `I`, `O`, `0`, `1`). */
14
- export const RoomCode = z.string().regex(/^[A-HJ-NP-Z2-9]{5}$/);
15
-
16
- /** A room's lifecycle: new players join only in the lobby. */
17
- export const RoomPhase = z.enum(['lobby', 'playing', 'ended']);
18
-
19
- /**
20
- * Who hears a send. `host` (default): guests' unaddressed sends reach the
21
- * host only. `relay`: every unaddressed send reaches everyone, sender
22
- * included.
23
- */
24
- export const RoomModel = z.enum(['host', 'relay']);
25
-
26
- /** A seat in a room, keyed by user (any tab of that user reclaims it). */
27
- export const RoomSeat = z.object({
28
- userId: UserId,
29
- /** The player's username when they sat down. */
30
- name: z.string(),
31
- /** Avatar token; never empty. */
32
- avatar: z.string(),
33
- ready: z.boolean(),
34
- /** `false` while the seat is held for a dropped player's reconnect grace. */
35
- connected: z.boolean(),
36
- /** Small per-seat game data (team, colour…), at most 2 KB of JSON. */
37
- data: Json.optional(),
38
- });
39
-
40
- /** The public description of a room (`room` event, room and race acks, the `hello` snapshot). */
41
- export const RoomInfo = z.object({
42
- id: OpaqueId,
43
- code: RoomCode,
44
- game: Slug,
45
- /** `race` for race rooms. */
46
- mode: GameMode.optional(),
47
- hostId: UserId,
48
- /** Listed in `room.list` / `race.list`. */
49
- public: z.boolean(),
50
- phase: RoomPhase,
51
- maxPlayers: z.number().int().positive(),
52
- model: RoomModel,
53
- seats: z.array(RoomSeat),
54
- /** How many are watching. */
55
- spectators: z.number().int().nonnegative(),
56
- /** The chat mode for the seated players. */
57
- chatMode: ChatMode,
58
- /** Present on race rooms. */
59
- race: RaceState.optional(),
60
- });
61
-
62
- /** A room. */
63
- export type RoomInfo = z.infer<typeof RoomInfo>;
64
-
65
- /** A room's shared state for late joiners: a JSON object, at most 32 KB. */
66
- export const RoomState = z.record(z.string(), Json);
67
-
68
- /** A relayed game message (`room.msg`, and a join's replay history). */
69
- export const RoomMessage = z.object({
70
- from: UserId,
71
- data: Json,
72
- /** Per-room sequence number, shared with state patches. */
73
- seq: z.number().int().positive(),
74
- /** Set when the message went to one seat only. */
75
- to: UserId.optional(),
76
- });
77
-
78
- /** How to find a room: by its code (case-insensitive) or its id. */
79
- export const RoomTarget = z.object({
80
- code: z.string().optional(),
81
- id: z.string().optional(),
82
- /** Watch instead of taking a seat (if the game allows spectators). */
83
- spectate: z.boolean().optional(),
84
- });
85
-
86
- /** Why a room went away for this tab (`room.closed`). */
87
- export const RoomClosedReason = z.enum(['empty', 'ended', 'host_left', 'moved', 'other_tab', 'removed']);
88
-
89
- /** Why a seat was freed (`room.abandoned`). */
90
- export const SeatGoneReason = z.enum(['left', 'abandoned', 'kicked']);
1
+ /**
2
+ * Hub relay rooms (multiplayer-impl.md §6): realtime multiplayer for static
3
+ * games, relayed (never refereed) by the hub. A race is a room with a
4
+ * `race` block on top. Shared by the `room.*` / `race.*` ops and events and
5
+ * the `hello` snapshot.
6
+ */
7
+ import { z } from 'zod';
8
+ import { Json, Slug, UserId } from '../../common';
9
+ import { ChatMode } from '../../entities';
10
+ import { GameMode, OpaqueId } from './parties';
11
+ import { RaceState } from './races';
12
+
13
+ /** A room's join code: 5 characters from `A-Z` and `2-9` (no `I`, `O`, `0`, `1`). */
14
+ export const RoomCode = z.string().regex(/^[A-HJ-NP-Z2-9]{5}$/);
15
+
16
+ /** A room's lifecycle: new players join only in the lobby. */
17
+ export const RoomPhase = z.enum(['lobby', 'playing', 'ended']);
18
+
19
+ /**
20
+ * Who hears a send. `host` (default): guests' unaddressed sends reach the
21
+ * host only. `relay`: every unaddressed send reaches everyone, sender
22
+ * included.
23
+ */
24
+ export const RoomModel = z.enum(['host', 'relay']);
25
+
26
+ /** A seat in a room, keyed by user (any tab of that user reclaims it). */
27
+ export const RoomSeat = z.object({
28
+ userId: UserId,
29
+ /** The player's username when they sat down. */
30
+ name: z.string(),
31
+ /** Avatar token; never empty. */
32
+ avatar: z.string(),
33
+ ready: z.boolean(),
34
+ /** `false` while the seat is held for a dropped player's reconnect grace. */
35
+ connected: z.boolean(),
36
+ /** Small per-seat game data (team, colour…), at most 2 KB of JSON. */
37
+ data: Json.optional(),
38
+ });
39
+
40
+ /** The public description of a room (`room` event, room and race acks, the `hello` snapshot). */
41
+ export const RoomInfo = z.object({
42
+ id: OpaqueId,
43
+ code: RoomCode,
44
+ game: Slug,
45
+ /** `race` for race rooms. */
46
+ mode: GameMode.optional(),
47
+ hostId: UserId,
48
+ /** Listed in `room.list` / `race.list`. */
49
+ public: z.boolean(),
50
+ phase: RoomPhase,
51
+ maxPlayers: z.number().int().positive(),
52
+ model: RoomModel,
53
+ seats: z.array(RoomSeat),
54
+ /** How many are watching. */
55
+ spectators: z.number().int().nonnegative(),
56
+ /** The chat mode for the seated players. */
57
+ chatMode: ChatMode,
58
+ /** Present on race rooms. */
59
+ race: RaceState.optional(),
60
+ });
61
+
62
+ /** A room. */
63
+ export type RoomInfo = z.infer<typeof RoomInfo>;
64
+
65
+ /** A room's shared state for late joiners: a JSON object, at most 32 KB. */
66
+ export const RoomState = z.record(z.string(), Json);
67
+
68
+ /** A relayed game message (`room.msg`, and a join's replay history). */
69
+ export const RoomMessage = z.object({
70
+ from: UserId,
71
+ data: Json,
72
+ /** Per-room sequence number, shared with state patches. */
73
+ seq: z.number().int().positive(),
74
+ /** Set when the message went to one seat only. */
75
+ to: UserId.optional(),
76
+ });
77
+
78
+ /** How to find a room: by its code (case-insensitive) or its id. */
79
+ export const RoomTarget = z.object({
80
+ code: z.string().optional(),
81
+ id: z.string().optional(),
82
+ /** Watch instead of taking a seat (if the game allows spectators). */
83
+ spectate: z.boolean().optional(),
84
+ });
85
+
86
+ /** Why a room went away for this tab (`room.closed`). */
87
+ export const RoomClosedReason = z.enum(['empty', 'ended', 'host_left', 'moved', 'other_tab', 'removed']);
88
+
89
+ /** Why a seat was freed (`room.abandoned`). */
90
+ export const SeatGoneReason = z.enum(['left', 'abandoned', 'kicked']);
@@ -1,17 +1,17 @@
1
- /** The multiplayer part of the socket `hello` snapshot (multiplayer-impl.md §4.5). */
2
- import { z } from 'zod';
3
- import { Invite, Party } from './parties';
4
- import { RoomInfo } from './rooms';
5
-
6
- /**
7
- * The multiplayer fields of the `hello` ack, to spread into the snapshot
8
- * schema (`z.object({ ...other, ...MultiplayerSnapshot.shape })`).
9
- */
10
- export const MultiplayerSnapshot = z.object({
11
- /** Pending invites addressed to me. */
12
- invites: z.array(Invite),
13
- /** The party I'm in, or `null`. */
14
- party: Party.nullable(),
15
- /** The room seat I still hold (not ended), or `null`; `room.join` it again to reconnect. */
16
- room: RoomInfo.nullable(),
17
- });
1
+ /** The multiplayer part of the socket `hello` snapshot (multiplayer-impl.md §4.5). */
2
+ import { z } from 'zod';
3
+ import { Invite, Party } from './parties';
4
+ import { RoomInfo } from './rooms';
5
+
6
+ /**
7
+ * The multiplayer fields of the `hello` ack, to spread into the snapshot
8
+ * schema (`z.object({ ...other, ...MultiplayerSnapshot.shape })`).
9
+ */
10
+ export const MultiplayerSnapshot = z.object({
11
+ /** Pending invites addressed to me. */
12
+ invites: z.array(Invite),
13
+ /** The party I'm in, or `null`. */
14
+ party: Party.nullable(),
15
+ /** The room seat I still hold (not ended), or `null`; `room.join` it again to reconnect. */
16
+ room: RoomInfo.nullable(),
17
+ });