@pixelvibing/cli 0.8.0 → 0.10.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.
@@ -5,7 +5,7 @@ import { promisify, parseArgs } from "node:util";
5
5
  import { randomBytes, createHash } from "node:crypto";
6
6
  import { createServer } from "node:http";
7
7
  import { stat, readFile, writeFile, mkdir, readdir, lstat, rm, rename, realpath as realpath$1 } from "node:fs/promises";
8
- import { deflateSync, inflateSync } from "node:zlib";
8
+ import { inflate, deflateSync, inflateSync } from "node:zlib";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { spawn } from "node:child_process";
11
11
  import { hostname, homedir } from "node:os";
@@ -339,11 +339,12 @@ class SignedOut extends Error {
339
339
  }
340
340
  }
341
341
  class ApiError extends Error {
342
- constructor(status, message, body = {}) {
342
+ constructor(status, message, body = {}, retryAfter = null) {
343
343
  super(message);
344
344
  this.name = "ApiError";
345
345
  this.status = status;
346
346
  this.body = body;
347
+ this.retryAfter = retryAfter;
347
348
  }
348
349
  }
349
350
  async function refusalOf(response) {
@@ -383,7 +384,8 @@ function createCaller(fetcher = (input, init2) => fetch(input, init2), base = AP
383
384
  }
384
385
  if (!response.ok) {
385
386
  const refused = await refusalOf(response);
386
- throw new ApiError(response.status, refused.message, refused.body);
387
+ const wait = Number(response.headers.get("retry-after") ?? Number.NaN);
388
+ throw new ApiError(response.status, refused.message, refused.body, Number.isFinite(wait) ? wait : null);
387
389
  }
388
390
  return response;
389
391
  };
@@ -1303,6 +1305,174 @@ const editor = {
1303
1305
  answer(context, { opened: local.root, app: found.command }, `Opening ${local.root} in PixelVibing.`);
1304
1306
  }
1305
1307
  };
