cajonzero 0.1.2 → 0.1.3

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.
@@ -1,4 +1,4 @@
1
- import { T as TimestampedClientInput, N as NetPublic, O as NetSecret } from './providers-BX7e4pNW.js';
1
+ import { T as TimestampedClientInput, N as NetPublic, O as NetSecret } from './providers-BwJKzGkd.js';
2
2
 
3
3
  /**
4
4
  * cajonzero — mudb protocol envelope (message-id tables + `raw` channel).
package/dist/core.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { M as MapEntry, W as WorldSeed } from './providers-BX7e4pNW.js';
2
- export { c as EMPTY_MAP_CATALOG, E as EnvProvider, d as MapCatalog, e as Repos, R as Room, f as RoomOptions, g as RuntimeProviders, h as SETTINGS, i as SETTING_KEYS, a as SettingDefaults, j as SettingDefinition, k as Settings, l as SettingsError, m as SettingsErrorCode, n as SettingsService, o as StorageFactory, S as StorageSource, p as Store, q as WorldSeedReader, r as configureDefaultKV, s as createMemoryStore, t as createRepos, u as createSettingsService, v as createStore, w as findMap, x as getRuntimeProviders, y as isSettingKey, z as memoryDriver, A as parseMapCatalog, B as resolveAdminToken, C as resolveEnvironment, F as resolveRooms, G as resolveSessionSecret, H as resolveSettingDefaults, I as resolveStore, J as setRuntimeProviders, K as setStorageFactory } from './providers-BX7e4pNW.js';
3
- export { C as CajonzeroConfig, D as DEFAULT_CONFIG, G as GAME_NET_INBOUND_BASE, a as GAME_NET_OUTBOUND_BASE, I as INBOUND_LAYOUT, M as MAX_INBOUND_FRAME_BYTES, b as MapsConfig, N as NetCodec, c as NetCodecError, d as NetCodecErrorCode, e as NetCodecHandlers, f as NetCodecOptions, O as OFFICIAL_PREFIX, g as OUTBOUND_LAYOUT, P as ProtocolDirection, h as ProtocolLayout, i as ProtocolSchemaLike, R as RoomConfig, j as chainLayouts, l as layoutMessages, k as layoutProtocol, m as matchesLayout, n as messageIds, o as messageNameAt, r as readNetFrameHeader, p as readNetMessageType, q as resolveConfig } from './config-BOzAyP7m.js';
1
+ import { M as MapEntry, W as WorldSeed } from './providers-BwJKzGkd.js';
2
+ export { c as EMPTY_MAP_CATALOG, E as EnvProvider, d as MapCatalog, e as Repos, R as Room, f as RoomOptions, g as RuntimeProviders, h as SETTINGS, i as SETTING_KEYS, a as SettingDefaults, j as SettingDefinition, k as Settings, l as SettingsError, m as SettingsErrorCode, n as SettingsService, o as StorageFactory, S as StorageSource, p as Store, q as WorldSeedReader, r as configureDefaultKV, s as createMemoryStore, t as createRepos, u as createSettingsService, v as createStore, w as findMap, x as getRuntimeProviders, y as isSettingKey, z as memoryDriver, A as parseMapCatalog, B as resolveAdminToken, C as resolveEnvironment, F as resolveRooms, G as resolveSessionSecret, H as resolveSettingDefaults, I as resolveStore, J as setRuntimeProviders, K as setStorageFactory } from './providers-BwJKzGkd.js';
3
+ export { C as CajonzeroConfig, D as DEFAULT_CONFIG, G as GAME_NET_INBOUND_BASE, a as GAME_NET_OUTBOUND_BASE, I as INBOUND_LAYOUT, M as MAX_INBOUND_FRAME_BYTES, b as MapsConfig, N as NetCodec, c as NetCodecError, d as NetCodecErrorCode, e as NetCodecHandlers, f as NetCodecOptions, O as OFFICIAL_PREFIX, g as OUTBOUND_LAYOUT, P as ProtocolDirection, h as ProtocolLayout, i as ProtocolSchemaLike, R as RoomConfig, j as chainLayouts, l as layoutMessages, k as layoutProtocol, m as matchesLayout, n as messageIds, o as messageNameAt, r as readNetFrameHeader, p as readNetMessageType, q as resolveConfig } from './config-B32NDXlr.js';
4
4
  import 'unstorage';
5
5
  import 'gl-matrix';
6
6
  import 'zod';
@@ -15,7 +15,9 @@ import 'zod';
15
15
  * browser loads those straight from the same root.
16
16
  *
17
17
  * The seed is resolved on demand from object storage: map bytes stay on the
18
- * CDN, never in the deployment KV.
18
+ * CDN, never in the deployment KV. When the map owner uploaded a precomputed
19
+ * `seed.json[.gz]` pack (`cj0 maps pack`), the whole graph is replaced by a
20
+ * single request — which is what keeps room startup inside Worker quotas.
19
21
  *
20
22
  * Worker path: `fetch`, `AbortController`, `TextDecoder` only.
21
23
  */
