@mapletip/data 0.1.0 → 0.3.0
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 +85 -4
- package/client/core.d.ts +6 -0
- package/client/core.js +15 -2
- package/client/items.d.ts +37 -1
- package/client/items.js +88 -13
- package/client/monster-client.d.ts +6 -1
- package/client/monster-client.js +14 -1
- package/client/monsters.d.ts +15 -7
- package/client/monsters.js +25 -0
- package/client/static-client.d.ts +3 -3
- package/client/static-client.js +2 -2
- package/generated/item-browse-pack-2.d.ts +238 -0
- package/generated/item-browse-pack-2.validator.js +17 -0
- package/generated/monster-catalog-2-1.d.ts +253 -0
- package/generated/monster-catalog-2-1.validator.js +17 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ bundles the domains it imports:
|
|
|
13
13
|
```ts
|
|
14
14
|
import { getMonsterBrowse, openMonsterProduct } from '@mapletip/data/monsters';
|
|
15
15
|
import { openRelease, resolveTargetAvailability } from '@mapletip/data/release';
|
|
16
|
-
import { openItemProduct } from '@mapletip/data/items';
|
|
16
|
+
import { getItemBrowse, openItemProduct } from '@mapletip/data/items';
|
|
17
17
|
import { openEquipmentProduct } from '@mapletip/data/equipment';
|
|
18
18
|
import { openQuestProduct } from '@mapletip/data/quests';
|
|
19
19
|
import { openMediaProduct, resolveMediaUrl } from '@mapletip/data/media';
|
|
@@ -55,6 +55,38 @@ existing typed failure (`UNKNOWN_BUILD`, `UNSUPPORTED_PRODUCT`, `SCOPE_ERROR`,
|
|
|
55
55
|
Supported builds are `cw-launch` (`classic-world`) and `gms-v83` (`gms`). When several
|
|
56
56
|
reads share one release, keep using `openRelease` and reuse the opened release client.
|
|
57
57
|
|
|
58
|
+
### Monster sprites
|
|
59
|
+
|
|
60
|
+
Monster catalog contract `2.1` gives every row a `sprite` fact: when `known`, its value
|
|
61
|
+
is the content ID (PNG SHA-256) of the `monster.sprite` image that the same build's Media
|
|
62
|
+
product selects. `monsterSpriteUrl` turns it into the blob URL under the release root, so
|
|
63
|
+
a browse page needs no Media catalog, Media detail pack or second product:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { getMonsterBrowse, hasMonsterSprites, monsterSpriteUrl } from '@mapletip/data/monsters';
|
|
67
|
+
|
|
68
|
+
const catalog = await getMonsterBrowse({ baseUrl: releaseRootUrl, pin: releasePin, buildId: 'gms-v83' });
|
|
69
|
+
if (!hasMonsterSprites(catalog)) throw new Error('this pin predates Monster sprites');
|
|
70
|
+
const rows = catalog.records.map((row) => ({
|
|
71
|
+
key: row.key,
|
|
72
|
+
spriteUrl: row.sprite.state === 'known' ? monsterSpriteUrl(releaseRootUrl, row.sprite.value) : undefined,
|
|
73
|
+
}));
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `sprite` state mirrors Media exactly: `unknown` (for example, a monster with only
|
|
77
|
+
stub frames), `absent` with its coverage key, or `unsupported`. Never substitute a
|
|
78
|
+
placeholder or another build's image for a non-known state. Release assembly fails unless
|
|
79
|
+
every row agrees with Media and every known content ID names a blob in the release.
|
|
80
|
+
`monsterSpriteUrl` throws `SCHEMA_VALIDATION_ERROR` for anything but a lowercase 64-hex
|
|
81
|
+
digest. `getMonsterBrowse` still requests only the release manifest named by the pin, the
|
|
82
|
+
build index, the product manifest and the Monster catalog.
|
|
83
|
+
|
|
84
|
+
Releases built before revision `2.1` carry contract-`2.0` catalogs without `sprite`; this
|
|
85
|
+
SDK reads both, and `hasMonsterSprites` narrows a catalog to `2.1`. `@mapletip/data`
|
|
86
|
+
`0.2.0` and older reject a `2.1` catalog with a typed `SCHEMA_VALIDATION_ERROR` instead of
|
|
87
|
+
returning rows without sprites, so upgrade to `0.3.0` before adopting a pin that carries
|
|
88
|
+
it.
|
|
89
|
+
|
|
58
90
|
`pin` is the parsed `pin.json` published next to the release root by whoever assembled
|
|
59
91
|
the release: it names the manifest path, SHA-256 and byte length and is the caller's
|
|
60
92
|
trust anchor. The SDK never resolves a "current" release on its own and never fetches a
|
|
@@ -65,6 +97,54 @@ The same import runs in Node (SSR, prerendering, scripts) and in browsers using
|
|
|
65
97
|
(HTTPS or localhost) because SHA-256 verification has no fallback. Rendering mode,
|
|
66
98
|
routing, caching and UI adaptation stay with the consumer.
|
|
67
99
|
|
|
100
|
+
## Items browse
|
|
101
|
+
|
|
102
|
+
`getItemBrowse` reads one build's verified Item browse roster in a single call:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { getItemBrowse, itemIconUrl } from '@mapletip/data/items';
|
|
106
|
+
|
|
107
|
+
const browse = await getItemBrowse({
|
|
108
|
+
baseUrl: releaseRootUrl,
|
|
109
|
+
pin: releasePin,
|
|
110
|
+
buildId: 'gms-v83',
|
|
111
|
+
signal: abortController.signal,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const row of browse.records) {
|
|
115
|
+
console.log(row.key, row.name.state === 'known' ? row.name.value : row.key);
|
|
116
|
+
if (row.icon.state === 'known') image.src = itemIconUrl(releaseRootUrl, row.icon.value);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It is exactly `openRelease(options)` → `openItemBuild(options.buildId)` → `loadBrowse()`:
|
|
121
|
+
it adds no cache, no fallback build selection and no latest lookup. The read is
|
|
122
|
+
build-scoped: it fetches only the browse packs named by that build's Item product
|
|
123
|
+
manifest, and a build ID that the pinned release does not carry rejects with
|
|
124
|
+
`UNKNOWN_BUILD` instead of falling back to another build. Each row is the compact
|
|
125
|
+
Rust-owned browse record plus its stable public `key`; `scope` and `coverage` come from
|
|
126
|
+
the verified packs, every pack must agree on `coverage`, and the roster must match the
|
|
127
|
+
manifest's `totalEntityCount`, so a short roster never resolves. `fetch`, `signal`,
|
|
128
|
+
`onRequest` and the validated `preloaded` release metadata behave exactly as they do on
|
|
129
|
+
`openRelease`.
|
|
130
|
+
|
|
131
|
+
Omitted row fields are coverage claims, not zeroes:
|
|
132
|
+
|
|
133
|
+
- An omitted relationship count means zero recorded entities when
|
|
134
|
+
`coverage.relationships[key]` is `complete`. `unsupported` is not zero: it means the
|
|
135
|
+
producer cannot supply that relationship for this build. `partial` means the recorded
|
|
136
|
+
count is a lower bound, so `monster-drop` on `gms-v83` is `partial`.
|
|
137
|
+
- An omitted `scroll.tier` is unresolved, not absent, when `coverage.scrollTier` is
|
|
138
|
+
`complete`; an omitted `scroll.cursed` means the Scroll has no cursed property.
|
|
139
|
+
- `scroll.targets` are verified identities. `scroll.targetLabel` is a label derived from
|
|
140
|
+
the Scroll's name and is never an identity.
|
|
141
|
+
|
|
142
|
+
`itemIconUrl(baseUrl, contentId)` resolves a Media blob through the same core path
|
|
143
|
+
resolver as `resolveMediaUrl` and returns `<baseUrl>/media/images/<ab>/<contentId>.png`.
|
|
144
|
+
It rejects any content ID that is not a 64-character lowercase SHA-256 digest with
|
|
145
|
+
`SCHEMA_VALIDATION_ERROR`, so a row's `icon.value` can be used without building a
|
|
146
|
+
make-believe descriptor.
|
|
147
|
+
|
|
68
148
|
## What each entry loads
|
|
69
149
|
|
|
70
150
|
Monster catalog validation and the release/manifest/index validators are imported
|
|
@@ -73,12 +153,13 @@ operation that needs it, as a separate on-demand chunk:
|
|
|
73
153
|
|
|
74
154
|
| Entry | Eager generated validators | Loaded on demand |
|
|
75
155
|
| --- | --- | --- |
|
|
76
|
-
| `/monsters` | release pin, release manifest, build index, product manifest, Monster catalog 1.x and 2.
|
|
77
|
-
| `/items
|
|
156
|
+
| `/monsters` | release pin, release manifest, build index, product manifest, Monster catalog 1.x, 2.0 and 2.1 | Monster detail, location/relationship, core-relationship and evidence packs |
|
|
157
|
+
| `/items` | product manifest only (every Item validator is on demand) | browse, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` |
|
|
158
|
+
| `/equipment` | that domain's catalog, detail, relationship and evidence packs | — |
|
|
78
159
|
| `/quests` | none (Quest validators are always on demand) | catalog, browse, detail, relationship, auxiliary and evidence packs |
|
|
79
160
|
| `/media` | that domain's catalog, detail packs | — |
|
|
80
161
|
| `/release` | release pin, release manifest, build index, product manifest, Monster catalog (Monster is the release client's build selector) | Item, Equipment, Media, Quest and static-domain clients |
|
|
81
|
-
| `.` and `/client` | every domain's eager validators (compatibility view) | Quest and Monster detail/relationship/evidence validators |
|
|
162
|
+
| `.` and `/client` | every domain's eager validators (compatibility view) | every Item validator, plus Quest and Monster detail/relationship/evidence validators |
|
|
82
163
|
|
|
83
164
|
A Monster-browse-only browser consumer therefore ships the release and Monster catalog
|
|
84
165
|
code and nothing else; the repository's `tools/benchmark-bundle.mjs` measures it against
|
package/client/core.d.ts
CHANGED
|
@@ -118,7 +118,13 @@ export declare class MissingPageError extends StaticClientError {
|
|
|
118
118
|
}
|
|
119
119
|
export declare function normalizedBaseUrl(value: string | URL): URL;
|
|
120
120
|
export declare function verifyBlob(bytes: Uint8Array, descriptor: IntegrityDescriptor): Promise<void>;
|
|
121
|
+
export declare function resolveReleasePath(baseUrl: URL | string, path: string): string;
|
|
121
122
|
export declare function resolveMediaUrl(baseUrl: URL | string, descriptor: IntegrityDescriptor): string;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a Media content ID (the PNG's lowercase SHA-256 digest) to its blob URL under
|
|
125
|
+
* the release root. `subject` names the caller's field in the validation error.
|
|
126
|
+
*/
|
|
127
|
+
export declare function resolveMediaContentUrl(baseUrl: URL | string, contentId: string, subject: string): string;
|
|
122
128
|
export declare function assertCompatibleManifest(manifest: ConnectedProductManifest | ReleaseManifest): number;
|
|
123
129
|
export declare function createJsonFetcher(options: {
|
|
124
130
|
readonly baseUrl: URL;
|
package/client/core.js
CHANGED
|
@@ -120,9 +120,22 @@ export async function verifyBlob(bytes, descriptor) {
|
|
|
120
120
|
throw new IntegrityError(`Byte length mismatch for ${descriptor.path}: expected ${descriptor.byteLength}, got ${bytes.byteLength}`);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
export function
|
|
123
|
+
export function resolveReleasePath(baseUrl, path) {
|
|
124
124
|
const base = baseUrl instanceof URL ? baseUrl : normalizedBaseUrl(baseUrl);
|
|
125
|
-
return resolveDescriptorUrl(base,
|
|
125
|
+
return resolveDescriptorUrl(base, path).toString();
|
|
126
|
+
}
|
|
127
|
+
export function resolveMediaUrl(baseUrl, descriptor) {
|
|
128
|
+
return resolveReleasePath(baseUrl, descriptor.path);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a Media content ID (the PNG's lowercase SHA-256 digest) to its blob URL under
|
|
132
|
+
* the release root. `subject` names the caller's field in the validation error.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveMediaContentUrl(baseUrl, contentId, subject) {
|
|
135
|
+
if (!/^[0-9a-f]{64}$/.test(contentId)) {
|
|
136
|
+
throw new SchemaValidationError(contentId, `${subject} content ID must be a SHA-256 hex digest`);
|
|
137
|
+
}
|
|
138
|
+
return resolveReleasePath(baseUrl, `media/images/${contentId.slice(0, 2)}/${contentId}.png`);
|
|
126
139
|
}
|
|
127
140
|
function parseJson(bytes, path) {
|
|
128
141
|
try {
|
package/client/items.d.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mapletip/data/items` — Item browse and Item product operations.
|
|
3
|
+
*
|
|
4
|
+
* Every Item validator is loaded on the first operation that needs it and the release
|
|
5
|
+
* client is imported on demand, so a browse-only consumer never downloads catalog,
|
|
6
|
+
* detail, relationship, evidence validators or the Monster client.
|
|
7
|
+
*/
|
|
8
|
+
import type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord } from '../generated/item-browse-pack-2.d.ts';
|
|
1
9
|
import type { ItemCatalog } from '../generated/item-catalog-2.d.ts';
|
|
2
10
|
import type { ItemDetailRecord } from '../generated/item-detail-2.d.ts';
|
|
3
11
|
import type { ItemRelationshipPage } from '../generated/item-relationship-pack-2.d.ts';
|
|
4
12
|
import type { ItemEvidencePage } from '../generated/item-evidence-pack-2.d.ts';
|
|
5
|
-
import
|
|
13
|
+
import type { OpenReleaseOptions } from './release.js';
|
|
14
|
+
import { type GetRelationshipOptions, type OpenProductOptions, type OperationOptions, type ProductScope, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
6
15
|
export type StaticItemCatalog = ItemCatalog;
|
|
7
16
|
export type StaticItemDetail = ItemDetailRecord;
|
|
8
17
|
export type { GetRelationshipOptions };
|
|
18
|
+
export type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord };
|
|
19
|
+
export type { OpenReleaseOptions };
|
|
20
|
+
/** One browse row: the compact record plus its stable public key. */
|
|
21
|
+
export type ItemBrowseRow = ItemBrowseRecord & {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
};
|
|
24
|
+
/** The whole verified browse roster of one Item build. */
|
|
25
|
+
export interface StaticItemBrowse {
|
|
26
|
+
readonly scope: ProductScope;
|
|
27
|
+
readonly coverage: ItemBrowseCoverage;
|
|
28
|
+
readonly records: readonly ItemBrowseRow[];
|
|
29
|
+
}
|
|
9
30
|
export interface StaticItemProductClient {
|
|
10
31
|
readonly manifest: StaticProductManifest;
|
|
11
32
|
loadCatalog(options?: OperationOptions): Promise<StaticItemCatalog>;
|
|
33
|
+
loadBrowse(options?: OperationOptions): Promise<StaticItemBrowse>;
|
|
12
34
|
getDetail(key: string, options?: OperationOptions): Promise<StaticItemDetail>;
|
|
13
35
|
getRelationshipPage(pageKey: string, options?: GetRelationshipOptions): Promise<ItemRelationshipPage>;
|
|
14
36
|
getEvidencePage(pageKey: string, options?: OperationOptions): Promise<ItemEvidencePage>;
|
|
@@ -20,3 +42,17 @@ export declare function openItemProduct(options: OpenProductOptions): Promise<St
|
|
|
20
42
|
export declare function isItemProductClient(client: {
|
|
21
43
|
readonly manifest: StaticProductManifest;
|
|
22
44
|
}): client is StaticItemProductClient;
|
|
45
|
+
/** Options for the one-shot Item browse read: an opened release plus the build to read. */
|
|
46
|
+
export type GetItemBrowseOptions = OpenReleaseOptions & {
|
|
47
|
+
readonly buildId: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Read one build's verified Item browse roster in a single call.
|
|
51
|
+
*
|
|
52
|
+
* Equivalent to `openRelease(options)` → `openItemBuild(options.buildId)` →
|
|
53
|
+
* `loadBrowse()`; it adds no caching, fallback or alternate data path. The release
|
|
54
|
+
* client is imported on demand so the Item browse entry stays narrow.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getItemBrowse(options: GetItemBrowseOptions): Promise<StaticItemBrowse>;
|
|
57
|
+
/** Resolve one Item icon blob URL from its content-addressed media digest. */
|
|
58
|
+
export declare function itemIconUrl(baseUrl: URL | string, contentId: string): string;
|
package/client/items.js
CHANGED
|
@@ -1,41 +1,98 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedCapabilityError, UnsupportedProductError, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, resolveMediaContentUrl, } from './core.js';
|
|
2
|
+
const loadBrowsePackValidator = lazyValidator(() => import('../generated/item-browse-pack-2.validator.js'));
|
|
3
|
+
const loadCatalogValidator = lazyValidator(() => import('../generated/item-catalog-2.validator.js'));
|
|
4
|
+
const loadDetailValidator = lazyValidator(() => import('../generated/item-detail-2.validator.js'));
|
|
5
|
+
const loadDetailPackValidator = lazyValidator(() => import('../generated/item-pack-2.validator.js'));
|
|
6
|
+
const loadRelationshipPackValidator = lazyValidator(() => import('../generated/item-relationship-pack-2.validator.js'));
|
|
7
|
+
const loadEvidencePackValidator = lazyValidator(() => import('../generated/item-evidence-pack-2.validator.js'));
|
|
7
8
|
/** Build an Item client over an already verified product manifest. */
|
|
8
9
|
export function itemProductClient(context) {
|
|
9
10
|
const { manifest, fetchEnvelope } = context;
|
|
10
11
|
if (manifest.productId !== 'items')
|
|
11
12
|
throw new UnsupportedProductError(manifest.productId);
|
|
12
13
|
let catalogPromise;
|
|
14
|
+
let browsePromise;
|
|
13
15
|
const detailPackCache = new Map();
|
|
14
16
|
const relationshipPackCache = new Map();
|
|
15
17
|
const evidencePackCache = new Map();
|
|
16
18
|
async function loadCatalog(operation) {
|
|
17
19
|
if (!catalogPromise) {
|
|
18
|
-
catalogPromise =
|
|
20
|
+
catalogPromise = (async () => {
|
|
21
|
+
const validator = await loadCatalogValidator();
|
|
22
|
+
return fetchEnvelope(manifest.catalog, 'catalog', validator, operation);
|
|
23
|
+
})();
|
|
19
24
|
void catalogPromise.catch(() => { catalogPromise = undefined; });
|
|
20
25
|
}
|
|
21
26
|
return catalogPromise;
|
|
22
27
|
}
|
|
28
|
+
async function loadBrowse(operation) {
|
|
29
|
+
const descriptors = manifest.browsePacks;
|
|
30
|
+
if (!descriptors || descriptors.length === 0) {
|
|
31
|
+
throw new UnsupportedCapabilityError('Item product manifest does not provide browse packs');
|
|
32
|
+
}
|
|
33
|
+
if (!browsePromise) {
|
|
34
|
+
browsePromise = (async () => {
|
|
35
|
+
const validator = await loadBrowsePackValidator();
|
|
36
|
+
const packs = await Promise.all(descriptors.map((descriptor) => (fetchEnvelope(descriptor, 'browse', validator, operation))));
|
|
37
|
+
const rows = [];
|
|
38
|
+
let previousKey;
|
|
39
|
+
const coverage = packs[0].coverage;
|
|
40
|
+
const coverageJson = JSON.stringify(coverage);
|
|
41
|
+
for (const [index, pack] of packs.entries()) {
|
|
42
|
+
const descriptor = descriptors[index];
|
|
43
|
+
const keys = Object.keys(pack.records);
|
|
44
|
+
if (keys.length !== descriptor.count) {
|
|
45
|
+
throw new SchemaValidationError(descriptor.path, `browse pack has ${keys.length} records but its descriptor declares ${descriptor.count}`);
|
|
46
|
+
}
|
|
47
|
+
const packCoverageJson = JSON.stringify(pack.coverage);
|
|
48
|
+
if (packCoverageJson !== coverageJson) {
|
|
49
|
+
throw new SchemaValidationError(descriptor.path, 'browse pack coverage does not match the first browse pack');
|
|
50
|
+
}
|
|
51
|
+
if (keys[0] !== descriptor.firstKey || keys.at(-1) !== descriptor.lastKey) {
|
|
52
|
+
throw new SchemaValidationError(descriptor.path, `browse pack keys do not match the descriptor range ${descriptor.firstKey}..${descriptor.lastKey}`);
|
|
53
|
+
}
|
|
54
|
+
// One strictly increasing walk across packs rejects duplicate keys and
|
|
55
|
+
// overlapping or out-of-order ranges.
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const record = pack.records[key];
|
|
58
|
+
if (!record) {
|
|
59
|
+
throw new SchemaValidationError(descriptor.path, `browse pack is missing record ${key}`);
|
|
60
|
+
}
|
|
61
|
+
if (previousKey !== undefined && key <= previousKey) {
|
|
62
|
+
throw new SchemaValidationError(descriptor.path, `browse record key ${key} is out of order`);
|
|
63
|
+
}
|
|
64
|
+
previousKey = key;
|
|
65
|
+
rows.push({ ...record, key });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (rows.length !== manifest.totalEntityCount) {
|
|
69
|
+
throw new SchemaValidationError(manifest.catalog.path, `browse roster has ${rows.length} rows but the manifest declares ${manifest.totalEntityCount}`);
|
|
70
|
+
}
|
|
71
|
+
return { scope: manifest.scope, coverage, records: rows };
|
|
72
|
+
})();
|
|
73
|
+
void browsePromise.catch(() => { browsePromise = undefined; });
|
|
74
|
+
}
|
|
75
|
+
return browsePromise;
|
|
76
|
+
}
|
|
23
77
|
async function getDetail(key, operation) {
|
|
24
78
|
const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Item');
|
|
25
|
-
const
|
|
79
|
+
const packValidator = await loadDetailPackValidator();
|
|
80
|
+
const pack = await memoized(detailPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'detail', packValidator, operation)));
|
|
26
81
|
if (!Object.hasOwn(pack.records, key))
|
|
27
82
|
throw new MissingRecordError(key, 'Item');
|
|
28
83
|
const detail = pack.records[key];
|
|
29
84
|
if (!detail)
|
|
30
85
|
throw new MissingRecordError(key, 'Item');
|
|
31
|
-
|
|
32
|
-
|
|
86
|
+
const validator = await loadDetailValidator();
|
|
87
|
+
if (!validator(detail)) {
|
|
88
|
+
throw new SchemaValidationError(descriptor.path, JSON.stringify(validator.errors ?? []));
|
|
33
89
|
}
|
|
34
90
|
return detail;
|
|
35
91
|
}
|
|
36
92
|
async function getRelationshipPage(pageKey, operation) {
|
|
37
93
|
const descriptor = rangeDescriptor(manifest.relationshipPacks, pageKey, 'relationship');
|
|
38
|
-
const
|
|
94
|
+
const validator = await loadRelationshipPackValidator();
|
|
95
|
+
const pack = await memoized(relationshipPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'relationship', validator, operation)));
|
|
39
96
|
const page = pack.records[pageKey];
|
|
40
97
|
if (!page)
|
|
41
98
|
throw new MissingPageError(pageKey, 'relationship');
|
|
@@ -46,13 +103,14 @@ export function itemProductClient(context) {
|
|
|
46
103
|
}
|
|
47
104
|
async function getEvidencePage(pageKey, operation) {
|
|
48
105
|
const descriptor = rangeDescriptor(manifest.evidencePacks ?? [], pageKey, 'evidence');
|
|
49
|
-
const
|
|
106
|
+
const validator = await loadEvidencePackValidator();
|
|
107
|
+
const pack = await memoized(evidencePackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'evidence', validator, operation)));
|
|
50
108
|
const page = pack.records[pageKey];
|
|
51
109
|
if (!page)
|
|
52
110
|
throw new MissingPageError(pageKey, 'evidence');
|
|
53
111
|
return page;
|
|
54
112
|
}
|
|
55
|
-
return { manifest, loadCatalog, getDetail, getRelationshipPage, getEvidencePage };
|
|
113
|
+
return { manifest, loadCatalog, loadBrowse, getDetail, getRelationshipPage, getEvidencePage };
|
|
56
114
|
}
|
|
57
115
|
/** Open one pinned Item product. */
|
|
58
116
|
export async function openItemProduct(options) {
|
|
@@ -61,3 +119,20 @@ export async function openItemProduct(options) {
|
|
|
61
119
|
export function isItemProductClient(client) {
|
|
62
120
|
return client.manifest.productId === 'items';
|
|
63
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Read one build's verified Item browse roster in a single call.
|
|
124
|
+
*
|
|
125
|
+
* Equivalent to `openRelease(options)` → `openItemBuild(options.buildId)` →
|
|
126
|
+
* `loadBrowse()`; it adds no caching, fallback or alternate data path. The release
|
|
127
|
+
* client is imported on demand so the Item browse entry stays narrow.
|
|
128
|
+
*/
|
|
129
|
+
export async function getItemBrowse(options) {
|
|
130
|
+
const { openRelease } = await import('./release.js');
|
|
131
|
+
const release = await openRelease(options);
|
|
132
|
+
const items = await release.openItemBuild(options.buildId);
|
|
133
|
+
return items.loadBrowse({ signal: options.signal });
|
|
134
|
+
}
|
|
135
|
+
/** Resolve one Item icon blob URL from its content-addressed media digest. */
|
|
136
|
+
export function itemIconUrl(baseUrl, contentId) {
|
|
137
|
+
return resolveMediaContentUrl(baseUrl, contentId, 'Item icon');
|
|
138
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { MonsterCatalog } from '../generated/monster-catalog.d.ts';
|
|
10
10
|
import type { MonsterCatalogV2 } from '../generated/monster-catalog-2.d.ts';
|
|
11
|
+
import type { MonsterCatalogV2_1 } from '../generated/monster-catalog-2-1.d.ts';
|
|
11
12
|
import type { MonsterDetailRecord, PageDescriptor } from '../generated/monster-detail.d.ts';
|
|
12
13
|
import type { MonsterDetailRecordV2 } from '../generated/monster-detail-2.d.ts';
|
|
13
14
|
import type { MonsterEvidencePage } from '../generated/monster-evidence-pack.d.ts';
|
|
@@ -15,7 +16,11 @@ import type { MonsterEvidencePageV2 } from '../generated/monster-evidence-pack-2
|
|
|
15
16
|
import type { MonsterLocationOccurrence } from '../generated/monster-relationship-pack.d.ts';
|
|
16
17
|
import type { MonsterCoreRelationshipPackV2 } from '../generated/monster-core-relationship-pack-2.d.ts';
|
|
17
18
|
import { type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A verified Monster catalog. Contract 2.1 records carry `sprite`, a Media content ID
|
|
21
|
+
* fact for the monster's representative image; 2.0 and 1.x records have none.
|
|
22
|
+
*/
|
|
23
|
+
export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1;
|
|
19
24
|
export type StaticMonsterDetail = MonsterDetailRecord | MonsterDetailRecordV2;
|
|
20
25
|
export type StaticMonsterEvidencePage = MonsterEvidencePage | MonsterEvidencePageV2;
|
|
21
26
|
export interface StaticProductClient {
|
package/client/monster-client.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import validateCatalog from '../generated/monster-catalog.validator.js';
|
|
2
2
|
import validateCatalogV2 from '../generated/monster-catalog-2.validator.js';
|
|
3
|
+
import validateCatalogV2_1 from '../generated/monster-catalog-2-1.validator.js';
|
|
3
4
|
import { MissingPageError, MissingRecordError, SchemaValidationError, UnsupportedProductError, assertEnvelopeVersion, assertSameScope, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, } from './core.js';
|
|
5
|
+
/**
|
|
6
|
+
* Contract-2 catalog validator that picks the revision's strict schema from the document's
|
|
7
|
+
* own `contractVersion`. Unsupported revisions fail the 2.0 schema with a typed error.
|
|
8
|
+
*/
|
|
9
|
+
const validateCatalogRevision = Object.assign((data) => {
|
|
10
|
+
const validator = (data?.contractVersion === '2.1'
|
|
11
|
+
? validateCatalogV2_1
|
|
12
|
+
: validateCatalogV2);
|
|
13
|
+
const valid = validator(data);
|
|
14
|
+
validateCatalogRevision.errors = validator.errors;
|
|
15
|
+
return valid;
|
|
16
|
+
}, { errors: undefined });
|
|
4
17
|
const loadDetailPack = lazyValidator(() => import('../generated/monster-pack.validator.js'));
|
|
5
18
|
const loadDetail = lazyValidator(() => import('../generated/monster-detail.validator.js'));
|
|
6
19
|
const loadDetailPackV2 = lazyValidator(() => import('../generated/monster-pack-2.validator.js'));
|
|
@@ -26,7 +39,7 @@ export function monsterProductClient(context) {
|
|
|
26
39
|
async function loadCatalog(operation) {
|
|
27
40
|
if (!catalogPromise) {
|
|
28
41
|
catalogPromise = contract2
|
|
29
|
-
? fetchEnvelope(manifest.catalog, 'catalog',
|
|
42
|
+
? fetchEnvelope(manifest.catalog, 'catalog', validateCatalogRevision, operation)
|
|
30
43
|
: fetchEnvelope(manifest.catalog, 'catalog', validateCatalog, operation);
|
|
31
44
|
void catalogPromise.catch(() => { catalogPromise = undefined; });
|
|
32
45
|
}
|
package/client/monsters.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* `@mapletip/data/monsters` — Monster browse and Monster product operations.
|
|
3
|
-
*
|
|
4
|
-
* This entry pulls the verified release core and the Monster client (with its catalog
|
|
5
|
-
* validators) and nothing else: Item, Equipment, Media, Quest, static-domain code and
|
|
6
|
-
* Monster detail/relationship/evidence validators stay out of the initial graph.
|
|
7
|
-
*/
|
|
1
|
+
import type { MonsterCatalogV2_1 } from '../generated/monster-catalog-2-1.d.ts';
|
|
8
2
|
import type { StaticMonsterCatalog } from './monster-client.js';
|
|
9
3
|
import { type OpenReleaseOptions } from './release.js';
|
|
10
4
|
export { isMonsterProductClient, openMonsterProduct } from './monster-client.js';
|
|
11
5
|
export type { StaticMonsterCatalog, StaticMonsterDetail, StaticMonsterEvidencePage, StaticProductClient, } from './monster-client.js';
|
|
6
|
+
export type { MonsterCatalogV2_1 };
|
|
12
7
|
export type { OpenReleaseOptions };
|
|
13
8
|
/** Options for the one-shot Monster catalog read: an opened release plus the build to read. */
|
|
14
9
|
export type GetMonsterBrowseOptions = OpenReleaseOptions & {
|
|
@@ -22,3 +17,16 @@ export type GetMonsterBrowseOptions = OpenReleaseOptions & {
|
|
|
22
17
|
* lower-level release client when several domain reads share one release.
|
|
23
18
|
*/
|
|
24
19
|
export declare function getMonsterBrowse(options: GetMonsterBrowseOptions): Promise<StaticMonsterCatalog>;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve one Monster sprite blob URL from a catalog row's known `sprite` content ID.
|
|
22
|
+
*
|
|
23
|
+
* The blob lives under the same release root as the catalog, so no Media product is
|
|
24
|
+
* opened. Throws `SchemaValidationError` for anything but a lowercase SHA-256 digest.
|
|
25
|
+
*/
|
|
26
|
+
export declare function monsterSpriteUrl(baseUrl: URL | string, contentId: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Narrow a verified catalog to contract 2.1, whose every row carries `sprite`. Earlier
|
|
29
|
+
* pins return 1.x or 2.0 catalogs without it; the loose 1.x type keeps a plain field
|
|
30
|
+
* check from narrowing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function hasMonsterSprites(catalog: StaticMonsterCatalog): catalog is MonsterCatalogV2_1;
|
package/client/monsters.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@mapletip/data/monsters` — Monster browse and Monster product operations.
|
|
3
|
+
*
|
|
4
|
+
* This entry pulls the verified release core and the Monster client (with its catalog
|
|
5
|
+
* validators) and nothing else: Item, Equipment, Media, Quest, static-domain code and
|
|
6
|
+
* Monster detail/relationship/evidence validators stay out of the initial graph.
|
|
7
|
+
*/
|
|
8
|
+
import { resolveMediaContentUrl } from './core.js';
|
|
1
9
|
import { openRelease } from './release.js';
|
|
2
10
|
export { isMonsterProductClient, openMonsterProduct } from './monster-client.js';
|
|
3
11
|
/**
|
|
@@ -12,3 +20,20 @@ export async function getMonsterBrowse(options) {
|
|
|
12
20
|
const monster = await release.openBuild(options.buildId);
|
|
13
21
|
return monster.loadCatalog({ signal: options.signal });
|
|
14
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve one Monster sprite blob URL from a catalog row's known `sprite` content ID.
|
|
25
|
+
*
|
|
26
|
+
* The blob lives under the same release root as the catalog, so no Media product is
|
|
27
|
+
* opened. Throws `SchemaValidationError` for anything but a lowercase SHA-256 digest.
|
|
28
|
+
*/
|
|
29
|
+
export function monsterSpriteUrl(baseUrl, contentId) {
|
|
30
|
+
return resolveMediaContentUrl(baseUrl, contentId, 'Monster sprite');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Narrow a verified catalog to contract 2.1, whose every row carries `sprite`. Earlier
|
|
34
|
+
* pins return 1.x or 2.0 catalogs without it; the loose 1.x type keeps a plain field
|
|
35
|
+
* check from narrowing.
|
|
36
|
+
*/
|
|
37
|
+
export function hasMonsterSprites(catalog) {
|
|
38
|
+
return catalog.format === 'mapletip.monster-catalog-2' && catalog.contractVersion === '2.1';
|
|
39
|
+
}
|
|
@@ -17,8 +17,8 @@ export { ContractVersionError, IntegrityError, InvalidJsonError, InvalidPathErro
|
|
|
17
17
|
export type { GetRelationshipOptions, IntegrityDescriptor, OpenProductOptions, OperationOptions, ProductIdentity, ProductManifestDescriptor, ProductScope, RequestKind, RequestLogEntry, RequestLogHook, StaticProductManifest, } from './core.js';
|
|
18
18
|
export { isMonsterProductClient, openMonsterProduct } from './monster-client.js';
|
|
19
19
|
export type { StaticMonsterCatalog, StaticMonsterDetail, StaticMonsterEvidencePage, StaticProductClient, } from './monster-client.js';
|
|
20
|
-
export { isItemProductClient, openItemProduct } from './items.js';
|
|
21
|
-
export type { StaticItemCatalog, StaticItemDetail, StaticItemProductClient } from './items.js';
|
|
20
|
+
export { getItemBrowse, isItemProductClient, itemIconUrl, openItemProduct } from './items.js';
|
|
21
|
+
export type { GetItemBrowseOptions, ItemBrowseCoverage, ItemBrowsePack, ItemBrowseRecord, ItemBrowseRow, StaticItemBrowse, StaticItemCatalog, StaticItemDetail, StaticItemProductClient, } from './items.js';
|
|
22
22
|
export { isEquipmentProductClient, openEquipmentProduct } from './equipment.js';
|
|
23
23
|
export type { StaticEquipmentCatalog, StaticEquipmentDetail, StaticEquipmentProductClient, } from './equipment.js';
|
|
24
24
|
export { isMediaProductClient, openMediaProduct } from './media.js';
|
|
@@ -28,7 +28,7 @@ export type { QuestAuxiliaryPack, QuestAuxiliaryPage, QuestBrowsePack, QuestBrow
|
|
|
28
28
|
export type { StaticDomainCatalog, StaticDomainCatalogRecord, StaticDomainDetailRecord, StaticDomainFactState, StaticDomainProductClient, StaticDomainSourceIdentity, } from './static-domains.js';
|
|
29
29
|
export { openRelease, resolveTargetAvailability } from './release.js';
|
|
30
30
|
export type { OpenReleaseOptions, ReleaseProductDescriptor, StaticReleaseBuild, StaticReleaseClient, TargetAvailability, } from './release.js';
|
|
31
|
-
export { getMonsterBrowse } from './monsters.js';
|
|
31
|
+
export { getMonsterBrowse, hasMonsterSprites, monsterSpriteUrl } from './monsters.js';
|
|
32
32
|
export type { GetMonsterBrowseOptions } from './monsters.js';
|
|
33
33
|
export type AnyProductClient = StaticProductClient | StaticMediaProductClient | StaticQuestProductClient | StaticItemProductClient | StaticEquipmentProductClient | StaticDomainProductClient;
|
|
34
34
|
/**
|
package/client/static-client.js
CHANGED
|
@@ -15,12 +15,12 @@ import { questProductClient } from './quests.js';
|
|
|
15
15
|
import { staticDomainProductClient } from './static-domains.js';
|
|
16
16
|
export { ContractVersionError, IntegrityError, InvalidJsonError, InvalidPathError, MissingPageError, MissingRecordError, RequestError, SchemaValidationError, ScopeError, SecureContextRequiredError, StaticClientError, UnknownBuildError, UnsupportedCapabilityError, UnsupportedProductError, resolveMediaUrl, verifyBlob, } from './core.js';
|
|
17
17
|
export { isMonsterProductClient, openMonsterProduct } from './monster-client.js';
|
|
18
|
-
export { isItemProductClient, openItemProduct } from './items.js';
|
|
18
|
+
export { getItemBrowse, isItemProductClient, itemIconUrl, openItemProduct } from './items.js';
|
|
19
19
|
export { isEquipmentProductClient, openEquipmentProduct } from './equipment.js';
|
|
20
20
|
export { isMediaProductClient, openMediaProduct } from './media.js';
|
|
21
21
|
export { isQuestProductClient, openQuestProduct } from './quests.js';
|
|
22
22
|
export { openRelease, resolveTargetAvailability } from './release.js';
|
|
23
|
-
export { getMonsterBrowse } from './monsters.js';
|
|
23
|
+
export { getMonsterBrowse, hasMonsterSprites, monsterSpriteUrl } from './monsters.js';
|
|
24
24
|
/**
|
|
25
25
|
* Open one pinned product of any supported domain.
|
|
26
26
|
*
|