castle-web-sdk 0.4.47 → 0.4.48
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/README.md +197 -0
- package/dist/castle.d.ts +7 -3
- package/dist/castle.js +2 -0
- package/dist/commands.d.ts +128 -1
- package/dist/commands.js +3 -0
- package/dist/display.d.ts +3 -0
- package/dist/display.js +9 -3
- package/dist/hub.d.ts +14 -2
- package/dist/hub.js +34 -0
- package/dist/multiplayer.d.ts +7 -1
- package/dist/multiplayer.js +63 -2
- package/dist/multiplayerProtocol.d.ts +2 -0
- package/dist/party.d.ts +23 -0
- package/dist/party.js +91 -0
- package/dist/save.d.ts +26 -1
- package/dist/save.js +52 -6
- package/dist/server/hub.d.ts +24 -1
- package/dist/server/hub.js +35 -3
- package/dist/server/platformHandle.d.ts +9 -1
- package/dist/server/storage.d.ts +2 -0
- package/dist/server/storage.js +11 -3
- package/dist/server/wrapper.d.ts +7 -1
- package/dist/server/wrapper.js +10 -1
- package/dist/social.d.ts +9 -0
- package/dist/social.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,12 +26,15 @@ import { setup, initCard, Store, Leaderboard } from "castle-web-sdk";
|
|
|
26
26
|
- [Pass](#pass)
|
|
27
27
|
- [Snapshot](#snapshot)
|
|
28
28
|
- [Portal](#portal)
|
|
29
|
+
- [Social](#social)
|
|
30
|
+
- [Party](#party)
|
|
29
31
|
- [Hub](#hub)
|
|
30
32
|
- [Decks](#decks)
|
|
31
33
|
- [Clipboard](#clipboard)
|
|
32
34
|
- [Haptics](#haptics)
|
|
33
35
|
- [Agent](#agent)
|
|
34
36
|
- [Lifecycle](#lifecycle)
|
|
37
|
+
- [Display](#display)
|
|
35
38
|
- [Multiplayer](#multiplayer)
|
|
36
39
|
- [Setup](#setup)
|
|
37
40
|
- [Saving files](#writefilepath-contents-promisevoid)
|
|
@@ -65,6 +68,18 @@ reconnects temporary socket drops automatically. `onStateChange` reports
|
|
|
65
68
|
`acquiring`, `connecting`, `connected`, `reconnecting`, and `closed`; its optional
|
|
66
69
|
metadata distinguishes a server `bye`, a replaced server version, an explicit
|
|
67
70
|
`leave`, and connection failures.
|
|
71
|
+
|
|
72
|
+
`connection.serverBundleId` and `connection.serverBundleSha256` identify the server publication running the connected session. These read-only diagnostics are `null` before a live connection, while disconnected, in solo mode, or when the host does not provide them. The ID identifies a publication; the SHA-256 identifies its code bytes, so two publications can share a hash. Read the values again after a reconnect or a destination join. They describe the running server, not the client bundle or the deck’s newest save.
|
|
73
|
+
|
|
74
|
+
Experimental: `Multiplayer.join({ mode: "public", pongTimeoutMs: 10_000 })`
|
|
75
|
+
reconnects if a keepalive ping receives no pong within the given milliseconds.
|
|
76
|
+
Omitting the option (or passing `0`) preserves the existing keepalive behavior.
|
|
77
|
+
The deadline starts at a ping, normally sent every 25 seconds; it is not a
|
|
78
|
+
deadline for game messages. A deadline callback delayed by more than one second (for example during page
|
|
79
|
+
suspension) sends one fresh ping and waits a full deadline before reconnecting. Only one
|
|
80
|
+
such extension is allowed without a pong, so repeated timer throttling cannot
|
|
81
|
+
postpone recovery indefinitely.
|
|
82
|
+
|
|
68
83
|
During local development, reloading a Play panel restores that panel's session
|
|
69
84
|
when its dev server is still running. A separate Play panel joins separately.
|
|
70
85
|
|
|
@@ -147,6 +162,13 @@ export default {
|
|
|
147
162
|
|
|
148
163
|
`session.storage.deck` is shared by everyone playing.
|
|
149
164
|
`session.storage.user(userId)` is one player's public data.
|
|
165
|
+
`await session.storage.playthrough(userId)` opens that player's generation-bound
|
|
166
|
+
game state. Its `resetGeneration` is present only after an administrator has
|
|
167
|
+
reset that player's progress for this deck. It is a monotonic reset marker,
|
|
168
|
+
separate from the handle's `generation`, which fences playthrough operations.
|
|
169
|
+
Deck-scope storage writes are not fenced by either generation; a server using
|
|
170
|
+
`session.storage.deck` must discard its own stale queued writes and cached
|
|
171
|
+
per-player values when it observes a newer reset marker.
|
|
150
172
|
`session.storage.board(name)` is a leaderboard. The root and its buckets also
|
|
151
173
|
support the [atomic save API](#atomic-saves). Each scope has the same
|
|
152
174
|
`get`, `set`, `remove`, `increment` and `list` as `Store`.
|
|
@@ -196,6 +218,10 @@ dev serve.
|
|
|
196
218
|
|
|
197
219
|
- `session.hub.place`, `.title` and `.result` are the place and the result shape
|
|
198
220
|
as published (`result` is `null` for a place that reports none, such as a town).
|
|
221
|
+
- `session.hub.reconnectWindowMs`, when present, is Castle's pinned hub-place
|
|
222
|
+
reconnect grace in milliseconds, inclusive at the deadline. It is absent on
|
|
223
|
+
sessions without a pinned grace and older hosts. Kits use it to match visit retention to the
|
|
224
|
+
transport window; deck code does not choose it.
|
|
199
225
|
- `session.hub.arrival(playerId)` is how a player came in when Castle placed them
|
|
200
226
|
through a hub door: `{ fromDeckId, doorId }` (`doorId` is `null` when the door
|
|
201
227
|
names none), else `null`. It is Castle's word, never a client's, and it is also
|
|
@@ -257,6 +283,9 @@ dev serve.
|
|
|
257
283
|
ids. Missing lists are empty on older hosts. Mutual friends keep the chat age
|
|
258
284
|
and block rules; follows in either direction require no block, with no age rule.
|
|
259
285
|
These lists belong to the viewer only and must never go to another client.
|
|
286
|
+
Optional third argument `{ strict: true }` rejects malformed raw lists before
|
|
287
|
+
normalization and adds `validated: true` to a successful result. Without it,
|
|
288
|
+
the existing response shape and older-host behavior remain unchanged.
|
|
260
289
|
- `session.hub.hiddenPair(playerIdA, playerIdB)` is true when a block hides these
|
|
261
290
|
two players from each other (either one blocked the other). Never send either
|
|
262
291
|
one the other's pose, avatar or name. The block lists themselves never reach
|
|
@@ -625,6 +654,61 @@ claim can fail with writer loss while `committed === true` and its original
|
|
|
625
654
|
receipt; it never takes ownership again. Transport ambiguity keeps the outcome
|
|
626
655
|
unknown until receipt lookup or identical replay settles it.
|
|
627
656
|
|
|
657
|
+
### Conditional writes and recovery (gated)
|
|
658
|
+
|
|
659
|
+
`Store.conditionalWrite(input)` and `session.storage.conditionalWrite(input)`
|
|
660
|
+
use the same request and receipt as `transaction`. Read with `snapshot`, then
|
|
661
|
+
pass its `access` and each target's `revision` as `expectedAccess` and
|
|
662
|
+
`expectedRevision`. Use `null` to create an absent target. A revision belongs
|
|
663
|
+
to a **prefix**, so changing any key in it changes the revision. For an
|
|
664
|
+
independent value, give it a separate, non-overlapping prefix.
|
|
665
|
+
|
|
666
|
+
This additive API is off by default. The platform must enable conditional
|
|
667
|
+
storage for the deck after its database index and host support are ready.
|
|
668
|
+
Older hosts and the dev memory store do not support it. Keep a dependent
|
|
669
|
+
feature off when the API is unavailable; never fall back to ordinary writes.
|
|
670
|
+
Existing `get`, `set` and save methods keep their behavior.
|
|
671
|
+
|
|
672
|
+
One call changes up to eight targets, with up to eight explicit key changes
|
|
673
|
+
per target, across the same deck and at most two players. Every target needs
|
|
674
|
+
its expected revision. A read-only `check` can use `expectedRevision: null`
|
|
675
|
+
to require absence without creating that target. All changes commit together. Clients may address their
|
|
676
|
+
own user/private targets and writable deck targets; only the deck server can
|
|
677
|
+
condition on another player's user target. Servers cannot access private data.
|
|
678
|
+
Existing quota, byte and rate allowances still apply. A prefix replacement
|
|
679
|
+
also removes its previous rows, subject to those allowances.
|
|
680
|
+
|
|
681
|
+
On a revision conflict, `SaveError.code` is
|
|
682
|
+
`CAULDRON_STORAGE_REVISION_CONFLICT`, `committed` is `false`, and `current`
|
|
683
|
+
contains the current values and revisions read at the failed comparison.
|
|
684
|
+
If that full snapshot exceeds the byte allowance, the call fails with
|
|
685
|
+
`CAULDRON_STORAGE_TOO_LARGE`; no changes commit. Keep the player's proposed
|
|
686
|
+
change and reload before offering another choice.
|
|
687
|
+
|
|
688
|
+
Use a unique `operationId` for each action. Retrying the **identical request**
|
|
689
|
+
after a lost reply returns its original committed receipt, even if values
|
|
690
|
+
have since changed. Reusing the ID with different input is refused. Failed
|
|
691
|
+
comparisons do not create receipts. Receipts have no automatic expiry and
|
|
692
|
+
are retained beyond 24 hours; an explicit data/account reset may remove them.
|
|
693
|
+
Never reuse old operation IDs or access tokens after a reset.
|
|
694
|
+
|
|
695
|
+
`recentOperations({ limit?, cursor? })`, on both root APIs, discovers the
|
|
696
|
+
caller's committed operation IDs after a restart. It returns
|
|
697
|
+
`{ operations: [{ operationId, committedAt, receipt }], cursor }`, newest
|
|
698
|
+
first. `limit` defaults to 16 and is at most 32; pass the returned cursor for
|
|
699
|
+
the next page. This lists existing save receipts too. Client and server
|
|
700
|
+
histories are separate; all layers of the same deck share the server history.
|
|
701
|
+
Use a smaller limit if the reply exceeds the byte allowance. A history page
|
|
702
|
+
is discovery, not a snapshot of current values: reload with `snapshot` before
|
|
703
|
+
building the next change. `operation(id)` still looks up a known ID.
|
|
704
|
+
|
|
705
|
+
Pairing can atomically create a project and reserve both players' recovery
|
|
706
|
+
entries. The game server must still validate consent, membership and editing
|
|
707
|
+
leases. A market sale can atomically update stock, the daily cap and the
|
|
708
|
+
purse, once **every** writer of those values uses the managed targets.
|
|
709
|
+
Durable game recovery entries belong in those targets; recent operation
|
|
710
|
+
receipts alone do not define permanent project ownership or sale policy.
|
|
711
|
+
|
|
628
712
|
### Save coordinator
|
|
629
713
|
|
|
630
714
|
`createSaveCoordinator`, from `castle-web-sdk` or `castle-web-sdk/server`,
|
|
@@ -1075,14 +1159,111 @@ Snapshot.onTaken(() => {
|
|
|
1075
1159
|
});
|
|
1076
1160
|
```
|
|
1077
1161
|
|
|
1162
|
+
## Social
|
|
1163
|
+
|
|
1164
|
+
`Social` gives a Cauldron deck the signed-in player's mutual Castle follows.
|
|
1165
|
+
The host supplies the player and current deck from trusted play context; a deck
|
|
1166
|
+
cannot ask about another account or deck. A host from before Social rejects with
|
|
1167
|
+
`CastleError` code `UNKNOWN_COMMAND`.
|
|
1168
|
+
|
|
1169
|
+
- `Social.friends()` returns at most 20 mutual follows as
|
|
1170
|
+
`{ userId, username, photoUrl? }`. A one-way follow is not a friend here.
|
|
1171
|
+
- `Social.presence()` returns the mutual friends playing this deck now as
|
|
1172
|
+
`{ userId, username, photoUrl?, sessionId, routeId?, slot?, updatedAt }`.
|
|
1173
|
+
Castle's now-playing privacy and multiplayer gates apply. It fails closed to
|
|
1174
|
+
`[]` when presence is gated or unavailable.
|
|
1175
|
+
|
|
1176
|
+
Presence has no event in v1. Poll `Social.presence()` when the game needs to
|
|
1177
|
+
refresh its list; do not treat `updatedAt` as a durable offline record.
|
|
1178
|
+
|
|
1179
|
+
```js
|
|
1180
|
+
import { Social } from "castle-web-sdk";
|
|
1181
|
+
|
|
1182
|
+
const friends = await Social.friends();
|
|
1183
|
+
const playingHere = await Social.presence();
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
## Party
|
|
1187
|
+
|
|
1188
|
+
`Party` creates and drives Castle parties with the signed-in player's account.
|
|
1189
|
+
Castle still enforces membership, roles, age and party permissions; refusals are
|
|
1190
|
+
`CastleError`s carrying the server's code. Party and user-id arrays are bounded
|
|
1191
|
+
to 32 users.
|
|
1192
|
+
|
|
1193
|
+
- `Party.current()` returns the party this play was launched with as
|
|
1194
|
+
`{ partyId, name, status, hasUnreadMessages }`, or `null`. The party id comes
|
|
1195
|
+
only from trusted host context, and no request is made when the play has none.
|
|
1196
|
+
- `Party.create(userIds)` creates a party containing those users and the
|
|
1197
|
+
signed-in player. It never sets an initial deck. Creating one does not change
|
|
1198
|
+
`Party.current()` or the launch party used by
|
|
1199
|
+
`Multiplayer.join({ mode: "party" })`.
|
|
1200
|
+
- `Party.invite(partyId, userIds)` invites more users.
|
|
1201
|
+
- `Party.respond(partyId, accept)` accepts or rejects an invitation.
|
|
1202
|
+
- `Party.members(partyId)` returns
|
|
1203
|
+
`{ userId, username, photoUrl?, status, role }` rows. `role` is `member`,
|
|
1204
|
+
`host` or `owner`.
|
|
1205
|
+
- `Party.leave(partyId)` leaves the party.
|
|
1206
|
+
- `Party.messages(partyId, cursor?)` returns
|
|
1207
|
+
`{ messages, cursor }`. Messages are newest first; pass the cursor to load
|
|
1208
|
+
**older** history (the opposite direction from `Hub.chat(after)`). Each message
|
|
1209
|
+
has `{ id, fromUserId, fromUsername, body, text, createdAt }`. `body` is the
|
|
1210
|
+
Castle chat JSON; `text` is non-null only when that body is plain text with no
|
|
1211
|
+
tags, links or embeds.
|
|
1212
|
+
- `Party.say(partyId, text)` sends plain text to the party. The host trims it
|
|
1213
|
+
and caps it at 1,000 characters, then returns the stored `PartyMessage`.
|
|
1214
|
+
|
|
1215
|
+
```js
|
|
1216
|
+
import { Party } from "castle-web-sdk";
|
|
1217
|
+
|
|
1218
|
+
const party = await Party.current();
|
|
1219
|
+
if (party) {
|
|
1220
|
+
const page = await Party.messages(party.partyId);
|
|
1221
|
+
await Party.say(party.partyId, "Meet by the gate!");
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1078
1225
|
## Hub
|
|
1079
1226
|
|
|
1227
|
+
Hub contract v1.95 adds optional invites. A place declaring `hub.invite: true`
|
|
1228
|
+
can call `hub.invite(playerId)` on its server to offer **Invite a friend**.
|
|
1229
|
+
The hub kit asks `Hub.invite()` for a link; `Hub.invite({ crew: true })` makes
|
|
1230
|
+
a crew link for up to five invitees. Links expire within 24 hours and default
|
|
1231
|
+
to one invitee. Hosts without invite support return `unavailable`.
|
|
1232
|
+
|
|
1233
|
+
The trusted host resolves the link after sign-in, retains its token, and requests
|
|
1234
|
+
the inviter's layer when it has room. An invite can also route through the
|
|
1235
|
+
existing `Hub.inviteArrival()` result `{ status: 'route', doorId }` to the
|
|
1236
|
+
inviter's house or active run behind a curated door that allows late joins.
|
|
1237
|
+
Contract v1.113 active-run routing requires root `hub.bringFriend: true`
|
|
1238
|
+
in addition to `invite: true` on both the town and destination. The destination
|
|
1239
|
+
also declares `hub.bringFriend: true` so its saved bundle and fresh pin retain
|
|
1240
|
+
the invite capability;
|
|
1241
|
+
it admits only to the matching live run while the inviter is present. A full,
|
|
1242
|
+
ended or unavailable run is refused rather than recreated. The kit calls
|
|
1243
|
+
`Hub.inviteArrival()` after joining that destination. Ghost verifies arrival
|
|
1244
|
+
before creating mutual follows among the eligible
|
|
1245
|
+
over-13 members of that link. Under-13 arrivals create no follows. The arrival
|
|
1246
|
+
surface shows an authored now-friends line and `Hub.undoInvite(userId)` removes
|
|
1247
|
+
both follow directions for that pair. No player-entered message travels with a link.
|
|
1248
|
+
These operations retain the existing hub access gates. Link creation and arrival
|
|
1249
|
+
completion wait up to 16 seconds when the server presence roster is still catching
|
|
1250
|
+
up with the game connection.
|
|
1251
|
+
|
|
1080
1252
|
`Hub` moves a player between hub places: every deck built on the hub-game kit is
|
|
1081
1253
|
a place, and a door is a row in Castle's curated door table. A deck names only a
|
|
1082
1254
|
door; the host supplies the current place, the session it joined there and the
|
|
1083
1255
|
party, and calls Castle with the player's own account. Hosts without hub support
|
|
1084
1256
|
answer `unavailable`; older hosts reject with `CastleError` code `UNKNOWN_COMMAND`.
|
|
1085
1257
|
|
|
1258
|
+
`Hub.signIn({ reason })` asks the host to open its native sign-in flow for an
|
|
1259
|
+
anonymous player. `reason` is the short explanation shown to the player (at most
|
|
1260
|
+
200 characters and 512 UTF-8 bytes). It resolves to
|
|
1261
|
+
`{ status: 'signedIn', userId? }`, `{ status: 'dismissed' }`, or
|
|
1262
|
+
`{ status: 'unavailable' }`. `userId` is present only when the host can return
|
|
1263
|
+
the signed-in account id. A new host without native sign-in support returns
|
|
1264
|
+
`unavailable`; an older host that does not know this command rejects with
|
|
1265
|
+
`UNKNOWN_COMMAND`.
|
|
1266
|
+
|
|
1086
1267
|
- `Hub.doors()` reads the current place's doors and who is inside them:
|
|
1087
1268
|
`{ status: "ok", doors }` or `{ status: "unavailable", doors: null }`. A row has
|
|
1088
1269
|
`doorId`, `launcherKind` (`door`, `table`, `cabinet`, `gate`), `placementKind`
|
|
@@ -1511,6 +1692,13 @@ startGame(saved ?? newGame());
|
|
|
1511
1692
|
Lifecycle.ready();
|
|
1512
1693
|
```
|
|
1513
1694
|
|
|
1695
|
+
## Display
|
|
1696
|
+
|
|
1697
|
+
`getDisplay()` returns a `DeckDisplay`. Its optional `orientation` field is the
|
|
1698
|
+
deck's preferred display orientation: `landscape`, `portrait`, or `any`.
|
|
1699
|
+
Missing or unrecognized preferences mean `any`; read the value as
|
|
1700
|
+
`display.orientation ?? "any"`.
|
|
1701
|
+
|
|
1514
1702
|
## Setup
|
|
1515
1703
|
|
|
1516
1704
|
Startup, editor-mode check, and a file-write call for editor UI.
|
|
@@ -1702,3 +1890,12 @@ places may save hearts. Guests and unavailable hosts return
|
|
|
1702
1890
|
`{ status: 'unavailable', hearts: [] }`; older hosts throw `UNKNOWN_COMMAND`.
|
|
1703
1891
|
Invalid lists throw `INVALID_ARGUMENT`, and other host failures use `CastleError`.
|
|
1704
1892
|
The dev serve keeps hearts per account in memory until it stops.
|
|
1893
|
+
|
|
1894
|
+
Server hub analytics (hub-game contract v1.93): `session.hub.track(playerId,
|
|
1895
|
+
{ kind, subject?, value?, result?, flags? })` sends a numeric event through the
|
|
1896
|
+
server's authenticated platform channel. It returns `Promise<boolean>`: `true`
|
|
1897
|
+
acknowledges queueing, while unsupported hosts and refused delivery return `false`.
|
|
1898
|
+
It never throws. Only opted-in hub bundles are recorded; Ghost stamps the player
|
|
1899
|
+
and town from the session and door pin and excludes the town creator's own plays.
|
|
1900
|
+
The hub-game kit exposes `hub.track(playerId, eventName, properties)` only on
|
|
1901
|
+
places declaring `hub.analytics: true`; see its contract for the numeric vocabulary.
|
package/dist/castle.d.ts
CHANGED
|
@@ -13,7 +13,11 @@ export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckF
|
|
|
13
13
|
export { Haptics } from "./haptics";
|
|
14
14
|
export type { CastleHapticsApi, HapticsResult, HapticsStatus, HapticStyle } from "./haptics";
|
|
15
15
|
export { Hub } from "./hub";
|
|
16
|
-
export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubHeartsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
|
|
16
|
+
export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubInviteResult, HubInviteArrivalResult, HubHeartsResult, HubGoParams, HubSignInParams, HubSignInResult, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
|
|
17
|
+
export { Social } from "./social";
|
|
18
|
+
export type { CastleSocialApi, SocialFriend, SocialPresence } from "./social";
|
|
19
|
+
export { Party } from "./party";
|
|
20
|
+
export type { CastlePartyApi, PartyMember, PartyMessage, PartyMessagesResult, PartyStatus, PartySummary, } from "./party";
|
|
17
21
|
export { Leaderboard } from "./leaderboard";
|
|
18
22
|
export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardShowOptions, LeaderboardShowResult, LeaderboardShowStatus, LeaderboardSort, } from "./leaderboard";
|
|
19
23
|
export { Lifecycle } from "./lifecycle";
|
|
@@ -35,7 +39,7 @@ export type { CastlePortalApi, PortalOpenResult, PortalOpenStatus, PortalPrefetc
|
|
|
35
39
|
export { CARD_RATIO, deleteFile, fileUrl, initCard, onBeforeRestart, onFilesChanged, onSaveReloadState, openFile, renameFile, requestReload, setup, takeReloadState, writeFileOnce, } from "./runtime";
|
|
36
40
|
export type { FileChange, FilesChangedEvent } from "./runtime";
|
|
37
41
|
export { cardUnit, fitStage, fovForAspect, getDisplay } from "./display";
|
|
38
|
-
export type { DeckDisplay, DeckLayout } from "./display";
|
|
42
|
+
export type { DeckDisplay, DeckLayout, DeckOrientation } from "./display";
|
|
39
43
|
export { flushSaves, hasPendingSave, onSaveState, writeFile, } from "./saveQueue";
|
|
40
44
|
export type { SaveState } from "./saveQueue";
|
|
41
45
|
export { SharedStorage, Storage } from "./storage";
|
|
@@ -49,4 +53,4 @@ export type { CastleUser, CastleUserApi } from "./user";
|
|
|
49
53
|
export { createSaveCoordinator } from "./saveCoordinator";
|
|
50
54
|
export type { CoordinatedChange, CoordinatedSave, SaveCoordinator, SaveStatus, } from "./saveCoordinator";
|
|
51
55
|
export { SaveError } from "./save";
|
|
52
|
-
export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
|
|
56
|
+
export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveConditionalCheck, SaveConditionalWriteInput, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRecentOperationsInput, SaveRecentOperationsPage, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
|
package/dist/castle.js
CHANGED
|
@@ -9,6 +9,8 @@ export { experiment, experiments, experimentsReady } from "./experiments";
|
|
|
9
9
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
10
10
|
export { Haptics } from "./haptics";
|
|
11
11
|
export { Hub } from "./hub";
|
|
12
|
+
export { Social } from "./social";
|
|
13
|
+
export { Party } from "./party";
|
|
12
14
|
export { Leaderboard } from "./leaderboard";
|
|
13
15
|
export { Lifecycle } from "./lifecycle";
|
|
14
16
|
export { Multiplayer } from "./multiplayer";
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SaveClaimed, SaveClaimInput, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveSnapshot, SaveTransactionInput } from "./save";
|
|
1
|
+
import type { SaveClaimed, SaveConditionalWriteInput, SaveClaimInput, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveRecentOperationsInput, SaveRecentOperationsPage, SaveReceipt, SaveSnapshot, SaveTransactionInput } from "./save";
|
|
2
2
|
import type { Json } from "./types";
|
|
3
3
|
export declare const CASTLE_SDK_PROTOCOL = 1;
|
|
4
4
|
export type StorageBlob = Record<string, string>;
|
|
@@ -84,6 +84,41 @@ export type PortalInfoResult = {
|
|
|
84
84
|
export interface PortalPrefetchResult {
|
|
85
85
|
status: PortalPrefetchStatus;
|
|
86
86
|
}
|
|
87
|
+
/** One mutual Castle friend visible to a Cauldron deck. */
|
|
88
|
+
export interface SocialFriend {
|
|
89
|
+
userId: string;
|
|
90
|
+
username: string;
|
|
91
|
+
photoUrl?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface SocialPresence extends SocialFriend {
|
|
94
|
+
sessionId: string;
|
|
95
|
+
routeId?: string;
|
|
96
|
+
slot?: string;
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
}
|
|
99
|
+
export type PartyStatus = "invited" | "joined" | "rejected" | "left" | "party_closed" | "banned";
|
|
100
|
+
export interface PartySummary {
|
|
101
|
+
partyId: string;
|
|
102
|
+
name: string;
|
|
103
|
+
status: PartyStatus;
|
|
104
|
+
hasUnreadMessages: boolean;
|
|
105
|
+
}
|
|
106
|
+
export interface PartyMember extends SocialFriend {
|
|
107
|
+
status: PartyStatus;
|
|
108
|
+
role: "member" | "host" | "owner";
|
|
109
|
+
}
|
|
110
|
+
export interface PartyMessage {
|
|
111
|
+
id: string;
|
|
112
|
+
fromUserId: string;
|
|
113
|
+
fromUsername: string | null;
|
|
114
|
+
body: Json;
|
|
115
|
+
text: string | null;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
}
|
|
118
|
+
export interface PartyMessagesResult {
|
|
119
|
+
messages: PartyMessage[];
|
|
120
|
+
cursor: string | null;
|
|
121
|
+
}
|
|
87
122
|
export interface HubGoParams {
|
|
88
123
|
doorId: string;
|
|
89
124
|
}
|
|
@@ -151,6 +186,15 @@ export interface HubReportParams {
|
|
|
151
186
|
export interface HubReportResult {
|
|
152
187
|
status: "ok" | "unavailable";
|
|
153
188
|
}
|
|
189
|
+
/** Why the hub is asking the player to replace an anonymous account. */
|
|
190
|
+
export interface HubSignInParams {
|
|
191
|
+
reason: string;
|
|
192
|
+
}
|
|
193
|
+
/** The outcome of the host-owned sign-in flow. */
|
|
194
|
+
export interface HubSignInResult {
|
|
195
|
+
status: "signedIn" | "dismissed" | "unavailable";
|
|
196
|
+
userId?: string;
|
|
197
|
+
}
|
|
154
198
|
export type HubFriendsResult = {
|
|
155
199
|
status: "ok";
|
|
156
200
|
friends: HubFriend[];
|
|
@@ -158,6 +202,29 @@ export type HubFriendsResult = {
|
|
|
158
202
|
status: "unavailable";
|
|
159
203
|
friends: null;
|
|
160
204
|
};
|
|
205
|
+
export interface HubInviteResult {
|
|
206
|
+
status: "ok" | "unavailable";
|
|
207
|
+
url?: string;
|
|
208
|
+
expiresAt?: string;
|
|
209
|
+
}
|
|
210
|
+
export interface HubInviteArrivalResult {
|
|
211
|
+
status: "ok" | "unavailable" | "route";
|
|
212
|
+
doorId?: string;
|
|
213
|
+
destination?: {
|
|
214
|
+
hub: {
|
|
215
|
+
version: 1;
|
|
216
|
+
townDeckId: string;
|
|
217
|
+
target: {
|
|
218
|
+
kind: "player";
|
|
219
|
+
id: string;
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
friends?: Array<{
|
|
224
|
+
userId: string;
|
|
225
|
+
username: string;
|
|
226
|
+
}>;
|
|
227
|
+
}
|
|
161
228
|
/** Account-wide hub door hearts, in oldest-first order (at most eight). */
|
|
162
229
|
export interface HubHeartsResult {
|
|
163
230
|
status: "ok" | "unavailable";
|
|
@@ -532,14 +599,50 @@ export interface CommandParams {
|
|
|
532
599
|
"portal.info": {
|
|
533
600
|
targetDeckId: string;
|
|
534
601
|
};
|
|
602
|
+
"social.friends": Record<string, never>;
|
|
603
|
+
"social.presence": Record<string, never>;
|
|
604
|
+
"party.current": Record<string, never>;
|
|
605
|
+
"party.create": {
|
|
606
|
+
userIds: string[];
|
|
607
|
+
};
|
|
608
|
+
"party.invite": {
|
|
609
|
+
partyId: string;
|
|
610
|
+
userIds: string[];
|
|
611
|
+
};
|
|
612
|
+
"party.respond": {
|
|
613
|
+
partyId: string;
|
|
614
|
+
accept: boolean;
|
|
615
|
+
};
|
|
616
|
+
"party.members": {
|
|
617
|
+
partyId: string;
|
|
618
|
+
};
|
|
619
|
+
"party.leave": {
|
|
620
|
+
partyId: string;
|
|
621
|
+
};
|
|
622
|
+
"party.messages": {
|
|
623
|
+
partyId: string;
|
|
624
|
+
cursor?: string;
|
|
625
|
+
};
|
|
626
|
+
"party.say": {
|
|
627
|
+
partyId: string;
|
|
628
|
+
text: string;
|
|
629
|
+
};
|
|
535
630
|
"hub.go": HubGoParams;
|
|
536
631
|
"hub.back": Record<string, never>;
|
|
537
632
|
"hub.doors": Record<string, never>;
|
|
538
633
|
"hub.friends": Record<string, never>;
|
|
634
|
+
"hub.invite": {
|
|
635
|
+
crew?: boolean;
|
|
636
|
+
};
|
|
637
|
+
"hub.inviteArrival": Record<string, never>;
|
|
638
|
+
"hub.undoInvite": {
|
|
639
|
+
userId: string;
|
|
640
|
+
};
|
|
539
641
|
"hub.hearts": Record<string, never>;
|
|
540
642
|
"hub.setHearts": {
|
|
541
643
|
doorIds: string[];
|
|
542
644
|
};
|
|
645
|
+
"hub.signIn": HubSignInParams;
|
|
543
646
|
"hub.jump": HubJumpParams;
|
|
544
647
|
"hub.report": HubReportParams;
|
|
545
648
|
"hub.chat": HubChatParams;
|
|
@@ -601,6 +704,8 @@ export interface CommandParams {
|
|
|
601
704
|
"cauldronStorage.transaction": SaveTransactionInput;
|
|
602
705
|
"cauldronStorage.claim": SaveClaimInput;
|
|
603
706
|
"cauldronStorage.initialize": SaveInitializeInput;
|
|
707
|
+
"cauldronStorage.conditionalWrite": SaveConditionalWriteInput;
|
|
708
|
+
"cauldronStorage.recentOperations": SaveRecentOperationsInput;
|
|
604
709
|
"cauldronStorage.operation": {
|
|
605
710
|
operationId: string;
|
|
606
711
|
};
|
|
@@ -706,12 +811,32 @@ export interface CommandResult {
|
|
|
706
811
|
"portal.open": PortalOpenResult;
|
|
707
812
|
"portal.prefetch": PortalPrefetchResult;
|
|
708
813
|
"portal.info": PortalInfoResult;
|
|
814
|
+
"social.friends": SocialFriend[];
|
|
815
|
+
"social.presence": SocialPresence[];
|
|
816
|
+
"party.current": PartySummary | null;
|
|
817
|
+
"party.create": PartySummary;
|
|
818
|
+
"party.invite": {
|
|
819
|
+
ok: true;
|
|
820
|
+
};
|
|
821
|
+
"party.respond": {
|
|
822
|
+
ok: true;
|
|
823
|
+
};
|
|
824
|
+
"party.members": PartyMember[];
|
|
825
|
+
"party.leave": {
|
|
826
|
+
ok: true;
|
|
827
|
+
};
|
|
828
|
+
"party.messages": PartyMessagesResult;
|
|
829
|
+
"party.say": PartyMessage;
|
|
709
830
|
"hub.go": HubGoResult;
|
|
710
831
|
"hub.back": HubBackResult;
|
|
711
832
|
"hub.doors": HubDoorsResult;
|
|
712
833
|
"hub.friends": HubFriendsResult;
|
|
834
|
+
"hub.invite": HubInviteResult;
|
|
835
|
+
"hub.inviteArrival": HubInviteArrivalResult;
|
|
836
|
+
"hub.undoInvite": HubReportResult;
|
|
713
837
|
"hub.hearts": HubHeartsResult;
|
|
714
838
|
"hub.setHearts": HubHeartsResult;
|
|
839
|
+
"hub.signIn": HubSignInResult;
|
|
715
840
|
"hub.jump": HubJumpResult;
|
|
716
841
|
"hub.report": HubReportResult;
|
|
717
842
|
"hub.chat": HubChatResult;
|
|
@@ -747,6 +872,8 @@ export interface CommandResult {
|
|
|
747
872
|
"cauldronStorage.transaction": SaveReceipt;
|
|
748
873
|
"cauldronStorage.claim": SaveClaimed;
|
|
749
874
|
"cauldronStorage.initialize": SaveOpened;
|
|
875
|
+
"cauldronStorage.conditionalWrite": SaveReceipt;
|
|
876
|
+
"cauldronStorage.recentOperations": SaveRecentOperationsPage;
|
|
750
877
|
"cauldronStorage.operation": SaveOperation;
|
|
751
878
|
"cauldronStorage.get": {
|
|
752
879
|
entries: StoreEntry[];
|
package/dist/commands.js
CHANGED
|
@@ -29,6 +29,9 @@ export const COMMAND_TIMEOUTS_MS = {
|
|
|
29
29
|
"hub.go": 30_000,
|
|
30
30
|
"hub.back": 30_000,
|
|
31
31
|
"hub.jump": 30_000,
|
|
32
|
+
// Native auth may include a provider handoff, but it must not leave a deck
|
|
33
|
+
// waiting forever if the host loses the completion callback.
|
|
34
|
+
"hub.signIn": 120_000,
|
|
32
35
|
};
|
|
33
36
|
// Once a host streams a command, each stream envelope restarts the wait at this
|
|
34
37
|
// length instead. The deck-agent service sends a heartbeat every 15 seconds, and the host
|
package/dist/display.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type DeckLayout = "card" | "fill";
|
|
2
|
+
export type DeckOrientation = "landscape" | "portrait" | "any";
|
|
2
3
|
export interface DeckDisplay {
|
|
3
4
|
layout: DeckLayout;
|
|
5
|
+
/** The orientation the deck prefers the host to use. */
|
|
6
|
+
orientation?: DeckOrientation;
|
|
4
7
|
/** Narrowest width / height the deck accepts. */
|
|
5
8
|
minAspect: number;
|
|
6
9
|
/** Widest width / height the deck accepts. */
|
package/dist/display.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// <meta name="castle-display" content="layout=fill">
|
|
7
7
|
// <meta name="castle-display" content="layout=fill, min-aspect=9/16, max-aspect=16/9">
|
|
8
|
+
// <meta name="castle-display" content="layout=fill, orientation=landscape">
|
|
8
9
|
//
|
|
9
10
|
// With no tag the deck is a `card`: the classic 5:7 portrait card, exactly as
|
|
10
11
|
// before. A `fill` deck takes whatever box the host gives it, as long as the
|
|
@@ -42,13 +43,18 @@ export function parseDisplayContent(content) {
|
|
|
42
43
|
continue;
|
|
43
44
|
fields[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim();
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const declaredOrientation = fields.orientation?.toLowerCase();
|
|
47
|
+
const orientation = declaredOrientation === "landscape" || declaredOrientation === "portrait"
|
|
48
|
+
? declaredOrientation
|
|
49
|
+
: "any";
|
|
50
|
+
if (fields.layout?.toLowerCase() !== "fill") {
|
|
51
|
+
return orientation === "any" ? CARD_DISPLAY : { ...CARD_DISPLAY, orientation };
|
|
52
|
+
}
|
|
47
53
|
let minAspect = parseAspect(fields["min-aspect"] ?? "") ?? FILL_MIN_ASPECT;
|
|
48
54
|
let maxAspect = parseAspect(fields["max-aspect"] ?? "") ?? FILL_MAX_ASPECT;
|
|
49
55
|
if (minAspect > maxAspect)
|
|
50
56
|
[minAspect, maxAspect] = [maxAspect, minAspect];
|
|
51
|
-
return { layout: "fill", minAspect, maxAspect };
|
|
57
|
+
return { layout: "fill", orientation, minAspect, maxAspect };
|
|
52
58
|
}
|
|
53
59
|
let cached = null;
|
|
54
60
|
/** The display this deck declared in its index.html (a card when it declared none). */
|
package/dist/hub.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HubBackResult, HubCancelResult, HubChatResult, HubDoorsResult, HubFriendsResult, HubHeartsResult, HubGoResult, HubJumpResult, HubReportResult, HubSayResult } from "./commands";
|
|
2
|
-
export type { HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubHeartsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./commands";
|
|
1
|
+
import type { HubBackResult, HubCancelResult, HubChatResult, HubDoorsResult, HubFriendsResult, HubInviteResult, HubInviteArrivalResult, HubHeartsResult, HubGoResult, HubJumpResult, HubReportResult, HubSayResult, HubSignInResult } from "./commands";
|
|
2
|
+
export type { HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubInviteResult, HubInviteArrivalResult, HubHeartsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, HubSignInParams, HubSignInResult, } from "./commands";
|
|
3
3
|
/**
|
|
4
4
|
* Doors between hub places. Every hub-kit deck is a place; ghost's door table
|
|
5
5
|
* is the allowlist, and the host supplies real authentication and the current
|
|
@@ -35,10 +35,22 @@ export interface CastleHubApi {
|
|
|
35
35
|
* on hosts without hub support.
|
|
36
36
|
*/
|
|
37
37
|
friends(): Promise<HubFriendsResult>;
|
|
38
|
+
/** Create an expiring link from this hub session. Crew links admit up to five invitees. */
|
|
39
|
+
invite(options?: {
|
|
40
|
+
crew?: boolean;
|
|
41
|
+
}): Promise<HubInviteResult>;
|
|
42
|
+
/** Hub-kit arrival hook: the trusted host retains the invite token. */
|
|
43
|
+
inviteArrival(): Promise<HubInviteArrivalResult>;
|
|
44
|
+
/** Undo both follow directions for one pair from this host's invite arrival. */
|
|
45
|
+
undoInvite(userId: string): Promise<HubReportResult>;
|
|
38
46
|
/** Read this signed-in account's hub door hearts. */
|
|
39
47
|
hearts(): Promise<HubHeartsResult>;
|
|
40
48
|
/** Replace the hearts, oldest first, at most eight distinct door ids. */
|
|
41
49
|
setHearts(doorIds: string[]): Promise<HubHeartsResult>;
|
|
50
|
+
/** Ask the host to let an anonymous player sign in to a permanent account. */
|
|
51
|
+
signIn(options: {
|
|
52
|
+
reason: string;
|
|
53
|
+
}): Promise<HubSignInResult>;
|
|
42
54
|
/**
|
|
43
55
|
* Acquire the layer of this place a friend is in. The destination is this
|
|
44
56
|
* same place in another session; join it exactly like a door's
|
package/dist/hub.js
CHANGED
|
@@ -5,8 +5,12 @@ export const Hub = {
|
|
|
5
5
|
back,
|
|
6
6
|
doors,
|
|
7
7
|
friends,
|
|
8
|
+
invite,
|
|
9
|
+
inviteArrival,
|
|
10
|
+
undoInvite,
|
|
8
11
|
hearts,
|
|
9
12
|
setHearts,
|
|
13
|
+
signIn,
|
|
10
14
|
jump,
|
|
11
15
|
report,
|
|
12
16
|
chat,
|
|
@@ -34,6 +38,19 @@ async function doors() {
|
|
|
34
38
|
async function friends() {
|
|
35
39
|
return hostRequest("hub.friends", {});
|
|
36
40
|
}
|
|
41
|
+
async function invite(options = {}) {
|
|
42
|
+
if (options.crew !== undefined && typeof options.crew !== "boolean")
|
|
43
|
+
throw new CastleError({ code: "INVALID_ARGUMENT", message: "Hub.invite crew must be a boolean.", operation: "Hub.invite" });
|
|
44
|
+
return hostRequest("hub.invite", options.crew === true ? { crew: true } : {});
|
|
45
|
+
}
|
|
46
|
+
async function inviteArrival() {
|
|
47
|
+
return hostRequest("hub.inviteArrival", {});
|
|
48
|
+
}
|
|
49
|
+
async function undoInvite(userId) {
|
|
50
|
+
if (!/^[1-9]\d{0,9}$/.test(userId))
|
|
51
|
+
throw new CastleError({ code: "INVALID_ARGUMENT", message: "Hub.undoInvite requires a userId.", operation: "Hub.undoInvite" });
|
|
52
|
+
return hostRequest("hub.undoInvite", { userId });
|
|
53
|
+
}
|
|
37
54
|
async function hearts() {
|
|
38
55
|
return hostRequest("hub.hearts", {});
|
|
39
56
|
}
|
|
@@ -45,6 +62,23 @@ async function setHearts(doorIds) {
|
|
|
45
62
|
}
|
|
46
63
|
return hostRequest("hub.setHearts", { doorIds });
|
|
47
64
|
}
|
|
65
|
+
const SIGN_IN_REASON_MAX_CHARACTERS = 200;
|
|
66
|
+
const SIGN_IN_REASON_MAX_BYTES = 512;
|
|
67
|
+
async function signIn(options) {
|
|
68
|
+
const reason = options?.reason;
|
|
69
|
+
if (typeof reason !== "string" ||
|
|
70
|
+
reason.length === 0 ||
|
|
71
|
+
reason.length > SIGN_IN_REASON_MAX_CHARACTERS ||
|
|
72
|
+
reason.includes("\0") ||
|
|
73
|
+
new TextEncoder().encode(reason).byteLength > SIGN_IN_REASON_MAX_BYTES) {
|
|
74
|
+
throw new CastleError({
|
|
75
|
+
code: "INVALID_ARGUMENT",
|
|
76
|
+
message: `Hub.signIn requires reason of at most ${SIGN_IN_REASON_MAX_CHARACTERS} characters and ${SIGN_IN_REASON_MAX_BYTES} bytes.`,
|
|
77
|
+
operation: "Hub.signIn",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return hostRequest("hub.signIn", { reason });
|
|
81
|
+
}
|
|
48
82
|
async function jump(userId) {
|
|
49
83
|
if (typeof userId !== "string" || userId.length === 0) {
|
|
50
84
|
throw new CastleError({
|
package/dist/multiplayer.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Json } from "./types";
|
|
|
4
4
|
import { type MultiplayerPlayer, type MultiplayerCharacterDisclosure, type MultiplayerCharacterTurnStatus } from "./multiplayerProtocol";
|
|
5
5
|
import { type MultiplayerVoice } from "./multiplayerVoice";
|
|
6
6
|
export type { MultiplayerJoinHint } from "./commands";
|
|
7
|
-
export type MultiplayerJoinOptions = {
|
|
7
|
+
export type MultiplayerJoinOptions = ({
|
|
8
8
|
mode: "named";
|
|
9
9
|
key: string;
|
|
10
10
|
} | {
|
|
@@ -14,6 +14,9 @@ export type MultiplayerJoinOptions = {
|
|
|
14
14
|
} | {
|
|
15
15
|
mode: "hub";
|
|
16
16
|
handoff: HubHandoffResult;
|
|
17
|
+
}) & {
|
|
18
|
+
/** Experimental opt-in: reconnect if a keepalive ping receives no pong. */
|
|
19
|
+
pongTimeoutMs?: number;
|
|
17
20
|
};
|
|
18
21
|
export interface MultiplayerDestinationOptions {
|
|
19
22
|
routeId: string;
|
|
@@ -184,6 +187,9 @@ export interface MultiplayerConnection {
|
|
|
184
187
|
readonly state: MultiplayerConnectionState;
|
|
185
188
|
readonly playerId: string;
|
|
186
189
|
readonly sessionId: string;
|
|
190
|
+
/** Current host publication diagnostics; null when unavailable or not connected. */
|
|
191
|
+
readonly serverBundleSha256: string | null;
|
|
192
|
+
readonly serverBundleId: string | null;
|
|
187
193
|
readonly isSolo: boolean;
|
|
188
194
|
/**
|
|
189
195
|
* Why the join fell back to solo; null while multiplayer is live.
|