@@ -41,6 +43,11 @@ interface HttpMapOptions {
41
43
  maxDownloadBytes?: number;
42
44
  /** Parallel voxel-chunk reads. Default 8. */
43
45
  concurrency?: number;
46
+ /**
47
+ * Try the precomputed `<root>seed.json[.gz]` pack before resolving the full
48
+ * map graph. Default `true`; disable it to force a graph re-read.
49
+ */
50
+ preferSeedPack?: boolean;
44
51
  }
45
52
  /**
46
53
  * A {@link WorldSeedReader} over one unpacked map root.
@@ -66,9 +73,10 @@ declare class HttpMapSource {
66
73
  /**
67
74
  * Load one map entry into a resolved {@link WorldSeed}.
68
75
  *
69
- * Reads `bundle.json`/manifest and the project graph straight from object
70
- * storage. Nothing is cached server-side: map bytes belong on the CDN, not in
71
- * the deployment KV.
76
+ * Prefers the precomputed seed pack (`<root>seed.json[.gz]`) — one request and
77
+ * no graph traversal, which is what keeps room startup inside Worker quotas.
78
+ * Falls back to reading `bundle.json`/manifest and the project graph when no
79
+ * pack is present.
72
80
  */
73
81
  declare function loadWorldSeedFromHttp(entry: MapEntry, options?: HttpMapOptions): Promise<WorldSeed>;
74
82
 
package/dist/core.js CHANGED
@@ -5132,147 +5132,6 @@ async function consumeSessionProof(proof, secret, consumed, now) {
5132
5132
  return payload;
5133
5133
  }
5134
5134
 
