@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,89 +1,89 @@
1
- /**
2
- * Field rules shared by several parts of the `game.*` manifest. Each rule is
3
- * defined once here.
4
- */
5
- import { z } from 'zod';
6
- import { MANIFEST_LIMITS, SEAT_LIMITS } from '../limits';
7
-
8
- /**
9
- * A non-empty string with no leading or trailing whitespace. Used for fields
10
- * the hub reads verbatim (commands, paths, titles), where stray spaces are a
11
- * mistake rather than something the hub would trim away.
12
- */
13
- export const Trimmed = z.string().regex(/^\S(?:[\s\S]*\S)?$/, 'must be non-empty, with no leading or trailing spaces');
14
-
15
- /** A label the hub trims and shows to players, at most `max` characters. */
16
- export function trimmedText(max: number) {
17
- return z.string().trim().min(1).max(max);
18
- }
19
-
20
- /**
21
- * A URL suffix relative to `/<slug>/` that invites and Join open, such as
22
- * `?lobby=1` or `play/online`. Not absolute, no `..`, no `//`.
23
- */
24
- export const LobbySuffix = z
25
- .string()
26
- .trim()
27
- .regex(new RegExp(`^[A-Za-z0-9_./?=&#%-]{0,${MANIFEST_LIMITS.lobbyMax}}$`), 'may only use letters, digits and _ . / ? = & # % -')
28
- .refine((s) => !s.startsWith('/') && !s.includes('..') && !s.includes('//'), 'must be a relative URL suffix (no leading /, no .., no //)');
29
-
30
- /**
31
- * A `[min, max]` player range: whole numbers, `lowest <= min <= max <= 8`.
32
- *
33
- * @param lowest - the smallest `min` allowed (1 for multiplayer, 2 for races)
34
- */
35
- export function playerRange(lowest: number) {
36
- const seatCount = z.number().int().min(lowest).max(SEAT_LIMITS.maxSeats);
37
- return z.tuple([seatCount, seatCount]).refine(([min, max]) => min <= max, `must be [min, max] with ${lowest} <= min <= max <= ${SEAT_LIMITS.maxSeats}`);
38
- }
39
-
40
- /**
41
- * An optional block that may also be written as `false` ("off"). `false`
42
- * parses to `undefined`, the same as leaving the field out.
43
- *
44
- * (A `z.union` with `z.literal(false)` would work too, but it hides the
45
- * block's own field errors behind a bare "Invalid input".)
46
- */
47
- export function orFalse<T extends z.ZodType>(block: T) {
48
- return z.preprocess((v: false | z.input<T>) => (v === false ? undefined : v), block.optional()).optional();
49
- }
50
-
51
- /** True when no two items share a `key`. */
52
- export function hasUniqueKeys(items: readonly { readonly key: string }[]): boolean {
53
- return new Set(items.map((i) => i.key)).size === items.length;
54
- }
55
-
56
- /** The placeholders a `build` or `start` command may use. */
57
- export const COMMAND_PLACEHOLDERS = ['PORT', 'GAME_PATH', 'GAME_SLUG'] as const;
58
-
59
- const PLACEHOLDER = /\{\{(\w+)\}\}/g;
60
- const KNOWN_PLACEHOLDERS: ReadonlySet<string> = new Set(COMMAND_PLACEHOLDERS);
61
-
62
- /**
63
- * A shell command the host runs in the app folder. It may use the
64
- * placeholders `{{PORT}}`, `{{GAME_PATH}}` and `{{GAME_SLUG}}`; any other
65
- * `{{NAME}}` fails the build.
66
- */
67
- export const Command = Trimmed.refine(
68
- (cmd) => [...cmd.matchAll(PLACEHOLDER)].every((m) => KNOWN_PLACEHOLDERS.has(m[1] ?? '')),
69
- `may only use the placeholders ${COMMAND_PLACEHOLDERS.map((p) => `{{${p}}}`).join(', ')}`,
70
- );
71
-
72
- /** True when a relative path names something strictly inside its folder. */
73
- function staysInside(path: string): boolean {
74
- let depth = 0;
75
- for (const part of path.split(/[\\/]/)) {
76
- if (part === '..') {
77
- depth -= 1;
78
- if (depth < 0) {
79
- return false;
80
- }
81
- } else if (part !== '' && part !== '.') {
82
- depth += 1;
83
- }
84
- }
85
- return depth > 0;
86
- }
87
-
88
- /** A file path relative to the app folder that stays inside it, e.g. `cover.png`. */
89
- export const AppFilePath = Trimmed.refine(staysInside, 'must be a file inside the app folder');
1
+ /**
2
+ * Field rules shared by several parts of the `game.*` manifest. Each rule is
3
+ * defined once here.
4
+ */
5
+ import { z } from 'zod';
6
+ import { MANIFEST_LIMITS, SEAT_LIMITS } from '../limits';
7
+
8
+ /**
9
+ * A non-empty string with no leading or trailing whitespace. Used for fields
10
+ * the hub reads verbatim (commands, paths, titles), where stray spaces are a
11
+ * mistake rather than something the hub would trim away.
12
+ */
13
+ export const Trimmed = z.string().regex(/^\S(?:[\s\S]*\S)?$/, 'must be non-empty, with no leading or trailing spaces');
14
+
15
+ /** A label the hub trims and shows to players, at most `max` characters. */
16
+ export function trimmedText(max: number) {
17
+ return z.string().trim().min(1).max(max);
18
+ }
19
+
20
+ /**
21
+ * A URL suffix relative to `/<slug>/` that invites and Join open, such as
22
+ * `?lobby=1` or `play/online`. Not absolute, no `..`, no `//`.
23
+ */
24
+ export const LobbySuffix = z
25
+ .string()
26
+ .trim()
27
+ .regex(new RegExp(`^[A-Za-z0-9_./?=&#%-]{0,${MANIFEST_LIMITS.lobbyMax}}$`), 'may only use letters, digits and _ . / ? = & # % -')
28
+ .refine((s) => !s.startsWith('/') && !s.includes('..') && !s.includes('//'), 'must be a relative URL suffix (no leading /, no .., no //)');
29
+
30
+ /**
31
+ * A `[min, max]` player range: whole numbers, `lowest <= min <= max <= 8`.
32
+ *
33
+ * @param lowest - the smallest `min` allowed (1 for multiplayer, 2 for races)
34
+ */
35
+ export function playerRange(lowest: number) {
36
+ const seatCount = z.number().int().min(lowest).max(SEAT_LIMITS.maxSeats);
37
+ return z.tuple([seatCount, seatCount]).refine(([min, max]) => min <= max, `must be [min, max] with ${lowest} <= min <= max <= ${SEAT_LIMITS.maxSeats}`);
38
+ }
39
+
40
+ /**
41
+ * An optional block that may also be written as `false` ("off"). `false`
42
+ * parses to `undefined`, the same as leaving the field out.
43
+ *
44
+ * (A `z.union` with `z.literal(false)` would work too, but it hides the
45
+ * block's own field errors behind a bare "Invalid input".)
46
+ */
47
+ export function orFalse<T extends z.ZodType>(block: T) {
48
+ return z.preprocess((v: false | z.input<T>) => (v === false ? undefined : v), block.optional()).optional();
49
+ }
50
+
51
+ /** True when no two items share a `key`. */
52
+ export function hasUniqueKeys(items: readonly { readonly key: string }[]): boolean {
53
+ return new Set(items.map((i) => i.key)).size === items.length;
54
+ }
55
+
56
+ /** The placeholders a `build` or `start` command may use. */
57
+ export const COMMAND_PLACEHOLDERS = ['PORT', 'GAME_PATH', 'GAME_SLUG'] as const;
58
+
59
+ const PLACEHOLDER = /\{\{(\w+)\}\}/g;
60
+ const KNOWN_PLACEHOLDERS: ReadonlySet<string> = new Set(COMMAND_PLACEHOLDERS);
61
+
62
+ /**
63
+ * A shell command the host runs in the app folder. It may use the
64
+ * placeholders `{{PORT}}`, `{{GAME_PATH}}` and `{{GAME_SLUG}}`; any other
65
+ * `{{NAME}}` fails the build.
66
+ */
67
+ export const Command = Trimmed.refine(
68
+ (cmd) => [...cmd.matchAll(PLACEHOLDER)].every((m) => KNOWN_PLACEHOLDERS.has(m[1] ?? '')),
69
+ `may only use the placeholders ${COMMAND_PLACEHOLDERS.map((p) => `{{${p}}}`).join(', ')}`,
70
+ );
71
+
72
+ /** True when a relative path names something strictly inside its folder. */
73
+ function staysInside(path: string): boolean {
74
+ let depth = 0;
75
+ for (const part of path.split(/[\\/]/)) {
76
+ if (part === '..') {
77
+ depth -= 1;
78
+ if (depth < 0) {
79
+ return false;
80
+ }
81
+ } else if (part !== '' && part !== '.') {
82
+ depth += 1;
83
+ }
84
+ }
85
+ return depth > 0;
86
+ }
87
+
88
+ /** A file path relative to the app folder that stays inside it, e.g. `cover.png`. */
89
+ export const AppFilePath = Trimmed.refine(staysInside, 'must be a file inside the app folder');
@@ -1,132 +1,132 @@
1
- /**
2
- * The `game` block of an app's `package.json`: how the host builds, serves
3
- * and shows the game (HANDOFF.md "The game block").
4
- *
5
- * This is the **strict** authoring schema `hub-doctor` runs in a game: a
6
- * mistake fails the parse. The host stays lenient at boot (a bad field falls
7
- * back to its default with a warning), so one typo never takes a game down.
8
- */
9
- import { z } from 'zod';
10
- import { MANIFEST_LIMITS } from '../limits';
11
- import { GameChanges } from './changes';
12
- import { GameControls } from './controls';
13
- import { AppFilePath, Command, Trimmed, orFalse } from './fields';
14
- import { GameLeaderboardDecls } from './leaderboards';
15
- import { GameMultiplayer, StaticGameMultiplayer } from './multiplayer';
16
- import { GameRace } from './race';
17
-
18
- /** The corner the injected hub menu button sits in. Default `top-left`. */
19
- export const MenuPosition = z.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
20
-
21
- /** A menu button corner. */
22
- export type MenuPosition = z.infer<typeof MenuPosition>;
23
-
24
- /** Pixels to push the menu button away from its corner, 0 to 600. */
25
- const OffsetPx = z.number().int().min(0).max(MANIFEST_LIMITS.menuOffsetMax);
26
-
27
- /** The menu offset as the host sends it: both axes, in px. */
28
- export const MenuOffsetXY = z.strictObject({
29
- /** Inward from the left or right edge, px. */
30
- x: OffsetPx,
31
- /** Down from a top corner or up from a bottom one, px. */
32
- y: OffsetPx,
33
- });
34
-
35
- /**
36
- * `game.menuOffset`: how far to push the menu button away from its corner so
37
- * it clears the game's own bars. A number moves it vertically only.
38
- */
39
- export const MenuOffset = z.union([OffsetPx, MenuOffsetXY.partial()]);
40
-
41
- /** Fields both app types share. */
42
- const commonFields = {
43
- /** Shown on the catalog card and in the menu. */
44
- title: Trimmed,
45
- /** Catalog, search and link-preview text. Falls back to `package.json` `description`. */
46
- description: Trimmed.optional(),
47
- /**
48
- * Credit shown on the tile, e.g. `"Mason & Phil"` (split into authors for the
49
- * catalog filter). Falls back to `package.json` `author`.
50
- */
51
- author: z.string().trim().min(1).optional(),
52
- /** Runs at boot when the source changed. May use `{{PORT}}`, `{{GAME_PATH}}`, `{{GAME_SLUG}}`. */
53
- build: Command.optional(),
54
- /** Tile and preview image, relative to the project root (not `serveDir`); must stay inside it. */
55
- image: AppFilePath.optional(),
56
- /** `true` leaves the app out of the catalog entirely. */
57
- hidden: z.boolean().optional(),
58
- /** `false` opts out of the injected hub menu and SDK. */
59
- hub: z.boolean().optional(),
60
- /** `false` opts out of the PWA install tags and service worker. */
61
- pwa: z.boolean().optional(),
62
- /** The corner the hub menu button sits in. Default `top-left`. */
63
- menuPosition: MenuPosition.optional(),
64
- /** Push the menu button away from its corner, px: a number (vertical) or `{ x, y }`. */
65
- menuOffset: MenuOffset.optional(),
66
- /** `true` opts into the shared daily challenges (`hub.daily.define(...)`). */
67
- dailyChallenges: z.boolean().optional(),
68
- /** Boards to create up front, so they show before the first score. */
69
- leaderboards: GameLeaderboardDecls.optional(),
70
- /** The gamepad-to-keyboard map for games that never call `hub.input.define()`. */
71
- controls: GameControls.optional(),
72
- /** Turns a single-player game into a race target; `true` for all defaults, `false` for off. */
73
- race: GameRace,
74
- /** Player-facing "What's new" notes, one per change, newest last. Keep it updated. */
75
- changes: GameChanges.optional(),
76
- };
77
-
78
- /** A static app: the build emits a folder the host serves. */
79
- export const StaticGame = z.strictObject({
80
- /** `static`: no running process. */
81
- type: z.literal('static'),
82
- /** The folder served at `/<slug>/`; must exist after `build`. */
83
- serveDir: Trimmed,
84
- /** Makes the game an online invite/join target (`hub-rooms` only); `false` for off. */
85
- multiplayer: orFalse(StaticGameMultiplayer),
86
- ...commonFields,
87
- });
88
-
89
- /** A process app: the host runs a long-lived server and proxies to it. */
90
- export const ProcessGame = z.strictObject({
91
- /** `process`: the host starts `start` and proxies `/<slug>/` to it. */
92
- type: z.literal('process'),
93
- /** The long-lived server command; must listen on the assigned port (`{{PORT}}` or `PORT`). */
94
- start: Command,
95
- /** The path the host polls until it answers below 500. Default `/`. */
96
- healthPath: Trimmed.startsWith('/').optional(),
97
- /** Makes the game an online invite/join target; `false` for off. */
98
- multiplayer: orFalse(GameMultiplayer),
99
- ...commonFields,
100
- });
101
-
102
- /** The `game` block of a game's `package.json`. */
103
- export const GameManifest = z.discriminatedUnion('type', [StaticGame, ProcessGame]);
104
-
105
- /** A parsed `game` block (defaults applied). */
106
- export type GameManifest = z.output<typeof GameManifest>;
107
-
108
- /** The `game` block as a game writes it. */
109
- export type GameManifestInput = z.input<typeof GameManifest>;
110
-
111
- /** A game's `package.json`: the `game` block plus the npm fields the host falls back to. */
112
- export const GamePackageJson = z.looseObject({
113
- /** The npm name; the title fallback. */
114
- name: z.string().optional(),
115
- /** The description fallback. */
116
- description: z.string().optional(),
117
- /** The author fallback: npm's string or `{ name }` form. */
118
- author: z.union([z.string(), z.looseObject({ name: z.string().optional() })]).optional(),
119
- /** How the host treats the app. */
120
- game: GameManifest,
121
- });
122
-
123
- /**
124
- * The host repo's own `game` block: only its "What's new" notes (shown as
125
- * "diffenderfer.games"). It is not an app, so it has no `type`.
126
- */
127
- export const HostManifest = z.strictObject({
128
- /** The host's display name. */
129
- title: Trimmed.optional(),
130
- /** The host's own notes (menu, friends, home page). */
131
- changes: GameChanges.optional(),
132
- });
1
+ /**
2
+ * The `game` block of an app's `package.json`: how the host builds, serves
3
+ * and shows the game (HANDOFF.md "The game block").
4
+ *
5
+ * This is the **strict** authoring schema `hub-doctor` runs in a game: a
6
+ * mistake fails the parse. The host stays lenient at boot (a bad field falls
7
+ * back to its default with a warning), so one typo never takes a game down.
8
+ */
9
+ import { z } from 'zod';
10
+ import { MANIFEST_LIMITS } from '../limits';
11
+ import { GameChanges } from './changes';
12
+ import { GameControls } from './controls';
13
+ import { AppFilePath, Command, Trimmed, orFalse } from './fields';
14
+ import { GameLeaderboardDecls } from './leaderboards';
15
+ import { GameMultiplayer, StaticGameMultiplayer } from './multiplayer';
16
+ import { GameRace } from './race';
17
+
18
+ /** The corner the injected hub menu button sits in. Default `top-left`. */
19
+ export const MenuPosition = z.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right']);
20
+
21
+ /** A menu button corner. */
22
+ export type MenuPosition = z.infer<typeof MenuPosition>;
23
+
24
+ /** Pixels to push the menu button away from its corner, 0 to 600. */
25
+ const OffsetPx = z.number().int().min(0).max(MANIFEST_LIMITS.menuOffsetMax);
26
+
27
+ /** The menu offset as the host sends it: both axes, in px. */
28
+ export const MenuOffsetXY = z.strictObject({
29
+ /** Inward from the left or right edge, px. */
30
+ x: OffsetPx,
31
+ /** Down from a top corner or up from a bottom one, px. */
32
+ y: OffsetPx,
33
+ });
34
+
35
+ /**
36
+ * `game.menuOffset`: how far to push the menu button away from its corner so
37
+ * it clears the game's own bars. A number moves it vertically only.
38
+ */
39
+ export const MenuOffset = z.union([OffsetPx, MenuOffsetXY.partial()]);
40
+
41
+ /** Fields both app types share. */
42
+ const commonFields = {
43
+ /** Shown on the catalog card and in the menu. */
44
+ title: Trimmed,
45
+ /** Catalog, search and link-preview text. Falls back to `package.json` `description`. */
46
+ description: Trimmed.optional(),
47
+ /**
48
+ * Credit shown on the tile, e.g. `"Mason & Phil"` (split into authors for the
49
+ * catalog filter). Falls back to `package.json` `author`.
50
+ */
51
+ author: z.string().trim().min(1).optional(),
52
+ /** Runs at boot when the source changed. May use `{{PORT}}`, `{{GAME_PATH}}`, `{{GAME_SLUG}}`. */
53
+ build: Command.optional(),
54
+ /** Tile and preview image, relative to the project root (not `serveDir`); must stay inside it. */
55
+ image: AppFilePath.optional(),
56
+ /** `true` leaves the app out of the catalog entirely. */
57
+ hidden: z.boolean().optional(),
58
+ /** `false` opts out of the injected hub menu and SDK. */
59
+ hub: z.boolean().optional(),
60
+ /** `false` opts out of the PWA install tags and service worker. */
61
+ pwa: z.boolean().optional(),
62
+ /** The corner the hub menu button sits in. Default `top-left`. */
63
+ menuPosition: MenuPosition.optional(),
64
+ /** Push the menu button away from its corner, px: a number (vertical) or `{ x, y }`. */
65
+ menuOffset: MenuOffset.optional(),
66
+ /** `true` opts into the shared daily challenges (`hub.daily.define(...)`). */
67
+ dailyChallenges: z.boolean().optional(),
68
+ /** Boards to create up front, so they show before the first score. */
69
+ leaderboards: GameLeaderboardDecls.optional(),
70
+ /** The gamepad-to-keyboard map for games that never call `hub.input.define()`. */
71
+ controls: GameControls.optional(),
72
+ /** Turns a single-player game into a race target; `true` for all defaults, `false` for off. */
73
+ race: GameRace,
74
+ /** Player-facing "What's new" notes, one per change, newest last. Keep it updated. */
75
+ changes: GameChanges.optional(),
76
+ };
77
+
78
+ /** A static app: the build emits a folder the host serves. */
79
+ export const StaticGame = z.strictObject({
80
+ /** `static`: no running process. */
81
+ type: z.literal('static'),
82
+ /** The folder served at `/<slug>/`; must exist after `build`. */
83
+ serveDir: Trimmed,
84
+ /** Makes the game an online invite/join target (`hub-rooms` only); `false` for off. */
85
+ multiplayer: orFalse(StaticGameMultiplayer),
86
+ ...commonFields,
87
+ });
88
+
89
+ /** A process app: the host runs a long-lived server and proxies to it. */
90
+ export const ProcessGame = z.strictObject({
91
+ /** `process`: the host starts `start` and proxies `/<slug>/` to it. */
92
+ type: z.literal('process'),
93
+ /** The long-lived server command; must listen on the assigned port (`{{PORT}}` or `PORT`). */
94
+ start: Command,
95
+ /** The path the host polls until it answers below 500. Default `/`. */
96
+ healthPath: Trimmed.startsWith('/').optional(),
97
+ /** Makes the game an online invite/join target; `false` for off. */
98
+ multiplayer: orFalse(GameMultiplayer),
99
+ ...commonFields,
100
+ });
101
+
102
+ /** The `game` block of a game's `package.json`. */
103
+ export const GameManifest = z.discriminatedUnion('type', [StaticGame, ProcessGame]);
104
+
105
+ /** A parsed `game` block (defaults applied). */
106
+ export type GameManifest = z.output<typeof GameManifest>;
107
+
108
+ /** The `game` block as a game writes it. */
109
+ export type GameManifestInput = z.input<typeof GameManifest>;
110
+
111
+ /** A game's `package.json`: the `game` block plus the npm fields the host falls back to. */
112
+ export const GamePackageJson = z.looseObject({
113
+ /** The npm name; the title fallback. */
114
+ name: z.string().optional(),
115
+ /** The description fallback. */
116
+ description: z.string().optional(),
117
+ /** The author fallback: npm's string or `{ name }` form. */
118
+ author: z.union([z.string(), z.looseObject({ name: z.string().optional() })]).optional(),
119
+ /** How the host treats the app. */
120
+ game: GameManifest,
121
+ });
122
+
123
+ /**
124
+ * The host repo's own `game` block: only its "What's new" notes (shown as
125
+ * "diffenderfer.games"). It is not an app, so it has no `type`.
126
+ */
127
+ export const HostManifest = z.strictObject({
128
+ /** The host's display name. */
129
+ title: Trimmed.optional(),
130
+ /** The host's own notes (menu, friends, home page). */
131
+ changes: GameChanges.optional(),
132
+ });
@@ -1,12 +1,12 @@
1
- /**
2
- * The `package.json` `game.*` manifest and the injected `window.__HUB__`
3
- * config.
4
- */
5
- export * from './changes';
6
- export * from './controls';
7
- export { AppFilePath, COMMAND_PLACEHOLDERS, Command, LobbySuffix } from './fields';
8
- export * from './game';
9
- export * from './inject';
10
- export * from './leaderboards';
11
- export * from './multiplayer';
12
- export * from './race';
1
+ /**
2
+ * The `package.json` `game.*` manifest and the injected `window.__HUB__`
3
+ * config.
4
+ */
5
+ export * from './changes';
6
+ export * from './controls';
7
+ export { AppFilePath, COMMAND_PLACEHOLDERS, Command, LobbySuffix } from './fields';
8
+ export * from './game';
9
+ export * from './inject';
10
+ export * from './leaderboards';
11
+ export * from './multiplayer';
12
+ export * from './race';
@@ -1,31 +1,31 @@
1
- /**
2
- * `window.__HUB__`: the small config object the host writes into every page
3
- * it injects (game pages, and the catalog page). The SDK and the menu read it
4
- * to find the game's slug, the API base and the menu placement.
5
- *
6
- * The same inline script may also set `window.__hubDailyDeep` (a
7
- * `?daily=YYYY-MM-DD` deep link captured before game code runs) and
8
- * `window.__hubInstall` (the PWA install prompt); those are not part of this
9
- * object.
10
- */
11
- import { z } from 'zod';
12
- import { Slug } from '../common';
13
- import { GameControls } from './controls';
14
- import { MenuOffsetXY, MenuPosition } from './game';
15
-
16
- /** The injected `window.__HUB__` config. */
17
- export const HubInject = z.strictObject({
18
- /** The game's slug. Missing on the catalog page. */
19
- slug: Slug.optional(),
20
- /** Where the REST API lives. Always `/_api` today. */
21
- apiBase: z.literal('/_api'),
22
- /** The menu button's corner (from `game.menuPosition`). Missing on the catalog page. */
23
- menu: MenuPosition.optional(),
24
- /** The menu button's offset from its corner (from `game.menuOffset`); only sent when not 0,0. */
25
- menuOffset: MenuOffsetXY.optional(),
26
- /** The game's `game.controls`, as written; only sent when set. */
27
- controls: GameControls.optional(),
28
- });
29
-
30
- /** The injected `window.__HUB__` config. */
31
- export type HubInject = z.infer<typeof HubInject>;
1
+ /**
2
+ * `window.__HUB__`: the small config object the host writes into every page
3
+ * it injects (game pages, and the catalog page). The SDK and the menu read it
4
+ * to find the game's slug, the API base and the menu placement.
5
+ *
6
+ * The same inline script may also set `window.__hubDailyDeep` (a
7
+ * `?daily=YYYY-MM-DD` deep link captured before game code runs) and
8
+ * `window.__hubInstall` (the PWA install prompt); those are not part of this
9
+ * object.
10
+ */
11
+ import { z } from 'zod';
12
+ import { Slug } from '../common';
13
+ import { GameControls } from './controls';
14
+ import { MenuOffsetXY, MenuPosition } from './game';
15
+
16
+ /** The injected `window.__HUB__` config. */
17
+ export const HubInject = z.strictObject({
18
+ /** The game's slug. Missing on the catalog page. */
19
+ slug: Slug.optional(),
20
+ /** Where the REST API lives. Always `/_api` today. */
21
+ apiBase: z.literal('/_api'),
22
+ /** The menu button's corner (from `game.menuPosition`). Missing on the catalog page. */
23
+ menu: MenuPosition.optional(),
24
+ /** The menu button's offset from its corner (from `game.menuOffset`); only sent when not 0,0. */
25
+ menuOffset: MenuOffsetXY.optional(),
26
+ /** The game's `game.controls`, as written; only sent when set. */
27
+ controls: GameControls.optional(),
28
+ });
29
+
30
+ /** The injected `window.__HUB__` config. */
31
+ export type HubInject = z.infer<typeof HubInject>;
@@ -1,29 +1,29 @@
1
- /**
2
- * `game.leaderboards`: boards declared up front, so they show on the catalog
3
- * before the first score.
4
- */
5
- import { z } from 'zod';
6
- import { SortDir } from '../api/leaderboards';
7
- import { Key } from '../common';
8
- import { MANIFEST_LIMITS } from '../limits';
9
- import { Trimmed, hasUniqueKeys } from './fields';
10
-
11
- /** One declared leaderboard. */
12
- export const LeaderboardDecl = z.strictObject({
13
- /** The board key `hub.submitScore(key, score)` uses: letters, digits and `_ . : -`, 1 to 64. */
14
- key: Key,
15
- /** What players see, at most 80 characters. Defaults to the key. */
16
- title: Trimmed.max(MANIFEST_LIMITS.leaderboardTitleMax).optional(),
17
- /** Which scores rank first. Default `desc`. */
18
- sort: SortDir.default('desc'),
19
- /** List players whose best is 0. Default `false`. */
20
- showZeros: z.boolean().optional(),
21
- /**
22
- * `server`: only the game's own server may submit (trusted multiplayer
23
- * results); the client endpoint refuses. Default `client`.
24
- */
25
- source: z.enum(['client', 'server']).default('client'),
26
- });
27
-
28
- /** `game.leaderboards`: unique keys. */
29
- export const GameLeaderboardDecls = z.array(LeaderboardDecl).refine(hasUniqueKeys, 'leaderboard keys must be unique');
1
+ /**
2
+ * `game.leaderboards`: boards declared up front, so they show on the catalog
3
+ * before the first score.
4
+ */
5
+ import { z } from 'zod';
6
+ import { SortDir } from '../api/leaderboards';
7
+ import { Key } from '../common';
8
+ import { MANIFEST_LIMITS } from '../limits';
9
+ import { Trimmed, hasUniqueKeys } from './fields';
10
+
11
+ /** One declared leaderboard. */
12
+ export const LeaderboardDecl = z.strictObject({
13
+ /** The board key `hub.submitScore(key, score)` uses: letters, digits and `_ . : -`, 1 to 64. */
14
+ key: Key,
15
+ /** What players see, at most 80 characters. Defaults to the key. */
16
+ title: Trimmed.max(MANIFEST_LIMITS.leaderboardTitleMax).optional(),
17
+ /** Which scores rank first. Default `desc`. */
18
+ sort: SortDir.default('desc'),
19
+ /** List players whose best is 0. Default `false`. */
20
+ showZeros: z.boolean().optional(),
21
+ /**
22
+ * `server`: only the game's own server may submit (trusted multiplayer
23
+ * results); the client endpoint refuses. Default `client`.
24
+ */
25
+ source: z.enum(['client', 'server']).default('client'),
26
+ });
27
+
28
+ /** `game.leaderboards`: unique keys. */
29
+ export const GameLeaderboardDecls = z.array(LeaderboardDecl).refine(hasUniqueKeys, 'leaderboard keys must be unique');