@pixelvibing/cli 0.9.0 → 0.11.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 +1268 -452
- package/package.json +1 -1
- package/skills/pixelvibing/SKILL.md +21 -10
package/dist/pixel-vibing-cli.js
CHANGED
|
@@ -4,8 +4,8 @@ import { realpath, promises } from "node:fs";
|
|
|
4
4
|
import { promisify, parseArgs } from "node:util";
|
|
5
5
|
import { randomBytes, createHash } from "node:crypto";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
|
-
import { stat, readFile, writeFile, mkdir, readdir, lstat, rm,
|
|
8
|
-
import { deflateSync, inflateSync } from "node:zlib";
|
|
7
|
+
import { stat, readFile, writeFile, mkdir, readdir, rename, lstat, rm, realpath as realpath$1 } from "node:fs/promises";
|
|
8
|
+
import { inflate, deflate, 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";
|
|
@@ -623,7 +623,7 @@ function bearerFetch(fetcher, apiUrl, token) {
|
|
|
623
623
|
}
|
|
624
624
|
function platformWith(fetcher, apiUrl, token) {
|
|
625
625
|
const carrying = bearerFetch(fetcher, apiUrl, token);
|
|
626
|
-
return { apiUrl, web: createWebApi(carrying, apiUrl), account: createAccountApi(carrying, apiUrl) };
|
|
626
|
+
return { apiUrl, web: createWebApi(carrying, apiUrl), account: createAccountApi(carrying, apiUrl), call: createCaller(carrying, apiUrl) };
|
|
627
627
|
}
|
|
628
628
|
async function signedIn(context) {
|
|
629
629
|
const apiUrl = apiUrlOf(context);
|
|
@@ -1016,66 +1016,6 @@ function nameProblem(name) {
|
|
|
1016
1016
|
}
|
|
1017
1017
|
return null;
|
|
1018
1018
|
}
|
|
1019
|
-
const PULLED_FILE = `${PROJECT_DIRECTORY}/pulled.json`;
|
|
1020
|
-
const PULLED_FORMAT = 1;
|
|
1021
|
-
const HEX64 = /^[0-9a-f]{64}$/;
|
|
1022
|
-
const UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
1023
|
-
const EMPTY_PULLED = { guide: null, assets: {} };
|
|
1024
|
-
class PulledError extends Error {
|
|
1025
|
-
constructor(message) {
|
|
1026
|
-
super(message);
|
|
1027
|
-
this.name = "PulledError";
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1031
|
-
function isPulledPath(path) {
|
|
1032
|
-
if (!path.startsWith("out/") || !path.endsWith(".png")) return false;
|
|
1033
|
-
const name = path.slice("out/".length, -".png".length);
|
|
1034
|
-
return !name.includes("\\") && nameProblem(name) === null;
|
|
1035
|
-
}
|
|
1036
|
-
function assetOf(id2, raw) {
|
|
1037
|
-
if (!UUID.test(id2)) throw new PulledError(`${PULLED_FILE}: ${id2} is not an asset's id`);
|
|
1038
|
-
if (!isRecord$2(raw)) throw new PulledError(`${PULLED_FILE}: the asset ${id2} is not an object`);
|
|
1039
|
-
const { kind, name, hash, files } = raw;
|
|
1040
|
-
if (typeof kind !== "string" || kind === "") throw new PulledError(`${PULLED_FILE}: the asset ${id2} names no kind`);
|
|
1041
|
-
if (typeof name !== "string" || name === "") throw new PulledError(`${PULLED_FILE}: the asset ${id2} names no name`);
|
|
1042
|
-
if (typeof hash !== "string" || !HEX64.test(hash)) throw new PulledError(`${PULLED_FILE}: the asset ${id2} has no hash`);
|
|
1043
|
-
if (!Array.isArray(files) || files.length === 0 || !files.every((file) => typeof file === "string" && isPulledPath(file))) {
|
|
1044
|
-
throw new PulledError(`${PULLED_FILE}: the asset ${id2} names a file that is not a picture under out/`);
|
|
1045
|
-
}
|
|
1046
|
-
return { kind, name, hash, files };
|
|
1047
|
-
}
|
|
1048
|
-
function guideOf(raw) {
|
|
1049
|
-
if (raw === void 0 || raw === null) return null;
|
|
1050
|
-
if (!isRecord$2(raw) || !Number.isInteger(raw.version) || raw.version < 1 || typeof raw.sha256 !== "string" || !HEX64.test(raw.sha256)) {
|
|
1051
|
-
throw new PulledError(`${PULLED_FILE}: the style guide's entry is not a version and a digest`);
|
|
1052
|
-
}
|
|
1053
|
-
return { version: raw.version, sha256: raw.sha256 };
|
|
1054
|
-
}
|
|
1055
|
-
function readPulled(text2) {
|
|
1056
|
-
let parsed2;
|
|
1057
|
-
try {
|
|
1058
|
-
parsed2 = JSON.parse(text2);
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
throw new PulledError(`${PULLED_FILE} is not valid JSON: ${error.message}`);
|
|
1061
|
-
}
|
|
1062
|
-
if (!isRecord$2(parsed2)) throw new PulledError(`${PULLED_FILE} is not an object`);
|
|
1063
|
-
if (parsed2.format !== PULLED_FORMAT) throw new PulledError(`${PULLED_FILE} names no format this CLI reads: expected "format": ${PULLED_FORMAT}`);
|
|
1064
|
-
const assets = {};
|
|
1065
|
-
if (parsed2.assets !== void 0) {
|
|
1066
|
-
if (!isRecord$2(parsed2.assets)) throw new PulledError(`${PULLED_FILE}: "assets" is not an object`);
|
|
1067
|
-
for (const [id2, raw] of Object.entries(parsed2.assets)) assets[id2] = assetOf(id2, raw);
|
|
1068
|
-
}
|
|
1069
|
-
return { guide: guideOf(parsed2.guide), assets };
|
|
1070
|
-
}
|
|
1071
|
-
function writePulled(record) {
|
|
1072
|
-
return `${JSON.stringify({ format: PULLED_FORMAT, ...record.guide === null ? {} : { guide: record.guide }, assets: record.assets }, null, 2)}
|
|
1073
|
-
`;
|
|
1074
|
-
}
|
|
1075
|
-
function ownerOf(record, file) {
|
|
1076
|
-
for (const [id2, asset] of Object.entries(record.assets)) if (asset.files.includes(file)) return id2;
|
|
1077
|
-
return null;
|
|
1078
|
-
}
|
|
1079
1019
|
const HUD_PIECE_NAMES = ["health-bar", "mana-bar", "button", "inventory-slot", "panel-frame", "minimap-frame"];
|
|
1080
1020
|
const ICON_SIZES = [16, 32, 64];
|
|
1081
1021
|
const DEFAULT_ICON_SIZE = 32;
|
|
@@ -1105,206 +1045,362 @@ function describedInterface(manifest2, kept, earlier) {
|
|
|
1105
1045
|
next[name] = { meta: { kind: "ui", ...piece.nineGrid === void 0 ? {} : { nineGrid: piece.nineGrid, scale: "stretch" } } };
|
|
1106
1046
|
return next;
|
|
1107
1047
|
}
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1048
|
+
const SECTOR_SIDE$1 = 128;
|
|
1049
|
+
const SECTOR_CELLS = SECTOR_SIDE$1 * SECTOR_SIDE$1;
|
|
1050
|
+
const SECTOR_BYTES = 512 * 1024;
|
|
1051
|
+
const MAP_LEVELS = { first: 0, last: 14 };
|
|
1052
|
+
const LARGEST_MAP_SIDE = 1e4;
|
|
1053
|
+
const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE$1);
|
|
1054
|
+
const SECTOR_FORMAT = 1;
|
|
1055
|
+
const MAGIC = [80, 86, 67];
|
|
1056
|
+
const PRESENT = 1;
|
|
1057
|
+
const AUDIO = 2;
|
|
1058
|
+
const INTERACT = 4;
|
|
1059
|
+
const EXTRA = 8;
|
|
1060
|
+
const U16 = 65535;
|
|
1061
|
+
const MAX_LAYERS = 255;
|
|
1062
|
+
const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
1063
|
+
class SectorError extends Error {
|
|
1064
|
+
constructor(message) {
|
|
1065
|
+
super(message);
|
|
1066
|
+
this.name = "SectorError";
|
|
1067
|
+
}
|
|
1111
1068
|
}
|
|
1112
|
-
function
|
|
1113
|
-
if (!
|
|
1114
|
-
|
|
1115
|
-
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
1069
|
+
function u16(value, what) {
|
|
1070
|
+
if (!Number.isInteger(value) || value < 0 || value > U16) throw new SectorError(`${what} is ${value}, and a sector holds a whole number from 0 to 65,535`);
|
|
1071
|
+
return value;
|
|
1116
1072
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1073
|
+
function encodeSector(cells) {
|
|
1074
|
+
if (cells.length !== SECTOR_CELLS) throw new SectorError(`a sector is ${SECTOR_CELLS} cells, not ${cells.length}`);
|
|
1075
|
+
const encoder2 = new TextEncoder();
|
|
1076
|
+
const bytes = [...MAGIC, SECTOR_FORMAT];
|
|
1077
|
+
const push16 = (value) => bytes.push(value & 255, value >> 8);
|
|
1078
|
+
for (const cell of cells) {
|
|
1079
|
+
if (cell === void 0) {
|
|
1080
|
+
bytes.push(0);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const extra = cell.extra === void 0 ? null : encoder2.encode(JSON.stringify(cell.extra));
|
|
1084
|
+
if (extra !== null && extra.length > U16) throw new SectorError("a tile's extra fields are past 65,535 bytes");
|
|
1085
|
+
if (cell.layers.length > MAX_LAYERS) throw new SectorError(`a tile has ${cell.layers.length} layers, and a sector holds at most ${MAX_LAYERS}`);
|
|
1086
|
+
bytes.push(PRESENT | (cell.audioZone === void 0 ? 0 : AUDIO) | (cell.interact === void 0 ? 0 : INTERACT) | (extra === null ? 0 : EXTRA));
|
|
1087
|
+
bytes.push(cell.layers.length);
|
|
1088
|
+
for (const layer of cell.layers) push16(u16(layer, "a layer id"));
|
|
1089
|
+
push16(u16(cell.blocking, "blocking"));
|
|
1090
|
+
push16(u16(cell.zone, "a zone"));
|
|
1091
|
+
if (cell.audioZone !== void 0) push16(u16(cell.audioZone, "an audio zone"));
|
|
1092
|
+
if (cell.interact !== void 0) push16(u16(cell.interact, "interact"));
|
|
1093
|
+
if (extra !== null) {
|
|
1094
|
+
push16(extra.length);
|
|
1095
|
+
for (const byte of extra) bytes.push(byte);
|
|
1096
|
+
}
|
|
1131
1097
|
}
|
|
1098
|
+
return Uint8Array.from(bytes);
|
|
1132
1099
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1100
|
+
function decodeSector(bytes) {
|
|
1101
|
+
if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
|
|
1102
|
+
if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
|
|
1103
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1104
|
+
let at = 4;
|
|
1105
|
+
const need = (count) => {
|
|
1106
|
+
if (at + count > bytes.length) throw new SectorError("the sector's bytes end before its last cell");
|
|
1107
|
+
};
|
|
1108
|
+
const read8 = () => {
|
|
1109
|
+
need(1);
|
|
1110
|
+
return bytes[at++];
|
|
1111
|
+
};
|
|
1112
|
+
const read16 = () => {
|
|
1113
|
+
need(2);
|
|
1114
|
+
const value = bytes[at] | bytes[at + 1] << 8;
|
|
1115
|
+
at += 2;
|
|
1116
|
+
return value;
|
|
1117
|
+
};
|
|
1118
|
+
const cells = [];
|
|
1119
|
+
for (let index = 0; index < SECTOR_CELLS; index += 1) {
|
|
1120
|
+
const flags = read8();
|
|
1121
|
+
if ((flags & PRESENT) === 0) {
|
|
1122
|
+
cells.push(void 0);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const count = read8();
|
|
1126
|
+
const layers = [];
|
|
1127
|
+
for (let layer = 0; layer < count; layer += 1) layers.push(read16());
|
|
1128
|
+
const blocking = read16();
|
|
1129
|
+
const zone = read16();
|
|
1130
|
+
const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
|
|
1131
|
+
const interact = (flags & INTERACT) === 0 ? void 0 : read16();
|
|
1132
|
+
let extra;
|
|
1133
|
+
if ((flags & EXTRA) !== 0) {
|
|
1134
|
+
const length = read16();
|
|
1135
|
+
need(length);
|
|
1136
|
+
let parsed2;
|
|
1137
|
+
try {
|
|
1138
|
+
parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
|
|
1139
|
+
} catch {
|
|
1140
|
+
throw new SectorError("a tile's extra fields are not JSON");
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
|
|
1143
|
+
const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
|
|
1144
|
+
if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
|
|
1145
|
+
extra = parsed2;
|
|
1146
|
+
at += length;
|
|
1147
|
+
}
|
|
1148
|
+
cells.push({
|
|
1149
|
+
layers,
|
|
1150
|
+
blocking,
|
|
1151
|
+
zone,
|
|
1152
|
+
...audioZone === void 0 ? {} : { audioZone },
|
|
1153
|
+
...interact === void 0 ? {} : { interact },
|
|
1154
|
+
...extra === void 0 ? {} : { extra }
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
|
|
1158
|
+
return cells;
|
|
1135
1159
|
}
|
|
1136
|
-
const
|
|
1137
|
-
function
|
|
1138
|
-
|
|
1160
|
+
const NAMED = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
1161
|
+
function cellOfTile(tile) {
|
|
1162
|
+
const extra = Object.fromEntries(Object.entries(tile).filter(([key]) => !NAMED.has(key)));
|
|
1163
|
+
return {
|
|
1164
|
+
layers: tile.layers,
|
|
1165
|
+
blocking: tile.blocking,
|
|
1166
|
+
zone: tile.zone,
|
|
1167
|
+
...tile.audioZone === void 0 ? {} : { audioZone: tile.audioZone },
|
|
1168
|
+
...tile.interact === void 0 ? {} : { interact: tile.interact },
|
|
1169
|
+
...Object.keys(extra).length === 0 ? {} : { extra }
|
|
1170
|
+
};
|
|
1139
1171
|
}
|
|
1140
|
-
function
|
|
1141
|
-
return
|
|
1172
|
+
function tileOfCell(cell, x, y, z) {
|
|
1173
|
+
return {
|
|
1174
|
+
...cell.extra ?? {},
|
|
1175
|
+
x,
|
|
1176
|
+
y,
|
|
1177
|
+
z,
|
|
1178
|
+
layers: [...cell.layers],
|
|
1179
|
+
blocking: cell.blocking,
|
|
1180
|
+
zone: cell.zone,
|
|
1181
|
+
...cell.audioZone === void 0 ? {} : { audioZone: cell.audioZone },
|
|
1182
|
+
...cell.interact === void 0 ? {} : { interact: cell.interact }
|
|
1183
|
+
};
|
|
1142
1184
|
}
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1185
|
+
const MIRROR_MAP_DIRECTORY = "map";
|
|
1186
|
+
const MIRROR_INDEX_FILE = `${MIRROR_MAP_DIRECTORY}/index.json`;
|
|
1187
|
+
const MIRROR_FORMAT = 1;
|
|
1188
|
+
class MirrorError extends Error {
|
|
1189
|
+
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
1190
|
+
// properties (docs/notes.md n-0023).
|
|
1191
|
+
constructor(message) {
|
|
1192
|
+
super(message);
|
|
1193
|
+
this.name = "MirrorError";
|
|
1194
|
+
}
|
|
1146
1195
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (problem !== null) throw new CliError(`${name} is not a name a picture can be written under (${problem}): give one with --as`);
|
|
1151
|
-
return name;
|
|
1196
|
+
function mirrorSectorPath(z, cx, cy) {
|
|
1197
|
+
if (!isPlace(z, cx, cy)) throw new MirrorError(`no sector of a map is at level ${z}, column ${cx}, row ${cy}`);
|
|
1198
|
+
return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
|
|
1152
1199
|
}
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1200
|
+
function isPlace(z, cx, cy) {
|
|
1201
|
+
const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
|
|
1202
|
+
return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
|
|
1203
|
+
}
|
|
1204
|
+
function isSize(value) {
|
|
1205
|
+
const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
|
|
1206
|
+
return typeof value === "object" && value !== null && side(value.width) && side(value.height);
|
|
1207
|
+
}
|
|
1208
|
+
function mirrorIndexText(index) {
|
|
1209
|
+
return `${JSON.stringify(index, null, 2)}
|
|
1210
|
+
`;
|
|
1211
|
+
}
|
|
1212
|
+
function parseMirrorIndex(text2) {
|
|
1213
|
+
let value;
|
|
1214
|
+
try {
|
|
1215
|
+
value = JSON.parse(text2);
|
|
1216
|
+
} catch {
|
|
1217
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
|
|
1218
|
+
}
|
|
1219
|
+
const index = value;
|
|
1220
|
+
if (typeof index !== "object" || index === null) throw new MirrorError(`${MIRROR_INDEX_FILE} is not an object`);
|
|
1221
|
+
if (index.format !== MIRROR_FORMAT)
|
|
1222
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
|
|
1223
|
+
if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
|
|
1224
|
+
if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
|
|
1225
|
+
if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
|
|
1226
|
+
for (const named of index.names) {
|
|
1227
|
+
if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
|
|
1168
1228
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
);
|
|
1229
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1230
|
+
for (const sector of index.sectors) {
|
|
1231
|
+
const [z, cx, cy, version2] = Array.isArray(sector) ? sector : [];
|
|
1232
|
+
if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
|
|
1233
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
|
|
1175
1234
|
}
|
|
1235
|
+
const key = `${z}:${cx}:${cy}`;
|
|
1236
|
+
if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
|
|
1237
|
+
throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
|
|
1238
|
+
}
|
|
1239
|
+
if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
|
|
1240
|
+
seen.add(key);
|
|
1176
1241
|
}
|
|
1177
|
-
|
|
1178
|
-
for (const image of asset.images) {
|
|
1179
|
-
const bytes = await platform.web.imageBytes(image.id);
|
|
1180
|
-
if (!isPng(bytes)) throw new CliError(`the platform answered something that is not a PNG for the image ${image.id}: nothing was written`);
|
|
1181
|
-
images.push(bytes);
|
|
1182
|
-
}
|
|
1183
|
-
for (const [index, file] of files.entries()) await boundary.writeFileReplacing(join(art, ...file.split("/")), images[index]);
|
|
1184
|
-
for (const stale of before?.files ?? []) {
|
|
1185
|
-
if (!files.includes(stale)) await boundary.removeFile(join(art, ...stale.split("/")));
|
|
1186
|
-
}
|
|
1187
|
-
if (pieces !== null || before?.files.some((file) => file.startsWith("out/ui/"))) {
|
|
1188
|
-
await describeInterface(boundary, art, pieces === null ? [] : files.map((file, index) => [file, pieces[index]]), before?.files ?? []);
|
|
1189
|
-
}
|
|
1190
|
-
await writeRecord(local, { ...record, assets: { ...record.assets, [asset.id]: { kind: asset.kind, name, hash: asset.hash, files } } });
|
|
1191
|
-
return { ...pulled, current: false };
|
|
1242
|
+
return index;
|
|
1192
1243
|
}
|
|
1193
|
-
async function
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1244
|
+
async function readMirroredMap(index, read, inflate2) {
|
|
1245
|
+
const tiles = [];
|
|
1246
|
+
for (const [z, cx, cy] of index.sectors) {
|
|
1247
|
+
const path = mirrorSectorPath(z, cx, cy);
|
|
1248
|
+
let cells;
|
|
1197
1249
|
try {
|
|
1198
|
-
|
|
1199
|
-
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) manifest2 = parsed2;
|
|
1200
|
-
else throw new Error("not an object");
|
|
1250
|
+
cells = decodeSector(await inflate2(await read(path)));
|
|
1201
1251
|
} catch (error) {
|
|
1202
|
-
throw new
|
|
1252
|
+
throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
|
|
1253
|
+
}
|
|
1254
|
+
for (const [at, cell] of cells.entries()) {
|
|
1255
|
+
if (cell === void 0) continue;
|
|
1256
|
+
const x = cx * SECTOR_SIDE$1 + at % SECTOR_SIDE$1;
|
|
1257
|
+
const y = cy * SECTOR_SIDE$1 + Math.floor(at / SECTOR_SIDE$1);
|
|
1258
|
+
if (x >= index.size.width || y >= index.size.height) continue;
|
|
1259
|
+
tiles.push(tileOfCell(cell, x, y, z));
|
|
1203
1260
|
}
|
|
1204
1261
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
written.map(([file, piece]) => [pictureName(file), piece]),
|
|
1208
|
-
earlier.map(pictureName)
|
|
1209
|
-
);
|
|
1210
|
-
await boundary.writeFileReplacing(path, new TextEncoder().encode(`${JSON.stringify(manifest2, null, 2)}
|
|
1211
|
-
`));
|
|
1262
|
+
tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
|
|
1263
|
+
return { width: index.size.width, height: index.size.height, tiles };
|
|
1212
1264
|
}
|
|
1213
|
-
function
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1265
|
+
function sectorsOfMap(map) {
|
|
1266
|
+
const sectors = /* @__PURE__ */ new Map();
|
|
1267
|
+
for (const tile of map.tiles) {
|
|
1268
|
+
const { x, y, z } = tile;
|
|
1269
|
+
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= LARGEST_MAP_SIDE || y >= LARGEST_MAP_SIDE || !isPlace(z, 0, 0)) {
|
|
1270
|
+
throw new MirrorError(`the tile at (${x}, ${y}) on level ${z} is at no place a map has`);
|
|
1271
|
+
}
|
|
1272
|
+
const cx = Math.floor(x / SECTOR_SIDE$1);
|
|
1273
|
+
const cy = Math.floor(y / SECTOR_SIDE$1);
|
|
1274
|
+
const key = `${z}:${cx}:${cy}`;
|
|
1275
|
+
let sector = sectors.get(key);
|
|
1276
|
+
if (sector === void 0) {
|
|
1277
|
+
sector = { z, cx, cy, cells: new Array(SECTOR_CELLS) };
|
|
1278
|
+
sectors.set(key, sector);
|
|
1279
|
+
}
|
|
1280
|
+
sector.cells[y % SECTOR_SIDE$1 * SECTOR_SIDE$1 + x % SECTOR_SIDE$1] = cellOfTile(tile);
|
|
1281
|
+
}
|
|
1282
|
+
return [...sectors.values()].sort((a, b) => a.z - b.z || a.cy - b.cy || a.cx - b.cx);
|
|
1223
1283
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
run: async (context, args) => {
|
|
1231
|
-
const local = await projectHere(context.io.cwd);
|
|
1232
|
-
const project = linkedOf(local);
|
|
1233
|
-
const platform = await signedIn(context);
|
|
1234
|
-
const [kind] = args.positionals;
|
|
1235
|
-
const page = await platform.web.assets(project, kind ?? null, stringOf(args, "before") ?? null);
|
|
1236
|
-
const more = page.next === null ? "" : `
|
|
1237
|
-
More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`} --before ${page.next}`;
|
|
1238
|
-
answer(context, page, `${listed(page.assets)}${more}`);
|
|
1284
|
+
class MapTextError extends Error {
|
|
1285
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1286
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1287
|
+
constructor(message) {
|
|
1288
|
+
super(message);
|
|
1289
|
+
this.name = "MapTextError";
|
|
1239
1290
|
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1291
|
+
}
|
|
1292
|
+
function tileKey(x, y, z) {
|
|
1293
|
+
return `${x}:${y}:${z}`;
|
|
1294
|
+
}
|
|
1295
|
+
function scanTileSources(text2) {
|
|
1296
|
+
const key = '"tiles"';
|
|
1297
|
+
let at = text2.indexOf(key);
|
|
1298
|
+
if (at === -1) return [];
|
|
1299
|
+
at = text2.indexOf("[", at + key.length);
|
|
1300
|
+
if (at === -1) return [];
|
|
1301
|
+
const sources = [];
|
|
1302
|
+
let depth = 0;
|
|
1303
|
+
let inString = false;
|
|
1304
|
+
let start = -1;
|
|
1305
|
+
for (let index = at + 1; index < text2.length; index += 1) {
|
|
1306
|
+
const char = text2[index];
|
|
1307
|
+
if (inString) {
|
|
1308
|
+
if (char === "\\") index += 1;
|
|
1309
|
+
else if (char === '"') inString = false;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (char === '"') {
|
|
1313
|
+
inString = true;
|
|
1314
|
+
} else if (char === "{") {
|
|
1315
|
+
if (depth === 0) start = index;
|
|
1316
|
+
depth += 1;
|
|
1317
|
+
} else if (char === "}") {
|
|
1318
|
+
depth -= 1;
|
|
1319
|
+
if (depth === 0 && start !== -1) {
|
|
1320
|
+
sources.push(text2.slice(start, index + 1));
|
|
1321
|
+
start = -1;
|
|
1322
|
+
}
|
|
1323
|
+
} else if (char === "]" && depth === 0) {
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1253
1326
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1327
|
+
return sources;
|
|
1328
|
+
}
|
|
1329
|
+
function isTile(value) {
|
|
1330
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1331
|
+
const record = value;
|
|
1332
|
+
return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
|
|
1333
|
+
}
|
|
1334
|
+
function parseMapText(text2, path = "the map") {
|
|
1335
|
+
let parsed2;
|
|
1258
1336
|
try {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
return false;
|
|
1337
|
+
parsed2 = JSON.parse(text2);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
|
|
1263
1340
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const named = env[EDITOR_VARIABLE]?.trim();
|
|
1267
|
-
if (named !== void 0 && named !== "") {
|
|
1268
|
-
if (!await exists$1(named)) throw new CliError(`${EDITOR_VARIABLE} names ${named}, which is not there`);
|
|
1269
|
-
return platform === "darwin" && named.endsWith(".app") ? { command: "open", args: ["-a", named, "--args"] } : { command: named, args: [] };
|
|
1341
|
+
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
1342
|
+
throw new MapTextError(`${path} holds no tiles array`);
|
|
1270
1343
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
return null;
|
|
1344
|
+
const document = parsed2;
|
|
1345
|
+
const sources = scanTileSources(text2);
|
|
1346
|
+
if (sources.length !== document.tiles.length) {
|
|
1347
|
+
throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
|
|
1277
1348
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1349
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1350
|
+
document.tiles.forEach((tile, index) => {
|
|
1351
|
+
if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
|
|
1352
|
+
byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
|
|
1353
|
+
});
|
|
1354
|
+
return { document, sources: byKey };
|
|
1355
|
+
}
|
|
1356
|
+
const TILE_KEYS = ["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"];
|
|
1357
|
+
function serializeTile(tile) {
|
|
1358
|
+
const ordered = {};
|
|
1359
|
+
for (const key of TILE_KEYS) {
|
|
1360
|
+
if (tile[key] !== void 0) ordered[key] = tile[key];
|
|
1283
1361
|
}
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
try {
|
|
1287
|
-
found = (await readdir(applications)).filter((name) => /^PixelVibing.*\.AppImage$/i.test(name)).sort();
|
|
1288
|
-
} catch {
|
|
1289
|
-
return null;
|
|
1362
|
+
for (const key of Object.keys(tile)) {
|
|
1363
|
+
if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
|
|
1290
1364
|
}
|
|
1291
|
-
|
|
1292
|
-
return newest === void 0 ? null : { command: join(applications, newest), args: [] };
|
|
1365
|
+
return JSON.stringify(ordered);
|
|
1293
1366
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
if (found === null) {
|
|
1302
|
-
throw new CliError(`the PixelVibing desktop app is not installed here: download it from ${DOWNLOAD_URL}, or set ${EDITOR_VARIABLE} to where it is`);
|
|
1367
|
+
function formatMap(next, previous) {
|
|
1368
|
+
const lines = [];
|
|
1369
|
+
let changed = 0;
|
|
1370
|
+
for (const tile of next.tiles) {
|
|
1371
|
+
{
|
|
1372
|
+
lines.push(serializeTile(tile));
|
|
1373
|
+
changed += 1;
|
|
1303
1374
|
}
|
|
1304
|
-
await context.services.launch(found.command, [...found.args, "--project", local.root]);
|
|
1305
|
-
answer(context, { opened: local.root, app: found.command }, `Opening ${local.root} in PixelVibing.`);
|
|
1306
1375
|
}
|
|
1307
|
-
};
|
|
1376
|
+
const rest = {};
|
|
1377
|
+
for (const key of Object.keys(next)) {
|
|
1378
|
+
if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
|
|
1379
|
+
}
|
|
1380
|
+
const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
|
|
1381
|
+
const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
|
|
1382
|
+
const body = lines.length === 0 ? "" : `
|
|
1383
|
+
${lines.join(",\n")}
|
|
1384
|
+
`;
|
|
1385
|
+
return { text: `${head}${body}]${trailer}}
|
|
1386
|
+
`, changed };
|
|
1387
|
+
}
|
|
1388
|
+
const SIGNATURE$1 = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1389
|
+
function isPng(bytes) {
|
|
1390
|
+
return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
|
|
1391
|
+
}
|
|
1392
|
+
function pngSize(bytes) {
|
|
1393
|
+
if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
|
|
1394
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1395
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
1396
|
+
}
|
|
1397
|
+
const GROUND_KINDS = /* @__PURE__ */ new Set(["ground", "floor", "wall", "cliff", "interior", "grass-stone"]);
|
|
1398
|
+
function isGroundName(name) {
|
|
1399
|
+
const { inner } = splitTileset(name);
|
|
1400
|
+
const kind = inner.split("/")[1] ?? "";
|
|
1401
|
+
return GROUND_KINDS.has(kind) || inner.includes("/wang-");
|
|
1402
|
+
}
|
|
1403
|
+
const MAX_ASSEMBLED_PX = 4096;
|
|
1308
1404
|
const TILE_SIZE = 256;
|
|
1309
1405
|
const EAI_MAGIC = 1229018181;
|
|
1310
1406
|
const EAI_VERSION = 1;
|
|
@@ -1559,7 +1655,7 @@ class AssetMetadataError extends Error {
|
|
|
1559
1655
|
this.name = "AssetMetadataError";
|
|
1560
1656
|
}
|
|
1561
1657
|
}
|
|
1562
|
-
function isRecord$
|
|
1658
|
+
function isRecord$2(value) {
|
|
1563
1659
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1564
1660
|
}
|
|
1565
1661
|
const MAX_ANIMATION_FRAMES = 64;
|
|
@@ -1571,7 +1667,7 @@ function readFrames(value, asset, animation) {
|
|
|
1571
1667
|
throw new AssetMetadataError(`${asset}: animation ${animation} has ${value.length} frames, and an animation holds at most ${MAX_ANIMATION_FRAMES}`);
|
|
1572
1668
|
}
|
|
1573
1669
|
return value.map((raw, index) => {
|
|
1574
|
-
if (!isRecord$
|
|
1670
|
+
if (!isRecord$2(raw)) {
|
|
1575
1671
|
throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} is not a frame`);
|
|
1576
1672
|
}
|
|
1577
1673
|
const name = raw.asset;
|
|
@@ -1590,7 +1686,7 @@ function readFrames(value, asset, animation) {
|
|
|
1590
1686
|
function readAnimations(value, asset, kind) {
|
|
1591
1687
|
const animations = /* @__PURE__ */ new Map();
|
|
1592
1688
|
if (value === void 0) return animations;
|
|
1593
|
-
if (!isRecord$
|
|
1689
|
+
if (!isRecord$2(value)) {
|
|
1594
1690
|
throw new AssetMetadataError(`${asset}: animations is not a map of name to frames`);
|
|
1595
1691
|
}
|
|
1596
1692
|
for (const [name, frames] of Object.entries(value)) {
|
|
@@ -1607,10 +1703,10 @@ function readAnimations(value, asset, kind) {
|
|
|
1607
1703
|
function readPlays(value, asset, animations, kind) {
|
|
1608
1704
|
const plays = /* @__PURE__ */ new Map();
|
|
1609
1705
|
if (value === void 0) return plays;
|
|
1610
|
-
if (!isRecord$
|
|
1706
|
+
if (!isRecord$2(value)) throw new AssetMetadataError(`${asset}: plays is not a map of animation to how it plays`);
|
|
1611
1707
|
for (const [name, play] of Object.entries(value)) {
|
|
1612
1708
|
if (!animations.has(name)) throw new AssetMetadataError(`${asset}: plays names ${name}, and there is no such animation`);
|
|
1613
|
-
if (!isRecord$
|
|
1709
|
+
if (!isRecord$2(play)) throw new AssetMetadataError(`${asset}: how ${name} plays is not an object`);
|
|
1614
1710
|
if (kind === "outfit" && play.mode !== "loop")
|
|
1615
1711
|
throw new AssetMetadataError(`${asset}: ${name} is an outfit's animation, which plays as the game drives it, in a loop`);
|
|
1616
1712
|
if (play.mode === "loop" || play.mode === "change") {
|
|
@@ -1633,7 +1729,7 @@ function readPlays(value, asset, animations, kind) {
|
|
|
1633
1729
|
}
|
|
1634
1730
|
function readNineGrid(value, asset) {
|
|
1635
1731
|
if (value === void 0) return void 0;
|
|
1636
|
-
if (!isRecord$
|
|
1732
|
+
if (!isRecord$2(value)) {
|
|
1637
1733
|
throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
|
|
1638
1734
|
}
|
|
1639
1735
|
const insets = {};
|
|
@@ -1658,7 +1754,7 @@ function readScale(value, asset) {
|
|
|
1658
1754
|
}
|
|
1659
1755
|
function readCollide(value, asset) {
|
|
1660
1756
|
if (value === void 0) return void 0;
|
|
1661
|
-
if (!isRecord$
|
|
1757
|
+
if (!isRecord$2(value)) {
|
|
1662
1758
|
throw new AssetMetadataError(`${asset}: collide is not a box of x, y, w and h in cells`);
|
|
1663
1759
|
}
|
|
1664
1760
|
const box = {};
|
|
@@ -1679,7 +1775,7 @@ const MIN_DRAW_SCALE = 0.05;
|
|
|
1679
1775
|
const MAX_DRAW_SCALE = 8;
|
|
1680
1776
|
function readOffset(value, asset) {
|
|
1681
1777
|
if (value === void 0) return void 0;
|
|
1682
|
-
if (!isRecord$
|
|
1778
|
+
if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
1683
1779
|
throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
|
|
1684
1780
|
}
|
|
1685
1781
|
for (const axis of ["x", "y"]) {
|
|
@@ -1701,7 +1797,7 @@ function readDrawScale(value, asset) {
|
|
|
1701
1797
|
return value;
|
|
1702
1798
|
}
|
|
1703
1799
|
function readEntry(value, asset) {
|
|
1704
|
-
if (!isRecord$
|
|
1800
|
+
if (!isRecord$2(value)) {
|
|
1705
1801
|
throw new AssetMetadataError(`${asset}: not an entry`);
|
|
1706
1802
|
}
|
|
1707
1803
|
const kind = value.kind;
|
|
@@ -1746,7 +1842,7 @@ function parseAssetMetadata(text2) {
|
|
|
1746
1842
|
} catch (error) {
|
|
1747
1843
|
throw new AssetMetadataError(`not JSON: ${error.message}`);
|
|
1748
1844
|
}
|
|
1749
|
-
if (!isRecord$
|
|
1845
|
+
if (!isRecord$2(parsed2)) {
|
|
1750
1846
|
throw new AssetMetadataError("not an object");
|
|
1751
1847
|
}
|
|
1752
1848
|
const version2 = parsed2.version;
|
|
@@ -1754,7 +1850,7 @@ function parseAssetMetadata(text2) {
|
|
|
1754
1850
|
throw new AssetMetadataError(`unsupported version ${String(version2)}, reader speaks v${METADATA_VERSION}`);
|
|
1755
1851
|
}
|
|
1756
1852
|
const rawAssets = parsed2.assets;
|
|
1757
|
-
if (rawAssets !== void 0 && !isRecord$
|
|
1853
|
+
if (rawAssets !== void 0 && !isRecord$2(rawAssets)) {
|
|
1758
1854
|
throw new AssetMetadataError("assets is not a map of name to entry");
|
|
1759
1855
|
}
|
|
1760
1856
|
const assets = /* @__PURE__ */ new Map();
|
|
@@ -1949,117 +2045,67 @@ function placeTileSprite(tx, ty, width, height, options = {}) {
|
|
|
1949
2045
|
height: scaledHeight
|
|
1950
2046
|
};
|
|
1951
2047
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
function tileKey(x, y, z) {
|
|
1961
|
-
return `${x}:${y}:${z}`;
|
|
1962
|
-
}
|
|
1963
|
-
function scanTileSources(text2) {
|
|
1964
|
-
const key = '"tiles"';
|
|
1965
|
-
let at = text2.indexOf(key);
|
|
1966
|
-
if (at === -1) return [];
|
|
1967
|
-
at = text2.indexOf("[", at + key.length);
|
|
1968
|
-
if (at === -1) return [];
|
|
1969
|
-
const sources = [];
|
|
1970
|
-
let depth = 0;
|
|
1971
|
-
let inString = false;
|
|
1972
|
-
let start = -1;
|
|
1973
|
-
for (let index = at + 1; index < text2.length; index += 1) {
|
|
1974
|
-
const char = text2[index];
|
|
1975
|
-
if (inString) {
|
|
1976
|
-
if (char === "\\") index += 1;
|
|
1977
|
-
else if (char === '"') inString = false;
|
|
1978
|
-
continue;
|
|
1979
|
-
}
|
|
1980
|
-
if (char === '"') {
|
|
1981
|
-
inString = true;
|
|
1982
|
-
} else if (char === "{") {
|
|
1983
|
-
if (depth === 0) start = index;
|
|
1984
|
-
depth += 1;
|
|
1985
|
-
} else if (char === "}") {
|
|
1986
|
-
depth -= 1;
|
|
1987
|
-
if (depth === 0 && start !== -1) {
|
|
1988
|
-
sources.push(text2.slice(start, index + 1));
|
|
1989
|
-
start = -1;
|
|
1990
|
-
}
|
|
1991
|
-
} else if (char === "]" && depth === 0) {
|
|
1992
|
-
break;
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
return sources;
|
|
1996
|
-
}
|
|
1997
|
-
function isTile(value) {
|
|
1998
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1999
|
-
const record = value;
|
|
2000
|
-
return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
|
|
2001
|
-
}
|
|
2002
|
-
function parseMapText(text2, path = "the map") {
|
|
2003
|
-
let parsed2;
|
|
2004
|
-
try {
|
|
2005
|
-
parsed2 = JSON.parse(text2);
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
|
|
2008
|
-
}
|
|
2009
|
-
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
2010
|
-
throw new MapTextError(`${path} holds no tiles array`);
|
|
2011
|
-
}
|
|
2012
|
-
const document = parsed2;
|
|
2013
|
-
const sources = scanTileSources(text2);
|
|
2014
|
-
if (sources.length !== document.tiles.length) {
|
|
2015
|
-
throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
|
|
2048
|
+
const WINDOW_PX = { width: 1536, height: 1024 };
|
|
2049
|
+
const SCALES = [TILE_SIZE, TILE_SIZE / 2, TILE_SIZE / 4, TILE_SIZE / 8, TILE_SIZE / 16];
|
|
2050
|
+
function scaleFor(tilesWide, tilesHigh) {
|
|
2051
|
+
if (tilesWide <= 0 || tilesHigh <= 0) return null;
|
|
2052
|
+
for (const scale of SCALES) {
|
|
2053
|
+
if (tilesWide * scale <= WINDOW_PX.width && tilesHigh * scale <= WINDOW_PX.height) return scale;
|
|
2016
2054
|
}
|
|
2017
|
-
|
|
2018
|
-
document.tiles.forEach((tile, index) => {
|
|
2019
|
-
if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
|
|
2020
|
-
byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
|
|
2021
|
-
});
|
|
2022
|
-
return { document, sources: byKey };
|
|
2055
|
+
return null;
|
|
2023
2056
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2057
|
+
({
|
|
2058
|
+
width: Math.floor(WINDOW_PX.width / SCALES.at(-1)),
|
|
2059
|
+
height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
|
|
2060
|
+
});
|
|
2061
|
+
Array.from({ length: 16 }, (_value, index) => index);
|
|
2062
|
+
const REFERENCE_FILE = /^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*\.png$/i;
|
|
2063
|
+
class ArtError extends Error {
|
|
2064
|
+
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
2065
|
+
// properties (docs/notes.md n-0023).
|
|
2066
|
+
constructor(message) {
|
|
2067
|
+
super(message);
|
|
2068
|
+
this.name = "ArtError";
|
|
2032
2069
|
}
|
|
2033
|
-
return JSON.stringify(ordered);
|
|
2034
2070
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2071
|
+
const CONTAINER_FILE = join("build", "assets.eac");
|
|
2072
|
+
const STYLE_GUIDE_FILE$1 = "style-guide.md";
|
|
2073
|
+
const REFERENCE_DIRECTORY = "ref";
|
|
2074
|
+
async function listReferences(boundary, artDirectory) {
|
|
2075
|
+
const root = join(artDirectory, REFERENCE_DIRECTORY);
|
|
2076
|
+
const names = [];
|
|
2077
|
+
const walk = async (directory, prefix) => {
|
|
2078
|
+
let entries;
|
|
2079
|
+
try {
|
|
2080
|
+
entries = await readdir(await boundary.resolve(directory), { withFileTypes: true });
|
|
2081
|
+
} catch {
|
|
2082
|
+
return;
|
|
2042
2083
|
}
|
|
2084
|
+
for (const entry of entries) {
|
|
2085
|
+
const relative2 = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
2086
|
+
if (entry.isDirectory()) await walk(join(directory, entry.name), relative2);
|
|
2087
|
+
else if (entry.name.toLowerCase().endsWith(".png")) names.push(relative2);
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
await walk(root, "");
|
|
2091
|
+
return names.sort();
|
|
2092
|
+
}
|
|
2093
|
+
async function readReference(boundary, artDirectory, name) {
|
|
2094
|
+
if (!REFERENCE_FILE.test(name) || name.split("/").includes("..")) {
|
|
2095
|
+
throw new ArtError(`${name} is not a reference under ${REFERENCE_DIRECTORY}/`);
|
|
2043
2096
|
}
|
|
2044
|
-
|
|
2045
|
-
for (const key of Object.keys(next)) {
|
|
2046
|
-
if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
|
|
2047
|
-
}
|
|
2048
|
-
const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
|
|
2049
|
-
const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
|
|
2050
|
-
const body = lines.length === 0 ? "" : `
|
|
2051
|
-
${lines.join(",\n")}
|
|
2052
|
-
`;
|
|
2053
|
-
return { text: `${head}${body}]${trailer}}
|
|
2054
|
-
`, changed };
|
|
2097
|
+
return boundary.readBytes(join(artDirectory, REFERENCE_DIRECTORY, name));
|
|
2055
2098
|
}
|
|
2099
|
+
const MANIFEST_DIRECTORY = "manifest";
|
|
2100
|
+
const OUT_DIRECTORY = "out";
|
|
2101
|
+
const ID_MAP_FILE = "id-map.json";
|
|
2056
2102
|
const MAX_SIDE$1 = 65535;
|
|
2057
2103
|
const encoder$1 = new TextEncoder();
|
|
2058
|
-
function isRecord(value) {
|
|
2104
|
+
function isRecord$1(value) {
|
|
2059
2105
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2060
2106
|
}
|
|
2061
2107
|
function metadataOf(entry) {
|
|
2062
|
-
const raw = isRecord(entry.meta) && entry.meta.kind === void 0 ? { kind: "tile", ...entry.meta } : entry.meta;
|
|
2108
|
+
const raw = isRecord$1(entry.meta) && entry.meta.kind === void 0 ? { kind: "tile", ...entry.meta } : entry.meta;
|
|
2063
2109
|
try {
|
|
2064
2110
|
const parsed2 = parseAssetMetadata(JSON.stringify({ version: METADATA_VERSION, assets: { [entry.name]: raw } }));
|
|
2065
2111
|
return parsed2.assets.get(entry.name);
|
|
@@ -2109,115 +2155,737 @@ function sealProjectArt(art) {
|
|
|
2109
2155
|
}
|
|
2110
2156
|
return { container: sealContainer([...entries, ...pages]), pictures: pictures.length, described: described.size };
|
|
2111
2157
|
}
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2158
|
+
const PROJECT_SEAL_STEP = "project:seal";
|
|
2159
|
+
const MAX_PICTURE_BYTES = 64 * 1024 * 1024;
|
|
2160
|
+
const MAX_TEXT_BYTES = 16 * 1024 * 1024;
|
|
2161
|
+
const MAX_PICTURES = 2e4;
|
|
2162
|
+
function shown(artDirectory, relative2) {
|
|
2163
|
+
return [basename(artDirectory), ...relative2.split(/[\\/]/)].join("/");
|
|
2164
|
+
}
|
|
2165
|
+
async function sized(boundary, path, most, what) {
|
|
2166
|
+
const { size } = await stat(await boundary.resolve(path));
|
|
2167
|
+
if (size > most) throw new ProjectError(`${what} is ${size} bytes, and one is read at most ${most}`);
|
|
2168
|
+
}
|
|
2169
|
+
async function readPictures(boundary, artDirectory) {
|
|
2170
|
+
const pictures = [];
|
|
2171
|
+
const walk = async (directory, prefix) => {
|
|
2172
|
+
let entries;
|
|
2173
|
+
try {
|
|
2174
|
+
entries = await readdir(await boundary.resolve(directory), { withFileTypes: true });
|
|
2175
|
+
} catch {
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
for (const entry of entries) {
|
|
2179
|
+
const name = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
2180
|
+
if (entry.isDirectory()) await walk(join(directory, entry.name), name);
|
|
2181
|
+
else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
2182
|
+
if (pictures.length >= MAX_PICTURES) throw new ProjectError(`${shown(artDirectory, OUT_DIRECTORY)} holds more than ${MAX_PICTURES} pictures`);
|
|
2183
|
+
const path = join(directory, entry.name);
|
|
2184
|
+
await sized(boundary, path, MAX_PICTURE_BYTES, `${shown(artDirectory, OUT_DIRECTORY)}/${name}`);
|
|
2185
|
+
pictures.push({ name: name.slice(0, -".png".length), bytes: await boundary.readBytes(path) });
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
await walk(join(artDirectory, OUT_DIRECTORY), "");
|
|
2190
|
+
return pictures;
|
|
2191
|
+
}
|
|
2192
|
+
async function readEntries(boundary, artDirectory) {
|
|
2193
|
+
const directory = join(artDirectory, MANIFEST_DIRECTORY);
|
|
2194
|
+
const entries = [];
|
|
2195
|
+
for (const file of (await boundary.list(directory)).filter((name) => name.endsWith(".json")).sort()) {
|
|
2196
|
+
let manifest2;
|
|
2197
|
+
await sized(boundary, join(directory, file), MAX_TEXT_BYTES, `${MANIFEST_DIRECTORY}/${file}`);
|
|
2198
|
+
try {
|
|
2199
|
+
manifest2 = JSON.parse(await boundary.readText(join(directory, file)));
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
throw new ProjectError(`${MANIFEST_DIRECTORY}/${file} is not valid JSON: ${error.message}`);
|
|
2202
|
+
}
|
|
2203
|
+
if (typeof manifest2 !== "object" || manifest2 === null || Array.isArray(manifest2)) throw new ProjectError(`${MANIFEST_DIRECTORY}/${file} is not an object`);
|
|
2204
|
+
for (const [name, entry] of Object.entries(manifest2)) {
|
|
2205
|
+
if (name.startsWith("$")) continue;
|
|
2206
|
+
entries.push({ name, meta: entry?.meta });
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return entries;
|
|
2210
|
+
}
|
|
2211
|
+
async function readIdMap(boundary, artDirectory) {
|
|
2212
|
+
const path = join(artDirectory, ID_MAP_FILE);
|
|
2213
|
+
if (!await boundary.exists(path)) return null;
|
|
2214
|
+
await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, ID_MAP_FILE));
|
|
2215
|
+
return boundary.readText(path);
|
|
2216
|
+
}
|
|
2217
|
+
async function readProjectArt(boundary, artDirectory) {
|
|
2218
|
+
return {
|
|
2219
|
+
pictures: await readPictures(boundary, artDirectory),
|
|
2220
|
+
entries: await readEntries(boundary, artDirectory),
|
|
2221
|
+
idMap: await readIdMap(boundary, artDirectory)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
async function readProjectMap(boundary, artDirectory) {
|
|
2225
|
+
const path = join(artDirectory, "map.json");
|
|
2226
|
+
if (!await boundary.exists(path)) return null;
|
|
2227
|
+
await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, "map.json"));
|
|
2228
|
+
return boundary.readText(path);
|
|
2229
|
+
}
|
|
2230
|
+
const MAX_MIRRORED_SECTOR_BYTES = 256 * 1024;
|
|
2231
|
+
const inflated$1 = promisify(inflate);
|
|
2232
|
+
async function readMirroredProjectMap(boundary, artDirectory) {
|
|
2233
|
+
const indexPath = join(artDirectory, MIRROR_INDEX_FILE);
|
|
2234
|
+
if (!await boundary.exists(indexPath)) return null;
|
|
2235
|
+
await sized(boundary, indexPath, MAX_TEXT_BYTES, shown(artDirectory, MIRROR_INDEX_FILE));
|
|
2236
|
+
const index = parseMirrorIndex(await boundary.readText(indexPath));
|
|
2237
|
+
return readMirroredMap(
|
|
2238
|
+
index,
|
|
2239
|
+
async (relative2) => {
|
|
2240
|
+
const path = join(artDirectory, relative2);
|
|
2241
|
+
await sized(boundary, path, MAX_MIRRORED_SECTOR_BYTES, shown(artDirectory, relative2));
|
|
2242
|
+
return boundary.readBytes(path);
|
|
2243
|
+
},
|
|
2244
|
+
async (bytes) => new Uint8Array(await inflated$1(bytes, { maxOutputLength: SECTOR_BYTES }))
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
async function sealProject(boundary, artDirectory) {
|
|
2248
|
+
try {
|
|
2249
|
+
const sealed = sealProjectArt(await readProjectArt(boundary, artDirectory));
|
|
2250
|
+
await boundary.writeFileReplacing(join(artDirectory, CONTAINER_FILE), sealed.container);
|
|
2251
|
+
const pictures = `${sealed.pictures} ${sealed.pictures === 1 ? "picture" : "pictures"}`;
|
|
2252
|
+
const described = `${sealed.described} ${sealed.described === 1 ? "asset" : "assets"} described`;
|
|
2253
|
+
return [{ step: PROJECT_SEAL_STEP, code: 0, output: `sealed ${pictures} into ${shown(artDirectory, CONTAINER_FILE)}, ${described}` }];
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
if (!(error instanceof ProjectError)) throw error;
|
|
2256
|
+
return [{ step: PROJECT_SEAL_STEP, code: 1, output: `the container was not rebuilt: ${error.message}` }];
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const MIRROR_FILE = ".mirror.json";
|
|
2260
|
+
const MOVING_FILE = ".moving";
|
|
2261
|
+
const ASIDE_DIRECTORY = "before-platform";
|
|
2262
|
+
const OWN = ["map.json", OUT_DIRECTORY, MANIFEST_DIRECTORY, ID_MAP_FILE, REFERENCE_DIRECTORY, STYLE_GUIDE_FILE$1];
|
|
2263
|
+
const MAX_SECTOR_FILE_BYTES = 256 * 1024;
|
|
2264
|
+
const MANIFEST_NAME = /^[a-z][a-z0-9-]{0,39}$/;
|
|
2265
|
+
const IMAGE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2266
|
+
const MAX_RECORD_BYTES$1 = MAX_TEXT_BYTES;
|
|
2267
|
+
const MAX_FOLDER_BYTES = 1024 * 1024 * 1024;
|
|
2268
|
+
class MirrorRefused extends Error {
|
|
2269
|
+
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
2270
|
+
// properties (docs/notes.md n-0023).
|
|
2271
|
+
constructor(message) {
|
|
2272
|
+
super(message);
|
|
2273
|
+
this.name = "MirrorRefused";
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function readMirrorFile(boundary, art) {
|
|
2277
|
+
const path = join(art, MIRROR_FILE);
|
|
2278
|
+
if (!await boundary.exists(path)) return { format: MIRROR_FORMAT, art: null };
|
|
2279
|
+
let read = null;
|
|
2280
|
+
await sized(boundary, path, MAX_RECORD_BYTES$1, MIRROR_FILE);
|
|
2281
|
+
try {
|
|
2282
|
+
read = JSON.parse(await boundary.readText(path));
|
|
2283
|
+
} catch {
|
|
2284
|
+
throw new MirrorRefused(`${MIRROR_FILE} is not JSON: remove it and the mirror is written again`);
|
|
2285
|
+
}
|
|
2286
|
+
if (read === null || read.format !== MIRROR_FORMAT) throw new MirrorRefused(`${MIRROR_FILE} is not a mirror's record this editor reads`);
|
|
2287
|
+
const index = read.art ?? null;
|
|
2288
|
+
const record = (value) => typeof value === "object" && value !== null && Object.values(value).every((id2) => typeof id2 === "string");
|
|
2289
|
+
if (index !== null && (!record(index.pictures) || !record(index.references) || !Array.isArray(index.manifests))) {
|
|
2290
|
+
throw new MirrorRefused(`${MIRROR_FILE} does not say what the mirror holds: remove it and the mirror is written again`);
|
|
2291
|
+
}
|
|
2292
|
+
if (index !== null) {
|
|
2293
|
+
const named = [...Object.keys(index.pictures), ...Object.keys(index.references)];
|
|
2294
|
+
const bad = named.find((name) => nameProblem(name) !== null) ?? index.manifests.find((manifest2) => typeof manifest2 !== "string" || !MANIFEST_NAME.test(manifest2));
|
|
2295
|
+
if (bad !== void 0)
|
|
2296
|
+
throw new MirrorRefused(`${MIRROR_FILE} names ${String(bad)}, which the mirror never writes: remove it and the mirror is written again`);
|
|
2297
|
+
}
|
|
2298
|
+
return { format: MIRROR_FORMAT, art: index };
|
|
2299
|
+
}
|
|
2300
|
+
async function writeMirrorFile(boundary, art, file) {
|
|
2301
|
+
await boundary.writeFileReplacing(join(art, MIRROR_FILE), `${JSON.stringify(file, null, 2)}
|
|
2302
|
+
`);
|
|
2303
|
+
}
|
|
2304
|
+
async function isMirrored(boundary, art) {
|
|
2305
|
+
return await boundary.exists(join(art, MIRROR_FILE)) || await boundary.exists(join(art, MIRROR_INDEX_FILE));
|
|
2306
|
+
}
|
|
2307
|
+
async function holdsAny(boundary, directory) {
|
|
2308
|
+
if (!await boundary.exists(directory)) return false;
|
|
2309
|
+
for (const entry of await readdir(await boundary.resolve(directory), { withFileTypes: true })) {
|
|
2310
|
+
if (!entry.isDirectory() || await holdsAny(boundary, join(directory, entry.name))) return true;
|
|
2311
|
+
}
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
async function paintsAny(boundary, path) {
|
|
2315
|
+
if (!await boundary.exists(path)) return false;
|
|
2316
|
+
await sized(boundary, path, MAX_TEXT_BYTES, "map.json");
|
|
2317
|
+
try {
|
|
2318
|
+
const map = parseMapText(await boundary.readText(path), "pixelvibing/map.json").document;
|
|
2319
|
+
const SCAFFOLD_TILE = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone"]);
|
|
2320
|
+
if (Object.keys(map).some((key) => key !== "width" && key !== "height" && key !== "tiles")) return true;
|
|
2321
|
+
return map.tiles.some(
|
|
2322
|
+
(tile) => tile.layers.length > 0 || tile.blocking !== 0 || tile.zone !== 0 || Object.keys(tile).some((key) => !SCAFFOLD_TILE.has(key))
|
|
2323
|
+
);
|
|
2324
|
+
} catch {
|
|
2325
|
+
return true;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
async function mirrorState(boundary, art) {
|
|
2329
|
+
const file = await readMirrorFile(boundary, art);
|
|
2330
|
+
const indexPath = join(art, MIRROR_INDEX_FILE);
|
|
2331
|
+
const mirrored2 = await isMirrored(boundary, art);
|
|
2332
|
+
const movingPath = join(art, MOVING_FILE);
|
|
2333
|
+
const readSized = async (path, what) => {
|
|
2334
|
+
await sized(boundary, path, MAX_RECORD_BYTES$1, what);
|
|
2335
|
+
return boundary.readText(path);
|
|
2336
|
+
};
|
|
2337
|
+
const moving = await boundary.exists(movingPath) ? (await readSized(movingPath, MOVING_FILE)).trim() : "";
|
|
2338
|
+
return {
|
|
2339
|
+
map: await boundary.exists(indexPath) ? parseMirrorIndex(await readSized(indexPath, MIRROR_INDEX_FILE)) : null,
|
|
2340
|
+
art: file.art,
|
|
2341
|
+
moving: moving === "" ? null : moving,
|
|
2342
|
+
mirrored: mirrored2,
|
|
2343
|
+
own: mirrored2 ? { map: false, art: false } : {
|
|
2344
|
+
map: await paintsAny(boundary, join(art, "map.json")),
|
|
2345
|
+
art: await holdsAny(boundary, join(art, OUT_DIRECTORY)) || await holdsAny(boundary, join(art, REFERENCE_DIRECTORY))
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
async function markMoving(boundary, art, project) {
|
|
2350
|
+
if (project === null) return boundary.removeFile(join(art, MOVING_FILE));
|
|
2351
|
+
if (!IMAGE_ID.test(project)) throw new MirrorRefused("a folder is moved onto a platform project named by its id");
|
|
2352
|
+
await boundary.writeFileReplacing(join(art, MOVING_FILE), `${project}
|
|
2353
|
+
`);
|
|
2354
|
+
}
|
|
2355
|
+
async function setAside(boundary, art) {
|
|
2356
|
+
if (await isMirrored(boundary, art)) return;
|
|
2357
|
+
const held = [];
|
|
2358
|
+
for (const name of OWN) if (await boundary.exists(join(art, name))) held.push(name);
|
|
2359
|
+
if (held.length > 0) {
|
|
2360
|
+
let aside = join(art, ASIDE_DIRECTORY);
|
|
2361
|
+
for (let second = 2; await boundary.exists(aside); second += 1) aside = join(art, `${ASIDE_DIRECTORY}-${second}`);
|
|
2362
|
+
await boundary.writeFileReplacing(join(aside, ".kept"), "the folder's own, before it was a mirror of its platform project\n");
|
|
2363
|
+
for (const name of held) await rename(await boundary.resolve(join(art, name)), await boundary.resolve(join(aside, name)));
|
|
2364
|
+
}
|
|
2365
|
+
await writeMirrorFile(boundary, art, { format: MIRROR_FORMAT, art: null });
|
|
2366
|
+
}
|
|
2367
|
+
const MOST_MANIFEST_BYTES = MAX_TEXT_BYTES;
|
|
2368
|
+
async function readFolder(boundary, art) {
|
|
2369
|
+
if (await isMirrored(boundary, art)) throw new MirrorRefused("this folder is already a mirror: it holds nothing of its own to move");
|
|
2370
|
+
const read = await readProjectArt(boundary, art);
|
|
2371
|
+
const assets = [];
|
|
2372
|
+
const directory = join(art, MANIFEST_DIRECTORY);
|
|
2373
|
+
const files = await boundary.exists(directory) ? (await boundary.list(directory)).filter((name) => name.endsWith(".json")).sort() : [];
|
|
2374
|
+
for (const file of files) {
|
|
2375
|
+
const manifest2 = file.slice(0, -".json".length);
|
|
2376
|
+
if (!MANIFEST_NAME.test(manifest2)) throw new MirrorRefused(`${MANIFEST_DIRECTORY}/${file} is not a manifest the platform keeps: rename it`);
|
|
2377
|
+
const text2 = await boundary.readText(join(directory, file));
|
|
2378
|
+
if (text2.length > MOST_MANIFEST_BYTES) throw new MirrorRefused(`${MANIFEST_DIRECTORY}/${file} is past what a manifest may weigh`);
|
|
2379
|
+
let entries;
|
|
2380
|
+
try {
|
|
2381
|
+
entries = JSON.parse(text2);
|
|
2382
|
+
} catch {
|
|
2383
|
+
throw new MirrorRefused(`${MANIFEST_DIRECTORY}/${file} is not JSON`);
|
|
2384
|
+
}
|
|
2385
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries)) throw new MirrorRefused(`${MANIFEST_DIRECTORY}/${file} is not a manifest`);
|
|
2386
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
2387
|
+
if (typeof entry === "object" && entry !== null) assets.push({ name: checkedName(name, "the asset"), manifest: manifest2, entry });
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
const idMap = {};
|
|
2391
|
+
if (read.idMap !== null) {
|
|
2392
|
+
let parsed2 = {};
|
|
2393
|
+
try {
|
|
2394
|
+
parsed2 = JSON.parse(read.idMap);
|
|
2395
|
+
} catch {
|
|
2396
|
+
throw new MirrorRefused(`${ID_MAP_FILE} is not JSON`);
|
|
2397
|
+
}
|
|
2398
|
+
for (const [id2, name] of Object.entries(parsed2.looktypes ?? {})) if (typeof name === "string") idMap[id2] = name;
|
|
2399
|
+
}
|
|
2400
|
+
let total = read.pictures.reduce((sum, picture) => sum + picture.bytes.length, 0);
|
|
2401
|
+
if (total > MAX_FOLDER_BYTES) throw new MirrorRefused(`this folder's pictures weigh past ${MAX_FOLDER_BYTES / 1024 / 1024} MB, more than one move sends`);
|
|
2402
|
+
const references = [];
|
|
2403
|
+
for (const file of await listReferences(boundary, art)) {
|
|
2404
|
+
await sized(boundary, join(art, REFERENCE_DIRECTORY, file), MAX_PICTURE_BYTES, `${REFERENCE_DIRECTORY}/${file}`);
|
|
2405
|
+
const bytes = await readReference(boundary, art, file);
|
|
2406
|
+
total += bytes.length;
|
|
2407
|
+
if (total > MAX_FOLDER_BYTES) throw new MirrorRefused(`this folder's pictures weigh past ${MAX_FOLDER_BYTES / 1024 / 1024} MB, more than one move sends`);
|
|
2408
|
+
references.push({ name: file.replace(/\.png$/i, ""), bytes });
|
|
2409
|
+
}
|
|
2410
|
+
return { map: await readProjectMap(boundary, art), assets, pictures: read.pictures.map(({ name, bytes }) => ({ name, bytes })), idMap, references };
|
|
2411
|
+
}
|
|
2412
|
+
function checkedName(name, what) {
|
|
2413
|
+
const problem = typeof name === "string" ? nameProblem(name) : "it is not text";
|
|
2414
|
+
if (problem !== null) throw new MirrorRefused(`${what} ${String(name)} cannot be written to the folder: ${problem}`);
|
|
2415
|
+
return name;
|
|
2416
|
+
}
|
|
2417
|
+
async function writeMapMirror(boundary, art, mirror) {
|
|
2418
|
+
await setAside(boundary, art);
|
|
2419
|
+
const current = await mirrorState(boundary, art);
|
|
2420
|
+
const versions = new Map(
|
|
2421
|
+
(current.map?.sectors ?? []).map((sector) => [sector.slice(0, 3).join(":"), sector])
|
|
2422
|
+
);
|
|
2423
|
+
for (const sector of mirror.write) {
|
|
2424
|
+
const path = mirrorSectorPath(sector.z, sector.cx, sector.cy);
|
|
2425
|
+
if (!(sector.data instanceof Uint8Array) || sector.data.length > MAX_SECTOR_FILE_BYTES) throw new MirrorRefused(`${path} is past what a sector is kept in`);
|
|
2426
|
+
if (!Number.isInteger(sector.version) || sector.version < 1) throw new MirrorRefused(`${path} names no version`);
|
|
2427
|
+
await boundary.writeFileReplacing(join(art, path), sector.data);
|
|
2428
|
+
versions.set(`${sector.z}:${sector.cx}:${sector.cy}`, [sector.z, sector.cx, sector.cy, sector.version]);
|
|
2429
|
+
}
|
|
2430
|
+
const dropped = mirror.drop.map((place) => mirrorSectorPath(place.z, place.cx, place.cy));
|
|
2431
|
+
for (const place of mirror.drop) versions.delete(`${place.z}:${place.cx}:${place.cy}`);
|
|
2432
|
+
const index = parseMirrorIndex(
|
|
2433
|
+
mirrorIndexText({
|
|
2434
|
+
format: MIRROR_FORMAT,
|
|
2435
|
+
revision: mirror.revision,
|
|
2436
|
+
size: mirror.size,
|
|
2437
|
+
limit: mirror.limit,
|
|
2438
|
+
names: mirror.names,
|
|
2439
|
+
sectors: [...versions.values()]
|
|
2440
|
+
})
|
|
2441
|
+
);
|
|
2442
|
+
await boundary.writeFileReplacing(join(art, MIRROR_INDEX_FILE), mirrorIndexText(index));
|
|
2443
|
+
for (const path of dropped) await boundary.removeFile(join(art, path)).catch(() => void 0);
|
|
2444
|
+
return index;
|
|
2445
|
+
}
|
|
2446
|
+
function checkedPictures(pictures, held, what) {
|
|
2447
|
+
const names = Object.keys(pictures);
|
|
2448
|
+
if (names.length > MAX_PICTURES) throw new MirrorRefused(`the mirror holds at most ${MAX_PICTURES} ${what}s`);
|
|
2449
|
+
for (const name of names) {
|
|
2450
|
+
checkedName(name, `the ${what}`);
|
|
2451
|
+
const picture = pictures[name];
|
|
2452
|
+
if (typeof picture.image !== "string" || !IMAGE_ID.test(picture.image)) throw new MirrorRefused(`the ${what} ${name} names no image`);
|
|
2453
|
+
if (picture.bytes === void 0) {
|
|
2454
|
+
if (held[name] !== picture.image) throw new MirrorRefused(`the ${what} ${name} comes with no bytes, and the folder does not hold its image`);
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (!(picture.bytes instanceof Uint8Array) || picture.bytes.length > MAX_PICTURE_BYTES || !isPng(picture.bytes)) {
|
|
2458
|
+
throw new MirrorRefused(`the ${what} ${name} is not a PNG the folder can keep`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async function writeArtMirror(boundary, art, mirror) {
|
|
2463
|
+
await setAside(boundary, art);
|
|
2464
|
+
const file = await readMirrorFile(boundary, art);
|
|
2465
|
+
const before = file.art ?? { pictures: {}, references: {}, manifests: [] };
|
|
2466
|
+
checkedPictures(mirror.pictures, before.pictures, "picture");
|
|
2467
|
+
checkedPictures(mirror.references, before.references, "reference");
|
|
2468
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
2469
|
+
for (const asset of mirror.assets) {
|
|
2470
|
+
checkedName(asset.name, "the asset");
|
|
2471
|
+
if (typeof asset.manifest !== "string" || !MANIFEST_NAME.test(asset.manifest)) throw new MirrorRefused(`${asset.name} names no manifest the folder keeps`);
|
|
2472
|
+
const entries = manifests.get(asset.manifest) ?? {};
|
|
2473
|
+
entries[asset.name] = asset.entry;
|
|
2474
|
+
manifests.set(asset.manifest, entries);
|
|
2475
|
+
}
|
|
2476
|
+
const looktypes = {};
|
|
2477
|
+
for (const [id2, name] of Object.entries(mirror.idMap)) {
|
|
2478
|
+
if (!/^[0-9]+$/.test(id2)) throw new MirrorRefused(`the id map names an id that is none: ${id2}`);
|
|
2479
|
+
looktypes[id2] = checkedName(name, "the id map's asset");
|
|
2480
|
+
}
|
|
2481
|
+
if (mirror.styleGuide !== null && (typeof mirror.styleGuide !== "string" || mirror.styleGuide.length > MAX_TEXT_BYTES)) {
|
|
2482
|
+
throw new MirrorRefused("the style guide is not text the folder can keep");
|
|
2483
|
+
}
|
|
2484
|
+
for (const [name, picture] of Object.entries(mirror.pictures)) {
|
|
2485
|
+
if (picture.bytes !== void 0) await boundary.writeFileReplacing(join(art, OUT_DIRECTORY, `${name}.png`), picture.bytes);
|
|
2486
|
+
}
|
|
2487
|
+
for (const [name, picture] of Object.entries(mirror.references)) {
|
|
2488
|
+
if (picture.bytes !== void 0) await boundary.writeFileReplacing(join(art, REFERENCE_DIRECTORY, `${name}.png`), picture.bytes);
|
|
2489
|
+
}
|
|
2490
|
+
for (const [manifest2, entries] of manifests) {
|
|
2491
|
+
await boundary.writeFileReplacing(join(art, MANIFEST_DIRECTORY, `${manifest2}.json`), `${JSON.stringify(entries, null, 2)}
|
|
2492
|
+
`);
|
|
2493
|
+
}
|
|
2494
|
+
await boundary.writeFileReplacing(join(art, ID_MAP_FILE), `${JSON.stringify({ looktypes }, null, 2)}
|
|
2495
|
+
`);
|
|
2496
|
+
if (mirror.styleGuide !== null) await boundary.writeFileReplacing(join(art, STYLE_GUIDE_FILE$1), mirror.styleGuide);
|
|
2497
|
+
const index = {
|
|
2498
|
+
revision: mirror.revision,
|
|
2499
|
+
pictures: Object.fromEntries(Object.entries(mirror.pictures).map(([name, picture]) => [name, picture.image])),
|
|
2500
|
+
references: Object.fromEntries(Object.entries(mirror.references).map(([name, picture]) => [name, picture.image])),
|
|
2501
|
+
manifests: [...manifests.keys()].sort()
|
|
2502
|
+
};
|
|
2503
|
+
await writeMirrorFile(boundary, art, { ...file, art: index });
|
|
2504
|
+
const gone = async (relative2) => boundary.removeFile(join(art, relative2)).catch(() => void 0);
|
|
2505
|
+
for (const name of Object.keys(before.pictures)) if (index.pictures[name] === void 0) await gone(join(OUT_DIRECTORY, `${name}.png`));
|
|
2506
|
+
for (const name of Object.keys(before.references)) if (index.references[name] === void 0) await gone(join(REFERENCE_DIRECTORY, `${name}.png`));
|
|
2507
|
+
for (const manifest2 of before.manifests) if (!index.manifests.includes(manifest2)) await gone(join(MANIFEST_DIRECTORY, `${manifest2}.json`));
|
|
2508
|
+
return index;
|
|
2509
|
+
}
|
|
2510
|
+
const SECTOR_SIDE = 128;
|
|
2511
|
+
const MOST_READ = 32;
|
|
2512
|
+
const MOST_WAIT_SECONDS$1 = 60;
|
|
2513
|
+
const WRITTEN_AT_ONCE = 64;
|
|
2514
|
+
const pause = (seconds) => new Promise((done) => setTimeout(done, seconds * 1e3));
|
|
2515
|
+
async function patiently(deps, attempt, most = MOST_WAIT_SECONDS$1) {
|
|
2516
|
+
let waited = 0;
|
|
2517
|
+
for (; ; ) {
|
|
2518
|
+
try {
|
|
2519
|
+
return await attempt();
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
const wait = error instanceof ApiError && error.status === 429 ? Math.max(1, Math.min(error.retryAfter ?? 1, 30)) : null;
|
|
2522
|
+
if (wait === null || waited + wait > most) throw error;
|
|
2523
|
+
waited += wait;
|
|
2524
|
+
await (deps.wait ?? pause)(wait);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function fromBase64(text2) {
|
|
2529
|
+
const binary = atob(text2);
|
|
2530
|
+
const bytes = new Uint8Array(binary.length);
|
|
2531
|
+
for (let at = 0; at < binary.length; at += 1) bytes[at] = binary.charCodeAt(at);
|
|
2532
|
+
return bytes;
|
|
2533
|
+
}
|
|
2534
|
+
function readsOf(places) {
|
|
2535
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2536
|
+
for (const [cx, cy] of places) rows.set(cy, [...rows.get(cy) ?? [], cx]);
|
|
2537
|
+
const reads = [];
|
|
2538
|
+
for (const [cy, columns] of [...rows.entries()].sort((a, b) => a[0] - b[0])) {
|
|
2539
|
+
const sorted = [...columns].sort((a, b) => a - b);
|
|
2540
|
+
let from = sorted[0];
|
|
2541
|
+
let to = from;
|
|
2542
|
+
for (const cx of sorted.slice(1)) {
|
|
2543
|
+
if (cx - from + 1 > MOST_READ) {
|
|
2544
|
+
reads.push({ cy, from, to });
|
|
2545
|
+
from = cx;
|
|
2546
|
+
}
|
|
2547
|
+
to = cx;
|
|
2548
|
+
}
|
|
2549
|
+
reads.push({ cy, from, to });
|
|
2550
|
+
}
|
|
2551
|
+
return reads;
|
|
2552
|
+
}
|
|
2553
|
+
async function syncMap(deps) {
|
|
2554
|
+
const base = `/projects/${encodeURIComponent(deps.project)}/map`;
|
|
2555
|
+
const get = (path) => patiently(deps, async () => (await deps.call(path)).json());
|
|
2556
|
+
const outline = await get(base);
|
|
2557
|
+
const held = (await deps.mirror.state()).map;
|
|
2558
|
+
const holds = new Map((held?.sectors ?? []).map(([z, cx, cy, version2]) => [`${z}:${cx}:${cy}`, version2]));
|
|
2559
|
+
const names = outline.levels.flatMap((level) => level.name === null ? [] : [{ z: level.z, name: level.name }]);
|
|
2560
|
+
const meta = { revision: outline.revision, size: outline.size, limit: outline.limit, names };
|
|
2561
|
+
const kept = /* @__PURE__ */ new Set();
|
|
2562
|
+
let index = null;
|
|
2563
|
+
let pending = [];
|
|
2564
|
+
const flush = async () => {
|
|
2565
|
+
if (pending.length === 0) return;
|
|
2566
|
+
index = await deps.mirror.map({ ...meta, write: pending, drop: [] });
|
|
2567
|
+
pending = [];
|
|
2568
|
+
};
|
|
2569
|
+
for (const { z } of outline.levels) {
|
|
2570
|
+
const versions = await get(`${base}/versions?z=${z}`);
|
|
2571
|
+
const changed = [];
|
|
2572
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
2573
|
+
for (const [cx, cy, version2] of versions.sectors) {
|
|
2574
|
+
kept.add(`${z}:${cx}:${cy}`);
|
|
2575
|
+
if (holds.get(`${z}:${cx}:${cy}`) !== version2) {
|
|
2576
|
+
changed.push([cx, cy]);
|
|
2577
|
+
wanted.set(`${cx}:${cy}`, version2);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
for (const read of readsOf(changed)) {
|
|
2581
|
+
const area = {
|
|
2582
|
+
z,
|
|
2583
|
+
left: read.from * SECTOR_SIDE,
|
|
2584
|
+
top: read.cy * SECTOR_SIDE,
|
|
2585
|
+
right: Math.min(read.to * SECTOR_SIDE + SECTOR_SIDE - 1, outline.size.width - 1),
|
|
2586
|
+
bottom: Math.min(read.cy * SECTOR_SIDE + SECTOR_SIDE - 1, outline.size.height - 1)
|
|
2587
|
+
};
|
|
2588
|
+
const query = `z=${area.z}&left=${area.left}&top=${area.top}&right=${area.right}&bottom=${area.bottom}`;
|
|
2589
|
+
const found = await get(`${base}/sectors?${query}`);
|
|
2590
|
+
for (const sector of found.sectors) {
|
|
2591
|
+
if (!wanted.has(`${sector.cx}:${sector.cy}`)) continue;
|
|
2592
|
+
pending.push({ z, cx: sector.cx, cy: sector.cy, version: sector.version, data: fromBase64(sector.data) });
|
|
2593
|
+
if (pending.length >= WRITTEN_AT_ONCE) await flush();
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
await flush();
|
|
2598
|
+
const drop = [...holds.keys()].filter((key) => !kept.has(key)).map((key) => {
|
|
2599
|
+
const [z, cx, cy] = key.split(":").map(Number);
|
|
2600
|
+
return { z, cx, cy };
|
|
2601
|
+
});
|
|
2602
|
+
if (drop.length > 0 || index === null) index = await deps.mirror.map({ ...meta, write: [], drop });
|
|
2603
|
+
return index;
|
|
2604
|
+
}
|
|
2605
|
+
async function syncArt(deps) {
|
|
2606
|
+
const base = `/projects/${encodeURIComponent(deps.project)}`;
|
|
2607
|
+
const get = (path) => patiently(deps, async () => (await deps.call(path)).json());
|
|
2608
|
+
const bytes = (image) => patiently(deps, async () => new Uint8Array(await (await deps.call(`/images/${encodeURIComponent(image)}`)).arrayBuffer()));
|
|
2609
|
+
const art = await get(`${base}/art`);
|
|
2610
|
+
const references = await get(`${base}/art/references`);
|
|
2611
|
+
let styleGuide = null;
|
|
2612
|
+
try {
|
|
2613
|
+
styleGuide = (await get(`${base}/style-guide`)).text;
|
|
2614
|
+
} catch (error) {
|
|
2615
|
+
if (!(error instanceof ApiError && error.status === 404)) throw error;
|
|
2616
|
+
}
|
|
2617
|
+
const held = (await deps.mirror.state()).art;
|
|
2618
|
+
const pictureOf = async (name, image, holds) => holds?.[name] === image ? { image } : { image, bytes: await bytes(image) };
|
|
2619
|
+
const pictures = {};
|
|
2620
|
+
for (const [name, image] of Object.entries(art.pictures)) pictures[name] = await pictureOf(name, image, held?.pictures);
|
|
2621
|
+
const referenced = {};
|
|
2622
|
+
for (const { name, image } of references) referenced[name] = await pictureOf(name, image, held?.references);
|
|
2623
|
+
await deps.mirror.art({ revision: art.revision, assets: art.assets, pictures, idMap: art.idMap, references: referenced, styleGuide });
|
|
2624
|
+
}
|
|
2625
|
+
function mirrorOf(local) {
|
|
2626
|
+
const art = artOf(local);
|
|
2627
|
+
const boundary = new Boundary([art]);
|
|
2628
|
+
return {
|
|
2629
|
+
state: () => mirrorState(boundary, art),
|
|
2630
|
+
map: (each) => writeMapMirror(boundary, art, each),
|
|
2631
|
+
art: (each) => writeArtMirror(boundary, art, each),
|
|
2632
|
+
folder: () => readFolder(boundary, art),
|
|
2633
|
+
moving: (project) => markMoving(boundary, art, project)
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
const deflated = promisify(deflate);
|
|
2637
|
+
const inflated = promisify(inflate);
|
|
2638
|
+
const nodeZlib = {
|
|
2639
|
+
deflate: async (bytes) => new Uint8Array(await deflated(bytes)),
|
|
2640
|
+
inflate: async (bytes) => new Uint8Array(await inflated(bytes, { maxOutputLength: SECTOR_BYTES }))
|
|
2641
|
+
};
|
|
2642
|
+
const HOLDS_ITS_OWN = "this folder holds a map or art of its own, written before it was its platform project's: move it onto the platform with `pixel-vibing-cli move`, or pass --aside to keep its own files under pixelvibing/before-platform/";
|
|
2643
|
+
async function checkFolder(local, aside) {
|
|
2644
|
+
const state = await mirrorOf(local).state();
|
|
2645
|
+
if (state.moving !== null) throw new CliError("a move of this folder onto the platform stopped before it was done: run `pixel-vibing-cli move` again");
|
|
2646
|
+
if (!aside && !state.mirrored && (state.own.map || state.own.art)) throw new CliError(HOLDS_ITS_OWN);
|
|
2647
|
+
}
|
|
2648
|
+
async function bringFolder(platform, local, options) {
|
|
2649
|
+
await checkFolder(local, options.aside);
|
|
2650
|
+
const deps = { project: linkedOf(local), call: platform.call, mirror: mirrorOf(local) };
|
|
2651
|
+
if (options.map) await syncMap(deps);
|
|
2652
|
+
await syncArt(deps);
|
|
2653
|
+
}
|
|
2654
|
+
const PULLED_FILE = `${PROJECT_DIRECTORY}/pulled.json`;
|
|
2655
|
+
const PULLED_FORMAT = 1;
|
|
2656
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
2657
|
+
const UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
2658
|
+
const EMPTY_PULLED = { guide: null, assets: {} };
|
|
2659
|
+
class PulledError extends Error {
|
|
2660
|
+
constructor(message) {
|
|
2661
|
+
super(message);
|
|
2662
|
+
this.name = "PulledError";
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2666
|
+
function isPulledPath(path) {
|
|
2667
|
+
const directory = path.startsWith("out/") ? "out/" : path.startsWith("ref/") ? "ref/" : null;
|
|
2668
|
+
if (directory === null || !path.endsWith(".png")) return false;
|
|
2669
|
+
const name = path.slice(directory.length, -".png".length);
|
|
2670
|
+
return !name.includes("\\") && nameProblem(name) === null;
|
|
2671
|
+
}
|
|
2672
|
+
function assetOf(id2, raw) {
|
|
2673
|
+
if (!UUID.test(id2)) throw new PulledError(`${PULLED_FILE}: ${id2} is not an asset's id`);
|
|
2674
|
+
if (!isRecord(raw)) throw new PulledError(`${PULLED_FILE}: the asset ${id2} is not an object`);
|
|
2675
|
+
const { kind, name, hash, files } = raw;
|
|
2676
|
+
if (typeof kind !== "string" || kind === "") throw new PulledError(`${PULLED_FILE}: the asset ${id2} names no kind`);
|
|
2677
|
+
if (typeof name !== "string" || name === "") throw new PulledError(`${PULLED_FILE}: the asset ${id2} names no name`);
|
|
2678
|
+
if (typeof hash !== "string" || !HEX64.test(hash)) throw new PulledError(`${PULLED_FILE}: the asset ${id2} has no hash`);
|
|
2679
|
+
if (!Array.isArray(files) || files.length === 0 || !files.every((file) => typeof file === "string" && isPulledPath(file))) {
|
|
2680
|
+
throw new PulledError(`${PULLED_FILE}: the asset ${id2} names a file that is not a picture under out/ or ref/`);
|
|
2681
|
+
}
|
|
2682
|
+
return { kind, name, hash, files };
|
|
2683
|
+
}
|
|
2684
|
+
function guideOf(raw) {
|
|
2685
|
+
if (raw === void 0 || raw === null) return null;
|
|
2686
|
+
if (!isRecord(raw) || !Number.isInteger(raw.version) || raw.version < 1 || typeof raw.sha256 !== "string" || !HEX64.test(raw.sha256)) {
|
|
2687
|
+
throw new PulledError(`${PULLED_FILE}: the style guide's entry is not a version and a digest`);
|
|
2688
|
+
}
|
|
2689
|
+
return { version: raw.version, sha256: raw.sha256 };
|
|
2690
|
+
}
|
|
2691
|
+
function readPulled(text2) {
|
|
2692
|
+
let parsed2;
|
|
2693
|
+
try {
|
|
2694
|
+
parsed2 = JSON.parse(text2);
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
throw new PulledError(`${PULLED_FILE} is not valid JSON: ${error.message}`);
|
|
2697
|
+
}
|
|
2698
|
+
if (!isRecord(parsed2)) throw new PulledError(`${PULLED_FILE} is not an object`);
|
|
2699
|
+
if (parsed2.format !== PULLED_FORMAT) throw new PulledError(`${PULLED_FILE} names no format this CLI reads: expected "format": ${PULLED_FORMAT}`);
|
|
2700
|
+
const assets = {};
|
|
2701
|
+
if (parsed2.assets !== void 0) {
|
|
2702
|
+
if (!isRecord(parsed2.assets)) throw new PulledError(`${PULLED_FILE}: "assets" is not an object`);
|
|
2703
|
+
for (const [id2, raw] of Object.entries(parsed2.assets)) assets[id2] = assetOf(id2, raw);
|
|
2704
|
+
}
|
|
2705
|
+
return { guide: guideOf(parsed2.guide), assets };
|
|
2706
|
+
}
|
|
2707
|
+
function writePulled(record) {
|
|
2708
|
+
return `${JSON.stringify({ format: PULLED_FORMAT, ...record.guide === null ? {} : { guide: record.guide }, assets: record.assets }, null, 2)}
|
|
2709
|
+
`;
|
|
2117
2710
|
}
|
|
2118
|
-
const
|
|
2119
|
-
const
|
|
2120
|
-
const
|
|
2121
|
-
function
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2711
|
+
const MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
2712
|
+
const artBoundary = (local) => new Boundary([artOf(local)]);
|
|
2713
|
+
const recordPath = (local) => join(local.root, ...PULLED_FILE.split("/"));
|
|
2714
|
+
async function readRecord(local) {
|
|
2715
|
+
const boundary = artBoundary(local);
|
|
2716
|
+
const path = recordPath(local);
|
|
2717
|
+
if (!await boundary.exists(path)) return EMPTY_PULLED;
|
|
2718
|
+
const { size } = await stat(await boundary.resolve(path));
|
|
2719
|
+
if (size > MAX_RECORD_BYTES) throw new CliError(`${PULLED_FILE} is ${size} bytes, and is read at most ${MAX_RECORD_BYTES}`);
|
|
2720
|
+
try {
|
|
2721
|
+
return readPulled(await boundary.readText(path));
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
if (error instanceof PulledError) throw new CliError(error.message);
|
|
2724
|
+
throw error;
|
|
2125
2725
|
}
|
|
2126
|
-
return null;
|
|
2127
2726
|
}
|
|
2128
|
-
({
|
|
2129
|
-
|
|
2130
|
-
height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
|
|
2131
|
-
});
|
|
2132
|
-
Array.from({ length: 16 }, (_value, index) => index);
|
|
2133
|
-
const CONTAINER_FILE = join("build", "assets.eac");
|
|
2134
|
-
const MANIFEST_DIRECTORY = "manifest";
|
|
2135
|
-
const OUT_DIRECTORY = "out";
|
|
2136
|
-
const ID_MAP_FILE = "id-map.json";
|
|
2137
|
-
const PROJECT_SEAL_STEP = "project:seal";
|
|
2138
|
-
const MAX_PICTURE_BYTES = 64 * 1024 * 1024;
|
|
2139
|
-
const MAX_TEXT_BYTES = 16 * 1024 * 1024;
|
|
2140
|
-
const MAX_PICTURES = 2e4;
|
|
2141
|
-
function shown(artDirectory, relative2) {
|
|
2142
|
-
return [basename(artDirectory), ...relative2.split(/[\\/]/)].join("/");
|
|
2727
|
+
async function writeRecord(local, record) {
|
|
2728
|
+
await artBoundary(local).writeFileReplacing(recordPath(local), writePulled(record));
|
|
2143
2729
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2730
|
+
const REFERENCE_KINDS = /* @__PURE__ */ new Set(["level-design", "hud-mockup"]);
|
|
2731
|
+
function setPieces(asset) {
|
|
2732
|
+
return setPiecesOf(asset.kind, asset.images.length, asset.data);
|
|
2147
2733
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
const walk = async (directory, prefix) => {
|
|
2151
|
-
let entries;
|
|
2152
|
-
try {
|
|
2153
|
-
entries = await readdir(await boundary.resolve(directory), { withFileTypes: true });
|
|
2154
|
-
} catch {
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
for (const entry of entries) {
|
|
2158
|
-
const name = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
2159
|
-
if (entry.isDirectory()) await walk(join(directory, entry.name), name);
|
|
2160
|
-
else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
2161
|
-
if (pictures.length >= MAX_PICTURES) throw new ProjectError(`${shown(artDirectory, OUT_DIRECTORY)} holds more than ${MAX_PICTURES} pictures`);
|
|
2162
|
-
const path = join(directory, entry.name);
|
|
2163
|
-
await sized(boundary, path, MAX_PICTURE_BYTES, `${shown(artDirectory, OUT_DIRECTORY)}/${name}`);
|
|
2164
|
-
pictures.push({ name: name.slice(0, -".png".length), bytes: await boundary.readBytes(path) });
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
};
|
|
2168
|
-
await walk(join(artDirectory, OUT_DIRECTORY), "");
|
|
2169
|
-
return pictures;
|
|
2734
|
+
function namesOf(name, count) {
|
|
2735
|
+
return count === 1 ? [name] : Array.from({ length: count }, (_, index) => `${name}/${index}`);
|
|
2170
2736
|
}
|
|
2171
|
-
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2737
|
+
function nameFor(asset, record, given) {
|
|
2738
|
+
const name = given ?? record.assets[asset.id]?.name ?? asset.name ?? SET_NAMES[asset.kind] ?? `${asset.kind}-${asset.id.slice(0, 8)}`;
|
|
2739
|
+
const problem = nameProblem(name);
|
|
2740
|
+
if (problem !== null) throw new CliError(`${name} is not a name a picture can be landed under (${problem}): give one with --as`);
|
|
2741
|
+
return name;
|
|
2742
|
+
}
|
|
2743
|
+
async function pullAsset(platform, local, ref, given, options = {}) {
|
|
2744
|
+
const project = linkedOf(local);
|
|
2745
|
+
await checkFolder(local, options.aside === true);
|
|
2746
|
+
const asset = await platform.web.asset(project, ref);
|
|
2747
|
+
if (asset.images.length === 0) throw new CliError(`the asset ${asset.id} has no image the platform still keeps`);
|
|
2748
|
+
if (asset.kind === "terrain") {
|
|
2749
|
+
throw new CliError(`${asset.id} is a terrain set: land it from the editor's Library, which cuts a set into the cells its brush paints. Nothing was landed`);
|
|
2750
|
+
}
|
|
2751
|
+
const record = await readRecord(local);
|
|
2752
|
+
const name = nameFor(asset, record, given);
|
|
2753
|
+
const pieces = setPieces(asset);
|
|
2754
|
+
const reference = REFERENCE_KINDS.has(asset.kind);
|
|
2755
|
+
const names = pieces === null ? namesOf(name, asset.images.length) : pieceNames(name, pieces);
|
|
2756
|
+
const files = names.map((each) => `${reference ? "ref" : "out"}/${each}.png`);
|
|
2757
|
+
const pulled = { id: asset.id, kind: asset.kind, name, hash: asset.hash, files, data: asset.data };
|
|
2758
|
+
const base = `/projects/${encodeURIComponent(project)}`;
|
|
2759
|
+
const json2 = async (path, init2) => (await platform.call(path, init2)).json();
|
|
2760
|
+
const art = await json2(`${base}/art`);
|
|
2761
|
+
const references = new Map((await json2(`${base}/art/references`)).map((each) => [each.name, each.image]));
|
|
2762
|
+
const held = (each) => reference ? references.get(each) : art.pictures[each];
|
|
2763
|
+
const landing = [];
|
|
2764
|
+
for (const [index, each] of names.entries()) {
|
|
2765
|
+
const image = asset.images[index].id;
|
|
2766
|
+
const holds = held(each);
|
|
2767
|
+
if (holds === image) continue;
|
|
2768
|
+
if (holds !== void 0) {
|
|
2769
|
+
throw new CliError(
|
|
2770
|
+
`the project's art already holds ${reference ? "a reference" : "a picture"} named ${each}, of something else: nothing was landed. Give this one another name with --as`
|
|
2771
|
+
);
|
|
2181
2772
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2773
|
+
landing.push({ name: each, image, piece: pieces?.[index] ?? null });
|
|
2774
|
+
}
|
|
2775
|
+
if (landing.length > 0) {
|
|
2776
|
+
if (reference) {
|
|
2777
|
+
let revision = art.revision;
|
|
2778
|
+
for (const each of landing)
|
|
2779
|
+
revision = (await json2(`${base}/art/references`, { method: "POST", body: { revision, name: each.name, image: each.image } })).revision;
|
|
2780
|
+
} else {
|
|
2781
|
+
const assets = landing.map(
|
|
2782
|
+
(each) => each.piece === null ? { name: each.name, manifest: "editor", entry: { command: ["cli", asset.kind, asset.id] }, image: each.image } : { name: each.name, manifest: "interface", entry: describedInterface({}, [[each.name, each.piece]], [])[each.name], image: each.image }
|
|
2783
|
+
);
|
|
2784
|
+
await json2(`${base}/art/land`, { method: "POST", body: { revision: art.revision, assets } });
|
|
2186
2785
|
}
|
|
2187
2786
|
}
|
|
2188
|
-
|
|
2787
|
+
await writeRecord(local, { ...record, assets: { ...record.assets, [asset.id]: { kind: asset.kind, name, hash: asset.hash, files } } });
|
|
2788
|
+
await bringFolder(platform, local, { map: false, aside: options.aside === true });
|
|
2789
|
+
return { ...pulled, current: landing.length === 0 };
|
|
2189
2790
|
}
|
|
2190
|
-
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, ID_MAP_FILE));
|
|
2194
|
-
return boundary.readText(path);
|
|
2791
|
+
function pulledWords(pulled) {
|
|
2792
|
+
const where = pulled.files.length === 1 ? `pixelvibing/${pulled.files[0]}` : `${pulled.files.length} pictures under pixelvibing/`;
|
|
2793
|
+
return pulled.current ? `${pulled.name} is current: the project's art holds it, and ${where} is what the platform has.` : `Pulled ${pulled.name}, ${pulled.kind}, into the project's art, and wrote ${where}.`;
|
|
2195
2794
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2795
|
+
const date = (at) => at.slice(0, 10);
|
|
2796
|
+
function listed(assets) {
|
|
2797
|
+
if (assets.length === 0) return "No assets.";
|
|
2798
|
+
return assets.map(
|
|
2799
|
+
(asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}`
|
|
2800
|
+
).join("\n");
|
|
2202
2801
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2802
|
+
const list = {
|
|
2803
|
+
name: "list",
|
|
2804
|
+
summary: "List the platform project's assets, of one kind when given, newest first",
|
|
2805
|
+
usage: "[<kind>] [--before <id>]",
|
|
2806
|
+
options: { before: { type: "string", value: "<id>", description: "the page after the asset with this id, as the last page said" } },
|
|
2807
|
+
positionals: [0, 1],
|
|
2808
|
+
run: async (context, args) => {
|
|
2809
|
+
const local = await projectHere(context.io.cwd);
|
|
2810
|
+
const project = linkedOf(local);
|
|
2811
|
+
const platform = await signedIn(context);
|
|
2812
|
+
const [kind] = args.positionals;
|
|
2813
|
+
const page = await platform.web.assets(project, kind ?? null, stringOf(args, "before") ?? null);
|
|
2814
|
+
const more = page.next === null ? "" : `
|
|
2815
|
+
More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`} --before ${page.next}`;
|
|
2816
|
+
answer(context, page, `${listed(page.assets)}${more}`);
|
|
2817
|
+
}
|
|
2818
|
+
};
|
|
2819
|
+
const pull = {
|
|
2820
|
+
name: "pull",
|
|
2821
|
+
summary: "Land one asset, by its id or its name, in the project's art, and write it into the local project",
|
|
2822
|
+
usage: "<id|name> [--as <name>] [--aside]",
|
|
2823
|
+
options: {
|
|
2824
|
+
as: { type: "string", value: "<name>", description: "land it under this name instead of the platform's" },
|
|
2825
|
+
aside: { type: "boolean", description: "keep a folder's own map and art under pixelvibing/before-platform/, and write it from the platform" }
|
|
2826
|
+
},
|
|
2827
|
+
positionals: [1, 1],
|
|
2828
|
+
run: async (context, args) => {
|
|
2829
|
+
const local = await projectHere(context.io.cwd);
|
|
2830
|
+
linkedOf(local);
|
|
2831
|
+
const platform = await signedIn(context);
|
|
2832
|
+
const pulled = await pullAsset(platform, local, args.positionals[0], stringOf(args, "as"), { aside: flagOf(args, "aside") });
|
|
2833
|
+
answer(context, pulled, pulledWords(pulled));
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
const DOWNLOAD_URL = "https://github.com/Pixel-Vibing/pixel-vibing/releases/latest";
|
|
2837
|
+
const EDITOR_VARIABLE = "PIXELVIBING_EDITOR";
|
|
2838
|
+
async function exists$1(path) {
|
|
2839
|
+
try {
|
|
2840
|
+
await stat(path);
|
|
2841
|
+
return true;
|
|
2842
|
+
} catch {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2208
2845
|
}
|
|
2209
|
-
async function
|
|
2846
|
+
async function findEditor(platform, env, home) {
|
|
2847
|
+
const named = env[EDITOR_VARIABLE]?.trim();
|
|
2848
|
+
if (named !== void 0 && named !== "") {
|
|
2849
|
+
if (!await exists$1(named)) throw new CliError(`${EDITOR_VARIABLE} names ${named}, which is not there`);
|
|
2850
|
+
return platform === "darwin" && named.endsWith(".app") ? { command: "open", args: ["-a", named, "--args"] } : { command: named, args: [] };
|
|
2851
|
+
}
|
|
2852
|
+
if (platform === "win32") {
|
|
2853
|
+
const places = [env.LOCALAPPDATA && join(env.LOCALAPPDATA, "Programs", "PixelVibing"), env.ProgramFiles && join(env.ProgramFiles, "PixelVibing")];
|
|
2854
|
+
for (const place of places) {
|
|
2855
|
+
if (typeof place === "string" && await exists$1(join(place, "PixelVibing.exe"))) return { command: join(place, "PixelVibing.exe"), args: [] };
|
|
2856
|
+
}
|
|
2857
|
+
return null;
|
|
2858
|
+
}
|
|
2859
|
+
if (platform === "darwin") {
|
|
2860
|
+
for (const app of ["/Applications/PixelVibing.app", join(home, "Applications", "PixelVibing.app")]) {
|
|
2861
|
+
if (await exists$1(app)) return { command: "open", args: ["-a", app, "--args"] };
|
|
2862
|
+
}
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
const applications = join(home, "Applications");
|
|
2866
|
+
let found = [];
|
|
2210
2867
|
try {
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
const described = `${sealed.described} ${sealed.described === 1 ? "asset" : "assets"} described`;
|
|
2215
|
-
return [{ step: PROJECT_SEAL_STEP, code: 0, output: `sealed ${pictures} into ${shown(artDirectory, CONTAINER_FILE)}, ${described}` }];
|
|
2216
|
-
} catch (error) {
|
|
2217
|
-
if (!(error instanceof ProjectError)) throw error;
|
|
2218
|
-
return [{ step: PROJECT_SEAL_STEP, code: 1, output: `the container was not rebuilt: ${error.message}` }];
|
|
2868
|
+
found = (await readdir(applications)).filter((name) => /^PixelVibing.*\.AppImage$/i.test(name)).sort();
|
|
2869
|
+
} catch {
|
|
2870
|
+
return null;
|
|
2219
2871
|
}
|
|
2872
|
+
const newest = found.at(-1);
|
|
2873
|
+
return newest === void 0 ? null : { command: join(applications, newest), args: [] };
|
|
2220
2874
|
}
|
|
2875
|
+
const editor = {
|
|
2876
|
+
name: "editor",
|
|
2877
|
+
summary: "Open the PixelVibing desktop app on this local project",
|
|
2878
|
+
run: async (context) => {
|
|
2879
|
+
const local = await projectHere(context.io.cwd);
|
|
2880
|
+
const { platform, home } = context.services;
|
|
2881
|
+
const found = await findEditor(platform, context.io.env, home);
|
|
2882
|
+
if (found === null) {
|
|
2883
|
+
throw new CliError(`the PixelVibing desktop app is not installed here: download it from ${DOWNLOAD_URL}, or set ${EDITOR_VARIABLE} to where it is`);
|
|
2884
|
+
}
|
|
2885
|
+
await context.services.launch(found.command, [...found.args, "--project", local.root]);
|
|
2886
|
+
answer(context, { opened: local.root, app: found.command }, `Opening ${local.root} in PixelVibing.`);
|
|
2887
|
+
}
|
|
2888
|
+
};
|
|
2221
2889
|
const DRAW_LAYERS = ["ground", "object", "foreground"];
|
|
2222
2890
|
function bandOfAsset(facts) {
|
|
2223
2891
|
if (facts.name === null) return "ground";
|
|
@@ -2740,14 +3408,14 @@ const paeth = (a, b, c) => {
|
|
|
2740
3408
|
const pc = Math.abs(p - c);
|
|
2741
3409
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
2742
3410
|
};
|
|
2743
|
-
function unfilter(
|
|
3411
|
+
function unfilter(inflated2, header) {
|
|
2744
3412
|
const bpp = CHANNELS[header.colorType] * (header.depth / 8);
|
|
2745
3413
|
const stride = header.width * bpp;
|
|
2746
3414
|
const out = new Uint8Array(header.height * stride);
|
|
2747
3415
|
for (let y = 0; y < header.height; y += 1) {
|
|
2748
|
-
const filter =
|
|
3416
|
+
const filter = inflated2[y * (stride + 1)];
|
|
2749
3417
|
if (filter > 4) throw new PngError(`a row has filter type ${filter}, which PNG does not define`);
|
|
2750
|
-
const row =
|
|
3418
|
+
const row = inflated2.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
2751
3419
|
const at = y * stride;
|
|
2752
3420
|
for (let x = 0; x < stride; x += 1) {
|
|
2753
3421
|
const a = x >= bpp ? out[at + x - bpp] : 0;
|
|
@@ -2792,14 +3460,14 @@ function decodeWith(zlib, bytes) {
|
|
|
2792
3460
|
if (header === null) throw new PngError("the PNG has no IHDR chunk");
|
|
2793
3461
|
const bpp = CHANNELS[header.colorType] * (header.depth / 8);
|
|
2794
3462
|
const expected = header.height * (1 + header.width * bpp);
|
|
2795
|
-
let
|
|
3463
|
+
let inflated2;
|
|
2796
3464
|
try {
|
|
2797
|
-
|
|
3465
|
+
inflated2 = zlib.inflate(concat(data), expected);
|
|
2798
3466
|
} catch {
|
|
2799
3467
|
throw new PngError("the PNG's image data could not be inflated to the size its header promises");
|
|
2800
3468
|
}
|
|
2801
|
-
if (
|
|
2802
|
-
return toRgba(unfilter(
|
|
3469
|
+
if (inflated2.length !== expected) throw new PngError("the PNG's image data is not the size its header promises");
|
|
3470
|
+
return toRgba(unfilter(inflated2, header), header, palette, transparency);
|
|
2803
3471
|
}
|
|
2804
3472
|
function toRgba(samples, header, palette, transparency) {
|
|
2805
3473
|
const { width, height, depth, colorType } = header;
|
|
@@ -2928,10 +3596,12 @@ async function writeWhole(out, files, engine) {
|
|
|
2928
3596
|
const exportCommand = {
|
|
2929
3597
|
name: "export",
|
|
2930
3598
|
summary: "Export the local project's art and map for an engine: default, pixi, phaser or godot",
|
|
2931
|
-
usage: "<default|pixi|phaser|godot> [--out <dir>] [--godot-root <res://…>]",
|
|
3599
|
+
usage: "<default|pixi|phaser|godot> [--out <dir>] [--godot-root <res://…>] [--offline] [--aside]",
|
|
2932
3600
|
options: {
|
|
2933
3601
|
out: { type: "string", value: "<dir>", description: "where to write it; pixelvibing/export/<engine>/ unless given" },
|
|
2934
|
-
"godot-root": { type: "string", value: "<res://…>", description: `the Godot project folder the files go in, ${DEFAULT_GODOT_ROOT} unless given` }
|
|
3602
|
+
"godot-root": { type: "string", value: "<res://…>", description: `the Godot project folder the files go in, ${DEFAULT_GODOT_ROOT} unless given` },
|
|
3603
|
+
offline: { type: "boolean", description: "read the folder as it is, without bringing it to the platform's state first" },
|
|
3604
|
+
aside: { type: "boolean", description: "keep a folder's own map and art under pixelvibing/before-platform/, and write it from the platform" }
|
|
2935
3605
|
},
|
|
2936
3606
|
positionals: [1, 1],
|
|
2937
3607
|
run: async (context, args) => {
|
|
@@ -2942,6 +3612,7 @@ const exportCommand = {
|
|
|
2942
3612
|
throw new CliError("--godot-root is a res:// path of letters, digits and . _ - /, with no ..");
|
|
2943
3613
|
}
|
|
2944
3614
|
const local = await projectHere(context.io.cwd);
|
|
3615
|
+
if (!flagOf(args, "offline")) await bringFolder(await signedIn(context), local, { map: true, aside: flagOf(args, "aside") });
|
|
2945
3616
|
const art = artOf(local);
|
|
2946
3617
|
const out = resolve(context.io.cwd, stringOf(args, "out") ?? join(art, "export", engine));
|
|
2947
3618
|
if (!await replaceable(out)) throw new CliError(`${out} holds files that are not an export's: nothing was written. Name an empty directory with --out`);
|
|
@@ -2949,7 +3620,8 @@ const exportCommand = {
|
|
|
2949
3620
|
try {
|
|
2950
3621
|
const boundary = new Boundary([art]);
|
|
2951
3622
|
const read = await readProjectArt(boundary, art);
|
|
2952
|
-
const
|
|
3623
|
+
const mirrored2 = await readMirroredProjectMap(boundary, art);
|
|
3624
|
+
const mapText = mirrored2 === null ? await readProjectMap(boundary, art) : null;
|
|
2953
3625
|
checkExportable(
|
|
2954
3626
|
read.pictures.map((picture) => {
|
|
2955
3627
|
if (!isPng(picture.bytes)) throw new CliError(`the picture ${picture.name} cannot be read: it is not a PNG`);
|
|
@@ -2967,7 +3639,7 @@ const exportCommand = {
|
|
|
2967
3639
|
{
|
|
2968
3640
|
pictures,
|
|
2969
3641
|
entries: read.entries,
|
|
2970
|
-
map: mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document,
|
|
3642
|
+
map: mirrored2 ?? (mapText === null ? null : parseMapText(mapText, "pixelvibing/map.json").document),
|
|
2971
3643
|
idMap: read.idMap === null ? null : parseIdMap(JSON.parse(read.idMap))
|
|
2972
3644
|
},
|
|
2973
3645
|
engine,
|
|
@@ -2975,7 +3647,7 @@ const exportCommand = {
|
|
|
2975
3647
|
godotRoot === void 0 ? {} : { godotRoot }
|
|
2976
3648
|
);
|
|
2977
3649
|
} catch (error) {
|
|
2978
|
-
const refusals = [ProjectError, PackError, PngError, MapTextError, IdMapError, SyntaxError, CliError];
|
|
3650
|
+
const refusals = [ProjectError, PackError, PngError, MapTextError, MirrorError, IdMapError, SyntaxError, CliError];
|
|
2979
3651
|
if (refusals.some((kind) => error instanceof kind)) throw new CliError(`nothing was exported: ${error.message}`);
|
|
2980
3652
|
throw error;
|
|
2981
3653
|
}
|
|
@@ -3430,7 +4102,8 @@ const generate = {
|
|
|
3430
4102
|
reference: { type: "string", multiple: true, value: "<png>", description: "an image to draw from" },
|
|
3431
4103
|
quality: { type: "string", value: "<quality>", description: `${EDIT_QUALITIES.join(", ")}; ${DEFAULT_QUALITY} unless given` },
|
|
3432
4104
|
yes: { type: "boolean", short: "y", description: "start it without asking, once the cost is said" },
|
|
3433
|
-
as: { type: "string", value: "<name>", description: "
|
|
4105
|
+
as: { type: "string", value: "<name>", description: "land it under this name instead of the platform's" },
|
|
4106
|
+
aside: { type: "boolean", description: "keep a folder's own map and art under pixelvibing/before-platform/, and write it from the platform" }
|
|
3434
4107
|
},
|
|
3435
4108
|
positionals: [1, 1],
|
|
3436
4109
|
run: async (context, args) => {
|
|
@@ -3438,6 +4111,7 @@ const generate = {
|
|
|
3438
4111
|
const project = linkedOf(local);
|
|
3439
4112
|
const kind = args.positionals[0];
|
|
3440
4113
|
const request = await requestOf(context, kind, args);
|
|
4114
|
+
await checkFolder(local, flagOf(args, "aside"));
|
|
3441
4115
|
const platform = await signedIn(context);
|
|
3442
4116
|
const cost = costOf(await platform.web.prices(), request);
|
|
3443
4117
|
await confirmed(context, platform, request.words, cost, flagOf(args, "yes"));
|
|
@@ -3446,7 +4120,7 @@ const generate = {
|
|
|
3446
4120
|
`);
|
|
3447
4121
|
const job = await followed(context, platform, jobId);
|
|
3448
4122
|
if (job.state === "failed") throw new CliError(`the generation failed: ${job.reason ?? "the platform gave no reason"}`);
|
|
3449
|
-
const pulled = await pullAsset(platform, local, jobId, stringOf(args, "as"));
|
|
4123
|
+
const pulled = await pullAsset(platform, local, jobId, stringOf(args, "as"), { aside: flagOf(args, "aside") });
|
|
3450
4124
|
answer(context, { job: jobId, cost, ...pulled }, pulledWords(pulled));
|
|
3451
4125
|
}
|
|
3452
4126
|
};
|
|
@@ -3754,15 +4428,156 @@ const link = {
|
|
|
3754
4428
|
const seal = {
|
|
3755
4429
|
name: "seal",
|
|
3756
4430
|
summary: "Seal the local project's art into its container, as the editor does",
|
|
3757
|
-
|
|
4431
|
+
usage: "[--offline] [--aside]",
|
|
4432
|
+
options: {
|
|
4433
|
+
offline: { type: "boolean", description: "read the folder as it is, without bringing it to the platform's state first" },
|
|
4434
|
+
aside: { type: "boolean", description: "keep a folder's own map and art under pixelvibing/before-platform/, and write it from the platform" }
|
|
4435
|
+
},
|
|
4436
|
+
run: async (context, args) => {
|
|
3758
4437
|
const local = await projectHere(context.io.cwd);
|
|
4438
|
+
if (!flagOf(args, "offline")) await bringFolder(await signedIn(context), local, { map: false, aside: flagOf(args, "aside") });
|
|
3759
4439
|
const art = artOf(local);
|
|
3760
4440
|
const [result] = await sealProject(new Boundary([art]), art);
|
|
3761
4441
|
if (result === void 0 || result.code !== 0) throw new CliError(result?.output ?? "the container was not rebuilt");
|
|
3762
4442
|
answer(context, { sealed: true, output: result.output }, result.output);
|
|
3763
4443
|
}
|
|
3764
4444
|
};
|
|
3765
|
-
const
|
|
4445
|
+
const SECTORS_A_WRITE = 64;
|
|
4446
|
+
const BYTES_A_WRITE = 11 * 1024 * 1024;
|
|
4447
|
+
const MOST_WAIT_SECONDS = 30 * 60;
|
|
4448
|
+
class MoveRefused extends Error {
|
|
4449
|
+
// Assigned in the body: Node's strip-only TypeScript mode rejects parameter
|
|
4450
|
+
// properties (docs/notes.md n-0023).
|
|
4451
|
+
constructor(message) {
|
|
4452
|
+
super(message);
|
|
4453
|
+
this.name = "MoveRefused";
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
function toBase64(bytes) {
|
|
4457
|
+
let binary = "";
|
|
4458
|
+
for (let at = 0; at < bytes.length; at += 32768) binary += String.fromCharCode(...bytes.subarray(at, at + 32768));
|
|
4459
|
+
return btoa(binary);
|
|
4460
|
+
}
|
|
4461
|
+
function marksAny(outline) {
|
|
4462
|
+
const bits = atob(outline);
|
|
4463
|
+
for (let at = 0; at < bits.length; at += 1) if (bits.charCodeAt(at) !== 0) return true;
|
|
4464
|
+
return false;
|
|
4465
|
+
}
|
|
4466
|
+
async function platformIsEmpty(deps) {
|
|
4467
|
+
const base = `/projects/${encodeURIComponent(deps.project)}`;
|
|
4468
|
+
const outline = await (await deps.call(`${base}/map`)).json();
|
|
4469
|
+
if (outline.levels.some((level) => marksAny(level.sectors))) return false;
|
|
4470
|
+
const art = await (await deps.call(`${base}/art`)).json();
|
|
4471
|
+
if (art.assets.length > 0) return false;
|
|
4472
|
+
const references = await (await deps.call(`${base}/art/references`)).json();
|
|
4473
|
+
return references.length === 0;
|
|
4474
|
+
}
|
|
4475
|
+
async function moveFolder(deps) {
|
|
4476
|
+
const base = `/projects/${encodeURIComponent(deps.project)}`;
|
|
4477
|
+
const call = (path, init2) => patiently(deps, async () => (await deps.call(path, init2)).json(), MOST_WAIT_SECONDS);
|
|
4478
|
+
const folder = await deps.mirror.folder();
|
|
4479
|
+
const pictured = new Set(folder.pictures.map((picture) => picture.name));
|
|
4480
|
+
const drawn = folder.assets.filter((asset) => pictured.has(asset.name));
|
|
4481
|
+
const named = new Set(drawn.map((asset) => asset.name));
|
|
4482
|
+
const idMap = Object.fromEntries(Object.entries(folder.idMap).filter(([, name]) => named.has(name)));
|
|
4483
|
+
try {
|
|
4484
|
+
sealProjectArt({
|
|
4485
|
+
pictures: folder.pictures,
|
|
4486
|
+
entries: drawn.map((asset) => ({ name: asset.name, meta: asset.entry.meta })),
|
|
4487
|
+
idMap: Object.keys(idMap).length === 0 ? null : JSON.stringify({ looktypes: idMap })
|
|
4488
|
+
});
|
|
4489
|
+
} catch (error) {
|
|
4490
|
+
throw new MoveRefused(
|
|
4491
|
+
`this folder's art cannot be read as the editor reads it, and nothing was sent: ${error instanceof Error ? error.message : String(error)}`
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
let map;
|
|
4495
|
+
try {
|
|
4496
|
+
map = folder.map === null ? null : parseMapText(folder.map, "pixelvibing/map.json").document;
|
|
4497
|
+
} catch (error) {
|
|
4498
|
+
throw new MoveRefused(`this folder's map cannot be read, and nothing was sent: ${error instanceof Error ? error.message : String(error)}`);
|
|
4499
|
+
}
|
|
4500
|
+
const outline = await call(`${base}/map`);
|
|
4501
|
+
if (map !== null && (map.width > outline.limit.width || map.height > outline.limit.height)) {
|
|
4502
|
+
throw new MoveRefused(
|
|
4503
|
+
`this folder's map is ${map.width} × ${map.height} tiles, and the project's map grows to at most ${outline.limit.width} × ${outline.limit.height}: nothing was sent`
|
|
4504
|
+
);
|
|
4505
|
+
}
|
|
4506
|
+
const sectors = map === null ? [] : sectorsOfMap(map);
|
|
4507
|
+
await deps.mirror.moving(deps.project);
|
|
4508
|
+
const keep2 = (bytes2) => call("/images", { method: "POST", bytes: { data: bytes2, type: "image/png" } }).then((kept) => kept.id);
|
|
4509
|
+
const pictures = {};
|
|
4510
|
+
for (const [index, picture] of folder.pictures.entries()) {
|
|
4511
|
+
deps.progress?.(`sending the pictures: ${index + 1} of ${folder.pictures.length}`);
|
|
4512
|
+
pictures[picture.name] = await keep2(picture.bytes);
|
|
4513
|
+
}
|
|
4514
|
+
const references = [];
|
|
4515
|
+
for (const reference of folder.references) references.push({ name: reference.name, image: await keep2(reference.bytes) });
|
|
4516
|
+
const assets = drawn.map((asset) => ({ ...asset, image: pictures[asset.name] }));
|
|
4517
|
+
const art = await call(`${base}/art`);
|
|
4518
|
+
deps.progress?.("sending the art");
|
|
4519
|
+
await call(`${base}/art/import`, { method: "POST", body: { revision: art.revision, assets, pictures, idMap, references } });
|
|
4520
|
+
let revision = outline.revision;
|
|
4521
|
+
if (map !== null && (map.width > outline.size.width || map.height > outline.size.height)) {
|
|
4522
|
+
const size = { width: Math.max(map.width, outline.size.width), height: Math.max(map.height, outline.size.height) };
|
|
4523
|
+
revision = (await call(`${base}/map/size`, { method: "PUT", body: { revision, ...size } })).revision;
|
|
4524
|
+
}
|
|
4525
|
+
let batch = [];
|
|
4526
|
+
let bytes = 0;
|
|
4527
|
+
let sent = 0;
|
|
4528
|
+
const send = async () => {
|
|
4529
|
+
if (batch.length === 0) return;
|
|
4530
|
+
revision = (await call(`${base}/map/sectors`, { method: "PUT", body: { revision, sectors: batch } })).revision;
|
|
4531
|
+
sent += batch.length;
|
|
4532
|
+
deps.progress?.(`sending the map: ${sent} of ${sectors.length} parts`);
|
|
4533
|
+
batch = [];
|
|
4534
|
+
bytes = 0;
|
|
4535
|
+
};
|
|
4536
|
+
for (const sector of sectors) {
|
|
4537
|
+
const data = toBase64(await deps.zlib.deflate(encodeSector(sector.cells)));
|
|
4538
|
+
if (batch.length >= SECTORS_A_WRITE || bytes + data.length > BYTES_A_WRITE) await send();
|
|
4539
|
+
batch.push({ z: sector.z, cx: sector.cx, cy: sector.cy, data });
|
|
4540
|
+
bytes += data.length;
|
|
4541
|
+
}
|
|
4542
|
+
await send();
|
|
4543
|
+
await deps.mirror.moving(null);
|
|
4544
|
+
}
|
|
4545
|
+
const move = {
|
|
4546
|
+
name: "move",
|
|
4547
|
+
summary: "Move this folder's own map and art onto its platform project, once, when that project holds none",
|
|
4548
|
+
run: async (context) => {
|
|
4549
|
+
const local = await projectHere(context.io.cwd);
|
|
4550
|
+
const project = linkedOf(local);
|
|
4551
|
+
const platform = await signedIn(context);
|
|
4552
|
+
const mirror = mirrorOf(local);
|
|
4553
|
+
const state = await mirror.state();
|
|
4554
|
+
const again = state.moving === project;
|
|
4555
|
+
if (!again) {
|
|
4556
|
+
if (state.mirrored || !(state.own.map || state.own.art)) {
|
|
4557
|
+
throw new CliError("this folder holds no map or art of its own to move: it is written from its platform project already");
|
|
4558
|
+
}
|
|
4559
|
+
if (!await platformIsEmpty({ project, call: platform.call })) {
|
|
4560
|
+
throw new CliError(
|
|
4561
|
+
"the platform project already holds a map, art or a reference, and a folder is moved only onto one holding none: nothing was sent. Pass --aside to a pull, a seal or an export to keep this folder's own files aside instead"
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
try {
|
|
4566
|
+
await moveFolder({ project, call: platform.call, mirror, zlib: nodeZlib, progress: (words) => context.io.warn(`${words}
|
|
4567
|
+
`) });
|
|
4568
|
+
} catch (error) {
|
|
4569
|
+
if (error instanceof MoveRefused) throw new CliError(error.message);
|
|
4570
|
+
throw new CliError(`the move stopped, and is sent whole again by \`pixel-vibing-cli move\`: ${error instanceof Error ? error.message : String(error)}`);
|
|
4571
|
+
}
|
|
4572
|
+
await bringFolder(platform, local, { map: true, aside: true });
|
|
4573
|
+
answer(
|
|
4574
|
+
context,
|
|
4575
|
+
{ moved: true, project },
|
|
4576
|
+
`Moved this folder onto its platform project${again ? ", sent whole again" : ""}: its own files are kept under pixelvibing/before-platform/, and the folder is written from the platform.`
|
|
4577
|
+
);
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
const version = "0.11.0";
|
|
3766
4581
|
const manifest = {
|
|
3767
4582
|
version
|
|
3768
4583
|
};
|
|
@@ -4209,6 +5024,7 @@ const COMMANDS = [
|
|
|
4209
5024
|
whoami,
|
|
4210
5025
|
init,
|
|
4211
5026
|
link,
|
|
5027
|
+
move,
|
|
4212
5028
|
generate,
|
|
4213
5029
|
list,
|
|
4214
5030
|
pull,
|