5135
- // src/core/maps/catalog.ts
5136
- var MAP_CATALOG_VERSION = 1;
5137
- var SLUG_RE = /^[a-z0-9][a-z0-9-_]*$/;
5138
- var MAX_SLUG_LENGTH = 63;
5139
- var WORLD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5140
- var EMPTY_MAP_CATALOG = { version: MAP_CATALOG_VERSION, maps: [] };
5141
- var MapCatalogError = class extends Error {
5142
- code;
5143
- constructor(code, message) {
5144
- super(message);
5145
- this.name = "MapCatalogError";
5146
- this.code = code;
5147
- }
5148
- };
5149
- function fail(code, message) {
5150
- throw new MapCatalogError(code, message);
5151
- }
5152
- function isMapSlug(value) {
5153
- return value.length > 0 && value.length <= MAX_SLUG_LENGTH && SLUG_RE.test(value);
5154
- }
5155
- function assertMapSlug(slug) {
5156
- if (!isMapSlug(slug)) fail("INVALID_SLUG", `invalid map slug "${slug}"`);
5157
- return slug;
5158
- }
5159
- function assertWorldId(worldId) {
5160
- if (!WORLD_ID_RE.test(worldId) || worldId === "." || worldId === "..") {
5161
- fail("INVALID_PATH", `invalid world id "${worldId}"`);
5162
- }
5163
- return worldId;
5164
- }
5165
- function normalizeRootUrl(raw) {
5166
- if (typeof raw !== "string" || raw.trim().length === 0) {
5167
- fail("INVALID_ROOT", "map root must be a non-empty URL");
5168
- }
5169
- let url;
5170
- try {
5171
- url = new URL(raw.trim());
5172
- } catch {
5173
- fail("INVALID_ROOT", `map root is not a valid URL: ${raw}`);
5174
- }
5175
- if (url.protocol !== "http:" && url.protocol !== "https:") {
5176
- fail("INVALID_ROOT", `map root must be http(s): ${raw}`);
5177
- }
5178
- url.hash = "";
5179
- if (!url.pathname.endsWith("/")) {
5180
- const lastSegment = url.pathname.split("/").pop() ?? "";
5181
- if (/\.[A-Za-z0-9]+$/.test(lastSegment)) {
5182
- fail(
5183
- "INVALID_ROOT",
5184
- `map root must be the unpacked map directory, not a file ("${lastSegment}"); use its parent directory ending in "/"`
5185
- );
5186
- }
5187
- url.pathname += "/";
5188
- }
5189
- return url.toString();
5190
- }
5191
- function assertRelativeMapPath(relPath) {
5192
- if (relPath.length === 0 || relPath.includes("\0") || relPath.includes("\\")) {
5193
- fail("INVALID_PATH", `invalid map path "${relPath}"`);
5194
- }
5195
- if (relPath.startsWith("/")) fail("INVALID_PATH", `absolute map path "${relPath}"`);
5196
- const parts2 = [];
5197
- for (const part of relPath.split("/")) {
5198
- if (part === "" || part === ".") continue;
5199
- if (part === "..") fail("INVALID_PATH", `map path escapes the root: "${relPath}"`);
5200
- parts2.push(part);
5201
- }
5202
- if (parts2.length === 0) fail("INVALID_PATH", "empty map path");
5203
- return parts2.join("/");
5204
- }
5205
- function mapFilePath(root, relPath) {
5206
- const base = new URL(normalizeRootUrl(root));
5207
- const relative = assertRelativeMapPath(relPath);
5208
- const url = new URL(relative, base);
5209
- if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) {
5210
- fail("INVALID_PATH", `map path escapes the root: "${relPath}"`);
5211
- }
5212
- if (base.search.length > 0) url.search = base.search;
5213
- url.hash = "";
5214
- return url.toString();
5215
- }
5216
- function optionalString(value, label, code = "INVALID_MAP") {
5217
- if (value === void 0) return void 0;
5218
- if (typeof value !== "string" || value.trim().length === 0) {
5219
- fail(code, `${label} must be a non-empty string`);
5220
- }
5221
- return value.trim();
5222
- }
5223
- function parseEntry(value, index) {
5224
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
5225
- fail("INVALID_MAP", `maps[${index}] must be an object`);
5226
- }
5227
- const record = value;
5228
- const slug = optionalString(record.slug, `maps[${index}].slug`, "INVALID_SLUG");
5229
- if (slug === void 0) fail("INVALID_SLUG", `maps[${index}].slug is required`);
5230
- assertMapSlug(slug);
5231
- const rootRaw = optionalString(record.root, `maps[${index}].root`, "INVALID_ROOT");
5232
- if (rootRaw === void 0) fail("INVALID_ROOT", `maps[${slug}].root is required`);
5233
- const root = normalizeRootUrl(rootRaw);
5234
- const worldId = optionalString(record.worldId, `maps[${index}].worldId`);
5235
- const assetRoot = optionalString(record.assetRoot, `maps[${index}].assetRoot`, "INVALID_ROOT");
5236
- const name = optionalString(record.name, `maps[${index}].name`);
5237
- const author = optionalString(record.author, `maps[${index}].author`);
5238
- const version = optionalString(record.version, `maps[${index}].version`);
5239
- const cover = optionalString(record.cover, `maps[${index}].cover`);
5240
- return {
5241
- slug,
5242
- root,
5243
- ...worldId !== void 0 ? { worldId: assertWorldId(worldId) } : {},
5244
- ...name !== void 0 ? { name } : {},
5245
- ...author !== void 0 ? { author } : {},
5246
- ...version !== void 0 ? { version } : {},
5247
- ...cover !== void 0 ? { cover } : {},
5248
- ...assetRoot !== void 0 ? { assetRoot: normalizeRootUrl(assetRoot) } : {}
5249
- };
5250
- }
5251
- function parseMapCatalog(input) {
5252
- if (input === void 0 || input === null) return EMPTY_MAP_CATALOG;
5253
- if (typeof input !== "object" || Array.isArray(input)) {
5254
- fail("INVALID_CATALOG", "maps.json must be an object");
5255
- }
5256
- const record = input;
5257
- const version = record.version ?? MAP_CATALOG_VERSION;
5258
- if (version !== MAP_CATALOG_VERSION) {
5259
- fail("INVALID_CATALOG", `unsupported maps.json version ${String(version)}`);
5260
- }
5261
- if (!Array.isArray(record.maps)) {
5262
- fail("INVALID_CATALOG", "maps.json `maps` must be an array");
5263
- }
5264
- const maps = record.maps.map((entry, index) => parseEntry(entry, index));
5265
- const seen = /* @__PURE__ */ new Set();
5266
- for (const map of maps) {
5267
- if (seen.has(map.slug)) fail("DUPLICATE_SLUG", `duplicate map slug "${map.slug}"`);
5268
- seen.add(map.slug);
5269
- }
5270
- return { version: MAP_CATALOG_VERSION, maps };
5271
- }
5272
- function findMap(catalog, slug) {
5273
- return catalog.maps.find((entry) => entry.slug === slug);
5274
- }
5275
-
5276
5135
  // src/vendor/mudb/box-schema/backend/database-type.ts
5277
5136
  var DB;