1308
+ const SECTOR_SIDE = 128;
1309
+ const SECTOR_CELLS = SECTOR_SIDE * SECTOR_SIDE;
1310
+ const SECTOR_BYTES = 512 * 1024;
1311
+ const MAP_LEVELS = { first: 0, last: 14 };
1312
+ const LARGEST_MAP_SIDE = 1e4;
1313
+ const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE);
1314
+ const SECTOR_FORMAT = 1;
1315
+ const MAGIC = [80, 86, 67];
1316
+ const PRESENT = 1;
1317
+ const AUDIO = 2;
1318
+ const INTERACT = 4;
1319
+ const EXTRA = 8;
1320
+ const NAMED = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
1321
+ class SectorError extends Error {
1322
+ constructor(message) {
1323
+ super(message);
1324
+ this.name = "SectorError";
1325
+ }
1326
+ }
1327
+ function decodeSector(bytes) {
1328
+ if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
1329
+ if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
1330
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1331
+ let at = 4;
1332
+ const need = (count) => {
1333
+ if (at + count > bytes.length) throw new SectorError("the sector's bytes end before its last cell");
1334
+ };
1335
+ const read8 = () => {
1336
+ need(1);
1337
+ return bytes[at++];
1338
+ };
1339
+ const read16 = () => {
1340
+ need(2);
1341
+ const value = bytes[at] | bytes[at + 1] << 8;
1342
+ at += 2;
1343
+ return value;
1344
+ };
1345
+ const cells = [];
1346
+ for (let index = 0; index < SECTOR_CELLS; index += 1) {
1347
+ const flags = read8();
1348
+ if ((flags & PRESENT) === 0) {
1349
+ cells.push(void 0);
1350
+ continue;
1351
+ }
1352
+ const count = read8();
1353
+ const layers = [];
1354
+ for (let layer = 0; layer < count; layer += 1) layers.push(read16());
1355
+ const blocking = read16();
1356
+ const zone = read16();
1357
+ const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
1358
+ const interact = (flags & INTERACT) === 0 ? void 0 : read16();
1359
+ let extra;
1360
+ if ((flags & EXTRA) !== 0) {
1361
+ const length = read16();
1362
+ need(length);
1363
+ let parsed2;
1364
+ try {
1365
+ parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
1366
+ } catch {
1367
+ throw new SectorError("a tile's extra fields are not JSON");
1368
+ }
1369
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
1370
+ const named = Object.keys(parsed2).find((key) => NAMED.has(key));
1371
+ if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
1372
+ extra = parsed2;
1373
+ at += length;
1374
+ }
1375
+ cells.push({
1376
+ layers,
1377
+ blocking,
1378
+ zone,
1379
+ ...audioZone === void 0 ? {} : { audioZone },
1380
+ ...interact === void 0 ? {} : { interact },
1381
+ ...extra === void 0 ? {} : { extra }
1382
+ });
1383
+ }
1384
+ if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
1385
+ return cells;
1386
+ }
1387
+ function tileOfCell(cell, x, y, z) {
1388
+ return {
1389
+ ...cell.extra ?? {},
1390
+ x,
1391
+ y,
1392
+ z,
1393
+ layers: [...cell.layers],
1394
+ blocking: cell.blocking,
1395
+ zone: cell.zone,
1396
+ ...cell.audioZone === void 0 ? {} : { audioZone: cell.audioZone },
1397
+ ...cell.interact === void 0 ? {} : { interact: cell.interact }
1398
+ };
1399
+ }
1400
+ const MIRROR_MAP_DIRECTORY = "map";
1401
+ const MIRROR_INDEX_FILE = `${MIRROR_MAP_DIRECTORY}/index.json`;
1402
+ const MIRROR_FORMAT = 1;
1403
+ class MirrorError extends Error {
1404
+ // Assigned in the body: Node's strip-only TypeScript mode rejects parameter
1405
+ // properties (docs/notes.md n-0023).
1406
+ constructor(message) {
1407
+ super(message);
1408
+ this.name = "MirrorError";
1409
+ }
1410
+ }
1411
+ function mirrorSectorPath(z, cx, cy) {
1412
+ if (!isPlace(z, cx, cy)) throw new MirrorError(`no sector of a map is at level ${z}, column ${cx}, row ${cy}`);
1413
+ return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
1414
+ }
1415
+ function isPlace(z, cx, cy) {
1416
+ const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
1417
+ return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
1418
+ }
1419
+ function isSize(value) {
1420
+ const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
1421
+ return typeof value === "object" && value !== null && side(value.width) && side(value.height);
1422
+ }
1423
+ function parseMirrorIndex(text2) {
1424
+ let value;
1425
+ try {
1426
+ value = JSON.parse(text2);
1427
+ } catch {
1428
+ throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
1429
+ }
1430
+ const index = value;
1431
+ if (typeof index !== "object" || index === null) throw new MirrorError(`${MIRROR_INDEX_FILE} is not an object`);
1432
+ if (index.format !== MIRROR_FORMAT)
1433
+ throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
1434
+ if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
1435
+ if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
1436
+ if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
1437
+ for (const named of index.names) {
1438
+ if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
1439
+ }
1440
+ const seen = /* @__PURE__ */ new Set();
1441
+ for (const sector of index.sectors) {
1442
+ const [z, cx, cy, version2] = Array.isArray(sector) ? sector : [];
1443
+ if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
1444
+ throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
1445
+ }
1446
+ const key = `${z}:${cx}:${cy}`;
1447
+ if (cx * SECTOR_SIDE >= index.size.width || cy * SECTOR_SIDE >= index.size.height) {
1448
+ throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
1449
+ }
1450
+ if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
1451
+ seen.add(key);
1452
+ }
1453
+ return index;
1454
+ }
1455
+ async function readMirroredMap(index, read, inflate2) {
1456
+ const tiles = [];
1457
+ for (const [z, cx, cy] of index.sectors) {
1458
+ const path = mirrorSectorPath(z, cx, cy);
1459
+ let cells;
1460
+ try {
1461
+ cells = decodeSector(await inflate2(await read(path)));
1462
+ } catch (error) {
1463
+ throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
1464
+ }
1465
+ for (const [at, cell] of cells.entries()) {
1466
+ if (cell === void 0) continue;
1467
+ const x = cx * SECTOR_SIDE + at % SECTOR_SIDE;
1468
+ const y = cy * SECTOR_SIDE + Math.floor(at / SECTOR_SIDE);
1469
+ if (x >= index.size.width || y >= index.size.height) continue;
1470
+ tiles.push(tileOfCell(cell, x, y, z));
1471
+ }
1472
+ }
1473
+ tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
1474
+ return { width: index.size.width, height: index.size.height, tiles };
1475
+ }
1306
1476
  const TILE_SIZE = 256;
1307
1477
  const EAI_MAGIC = 1229018181;
1308
1478
  const EAI_VERSION = 1;
@@ -2204,6 +2374,23 @@ async function readProjectMap(boundary, artDirectory) {
2204
2374
  await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, "map.json"));
2205
2375
  return boundary.readText(path);
2206
2376
  }
