@diffenderfer-games/hub 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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -21
- package/README.md +191 -139
- package/dist/doctor/cli.mjs +49 -19
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.js +1 -1
- package/dist/shared/globals.js +8 -3
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/package.json +89 -89
- package/src/auto.ts +39 -39
- package/src/core/connection.ts +299 -299
- package/src/core/errors.ts +147 -147
- package/src/core/game-data.ts +38 -38
- package/src/core/hub-economy.ts +179 -179
- package/src/core/hub-progress.ts +205 -205
- package/src/core/hub.ts +131 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/local-store.ts +261 -261
- package/src/core/offline/queue.ts +171 -171
- package/src/core/page.ts +74 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +152 -152
- package/src/core/storage/json-guards.ts +74 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +1 -1
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +161 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +67 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +78 -76
- package/src/doctor/cli.ts +98 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/globals.ts +115 -110
- package/src/index.ts +50 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +164 -164
- package/src/input/input-system.ts +393 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/nav-state.ts +191 -191
- package/src/input/navigator.ts +171 -171
- package/src/input/on-screen-controls.ts +101 -101
- package/src/input/page-control.ts +43 -43
- package/src/input/page-input.ts +193 -193
- package/src/input/player-remaps.ts +77 -77
- package/src/input/source-values.ts +116 -116
- package/src/input/spatial.ts +47 -47
- package/src/input/types.ts +331 -331
- package/src/input/zero-config.ts +177 -177
- package/src/input.ts +22 -22
- package/src/mp.ts +10 -10
- package/src/overlay/controls-panel.ts +163 -163
- package/src/overlay/dom.ts +46 -46
- package/src/overlay/layout-clusters.ts +162 -162
- package/src/overlay/layout-geometry.ts +136 -136
- package/src/overlay/layout.ts +203 -203
- package/src/overlay/listener-scope.ts +23 -23
- package/src/overlay/on-screen-keyboard.ts +137 -137
- package/src/overlay/overlay.ts +208 -208
- package/src/overlay/styles.ts +9 -9
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +121 -121
- package/src/realtime/cross-bundle.ts +29 -29
- package/src/realtime/fallback-notice.ts +48 -48
- package/src/realtime/hub-link.ts +65 -65
- package/src/realtime/latest-state.ts +33 -33
- package/src/realtime/multiplayer-forwarder.ts +28 -28
- package/src/realtime/multiplayer-types.ts +52 -52
- package/src/realtime/page-runtime.ts +60 -60
- package/src/realtime/race-forwarder.ts +59 -59
- package/src/realtime/race-types.ts +132 -132
- package/src/realtime/room-types.ts +125 -125
- package/src/realtime/rooms-forwarder.ts +79 -79
- package/src/realtime/runtime-api.ts +49 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +72 -72
- package/src/realtime/social-types.ts +78 -78
- package/src/realtime/types.ts +53 -53
- package/src/realtime/wire-types.ts +72 -72
- package/src/rooms.ts +10 -10
- package/src/social.ts +18 -18
- package/src/types.ts +13 -13
- package/src/uistack/page-stack.ts +45 -45
- package/src/uistack/play-suspension.ts +82 -82
- package/src/uistack/types.ts +76 -76
- package/src/uistack/ui-stack.ts +144 -144
package/src/uistack/ui-stack.ts
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The overlay stack implementation: two edges (`open` when depth goes
|
|
3
|
-
* 0 → 1, `close` when it returns to 0) mirrored as window events, the
|
|
4
|
-
* `autoPause` helper, and the `play` suspension.
|
|
5
|
-
*/
|
|
6
|
-
import { LEGACY_GLOBAL_NAMES } from '../globals';
|
|
7
|
-
import { isGroupSwitch, PlaySuspension } from './play-suspension';
|
|
8
|
-
import type { AutoPauseOptions, OverlayEdge, OverlayEvent, OverlayListener, OverlayReason, UiStack, UiStackHandoff } from './types';
|
|
9
|
-
|
|
10
|
-
/** Bump when the UiStack contract gains behaviour; idle older instances are replaced. */
|
|
11
|
-
export const UI_STACK_VERSION = 1;
|
|
12
|
-
|
|
13
|
-
/** One open entry (its identity is what `pop` removes). */
|
|
14
|
-
interface StackEntry {
|
|
15
|
-
readonly reason: OverlayReason;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The page-wide overlay stack (see {@link UiStack}). */
|
|
19
|
-
export class UiStackImpl implements UiStack {
|
|
20
|
-
readonly version = UI_STACK_VERSION;
|
|
21
|
-
private readonly entries: StackEntry[] = [];
|
|
22
|
-
private readonly listeners: Record<OverlayEdge, Set<OverlayListener>> = { open: new Set(), close: new Set() };
|
|
23
|
-
private isPausable = true;
|
|
24
|
-
private suspension: PlaySuspension | null = null;
|
|
25
|
-
|
|
26
|
-
constructor(handoff?: UiStackHandoff) {
|
|
27
|
-
if (handoff === undefined) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
handoff.listeners.open.forEach((listener) => this.listeners.open.add(listener));
|
|
31
|
-
handoff.listeners.close.forEach((listener) => this.listeners.close.add(listener));
|
|
32
|
-
this.isPausable = handoff.canPause;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get isOpen(): boolean {
|
|
36
|
-
return this.entries.length > 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get depth(): number {
|
|
40
|
-
return this.entries.length;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get canPause(): boolean {
|
|
44
|
-
return this.isPausable;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
push(reason: OverlayReason): () => void {
|
|
48
|
-
const entry: StackEntry = { reason };
|
|
49
|
-
this.entries.push(entry);
|
|
50
|
-
if (this.entries.length === 1) {
|
|
51
|
-
this.suspendPlay();
|
|
52
|
-
this.emit('open', { reason, canPause: this.isPausable });
|
|
53
|
-
}
|
|
54
|
-
let wasPopped = false;
|
|
55
|
-
return () => {
|
|
56
|
-
if (wasPopped) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
wasPopped = true;
|
|
60
|
-
const index = this.entries.indexOf(entry);
|
|
61
|
-
if (index < 0) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
this.entries.splice(index, 1);
|
|
65
|
-
if (this.entries.length === 0) {
|
|
66
|
-
// Listeners run before `play` is restored, so a game that pauses in its close handler is honoured.
|
|
67
|
-
this.emit('close', { reason, canPause: this.isPausable });
|
|
68
|
-
this.suspension?.release();
|
|
69
|
-
this.suspension = null;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
on(edge: OverlayEdge, listener: OverlayListener): () => void {
|
|
75
|
-
this.listeners[edge].add(listener);
|
|
76
|
-
return () => {
|
|
77
|
-
this.listeners[edge].delete(listener);
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
autoPause(options: AutoPauseOptions): () => void {
|
|
82
|
-
let wasPausedByUs = false;
|
|
83
|
-
const pauseOnOpen = (event: OverlayEvent): void => {
|
|
84
|
-
if (!event.canPause) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
wasPausedByUs = true;
|
|
88
|
-
options.pause();
|
|
89
|
-
};
|
|
90
|
-
const resumeOnClose = (): void => {
|
|
91
|
-
if (wasPausedByUs && options.resumeOnClose === true) {
|
|
92
|
-
options.resume?.();
|
|
93
|
-
}
|
|
94
|
-
wasPausedByUs = false;
|
|
95
|
-
};
|
|
96
|
-
const stopOpen = this.on('open', pauseOnOpen);
|
|
97
|
-
const stopClose = this.on('close', resumeOnClose);
|
|
98
|
-
// Registered while hub UI is already up: pause now, as if it just opened.
|
|
99
|
-
const firstEntry = this.entries[0];
|
|
100
|
-
if (firstEntry !== undefined) {
|
|
101
|
-
pauseOnOpen({ reason: firstEntry.reason, canPause: this.isPausable });
|
|
102
|
-
}
|
|
103
|
-
return () => {
|
|
104
|
-
stopOpen();
|
|
105
|
-
stopClose();
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
setCanPause(canPause: boolean): void {
|
|
110
|
-
this.isPausable = canPause;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
handoff(): UiStackHandoff {
|
|
114
|
-
const handoff: UiStackHandoff = {
|
|
115
|
-
listeners: { open: [...this.listeners.open], close: [...this.listeners.close] },
|
|
116
|
-
canPause: this.isPausable,
|
|
117
|
-
};
|
|
118
|
-
this.listeners.open.clear();
|
|
119
|
-
this.listeners.close.clear();
|
|
120
|
-
return handoff;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private suspendPlay(): void {
|
|
124
|
-
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the page input system may be another bundle's
|
|
125
|
-
const input: unknown = typeof window === 'undefined' ? undefined : window[LEGACY_GLOBAL_NAMES.input];
|
|
126
|
-
if (this.suspension === null && isGroupSwitch(input)) {
|
|
127
|
-
this.suspension = new PlaySuspension(input);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private emit(edge: OverlayEdge, event: OverlayEvent): void {
|
|
132
|
-
for (const listener of [...this.listeners[edge]]) {
|
|
133
|
-
try {
|
|
134
|
-
listener(event);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
// eslint-disable-next-line no-console -- a game's listener failing must not break the hub UI; surface it
|
|
137
|
-
console.error('[hub] overlay listener failed', error);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (typeof window !== 'undefined') {
|
|
141
|
-
window.dispatchEvent(new CustomEvent(`hub:overlay-${edge}`, { detail: event }));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The overlay stack implementation: two edges (`open` when depth goes
|
|
3
|
+
* 0 → 1, `close` when it returns to 0) mirrored as window events, the
|
|
4
|
+
* `autoPause` helper, and the `play` suspension.
|
|
5
|
+
*/
|
|
6
|
+
import { LEGACY_GLOBAL_NAMES } from '../globals';
|
|
7
|
+
import { isGroupSwitch, PlaySuspension } from './play-suspension';
|
|
8
|
+
import type { AutoPauseOptions, OverlayEdge, OverlayEvent, OverlayListener, OverlayReason, UiStack, UiStackHandoff } from './types';
|
|
9
|
+
|
|
10
|
+
/** Bump when the UiStack contract gains behaviour; idle older instances are replaced. */
|
|
11
|
+
export const UI_STACK_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
/** One open entry (its identity is what `pop` removes). */
|
|
14
|
+
interface StackEntry {
|
|
15
|
+
readonly reason: OverlayReason;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The page-wide overlay stack (see {@link UiStack}). */
|
|
19
|
+
export class UiStackImpl implements UiStack {
|
|
20
|
+
readonly version = UI_STACK_VERSION;
|
|
21
|
+
private readonly entries: StackEntry[] = [];
|
|
22
|
+
private readonly listeners: Record<OverlayEdge, Set<OverlayListener>> = { open: new Set(), close: new Set() };
|
|
23
|
+
private isPausable = true;
|
|
24
|
+
private suspension: PlaySuspension | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(handoff?: UiStackHandoff) {
|
|
27
|
+
if (handoff === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
handoff.listeners.open.forEach((listener) => this.listeners.open.add(listener));
|
|
31
|
+
handoff.listeners.close.forEach((listener) => this.listeners.close.add(listener));
|
|
32
|
+
this.isPausable = handoff.canPause;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get isOpen(): boolean {
|
|
36
|
+
return this.entries.length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get depth(): number {
|
|
40
|
+
return this.entries.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get canPause(): boolean {
|
|
44
|
+
return this.isPausable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
push(reason: OverlayReason): () => void {
|
|
48
|
+
const entry: StackEntry = { reason };
|
|
49
|
+
this.entries.push(entry);
|
|
50
|
+
if (this.entries.length === 1) {
|
|
51
|
+
this.suspendPlay();
|
|
52
|
+
this.emit('open', { reason, canPause: this.isPausable });
|
|
53
|
+
}
|
|
54
|
+
let wasPopped = false;
|
|
55
|
+
return () => {
|
|
56
|
+
if (wasPopped) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
wasPopped = true;
|
|
60
|
+
const index = this.entries.indexOf(entry);
|
|
61
|
+
if (index < 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.entries.splice(index, 1);
|
|
65
|
+
if (this.entries.length === 0) {
|
|
66
|
+
// Listeners run before `play` is restored, so a game that pauses in its close handler is honoured.
|
|
67
|
+
this.emit('close', { reason, canPause: this.isPausable });
|
|
68
|
+
this.suspension?.release();
|
|
69
|
+
this.suspension = null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
on(edge: OverlayEdge, listener: OverlayListener): () => void {
|
|
75
|
+
this.listeners[edge].add(listener);
|
|
76
|
+
return () => {
|
|
77
|
+
this.listeners[edge].delete(listener);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
autoPause(options: AutoPauseOptions): () => void {
|
|
82
|
+
let wasPausedByUs = false;
|
|
83
|
+
const pauseOnOpen = (event: OverlayEvent): void => {
|
|
84
|
+
if (!event.canPause) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
wasPausedByUs = true;
|
|
88
|
+
options.pause();
|
|
89
|
+
};
|
|
90
|
+
const resumeOnClose = (): void => {
|
|
91
|
+
if (wasPausedByUs && options.resumeOnClose === true) {
|
|
92
|
+
options.resume?.();
|
|
93
|
+
}
|
|
94
|
+
wasPausedByUs = false;
|
|
95
|
+
};
|
|
96
|
+
const stopOpen = this.on('open', pauseOnOpen);
|
|
97
|
+
const stopClose = this.on('close', resumeOnClose);
|
|
98
|
+
// Registered while hub UI is already up: pause now, as if it just opened.
|
|
99
|
+
const firstEntry = this.entries[0];
|
|
100
|
+
if (firstEntry !== undefined) {
|
|
101
|
+
pauseOnOpen({ reason: firstEntry.reason, canPause: this.isPausable });
|
|
102
|
+
}
|
|
103
|
+
return () => {
|
|
104
|
+
stopOpen();
|
|
105
|
+
stopClose();
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setCanPause(canPause: boolean): void {
|
|
110
|
+
this.isPausable = canPause;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handoff(): UiStackHandoff {
|
|
114
|
+
const handoff: UiStackHandoff = {
|
|
115
|
+
listeners: { open: [...this.listeners.open], close: [...this.listeners.close] },
|
|
116
|
+
canPause: this.isPausable,
|
|
117
|
+
};
|
|
118
|
+
this.listeners.open.clear();
|
|
119
|
+
this.listeners.close.clear();
|
|
120
|
+
return handoff;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private suspendPlay(): void {
|
|
124
|
+
// eslint-disable-next-line no-restricted-syntax -- trust boundary: the page input system may be another bundle's
|
|
125
|
+
const input: unknown = typeof window === 'undefined' ? undefined : window[LEGACY_GLOBAL_NAMES.input];
|
|
126
|
+
if (this.suspension === null && isGroupSwitch(input)) {
|
|
127
|
+
this.suspension = new PlaySuspension(input);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private emit(edge: OverlayEdge, event: OverlayEvent): void {
|
|
132
|
+
for (const listener of [...this.listeners[edge]]) {
|
|
133
|
+
try {
|
|
134
|
+
listener(event);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// eslint-disable-next-line no-console -- a game's listener failing must not break the hub UI; surface it
|
|
137
|
+
console.error('[hub] overlay listener failed', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (typeof window !== 'undefined') {
|
|
141
|
+
window.dispatchEvent(new CustomEvent(`hub:overlay-${edge}`, { detail: event }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|