5278
5137
  ((DB2) => {
@@ -13850,16 +13709,268 @@ function decodeBlockInfo(bytes) {
13850
13709
  return null;
13851
13710
  }
13852
13711
  }
13853
- async function loadBlockInfo(reader, worldId, hash = BLOCK_INFO_HASH) {
13712
+ async function readBlockInfoBytes(reader, worldId, hash = BLOCK_INFO_HASH) {
13854
13713
  try {
13855
13714
  const bytes = await reader.read(`worlds/${worldId}/data/blobs/${hash}`);
13856
13715
  if (bytes === null || bytes.length === 0) return null;
13857
- return decodeBlockInfo(bytes);
13716
+ return bytes;
13858
13717
  } catch {
13859
13718
  return null;
13860
13719
  }
13861
13720
  }
13862
13721
 
13722
+ // src/core/maps/seed-pack.ts
13723
+ var SEED_PACK_VERSION = 1;
13724
+ var SEED_PACK_FORMAT = "cajonzero-seed";
13725
+ var SEED_PACK_FILES = ["seed.json.gz", "seed.json"];
13726
+ function isGzip(bytes) {
13727
+ return bytes.length >= 2 && bytes[0] === 31 && bytes[1] === 139;
13728
+ }
13729
+ async function gunzipBytes(bytes) {
13730
+ if (typeof DecompressionStream === "undefined") {
13731
+ throw new Error("this runtime does not support gzip seed packs");
13732
+ }
13733
+ const buffer = bytes.buffer.slice(
13734
+ bytes.byteOffset,
13735
+ bytes.byteOffset + bytes.byteLength
13736
+ );
13737
+ const stream = new Blob([buffer]).stream().pipeThrough(new DecompressionStream("gzip"));
13738
+ return new Uint8Array(await new Response(stream).arrayBuffer());
13739
+ }
13740
+ async function decodeSeedPackAuto(bytes) {
13741
+ return decodeSeedPack(isGzip(bytes) ? await gunzipBytes(bytes) : bytes);
13742
+ }
13743
+ async function loadSeedPackFromAssets(assets, slug, worldId) {
13744
+ if (assets === void 0) return null;
13745
+ for (const file of SEED_PACK_FILES) {
13746
+ const suffix = file.slice("seed".length);
13747
+ const path = `/seed/${encodeURIComponent(slug)}${suffix}`;
13748
+ try {
13749
+ const response = await assets.fetch(new Request(`https://cajonzero.internal${path}`));
13750
+ if (!response.ok) continue;
13751
+ const bytes = new Uint8Array(await response.arrayBuffer());
13752
+ const seed = await decodeSeedPackAuto(bytes);
13753
+ if (worldId !== void 0 && worldId.length > 0 && seed.mapId !== worldId) continue;
13754
+ return seed;
13755
+ } catch {
13756
+ }
13757
+ }
13758
+ return null;
13759
+ }
13760
+ function base64ToBytes(text) {
13761
+ const binary = atob(text);
13762
+ const out = new Uint8Array(binary.length);
13763
+ for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
13764
+ return out;
13765
+ }
13766
+ function isRecord2(value) {
13767
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13768
+ }
13769
+ function decodeSeedPack(bytes) {
13770
+ let parsed;
13771
+ try {
13772
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
13773
+ } catch {
13774
+ throw new Error("seed pack is not valid JSON");
13775
+ }
13776
+ if (!isRecord2(parsed) || parsed["format"] !== SEED_PACK_FORMAT) {
13777
+ throw new Error("not a cajonzero seed pack");
13778
+ }
13779
+ if (parsed["version"] !== SEED_PACK_VERSION) {
13780
+ throw new Error(
13781
+ `unsupported seed pack version ${String(parsed["version"])} (expected ${SEED_PACK_VERSION})`
13782
+ );
13783
+ }
13784
+ if (typeof parsed["mapId"] !== "string" || parsed["mapId"].length === 0) {
13785
+ throw new Error("seed pack is missing mapId");
13786
+ }
13787
+ if (!isRecord2(parsed["seed"])) {
13788
+ throw new Error("seed pack is missing the seed payload");
13789
+ }
13790
+ const rawSeed = parsed["seed"];
13791
+ if (rawSeed["version"] !== WORLD_SEED_VERSION) {
13792
+ throw new Error(
13793
+ `seed pack was built for world-seed v${String(rawSeed["version"])} (this runtime expects v${WORLD_SEED_VERSION}); re-run cj0 maps pack`
13794
+ );
13795
+ }
13796
+ const rawVoxels = rawSeed["voxels"];
13797
+ let voxels = null;
13798
+ if (rawVoxels !== null) {
13799
+ if (!isRecord2(rawVoxels)) throw new Error("seed pack voxels are malformed");
13800
+ const blobs = rawVoxels["chunkBlobs"];
13801
+ if (!Array.isArray(blobs)) throw new Error("seed pack voxels are malformed");
13802
+ const rawShape = rawVoxels["shape"];
13803
+ const shape = isRecord2(rawShape) ? {
13804
+ x: Number(rawShape["x"]) || 0,
13805
+ y: Number(rawShape["y"]) || 0,
13806
+ z: Number(rawShape["z"]) || 0
13807
+ } : { x: 0, y: 0, z: 0 };
13808
+ const hashes = rawVoxels["chunkHashes"];
13809
+ voxels = {
13810
+ shape,
13811
+ chunkCount: Number(rawVoxels["chunkCount"]) || blobs.length,
13812
+ chunkBlobs: blobs.map(
13813
+ (blob) => typeof blob === "string" ? base64ToBytes(blob) : new Uint8Array(0)
13814
+ ),
13815
+ chunkHashes: Array.isArray(hashes) ? hashes.map((hash) => typeof hash === "string" ? hash : "") : []
13816
+ };
13817
+ }
13818
+ const { voxels: _ignored, ...rest } = rawSeed;
13819
+ const seed = {
13820
+ ...rest,
13821
+ mapId: parsed["mapId"],
13822
+ voxels
13823
+ };
13824
+ if (typeof parsed["blockInfo"] === "string" && parsed["blockInfo"].length > 0) {
13825
+ const blockInfo = decodeBlockInfo(base64ToBytes(parsed["blockInfo"]));
13826
+ if (blockInfo !== null) seed.blockInfo = blockInfo;
13827
+ }
13828
+ return seed;
13829
+ }
13830
+
13831
+ // src/core/maps/catalog.ts
13832
+ var MAP_CATALOG_VERSION = 1;
13833
+ var SLUG_RE = /^[a-z0-9][a-z0-9-_]*$/;
13834
+ var MAX_SLUG_LENGTH = 63;
13835
+ var WORLD_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13836
+ var EMPTY_MAP_CATALOG = { version: MAP_CATALOG_VERSION, maps: [] };
13837
+ var MapCatalogError = class extends Error {
13838
+ code;
13839
+ constructor(code, message) {
13840
+ super(message);
13841
+ this.name = "MapCatalogError";
13842
+ this.code = code;
13843
+ }
13844
+ };
13845
+ function fail(code, message) {
13846
+ throw new MapCatalogError(code, message);
13847
+ }
13848
+ function isMapSlug(value) {
13849
+ return value.length > 0 && value.length <= MAX_SLUG_LENGTH && SLUG_RE.test(value);
13850
+ }
13851
+ function assertMapSlug(slug) {
13852
+ if (!isMapSlug(slug)) fail("INVALID_SLUG", `invalid map slug "${slug}"`);
13853
+ return slug;
13854
+ }
13855
+ function assertWorldId(worldId) {
13856
+ if (!WORLD_ID_RE.test(worldId) || worldId === "." || worldId === "..") {
13857
+ fail("INVALID_PATH", `invalid world id "${worldId}"`);
13858
+ }
13859
+ return worldId;
13860
+ }
13861
+ function normalizeRootUrl(raw) {
13862
+ if (typeof raw !== "string" || raw.trim().length === 0) {
13863
+ fail("INVALID_ROOT", "map root must be a non-empty URL");
13864
+ }
13865
+ let url;
13866
+ try {
13867
+ url = new URL(raw.trim());
13868
+ } catch {
13869
+ fail("INVALID_ROOT", `map root is not a valid URL: ${raw}`);
13870
+ }
13871
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
13872
+ fail("INVALID_ROOT", `map root must be http(s): ${raw}`);
13873
+ }
13874
+ url.hash = "";
13875
+ if (!url.pathname.endsWith("/")) {
13876
+ const lastSegment = url.pathname.split("/").pop() ?? "";
13877
+ if (/\.[A-Za-z0-9]+$/.test(lastSegment)) {
13878
+ fail(
13879
+ "INVALID_ROOT",
13880
+ `map root must be the unpacked map directory, not a file ("${lastSegment}"); use its parent directory ending in "/"`
13881
+ );
13882
+ }
13883
+ url.pathname += "/";
13884
+ }
13885
+ return url.toString();
13886
+ }
13887
+ function assertRelativeMapPath(relPath) {
13888
+ if (relPath.length === 0 || relPath.includes("\0") || relPath.includes("\\")) {
13889
+ fail("INVALID_PATH", `invalid map path "${relPath}"`);
13890
+ }
13891
+ if (relPath.startsWith("/")) fail("INVALID_PATH", `absolute map path "${relPath}"`);
13892
+ const parts2 = [];
13893
+ for (const part of relPath.split("/")) {
13894
+ if (part === "" || part === ".") continue;
13895
+ if (part === "..") fail("INVALID_PATH", `map path escapes the root: "${relPath}"`);
13896
+ parts2.push(part);
13897
+ }
13898
+ if (parts2.length === 0) fail("INVALID_PATH", "empty map path");
13899
+ return parts2.join("/");
13900
+ }
13901
+ function mapFilePath(root, relPath) {
13902
+ const base = new URL(normalizeRootUrl(root));
13903
+ const relative = assertRelativeMapPath(relPath);
13904
+ const url = new URL(relative, base);
13905
+ if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) {
13906
+ fail("INVALID_PATH", `map path escapes the root: "${relPath}"`);
13907
+ }
13908
+ if (base.search.length > 0) url.search = base.search;
13909
+ url.hash = "";
13910
+ return url.toString();
13911
+ }
13912
+ function optionalString(value, label, code = "INVALID_MAP") {
13913
+ if (value === void 0) return void 0;
13914
+ if (typeof value !== "string" || value.trim().length === 0) {
13915
+ fail(code, `${label} must be a non-empty string`);
13916
+ }
13917
+ return value.trim();
13918
+ }
13919
+ function parseEntry(value, index) {
13920
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
13921
+ fail("INVALID_MAP", `maps[${index}] must be an object`);
13922
+ }
13923
+ const record = value;
13924
+ const slug = optionalString(record.slug, `maps[${index}].slug`, "INVALID_SLUG");
13925
+ if (slug === void 0) fail("INVALID_SLUG", `maps[${index}].slug is required`);
13926
+ assertMapSlug(slug);
13927
+ const rootRaw = optionalString(record.root, `maps[${index}].root`, "INVALID_ROOT");
13928
+ if (rootRaw === void 0) fail("INVALID_ROOT", `maps[${slug}].root is required`);
13929
+ const root = normalizeRootUrl(rootRaw);
13930
+ const worldId = optionalString(record.worldId, `maps[${index}].worldId`);
13931
+ const assetRoot = optionalString(record.assetRoot, `maps[${index}].assetRoot`, "INVALID_ROOT");
13932
+ const name = optionalString(record.name, `maps[${index}].name`);
13933
+ const author = optionalString(record.author, `maps[${index}].author`);
13934
+ const version = optionalString(record.version, `maps[${index}].version`);
13935
+ const etag = optionalString(record.etag, `maps[${index}].etag`);
13936
+ const cover = optionalString(record.cover, `maps[${index}].cover`);
13937
+ return {
13938
+ slug,
13939
+ root,
13940
+ ...worldId !== void 0 ? { worldId: assertWorldId(worldId) } : {},
13941
+ ...name !== void 0 ? { name } : {},
13942
+ ...author !== void 0 ? { author } : {},
13943
+ ...version !== void 0 ? { version } : {},
13944
+ ...etag !== void 0 ? { etag } : {},
13945
+ ...cover !== void 0 ? { cover } : {},
13946
+ ...assetRoot !== void 0 ? { assetRoot: normalizeRootUrl(assetRoot) } : {}
13947
+ };
13948
+ }
13949
+ function parseMapCatalog(input) {
13950
+ if (input === void 0 || input === null) return EMPTY_MAP_CATALOG;
13951
+ if (typeof input !== "object" || Array.isArray(input)) {
13952
+ fail("INVALID_CATALOG", "maps.json must be an object");
13953
+ }
13954
+ const record = input;
13955
+ const version = record.version ?? MAP_CATALOG_VERSION;
13956
+ if (version !== MAP_CATALOG_VERSION) {
13957
+ fail("INVALID_CATALOG", `unsupported maps.json version ${String(version)}`);
13958
+ }
13959
+ if (!Array.isArray(record.maps)) {
13960
+ fail("INVALID_CATALOG", "maps.json `maps` must be an array");
13961
+ }
13962
+ const maps = record.maps.map((entry, index) => parseEntry(entry, index));
13963
+ const seen = /* @__PURE__ */ new Set();
13964
+ for (const map of maps) {
13965
+ if (seen.has(map.slug)) fail("DUPLICATE_SLUG", `duplicate map slug "${map.slug}"`);
13966
+ seen.add(map.slug);
13967
+ }
13968
+ return { version: MAP_CATALOG_VERSION, maps };
13969
+ }
13970
+ function findMap(catalog, slug) {
13971
+ return catalog.maps.find((entry) => entry.slug === slug);
13972
+ }
13973
+
13863
13974
  // src/core/maps/http.ts