2377
+ const MAX_MIRRORED_SECTOR_BYTES = 256 * 1024;
2378
+ const inflated = promisify(inflate);
2379
+ async function readMirroredProjectMap(boundary, artDirectory) {
2380
+ const indexPath = join(artDirectory, MIRROR_INDEX_FILE);
2381
+ if (!await boundary.exists(indexPath)) return null;
2382
+ await sized(boundary, indexPath, MAX_TEXT_BYTES, shown(artDirectory, MIRROR_INDEX_FILE));
2383
+ const index = parseMirrorIndex(await boundary.readText(indexPath));
2384
+ return readMirroredMap(
2385
+ index,
2386
+ async (relative2) => {
2387
+ const path = join(artDirectory, relative2);
2388
+ await sized(boundary, path, MAX_MIRRORED_SECTOR_BYTES, shown(artDirectory, relative2));
2389
+ return boundary.readBytes(path);
2390
+ },
2391
+ async (bytes) => new Uint8Array(await inflated(bytes, { maxOutputLength: SECTOR_BYTES }))
2392
+ );
2393
+ }
2207
2394
  async function sealProject(boundary, artDirectory) {
2208
2395
  try {
2209
2396
  const sealed = sealProjectArt(await readProjectArt(boundary, artDirectory));
@@ -2738,14 +2925,14 @@ const paeth = (a, b, c) => {
2738
2925
  const pc = Math.abs(p - c);
2739
2926
  return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
2740
2927
  };
2741
- function unfilter(inflated, header) {
2928
+ function unfilter(inflated2, header) {
2742
2929
  const bpp = CHANNELS[header.colorType] * (header.depth / 8);
2743
2930
  const stride = header.width * bpp;
2744
2931
  const out = new Uint8Array(header.height * stride);
2745
2932
  for (let y = 0; y < header.height; y += 1) {
2746
- const filter = inflated[y * (stride + 1)];
2933
+ const filter = inflated2[y * (stride + 1)];
2747
2934
  if (filter > 4) throw new PngError(`a row has filter type ${filter}, which PNG does not define`);
2748
- const row = inflated.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
2935
+ const row = inflated2.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
2749
2936
  const at = y * stride;
2750
2937
  for (let x = 0; x < stride; x += 1) {
2751
2938
  const a = x >= bpp ? out[at + x - bpp] : 0;
@@ -2790,14 +2977,14 @@ function decodeWith(zlib, bytes) {
2790
2977
  if (header === null) throw new PngError("the PNG has no IHDR chunk");
2791
2978
  const bpp = CHANNELS[header.colorType] * (header.depth / 8);
2792
2979
  const expected = header.height * (1 + header.width * bpp);
2793
- let inflated;
2980
+ let inflated2;
2794
2981
  try {
2795
- inflated = zlib.inflate(concat(data), expected);
2982
+ inflated2 = zlib.inflate(concat(data), expected);
2796
2983
  } catch {
2797
2984
  throw new PngError("the PNG's image data could not be inflated to the size its header promises");
2798
2985
  }
2799
- if (inflated.length !== expected) throw new PngError("the PNG's image data is not the size its header promises");
2800
- return toRgba(unfilter(inflated, header), header, palette, transparency);
2986
+ if (inflated2.length !== expected) throw new PngError("the PNG's image data is not the size its header promises");
2987
+ return toRgba(unfilter(inflated2, header), header, palette, transparency);
2801
2988
  }
2802
2989
  function toRgba(samples, header, palette, transparency) {
2803
2990
  const { width, height, depth, colorType } = header;
@@ -2947,7 +3134,8 @@ const exportCommand = {
2947
3134
  try {
2948
3135
  const boundary = new Boundary([art]);
2949
3136
  const read = await readProjectArt(boundary, art);
2950
- const mapText = await readProjectMap(boundary, art);
3137
+ const mirrored2 = await readMirroredProjectMap(boundary, art);
3138
+ const mapText = mirrored2 === null ? await readProjectMap(boundary, art) : null;
2951
3139
  checkExportable(
2952
3140
  read.pictures.map((picture) => {
2953
3141
  if (!isPng(picture.bytes)) throw new CliError(`the picture ${picture.name} cannot be read: it is not a PNG`);
@@ -2965,7 +3153,7 @@ const exportCommand = {
2965
3153
  {
2966
3154
  pictures,
2967
3155
  entries: read.entries,
2968
- map: mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document,
3156
+ map: mirrored2 ?? (mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document),
2969
3157
  idMap: read.idMap === null ? null : parseIdMap(JSON.parse(read.idMap))
2970
3158
  },
2971
3159
  engine,
@@ -2973,7 +3161,7 @@ const exportCommand = {
2973
3161
  godotRoot === void 0 ? {} : { godotRoot }
2974
3162
  );
2975
3163
  } catch (error) {
2976
- const refusals = [ProjectError, PackError, PngError, MapTextError, IdMapError, SyntaxError, CliError];
3164
+ const refusals = [ProjectError, PackError, PngError, MapTextError, MirrorError, IdMapError, SyntaxError, CliError];
2977
3165
  if (refusals.some((kind) => error instanceof kind)) throw new CliError(`nothing was exported: ${error.message}`);
2978
3166
  throw error;
2979
3167
  }
@@ -3760,7 +3948,7 @@ const seal = {
3760
3948
  answer(context, { sealed: true, output: result.output }, result.output);
3761
3949
  }
3762
3950
  };
3763
- const version = "0.8.0";
3951
+ const version = "0.10.0";
3764
3952
  const manifest = {
3765
3953
  version
3766
3954
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelvibing/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "PixelVibing: pixel art game assets from the command line, and the Skills that teach coding assistants to make them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://pixelvibing.com",