castle-web-sdk 0.4.46 → 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 +276 -2
- package/dist/avatar.d.ts +6 -2
- package/dist/avatar.js +12 -4
- package/dist/castle.d.ts +7 -3
- package/dist/castle.js +2 -0
- package/dist/commands.d.ts +142 -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 +18 -2
- package/dist/hub.js +47 -0
- package/dist/multiplayer.d.ts +7 -1
- package/dist/multiplayer.js +80 -3
- 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 +54 -7
- package/dist/server/hub.d.ts +110 -0
- package/dist/server/hub.js +104 -2
- package/dist/server/platformHandle.d.ts +39 -1
- package/dist/server/storage.d.ts +15 -0
- package/dist/server/storage.js +57 -3
- package/dist/server/wrapper.d.ts +43 -6
- package/dist/server/wrapper.js +93 -9
- 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,10 +162,34 @@ 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`.
|
|
153
175
|
|
|
176
|
+
`session.storage.town(userId)` lets a door deck's server read and write one
|
|
177
|
+
signed-in player's rows in its source town's deck scope, for things such as
|
|
178
|
+
hats bought in town. It has `get`, `set`, `remove` and `increment`; quota counts
|
|
179
|
+
against the town. Every key is one of the town's per-player namespaces
|
|
180
|
+
(`tokens`, `spent`, `own`, `hat`, `chirp`) followed by the user id: for user
|
|
181
|
+
`7`, `own:7:tophat` and `hat:7` work; `own:tophat:7` and `quay:7` do not. The
|
|
182
|
+
rest of the town's rows are its own. The player must be live in this session
|
|
183
|
+
(a player who joined a few seconds ago can briefly get
|
|
184
|
+
`CAULDRON_HUB_NOT_IN_SESSION`, so retry later rather than giving up); in a house, only the owner's rows are
|
|
185
|
+
accessible. Calls reject with `CAULDRON_HUB_NO_TOWN` without a source town
|
|
186
|
+
(including a town calling it), `CAULDRON_HUB_NOT_IN_SESSION` for an absent
|
|
187
|
+
player, or `CAULDRON_HUB_NOT_OWNER` for someone other than the house's owner.
|
|
188
|
+
Invalid key segments reject with `CAULDRON_STORAGE_INVALID_ARGUMENT`.
|
|
189
|
+
During dev serve, keys live under `town/` in the dev deck's own deck scope,
|
|
190
|
+
so testing a door never changes a real town. An unpublished deck still needs
|
|
191
|
+
a published deck id for storage.
|
|
192
|
+
|
|
154
193
|
Two differences follow from a session server not being a player:
|
|
155
194
|
|
|
156
195
|
- **It owns `deck` scope.** Once a deck has a published server, clients read
|
|
@@ -173,8 +212,16 @@ kit's contract). `save-deck` publishes the block's titles and result shapes with
|
|
|
173
212
|
the server bundle, and Castle pins them to every session of the place. Those
|
|
174
213
|
sessions have `session.hub`; every other session has `session.hub === null`.
|
|
175
214
|
|
|
215
|
+
`session.hub.owner` is the owner's user id string on an owner door instance,
|
|
216
|
+
passed by the platform. It is absent on every other hub session, including the
|
|
217
|
+
dev serve.
|
|
218
|
+
|
|
176
219
|
- `session.hub.place`, `.title` and `.result` are the place and the result shape
|
|
177
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.
|
|
178
225
|
- `session.hub.arrival(playerId)` is how a player came in when Castle placed them
|
|
179
226
|
through a hub door: `{ fromDeckId, doorId }` (`doorId` is `null` when the door
|
|
180
227
|
names none), else `null`. It is Castle's word, never a client's, and it is also
|
|
@@ -199,6 +246,46 @@ sessions have `session.hub`; every other session has `session.hub === null`.
|
|
|
199
246
|
id the kit draws, `color` `'#rrggbb'`, `rarity` 0-9), or clears it with `null`;
|
|
200
247
|
the latest wins. Castle takes it only from a curated hub place. Resolves
|
|
201
248
|
`{ wearable }`; `readResults` returns it as `players[userId].wearable`.
|
|
249
|
+
- `session.hub.unlock(playerId, itemId)` gives a signed-in player one of the
|
|
250
|
+
deck's declared earned items (castle.json `hub.unlocks`: `{ id, class, shape,
|
|
251
|
+
name, deed, color }`, at most 8, `class` one of `badge`, `pin`, `belt`, `sash`,
|
|
252
|
+
`held`). v1.70 also permits up to 6 home items separately: `furniture` or
|
|
253
|
+
`deed`, with `piece` in place of `shape` (the same lowercase id format).
|
|
254
|
+
Each id is unique across both lists. Castle takes it only from a curated hub
|
|
255
|
+
place and only for an id the saved deck declares; a first wearable unlock
|
|
256
|
+
is also shown. Resolves
|
|
257
|
+
`{ unlocked: true, isNew, earned }`, where `earned` is what the player now
|
|
258
|
+
shows (`{ deckId, id, class, shape, name, deed, color }`) or `null`, including
|
|
259
|
+
for home grants. A home grant never changes what the avatar shows; a home
|
|
260
|
+
item appears only in the owned list returned by `readUnlocks`.
|
|
261
|
+
`readResults` returns the shown one as `players[userId].earned`, absent when
|
|
262
|
+
none.
|
|
263
|
+
- `session.hub.wearEarned(playerId, { deckId, id } | null)` chooses which owned
|
|
264
|
+
earned item a player shows, or none. Castle takes it only from the town.
|
|
265
|
+
Home items are refused with `CAULDRON_HUB_EARNED_NOT_WEARABLE`.
|
|
266
|
+
Resolves `{ earned }`.
|
|
267
|
+
- `session.hub.readUnlocks(userIds)` reads the earned items players here own:
|
|
268
|
+
`{ players: { [userId]: { owned: [...] } } }`, including home items with
|
|
269
|
+
their `class` and `piece`. `MultiplayerHubEarnedItem` and
|
|
270
|
+
`MultiplayerHubEarnedClass` describe wearable items only;
|
|
271
|
+
`MultiplayerHubHomeItem` and `MultiplayerHubHomeClass` describe home items.
|
|
272
|
+
Only the owned list uses the union of both item types; unlock/wear receipts
|
|
273
|
+
and arrival results keep the wearable-only type.
|
|
274
|
+
- These three reject with `CAULDRON_HUB_UNAVAILABLE` on a runtime without them.
|
|
275
|
+
- `session.hub.friendsAmong(playerId, userIds)` resolves the subset of candidate
|
|
276
|
+
user ids that are this connected player's friends (`Promise<string[]>`).
|
|
277
|
+
The platform checks mutual follows, age, blocks, the hub gate and the player's
|
|
278
|
+
recent client friends ask. It uses `hubFriendsAmong` or the older runtime's
|
|
279
|
+
storage broker and rejects on platform errors. The hub-game kit wraps it with
|
|
280
|
+
decimal-id filtering, a 100-candidate cap, caching and a silent `[]` fallback.
|
|
281
|
+
- `session.hub.relationsAmong(playerId, userIds)` makes the same single host read
|
|
282
|
+
and resolves `{ friends, follows, followedBy }`, each limited to asked string
|
|
283
|
+
ids. Missing lists are empty on older hosts. Mutual friends keep the chat age
|
|
284
|
+
and block rules; follows in either direction require no block, with no age rule.
|
|
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.
|
|
202
289
|
- `session.hub.hiddenPair(playerIdA, playerIdB)` is true when a block hides these
|
|
203
290
|
two players from each other (either one blocked the other). Never send either
|
|
204
291
|
one the other's pose, avatar or name. The block lists themselves never reach
|
|
@@ -567,6 +654,61 @@ claim can fail with writer loss while `committed === true` and its original
|
|
|
567
654
|
receipt; it never takes ownership again. Transport ambiguity keeps the outcome
|
|
568
655
|
unknown until receipt lookup or identical replay settles it.
|
|
569
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
|
+
|
|
570
712
|
### Save coordinator
|
|
571
713
|
|
|
572
714
|
`createSaveCoordinator`, from `castle-web-sdk` or `castle-web-sdk/server`,
|
|
@@ -895,7 +1037,7 @@ itself (ones Castle runs across decks), on the serve only.
|
|
|
895
1037
|
|
|
896
1038
|
The player's platform avatar: one per account, worn in every hub place
|
|
897
1039
|
(cauldron-game-server `docs/HUB.md`, decision 4). In v1 it is an index into the
|
|
898
|
-
hub-game kit's character templates
|
|
1040
|
+
hub-game kit's character templates, with an optional colour choice. The
|
|
899
1041
|
hub-game kit is the caller: it dresses the player and offers the choice in its
|
|
900
1042
|
picker. Other players' avatars are not read here; a place's server learns them
|
|
901
1043
|
from ghost.
|
|
@@ -907,7 +1049,7 @@ every account has (`chosen: false`). Anonymous accounts have one too.
|
|
|
907
1049
|
`{ status: "unavailable", avatar: null }` for a signed-out visitor; a host from
|
|
908
1050
|
before avatars rejects with `UNKNOWN_COMMAND`. Either way the deck picks its own.
|
|
909
1051
|
|
|
910
|
-
### `Avatar.set(avatar): Promise<AvatarResult>`
|
|
1052
|
+
### `Avatar.set(avatar, options?: { color?: number }): Promise<AvatarResult>`
|
|
911
1053
|
|
|
912
1054
|
Makes `avatar` the account's from now on, everywhere. Only a curated hub place
|
|
913
1055
|
(a deck an enabled hub door leads into or out of) may: anywhere else, and for a
|
|
@@ -915,6 +1057,15 @@ signed-out visitor, the answer is `unavailable` and nothing changes. Ghost
|
|
|
915
1057
|
refuses an index outside its template set (`INVALID_ARGUMENT`) and more than 30
|
|
916
1058
|
choices a minute (`RATE_LIMIT_EXCEEDED`).
|
|
917
1059
|
|
|
1060
|
+
A host that supports avatar colours includes `color` in `AvatarResult`; the
|
|
1061
|
+
field is absent from older hosts. `Avatar.colors === true` marks an SDK whose
|
|
1062
|
+
`Avatar.set` carries `{ color }`; the hub-game kit requires both the host field
|
|
1063
|
+
and this marker to offer platform colours. Only after that support is known, pass
|
|
1064
|
+
`{ color }` to choose a non-negative integer colour for the current avatar.
|
|
1065
|
+
The host accepts 0–255; ghost holds the offered range. Changing character
|
|
1066
|
+
without options clears its colour. Omitting colour sends the existing command
|
|
1067
|
+
and mutation unchanged. The read query will ask for colour once ghost serves it.
|
|
1068
|
+
|
|
918
1069
|
## Pass
|
|
919
1070
|
|
|
920
1071
|
A pass is something a creator sells to players for Castle bricks (the
|
|
@@ -1008,14 +1159,111 @@ Snapshot.onTaken(() => {
|
|
|
1008
1159
|
});
|
|
1009
1160
|
```
|
|
1010
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
|
+
|
|
1011
1225
|
## Hub
|
|
1012
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
|
+
|
|
1013
1252
|
`Hub` moves a player between hub places: every deck built on the hub-game kit is
|
|
1014
1253
|
a place, and a door is a row in Castle's curated door table. A deck names only a
|
|
1015
1254
|
door; the host supplies the current place, the session it joined there and the
|
|
1016
1255
|
party, and calls Castle with the player's own account. Hosts without hub support
|
|
1017
1256
|
answer `unavailable`; older hosts reject with `CastleError` code `UNKNOWN_COMMAND`.
|
|
1018
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
|
+
|
|
1019
1267
|
- `Hub.doors()` reads the current place's doors and who is inside them:
|
|
1020
1268
|
`{ status: "ok", doors }` or `{ status: "unavailable", doors: null }`. A row has
|
|
1021
1269
|
`doorId`, `launcherKind` (`door`, `table`, `cabinet`, `gate`), `placementKind`
|
|
@@ -1444,6 +1692,13 @@ startGame(saved ?? newGame());
|
|
|
1444
1692
|
Lifecycle.ready();
|
|
1445
1693
|
```
|
|
1446
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
|
+
|
|
1447
1702
|
## Setup
|
|
1448
1703
|
|
|
1449
1704
|
Startup, editor-mode check, and a file-write call for editor UI.
|
|
@@ -1625,3 +1880,22 @@ Common codes:
|
|
|
1625
1880
|
that lacks it.
|
|
1626
1881
|
- `CAULDRON_STORAGE_STALE_GENERATION` — the handle belongs to an older reset
|
|
1627
1882
|
generation; reopen explicitly rather than replaying work into the new one.
|
|
1883
|
+
|
|
1884
|
+
### Your games: hub hearts
|
|
1885
|
+
|
|
1886
|
+
`Hub.hearts()` returns `{ status: 'ok', hearts: string[] }` for the signed-in
|
|
1887
|
+
account. `Hub.setHearts(doorIds)` replaces that list with at most eight distinct
|
|
1888
|
+
door ids, oldest first. The host supplies the current deck id; only curated hub
|
|
1889
|
+
places may save hearts. Guests and unavailable hosts return
|
|
1890
|
+
`{ status: 'unavailable', hearts: [] }`; older hosts throw `UNKNOWN_COMMAND`.
|
|
1891
|
+
Invalid lists throw `INVALID_ARGUMENT`, and other host failures use `CastleError`.
|
|
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/avatar.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { AvatarResult } from "./commands";
|
|
2
2
|
export type { AvatarResult } from "./commands";
|
|
3
3
|
export interface CastleAvatarApi {
|
|
4
|
+
/** Present only in an SDK whose Avatar.set carries { color }; the hub-game kit offers colours only when it is here. */
|
|
5
|
+
readonly colors?: true;
|
|
4
6
|
/**
|
|
5
7
|
* The player's avatar: their choice, or the default every account has.
|
|
6
8
|
* `unavailable` for a signed-out visitor, or on a host without avatars.
|
|
7
9
|
*/
|
|
8
10
|
get(): Promise<AvatarResult>;
|
|
9
|
-
/** Choose the avatar
|
|
10
|
-
set(avatar: number
|
|
11
|
+
/** Choose the avatar, and optionally its colour, worn everywhere. Requires a host with colours for options.color. */
|
|
12
|
+
set(avatar: number, options?: {
|
|
13
|
+
color?: number;
|
|
14
|
+
}): Promise<AvatarResult>;
|
|
11
15
|
}
|
|
12
16
|
export declare const Avatar: CastleAvatarApi;
|
package/dist/avatar.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Avatar — the player's platform avatar (cauldron-game-server docs/HUB.md,
|
|
2
2
|
// decision 4): one per account, worn in every hub place. v1 is an index into the
|
|
3
|
-
// hub-game kit's character templates
|
|
3
|
+
// hub-game kit's character templates, with optional colour choices.
|
|
4
4
|
//
|
|
5
5
|
// The hub-game kit is the one caller: it reads the avatar to dress the player
|
|
6
6
|
// and offers the choice in its picker. Other players' avatars are not read
|
|
7
7
|
// here; a place's server learns them from ghost.
|
|
8
8
|
import { CastleError } from "./errors";
|
|
9
9
|
import { hostRequest } from "./transport";
|
|
10
|
-
export const Avatar = { get, set };
|
|
10
|
+
export const Avatar = { get, set, colors: true };
|
|
11
11
|
async function get() {
|
|
12
12
|
return hostRequest("avatar.get", {});
|
|
13
13
|
}
|
|
14
|
-
async function set(avatar) {
|
|
14
|
+
async function set(avatar, options) {
|
|
15
15
|
if (typeof avatar !== "number" || !Number.isInteger(avatar) || avatar < 0) {
|
|
16
16
|
throw new CastleError({
|
|
17
17
|
code: "INVALID_ARGUMENT",
|
|
@@ -19,5 +19,13 @@ async function set(avatar) {
|
|
|
19
19
|
operation: "Avatar.set",
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
const color = options?.color;
|
|
23
|
+
if (color !== undefined && (!Number.isInteger(color) || color < 0)) {
|
|
24
|
+
throw new CastleError({
|
|
25
|
+
code: "INVALID_ARGUMENT",
|
|
26
|
+
message: "Avatar.set requires a non-negative integer colour.",
|
|
27
|
+
operation: "Avatar.set",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return hostRequest("avatar.set", color === undefined ? { avatar } : { avatar, color });
|
|
23
31
|
}
|
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, 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";
|