13864
13975
  var MapSourceError = class extends Error {
13865
13976
  code;
@@ -13962,14 +14073,14 @@ var HttpMapSource = class {
13962
14073
  return new Uint8Array(buffer);
13963
14074
  }
13964
14075
  };
13965
- function isRecord2(value) {
14076
+ function isRecord3(value) {
13966
14077
  return typeof value === "object" && value !== null && !Array.isArray(value);
13967
14078
  }
13968
14079
  async function resolveWorldId(source) {
13969
14080
  const bundle = await source.readJson("bundle.json");
13970
14081
  if (bundle === null) fail2("MAP_NO_WORLD", "bundle.json is missing or not JSON");
13971
14082
  const members = Array.isArray(bundle.members) ? bundle.members : [];
13972
- const records = members.filter(isRecord2);
14083
+ const records = members.filter(isRecord3);
13973
14084
  const main = records.find((member) => member.mapRole === "main") ?? records[0];
13974
14085
  const candidate = (main !== void 0 && typeof main.id === "string" ? main.id : void 0) ?? (typeof bundle.mainProjectId === "string" ? bundle.mainProjectId : void 0);
13975
14086
  if (candidate === void 0) {
@@ -13978,6 +14089,13 @@ async function resolveWorldId(source) {
13978
14089
  return assertWorldId(candidate);
13979
14090
  }
13980
14091
  async function loadWorldSeedFromHttp(entry, options = {}) {
14092
+ if (options.preferSeedPack !== false) {
14093
+ const packed = await loadSeedPackFromHttp(entry, options);
14094
+ if (packed !== null) return packed;
14095
+ }
14096
+ return (await loadWorldSeedGraphFromHttp(entry, options)).seed;
14097
+ }
14098
+ async function loadWorldSeedGraphFromHttp(entry, options = {}) {
13981
14099
  const source = new HttpMapSource(entry.root, options);
13982
14100
  const worldId = entry.worldId ?? await resolveWorldId(source);
13983
14101
  const manifest = await source.readJson(
@@ -13986,13 +14104,35 @@ async function loadWorldSeedFromHttp(entry, options = {}) {
13986
14104
  if (manifest === null) {
13987
14105
  fail2("MAP_NO_WORLD", `worlds/${worldId}/manifest.json is missing or not JSON`);
13988
14106
  }
13989
- return resolveWorldSeed(
14107
+ const seed = await resolveWorldSeed(
13990
14108
  { worldId, manifest, reader: source },
13991
14109
  { concurrency: options.concurrency ?? DEFAULT_CONCURRENCY }
13992
- ).then(async (seed) => {
13993
- const blockInfo = await loadBlockInfo(source, worldId);
13994
- return blockInfo === null ? seed : { ...seed, blockInfo };
13995
- });
14110
+ );
14111
+ const blockInfoBytes = await readBlockInfoBytes(source, worldId) ?? void 0;
14112
+ const blockInfo = blockInfoBytes === void 0 ? null : decodeBlockInfo(blockInfoBytes);
14113
+ return {
14114
+ seed: blockInfo === null ? seed : { ...seed, blockInfo },
14115
+ ...blockInfoBytes !== void 0 ? { blockInfoBytes } : {}
14116
+ };
14117
+ }
14118
+ async function loadSeedPackFromHttp(entry, options) {
14119
+ const source = new HttpMapSource(entry.root, options);
14120
+ for (const file of SEED_PACK_FILES) {
14121
+ let bytes;
14122
+ try {
14123
+ bytes = await source.read(file);
14124
+ } catch {
14125
+ continue;
14126
+ }
14127
+ if (bytes === null) continue;
14128
+ try {
14129
+ const seed = await decodeSeedPackAuto(bytes);
14130
+ if (entry.worldId !== void 0 && seed.mapId !== entry.worldId) continue;
14131
+ return seed;
14132
+ } catch {
14133
+ }
14134
+ }
14135
+ return null;
13996
14136
  }
13997
14137
  var MEBIBYTE = 1024 * 1024;
13998
14138
  var ZERO_PLAYER_TIMEOUT_DEFAULT_MS = 48e4;
@@ -16576,6 +16716,8 @@ var Room = class {
16576
16716
  createPair;
16577
16717
  logger;
16578
16718
  secret;
16719
+ /** Worker static-assets binding, used to read seed packs bundled at deploy. */
16720
+ assets;
16579
16721
  engine;
16580
16722
  script;
16581
16723
  explicitScriptHost;
@@ -16638,6 +16780,10 @@ var Room = class {
16638
16780
  pendingEngineMap;
16639
16781
  /** Identity of the map the current engine was seeded from. */
16640
16782
  seededMap;
16783
+ /** Non-fatal seed warnings (missing blobs, skipped entities, …). */
16784
+ seedWarnings = [];
16785
+ /** Last map-load failure; cleared on a successful seed. */
16786
+ mapLoadError;
16641
16787
  heartbeatBeats = 0;
16642
16788
  ticksRun = 0;
16643
16789
  lastTickError;
@@ -16661,6 +16807,7 @@ var Room = class {
16661
16807
  this.createPair = options.createPair ?? defaultCreatePair;
16662
16808
  this.logger = options.logger ?? defaultLogger;
16663
16809
  this.secret = options.secret ?? resolveSessionSecret(env);
16810
+ this.assets = env.ASSETS;
16664
16811
  this.engine = options.engine ?? getRoomEngineFactory()?.() ?? createNullEngine();
16665
16812
  this.explicitScriptHost = options.scriptHost !== void 0;
16666
16813
  this.script = options.scriptHost ?? this.engine;
@@ -16755,7 +16902,9 @@ var Room = class {
16755
16902
  ok: true,
16756
16903
  slug: this.mapBootstrap?.slug ?? null,
16757
16904
  seeded: this.worldReady,
16758
- entities: this.engine.getReplica().entities.length
16905
+ entities: this.engine.getReplica().entities.length,
16906
+ mapLoadError: this.mapLoadError ?? null,
16907
+ warnings: this.seedWarnings
16759
16908
  }),
16760
16909
  { headers: { "content-type": "application/json", "cache-control": "no-store" } }
16761
16910
  );
@@ -16786,6 +16935,8 @@ var Room = class {
16786
16935
  secretPackets: this.secretPackets,
16787
16936
  clockPings: this.clockPings,
16788
16937
  inboundIds: Object.fromEntries(this.inboundIds),
16938
+ mapLoadError: this.mapLoadError ?? null,
16939
+ mapWarnings: this.seedWarnings,
16789
16940
  voxel: this.engine.voxelGeometry?.() ?? null,
16790
16941
  sampleEntity: (() => {
16791
16942
  const entity = this.engine.getReplica().entities[0];
@@ -16883,21 +17034,32 @@ var Room = class {
16883
17034
  this.engine = engine;
16884
17035
  if (!this.explicitScriptHost) this.script = engine;
16885
17036
  this.seededMap = { slug: bootstrap.slug, root: bootstrap.root, worldId: seed.mapId };
17037
+ this.seedWarnings = seed.warnings;
17038
+ this.mapLoadError = void 0;
17039
+ for (const warning of seed.warnings) {
17040
+ this.logger.log(`cajonzero: map "${bootstrap.slug}" warning: ${warning}`);
17041
+ }
16886
17042
  this.applyPendingEngineSnapshot();
16887
17043
  this.worldReady = true;
16888
17044
  this.logger.log(`cajonzero: seeded room "${bootstrap.slug}" from "${seed.mapId}"`);
16889
17045
  } catch (error) {
17046
+ this.mapLoadError = describeError2(error);
16890
17047
  this.logger.error(
16891
- `cajonzero: map load failed for "${bootstrap.slug}": ${describeError2(error)}`
17048
+ `cajonzero: map load failed for "${bootstrap.slug}": ${this.mapLoadError}`
16892
17049
  );
16893
17050
  }
16894
17051
  }
16895
17052
  loadSeed(bootstrap) {
16896
- return loadWorldSeedFromHttp(bootstrap, {
16897
- ...bootstrap.maxDownloadBytes !== void 0 ? { maxDownloadBytes: bootstrap.maxDownloadBytes } : {},
16898
- ...bootstrap.concurrency !== void 0 ? { concurrency: bootstrap.concurrency } : {},
16899
- ...bootstrap.requestTimeoutMs !== void 0 ? { requestTimeoutMs: bootstrap.requestTimeoutMs } : {}
16900
- });
17053
+ return loadSeedPackFromAssets(this.assets, bootstrap.slug, bootstrap.worldId).then(
17054
+ (packed) => {
17055
+ if (packed !== null) return packed;
17056
+ return loadWorldSeedFromHttp(bootstrap, {
17057
+ ...bootstrap.maxDownloadBytes !== void 0 ? { maxDownloadBytes: bootstrap.maxDownloadBytes } : {},
17058
+ ...bootstrap.concurrency !== void 0 ? { concurrency: bootstrap.concurrency } : {},
17059
+ ...bootstrap.requestTimeoutMs !== void 0 ? { requestTimeoutMs: bootstrap.requestTimeoutMs } : {}
17060
+ });
17061
+ }
17062
+ );
16901
17063
  }
16902
17064
  /**
16903
17065
  * Run the full connect path and return a structured outcome (no `Response`,
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Context, Hono } from 'hono';
2
2
  import { ContentfulStatusCode } from 'hono/utils/http-status';
3
- import { b as Env, d as MapCatalog, e as Repos, L as SessionsRepo, D as DurableObjectNamespace } from './providers-BX7e4pNW.js';
3
+ import { b as Env, d as MapCatalog, e as Repos, L as SessionsRepo, D as DurableObjectNamespace } from './providers-BwJKzGkd.js';
4
4
  import { Store, SettingDefaults } from './core.js';
5
- export { c as EMPTY_MAP_CATALOG, M as MapEntry, R as Room, w as findMap, A as parseMapCatalog } from './providers-BX7e4pNW.js';
6
- import { C as CajonzeroConfig } from './config-BOzAyP7m.js';
7
- export { D as DEFAULT_CONFIG, G as GAME_NET_INBOUND_BASE, a as GAME_NET_OUTBOUND_BASE, b as MapsConfig, N as NetCodec, e as NetCodecHandlers, R as RoomConfig, q as resolveConfig } from './config-BOzAyP7m.js';
5
+ export { c as EMPTY_MAP_CATALOG, M as MapEntry, R as Room, w as findMap, A as parseMapCatalog } from './providers-BwJKzGkd.js';
6
+ import { C as CajonzeroConfig } from './config-B32NDXlr.js';
7
+ export { D as DEFAULT_CONFIG, G as GAME_NET_INBOUND_BASE, a as GAME_NET_OUTBOUND_BASE, b as MapsConfig, N as NetCodec, e as NetCodecHandlers, R as RoomConfig, q as resolveConfig } from './config-B32NDXlr.js';
8
8
  export { DO_CLASSES, ServeOptions, WorkersEntry, WorkersModule, serve } from './serve.js';
9
9
  import 'unstorage';
10
10
  import 'gl-matrix';
@@ -18,7 +18,7 @@ import 'zod';
18
18
  * can read the version without creating a circular import.
19
19
  */
20
20
  declare const NAME = "cajonzero";
21
- declare const VERSION = "0.1.1";
21
+ declare const VERSION = "0.1.3";
22
22
 
23
23
  /** The authenticated principal carried by a login session (T11 supplies it). */
24
24
  interface SessionIdentity {