@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -21
- package/README.md +26 -26
- package/dist/index.d.ts +2683 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +862 -18
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
- package/src/api/analytics.ts +81 -81
- package/src/api/auth.ts +77 -77
- package/src/api/catalog-activity.ts +66 -0
- package/src/api/catalog.ts +177 -171
- package/src/api/daily.ts +122 -122
- package/src/api/economy/bundle.ts +69 -69
- package/src/api/index.ts +92 -85
- package/src/api/leaderboards.ts +151 -151
- package/src/api/messages.ts +142 -142
- package/src/api/mp/chat.ts +36 -36
- package/src/api/mp/invite-links.ts +42 -0
- package/src/api/mp/parties.ts +5 -0
- package/src/api/mp/races.ts +211 -211
- package/src/api/mp/rooms.ts +122 -90
- package/src/api/mp/snapshot.ts +17 -17
- package/src/api/mp.ts +135 -110
- package/src/api/notify.ts +114 -111
- package/src/api/offline.ts +45 -0
- package/src/api/progress/game-data.ts +189 -189
- package/src/api/progress.ts +59 -59
- package/src/api/race.ts +47 -47
- package/src/api/reports.ts +208 -208
- package/src/api/social/config.ts +47 -47
- package/src/api/social/endpoints.ts +179 -179
- package/src/api/social/people.ts +118 -118
- package/src/api/social/presence.ts +95 -95
- package/src/api/social.ts +9 -9
- package/src/api/tournaments/endpoints.ts +102 -0
- package/src/api/tournaments/play-online.ts +71 -0
- package/src/api/tournaments/shapes.ts +202 -0
- package/src/api/tournaments.ts +7 -0
- package/src/api/version.ts +40 -40
- package/src/coerce.ts +50 -50
- package/src/common.ts +42 -42
- package/src/entities.ts +62 -62
- package/src/errors/codes/core.ts +10 -10
- package/src/errors/codes/multiplayer.ts +56 -43
- package/src/errors/codes/play.ts +5 -5
- package/src/errors/codes/social.ts +31 -31
- package/src/errors/index.ts +88 -88
- package/src/index.ts +16 -16
- package/src/limits.ts +125 -100
- package/src/logic/daily.ts +54 -54
- package/src/logic/index.ts +8 -8
- package/src/logic/presence.ts +62 -62
- package/src/logic/time.ts +81 -81
- package/src/manifest/changes.ts +55 -55
- package/src/manifest/controls.ts +42 -42
- package/src/manifest/fields.ts +89 -89
- package/src/manifest/game.ts +139 -132
- package/src/manifest/index.ts +13 -12
- package/src/manifest/inject.ts +31 -31
- package/src/manifest/labels.ts +99 -0
- package/src/manifest/leaderboards.ts +29 -29
- package/src/manifest/multiplayer.ts +79 -71
- package/src/manifest/race.ts +93 -93
- package/src/middleware.ts +47 -47
- package/src/realtime/define.ts +52 -52
- package/src/realtime/envelope.ts +67 -67
- package/src/realtime/events/multiplayer.ts +9 -8
- package/src/realtime/events/parties.ts +31 -31
- package/src/realtime/events/races.ts +27 -27
- package/src/realtime/events/rooms.ts +52 -48
- package/src/realtime/events/social.ts +81 -81
- package/src/realtime/events/tournaments.ts +12 -0
- package/src/realtime/index.ts +24 -24
- package/src/realtime/ops/empty.ts +8 -8
- package/src/realtime/ops/hello.ts +40 -40
- package/src/realtime/ops/multiplayer.ts +8 -8
- package/src/realtime/ops/parties.ts +13 -13
- package/src/realtime/ops/races.ts +99 -99
- package/src/realtime/ops/rooms.ts +22 -8
- package/src/realtime/ops/social.ts +58 -58
package/src/realtime/envelope.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The legacy `/_ws` envelope (multiplayer-impl.md §4.2), unchanged since v1
|
|
3
|
-
* and kept indefinitely (plan D6):
|
|
4
|
-
*
|
|
5
|
-
* ```
|
|
6
|
-
* client → server {t:'op', id, op, d}
|
|
7
|
-
* server → client {t:'ack', id, ok:true, d}
|
|
8
|
-
* {t:'ack', id, ok:false, err: ErrorBody}
|
|
9
|
-
* {t:'ev', ev, d}
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
import { z } from 'zod';
|
|
13
|
-
import { Json } from '../common';
|
|
14
|
-
import { ErrorBody, type ErrorCode } from '../errors';
|
|
15
|
-
|
|
16
|
-
/** A client's op frame. `id` is the client's own counter, echoed in the ack. */
|
|
17
|
-
export const OpFrame = z.object({
|
|
18
|
-
t: z.literal('op'),
|
|
19
|
-
id: z.number(),
|
|
20
|
-
op: z.string(),
|
|
21
|
-
d: Json.optional(),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
/** An op frame. */
|
|
25
|
-
export type OpFrame = z.infer<typeof OpFrame>;
|
|
26
|
-
|
|
27
|
-
/** A successful ack. */
|
|
28
|
-
export const OkAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(true), d: Json });
|
|
29
|
-
|
|
30
|
-
/** A failed ack. */
|
|
31
|
-
export const ErrAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(false), err: ErrorBody });
|
|
32
|
-
|
|
33
|
-
/** An ack frame: the server's answer to one op. */
|
|
34
|
-
export const AckFrame = z.discriminatedUnion('ok', [OkAckFrame, ErrAckFrame]);
|
|
35
|
-
|
|
36
|
-
/** An ack frame. */
|
|
37
|
-
export type AckFrame = z.infer<typeof AckFrame>;
|
|
38
|
-
|
|
39
|
-
/** A server-pushed event frame. */
|
|
40
|
-
export const EvFrame = z.object({ t: z.literal('ev'), ev: z.string(), d: Json });
|
|
41
|
-
|
|
42
|
-
/** An event frame. */
|
|
43
|
-
export type EvFrame = z.infer<typeof EvFrame>;
|
|
44
|
-
|
|
45
|
-
/** Any frame the server sends. */
|
|
46
|
-
export const ServerFrame = z.union([AckFrame, EvFrame]);
|
|
47
|
-
|
|
48
|
-
/** Close codes the client understands. */
|
|
49
|
-
export const CloseCode = {
|
|
50
|
-
/** Service restart: reconnect quietly. */
|
|
51
|
-
restart: 1012,
|
|
52
|
-
/** The account is suspended. */
|
|
53
|
-
suspended: 4003,
|
|
54
|
-
/** Too many sockets for this user or IP. */
|
|
55
|
-
tooMany: 4029,
|
|
56
|
-
/** A frame that wasn't a valid op. */
|
|
57
|
-
badMessage: 4400,
|
|
58
|
-
} as const;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Codes any op can fail with, raised by the envelope rather than the op:
|
|
62
|
-
* the per-socket rate limit, an unknown op name, and ops sent before `hello`.
|
|
63
|
-
*/
|
|
64
|
-
export const ENVELOPE_ERRORS: readonly ErrorCode[] = ['rate_limited', 'unknown_op', 'hello_required'];
|
|
65
|
-
|
|
66
|
-
/** Ops the server accepts before `hello`. */
|
|
67
|
-
export const PRE_HELLO_OPS: readonly string[] = ['hello', 'ping'];
|
|
1
|
+
/**
|
|
2
|
+
* The legacy `/_ws` envelope (multiplayer-impl.md §4.2), unchanged since v1
|
|
3
|
+
* and kept indefinitely (plan D6):
|
|
4
|
+
*
|
|
5
|
+
* ```
|
|
6
|
+
* client → server {t:'op', id, op, d}
|
|
7
|
+
* server → client {t:'ack', id, ok:true, d}
|
|
8
|
+
* {t:'ack', id, ok:false, err: ErrorBody}
|
|
9
|
+
* {t:'ev', ev, d}
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { Json } from '../common';
|
|
14
|
+
import { ErrorBody, type ErrorCode } from '../errors';
|
|
15
|
+
|
|
16
|
+
/** A client's op frame. `id` is the client's own counter, echoed in the ack. */
|
|
17
|
+
export const OpFrame = z.object({
|
|
18
|
+
t: z.literal('op'),
|
|
19
|
+
id: z.number(),
|
|
20
|
+
op: z.string(),
|
|
21
|
+
d: Json.optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** An op frame. */
|
|
25
|
+
export type OpFrame = z.infer<typeof OpFrame>;
|
|
26
|
+
|
|
27
|
+
/** A successful ack. */
|
|
28
|
+
export const OkAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(true), d: Json });
|
|
29
|
+
|
|
30
|
+
/** A failed ack. */
|
|
31
|
+
export const ErrAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(false), err: ErrorBody });
|
|
32
|
+
|
|
33
|
+
/** An ack frame: the server's answer to one op. */
|
|
34
|
+
export const AckFrame = z.discriminatedUnion('ok', [OkAckFrame, ErrAckFrame]);
|
|
35
|
+
|
|
36
|
+
/** An ack frame. */
|
|
37
|
+
export type AckFrame = z.infer<typeof AckFrame>;
|
|
38
|
+
|
|
39
|
+
/** A server-pushed event frame. */
|
|
40
|
+
export const EvFrame = z.object({ t: z.literal('ev'), ev: z.string(), d: Json });
|
|
41
|
+
|
|
42
|
+
/** An event frame. */
|
|
43
|
+
export type EvFrame = z.infer<typeof EvFrame>;
|
|
44
|
+
|
|
45
|
+
/** Any frame the server sends. */
|
|
46
|
+
export const ServerFrame = z.union([AckFrame, EvFrame]);
|
|
47
|
+
|
|
48
|
+
/** Close codes the client understands. */
|
|
49
|
+
export const CloseCode = {
|
|
50
|
+
/** Service restart: reconnect quietly. */
|
|
51
|
+
restart: 1012,
|
|
52
|
+
/** The account is suspended. */
|
|
53
|
+
suspended: 4003,
|
|
54
|
+
/** Too many sockets for this user or IP. */
|
|
55
|
+
tooMany: 4029,
|
|
56
|
+
/** A frame that wasn't a valid op. */
|
|
57
|
+
badMessage: 4400,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Codes any op can fail with, raised by the envelope rather than the op:
|
|
62
|
+
* the per-socket rate limit, an unknown op name, and ops sent before `hello`.
|
|
63
|
+
*/
|
|
64
|
+
export const ENVELOPE_ERRORS: readonly ErrorCode[] = ['rate_limited', 'unknown_op', 'hello_required'];
|
|
65
|
+
|
|
66
|
+
/** Ops the server accepts before `hello`. */
|
|
67
|
+
export const PRE_HELLO_OPS: readonly string[] = ['hello', 'ping'];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/** Socket events for invites, parties, hub rooms and
|
|
2
|
-
import type { EventDef } from '../define';
|
|
3
|
-
import { partyEvents } from './parties';
|
|
4
|
-
import { raceEvents } from './races';
|
|
5
|
-
import { roomEvents } from './rooms';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/** Socket events for invites, parties, hub rooms, races and tournaments. */
|
|
2
|
+
import type { EventDef } from '../define';
|
|
3
|
+
import { partyEvents } from './parties';
|
|
4
|
+
import { raceEvents } from './races';
|
|
5
|
+
import { roomEvents } from './rooms';
|
|
6
|
+
import { tournamentEvents } from './tournaments';
|
|
7
|
+
|
|
8
|
+
/** The multiplayer events, by event name. */
|
|
9
|
+
export const multiplayerEvents = { ...partyEvents, ...roomEvents, ...raceEvents, ...tournamentEvents } satisfies Record<string, EventDef>;
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
/** Invite and party events (multiplayer-impl.md §5.6). */
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { PlayerLite } from '../../entities';
|
|
4
|
-
import { ChatLineEvent } from '../../api/mp/chat';
|
|
5
|
-
import { Invite, InviteStatus, OpaqueId, Party } from '../../api/mp/parties';
|
|
6
|
-
import { event, type EventDef } from '../define';
|
|
7
|
-
|
|
8
|
-
/** The invite and party events, by event name. */
|
|
9
|
-
export const partyEvents = {
|
|
10
|
-
invite: event({
|
|
11
|
-
d: Invite,
|
|
12
|
-
doc: 'A friend invited you to a game.',
|
|
13
|
-
}),
|
|
14
|
-
'invite.update': event({
|
|
15
|
-
d: z.object({
|
|
16
|
-
id: OpaqueId,
|
|
17
|
-
status: InviteStatus,
|
|
18
|
-
/** Who accepted or declined (absent for cancelled and expired). */
|
|
19
|
-
by: PlayerLite.optional(),
|
|
20
|
-
}),
|
|
21
|
-
doc: 'An invite ended; sent to the inviter and the invitee (so its toast closes).',
|
|
22
|
-
}),
|
|
23
|
-
party: event({
|
|
24
|
-
d: Party.nullable(),
|
|
25
|
-
doc: "Your party's current state, or null when you left it or it dissolved.",
|
|
26
|
-
}),
|
|
27
|
-
'party.chat': event({
|
|
28
|
-
d: ChatLineEvent,
|
|
29
|
-
doc: 'A party chat line.',
|
|
30
|
-
}),
|
|
31
|
-
} satisfies Record<string, EventDef>;
|
|
1
|
+
/** Invite and party events (multiplayer-impl.md §5.6). */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { PlayerLite } from '../../entities';
|
|
4
|
+
import { ChatLineEvent } from '../../api/mp/chat';
|
|
5
|
+
import { Invite, InviteStatus, OpaqueId, Party } from '../../api/mp/parties';
|
|
6
|
+
import { event, type EventDef } from '../define';
|
|
7
|
+
|
|
8
|
+
/** The invite and party events, by event name. */
|
|
9
|
+
export const partyEvents = {
|
|
10
|
+
invite: event({
|
|
11
|
+
d: Invite,
|
|
12
|
+
doc: 'A friend invited you to a game.',
|
|
13
|
+
}),
|
|
14
|
+
'invite.update': event({
|
|
15
|
+
d: z.object({
|
|
16
|
+
id: OpaqueId,
|
|
17
|
+
status: InviteStatus,
|
|
18
|
+
/** Who accepted or declined (absent for cancelled and expired). */
|
|
19
|
+
by: PlayerLite.optional(),
|
|
20
|
+
}),
|
|
21
|
+
doc: 'An invite ended; sent to the inviter and the invitee (so its toast closes).',
|
|
22
|
+
}),
|
|
23
|
+
party: event({
|
|
24
|
+
d: Party.nullable(),
|
|
25
|
+
doc: "Your party's current state, or null when you left it or it dissolved.",
|
|
26
|
+
}),
|
|
27
|
+
'party.chat': event({
|
|
28
|
+
d: ChatLineEvent,
|
|
29
|
+
doc: 'A party chat line.',
|
|
30
|
+
}),
|
|
31
|
+
} satisfies Record<string, EventDef>;
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/** Race events (docs/plans/race.md §4). */
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { UserId } from '../../common';
|
|
4
|
-
import { RaceProgress, RaceResult, RaceStart, RaceStats } from '../../api/mp/races';
|
|
5
|
-
import { event, type EventDef } from '../define';
|
|
6
|
-
|
|
7
|
-
/** The race events, by event name. */
|
|
8
|
-
export const raceEvents = {
|
|
9
|
-
'race.start': event({
|
|
10
|
-
d: RaceStart,
|
|
11
|
-
doc: 'The countdown began: the seed is revealed now.',
|
|
12
|
-
}),
|
|
13
|
-
'race.status': event({
|
|
14
|
-
d: z.object({
|
|
15
|
-
userId: UserId,
|
|
16
|
-
stats: RaceStats,
|
|
17
|
-
progress: RaceProgress.optional(),
|
|
18
|
-
/** ms since the go (server clock). */
|
|
19
|
-
at: z.number().int().nonnegative(),
|
|
20
|
-
}),
|
|
21
|
-
doc: "Another racer's live status, for the race HUD.",
|
|
22
|
-
}),
|
|
23
|
-
'race.result': event({
|
|
24
|
-
d: z.object({ result: RaceResult }),
|
|
25
|
-
doc: 'The race was decided.',
|
|
26
|
-
}),
|
|
27
|
-
} satisfies Record<string, EventDef>;
|
|
1
|
+
/** Race events (docs/plans/race.md §4). */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { UserId } from '../../common';
|
|
4
|
+
import { RaceProgress, RaceResult, RaceStart, RaceStats } from '../../api/mp/races';
|
|
5
|
+
import { event, type EventDef } from '../define';
|
|
6
|
+
|
|
7
|
+
/** The race events, by event name. */
|
|
8
|
+
export const raceEvents = {
|
|
9
|
+
'race.start': event({
|
|
10
|
+
d: RaceStart,
|
|
11
|
+
doc: 'The countdown began: the seed is revealed now.',
|
|
12
|
+
}),
|
|
13
|
+
'race.status': event({
|
|
14
|
+
d: z.object({
|
|
15
|
+
userId: UserId,
|
|
16
|
+
stats: RaceStats,
|
|
17
|
+
progress: RaceProgress.optional(),
|
|
18
|
+
/** ms since the go (server clock). */
|
|
19
|
+
at: z.number().int().nonnegative(),
|
|
20
|
+
}),
|
|
21
|
+
doc: "Another racer's live status, for the race HUD.",
|
|
22
|
+
}),
|
|
23
|
+
'race.result': event({
|
|
24
|
+
d: z.object({ result: RaceResult }),
|
|
25
|
+
doc: 'The race was decided.',
|
|
26
|
+
}),
|
|
27
|
+
} satisfies Record<string, EventDef>;
|
|
@@ -1,48 +1,52 @@
|
|
|
1
|
-
/** Hub relay room events (multiplayer-impl.md §6). Race rooms send these too. */
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { Json, UserId } from '../../common';
|
|
4
|
-
import { ChatLineEvent } from '../../api/mp/chat';
|
|
5
|
-
import { OpaqueId } from '../../api/mp/parties';
|
|
6
|
-
import { RoomClosedReason, RoomInfo, RoomMessage, RoomState, SeatGoneReason } from '../../api/mp/rooms';
|
|
7
|
-
import { event, type EventDef } from '../define';
|
|
8
|
-
|
|
9
|
-
/** The room events, by event name. */
|
|
10
|
-
export const roomEvents = {
|
|
11
|
-
room: event({
|
|
12
|
-
d: z.object({
|
|
13
|
-
room: RoomInfo,
|
|
14
|
-
/** What the host passed to `room.end`; present once, on that update. */
|
|
15
|
-
results: Json.optional(),
|
|
16
|
-
}),
|
|
17
|
-
doc: 'The room changed: roster, ready flags, phase, host, race state.',
|
|
18
|
-
}),
|
|
19
|
-
'room.msg': event({
|
|
20
|
-
d: RoomMessage,
|
|
21
|
-
doc: 'A relayed game message.',
|
|
22
|
-
}),
|
|
23
|
-
'room.state': event({
|
|
24
|
-
d: z.object({
|
|
25
|
-
/** Shallow-merged into the state unless `replace`; a `null` value deletes its key. */
|
|
26
|
-
patch: RoomState,
|
|
27
|
-
replace: z.boolean(),
|
|
28
|
-
seq: z.number().int().positive(),
|
|
29
|
-
}),
|
|
30
|
-
doc: 'The shared state changed.',
|
|
31
|
-
}),
|
|
32
|
-
'room.chat': event({
|
|
33
|
-
d: ChatLineEvent,
|
|
34
|
-
doc: 'A room chat line.',
|
|
35
|
-
}),
|
|
36
|
-
'room.kicked': event({
|
|
37
|
-
d: z.object({ roomId: OpaqueId }),
|
|
38
|
-
doc: 'The host removed you from the room.',
|
|
39
|
-
}),
|
|
40
|
-
'room.closed': event({
|
|
41
|
-
d: z.object({ reason: RoomClosedReason }),
|
|
42
|
-
doc: 'The room is gone for this tab (emptied, ended,
|
|
43
|
-
}),
|
|
44
|
-
'room.abandoned': event({
|
|
45
|
-
d: z.object({ userId: UserId, reason: SeatGoneReason }),
|
|
46
|
-
doc: 'A player left mid-game (or their reconnect grace ran out); the game decides what that means.',
|
|
47
|
-
}),
|
|
48
|
-
|
|
1
|
+
/** Hub relay room events (multiplayer-impl.md §6). Race rooms send these too. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Json, UserId } from '../../common';
|
|
4
|
+
import { ChatLineEvent } from '../../api/mp/chat';
|
|
5
|
+
import { OpaqueId } from '../../api/mp/parties';
|
|
6
|
+
import { RoomClosedReason, RoomInfo, RoomMessage, RoomState, SeatChange, SeatGoneReason } from '../../api/mp/rooms';
|
|
7
|
+
import { event, type EventDef } from '../define';
|
|
8
|
+
|
|
9
|
+
/** The room events, by event name. */
|
|
10
|
+
export const roomEvents = {
|
|
11
|
+
room: event({
|
|
12
|
+
d: z.object({
|
|
13
|
+
room: RoomInfo,
|
|
14
|
+
/** What the host passed to `room.end`; present once, on that update. */
|
|
15
|
+
results: Json.optional(),
|
|
16
|
+
}),
|
|
17
|
+
doc: 'The room changed: roster, ready flags, phase, host, race state.',
|
|
18
|
+
}),
|
|
19
|
+
'room.msg': event({
|
|
20
|
+
d: RoomMessage,
|
|
21
|
+
doc: 'A relayed game message.',
|
|
22
|
+
}),
|
|
23
|
+
'room.state': event({
|
|
24
|
+
d: z.object({
|
|
25
|
+
/** Shallow-merged into the state unless `replace`; a `null` value deletes its key. */
|
|
26
|
+
patch: RoomState,
|
|
27
|
+
replace: z.boolean(),
|
|
28
|
+
seq: z.number().int().positive(),
|
|
29
|
+
}),
|
|
30
|
+
doc: 'The shared state changed.',
|
|
31
|
+
}),
|
|
32
|
+
'room.chat': event({
|
|
33
|
+
d: ChatLineEvent,
|
|
34
|
+
doc: 'A room chat line.',
|
|
35
|
+
}),
|
|
36
|
+
'room.kicked': event({
|
|
37
|
+
d: z.object({ roomId: OpaqueId }),
|
|
38
|
+
doc: 'The host removed you from the room.',
|
|
39
|
+
}),
|
|
40
|
+
'room.closed': event({
|
|
41
|
+
d: z.object({ reason: RoomClosedReason }),
|
|
42
|
+
doc: 'The room is gone for this tab (emptied, ended, moved, another tab took the seat, removed). `host_left` is no longer sent: the host role passes on.',
|
|
43
|
+
}),
|
|
44
|
+
'room.abandoned': event({
|
|
45
|
+
d: z.object({ userId: UserId, reason: SeatGoneReason }),
|
|
46
|
+
doc: 'A player left mid-game (or their reconnect grace ran out); the game decides what that means.',
|
|
47
|
+
}),
|
|
48
|
+
'room.seat': event({
|
|
49
|
+
d: SeatChange,
|
|
50
|
+
doc: 'A seat went away (held for the grace period), came back (reclaimed) or is gone. The game hands an away seat to its computer player and gives it back on `back`.',
|
|
51
|
+
}),
|
|
52
|
+
} satisfies Record<string, EventDef>;
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket events for presence, counts, friends, typing, config, DMs, notices
|
|
3
|
-
* and suspension (multiplayer-impl.md §4.6). Every event goes to all of the
|
|
4
|
-
* recipient's tabs.
|
|
5
|
-
*/
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { Millis, UserId } from '../../common';
|
|
8
|
-
import { PlayerLite } from '../../entities';
|
|
9
|
-
import { Message, MessageId } from '../../api/messages';
|
|
10
|
-
import { SocialConfig } from '../../api/social/config';
|
|
11
|
-
import { FriendChange } from '../../api/social/people';
|
|
12
|
-
import { Counts, Presence } from '../../api/social/presence';
|
|
13
|
-
import { event, type EventDef } from '../define';
|
|
14
|
-
|
|
15
|
-
/** What kind of thing a notice is about. `renamed`: an admin changed your username. */
|
|
16
|
-
export const NoticeKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open', 'invite', 'suggestion', 'renamed']);
|
|
17
|
-
|
|
18
|
-
/** A notice kind. */
|
|
19
|
-
export type NoticeKind = z.infer<typeof NoticeKind>;
|
|
20
|
-
|
|
21
|
-
/** A toast (`ev:notice`), sent instead of a push when the player has a visible tab. */
|
|
22
|
-
export const Notice = z.object({
|
|
23
|
-
kind: NoticeKind,
|
|
24
|
-
title: z.string(),
|
|
25
|
-
body: z.string().optional(),
|
|
26
|
-
/** A button: its label and the page it opens. */
|
|
27
|
-
action: z.object({ label: z.string(), url: z.string() }).optional(),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
/** A toast. */
|
|
31
|
-
export type Notice = z.infer<typeof Notice>;
|
|
32
|
-
|
|
33
|
-
/** The social events, by event name. */
|
|
34
|
-
export const socialEvents = {
|
|
35
|
-
presence: event({
|
|
36
|
-
d: z.object({
|
|
37
|
-
userId: UserId,
|
|
38
|
-
/** `null`: offline, or no longer visible to you. */
|
|
39
|
-
p: Presence.nullable(),
|
|
40
|
-
}),
|
|
41
|
-
doc: "A friend's presence changed.",
|
|
42
|
-
}),
|
|
43
|
-
counts: event({ d: Counts, doc: 'The online counts changed (broadcast to every tab, at most every 15 s).' }),
|
|
44
|
-
friends: event({
|
|
45
|
-
d: z.object({ change: FriendChange, user: PlayerLite }),
|
|
46
|
-
doc: 'Your friends list changed (refetch `GET /social/friends`).',
|
|
47
|
-
}),
|
|
48
|
-
typing: event({ d: z.object({ from: UserId }), doc: 'A friend is typing to you.' }),
|
|
49
|
-
config: event({ d: SocialConfig, doc: 'The site-wide social configuration changed (broadcast).' }),
|
|
50
|
-
dm: event({
|
|
51
|
-
d: z.object({
|
|
52
|
-
message: Message,
|
|
53
|
-
/** The other side of the conversation, from your point of view. */
|
|
54
|
-
conversationWith: UserId,
|
|
55
|
-
}),
|
|
56
|
-
doc: 'A DM was delivered: to the recipient, and to the sender\'s other tabs.',
|
|
57
|
-
}),
|
|
58
|
-
'dm.read': event({
|
|
59
|
-
d: z.object({
|
|
60
|
-
/** Who read. */
|
|
61
|
-
by: UserId,
|
|
62
|
-
/** Their messages up to and including this id are read. */
|
|
63
|
-
upTo: MessageId,
|
|
64
|
-
}),
|
|
65
|
-
doc: 'Messages were read (to the sender, and to the reader\'s own tabs to sync badges).',
|
|
66
|
-
}),
|
|
67
|
-
'dm.hidden': event({
|
|
68
|
-
d: z.object({ ids: z.array(MessageId) }),
|
|
69
|
-
doc: 'A moderator hid these messages: drop them from both sides of the conversation.',
|
|
70
|
-
}),
|
|
71
|
-
notice: event({ d: Notice, doc: 'A toast to show.' }),
|
|
72
|
-
suspended: event({
|
|
73
|
-
d: z.object({
|
|
74
|
-
/** When the suspension ends (epoch ms). */
|
|
75
|
-
until: Millis,
|
|
76
|
-
/** The moderator's reason, when given. */
|
|
77
|
-
reason: z.string().optional(),
|
|
78
|
-
}),
|
|
79
|
-
doc: 'The account is suspended; the socket then closes with 4003.',
|
|
80
|
-
}),
|
|
81
|
-
} satisfies Record<string, EventDef>;
|
|
1
|
+
/**
|
|
2
|
+
* Socket events for presence, counts, friends, typing, config, DMs, notices
|
|
3
|
+
* and suspension (multiplayer-impl.md §4.6). Every event goes to all of the
|
|
4
|
+
* recipient's tabs.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Millis, UserId } from '../../common';
|
|
8
|
+
import { PlayerLite } from '../../entities';
|
|
9
|
+
import { Message, MessageId } from '../../api/messages';
|
|
10
|
+
import { SocialConfig } from '../../api/social/config';
|
|
11
|
+
import { FriendChange } from '../../api/social/people';
|
|
12
|
+
import { Counts, Presence } from '../../api/social/presence';
|
|
13
|
+
import { event, type EventDef } from '../define';
|
|
14
|
+
|
|
15
|
+
/** What kind of thing a notice is about. `renamed`: an admin changed your username; `tournament`: a match is ready, a start is near, a forfeit warning. */
|
|
16
|
+
export const NoticeKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open', 'invite', 'suggestion', 'renamed', 'tournament']);
|
|
17
|
+
|
|
18
|
+
/** A notice kind. */
|
|
19
|
+
export type NoticeKind = z.infer<typeof NoticeKind>;
|
|
20
|
+
|
|
21
|
+
/** A toast (`ev:notice`), sent instead of a push when the player has a visible tab. */
|
|
22
|
+
export const Notice = z.object({
|
|
23
|
+
kind: NoticeKind,
|
|
24
|
+
title: z.string(),
|
|
25
|
+
body: z.string().optional(),
|
|
26
|
+
/** A button: its label and the page it opens. */
|
|
27
|
+
action: z.object({ label: z.string(), url: z.string() }).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** A toast. */
|
|
31
|
+
export type Notice = z.infer<typeof Notice>;
|
|
32
|
+
|
|
33
|
+
/** The social events, by event name. */
|
|
34
|
+
export const socialEvents = {
|
|
35
|
+
presence: event({
|
|
36
|
+
d: z.object({
|
|
37
|
+
userId: UserId,
|
|
38
|
+
/** `null`: offline, or no longer visible to you. */
|
|
39
|
+
p: Presence.nullable(),
|
|
40
|
+
}),
|
|
41
|
+
doc: "A friend's presence changed.",
|
|
42
|
+
}),
|
|
43
|
+
counts: event({ d: Counts, doc: 'The online counts changed (broadcast to every tab, at most every 15 s).' }),
|
|
44
|
+
friends: event({
|
|
45
|
+
d: z.object({ change: FriendChange, user: PlayerLite }),
|
|
46
|
+
doc: 'Your friends list changed (refetch `GET /social/friends`).',
|
|
47
|
+
}),
|
|
48
|
+
typing: event({ d: z.object({ from: UserId }), doc: 'A friend is typing to you.' }),
|
|
49
|
+
config: event({ d: SocialConfig, doc: 'The site-wide social configuration changed (broadcast).' }),
|
|
50
|
+
dm: event({
|
|
51
|
+
d: z.object({
|
|
52
|
+
message: Message,
|
|
53
|
+
/** The other side of the conversation, from your point of view. */
|
|
54
|
+
conversationWith: UserId,
|
|
55
|
+
}),
|
|
56
|
+
doc: 'A DM was delivered: to the recipient, and to the sender\'s other tabs.',
|
|
57
|
+
}),
|
|
58
|
+
'dm.read': event({
|
|
59
|
+
d: z.object({
|
|
60
|
+
/** Who read. */
|
|
61
|
+
by: UserId,
|
|
62
|
+
/** Their messages up to and including this id are read. */
|
|
63
|
+
upTo: MessageId,
|
|
64
|
+
}),
|
|
65
|
+
doc: 'Messages were read (to the sender, and to the reader\'s own tabs to sync badges).',
|
|
66
|
+
}),
|
|
67
|
+
'dm.hidden': event({
|
|
68
|
+
d: z.object({ ids: z.array(MessageId) }),
|
|
69
|
+
doc: 'A moderator hid these messages: drop them from both sides of the conversation.',
|
|
70
|
+
}),
|
|
71
|
+
notice: event({ d: Notice, doc: 'A toast to show.' }),
|
|
72
|
+
suspended: event({
|
|
73
|
+
d: z.object({
|
|
74
|
+
/** When the suspension ends (epoch ms). */
|
|
75
|
+
until: Millis,
|
|
76
|
+
/** The moderator's reason, when given. */
|
|
77
|
+
reason: z.string().optional(),
|
|
78
|
+
}),
|
|
79
|
+
doc: 'The account is suspended; the socket then closes with 4003.',
|
|
80
|
+
}),
|
|
81
|
+
} satisfies Record<string, EventDef>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Tournament events (batch-2026-10 R15). */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { TournamentSummary } from '../../api/tournaments/shapes';
|
|
4
|
+
import { event, type EventDef } from '../define';
|
|
5
|
+
|
|
6
|
+
/** The tournament events, by event name. */
|
|
7
|
+
export const tournamentEvents = {
|
|
8
|
+
tournament: event({
|
|
9
|
+
d: z.object({ tournament: TournamentSummary }),
|
|
10
|
+
doc: 'A tournament you are in, follow, created or were invited to changed (sign-ups, the start, a match ready or decided, the end).',
|
|
11
|
+
}),
|
|
12
|
+
} satisfies Record<string, EventDef>;
|
package/src/realtime/index.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `/_ws` socket contract: the legacy envelope plus every op and event,
|
|
3
|
-
* by name.
|
|
4
|
-
*/
|
|
5
|
-
import { multiplayerEvents } from './events/multiplayer';
|
|
6
|
-
import { socialEvents } from './events/social';
|
|
7
|
-
import { helloOps } from './ops/hello';
|
|
8
|
-
import { multiplayerOps } from './ops/multiplayer';
|
|
9
|
-
import { socialOps } from './ops/social';
|
|
10
|
-
|
|
11
|
-
export * from './define';
|
|
12
|
-
export * from './envelope';
|
|
13
|
-
|
|
14
|
-
/** Every socket op, by op name (an intersection type keeps the .d.ts small). */
|
|
15
|
-
export const ops: typeof helloOps & typeof socialOps & typeof multiplayerOps = { ...helloOps, ...socialOps, ...multiplayerOps };
|
|
16
|
-
|
|
17
|
-
/** Every socket event, by event name (an intersection type keeps the .d.ts small). */
|
|
18
|
-
export const events: typeof socialEvents & typeof multiplayerEvents = { ...socialEvents, ...multiplayerEvents };
|
|
19
|
-
|
|
20
|
-
/** A socket op name. */
|
|
21
|
-
export type OpName = keyof typeof ops;
|
|
22
|
-
|
|
23
|
-
/** A socket event name. */
|
|
24
|
-
export type EventName = keyof typeof events;
|
|
1
|
+
/**
|
|
2
|
+
* The `/_ws` socket contract: the legacy envelope plus every op and event,
|
|
3
|
+
* by name.
|
|
4
|
+
*/
|
|
5
|
+
import { multiplayerEvents } from './events/multiplayer';
|
|
6
|
+
import { socialEvents } from './events/social';
|
|
7
|
+
import { helloOps } from './ops/hello';
|
|
8
|
+
import { multiplayerOps } from './ops/multiplayer';
|
|
9
|
+
import { socialOps } from './ops/social';
|
|
10
|
+
|
|
11
|
+
export * from './define';
|
|
12
|
+
export * from './envelope';
|
|
13
|
+
|
|
14
|
+
/** Every socket op, by op name (an intersection type keeps the .d.ts small). */
|
|
15
|
+
export const ops: typeof helloOps & typeof socialOps & typeof multiplayerOps = { ...helloOps, ...socialOps, ...multiplayerOps };
|
|
16
|
+
|
|
17
|
+
/** Every socket event, by event name (an intersection type keeps the .d.ts small). */
|
|
18
|
+
export const events: typeof socialEvents & typeof multiplayerEvents = { ...socialEvents, ...multiplayerEvents };
|
|
19
|
+
|
|
20
|
+
/** A socket op name. */
|
|
21
|
+
export type OpName = keyof typeof ops;
|
|
22
|
+
|
|
23
|
+
/** A socket event name. */
|
|
24
|
+
export type EventName = keyof typeof events;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** The empty input and ack many socket ops share. */
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
|
|
4
|
-
/** An op that takes no input (`d` absent or `{}`). */
|
|
5
|
-
export const NoInput = z.object({});
|
|
6
|
-
|
|
7
|
-
/** An ack with no payload (`{}`). */
|
|
8
|
-
export const EmptyAck = z.object({});
|
|
1
|
+
/** The empty input and ack many socket ops share. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/** An op that takes no input (`d` absent or `{}`). */
|
|
5
|
+
export const NoInput = z.object({});
|
|
6
|
+
|
|
7
|
+
/** An ack with no payload (`{}`). */
|
|
8
|
+
export const EmptyAck = z.object({});
|