@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.
- package/dist/pixel-vibing-cli.js +202 -14
- package/package.json +1 -1
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
2980
|
+
let inflated2;
|
|
2794
2981
|
try {
|
|
2795
|
-
|
|
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 (
|
|
2800
|
-
return toRgba(unfilter(
|
|
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
|
|
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.
|
|
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.
|
|
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",
|