@diffenderfer-games/hub-contract 2.0.0-next.1 → 2.0.0-next.3
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 +26 -5
- package/dist/index.d.ts +2831 -187
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +862 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/catalog-activity.ts +66 -0
- package/src/api/catalog.ts +7 -1
- package/src/api/index.ts +7 -0
- package/src/api/mp/invite-links.ts +42 -0
- package/src/api/mp/parties.ts +5 -0
- package/src/api/mp/rooms.ts +34 -2
- package/src/api/mp.ts +25 -0
- package/src/api/notify.ts +4 -1
- package/src/api/offline.ts +45 -0
- 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/errors/codes/multiplayer.ts +14 -1
- package/src/limits.ts +25 -0
- package/src/manifest/game.ts +7 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/labels.ts +99 -0
- package/src/manifest/multiplayer.ts +8 -0
- package/src/realtime/events/multiplayer.ts +3 -2
- package/src/realtime/events/rooms.ts +6 -2
- package/src/realtime/events/social.ts +2 -2
- package/src/realtime/events/tournaments.ts +12 -0
- package/src/realtime/ops/rooms.ts +21 -7
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ const CORE_CODES = [
|
|
|
96
96
|
];
|
|
97
97
|
//#endregion
|
|
98
98
|
//#region src/errors/codes/multiplayer.ts
|
|
99
|
-
/** Error codes from invites, parties, hub rooms, races and the socket envelope. */
|
|
99
|
+
/** Error codes from invites, invite links, parties, hub rooms, races, tournaments and the socket envelope. */
|
|
100
100
|
const MULTIPLAYER_CODES = [
|
|
101
101
|
"unknown_op",
|
|
102
102
|
"hello_required",
|
|
@@ -113,6 +113,9 @@ const MULTIPLAYER_CODES = [
|
|
|
113
113
|
"bad_info",
|
|
114
114
|
"launch_invalid",
|
|
115
115
|
"not_joinable",
|
|
116
|
+
"link_invalid",
|
|
117
|
+
"link_room_gone",
|
|
118
|
+
"too_many_links",
|
|
116
119
|
"bad_quick",
|
|
117
120
|
"room_not_found",
|
|
118
121
|
"not_in_room",
|
|
@@ -131,7 +134,15 @@ const MULTIPLAYER_CODES = [
|
|
|
131
134
|
"race_over",
|
|
132
135
|
"too_fast",
|
|
133
136
|
"bad_stats",
|
|
134
|
-
"bad_param"
|
|
137
|
+
"bad_param",
|
|
138
|
+
"tournament_not_found",
|
|
139
|
+
"tournament_closed",
|
|
140
|
+
"tournament_full",
|
|
141
|
+
"tournament_over",
|
|
142
|
+
"not_creator",
|
|
143
|
+
"not_entered",
|
|
144
|
+
"cannot_join",
|
|
145
|
+
"match_not_ready"
|
|
135
146
|
];
|
|
136
147
|
//#endregion
|
|
137
148
|
//#region src/errors/codes/play.ts
|
|
@@ -605,6 +616,30 @@ const MANIFEST_LIMITS = {
|
|
|
605
616
|
/** Largest `game.race.minMs`, in ms (10 minutes). */
|
|
606
617
|
raceMinMsMax: 6e5
|
|
607
618
|
};
|
|
619
|
+
/** What a tournament may be (batch-2026-10 R15). The server enforces each one. */
|
|
620
|
+
const TOURNAMENT_LIMITS = {
|
|
621
|
+
/** Longest title, in characters. */
|
|
622
|
+
titleMax: 48,
|
|
623
|
+
/** Fewest players. */
|
|
624
|
+
playersMin: 2,
|
|
625
|
+
/** Most players. */
|
|
626
|
+
playersMax: 64,
|
|
627
|
+
/** Most games in the set a tournament picks from. */
|
|
628
|
+
gamesMax: 8,
|
|
629
|
+
/** Lives per player: 1 single, 2 double, 3 triple elimination. */
|
|
630
|
+
livesMax: 3,
|
|
631
|
+
/** The default and the longest window to join a ready match before forfeiting, in minutes. */
|
|
632
|
+
readyMinutesDefault: 10,
|
|
633
|
+
readyMinutesMax: 60,
|
|
634
|
+
/** The longest "remind me before it starts", in minutes (one day). */
|
|
635
|
+
leadMinutesMax: 1440,
|
|
636
|
+
/** The default reminder for players who signed up for a scheduled tournament, in minutes. */
|
|
637
|
+
leadMinutesDefault: 10,
|
|
638
|
+
/** The furthest ahead a tournament may be scheduled, in days. */
|
|
639
|
+
scheduleDaysMax: 30,
|
|
640
|
+
/** Most friends invited in one call. */
|
|
641
|
+
invitesMax: 50
|
|
642
|
+
};
|
|
608
643
|
//#endregion
|
|
609
644
|
//#region src/api/mp/races.ts
|
|
610
645
|
/**
|
|
@@ -926,9 +961,65 @@ const RaceBlock = z.strictObject({
|
|
|
926
961
|
*/
|
|
927
962
|
const GameRace = z.preprocess((v) => v === true ? {} : v === false ? void 0 : v, RaceBlock.optional()).optional();
|
|
928
963
|
//#endregion
|
|
964
|
+
//#region src/api/catalog-activity.ts
|
|
965
|
+
/**
|
|
966
|
+
* Who is in each game right now (aggregate head counts only: never who), and
|
|
967
|
+
* the signed-in player's own activity in one game. The catalog's cards, the
|
|
968
|
+
* menu's game list and the game sheet show them.
|
|
969
|
+
*/
|
|
970
|
+
/** A non-negative head count. */
|
|
971
|
+
const HeadCount$1 = z.number().int().nonnegative();
|
|
972
|
+
/**
|
|
973
|
+
* How many people have a game open right now: claimed accounts and guests.
|
|
974
|
+
* Counts only, so no player is ever identified.
|
|
975
|
+
*/
|
|
976
|
+
const GamePlayerCount = z.object({
|
|
977
|
+
accounts: HeadCount$1,
|
|
978
|
+
guests: HeadCount$1
|
|
979
|
+
});
|
|
980
|
+
/** Head counts per game slug; games nobody has open are absent. */
|
|
981
|
+
const GamePlayerCounts = z.record(Slug, GamePlayerCount);
|
|
982
|
+
/** The caller's activity in one game, within the analytics retention window. */
|
|
983
|
+
const MyGameActivity = z.object({
|
|
984
|
+
/** Times the game was opened. */
|
|
985
|
+
plays: HeadCount$1,
|
|
986
|
+
/** Minutes with the game open and visible (one heartbeat a minute). */
|
|
987
|
+
minutesPlayed: HeadCount$1,
|
|
988
|
+
/** The first and latest event in the window, epoch ms; `null` when there are none. */
|
|
989
|
+
firstPlayedAt: Millis.nullable(),
|
|
990
|
+
lastPlayedAt: Millis.nullable(),
|
|
991
|
+
/** How many days back the counts reach. */
|
|
992
|
+
windowDays: z.number().int().positive()
|
|
993
|
+
});
|
|
994
|
+
/** The activity endpoints (part of the catalog area). */
|
|
995
|
+
const catalogActivityEndpoints = {
|
|
996
|
+
...session.group({ getGamePlayerCounts: {
|
|
997
|
+
method: "GET",
|
|
998
|
+
path: "/catalog/players",
|
|
999
|
+
response: z.object({ games: GamePlayerCounts }),
|
|
1000
|
+
doc: {
|
|
1001
|
+
summary: "How many accounts and guests have each game open right now (counts only)",
|
|
1002
|
+
tags: ["catalog"]
|
|
1003
|
+
}
|
|
1004
|
+
} }),
|
|
1005
|
+
...use(requireUser, game).path("/games/:slug").group({ getMyGameActivity: {
|
|
1006
|
+
method: "GET",
|
|
1007
|
+
path: "/me/activity",
|
|
1008
|
+
response: MyGameActivity,
|
|
1009
|
+
errors: {
|
|
1010
|
+
401: ErrorBody,
|
|
1011
|
+
404: ErrorBody
|
|
1012
|
+
},
|
|
1013
|
+
doc: {
|
|
1014
|
+
summary: "The caller's plays and time played in this game",
|
|
1015
|
+
tags: ["catalog"]
|
|
1016
|
+
}
|
|
1017
|
+
} })
|
|
1018
|
+
};
|
|
1019
|
+
//#endregion
|
|
929
1020
|
//#region src/api/catalog.ts
|
|
930
1021
|
/**
|
|
931
|
-
* The game catalog, favorites
|
|
1022
|
+
* The game catalog, favorites, What's new notes, and who is playing what (counts only).
|
|
932
1023
|
*/
|
|
933
1024
|
/** A `[min, max]` player count, each 1 to 8. */
|
|
934
1025
|
const PlayerRange = z.tuple([z.number().int().min(1).max(8), z.number().int().min(1).max(8)]);
|
|
@@ -987,7 +1078,9 @@ const GameInfo = z.object({
|
|
|
987
1078
|
/** `null` for single-player games. */
|
|
988
1079
|
multiplayer: MultiplayerSummary.nullable(),
|
|
989
1080
|
/** `null` unless the game supports races. */
|
|
990
|
-
race: RaceSummary.nullable()
|
|
1081
|
+
race: RaceSummary.nullable(),
|
|
1082
|
+
/** Present (and `true`) when tournaments can be played in this game: it races, or declares `multiplayer.tournaments`. */
|
|
1083
|
+
tournaments: z.literal(true).optional()
|
|
991
1084
|
});
|
|
992
1085
|
/** `{ games }`: the caller's starred slugs, oldest star first. */
|
|
993
1086
|
const Favorites = z.object({ games: z.array(Slug) });
|
|
@@ -1006,6 +1099,7 @@ const ChangeNote = z.object({
|
|
|
1006
1099
|
});
|
|
1007
1100
|
/** The catalog endpoints. */
|
|
1008
1101
|
const catalogEndpoints = {
|
|
1102
|
+
...catalogActivityEndpoints,
|
|
1009
1103
|
...session.group({
|
|
1010
1104
|
listGames: {
|
|
1011
1105
|
method: "GET",
|
|
@@ -1926,6 +2020,182 @@ upTo: requiredId("upTo must be a message id.") }),
|
|
|
1926
2020
|
}
|
|
1927
2021
|
}) };
|
|
1928
2022
|
//#endregion
|
|
2023
|
+
//#region src/api/tournaments/shapes.ts
|
|
2024
|
+
/**
|
|
2025
|
+
* Tournaments (batch-2026-10 R15): brackets of 1v1 matches in one game or a
|
|
2026
|
+
* set of games. Single elimination, or double/triple elimination with
|
|
2027
|
+
* redemption brackets; seeded by rank in the game or at random; started at a
|
|
2028
|
+
* scheduled time or once enough players sign up. Open tournaments are
|
|
2029
|
+
* listed for anyone; closed ones only for the players invited.
|
|
2030
|
+
*
|
|
2031
|
+
* A match is an ordinary launch into the game (`host` for the better seed,
|
|
2032
|
+
* `guest` for the other, sharing a two-player party), carrying
|
|
2033
|
+
* {@link TournamentLaunch}; the game reports the winner the way it already
|
|
2034
|
+
* ends a match.
|
|
2035
|
+
*/
|
|
2036
|
+
/** A tournament's id. */
|
|
2037
|
+
const TournamentId = z.number().int().positive();
|
|
2038
|
+
/** `open`: listed, anyone can sign up or follow. `closed`: only invited players see it. */
|
|
2039
|
+
const TournamentVisibility = z.enum(["open", "closed"]);
|
|
2040
|
+
/** How the bracket is seeded: by each player's wins in the tournament's games, or at random. */
|
|
2041
|
+
const TournamentSeeding = z.enum(["rank", "random"]);
|
|
2042
|
+
/** Which game each match is: the one game, one random pick from the set for the whole tournament, or a random pick per match. */
|
|
2043
|
+
const TournamentGamePick = z.enum([
|
|
2044
|
+
"fixed",
|
|
2045
|
+
"random-once",
|
|
2046
|
+
"random-each"
|
|
2047
|
+
]);
|
|
2048
|
+
/** When a tournament starts: at a set time (with whoever signed up), or as soon as `minPlayers` sign up. */
|
|
2049
|
+
const TournamentStart = z.discriminatedUnion("kind", [z.object({
|
|
2050
|
+
kind: z.literal("scheduled"),
|
|
2051
|
+
at: Millis
|
|
2052
|
+
}), z.object({ kind: z.literal("filled") })]);
|
|
2053
|
+
/** `signup` → `running` → `done`; or `cancelled` (by its creator, or too few players at the start). */
|
|
2054
|
+
const TournamentPhase = z.enum([
|
|
2055
|
+
"signup",
|
|
2056
|
+
"running",
|
|
2057
|
+
"done",
|
|
2058
|
+
"cancelled"
|
|
2059
|
+
]);
|
|
2060
|
+
/** Where a match sits: the winners bracket, a redemption bracket (players with losses), or the final ladder. */
|
|
2061
|
+
const TournamentStage = z.enum([
|
|
2062
|
+
"winners",
|
|
2063
|
+
"redemption",
|
|
2064
|
+
"final"
|
|
2065
|
+
]);
|
|
2066
|
+
/** How a match was decided. */
|
|
2067
|
+
const TournamentDecision = z.enum([
|
|
2068
|
+
"played",
|
|
2069
|
+
"forfeit",
|
|
2070
|
+
"bye",
|
|
2071
|
+
"timeout",
|
|
2072
|
+
"withdrawn"
|
|
2073
|
+
]);
|
|
2074
|
+
/** A player count within the tournament limits. */
|
|
2075
|
+
const PlayerCount = z.number().int().min(TOURNAMENT_LIMITS.playersMin).max(TOURNAMENT_LIMITS.playersMax);
|
|
2076
|
+
/** `POST /tournaments`. Everything but `games` has a default: the quick "public 1v1, single elimination". */
|
|
2077
|
+
const TournamentCreate = z.object({
|
|
2078
|
+
/** Shown to everyone who can see the tournament; checked by the content filter. Default: made from the game's title. */
|
|
2079
|
+
title: z.string().trim().min(1).max(TOURNAMENT_LIMITS.titleMax).optional(),
|
|
2080
|
+
/** The game, or the set to pick from; each must support tournaments. */
|
|
2081
|
+
games: z.array(Slug).min(1).max(TOURNAMENT_LIMITS.gamesMax),
|
|
2082
|
+
pick: TournamentGamePick.default("fixed"),
|
|
2083
|
+
/** Lives per player: 1 single (default), 2 double, 3 triple elimination. */
|
|
2084
|
+
lives: z.number().int().min(1).max(TOURNAMENT_LIMITS.livesMax).default(1),
|
|
2085
|
+
seeding: TournamentSeeding.default("random"),
|
|
2086
|
+
visibility: TournamentVisibility.default("open"),
|
|
2087
|
+
start: TournamentStart.default({ kind: "filled" }),
|
|
2088
|
+
/** Players needed to start (a `filled` start), or to go ahead at the scheduled time. Default 2. */
|
|
2089
|
+
minPlayers: PlayerCount.default(2),
|
|
2090
|
+
/** Most players who can sign up. Default 2. */
|
|
2091
|
+
maxPlayers: PlayerCount.default(2),
|
|
2092
|
+
/** Minutes a ready match waits for its players before the absent side forfeits. */
|
|
2093
|
+
readyMinutes: z.number().int().min(1).max(TOURNAMENT_LIMITS.readyMinutesMax).default(TOURNAMENT_LIMITS.readyMinutesDefault),
|
|
2094
|
+
/** Friends to invite now (closed tournaments, or a nudge for open ones). */
|
|
2095
|
+
invite: z.array(UserId).max(TOURNAMENT_LIMITS.invitesMax).optional()
|
|
2096
|
+
});
|
|
2097
|
+
/** One entrant, as the bracket shows them. */
|
|
2098
|
+
const TournamentPlayer = z.object({
|
|
2099
|
+
user: PlayerLite,
|
|
2100
|
+
/** 1 is the top seed; `null` until the bracket is drawn. */
|
|
2101
|
+
seed: z.number().int().positive().nullable(),
|
|
2102
|
+
wins: z.number().int().nonnegative(),
|
|
2103
|
+
losses: z.number().int().nonnegative(),
|
|
2104
|
+
/** Knocked out (lost every life, or withdrew). */
|
|
2105
|
+
out: z.boolean()
|
|
2106
|
+
});
|
|
2107
|
+
/** One match in the bracket. Sides are user ids (see the detail's `players`). */
|
|
2108
|
+
const TournamentMatch = z.object({
|
|
2109
|
+
id: z.string(),
|
|
2110
|
+
stage: TournamentStage,
|
|
2111
|
+
/** Redemption bracket number (losses), 0 elsewhere. */
|
|
2112
|
+
level: z.number().int().nonnegative(),
|
|
2113
|
+
round: z.number().int().positive(),
|
|
2114
|
+
slot: z.number().int().nonnegative(),
|
|
2115
|
+
/** `null` while unknown, or the empty side of a bye. */
|
|
2116
|
+
a: UserId.nullable(),
|
|
2117
|
+
b: UserId.nullable(),
|
|
2118
|
+
status: z.enum([
|
|
2119
|
+
"waiting",
|
|
2120
|
+
"ready",
|
|
2121
|
+
"done"
|
|
2122
|
+
]),
|
|
2123
|
+
winnerId: UserId.nullable(),
|
|
2124
|
+
decision: TournamentDecision.nullable(),
|
|
2125
|
+
/** The game this match is played in, once known. */
|
|
2126
|
+
game: Slug.nullable(),
|
|
2127
|
+
/** Ready matches: when the absent side forfeits, epoch ms. */
|
|
2128
|
+
deadlineAt: Millis.nullable(),
|
|
2129
|
+
/** Whether the match has started in the game. */
|
|
2130
|
+
started: z.boolean()
|
|
2131
|
+
});
|
|
2132
|
+
/** A ready match of mine: who against, in which game, and until when. */
|
|
2133
|
+
const MyTournamentMatch = z.object({
|
|
2134
|
+
id: z.string(),
|
|
2135
|
+
opponent: PlayerLite.nullable(),
|
|
2136
|
+
game: Slug,
|
|
2137
|
+
gameTitle: z.string(),
|
|
2138
|
+
deadlineAt: Millis,
|
|
2139
|
+
started: z.boolean()
|
|
2140
|
+
});
|
|
2141
|
+
/** The viewer's own relation to a tournament. */
|
|
2142
|
+
const TournamentMe = z.object({
|
|
2143
|
+
joined: z.boolean(),
|
|
2144
|
+
/** Invited and not yet signed up. */
|
|
2145
|
+
invited: z.boolean(),
|
|
2146
|
+
following: z.boolean(),
|
|
2147
|
+
/** Minutes before a scheduled start to remind me; `null` for no reminder. */
|
|
2148
|
+
leadMinutes: z.number().int().nonnegative().nullable(),
|
|
2149
|
+
isCreator: z.boolean(),
|
|
2150
|
+
/** Still in (signed up and not knocked out). */
|
|
2151
|
+
stillIn: z.boolean(),
|
|
2152
|
+
nextMatch: MyTournamentMatch.nullable()
|
|
2153
|
+
});
|
|
2154
|
+
/** A tournament in lists. */
|
|
2155
|
+
const TournamentSummary = z.object({
|
|
2156
|
+
id: TournamentId,
|
|
2157
|
+
title: z.string(),
|
|
2158
|
+
creator: PlayerLite.nullable(),
|
|
2159
|
+
visibility: TournamentVisibility,
|
|
2160
|
+
phase: TournamentPhase,
|
|
2161
|
+
/** The game set (one game for `fixed`). */
|
|
2162
|
+
games: z.array(Slug),
|
|
2163
|
+
pick: TournamentGamePick,
|
|
2164
|
+
/** The game every match is in, once known (`fixed`, or `random-once` after the start); `null` for `random-each`. */
|
|
2165
|
+
game: Slug.nullable(),
|
|
2166
|
+
/** A title per game slug. */
|
|
2167
|
+
gameTitles: z.record(Slug, z.string()),
|
|
2168
|
+
lives: z.number().int().min(1),
|
|
2169
|
+
seeding: TournamentSeeding,
|
|
2170
|
+
start: TournamentStart,
|
|
2171
|
+
minPlayers: z.number().int(),
|
|
2172
|
+
maxPlayers: z.number().int(),
|
|
2173
|
+
readyMinutes: z.number().int(),
|
|
2174
|
+
/** How many have signed up. */
|
|
2175
|
+
entrants: z.number().int().nonnegative(),
|
|
2176
|
+
createdAt: Millis,
|
|
2177
|
+
startedAt: Millis.nullable(),
|
|
2178
|
+
endedAt: Millis.nullable(),
|
|
2179
|
+
champion: PlayerLite.nullable(),
|
|
2180
|
+
me: TournamentMe
|
|
2181
|
+
});
|
|
2182
|
+
/** A tournament with its players and bracket. */
|
|
2183
|
+
const TournamentDetail = TournamentSummary.extend({
|
|
2184
|
+
players: z.array(TournamentPlayer),
|
|
2185
|
+
matches: z.array(TournamentMatch),
|
|
2186
|
+
/** The creator's link that lets friends of friends in (closed tournaments, creator only). */
|
|
2187
|
+
inviteUrl: z.string().nullable()
|
|
2188
|
+
});
|
|
2189
|
+
/** What a launch into a tournament match carries (`launch.tournament`). */
|
|
2190
|
+
const TournamentLaunch = z.object({
|
|
2191
|
+
id: TournamentId,
|
|
2192
|
+
title: z.string(),
|
|
2193
|
+
/** The match id within the tournament; own-server games send it back with the result. */
|
|
2194
|
+
match: z.string(),
|
|
2195
|
+
/** Who to play. */
|
|
2196
|
+
opponent: PlayerLite.nullable()
|
|
2197
|
+
});
|
|
2198
|
+
//#endregion
|
|
1929
2199
|
//#region src/api/mp/parties.ts
|
|
1930
2200
|
/**
|
|
1931
2201
|
* Invites, parties and launches (multiplayer-impl.md §5.6): the shapes the
|
|
@@ -2010,9 +2280,44 @@ const Launch = z.object({
|
|
|
2010
2280
|
/** Guest launches: the party leader (`null` if the account is gone). */
|
|
2011
2281
|
host: PlayerLite.nullable().optional(),
|
|
2012
2282
|
/** Join and watch launches: the friend being joined (`null` if the account is gone). */
|
|
2013
|
-
target: PlayerLite.nullable().optional()
|
|
2283
|
+
target: PlayerLite.nullable().optional(),
|
|
2284
|
+
/** `link`: opened from an invite link; `rejoin`: taking back a seat I still hold (both are join or watch launches). */
|
|
2285
|
+
via: z.enum(["link", "rejoin"]).optional(),
|
|
2286
|
+
/** Host and guest launches into a tournament match: which tournament, which match, and who to play. */
|
|
2287
|
+
tournament: TournamentLaunch.optional()
|
|
2014
2288
|
});
|
|
2015
2289
|
//#endregion
|
|
2290
|
+
//#region src/api/mp/invite-links.ts
|
|
2291
|
+
/**
|
|
2292
|
+
* Invite links (batch-2026-10 R13): a link any room member can share that
|
|
2293
|
+
* drops whoever opens it into that room, as a player when a seat is free,
|
|
2294
|
+
* otherwise as a spectator. A link is an unguessable token kept by the hub;
|
|
2295
|
+
* it expires (24 h) and its creator or the room's host can revoke it. Link
|
|
2296
|
+
* joiners get the room's usual chat rules and are not friended.
|
|
2297
|
+
*/
|
|
2298
|
+
/** An invite link's token (base64url). */
|
|
2299
|
+
const InviteLinkToken = z.string().regex(/^[A-Za-z0-9_-]{16,64}$/);
|
|
2300
|
+
/** A live invite link. */
|
|
2301
|
+
const InviteLink = z.object({
|
|
2302
|
+
token: InviteLinkToken,
|
|
2303
|
+
/** `/<slug>/<lobby>?hub_link=<token>`, relative to the site origin. */
|
|
2304
|
+
url: z.string(),
|
|
2305
|
+
game: Slug,
|
|
2306
|
+
mode: GameMode.optional(),
|
|
2307
|
+
/** The room it opens: a hub room code, or an own-server game's room id from presence. */
|
|
2308
|
+
room: z.string(),
|
|
2309
|
+
/** Who made it. */
|
|
2310
|
+
createdBy: UserId,
|
|
2311
|
+
/** Epoch ms. */
|
|
2312
|
+
expiresAt: Millis
|
|
2313
|
+
});
|
|
2314
|
+
/** `:token` of an invite link. */
|
|
2315
|
+
const InviteLinkParams = z.object({ token: InviteLinkToken });
|
|
2316
|
+
/** `POST /mp/links` body: a link to the room I'm in, in this game. */
|
|
2317
|
+
const InviteLinkCreate = z.object({ game: Slug });
|
|
2318
|
+
/** `GET /mp/links` query: my links in a game (and the links to rooms I host). */
|
|
2319
|
+
const InviteLinkQuery = z.object({ game: Slug });
|
|
2320
|
+
//#endregion
|
|
2016
2321
|
//#region src/api/mp/chat.ts
|
|
2017
2322
|
/**
|
|
2018
2323
|
* Group chat in hub rooms and parties (multiplayer.md §4.3, §4.8): what a
|
|
@@ -2066,6 +2371,14 @@ const RoomPhase = z.enum([
|
|
|
2066
2371
|
* included.
|
|
2067
2372
|
*/
|
|
2068
2373
|
const RoomModel = z.enum(["host", "relay"]);
|
|
2374
|
+
/**
|
|
2375
|
+
* When the host role moves on mid-game. `grace` (default): only once the
|
|
2376
|
+
* host's seat is freed (they left, or their reconnect grace ran out), so a
|
|
2377
|
+
* host whose browser holds hidden state can come back and carry on. `drop`:
|
|
2378
|
+
* as soon as the host drops, for games whose whole truth is in the shared
|
|
2379
|
+
* state, so the match never waits.
|
|
2380
|
+
*/
|
|
2381
|
+
const HostMigration = z.enum(["grace", "drop"]);
|
|
2069
2382
|
/** A seat in a room, keyed by user (any tab of that user reclaims it). */
|
|
2070
2383
|
const RoomSeat = z.object({
|
|
2071
2384
|
userId: UserId,
|
|
@@ -2076,6 +2389,8 @@ const RoomSeat = z.object({
|
|
|
2076
2389
|
ready: z.boolean(),
|
|
2077
2390
|
/** `false` while the seat is held for a dropped player's reconnect grace. */
|
|
2078
2391
|
connected: z.boolean(),
|
|
2392
|
+
/** While the seat is held (`connected: false`): when the grace runs out and the seat is freed (epoch ms). */
|
|
2393
|
+
graceEndsAt: Millis.optional(),
|
|
2079
2394
|
/** Small per-seat game data (team, colour…), at most 2 KB of JSON. */
|
|
2080
2395
|
data: Json.optional()
|
|
2081
2396
|
});
|
|
@@ -2127,12 +2442,32 @@ const RoomClosedReason = z.enum([
|
|
|
2127
2442
|
"other_tab",
|
|
2128
2443
|
"removed"
|
|
2129
2444
|
]);
|
|
2130
|
-
/** Why a seat was freed (`room.abandoned`). */
|
|
2445
|
+
/** Why a seat was freed (`room.abandoned`, `room.seat` `gone`). */
|
|
2131
2446
|
const SeatGoneReason = z.enum([
|
|
2132
2447
|
"left",
|
|
2133
2448
|
"abandoned",
|
|
2134
2449
|
"kicked"
|
|
2135
2450
|
]);
|
|
2451
|
+
/**
|
|
2452
|
+
* What happened to a seat (`room.seat`): `away` (the player dropped; the seat
|
|
2453
|
+
* is held until `graceEndsAt`, so the game can hand it to its computer
|
|
2454
|
+
* player), `back` (they reclaimed it: give control back), `gone` (freed for
|
|
2455
|
+
* good: left, kicked, or the grace ran out).
|
|
2456
|
+
*/
|
|
2457
|
+
const SeatStatus = z.enum([
|
|
2458
|
+
"away",
|
|
2459
|
+
"back",
|
|
2460
|
+
"gone"
|
|
2461
|
+
]);
|
|
2462
|
+
/** A seat change (`room.seat`). */
|
|
2463
|
+
const SeatChange = z.object({
|
|
2464
|
+
userId: UserId,
|
|
2465
|
+
status: SeatStatus,
|
|
2466
|
+
/** `away`: when the held seat is freed (epoch ms). */
|
|
2467
|
+
graceEndsAt: Millis.optional(),
|
|
2468
|
+
/** `gone`: why. */
|
|
2469
|
+
reason: SeatGoneReason.optional()
|
|
2470
|
+
});
|
|
2136
2471
|
//#endregion
|
|
2137
2472
|
//#region src/api/mp/snapshot.ts
|
|
2138
2473
|
/** The multiplayer part of the socket `hello` snapshot (multiplayer-impl.md §4.5). */
|
|
@@ -2226,6 +2561,53 @@ const mpEndpoints = {
|
|
|
2226
2561
|
tags: ["mp"]
|
|
2227
2562
|
}
|
|
2228
2563
|
},
|
|
2564
|
+
createInviteLink: {
|
|
2565
|
+
method: "POST",
|
|
2566
|
+
path: "/mp/links",
|
|
2567
|
+
body: InviteLinkCreate,
|
|
2568
|
+
response: InviteLink,
|
|
2569
|
+
errors: {
|
|
2570
|
+
400: ErrorBody,
|
|
2571
|
+
401: ErrorBody,
|
|
2572
|
+
403: ErrorBody,
|
|
2573
|
+
404: ErrorBody,
|
|
2574
|
+
409: ErrorBody,
|
|
2575
|
+
429: ErrorBody
|
|
2576
|
+
},
|
|
2577
|
+
doc: {
|
|
2578
|
+
summary: "An invite link to the room I'm in (reused while it lives): anyone who opens it joins, or watches when it's full",
|
|
2579
|
+
tags: ["mp"]
|
|
2580
|
+
}
|
|
2581
|
+
},
|
|
2582
|
+
listInviteLinks: {
|
|
2583
|
+
method: "GET",
|
|
2584
|
+
path: "/mp/links",
|
|
2585
|
+
query: InviteLinkQuery,
|
|
2586
|
+
response: z.object({ links: z.array(InviteLink) }),
|
|
2587
|
+
errors: {
|
|
2588
|
+
401: ErrorBody,
|
|
2589
|
+
403: ErrorBody
|
|
2590
|
+
},
|
|
2591
|
+
doc: {
|
|
2592
|
+
summary: "My live invite links in a game, plus the links to rooms I host",
|
|
2593
|
+
tags: ["mp"]
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
revokeInviteLink: {
|
|
2597
|
+
method: "DELETE",
|
|
2598
|
+
path: "/mp/links/:token",
|
|
2599
|
+
params: InviteLinkParams,
|
|
2600
|
+
response: Ok,
|
|
2601
|
+
errors: {
|
|
2602
|
+
401: ErrorBody,
|
|
2603
|
+
403: ErrorBody,
|
|
2604
|
+
404: ErrorBody
|
|
2605
|
+
},
|
|
2606
|
+
doc: {
|
|
2607
|
+
summary: "Revoke an invite link (its creator, or the host of the room it opens)",
|
|
2608
|
+
tags: ["mp"]
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2229
2611
|
cancelGameInvite: {
|
|
2230
2612
|
method: "DELETE",
|
|
2231
2613
|
path: "/mp/invites/:id",
|
|
@@ -2258,6 +2640,23 @@ const mpEndpoints = {
|
|
|
2258
2640
|
tags: ["mp"]
|
|
2259
2641
|
}
|
|
2260
2642
|
},
|
|
2643
|
+
openInviteLink: {
|
|
2644
|
+
method: "POST",
|
|
2645
|
+
path: "/mp/links/:token/open",
|
|
2646
|
+
params: InviteLinkParams,
|
|
2647
|
+
response: LaunchTicket,
|
|
2648
|
+
errors: {
|
|
2649
|
+
401: ErrorBody,
|
|
2650
|
+
403: ErrorBody,
|
|
2651
|
+
404: ErrorBody,
|
|
2652
|
+
409: ErrorBody,
|
|
2653
|
+
429: ErrorBody
|
|
2654
|
+
},
|
|
2655
|
+
doc: {
|
|
2656
|
+
summary: "Open an invite link: a join launch when a seat is free, else a watch launch",
|
|
2657
|
+
tags: ["mp"]
|
|
2658
|
+
}
|
|
2659
|
+
},
|
|
2261
2660
|
setPartyJoinInfo: {
|
|
2262
2661
|
method: "POST",
|
|
2263
2662
|
path: "/mp/parties/:id/join-info",
|
|
@@ -2351,7 +2750,10 @@ const NotifyPrefs = z.object({
|
|
|
2351
2750
|
friendOnlineAll: z.boolean(),
|
|
2352
2751
|
/** Friends whose bell is on, ascending. */
|
|
2353
2752
|
friendOnline: z.array(UserId),
|
|
2354
|
-
/**
|
|
2753
|
+
/**
|
|
2754
|
+
* Games followed for "someone wants to play" (the game sheet's notify toggle):
|
|
2755
|
+
* an open public lobby or race, or a tournament opening, sorted.
|
|
2756
|
+
*/
|
|
2355
2757
|
lobbyOpen: z.array(Slug)
|
|
2356
2758
|
});
|
|
2357
2759
|
/** The preference kinds `PUT /social/notify` can flip. */
|
|
@@ -3507,6 +3909,263 @@ players: z.array(RecentPlayer) }),
|
|
|
3507
3909
|
})
|
|
3508
3910
|
};
|
|
3509
3911
|
//#endregion
|
|
3912
|
+
//#region src/api/tournaments/endpoints.ts
|
|
3913
|
+
/**
|
|
3914
|
+
* The tournament endpoints (batch-2026-10 R15). Anyone may look at open
|
|
3915
|
+
* tournaments; a closed one answers 404 to everyone it wasn't shared with.
|
|
3916
|
+
* Creating, inviting and following need a claimed account; guests may sign
|
|
3917
|
+
* up for open tournaments (and for closed ones they were invited to) and
|
|
3918
|
+
* play their matches.
|
|
3919
|
+
*/
|
|
3920
|
+
/** `:id` of a tournament. */
|
|
3921
|
+
const TournamentParams = z.object({ id: IdParam });
|
|
3922
|
+
/** `:id` and `:match` of a tournament match. */
|
|
3923
|
+
const MatchParams = z.object({
|
|
3924
|
+
id: IdParam,
|
|
3925
|
+
match: z.string().regex(/^m\d{1,4}$/)
|
|
3926
|
+
});
|
|
3927
|
+
/** The invite key from a shared link (`?hub_key=`), letting its opener into a closed tournament. */
|
|
3928
|
+
const InviteKey = z.string().regex(/^[A-Za-z0-9_-]{8,64}$/);
|
|
3929
|
+
/** `GET /tournaments` filters. */
|
|
3930
|
+
const TournamentListQuery = z.object({
|
|
3931
|
+
/** Only tournaments in this game. */
|
|
3932
|
+
game: z.union([z.literal(""), Slug]).optional() });
|
|
3933
|
+
/** The tournament endpoints. */
|
|
3934
|
+
const tournamentEndpoints = {
|
|
3935
|
+
...session.group({
|
|
3936
|
+
listTournaments: {
|
|
3937
|
+
method: "GET",
|
|
3938
|
+
path: "/tournaments",
|
|
3939
|
+
query: TournamentListQuery,
|
|
3940
|
+
response: z.object({ tournaments: z.array(TournamentSummary) }),
|
|
3941
|
+
doc: {
|
|
3942
|
+
summary: "Open tournaments plus the ones I am in, invited to or follow, soonest first",
|
|
3943
|
+
tags: ["tournaments"]
|
|
3944
|
+
}
|
|
3945
|
+
},
|
|
3946
|
+
getTournament: {
|
|
3947
|
+
method: "GET",
|
|
3948
|
+
path: "/tournaments/:id",
|
|
3949
|
+
params: TournamentParams,
|
|
3950
|
+
query: z.object({ key: InviteKey.optional() }),
|
|
3951
|
+
response: TournamentDetail,
|
|
3952
|
+
errors: { 404: ErrorBody },
|
|
3953
|
+
doc: {
|
|
3954
|
+
summary: "A tournament with its players and bracket (404 for a closed one not shared with me)",
|
|
3955
|
+
tags: ["tournaments"]
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
}),
|
|
3959
|
+
...requireClaimed.group({
|
|
3960
|
+
createTournament: {
|
|
3961
|
+
method: "POST",
|
|
3962
|
+
path: "/tournaments",
|
|
3963
|
+
body: TournamentCreate,
|
|
3964
|
+
response: TournamentDetail,
|
|
3965
|
+
errors: {
|
|
3966
|
+
400: ErrorBody,
|
|
3967
|
+
401: ErrorBody,
|
|
3968
|
+
403: ErrorBody,
|
|
3969
|
+
404: ErrorBody,
|
|
3970
|
+
422: ErrorBody,
|
|
3971
|
+
429: ErrorBody
|
|
3972
|
+
},
|
|
3973
|
+
doc: {
|
|
3974
|
+
summary: "Create a tournament (I am signed up to it); the title goes through the content filter",
|
|
3975
|
+
tags: ["tournaments"]
|
|
3976
|
+
}
|
|
3977
|
+
},
|
|
3978
|
+
inviteToTournament: {
|
|
3979
|
+
method: "POST",
|
|
3980
|
+
path: "/tournaments/:id/invites",
|
|
3981
|
+
params: TournamentParams,
|
|
3982
|
+
body: z.object({ userIds: z.array(UserId).min(1).max(TOURNAMENT_LIMITS.invitesMax) }),
|
|
3983
|
+
response: z.object({ invited: z.number().int().nonnegative() }),
|
|
3984
|
+
errors: {
|
|
3985
|
+
401: ErrorBody,
|
|
3986
|
+
403: ErrorBody,
|
|
3987
|
+
404: ErrorBody,
|
|
3988
|
+
409: ErrorBody,
|
|
3989
|
+
429: ErrorBody
|
|
3990
|
+
},
|
|
3991
|
+
doc: {
|
|
3992
|
+
summary: "Creator: invite friends (others are skipped without saying why)",
|
|
3993
|
+
tags: ["tournaments"]
|
|
3994
|
+
}
|
|
3995
|
+
},
|
|
3996
|
+
followTournament: {
|
|
3997
|
+
method: "PUT",
|
|
3998
|
+
path: "/tournaments/:id/follow",
|
|
3999
|
+
params: TournamentParams,
|
|
4000
|
+
body: z.object({ leadMinutes: z.number().int().min(0).max(TOURNAMENT_LIMITS.leadMinutesMax).nullish() }),
|
|
4001
|
+
response: TournamentSummary,
|
|
4002
|
+
errors: {
|
|
4003
|
+
401: ErrorBody,
|
|
4004
|
+
403: ErrorBody,
|
|
4005
|
+
404: ErrorBody
|
|
4006
|
+
},
|
|
4007
|
+
doc: {
|
|
4008
|
+
summary: "Follow a tournament (hear when it opens, starts and ends); leadMinutes: remind me that long before a scheduled start (absent: no reminder)",
|
|
4009
|
+
tags: ["tournaments"]
|
|
4010
|
+
}
|
|
4011
|
+
},
|
|
4012
|
+
unfollowTournament: {
|
|
4013
|
+
method: "DELETE",
|
|
4014
|
+
path: "/tournaments/:id/follow",
|
|
4015
|
+
params: TournamentParams,
|
|
4016
|
+
response: Ok,
|
|
4017
|
+
errors: {
|
|
4018
|
+
401: ErrorBody,
|
|
4019
|
+
403: ErrorBody,
|
|
4020
|
+
404: ErrorBody
|
|
4021
|
+
},
|
|
4022
|
+
doc: {
|
|
4023
|
+
summary: "Stop following a tournament",
|
|
4024
|
+
tags: ["tournaments"]
|
|
4025
|
+
}
|
|
4026
|
+
},
|
|
4027
|
+
startTournament: {
|
|
4028
|
+
method: "POST",
|
|
4029
|
+
path: "/tournaments/:id/start",
|
|
4030
|
+
params: TournamentParams,
|
|
4031
|
+
response: TournamentDetail,
|
|
4032
|
+
errors: {
|
|
4033
|
+
401: ErrorBody,
|
|
4034
|
+
403: ErrorBody,
|
|
4035
|
+
404: ErrorBody,
|
|
4036
|
+
409: ErrorBody
|
|
4037
|
+
},
|
|
4038
|
+
doc: {
|
|
4039
|
+
summary: "Creator: start now with whoever signed up (at least two)",
|
|
4040
|
+
tags: ["tournaments"]
|
|
4041
|
+
}
|
|
4042
|
+
},
|
|
4043
|
+
cancelTournament: {
|
|
4044
|
+
method: "DELETE",
|
|
4045
|
+
path: "/tournaments/:id",
|
|
4046
|
+
params: TournamentParams,
|
|
4047
|
+
response: Ok,
|
|
4048
|
+
errors: {
|
|
4049
|
+
401: ErrorBody,
|
|
4050
|
+
403: ErrorBody,
|
|
4051
|
+
404: ErrorBody,
|
|
4052
|
+
409: ErrorBody
|
|
4053
|
+
},
|
|
4054
|
+
doc: {
|
|
4055
|
+
summary: "Creator (or an admin): cancel a tournament that has not finished",
|
|
4056
|
+
tags: ["tournaments"]
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}),
|
|
4060
|
+
...requireActive.group({
|
|
4061
|
+
joinTournament: {
|
|
4062
|
+
method: "POST",
|
|
4063
|
+
path: "/tournaments/:id/join",
|
|
4064
|
+
params: TournamentParams,
|
|
4065
|
+
body: z.object({ key: InviteKey.optional() }),
|
|
4066
|
+
response: TournamentDetail,
|
|
4067
|
+
errors: {
|
|
4068
|
+
401: ErrorBody,
|
|
4069
|
+
403: ErrorBody,
|
|
4070
|
+
404: ErrorBody,
|
|
4071
|
+
409: ErrorBody,
|
|
4072
|
+
429: ErrorBody
|
|
4073
|
+
},
|
|
4074
|
+
doc: {
|
|
4075
|
+
summary: "Sign up (open tournaments, or closed ones I was invited to or have the link for)",
|
|
4076
|
+
tags: ["tournaments"]
|
|
4077
|
+
}
|
|
4078
|
+
},
|
|
4079
|
+
leaveTournament: {
|
|
4080
|
+
method: "POST",
|
|
4081
|
+
path: "/tournaments/:id/leave",
|
|
4082
|
+
params: TournamentParams,
|
|
4083
|
+
response: TournamentDetail,
|
|
4084
|
+
errors: {
|
|
4085
|
+
401: ErrorBody,
|
|
4086
|
+
403: ErrorBody,
|
|
4087
|
+
404: ErrorBody,
|
|
4088
|
+
409: ErrorBody
|
|
4089
|
+
},
|
|
4090
|
+
doc: {
|
|
4091
|
+
summary: "Drop out: before the start my place is freed; after it I forfeit what is left",
|
|
4092
|
+
tags: ["tournaments"]
|
|
4093
|
+
}
|
|
4094
|
+
},
|
|
4095
|
+
launchTournamentMatch: {
|
|
4096
|
+
method: "POST",
|
|
4097
|
+
path: "/tournaments/:id/matches/:match/launch",
|
|
4098
|
+
params: MatchParams,
|
|
4099
|
+
response: LaunchTicket,
|
|
4100
|
+
errors: {
|
|
4101
|
+
401: ErrorBody,
|
|
4102
|
+
403: ErrorBody,
|
|
4103
|
+
404: ErrorBody,
|
|
4104
|
+
409: ErrorBody
|
|
4105
|
+
},
|
|
4106
|
+
doc: {
|
|
4107
|
+
summary: "A launch into my ready match: host for the better seed, guest for the other",
|
|
4108
|
+
tags: ["tournaments"]
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
})
|
|
4112
|
+
};
|
|
4113
|
+
//#endregion
|
|
4114
|
+
//#region src/api/tournaments/play-online.ts
|
|
4115
|
+
/**
|
|
4116
|
+
* "Play online" (batch-2026-10 R15): everything the viewer could play with
|
|
4117
|
+
* someone, now and later, for the home page card and its modal. Public
|
|
4118
|
+
* lobbies and races waiting for a player (joined through
|
|
4119
|
+
* `POST /mp/join { userId, fromLobbyOpen: true }`), invites to the viewer,
|
|
4120
|
+
* the tournaments they are in or follow (with a ready match first), and open
|
|
4121
|
+
* tournaments still taking sign-ups.
|
|
4122
|
+
*/
|
|
4123
|
+
/** A public lobby or race waiting for players right now. */
|
|
4124
|
+
const OpenLobbyEntry = z.object({
|
|
4125
|
+
/** Who is waiting (join them with `POST /mp/join`). */
|
|
4126
|
+
host: PlayerLite,
|
|
4127
|
+
game: Slug,
|
|
4128
|
+
gameTitle: z.string(),
|
|
4129
|
+
/** `race` for a race waiting for racers, otherwise the lobby's mode when it has one. */
|
|
4130
|
+
mode: GameMode.optional(),
|
|
4131
|
+
openSeats: z.number().int().positive(),
|
|
4132
|
+
/** When they started waiting, epoch ms. */
|
|
4133
|
+
since: Millis
|
|
4134
|
+
});
|
|
4135
|
+
/** The counts on the home card. */
|
|
4136
|
+
const PlayOnlineCounts = z.object({
|
|
4137
|
+
/** Lobbies to join now, plus my matches that are ready. */
|
|
4138
|
+
now: z.number().int().nonnegative(),
|
|
4139
|
+
/** Game invites and tournament invites waiting for me. */
|
|
4140
|
+
invites: z.number().int().nonnegative(),
|
|
4141
|
+
/** Tournaments I signed up for or follow, not over. */
|
|
4142
|
+
mine: z.number().int().nonnegative(),
|
|
4143
|
+
/** Open tournaments still taking sign-ups. */
|
|
4144
|
+
upcoming: z.number().int().nonnegative()
|
|
4145
|
+
});
|
|
4146
|
+
/** Everything playable online for the viewer. */
|
|
4147
|
+
const PlayOnline = z.object({
|
|
4148
|
+
counts: PlayOnlineCounts,
|
|
4149
|
+
lobbies: z.array(OpenLobbyEntry),
|
|
4150
|
+
gameInvites: z.array(Invite),
|
|
4151
|
+
/** Closed tournaments I'm invited to and haven't joined. */
|
|
4152
|
+
tournamentInvites: z.array(TournamentSummary),
|
|
4153
|
+
/** Mine: a ready match first, then the soonest. */
|
|
4154
|
+
mine: z.array(TournamentSummary),
|
|
4155
|
+
/** Open, taking sign-ups, not mine. */
|
|
4156
|
+
upcoming: z.array(TournamentSummary)
|
|
4157
|
+
});
|
|
4158
|
+
/** The play-online endpoint. Signed-out visitors get the open tournaments only. */
|
|
4159
|
+
const playOnlineEndpoints = { ...session.group({ getPlayOnline: {
|
|
4160
|
+
method: "GET",
|
|
4161
|
+
path: "/play-online",
|
|
4162
|
+
response: PlayOnline,
|
|
4163
|
+
doc: {
|
|
4164
|
+
summary: "Lobbies to join now, invites, my tournaments and upcoming open ones (the home page card and modal)",
|
|
4165
|
+
tags: ["tournaments"]
|
|
4166
|
+
}
|
|
4167
|
+
} }) };
|
|
4168
|
+
//#endregion
|
|
3510
4169
|
//#region src/api/version.ts
|
|
3511
4170
|
/**
|
|
3512
4171
|
* `GET /version`: which client versions this hub serves (plan §4.3.3). Games'
|
|
@@ -3541,6 +4200,41 @@ const versionEndpoints = { ...session.group({ getHubVersion: {
|
|
|
3541
4200
|
}
|
|
3542
4201
|
} }) };
|
|
3543
4202
|
//#endregion
|
|
4203
|
+
//#region src/api/offline.ts
|
|
4204
|
+
/**
|
|
4205
|
+
* Offline replay metadata: which public endpoints a client may queue while
|
|
4206
|
+
* offline and send later, and how the hub makes a replay land exactly once.
|
|
4207
|
+
*
|
|
4208
|
+
* A replayable call carries a client-made operation key in the
|
|
4209
|
+
* {@link OFFLINE_OP_HEADER} header. The hub remembers the reply to each key
|
|
4210
|
+
* for {@link OFFLINE_DEDUPE_TTL_MS}; a second request with the same key gets
|
|
4211
|
+
* that reply again without running the endpoint twice. Endpoints left out
|
|
4212
|
+
* are online-only (social, multiplayer, trades) or replay on their own terms
|
|
4213
|
+
* (analytics events are buffered and sent at least once, without a key).
|
|
4214
|
+
*/
|
|
4215
|
+
/** The request header that carries a replayable call's operation key. */
|
|
4216
|
+
const OFFLINE_OP_HEADER = "x-hub-op";
|
|
4217
|
+
/** An operation key: letters, digits and `_ . : -`, 1 to 100 characters. */
|
|
4218
|
+
const OfflineOpKey = z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
|
|
4219
|
+
/** How long the hub remembers an operation key's reply: 7 days. */
|
|
4220
|
+
const OFFLINE_DEDUPE_TTL_MS = 6048e5;
|
|
4221
|
+
/** The public endpoints that support offline replay, by name. */
|
|
4222
|
+
const OFFLINE_REPLAY_ENDPOINTS = {
|
|
4223
|
+
writeSave: "replay",
|
|
4224
|
+
deleteSave: "replay",
|
|
4225
|
+
updateGameStat: "replay",
|
|
4226
|
+
setGameProfile: "replay",
|
|
4227
|
+
submitScore: "replay",
|
|
4228
|
+
completeDailyChallenge: "replay",
|
|
4229
|
+
grantInventoryItems: "replay",
|
|
4230
|
+
exchangeInventoryItems: "replay",
|
|
4231
|
+
setGameFavorite: "replay"
|
|
4232
|
+
};
|
|
4233
|
+
/** Whether endpoint `name` supports offline replay. */
|
|
4234
|
+
function isOfflineReplayEndpoint(name) {
|
|
4235
|
+
return Object.hasOwn(OFFLINE_REPLAY_ENDPOINTS, name);
|
|
4236
|
+
}
|
|
4237
|
+
//#endregion
|
|
3544
4238
|
//#region src/api/index.ts
|
|
3545
4239
|
/**
|
|
3546
4240
|
* The public REST API: every endpoint a game, the menu or the social runtime
|
|
@@ -3561,6 +4255,8 @@ const publicEndpoints = {
|
|
|
3561
4255
|
...reportsEndpoints,
|
|
3562
4256
|
...mpEndpoints,
|
|
3563
4257
|
...raceEndpoints,
|
|
4258
|
+
...tournamentEndpoints,
|
|
4259
|
+
...playOnlineEndpoints,
|
|
3564
4260
|
...versionEndpoints
|
|
3565
4261
|
};
|
|
3566
4262
|
/** The public API spec (paths relative to `/_api`). */
|
|
@@ -3662,7 +4358,7 @@ const roomEvents = {
|
|
|
3662
4358
|
}),
|
|
3663
4359
|
"room.closed": event({
|
|
3664
4360
|
d: z.object({ reason: RoomClosedReason }),
|
|
3665
|
-
doc: "The room is gone for this tab (emptied, ended,
|
|
4361
|
+
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."
|
|
3666
4362
|
}),
|
|
3667
4363
|
"room.abandoned": event({
|
|
3668
4364
|
d: z.object({
|
|
@@ -3670,15 +4366,28 @@ const roomEvents = {
|
|
|
3670
4366
|
reason: SeatGoneReason
|
|
3671
4367
|
}),
|
|
3672
4368
|
doc: "A player left mid-game (or their reconnect grace ran out); the game decides what that means."
|
|
4369
|
+
}),
|
|
4370
|
+
"room.seat": event({
|
|
4371
|
+
d: SeatChange,
|
|
4372
|
+
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`."
|
|
3673
4373
|
})
|
|
3674
4374
|
};
|
|
3675
4375
|
//#endregion
|
|
4376
|
+
//#region src/realtime/events/tournaments.ts
|
|
4377
|
+
/** Tournament events (batch-2026-10 R15). */
|
|
4378
|
+
/** The tournament events, by event name. */
|
|
4379
|
+
const tournamentEvents = { tournament: event({
|
|
4380
|
+
d: z.object({ tournament: TournamentSummary }),
|
|
4381
|
+
doc: "A tournament you are in, follow, created or were invited to changed (sign-ups, the start, a match ready or decided, the end)."
|
|
4382
|
+
}) };
|
|
4383
|
+
//#endregion
|
|
3676
4384
|
//#region src/realtime/events/multiplayer.ts
|
|
3677
4385
|
/** The multiplayer events, by event name. */
|
|
3678
4386
|
const multiplayerEvents = {
|
|
3679
4387
|
...partyEvents,
|
|
3680
4388
|
...roomEvents,
|
|
3681
|
-
...raceEvents
|
|
4389
|
+
...raceEvents,
|
|
4390
|
+
...tournamentEvents
|
|
3682
4391
|
};
|
|
3683
4392
|
//#endregion
|
|
3684
4393
|
//#region src/realtime/events/social.ts
|
|
@@ -3687,7 +4396,7 @@ const multiplayerEvents = {
|
|
|
3687
4396
|
* and suspension (multiplayer-impl.md §4.6). Every event goes to all of the
|
|
3688
4397
|
* recipient's tabs.
|
|
3689
4398
|
*/
|
|
3690
|
-
/** What kind of thing a notice is about. `renamed`: an admin changed your username. */
|
|
4399
|
+
/** 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. */
|
|
3691
4400
|
const NoticeKind = z.enum([
|
|
3692
4401
|
"dm",
|
|
3693
4402
|
"friend_request",
|
|
@@ -3695,7 +4404,8 @@ const NoticeKind = z.enum([
|
|
|
3695
4404
|
"lobby_open",
|
|
3696
4405
|
"invite",
|
|
3697
4406
|
"suggestion",
|
|
3698
|
-
"renamed"
|
|
4407
|
+
"renamed",
|
|
4408
|
+
"tournament"
|
|
3699
4409
|
]);
|
|
3700
4410
|
/** A toast (`ev:notice`), sent instead of a push when the player has a visible tab. */
|
|
3701
4411
|
const Notice = z.object({
|
|
@@ -3944,6 +4654,13 @@ params: RaceParams.optional() }),
|
|
|
3944
4654
|
*/
|
|
3945
4655
|
/** A per-room sequence number ack (`room.send`, `room.state`). */
|
|
3946
4656
|
const SeqAck = z.object({ seq: z.number().int().positive() });
|
|
4657
|
+
/** A join's ack: the room, its state and the history this member may replay. */
|
|
4658
|
+
const RoomJoinAck = z.object({
|
|
4659
|
+
room: RoomInfo,
|
|
4660
|
+
state: RoomState,
|
|
4661
|
+
/** Recent messages this member may see, oldest first. */
|
|
4662
|
+
history: z.array(RoomMessage)
|
|
4663
|
+
});
|
|
3947
4664
|
/** The room ops, by op name. */
|
|
3948
4665
|
const roomOps = {
|
|
3949
4666
|
"room.create": op({
|
|
@@ -3956,6 +4673,8 @@ const roomOps = {
|
|
|
3956
4673
|
/** Seats; clamped to the game's max (and 8). Defaults to the max. */
|
|
3957
4674
|
maxPlayers: z.number().optional(),
|
|
3958
4675
|
model: RoomModel.optional(),
|
|
4676
|
+
/** When the host role moves on mid-game (default `grace`). */
|
|
4677
|
+
hostMigration: HostMigration.optional(),
|
|
3959
4678
|
/** The caller's party: when they lead it, its join info becomes `{ room: code }`. */
|
|
3960
4679
|
partyId: OpaqueId.optional()
|
|
3961
4680
|
}),
|
|
@@ -3973,12 +4692,7 @@ const roomOps = {
|
|
|
3973
4692
|
}),
|
|
3974
4693
|
"room.join": op({
|
|
3975
4694
|
d: RoomTarget,
|
|
3976
|
-
ack:
|
|
3977
|
-
room: RoomInfo,
|
|
3978
|
-
state: RoomState,
|
|
3979
|
-
/** Recent messages this member may see, oldest first. */
|
|
3980
|
-
history: z.array(RoomMessage)
|
|
3981
|
-
}),
|
|
4695
|
+
ack: RoomJoinAck,
|
|
3982
4696
|
errors: [
|
|
3983
4697
|
"room_not_found",
|
|
3984
4698
|
"banned",
|
|
@@ -3989,6 +4703,14 @@ const roomOps = {
|
|
|
3989
4703
|
],
|
|
3990
4704
|
doc: "Take a seat (lobby only), reclaim your own seat (any phase, any tab), or watch."
|
|
3991
4705
|
}),
|
|
4706
|
+
"room.reclaim": op({
|
|
4707
|
+
d: z.object({ game: Slug }),
|
|
4708
|
+
ack: z.object({
|
|
4709
|
+
/** The seat I still hold in this game, now reclaimed by this tab; `null` when there is none. */
|
|
4710
|
+
held: RoomJoinAck.nullable() }),
|
|
4711
|
+
errors: [],
|
|
4712
|
+
doc: "Take back the seat I still hold in this game (after a reload or from another tab); the room hears `room.seat` `back`."
|
|
4713
|
+
}),
|
|
3992
4714
|
"room.leave": op({
|
|
3993
4715
|
d: NoInput,
|
|
3994
4716
|
ack: EmptyAck,
|
|
@@ -4315,6 +5037,114 @@ const GameControls = ControlMap.extend({
|
|
|
4315
5037
|
/** Per-player maps, by gamepad order; pads past the end use the first. Overrides the shared map. */
|
|
4316
5038
|
players: z.array(ControlMap).min(1).max(SEAT_LIMITS.maxSeats).optional() });
|
|
4317
5039
|
//#endregion
|
|
5040
|
+
//#region src/manifest/labels.ts
|
|
5041
|
+
/**
|
|
5042
|
+
* `game.labels`: what kind of game it is, from one fixed vocabulary, so the
|
|
5043
|
+
* catalog can filter by them. Labels are grouped (genre, look, where it
|
|
5044
|
+
* plays, how many play); a game lists every label that fits.
|
|
5045
|
+
*
|
|
5046
|
+
* Authoring is lenient: an unknown label is a warning (`hub-doctor`, the
|
|
5047
|
+
* host's boot log), never an error, and the catalog ignores it.
|
|
5048
|
+
*/
|
|
5049
|
+
/** The vocabulary, by group. */
|
|
5050
|
+
const GAME_LABEL_GROUPS = {
|
|
5051
|
+
/** What kind of game. */
|
|
5052
|
+
genre: [
|
|
5053
|
+
"cards",
|
|
5054
|
+
"board",
|
|
5055
|
+
"puzzle",
|
|
5056
|
+
"arcade",
|
|
5057
|
+
"platformer",
|
|
5058
|
+
"real-time-strategy",
|
|
5059
|
+
"physics",
|
|
5060
|
+
"typing",
|
|
5061
|
+
"word",
|
|
5062
|
+
"sports",
|
|
5063
|
+
"racing"
|
|
5064
|
+
],
|
|
5065
|
+
/** How it looks. */
|
|
5066
|
+
form: ["2d", "3d"],
|
|
5067
|
+
/** Where it plays: `phone` (touch controls), `controller` (gamepad support), `desktop` (keyboard or mouse only). */
|
|
5068
|
+
platform: [
|
|
5069
|
+
"phone",
|
|
5070
|
+
"controller",
|
|
5071
|
+
"desktop"
|
|
5072
|
+
],
|
|
5073
|
+
/** How many play. */
|
|
5074
|
+
play: ["multiplayer", "single-player"]
|
|
5075
|
+
};
|
|
5076
|
+
/** The groups in display order. */
|
|
5077
|
+
const GAME_LABEL_GROUP_ORDER = [
|
|
5078
|
+
"genre",
|
|
5079
|
+
"form",
|
|
5080
|
+
"platform",
|
|
5081
|
+
"play"
|
|
5082
|
+
];
|
|
5083
|
+
/** Every label, in display order (group by group). */
|
|
5084
|
+
const GAME_LABELS = [
|
|
5085
|
+
...GAME_LABEL_GROUPS.genre,
|
|
5086
|
+
...GAME_LABEL_GROUPS.form,
|
|
5087
|
+
...GAME_LABEL_GROUPS.platform,
|
|
5088
|
+
...GAME_LABEL_GROUPS.play
|
|
5089
|
+
];
|
|
5090
|
+
/** One known label. */
|
|
5091
|
+
const GameLabel = z.enum(GAME_LABELS);
|
|
5092
|
+
/** Each label's display name. */
|
|
5093
|
+
const GAME_LABEL_TITLES = {
|
|
5094
|
+
cards: "Cards",
|
|
5095
|
+
board: "Board",
|
|
5096
|
+
puzzle: "Puzzle",
|
|
5097
|
+
arcade: "Arcade",
|
|
5098
|
+
platformer: "Platformer",
|
|
5099
|
+
"real-time-strategy": "Real-time strategy",
|
|
5100
|
+
physics: "Physics",
|
|
5101
|
+
typing: "Typing",
|
|
5102
|
+
word: "Word",
|
|
5103
|
+
sports: "Sports",
|
|
5104
|
+
racing: "Racing",
|
|
5105
|
+
"2d": "2D",
|
|
5106
|
+
"3d": "3D",
|
|
5107
|
+
phone: "Phone",
|
|
5108
|
+
controller: "Controller",
|
|
5109
|
+
desktop: "Desktop only",
|
|
5110
|
+
multiplayer: "Multiplayer",
|
|
5111
|
+
"single-player": "Single player"
|
|
5112
|
+
};
|
|
5113
|
+
/** Each group's display name. */
|
|
5114
|
+
const GAME_LABEL_GROUP_TITLES = {
|
|
5115
|
+
genre: "Genre",
|
|
5116
|
+
form: "Look",
|
|
5117
|
+
platform: "Plays on",
|
|
5118
|
+
play: "Players"
|
|
5119
|
+
};
|
|
5120
|
+
/** Whether `text` is a known label. */
|
|
5121
|
+
function isGameLabel(text) {
|
|
5122
|
+
return GameLabel.safeParse(text).success;
|
|
5123
|
+
}
|
|
5124
|
+
/** The group a label belongs to. */
|
|
5125
|
+
function groupOfLabel(label) {
|
|
5126
|
+
return GAME_LABEL_GROUP_ORDER.find((group) => GAME_LABEL_GROUPS[group].some((member) => member === label)) ?? "genre";
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* `game.labels` as written: short strings. Unknown ones parse (they are a
|
|
5130
|
+
* warning, see {@link splitGameLabels}).
|
|
5131
|
+
*/
|
|
5132
|
+
const GameLabels = z.array(z.string().trim().min(1).max(40)).max(24);
|
|
5133
|
+
/** Known labels (trimmed, lower-cased, de-duplicated, in vocabulary order) and the unknown ones as written. */
|
|
5134
|
+
function splitGameLabels(written) {
|
|
5135
|
+
const known = /* @__PURE__ */ new Set();
|
|
5136
|
+
const unknown = [];
|
|
5137
|
+
for (const text of written) {
|
|
5138
|
+
const candidate = text.trim().toLowerCase();
|
|
5139
|
+
if (isGameLabel(candidate)) known.add(candidate);
|
|
5140
|
+
else unknown.push(text);
|
|
5141
|
+
}
|
|
5142
|
+
return {
|
|
5143
|
+
labels: GAME_LABELS.filter((label) => known.has(label)),
|
|
5144
|
+
unknown
|
|
5145
|
+
};
|
|
5146
|
+
}
|
|
5147
|
+
//#endregion
|
|
4318
5148
|
//#region src/manifest/leaderboards.ts
|
|
4319
5149
|
/**
|
|
4320
5150
|
* `game.leaderboards`: boards declared up front, so they show on the catalog
|
|
@@ -4384,6 +5214,14 @@ const GameMultiplayer = z.strictObject({
|
|
|
4384
5214
|
spectate: z.boolean().default(false),
|
|
4385
5215
|
/** Offer quick match. Default `false`. */
|
|
4386
5216
|
quickMatch: z.boolean().default(false),
|
|
5217
|
+
/**
|
|
5218
|
+
* Tournaments can be played in this game (docs/multiplayer.md §15). Default
|
|
5219
|
+
* `false`. The game must play a 1v1 match from a `host`/`guest` launch that
|
|
5220
|
+
* carries `launch.tournament`, and report its winner: hub rooms end with
|
|
5221
|
+
* `room.end({ winnerId })`, own-server games post results with
|
|
5222
|
+
* `tournament`. Race games need no flag: their races are the matches.
|
|
5223
|
+
*/
|
|
5224
|
+
tournaments: z.boolean().default(false),
|
|
4387
5225
|
/** Modes players can be invited into (at most 16, unique keys). */
|
|
4388
5226
|
modes: z.array(MultiplayerMode).max(MANIFEST_LIMITS.modesMax).refine(hasUniqueKeys, "mode keys must be unique").default([]),
|
|
4389
5227
|
/**
|
|
@@ -4455,6 +5293,12 @@ const commonFields = {
|
|
|
4455
5293
|
menuPosition: MenuPosition.optional(),
|
|
4456
5294
|
/** Push the menu button away from its corner, px: a number (vertical) or `{ x, y }`. */
|
|
4457
5295
|
menuOffset: MenuOffset.optional(),
|
|
5296
|
+
/**
|
|
5297
|
+
* What kind of game it is, from the fixed vocabulary in `labels.ts` (genre, look,
|
|
5298
|
+
* where it plays, how many play), for the catalog's label filter. Unknown labels
|
|
5299
|
+
* are a warning, not an error.
|
|
5300
|
+
*/
|
|
5301
|
+
labels: GameLabels.optional(),
|
|
4458
5302
|
/** `true` opts into the shared daily challenges (`hub.daily.define(...)`). */
|
|
4459
5303
|
dailyChallenges: z.boolean().optional(),
|
|
4460
5304
|
/** Boards to create up front, so they show before the first score. */
|
|
@@ -4850,6 +5694,6 @@ function historyBadge(racerResult) {
|
|
|
4850
5694
|
}
|
|
4851
5695
|
}
|
|
4852
5696
|
//#endregion
|
|
4853
|
-
export { ACCOUNT_LIMITS, AckFrame, AppFilePath, Audience, AuthResult, AuthUser, Board, Bundle, BundleInput, ButtonIndex, CHAT_ERRORS, COMMAND_PLACEHOLDERS, ChangeEntry, ChangeNote, ChangeText, ChatAllowWord, ChatLine, ChatLineEvent, ChatMode, ChatSend, CloseCode, Command, ContentRejected, ControlMap, Conversation, Counts, Credentials, DailyCompletion, DailyCompletions, DayKey, ENVELOPE_ERRORS, ErrAckFrame, ErrorBody, ErrorCode, EvFrame, FriendChange, FriendEntry, FriendRequestInput, FriendRequestStatus, FriendRequests, FriendsList, GameChanges, GameControls, GameInfo, GameLeaderboardDecls, GameLeaderboards, GameManifest, GameMode, GameMultiplayer, GamePackageJson, GameRace, GameRecords, GameSnapshot, GameTicket, HexColor, HostManifest, HubInject, HubVersion, IdParam, Inventory, Invite, InviteCreated, InviteStatus, JoinInfo, Json, Key, KeyCode, Launch, LaunchKind, LaunchTicket, LeaderboardDecl, LeaderboardEntry, LobbySuffix, MANIFEST_LIMITS, MAX_BUNDLE_ITEMS, MAX_ITEM_QTY, MenuOffset, MenuOffsetXY, MenuPosition, Message, MessageId, MessagesPage, Metrics, Millis, ModeKey, MultiplayerMode, MultiplayerSnapshot, MultiplayerSummary, NotifyChange, NotifyPrefKind, NotifyPrefs, Ok, OkAckFrame, OpFrame, OpaqueId, PRE_HELLO_OPS, Party, PlayerCard, PlayerLite, PlayerRange, Presence, PresenceInput, PresenceKind, PresenceMode, PresenceRoom, ProcessGame, Profile, PushSubscriptionJson, QuickId, RACE_STATS_LIMITS, RaceBlock, RaceHistory, RaceHistoryQuery, RaceKey, RaceOption, RaceOutcome, RaceParam, RaceParamValue, RaceParams, RacePhase, RaceProgress, RaceRecord, RaceRecordPlayer, RaceResult, RaceResultPlayer, RaceStart, RaceStat, RaceStatFormat, RaceStatValue, RaceState, RaceStats, RaceSummary, Racer, RacerResult, RacerState, Rank, RecentPlayer, RejectReason, Relation, ReportInput, ReportKind, ReportReason, ReportSnapshot, ReportedMessage, Role, RoomClosedReason, RoomCode, RoomInfo, RoomMessage, RoomModel, RoomPhase, RoomSeat, RoomState, RoomTarget, SEAT_LIMITS, Save, SaveMeta, SeatGoneReason, SendMessageInput, ServerFrame, SessionUser, Slug, SocialConfig, SocialLimits, SocialMe, SocialMeResult, SortDir, StatMode, StaticGame, StaticGameMultiplayer, Stats, Suggestion, SuggestionId, SuggestionStatus, TopQuery, TradeOffer, TradePlayer, TradeStatus, Transport, UI_LIMIT_DEFAULTS, UNSUPPORTED_SUBSCRIPTION, USERNAME_PATTERN, UiLimits, UnreadSummary, UserId, UserIdParam, VersionRange, VoteResult, analyticsEndpoints, authEndpoints, badgeText, bundleInput, catalogEndpoints, countdown, dailyEndpoints, dayKey, economyEndpoints, elapsed, event, events, formatStat, game, hasOwnTime, headerLine, historyBadge, historyVerb, inWindow, leaderboardsEndpoints, messagesEndpoints, mpEndpoints, nameList, normalizeChangeText, notifyEndpoints, op, ops, outcomeLine, paramsText, progressEndpoints, publicApi, publicEndpoints, raceClock, raceEndpoints, racerStateTag, relativeTime, reportsEndpoints, requireActive, requireClaimed, requireTrader, requireUser, resultLabel, resultTitle, seedForDay, session, shownStats, socialEndpoints, statusParts, statusText, untilText, versionEndpoints, windowDays };
|
|
5697
|
+
export { ACCOUNT_LIMITS, AckFrame, AppFilePath, Audience, AuthResult, AuthUser, Board, Bundle, BundleInput, ButtonIndex, CHAT_ERRORS, COMMAND_PLACEHOLDERS, ChangeEntry, ChangeNote, ChangeText, ChatAllowWord, ChatLine, ChatLineEvent, ChatMode, ChatSend, CloseCode, Command, ContentRejected, ControlMap, Conversation, Counts, Credentials, DailyCompletion, DailyCompletions, DayKey, ENVELOPE_ERRORS, ErrAckFrame, ErrorBody, ErrorCode, EvFrame, FriendChange, FriendEntry, FriendRequestInput, FriendRequestStatus, FriendRequests, FriendsList, GAME_LABELS, GAME_LABEL_GROUPS, GAME_LABEL_GROUP_ORDER, GAME_LABEL_GROUP_TITLES, GAME_LABEL_TITLES, GameChanges, GameControls, GameInfo, GameLabel, GameLabels, GameLeaderboardDecls, GameLeaderboards, GameManifest, GameMode, GameMultiplayer, GamePackageJson, GamePlayerCount, GamePlayerCounts, GameRace, GameRecords, GameSnapshot, GameTicket, HexColor, HostManifest, HostMigration, HubInject, HubVersion, IdParam, Inventory, Invite, InviteCreated, InviteLink, InviteLinkCreate, InviteLinkParams, InviteLinkQuery, InviteLinkToken, InviteStatus, JoinInfo, Json, Key, KeyCode, Launch, LaunchKind, LaunchTicket, LeaderboardDecl, LeaderboardEntry, LobbySuffix, MANIFEST_LIMITS, MAX_BUNDLE_ITEMS, MAX_ITEM_QTY, MenuOffset, MenuOffsetXY, MenuPosition, Message, MessageId, MessagesPage, Metrics, Millis, ModeKey, MultiplayerMode, MultiplayerSnapshot, MultiplayerSummary, MyGameActivity, MyTournamentMatch, NotifyChange, NotifyPrefKind, NotifyPrefs, OFFLINE_DEDUPE_TTL_MS, OFFLINE_OP_HEADER, OFFLINE_REPLAY_ENDPOINTS, OfflineOpKey, Ok, OkAckFrame, OpFrame, OpaqueId, OpenLobbyEntry, PRE_HELLO_OPS, Party, PlayOnline, PlayOnlineCounts, PlayerCard, PlayerLite, PlayerRange, Presence, PresenceInput, PresenceKind, PresenceMode, PresenceRoom, ProcessGame, Profile, PushSubscriptionJson, QuickId, RACE_STATS_LIMITS, RaceBlock, RaceHistory, RaceHistoryQuery, RaceKey, RaceOption, RaceOutcome, RaceParam, RaceParamValue, RaceParams, RacePhase, RaceProgress, RaceRecord, RaceRecordPlayer, RaceResult, RaceResultPlayer, RaceStart, RaceStat, RaceStatFormat, RaceStatValue, RaceState, RaceStats, RaceSummary, Racer, RacerResult, RacerState, Rank, RecentPlayer, RejectReason, Relation, ReportInput, ReportKind, ReportReason, ReportSnapshot, ReportedMessage, Role, RoomClosedReason, RoomCode, RoomInfo, RoomMessage, RoomModel, RoomPhase, RoomSeat, RoomState, RoomTarget, SEAT_LIMITS, Save, SaveMeta, SeatChange, SeatGoneReason, SeatStatus, SendMessageInput, ServerFrame, SessionUser, Slug, SocialConfig, SocialLimits, SocialMe, SocialMeResult, SortDir, StatMode, StaticGame, StaticGameMultiplayer, Stats, Suggestion, SuggestionId, SuggestionStatus, TOURNAMENT_LIMITS, TopQuery, TournamentCreate, TournamentDecision, TournamentDetail, TournamentGamePick, TournamentId, TournamentLaunch, TournamentListQuery, TournamentMatch, TournamentMe, TournamentPhase, TournamentPlayer, TournamentSeeding, TournamentStage, TournamentStart, TournamentSummary, TournamentVisibility, TradeOffer, TradePlayer, TradeStatus, Transport, UI_LIMIT_DEFAULTS, UNSUPPORTED_SUBSCRIPTION, USERNAME_PATTERN, UiLimits, UnreadSummary, UserId, UserIdParam, VersionRange, VoteResult, analyticsEndpoints, authEndpoints, badgeText, bundleInput, catalogActivityEndpoints, catalogEndpoints, countdown, dailyEndpoints, dayKey, economyEndpoints, elapsed, event, events, formatStat, game, groupOfLabel, hasOwnTime, headerLine, historyBadge, historyVerb, inWindow, isGameLabel, isOfflineReplayEndpoint, leaderboardsEndpoints, messagesEndpoints, mpEndpoints, nameList, normalizeChangeText, notifyEndpoints, op, ops, outcomeLine, paramsText, playOnlineEndpoints, progressEndpoints, publicApi, publicEndpoints, raceClock, raceEndpoints, racerStateTag, relativeTime, reportsEndpoints, requireActive, requireClaimed, requireTrader, requireUser, resultLabel, resultTitle, seedForDay, session, shownStats, socialEndpoints, splitGameLabels, statusParts, statusText, tournamentEndpoints, untilText, versionEndpoints, windowDays };
|
|
4854
5698
|
|
|
4855
5699
|
//# sourceMappingURL=index.js.map
|