@pixelvibing/cli 0.9.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 +198 -12
- 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";
|
|
@@ -1305,6 +1305,174 @@ const editor = {
|
|
|
1305
1305
|
answer(context, { opened: local.root, app: found.command }, `Opening ${local.root} in PixelVibing.`);
|
|
1306
1306
|
}
|
|
1307
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
|
+
}
|
|
1308
1476
|
const TILE_SIZE = 256;
|
|
1309
1477
|
const EAI_MAGIC = 1229018181;
|
|
1310
1478
|
const EAI_VERSION = 1;
|
|
@@ -2206,6 +2374,23 @@ async function readProjectMap(boundary, artDirectory) {
|
|
|
2206
2374
|
await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, "map.json"));
|
|
2207
2375
|
return boundary.readText(path);
|
|
2208
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
|
+
}
|
|
2209
2394
|
async function sealProject(boundary, artDirectory) {
|
|
2210
2395
|
try {
|
|
2211
2396
|
const sealed = sealProjectArt(await readProjectArt(boundary, artDirectory));
|
|
@@ -2740,14 +2925,14 @@ const paeth = (a, b, c) => {
|
|
|
2740
2925
|
const pc = Math.abs(p - c);
|
|
2741
2926
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
2742
2927
|
};
|
|
2743
|
-
function unfilter(
|
|
2928
|
+
function unfilter(inflated2, header) {
|
|
2744
2929
|
const bpp = CHANNELS[header.colorType] * (header.depth / 8);
|
|
2745
2930
|
const stride = header.width * bpp;
|
|
2746
2931
|
const out = new Uint8Array(header.height * stride);
|
|
2747
2932
|
for (let y = 0; y < header.height; y += 1) {
|
|
2748
|
-
const filter =
|
|
2933
|
+
const filter = inflated2[y * (stride + 1)];
|
|
2749
2934
|
if (filter > 4) throw new PngError(`a row has filter type ${filter}, which PNG does not define`);
|
|
2750
|
-
const row =
|
|
2935
|
+
const row = inflated2.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
2751
2936
|
const at = y * stride;
|
|
2752
2937
|
for (let x = 0; x < stride; x += 1) {
|
|
2753
2938
|
const a = x >= bpp ? out[at + x - bpp] : 0;
|
|
@@ -2792,14 +2977,14 @@ function decodeWith(zlib, bytes) {
|
|
|
2792
2977
|
if (header === null) throw new PngError("the PNG has no IHDR chunk");
|
|
2793
2978
|
const bpp = CHANNELS[header.colorType] * (header.depth / 8);
|
|
2794
2979
|
const expected = header.height * (1 + header.width * bpp);
|
|
2795
|
-
let
|
|
2980
|
+
let inflated2;
|
|
2796
2981
|
try {
|
|
2797
|
-
|
|
2982
|
+
inflated2 = zlib.inflate(concat(data), expected);
|
|
2798
2983
|
} catch {
|
|
2799
2984
|
throw new PngError("the PNG's image data could not be inflated to the size its header promises");
|
|
2800
2985
|
}
|
|
2801
|
-
if (
|
|
2802
|
-
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);
|
|
2803
2988
|
}
|
|
2804
2989
|
function toRgba(samples, header, palette, transparency) {
|
|
2805
2990
|
const { width, height, depth, colorType } = header;
|
|
@@ -2949,7 +3134,8 @@ const exportCommand = {
|
|
|
2949
3134
|
try {
|
|
2950
3135
|
const boundary = new Boundary([art]);
|
|
2951
3136
|
const read = await readProjectArt(boundary, art);
|
|
2952
|
-
const
|
|
3137
|
+
const mirrored2 = await readMirroredProjectMap(boundary, art);
|
|
3138
|
+
const mapText = mirrored2 === null ? await readProjectMap(boundary, art) : null;
|
|
2953
3139
|
checkExportable(
|
|
2954
3140
|
read.pictures.map((picture) => {
|
|
2955
3141
|
if (!isPng(picture.bytes)) throw new CliError(`the picture ${picture.name} cannot be read: it is not a PNG`);
|
|
@@ -2967,7 +3153,7 @@ const exportCommand = {
|
|
|
2967
3153
|
{
|
|
2968
3154
|
pictures,
|
|
2969
3155
|
entries: read.entries,
|
|
2970
|
-
map: mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document,
|
|
3156
|
+
map: mirrored2 ?? (mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document),
|
|
2971
3157
|
idMap: read.idMap === null ? null : parseIdMap(JSON.parse(read.idMap))
|
|
2972
3158
|
},
|
|
2973
3159
|
engine,
|
|
@@ -2975,7 +3161,7 @@ const exportCommand = {
|
|
|
2975
3161
|
godotRoot === void 0 ? {} : { godotRoot }
|
|
2976
3162
|
);
|
|
2977
3163
|
} catch (error) {
|
|
2978
|
-
const refusals = [ProjectError, PackError, PngError, MapTextError, IdMapError, SyntaxError, CliError];
|
|
3164
|
+
const refusals = [ProjectError, PackError, PngError, MapTextError, MirrorError, IdMapError, SyntaxError, CliError];
|
|
2979
3165
|
if (refusals.some((kind) => error instanceof kind)) throw new CliError(`nothing was exported: ${error.message}`);
|
|
2980
3166
|
throw error;
|
|
2981
3167
|
}
|
|
@@ -3762,7 +3948,7 @@ const seal = {
|
|
|
3762
3948
|
answer(context, { sealed: true, output: result.output }, result.output);
|
|
3763
3949
|
}
|
|
3764
3950
|
};
|
|
3765
|
-
const version = "0.
|
|
3951
|
+
const version = "0.10.0";
|
|
3766
3952
|
const manifest = {
|
|
3767
3953
|
version
|
|
3768
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",
|