@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10
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 +163 -0
- package/LICENSE +21 -21
- package/README.md +203 -139
- package/dist/auto.d.ts +8 -8
- package/dist/auto.js +3 -3
- package/dist/avatar-pixi.d.ts +165 -0
- package/dist/avatar-pixi.d.ts.map +1 -0
- package/dist/avatar-pixi.js +299 -0
- package/dist/avatar-pixi.js.map +1 -0
- package/dist/avatar-svg.d.ts +4 -0
- package/dist/avatar-svg.js +5 -0
- package/dist/avatar-three.d.ts +69 -0
- package/dist/avatar-three.d.ts.map +1 -0
- package/dist/avatar-three.js +49 -0
- package/dist/avatar-three.js.map +1 -0
- package/dist/avatar.d.ts +7 -0
- package/dist/avatar.js +500 -0
- package/dist/avatar.js.map +1 -0
- package/dist/daily.d.ts +2 -2
- package/dist/doctor/cli.mjs +119 -28
- package/dist/doctor/cli.mjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/input.d.ts +2 -2
- package/dist/input.js +2 -2
- package/dist/mp.d.ts +3 -3
- package/dist/race.d.ts +1 -1
- package/dist/rooms.d.ts +3 -3
- package/dist/rooms.js +1 -1
- package/dist/shared/animator.d.ts +104 -0
- package/dist/shared/animator.d.ts.map +1 -0
- package/dist/shared/art.js +1946 -0
- package/dist/shared/art.js.map +1 -0
- package/dist/shared/atlas.d.ts +138 -0
- package/dist/shared/atlas.d.ts.map +1 -0
- package/dist/shared/avatar.d.ts +123 -0
- package/dist/shared/avatar.d.ts.map +1 -0
- package/dist/shared/bridge.js +18 -4
- package/dist/shared/bridge.js.map +1 -1
- package/dist/shared/client-version.gen.js +1 -1
- package/dist/shared/daily.d.ts +145 -19
- package/dist/shared/daily.d.ts.map +1 -1
- package/dist/shared/daily.js +138 -25
- package/dist/shared/daily.js.map +1 -1
- package/dist/shared/defineProperty.js +39 -0
- package/dist/shared/describe.d.ts +32 -0
- package/dist/shared/describe.d.ts.map +1 -0
- package/dist/shared/describe.js +109 -0
- package/dist/shared/describe.js.map +1 -0
- package/dist/shared/globals.js +9 -42
- package/dist/shared/globals.js.map +1 -1
- package/dist/shared/index.d.ts +1122 -46
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/input.d.ts +74 -14
- package/dist/shared/input.d.ts.map +1 -1
- package/dist/shared/input.js +179 -25
- package/dist/shared/input.js.map +1 -1
- package/dist/shared/json-guards.js +48 -0
- package/dist/shared/json-guards.js.map +1 -0
- package/dist/shared/latest-state.js +1 -1
- package/dist/shared/live-canvas.js +62 -0
- package/dist/shared/live-canvas.js.map +1 -0
- package/dist/shared/mp.js +10 -2
- package/dist/shared/mp.js.map +1 -1
- package/dist/shared/multiplayer-types.d.ts +15 -2
- package/dist/shared/multiplayer-types.d.ts.map +1 -1
- package/dist/shared/overlays.js +800 -0
- package/dist/shared/overlays.js.map +1 -0
- package/dist/shared/page-stack.js +2 -1
- package/dist/shared/page-stack.js.map +1 -1
- package/dist/shared/pose-key.js +249 -0
- package/dist/shared/pose-key.js.map +1 -0
- package/dist/shared/race-types.d.ts +1 -1
- package/dist/shared/race.js +1 -1
- package/dist/shared/room-types.d.ts +28 -3
- package/dist/shared/room-types.d.ts.map +1 -1
- package/dist/shared/rooms.d.ts.map +1 -1
- package/dist/shared/rooms.js +23 -4
- package/dist/shared/rooms.js.map +1 -1
- package/dist/shared/runtime-opener.js +1 -1
- package/dist/shared/scene.d.ts +90 -0
- package/dist/shared/scene.d.ts.map +1 -0
- package/dist/shared/social.d.ts +90 -9
- package/dist/shared/social.d.ts.map +1 -1
- package/dist/shared/social.js +6 -1
- package/dist/shared/social.js.map +1 -1
- package/dist/shared/src.js +4563 -667
- package/dist/shared/src.js.map +1 -1
- package/dist/shared/svg-markup.js +2354 -0
- package/dist/shared/svg-markup.js.map +1 -0
- package/dist/shared/svg.js +27 -0
- package/dist/shared/svg.js.map +1 -0
- package/dist/shared/types.d.ts +174 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/wire-types.d.ts +6 -2
- package/dist/shared/wire-types.d.ts.map +1 -1
- package/dist/social.d.ts +5 -5
- package/dist/social.js +2 -2
- package/dist/turns-rules.d.ts +2 -0
- package/dist/turns-rules.js +1 -0
- package/dist/turns.d.ts +61 -0
- package/dist/turns.d.ts.map +1 -0
- package/dist/turns.js +183 -0
- package/dist/turns.js.map +1 -0
- package/package.json +125 -89
- package/src/auto.ts +39 -39
- package/src/avatar/adapters/pixi-avatar.ts +73 -0
- package/src/avatar/adapters/pixi-crowd.ts +329 -0
- package/src/avatar/adapters/pixi-types.ts +84 -0
- package/src/avatar/adapters/three.ts +126 -0
- package/src/avatar/animator/animator.ts +285 -0
- package/src/avatar/animator/reactions.ts +44 -0
- package/src/avatar/animator/schedule.ts +73 -0
- package/src/avatar/art/beard.ts +49 -0
- package/src/avatar/art/glasses.ts +67 -0
- package/src/avatar/art/hair.ts +254 -0
- package/src/avatar/art/headwear.ts +75 -0
- package/src/avatar/art/index.ts +21 -0
- package/src/avatar/art/shape.ts +87 -0
- package/src/avatar/art-store.ts +36 -0
- package/src/avatar/atlas.ts +246 -0
- package/src/avatar/catalog-view.ts +14 -0
- package/src/avatar/colour.ts +109 -0
- package/src/avatar/describe.ts +79 -0
- package/src/avatar/face.ts +187 -0
- package/src/avatar/geom.ts +173 -0
- package/src/avatar/layout.ts +47 -0
- package/src/avatar/look-like.ts +52 -0
- package/src/avatar/outputs/bitmap.ts +75 -0
- package/src/avatar/outputs/canvas-paint.ts +177 -0
- package/src/avatar/outputs/canvas.ts +62 -0
- package/src/avatar/outputs/custom-element.ts +140 -0
- package/src/avatar/outputs/element.ts +58 -0
- package/src/avatar/outputs/live-canvas.ts +57 -0
- package/src/avatar/outputs/overlay-svg.ts +22 -0
- package/src/avatar/outputs/pose-key.ts +30 -0
- package/src/avatar/outputs/svg-markup.ts +113 -0
- package/src/avatar/outputs/svg.ts +41 -0
- package/src/avatar/overlays.ts +128 -0
- package/src/avatar/render/body.ts +116 -0
- package/src/avatar/render/colours.ts +81 -0
- package/src/avatar/render/compose.ts +320 -0
- package/src/avatar/render/context.ts +117 -0
- package/src/avatar/render/eyes.ts +78 -0
- package/src/avatar/render/eyewear.ts +118 -0
- package/src/avatar/render/frame.ts +73 -0
- package/src/avatar/render/head.ts +81 -0
- package/src/avatar/render/mouth.ts +86 -0
- package/src/avatar/render/props.ts +91 -0
- package/src/avatar/render/types.ts +93 -0
- package/src/avatar/render/walker-parts.ts +121 -0
- package/src/avatar/render/walker.ts +199 -0
- package/src/avatar/scene.ts +119 -0
- package/src/avatar-pixi.ts +20 -0
- package/src/avatar-svg.ts +21 -0
- package/src/avatar-three.ts +18 -0
- package/src/avatar.ts +63 -0
- package/src/core/avatar/avatar-cache.ts +95 -0
- package/src/core/avatar/avatar-ref.ts +34 -0
- package/src/core/avatar/avatar-types.ts +43 -0
- package/src/core/avatar/hub-avatar.ts +308 -0
- package/src/core/connection.ts +408 -299
- package/src/core/errors.ts +160 -147
- package/src/core/game-data.ts +40 -38
- package/src/core/hub-economy.ts +216 -179
- package/src/core/hub-progress.ts +269 -205
- package/src/core/hub.ts +352 -131
- package/src/core/menu-control.ts +61 -61
- package/src/core/offline/cloud-saves.ts +62 -0
- package/src/core/offline/events.ts +50 -50
- package/src/core/offline/favorites.ts +21 -21
- package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
- package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
- package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
- package/src/core/offline/hub-kinds/game-fields.ts +2 -0
- package/src/core/offline/hub-kinds/index.ts +52 -0
- package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
- package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
- package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
- package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
- package/src/core/offline/kind-adapters.ts +48 -0
- package/src/core/offline/local-inventory.ts +64 -0
- package/src/core/offline/local-store.ts +304 -261
- package/src/core/offline/offline-api.ts +65 -0
- package/src/core/offline/outbox-entry.ts +59 -0
- package/src/core/offline/outbox-kinds.ts +78 -0
- package/src/core/offline/outbox-merge.ts +39 -0
- package/src/core/offline/outbox-replayer.ts +141 -0
- package/src/core/offline/outbox-store.ts +85 -0
- package/src/core/offline/outbox.ts +232 -0
- package/src/core/offline/queue.ts +126 -171
- package/src/core/offline/replay-lease.ts +87 -0
- package/src/core/offline/replay-policy.ts +52 -0
- package/src/core/page-account.ts +121 -0
- package/src/core/page.ts +87 -74
- package/src/core/rest/endpoint-types.ts +22 -22
- package/src/core/rest/rest-client.ts +163 -152
- package/src/core/rest/routes.gen.ts +144 -88
- package/src/core/resume/hub-resume.ts +286 -0
- package/src/core/resume/local-resume.ts +130 -0
- package/src/core/resume/resume-list.ts +65 -0
- package/src/core/resume/resume-request.ts +52 -0
- package/src/core/resume/resume-types.ts +64 -0
- package/src/core/saved/daily-saves.ts +196 -0
- package/src/core/saved/daily-seed.ts +16 -0
- package/src/core/saved/hub-saved.ts +221 -0
- package/src/core/saved/local-saved.ts +122 -0
- package/src/core/saved/saved-list.ts +129 -0
- package/src/core/saved/saved-open.ts +104 -0
- package/src/core/saved/saved-order.ts +49 -0
- package/src/core/saved/saved-types.ts +24 -0
- package/src/core/storage/json-guards.ts +80 -74
- package/src/core/storage/key-value.ts +66 -66
- package/src/core/ticket.ts +43 -43
- package/src/core/turns/hub-turns.ts +161 -0
- package/src/core/turns/turn-game.ts +510 -0
- package/src/core/turns/your-turn-buzz.ts +69 -0
- package/src/core/types.ts +134 -134
- package/src/core/version/client-version.gen.ts +2 -2
- package/src/core/version/hub-version.ts +62 -62
- package/src/core/version/semver.ts +97 -97
- package/src/daily/daily-system.ts +276 -161
- package/src/daily/page-daily.ts +36 -36
- package/src/daily/types.ts +105 -67
- package/src/daily.ts +10 -10
- package/src/doctor/checks.ts +86 -76
- package/src/doctor/cli.ts +101 -106
- package/src/doctor/hub-version-fetch.ts +43 -0
- package/src/doctor/tsconfig.json +12 -12
- package/src/doctor/world-check.ts +61 -0
- package/src/globals.ts +115 -110
- package/src/index.ts +68 -50
- package/src/input/device-state.ts +146 -146
- package/src/input/edges.ts +55 -55
- package/src/input/focus-ring.ts +13 -0
- package/src/input/gamepad.ts +61 -61
- package/src/input/input-events.ts +32 -32
- package/src/input/input-registry.ts +171 -164
- package/src/input/input-system.ts +406 -393
- package/src/input/legacy-controls.ts +64 -64
- package/src/input/modality.ts +122 -0
- package/src/input/nav-state.ts +200 -191
- package/src/input/navigator.ts +182 -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 +164 -177
- package/src/input.ts +26 -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 +234 -208
- package/src/overlay/styles.ts +13 -12
- package/src/overlay/virtual-controls.ts +262 -262
- package/src/race.ts +10 -10
- package/src/realtime/bridge.ts +126 -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 +40 -28
- package/src/realtime/multiplayer-types.ts +66 -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 +151 -125
- package/src/realtime/rooms-forwarder.ts +104 -79
- package/src/realtime/runtime-api.ts +74 -49
- package/src/realtime/runtime-opener.ts +61 -61
- package/src/realtime/social-forwarder.ts +77 -72
- package/src/realtime/social-types.ts +86 -78
- package/src/realtime/types.ts +54 -53
- package/src/realtime/wire-types.ts +79 -72
- package/src/realtime/world-types.ts +20 -0
- package/src/rooms.ts +10 -10
- package/src/social.ts +19 -18
- package/src/turns-kit/check-rules.ts +281 -0
- package/src/turns-rules.ts +12 -0
- package/src/turns.ts +14 -0
- 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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Building {@link AvatarRef}s from codes, payloads and seeds. */
|
|
2
|
+
import { decodeLook, defaultLook, encodeLook } from '@diffenderfer-games/hub-contract/avatar';
|
|
3
|
+
import type { AvatarRef } from './avatar-types';
|
|
4
|
+
|
|
5
|
+
/** A ref for a code (`''` = hidden). */
|
|
6
|
+
export function refOf(code: string, isDefault = false): AvatarRef {
|
|
7
|
+
if (code === '') {
|
|
8
|
+
return { code: '', look: null, isDefault: false, hidden: true };
|
|
9
|
+
}
|
|
10
|
+
return { code, look: decodeLook(code)?.look ?? null, isDefault, hidden: false };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A seeded default look as a ref (bots, previews, local pass-and-play seats). */
|
|
14
|
+
export function botRef(seed: string | number): AvatarRef {
|
|
15
|
+
const look = defaultLook(seed);
|
|
16
|
+
return { code: encodeLook(look), look, isDefault: true, hidden: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Anything that carries a player's look: a seat, a `PlayerLite`, a racer, a ticket user. */
|
|
20
|
+
export interface CarriesLook {
|
|
21
|
+
readonly look?: string;
|
|
22
|
+
readonly userId?: number;
|
|
23
|
+
readonly id?: number;
|
|
24
|
+
readonly uid?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The ref a payload carries; an old hub's payload (no `look`) falls back to the id's default. */
|
|
28
|
+
export function refFrom(carrier: CarriesLook): AvatarRef {
|
|
29
|
+
if (carrier.look !== undefined) {
|
|
30
|
+
return refOf(carrier.look);
|
|
31
|
+
}
|
|
32
|
+
const id = carrier.userId ?? carrier.id ?? carrier.uid;
|
|
33
|
+
return id === undefined ? refOf('') : botRef(id);
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The shapes `hub.avatar` works with (avatars.md §6). */
|
|
2
|
+
import type { AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A player's avatar as games hold it: the code (pass it to any renderer),
|
|
6
|
+
* the decoded look, whether it is their generated default, and whether the
|
|
7
|
+
* player is hidden from you (draw the silhouette).
|
|
8
|
+
*/
|
|
9
|
+
export interface AvatarRef {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
/** `null` when hidden or the code is malformed (renderers draw the silhouette). */
|
|
12
|
+
readonly look: AvatarLook | null;
|
|
13
|
+
/** They never designed one: this is their generated default. */
|
|
14
|
+
readonly isDefault: boolean;
|
|
15
|
+
/** Hidden where the username is hidden (§2.8). */
|
|
16
|
+
readonly hidden: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A player's avatar changed (`hub.avatar.on('change')`). */
|
|
20
|
+
export interface AvatarChange {
|
|
21
|
+
readonly userId: number;
|
|
22
|
+
readonly ref: AvatarRef;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options for the designer. */
|
|
26
|
+
export interface OpenDesignerOptions {
|
|
27
|
+
/** Why it opened (`'game'`, `'world'`, …), for metrics only. */
|
|
28
|
+
readonly reason?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the designer reports when it closes. */
|
|
32
|
+
export interface DesignerResult {
|
|
33
|
+
/** The player saved a new look. */
|
|
34
|
+
readonly saved: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What the social runtime adds for avatars (feature version 5). */
|
|
38
|
+
export interface RuntimeAvatarApi {
|
|
39
|
+
/** Open "Make your avatar" over the page; resolves when it closes. */
|
|
40
|
+
openDesigner(options?: OpenDesignerOptions): Promise<DesignerResult>;
|
|
41
|
+
/** Live `ev:avatar` changes (yours, friends', your room's). */
|
|
42
|
+
on(event: 'change', listener: (change: { readonly userId: number; readonly code: string }) => void): () => void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hub.avatar` (avatars.md §6): players' looks without the renderer. Codes
|
|
3
|
+
* arrive in payloads (seats, `PlayerLite`, racers), so `from()` needs no
|
|
4
|
+
* request; `of()` and `many()` batch and cache the rest; your own look is
|
|
5
|
+
* cached on the device so it works offline. `load()` brings in the renderer
|
|
6
|
+
* (`@diffenderfer-games/hub/avatar`) and its art when a game wants to draw.
|
|
7
|
+
*
|
|
8
|
+
* It follows the page's account (the menu's account events, the runtime's
|
|
9
|
+
* socket hello, this client's sign-ins): when your account appears or
|
|
10
|
+
* changes, or the hello says your look is not the one cached, your look is
|
|
11
|
+
* fetched again and `change` fires. Each hello also re-checks the players
|
|
12
|
+
* this page asked about, so a change sent while the socket was down is not
|
|
13
|
+
* missed.
|
|
14
|
+
*/
|
|
15
|
+
import type { MyAvatar } from '@diffenderfer-games/hub-contract';
|
|
16
|
+
import { encodeLook, type AvatarLook } from '@diffenderfer-games/hub-contract/avatar';
|
|
17
|
+
import type * as AvatarEntry from '../../avatar';
|
|
18
|
+
import { pageBridge } from '../../realtime/bridge';
|
|
19
|
+
import { createRuntimeOpener } from '../../realtime/runtime-opener';
|
|
20
|
+
import type { AccountListener, PageUser } from '../page-account';
|
|
21
|
+
import { AvatarCache } from './avatar-cache';
|
|
22
|
+
import { botRef, refFrom, refOf, type CarriesLook } from './avatar-ref';
|
|
23
|
+
import type { AvatarChange, AvatarRef, DesignerResult, OpenDesignerOptions } from './avatar-types';
|
|
24
|
+
import { createPageStore } from '../storage/key-value';
|
|
25
|
+
|
|
26
|
+
/** What `hub.avatar` needs from its client. */
|
|
27
|
+
export interface AvatarHost {
|
|
28
|
+
/** The signed-in user's id, or `null` (`hub.userId()`). */
|
|
29
|
+
readonly currentUserId: () => number | null;
|
|
30
|
+
/** Hear every report of who is signed in (the page's account). */
|
|
31
|
+
readonly onAccount: (listener: AccountListener) => () => void;
|
|
32
|
+
/** `GET /avatar/me`; rejects when offline. */
|
|
33
|
+
readonly fetchMine: () => Promise<MyAvatar>;
|
|
34
|
+
/** `POST /avatar/batch`. */
|
|
35
|
+
readonly fetchCodes: (ids: readonly number[]) => Promise<Record<string, string>>;
|
|
36
|
+
/** Queue a save of your look (sent now when online, replayed later when not). */
|
|
37
|
+
readonly queueSave: (code: string, savedAt: number) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The renderer module `load()` resolves with. */
|
|
41
|
+
export type AvatarModule = typeof AvatarEntry;
|
|
42
|
+
|
|
43
|
+
type ChangeListener = (change: AvatarChange) => void;
|
|
44
|
+
|
|
45
|
+
const BATCH_MAX = 100;
|
|
46
|
+
const openWithRuntime = createRuntimeOpener(pageBridge);
|
|
47
|
+
|
|
48
|
+
/** The look `mine()` gives before the account exists (a new guest's is being made). */
|
|
49
|
+
const NO_ACCOUNT_YET = botRef('you');
|
|
50
|
+
|
|
51
|
+
/** `hub.avatar`. */
|
|
52
|
+
export class HubAvatar {
|
|
53
|
+
private readonly host: AvatarHost;
|
|
54
|
+
private readonly cache = new AvatarCache(createPageStore());
|
|
55
|
+
private readonly listeners = new Set<ChangeListener>();
|
|
56
|
+
private pending = new Map<number, ((code: string) => void)[]>();
|
|
57
|
+
private flushQueued = false;
|
|
58
|
+
private detachRuntime: (() => void) | null = null;
|
|
59
|
+
/** The account the last report named. */
|
|
60
|
+
private lastUserId: number | null;
|
|
61
|
+
/** The code of your look the page was last given (`mine`, `mineSync`, `change`). */
|
|
62
|
+
private mineShown: string | null = null;
|
|
63
|
+
/** Players this page asked about or was handed (re-checked on each socket hello). */
|
|
64
|
+
private readonly seen = new Set<number>();
|
|
65
|
+
|
|
66
|
+
constructor(host: AvatarHost) {
|
|
67
|
+
this.host = host;
|
|
68
|
+
this.lastUserId = host.currentUserId();
|
|
69
|
+
host.onAccount((user, source) => {
|
|
70
|
+
this.onAccount(user, source === 'session');
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Your look (your default if you never designed one). Works offline from
|
|
76
|
+
* the device cache. Before your account exists (a new guest's is made just
|
|
77
|
+
* after the game starts) it resolves at once with a default look, and
|
|
78
|
+
* `change` brings yours when the account is there.
|
|
79
|
+
*/
|
|
80
|
+
async mine(): Promise<AvatarRef> {
|
|
81
|
+
const userId = this.host.currentUserId();
|
|
82
|
+
if (userId === null) {
|
|
83
|
+
return this.showMine(NO_ACCOUNT_YET);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const mine = await this.host.fetchMine();
|
|
87
|
+
return this.noteMine(userId, mine);
|
|
88
|
+
} catch (failure) {
|
|
89
|
+
const cached = this.mineSync();
|
|
90
|
+
if (cached !== null) {
|
|
91
|
+
return cached;
|
|
92
|
+
}
|
|
93
|
+
throw failure;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Your cached look, or your default; `null` only while nobody is signed in. */
|
|
98
|
+
mineSync(): AvatarRef | null {
|
|
99
|
+
const userId = this.host.currentUserId();
|
|
100
|
+
if (userId === null) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const cached = this.cache.readMine(userId);
|
|
104
|
+
return this.showMine(cached === null ? botRef(userId) : refOf(cached.code, cached.isDefault));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Any player's look: from the cache, else batched with other requests in the same tick. */
|
|
108
|
+
of(userId: number): Promise<AvatarRef> {
|
|
109
|
+
this.seen.add(userId);
|
|
110
|
+
const cached = this.cache.codeOf(userId);
|
|
111
|
+
if (cached !== undefined) {
|
|
112
|
+
return Promise.resolve(refOf(cached));
|
|
113
|
+
}
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const waiting = this.pending.get(userId) ?? [];
|
|
116
|
+
waiting.push((code) => {
|
|
117
|
+
resolve(code === '' ? botRef(userId) : refOf(code));
|
|
118
|
+
});
|
|
119
|
+
this.pending.set(userId, waiting);
|
|
120
|
+
this.scheduleFlush();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Many players' looks at once. */
|
|
125
|
+
async many(userIds: readonly number[]): Promise<Map<number, AvatarRef>> {
|
|
126
|
+
const refs = await Promise.all(userIds.map(async (id) => [id, await this.of(id)] as const));
|
|
127
|
+
return new Map(refs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The look a payload already carries (a `RoomSeat`, `PlayerLite`, racer or ticket user). No request. */
|
|
131
|
+
from(carrier: CarriesLook): AvatarRef {
|
|
132
|
+
const ref = refFrom(carrier);
|
|
133
|
+
const id = carrier.userId ?? carrier.id ?? carrier.uid;
|
|
134
|
+
if (id !== undefined && carrier.look !== undefined) {
|
|
135
|
+
this.seen.add(id);
|
|
136
|
+
this.cache.remember(id, carrier.look);
|
|
137
|
+
}
|
|
138
|
+
return ref;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A seeded default for bots, previews and local pass-and-play seats: `bot('local:2')`. */
|
|
142
|
+
bot(seed: string | number): AvatarRef {
|
|
143
|
+
return botRef(seed);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Save your look (designer code paths; queued offline, latest wins). */
|
|
147
|
+
async save(look: AvatarLook): Promise<AvatarRef> {
|
|
148
|
+
const code = encodeLook(look);
|
|
149
|
+
const savedAt = Date.now();
|
|
150
|
+
const userId = this.host.currentUserId();
|
|
151
|
+
if (userId !== null) {
|
|
152
|
+
this.noteMine(userId, { code, isDefault: false, updatedAt: savedAt });
|
|
153
|
+
}
|
|
154
|
+
await this.host.queueSave(code, savedAt);
|
|
155
|
+
return refOf(code);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Hear about look changes (yours, friends', your room's, and yours when
|
|
160
|
+
* your account appears). Returns the unsubscribe function. Wait for this
|
|
161
|
+
* rather than polling `mine()`.
|
|
162
|
+
*/
|
|
163
|
+
on(_event: 'change', listener: ChangeListener): () => void {
|
|
164
|
+
this.listeners.add(listener);
|
|
165
|
+
this.attachRuntime();
|
|
166
|
+
return () => {
|
|
167
|
+
this.listeners.delete(listener);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Open "Make your avatar" over the game; resolves when it closes. */
|
|
172
|
+
openDesigner(options: OpenDesignerOptions = {}): Promise<DesignerResult> {
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
openWithRuntime((realtime) => {
|
|
175
|
+
if (realtime.avatar === undefined) {
|
|
176
|
+
resolve({ saved: false });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
void realtime.avatar.openDesigner(options).then(resolve, () => {
|
|
180
|
+
resolve({ saved: false });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Loads the renderer and its art; resolves when rendering is synchronous. */
|
|
187
|
+
async load(): Promise<AvatarModule> {
|
|
188
|
+
const avatar = await import('../../avatar');
|
|
189
|
+
await avatar.ready();
|
|
190
|
+
return avatar;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Remember the ref the page was given for your look. */
|
|
194
|
+
private showMine(ref: AvatarRef): AvatarRef {
|
|
195
|
+
this.mineShown = ref.code;
|
|
196
|
+
return ref;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Cache your look; `change` fires when it differs from the one the page last had. */
|
|
200
|
+
private noteMine(userId: number, mine: Pick<MyAvatar, 'code' | 'isDefault' | 'updatedAt'>): AvatarRef {
|
|
201
|
+
const before = this.mineShown ?? this.cache.readMine(userId)?.code ?? null;
|
|
202
|
+
this.cache.writeMine({ userId, code: mine.code, isDefault: mine.isDefault, updatedAt: mine.updatedAt });
|
|
203
|
+
const ref = this.showMine(refOf(mine.code, mine.isDefault));
|
|
204
|
+
if (mine.code !== before) {
|
|
205
|
+
this.emit({ userId, ref });
|
|
206
|
+
}
|
|
207
|
+
return ref;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Someone said who is signed in: fetch your look when the account is new or its look isn't the cached one. */
|
|
211
|
+
private onAccount(user: PageUser | null, isHello: boolean): void {
|
|
212
|
+
const userId = user === null || user.id === 0 ? null : user.id;
|
|
213
|
+
const isNewAccount = userId !== this.lastUserId;
|
|
214
|
+
this.lastUserId = userId;
|
|
215
|
+
if (user === null || userId === null) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (isNewAccount || (user.look !== null && user.look !== this.cache.readMine(userId)?.code)) {
|
|
219
|
+
void this.refreshMine(userId);
|
|
220
|
+
}
|
|
221
|
+
if (isHello) {
|
|
222
|
+
void this.recheckSeen();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async refreshMine(userId: number): Promise<void> {
|
|
227
|
+
const mine = await this.host.fetchMine().catch(() => null);
|
|
228
|
+
if (mine !== null && this.host.currentUserId() === userId) {
|
|
229
|
+
this.noteMine(userId, mine);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The socket (re)connected: a change sent while it was down never arrived, so ask again. */
|
|
234
|
+
private async recheckSeen(): Promise<void> {
|
|
235
|
+
if (this.listeners.size === 0 || this.seen.size === 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const codes = await this.fetchCodes([...this.seen]);
|
|
239
|
+
for (const [userId, code] of codes) {
|
|
240
|
+
if (code !== '' && code !== this.cache.codeOf(userId)) {
|
|
241
|
+
this.cache.remember(userId, code);
|
|
242
|
+
this.emit({ userId, ref: refOf(code) });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private emit(change: AvatarChange): void {
|
|
248
|
+
for (const listener of this.listeners) {
|
|
249
|
+
listener(change);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private attachRuntime(): void {
|
|
254
|
+
if (this.detachRuntime !== null) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.detachRuntime = pageBridge.subscribe((realtime) => realtime.avatar?.on('change', ({ userId, code }) => {
|
|
258
|
+
if (userId === this.host.currentUserId()) {
|
|
259
|
+
this.noteMine(userId, { code, isDefault: false, updatedAt: Date.now() });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.cache.remember(userId, code);
|
|
263
|
+
this.emit({ userId, ref: refOf(code) });
|
|
264
|
+
}) ?? (() => undefined));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private scheduleFlush(): void {
|
|
268
|
+
if (this.flushQueued) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.flushQueued = true;
|
|
272
|
+
queueMicrotask(() => {
|
|
273
|
+
this.flushQueued = false;
|
|
274
|
+
void this.flush();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private async flush(): Promise<void> {
|
|
279
|
+
const batch = this.pending;
|
|
280
|
+
this.pending = new Map();
|
|
281
|
+
const codes = await this.fetchCodes([...batch.keys()]);
|
|
282
|
+
for (const [id, waiting] of batch) {
|
|
283
|
+
const code = codes.get(id) ?? '';
|
|
284
|
+
if (code !== '') {
|
|
285
|
+
this.cache.remember(id, code);
|
|
286
|
+
}
|
|
287
|
+
for (const resolve of waiting) {
|
|
288
|
+
resolve(code);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Players' codes, in batches; offline or failing, the missing ones are left out (they get their default). */
|
|
294
|
+
private async fetchCodes(ids: readonly number[]): Promise<Map<number, string>> {
|
|
295
|
+
const codes = new Map<number, string>();
|
|
296
|
+
for (let i = 0; i < ids.length; i += BATCH_MAX) {
|
|
297
|
+
const slice = ids.slice(i, i + BATCH_MAX);
|
|
298
|
+
const answered = await this.host.fetchCodes(slice).catch((): Record<string, string> => ({}));
|
|
299
|
+
for (const id of slice) {
|
|
300
|
+
const code = answered[String(id)];
|
|
301
|
+
if (code !== undefined) {
|
|
302
|
+
codes.set(id, code);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return codes;
|
|
307
|
+
}
|
|
308
|
+
}
|