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.
- package/README.md +42 -3
- package/dist/bin/cj0.js +14243 -47
- package/dist/{config-BOzAyP7m.d.ts → config-B32NDXlr.d.ts} +1 -1
- package/dist/core.d.ts +15 -7
- package/dist/core.js +319 -157
- package/dist/index.d.ts +5 -5
- package/dist/index.js +184 -22
- package/dist/{providers-BX7e4pNW.d.ts → providers-BwJKzGkd.d.ts} +17 -0
- package/dist/scaffold.d.ts +67 -3
- package/dist/scaffold.js +293 -32
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +325 -163
- package/dist/ui/assets/index-CNHWg0_t.js +4404 -0
- package/dist/ui/assets/{main-BeABKxzY.js → main-DTcEV4JE.js} +16 -16
- package/dist/ui/index.html +1 -1
- package/package.json +4 -4
- package/dist/ui/assets/index-CaAHje8X.js +0 -4404
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as TimestampedClientInput, N as NetPublic, O as NetSecret } from './providers-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* the
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
14107
|
+
const seed = await resolveWorldSeed(
|
|
13990
14108
|
{ worldId, manifest, reader: source },
|
|
13991
14109
|
{ concurrency: options.concurrency ?? DEFAULT_CONCURRENCY }
|
|
13992
|
-
)
|
|
13993
|
-
|
|
13994
|
-
|
|
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}": ${
|
|
17048
|
+
`cajonzero: map load failed for "${bootstrap.slug}": ${this.mapLoadError}`
|
|
16892
17049
|
);
|
|
16893
17050
|
}
|
|
16894
17051
|
}
|
|
16895
17052
|
loadSeed(bootstrap) {
|
|
16896
|
-
return
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
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-
|
|
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-
|
|
6
|
-
import { C as CajonzeroConfig } from './config-
|
|
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-
|
|
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.
|
|
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 {
|