@pixelvibing/cli 0.10.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 +1167 -537
- 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 { inflate, 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,225 +1045,58 @@ 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
|
-
return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
|
|
1111
|
-
}
|
|
1112
|
-
function pngSize(bytes) {
|
|
1113
|
-
if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
|
|
1114
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1115
|
-
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
1116
|
-
}
|
|
1117
|
-
const MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
1118
|
-
const artBoundary = (local) => new Boundary([artOf(local)]);
|
|
1119
|
-
const recordPath = (local) => join(local.root, ...PULLED_FILE.split("/"));
|
|
1120
|
-
async function readRecord(local) {
|
|
1121
|
-
const boundary = artBoundary(local);
|
|
1122
|
-
const path = recordPath(local);
|
|
1123
|
-
if (!await boundary.exists(path)) return EMPTY_PULLED;
|
|
1124
|
-
const { size } = await stat(await boundary.resolve(path));
|
|
1125
|
-
if (size > MAX_RECORD_BYTES) throw new CliError(`${PULLED_FILE} is ${size} bytes, and is read at most ${MAX_RECORD_BYTES}`);
|
|
1126
|
-
try {
|
|
1127
|
-
return readPulled(await boundary.readText(path));
|
|
1128
|
-
} catch (error) {
|
|
1129
|
-
if (error instanceof PulledError) throw new CliError(error.message);
|
|
1130
|
-
throw error;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
async function writeRecord(local, record) {
|
|
1134
|
-
await artBoundary(local).writeFileReplacing(recordPath(local), writePulled(record));
|
|
1135
|
-
}
|
|
1136
|
-
const INTERFACE_MANIFEST = "manifest/interface.json";
|
|
1137
|
-
function setPieces(asset) {
|
|
1138
|
-
return setPiecesOf(asset.kind, asset.images.length, asset.data);
|
|
1139
|
-
}
|
|
1140
|
-
function pieceFiles(name, pieces) {
|
|
1141
|
-
return pieceNames(name, pieces).map((piece) => `out/${piece}.png`);
|
|
1142
|
-
}
|
|
1143
|
-
const pictureName = (file) => file.replace(/^out\//, "").replace(/\.png$/, "");
|
|
1144
|
-
function filesOf(name, count) {
|
|
1145
|
-
return count === 1 ? [`out/${name}.png`] : Array.from({ length: count }, (_, index) => `out/${name}/${index}.png`);
|
|
1146
|
-
}
|
|
1147
|
-
function nameFor(asset, record, given) {
|
|
1148
|
-
const name = given ?? record.assets[asset.id]?.name ?? asset.name ?? SET_NAMES[asset.kind] ?? `${asset.kind}-${asset.id.slice(0, 8)}`;
|
|
1149
|
-
const problem = nameProblem(name);
|
|
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;
|
|
1152
|
-
}
|
|
1153
|
-
async function pullAsset(platform, local, ref, given) {
|
|
1154
|
-
const project = linkedOf(local);
|
|
1155
|
-
const asset = await platform.web.asset(project, ref);
|
|
1156
|
-
if (asset.images.length === 0) throw new CliError(`the asset ${asset.id} has no image the platform still keeps`);
|
|
1157
|
-
const record = await readRecord(local);
|
|
1158
|
-
const boundary = artBoundary(local);
|
|
1159
|
-
const art = artOf(local);
|
|
1160
|
-
const name = nameFor(asset, record, given);
|
|
1161
|
-
const pieces = setPieces(asset);
|
|
1162
|
-
const files = pieces === null ? filesOf(name, asset.images.length) : pieceFiles(name, pieces);
|
|
1163
|
-
const pulled = { id: asset.id, kind: asset.kind, name, hash: asset.hash, files, data: asset.data };
|
|
1164
|
-
const before = record.assets[asset.id];
|
|
1165
|
-
const onDisk = async (file) => boundary.exists(join(art, ...file.split("/")));
|
|
1166
|
-
if (before !== void 0 && before.hash === asset.hash && before.name === name && (await Promise.all(before.files.map(onDisk))).every(Boolean)) {
|
|
1167
|
-
return { ...pulled, files: before.files, current: true };
|
|
1168
|
-
}
|
|
1169
|
-
for (const file of files) {
|
|
1170
|
-
const owner = ownerOf(record, file);
|
|
1171
|
-
if (owner !== asset.id && await onDisk(file)) {
|
|
1172
|
-
throw new CliError(
|
|
1173
|
-
`pixelvibing/${file} is already there, ${owner === null ? "written by the editor or by hand" : `pulled for the asset ${owner}`}: nothing was written. Give this one another name with --as`
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
const images = [];
|
|
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 };
|
|
1192
|
-
}
|
|
1193
|
-
async function describeInterface(boundary, art, written, earlier) {
|
|
1194
|
-
const path = join(art, ...INTERFACE_MANIFEST.split("/"));
|
|
1195
|
-
let manifest2 = {};
|
|
1196
|
-
if (await boundary.exists(path)) {
|
|
1197
|
-
try {
|
|
1198
|
-
const parsed2 = JSON.parse(await boundary.readText(path));
|
|
1199
|
-
if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) manifest2 = parsed2;
|
|
1200
|
-
else throw new Error("not an object");
|
|
1201
|
-
} catch (error) {
|
|
1202
|
-
throw new CliError(`pixelvibing/${INTERFACE_MANIFEST} is not a manifest (${error.message}): the pieces were written, and not described`);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
manifest2 = describedInterface(
|
|
1206
|
-
manifest2,
|
|
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
|
-
`));
|
|
1212
|
-
}
|
|
1213
|
-
function pulledWords(pulled) {
|
|
1214
|
-
const where = pulled.files.length === 1 ? `pixelvibing/${pulled.files[0]}` : `pixelvibing/out/${pulled.name}/ (${pulled.files.length} images)`;
|
|
1215
|
-
return pulled.current ? `${pulled.name} is current: ${where} is what the platform has.` : `Pulled ${pulled.name}, ${pulled.kind}, into ${where}.`;
|
|
1216
|
-
}
|
|
1217
|
-
const date = (at) => at.slice(0, 10);
|
|
1218
|
-
function listed(assets) {
|
|
1219
|
-
if (assets.length === 0) return "No assets.";
|
|
1220
|
-
return assets.map(
|
|
1221
|
-
(asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}`
|
|
1222
|
-
).join("\n");
|
|
1223
|
-
}
|
|
1224
|
-
const list = {
|
|
1225
|
-
name: "list",
|
|
1226
|
-
summary: "List the platform project's assets, of one kind when given, newest first",
|
|
1227
|
-
usage: "[<kind>] [--before <id>]",
|
|
1228
|
-
options: { before: { type: "string", value: "<id>", description: "the page after the asset with this id, as the last page said" } },
|
|
1229
|
-
positionals: [0, 1],
|
|
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}`);
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
const pull = {
|
|
1242
|
-
name: "pull",
|
|
1243
|
-
summary: "Pull one asset, by its id or its name, into the local project",
|
|
1244
|
-
usage: "<id|name> [--as <name>]",
|
|
1245
|
-
options: { as: { type: "string", value: "<name>", description: "write it under this name instead of the platform's" } },
|
|
1246
|
-
positionals: [1, 1],
|
|
1247
|
-
run: async (context, args) => {
|
|
1248
|
-
const local = await projectHere(context.io.cwd);
|
|
1249
|
-
linkedOf(local);
|
|
1250
|
-
const platform = await signedIn(context);
|
|
1251
|
-
const pulled = await pullAsset(platform, local, args.positionals[0], stringOf(args, "as"));
|
|
1252
|
-
answer(context, pulled, pulledWords(pulled));
|
|
1253
|
-
}
|
|
1254
|
-
};
|
|
1255
|
-
const DOWNLOAD_URL = "https://github.com/Pixel-Vibing/pixel-vibing/releases/latest";
|
|
1256
|
-
const EDITOR_VARIABLE = "PIXELVIBING_EDITOR";
|
|
1257
|
-
async function exists$1(path) {
|
|
1258
|
-
try {
|
|
1259
|
-
await stat(path);
|
|
1260
|
-
return true;
|
|
1261
|
-
} catch {
|
|
1262
|
-
return false;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
async function findEditor(platform, env, home) {
|
|
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: [] };
|
|
1270
|
-
}
|
|
1271
|
-
if (platform === "win32") {
|
|
1272
|
-
const places = [env.LOCALAPPDATA && join(env.LOCALAPPDATA, "Programs", "PixelVibing"), env.ProgramFiles && join(env.ProgramFiles, "PixelVibing")];
|
|
1273
|
-
for (const place of places) {
|
|
1274
|
-
if (typeof place === "string" && await exists$1(join(place, "PixelVibing.exe"))) return { command: join(place, "PixelVibing.exe"), args: [] };
|
|
1275
|
-
}
|
|
1276
|
-
return null;
|
|
1277
|
-
}
|
|
1278
|
-
if (platform === "darwin") {
|
|
1279
|
-
for (const app of ["/Applications/PixelVibing.app", join(home, "Applications", "PixelVibing.app")]) {
|
|
1280
|
-
if (await exists$1(app)) return { command: "open", args: ["-a", app, "--args"] };
|
|
1281
|
-
}
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
const applications = join(home, "Applications");
|
|
1285
|
-
let found = [];
|
|
1286
|
-
try {
|
|
1287
|
-
found = (await readdir(applications)).filter((name) => /^PixelVibing.*\.AppImage$/i.test(name)).sort();
|
|
1288
|
-
} catch {
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
const newest = found.at(-1);
|
|
1292
|
-
return newest === void 0 ? null : { command: join(applications, newest), args: [] };
|
|
1293
|
-
}
|
|
1294
|
-
const editor = {
|
|
1295
|
-
name: "editor",
|
|
1296
|
-
summary: "Open the PixelVibing desktop app on this local project",
|
|
1297
|
-
run: async (context) => {
|
|
1298
|
-
const local = await projectHere(context.io.cwd);
|
|
1299
|
-
const { platform, home } = context.services;
|
|
1300
|
-
const found = await findEditor(platform, context.io.env, home);
|
|
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`);
|
|
1303
|
-
}
|
|
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
|
-
}
|
|
1307
|
-
};
|
|
1308
|
-
const SECTOR_SIDE = 128;
|
|
1309
|
-
const SECTOR_CELLS = SECTOR_SIDE * SECTOR_SIDE;
|
|
1048
|
+
const SECTOR_SIDE$1 = 128;
|
|
1049
|
+
const SECTOR_CELLS = SECTOR_SIDE$1 * SECTOR_SIDE$1;
|
|
1310
1050
|
const SECTOR_BYTES = 512 * 1024;
|
|
1311
1051
|
const MAP_LEVELS = { first: 0, last: 14 };
|
|
1312
1052
|
const LARGEST_MAP_SIDE = 1e4;
|
|
1313
|
-
const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE);
|
|
1053
|
+
const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE$1);
|
|
1314
1054
|
const SECTOR_FORMAT = 1;
|
|
1315
1055
|
const MAGIC = [80, 86, 67];
|
|
1316
1056
|
const PRESENT = 1;
|
|
1317
1057
|
const AUDIO = 2;
|
|
1318
1058
|
const INTERACT = 4;
|
|
1319
1059
|
const EXTRA = 8;
|
|
1320
|
-
const
|
|
1060
|
+
const U16 = 65535;
|
|
1061
|
+
const MAX_LAYERS = 255;
|
|
1062
|
+
const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
|
|
1321
1063
|
class SectorError extends Error {
|
|
1322
1064
|
constructor(message) {
|
|
1323
1065
|
super(message);
|
|
1324
1066
|
this.name = "SectorError";
|
|
1325
1067
|
}
|
|
1326
1068
|
}
|
|
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;
|
|
1072
|
+
}
|
|
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
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return Uint8Array.from(bytes);
|
|
1099
|
+
}
|
|
1327
1100
|
function decodeSector(bytes) {
|
|
1328
1101
|
if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
|
|
1329
1102
|
if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
|
|
@@ -1367,7 +1140,7 @@ function decodeSector(bytes) {
|
|
|
1367
1140
|
throw new SectorError("a tile's extra fields are not JSON");
|
|
1368
1141
|
}
|
|
1369
1142
|
if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
|
|
1370
|
-
const named = Object.keys(parsed2).find((key) => NAMED.has(key));
|
|
1143
|
+
const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
|
|
1371
1144
|
if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
|
|
1372
1145
|
extra = parsed2;
|
|
1373
1146
|
at += length;
|
|
@@ -1384,6 +1157,18 @@ function decodeSector(bytes) {
|
|
|
1384
1157
|
if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
|
|
1385
1158
|
return cells;
|
|
1386
1159
|
}
|
|
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
|
+
};
|
|
1171
|
+
}
|
|
1387
1172
|
function tileOfCell(cell, x, y, z) {
|
|
1388
1173
|
return {
|
|
1389
1174
|
...cell.extra ?? {},
|
|
@@ -1420,6 +1205,10 @@ function isSize(value) {
|
|
|
1420
1205
|
const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
|
|
1421
1206
|
return typeof value === "object" && value !== null && side(value.width) && side(value.height);
|
|
1422
1207
|
}
|
|
1208
|
+
function mirrorIndexText(index) {
|
|
1209
|
+
return `${JSON.stringify(index, null, 2)}
|
|
1210
|
+
`;
|
|
1211
|
+
}
|
|
1423
1212
|
function parseMirrorIndex(text2) {
|
|
1424
1213
|
let value;
|
|
1425
1214
|
try {
|
|
@@ -1444,7 +1233,7 @@ function parseMirrorIndex(text2) {
|
|
|
1444
1233
|
throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
|
|
1445
1234
|
}
|
|
1446
1235
|
const key = `${z}:${cx}:${cy}`;
|
|
1447
|
-
if (cx * SECTOR_SIDE >= index.size.width || cy * SECTOR_SIDE >= index.size.height) {
|
|
1236
|
+
if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
|
|
1448
1237
|
throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
|
|
1449
1238
|
}
|
|
1450
1239
|
if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
|
|
@@ -1464,8 +1253,8 @@ async function readMirroredMap(index, read, inflate2) {
|
|
|
1464
1253
|
}
|
|
1465
1254
|
for (const [at, cell] of cells.entries()) {
|
|
1466
1255
|
if (cell === void 0) continue;
|
|
1467
|
-
const x = cx * SECTOR_SIDE + at % SECTOR_SIDE;
|
|
1468
|
-
const y = cy * SECTOR_SIDE + Math.floor(at / SECTOR_SIDE);
|
|
1256
|
+
const x = cx * SECTOR_SIDE$1 + at % SECTOR_SIDE$1;
|
|
1257
|
+
const y = cy * SECTOR_SIDE$1 + Math.floor(at / SECTOR_SIDE$1);
|
|
1469
1258
|
if (x >= index.size.width || y >= index.size.height) continue;
|
|
1470
1259
|
tiles.push(tileOfCell(cell, x, y, z));
|
|
1471
1260
|
}
|
|
@@ -1473,41 +1262,180 @@ async function readMirroredMap(index, read, inflate2) {
|
|
|
1473
1262
|
tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
|
|
1474
1263
|
return { width: index.size.width, height: index.size.height, tiles };
|
|
1475
1264
|
}
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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);
|
|
1283
|
+
}
|
|
1284
|
+
class MapTextError extends Error {
|
|
1483
1285
|
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1484
1286
|
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1485
1287
|
constructor(message) {
|
|
1486
1288
|
super(message);
|
|
1487
|
-
this.name = "
|
|
1289
|
+
this.name = "MapTextError";
|
|
1488
1290
|
}
|
|
1489
1291
|
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
if (
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
+
}
|
|
1326
|
+
}
|
|
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;
|
|
1336
|
+
try {
|
|
1337
|
+
parsed2 = JSON.parse(text2);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
|
|
1340
|
+
}
|
|
1341
|
+
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
1342
|
+
throw new MapTextError(`${path} holds no tiles array`);
|
|
1343
|
+
}
|
|
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`);
|
|
1348
|
+
}
|
|
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];
|
|
1361
|
+
}
|
|
1362
|
+
for (const key of Object.keys(tile)) {
|
|
1363
|
+
if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
|
|
1364
|
+
}
|
|
1365
|
+
return JSON.stringify(ordered);
|
|
1366
|
+
}
|
|
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;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
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;
|
|
1404
|
+
const TILE_SIZE = 256;
|
|
1405
|
+
const EAI_MAGIC = 1229018181;
|
|
1406
|
+
const EAI_VERSION = 1;
|
|
1407
|
+
const HEADER_SIZE$1 = 32;
|
|
1408
|
+
const ASSET_SIZE = 24;
|
|
1409
|
+
const ATLAS_SIZE = 12;
|
|
1410
|
+
class AssetIndexError extends Error {
|
|
1411
|
+
// Assigned in the body: the suites run under Node's strip-only TypeScript
|
|
1412
|
+
// mode, which rejects parameter properties (docs/notes.md n-0023).
|
|
1413
|
+
constructor(message) {
|
|
1414
|
+
super(message);
|
|
1415
|
+
this.name = "AssetIndexError";
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function parseAssetIndex(buffer) {
|
|
1419
|
+
if (buffer.byteLength < HEADER_SIZE$1) {
|
|
1420
|
+
throw new AssetIndexError(`buffer too small: ${buffer.byteLength} < ${HEADER_SIZE$1}`);
|
|
1421
|
+
}
|
|
1422
|
+
const view = new DataView(buffer);
|
|
1423
|
+
const magic = view.getUint32(0, true);
|
|
1424
|
+
if (magic !== EAI_MAGIC) {
|
|
1425
|
+
throw new AssetIndexError(`bad magic 0x${magic.toString(16)} (expected 0x${EAI_MAGIC.toString(16)})`);
|
|
1426
|
+
}
|
|
1427
|
+
const version2 = view.getUint32(4, true);
|
|
1428
|
+
if (version2 !== EAI_VERSION) {
|
|
1429
|
+
throw new AssetIndexError(`unsupported version ${version2}, reader speaks v${EAI_VERSION}`);
|
|
1430
|
+
}
|
|
1431
|
+
const assetCount = view.getUint32(8, true);
|
|
1432
|
+
const atlasCount = view.getUint32(12, true);
|
|
1433
|
+
const assetsOffset = view.getUint32(16, true);
|
|
1434
|
+
const atlasesOffset = view.getUint32(20, true);
|
|
1435
|
+
const stringsOffset = view.getUint32(24, true);
|
|
1436
|
+
const stringsLength = view.getUint32(28, true);
|
|
1437
|
+
if (buffer.byteLength !== stringsOffset + stringsLength) {
|
|
1438
|
+
throw new AssetIndexError(`file size ${buffer.byteLength} ≠ header-declared ${stringsOffset + stringsLength}`);
|
|
1511
1439
|
}
|
|
1512
1440
|
if (assetsOffset < HEADER_SIZE$1) {
|
|
1513
1441
|
throw new AssetIndexError(`assetsOffset ${assetsOffset} is inside the ${HEADER_SIZE$1}-byte header`);
|
|
@@ -1727,7 +1655,7 @@ class AssetMetadataError extends Error {
|
|
|
1727
1655
|
this.name = "AssetMetadataError";
|
|
1728
1656
|
}
|
|
1729
1657
|
}
|
|
1730
|
-
function isRecord$
|
|
1658
|
+
function isRecord$2(value) {
|
|
1731
1659
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1732
1660
|
}
|
|
1733
1661
|
const MAX_ANIMATION_FRAMES = 64;
|
|
@@ -1739,7 +1667,7 @@ function readFrames(value, asset, animation) {
|
|
|
1739
1667
|
throw new AssetMetadataError(`${asset}: animation ${animation} has ${value.length} frames, and an animation holds at most ${MAX_ANIMATION_FRAMES}`);
|
|
1740
1668
|
}
|
|
1741
1669
|
return value.map((raw, index) => {
|
|
1742
|
-
if (!isRecord$
|
|
1670
|
+
if (!isRecord$2(raw)) {
|
|
1743
1671
|
throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} is not a frame`);
|
|
1744
1672
|
}
|
|
1745
1673
|
const name = raw.asset;
|
|
@@ -1758,7 +1686,7 @@ function readFrames(value, asset, animation) {
|
|
|
1758
1686
|
function readAnimations(value, asset, kind) {
|
|
1759
1687
|
const animations = /* @__PURE__ */ new Map();
|
|
1760
1688
|
if (value === void 0) return animations;
|
|
1761
|
-
if (!isRecord$
|
|
1689
|
+
if (!isRecord$2(value)) {
|
|
1762
1690
|
throw new AssetMetadataError(`${asset}: animations is not a map of name to frames`);
|
|
1763
1691
|
}
|
|
1764
1692
|
for (const [name, frames] of Object.entries(value)) {
|
|
@@ -1775,10 +1703,10 @@ function readAnimations(value, asset, kind) {
|
|
|
1775
1703
|
function readPlays(value, asset, animations, kind) {
|
|
1776
1704
|
const plays = /* @__PURE__ */ new Map();
|
|
1777
1705
|
if (value === void 0) return plays;
|
|
1778
|
-
if (!isRecord$
|
|
1706
|
+
if (!isRecord$2(value)) throw new AssetMetadataError(`${asset}: plays is not a map of animation to how it plays`);
|
|
1779
1707
|
for (const [name, play] of Object.entries(value)) {
|
|
1780
1708
|
if (!animations.has(name)) throw new AssetMetadataError(`${asset}: plays names ${name}, and there is no such animation`);
|
|
1781
|
-
if (!isRecord$
|
|
1709
|
+
if (!isRecord$2(play)) throw new AssetMetadataError(`${asset}: how ${name} plays is not an object`);
|
|
1782
1710
|
if (kind === "outfit" && play.mode !== "loop")
|
|
1783
1711
|
throw new AssetMetadataError(`${asset}: ${name} is an outfit's animation, which plays as the game drives it, in a loop`);
|
|
1784
1712
|
if (play.mode === "loop" || play.mode === "change") {
|
|
@@ -1801,7 +1729,7 @@ function readPlays(value, asset, animations, kind) {
|
|
|
1801
1729
|
}
|
|
1802
1730
|
function readNineGrid(value, asset) {
|
|
1803
1731
|
if (value === void 0) return void 0;
|
|
1804
|
-
if (!isRecord$
|
|
1732
|
+
if (!isRecord$2(value)) {
|
|
1805
1733
|
throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
|
|
1806
1734
|
}
|
|
1807
1735
|
const insets = {};
|
|
@@ -1826,7 +1754,7 @@ function readScale(value, asset) {
|
|
|
1826
1754
|
}
|
|
1827
1755
|
function readCollide(value, asset) {
|
|
1828
1756
|
if (value === void 0) return void 0;
|
|
1829
|
-
if (!isRecord$
|
|
1757
|
+
if (!isRecord$2(value)) {
|
|
1830
1758
|
throw new AssetMetadataError(`${asset}: collide is not a box of x, y, w and h in cells`);
|
|
1831
1759
|
}
|
|
1832
1760
|
const box = {};
|
|
@@ -1847,7 +1775,7 @@ const MIN_DRAW_SCALE = 0.05;
|
|
|
1847
1775
|
const MAX_DRAW_SCALE = 8;
|
|
1848
1776
|
function readOffset(value, asset) {
|
|
1849
1777
|
if (value === void 0) return void 0;
|
|
1850
|
-
if (!isRecord$
|
|
1778
|
+
if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
1851
1779
|
throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
|
|
1852
1780
|
}
|
|
1853
1781
|
for (const axis of ["x", "y"]) {
|
|
@@ -1869,7 +1797,7 @@ function readDrawScale(value, asset) {
|
|
|
1869
1797
|
return value;
|
|
1870
1798
|
}
|
|
1871
1799
|
function readEntry(value, asset) {
|
|
1872
|
-
if (!isRecord$
|
|
1800
|
+
if (!isRecord$2(value)) {
|
|
1873
1801
|
throw new AssetMetadataError(`${asset}: not an entry`);
|
|
1874
1802
|
}
|
|
1875
1803
|
const kind = value.kind;
|
|
@@ -1914,7 +1842,7 @@ function parseAssetMetadata(text2) {
|
|
|
1914
1842
|
} catch (error) {
|
|
1915
1843
|
throw new AssetMetadataError(`not JSON: ${error.message}`);
|
|
1916
1844
|
}
|
|
1917
|
-
if (!isRecord$
|
|
1845
|
+
if (!isRecord$2(parsed2)) {
|
|
1918
1846
|
throw new AssetMetadataError("not an object");
|
|
1919
1847
|
}
|
|
1920
1848
|
const version2 = parsed2.version;
|
|
@@ -1922,7 +1850,7 @@ function parseAssetMetadata(text2) {
|
|
|
1922
1850
|
throw new AssetMetadataError(`unsupported version ${String(version2)}, reader speaks v${METADATA_VERSION}`);
|
|
1923
1851
|
}
|
|
1924
1852
|
const rawAssets = parsed2.assets;
|
|
1925
|
-
if (rawAssets !== void 0 && !isRecord$
|
|
1853
|
+
if (rawAssets !== void 0 && !isRecord$2(rawAssets)) {
|
|
1926
1854
|
throw new AssetMetadataError("assets is not a map of name to entry");
|
|
1927
1855
|
}
|
|
1928
1856
|
const assets = /* @__PURE__ */ new Map();
|
|
@@ -2117,129 +2045,79 @@ function placeTileSprite(tx, ty, width, height, options = {}) {
|
|
|
2117
2045
|
height: scaledHeight
|
|
2118
2046
|
};
|
|
2119
2047
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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;
|
|
2054
|
+
}
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
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).
|
|
2123
2066
|
constructor(message) {
|
|
2124
2067
|
super(message);
|
|
2125
|
-
this.name = "
|
|
2068
|
+
this.name = "ArtError";
|
|
2126
2069
|
}
|
|
2127
2070
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
function
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
let start = -1;
|
|
2141
|
-
for (let index = at + 1; index < text2.length; index += 1) {
|
|
2142
|
-
const char = text2[index];
|
|
2143
|
-
if (inString) {
|
|
2144
|
-
if (char === "\\") index += 1;
|
|
2145
|
-
else if (char === '"') inString = false;
|
|
2146
|
-
continue;
|
|
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;
|
|
2147
2083
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
if (
|
|
2152
|
-
depth += 1;
|
|
2153
|
-
} else if (char === "}") {
|
|
2154
|
-
depth -= 1;
|
|
2155
|
-
if (depth === 0 && start !== -1) {
|
|
2156
|
-
sources.push(text2.slice(start, index + 1));
|
|
2157
|
-
start = -1;
|
|
2158
|
-
}
|
|
2159
|
-
} else if (char === "]" && depth === 0) {
|
|
2160
|
-
break;
|
|
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);
|
|
2161
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}/`);
|
|
2162
2096
|
}
|
|
2163
|
-
return
|
|
2097
|
+
return boundary.readBytes(join(artDirectory, REFERENCE_DIRECTORY, name));
|
|
2164
2098
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2099
|
+
const MANIFEST_DIRECTORY = "manifest";
|
|
2100
|
+
const OUT_DIRECTORY = "out";
|
|
2101
|
+
const ID_MAP_FILE = "id-map.json";
|
|
2102
|
+
const MAX_SIDE$1 = 65535;
|
|
2103
|
+
const encoder$1 = new TextEncoder();
|
|
2104
|
+
function isRecord$1(value) {
|
|
2105
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2169
2106
|
}
|
|
2170
|
-
function
|
|
2171
|
-
|
|
2107
|
+
function metadataOf(entry) {
|
|
2108
|
+
const raw = isRecord$1(entry.meta) && entry.meta.kind === void 0 ? { kind: "tile", ...entry.meta } : entry.meta;
|
|
2172
2109
|
try {
|
|
2173
|
-
parsed2 = JSON.
|
|
2110
|
+
const parsed2 = parseAssetMetadata(JSON.stringify({ version: METADATA_VERSION, assets: { [entry.name]: raw } }));
|
|
2111
|
+
return parsed2.assets.get(entry.name);
|
|
2174
2112
|
} catch (error) {
|
|
2175
|
-
throw new
|
|
2176
|
-
}
|
|
2177
|
-
if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
|
|
2178
|
-
throw new MapTextError(`${path} holds no tiles array`);
|
|
2179
|
-
}
|
|
2180
|
-
const document = parsed2;
|
|
2181
|
-
const sources = scanTileSources(text2);
|
|
2182
|
-
if (sources.length !== document.tiles.length) {
|
|
2183
|
-
throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
|
|
2113
|
+
throw new ProjectError(`the manifest entry ${entry.name} does not describe its asset: ${error.message}`);
|
|
2184
2114
|
}
|
|
2185
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2186
|
-
document.tiles.forEach((tile, index) => {
|
|
2187
|
-
if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
|
|
2188
|
-
byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
|
|
2189
|
-
});
|
|
2190
|
-
return { document, sources: byKey };
|
|
2191
2115
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
}
|
|
2198
|
-
for (const key of Object.keys(tile)) {
|
|
2199
|
-
if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
|
|
2200
|
-
}
|
|
2201
|
-
return JSON.stringify(ordered);
|
|
2202
|
-
}
|
|
2203
|
-
function formatMap(next, previous) {
|
|
2204
|
-
const lines = [];
|
|
2205
|
-
let changed = 0;
|
|
2206
|
-
for (const tile of next.tiles) {
|
|
2207
|
-
{
|
|
2208
|
-
lines.push(serializeTile(tile));
|
|
2209
|
-
changed += 1;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const rest = {};
|
|
2213
|
-
for (const key of Object.keys(next)) {
|
|
2214
|
-
if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
|
|
2215
|
-
}
|
|
2216
|
-
const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
|
|
2217
|
-
const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
|
|
2218
|
-
const body = lines.length === 0 ? "" : `
|
|
2219
|
-
${lines.join(",\n")}
|
|
2220
|
-
`;
|
|
2221
|
-
return { text: `${head}${body}]${trailer}}
|
|
2222
|
-
`, changed };
|
|
2223
|
-
}
|
|
2224
|
-
const MAX_SIDE$1 = 65535;
|
|
2225
|
-
const encoder$1 = new TextEncoder();
|
|
2226
|
-
function isRecord(value) {
|
|
2227
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2228
|
-
}
|
|
2229
|
-
function metadataOf(entry) {
|
|
2230
|
-
const raw = isRecord(entry.meta) && entry.meta.kind === void 0 ? { kind: "tile", ...entry.meta } : entry.meta;
|
|
2231
|
-
try {
|
|
2232
|
-
const parsed2 = parseAssetMetadata(JSON.stringify({ version: METADATA_VERSION, assets: { [entry.name]: raw } }));
|
|
2233
|
-
return parsed2.assets.get(entry.name);
|
|
2234
|
-
} catch (error) {
|
|
2235
|
-
throw new ProjectError(`the manifest entry ${entry.name} does not describe its asset: ${error.message}`);
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
function describedAssets(entries) {
|
|
2239
|
-
const described = /* @__PURE__ */ new Map();
|
|
2240
|
-
for (const entry of entries) {
|
|
2241
|
-
if (entry.meta === void 0) continue;
|
|
2242
|
-
described.set(entry.name, metadataOf(entry));
|
|
2116
|
+
function describedAssets(entries) {
|
|
2117
|
+
const described = /* @__PURE__ */ new Map();
|
|
2118
|
+
for (const entry of entries) {
|
|
2119
|
+
if (entry.meta === void 0) continue;
|
|
2120
|
+
described.set(entry.name, metadataOf(entry));
|
|
2243
2121
|
}
|
|
2244
2122
|
return described;
|
|
2245
2123
|
}
|
|
@@ -2277,132 +2155,737 @@ function sealProjectArt(art) {
|
|
|
2277
2155
|
}
|
|
2278
2156
|
return { container: sealContainer([...entries, ...pages]), pictures: pictures.length, described: described.size };
|
|
2279
2157
|
}
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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("/");
|
|
2285
2164
|
}
|
|
2286
|
-
|
|
2287
|
-
const
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
+
`;
|
|
2710
|
+
}
|
|
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;
|
|
2293
2725
|
}
|
|
2294
|
-
return null;
|
|
2295
2726
|
}
|
|
2296
|
-
({
|
|
2297
|
-
|
|
2298
|
-
height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
|
|
2299
|
-
});
|
|
2300
|
-
Array.from({ length: 16 }, (_value, index) => index);
|
|
2301
|
-
const CONTAINER_FILE = join("build", "assets.eac");
|
|
2302
|
-
const MANIFEST_DIRECTORY = "manifest";
|
|
2303
|
-
const OUT_DIRECTORY = "out";
|
|
2304
|
-
const ID_MAP_FILE = "id-map.json";
|
|
2305
|
-
const PROJECT_SEAL_STEP = "project:seal";
|
|
2306
|
-
const MAX_PICTURE_BYTES = 64 * 1024 * 1024;
|
|
2307
|
-
const MAX_TEXT_BYTES = 16 * 1024 * 1024;
|
|
2308
|
-
const MAX_PICTURES = 2e4;
|
|
2309
|
-
function shown(artDirectory, relative2) {
|
|
2310
|
-
return [basename(artDirectory), ...relative2.split(/[\\/]/)].join("/");
|
|
2727
|
+
async function writeRecord(local, record) {
|
|
2728
|
+
await artBoundary(local).writeFileReplacing(recordPath(local), writePulled(record));
|
|
2311
2729
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
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);
|
|
2315
2733
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const walk = async (directory, prefix) => {
|
|
2319
|
-
let entries;
|
|
2320
|
-
try {
|
|
2321
|
-
entries = await readdir(await boundary.resolve(directory), { withFileTypes: true });
|
|
2322
|
-
} catch {
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
for (const entry of entries) {
|
|
2326
|
-
const name = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
2327
|
-
if (entry.isDirectory()) await walk(join(directory, entry.name), name);
|
|
2328
|
-
else if (entry.isFile() && entry.name.endsWith(".png")) {
|
|
2329
|
-
if (pictures.length >= MAX_PICTURES) throw new ProjectError(`${shown(artDirectory, OUT_DIRECTORY)} holds more than ${MAX_PICTURES} pictures`);
|
|
2330
|
-
const path = join(directory, entry.name);
|
|
2331
|
-
await sized(boundary, path, MAX_PICTURE_BYTES, `${shown(artDirectory, OUT_DIRECTORY)}/${name}`);
|
|
2332
|
-
pictures.push({ name: name.slice(0, -".png".length), bytes: await boundary.readBytes(path) });
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
};
|
|
2336
|
-
await walk(join(artDirectory, OUT_DIRECTORY), "");
|
|
2337
|
-
return pictures;
|
|
2734
|
+
function namesOf(name, count) {
|
|
2735
|
+
return count === 1 ? [name] : Array.from({ length: count }, (_, index) => `${name}/${index}`);
|
|
2338
2736
|
}
|
|
2339
|
-
|
|
2340
|
-
const
|
|
2341
|
-
const
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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
|
+
);
|
|
2349
2772
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
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 } });
|
|
2354
2785
|
}
|
|
2355
2786
|
}
|
|
2356
|
-
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
const path = join(artDirectory, ID_MAP_FILE);
|
|
2360
|
-
if (!await boundary.exists(path)) return null;
|
|
2361
|
-
await sized(boundary, path, MAX_TEXT_BYTES, shown(artDirectory, ID_MAP_FILE));
|
|
2362
|
-
return boundary.readText(path);
|
|
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 };
|
|
2363
2790
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
entries: await readEntries(boundary, artDirectory),
|
|
2368
|
-
idMap: await readIdMap(boundary, artDirectory)
|
|
2369
|
-
};
|
|
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}.`;
|
|
2370
2794
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
if (
|
|
2374
|
-
|
|
2375
|
-
|
|
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");
|
|
2376
2801
|
}
|
|
2377
|
-
const
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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
|
+
}
|
|
2393
2845
|
}
|
|
2394
|
-
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 = [];
|
|
2395
2867
|
try {
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
const described = `${sealed.described} ${sealed.described === 1 ? "asset" : "assets"} described`;
|
|
2400
|
-
return [{ step: PROJECT_SEAL_STEP, code: 0, output: `sealed ${pictures} into ${shown(artDirectory, CONTAINER_FILE)}, ${described}` }];
|
|
2401
|
-
} catch (error) {
|
|
2402
|
-
if (!(error instanceof ProjectError)) throw error;
|
|
2403
|
-
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;
|
|
2404
2871
|
}
|
|
2872
|
+
const newest = found.at(-1);
|
|
2873
|
+
return newest === void 0 ? null : { command: join(applications, newest), args: [] };
|
|
2405
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
|
+
};
|
|
2406
2889
|
const DRAW_LAYERS = ["ground", "object", "foreground"];
|
|
2407
2890
|
function bandOfAsset(facts) {
|
|
2408
2891
|
if (facts.name === null) return "ground";
|
|
@@ -3113,10 +3596,12 @@ async function writeWhole(out, files, engine) {
|
|
|
3113
3596
|
const exportCommand = {
|
|
3114
3597
|
name: "export",
|
|
3115
3598
|
summary: "Export the local project's art and map for an engine: default, pixi, phaser or godot",
|
|
3116
|
-
usage: "<default|pixi|phaser|godot> [--out <dir>] [--godot-root <res://…>]",
|
|
3599
|
+
usage: "<default|pixi|phaser|godot> [--out <dir>] [--godot-root <res://…>] [--offline] [--aside]",
|
|
3117
3600
|
options: {
|
|
3118
3601
|
out: { type: "string", value: "<dir>", description: "where to write it; pixelvibing/export/<engine>/ unless given" },
|
|
3119
|
-
"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" }
|
|
3120
3605
|
},
|
|
3121
3606
|
positionals: [1, 1],
|
|
3122
3607
|
run: async (context, args) => {
|
|
@@ -3127,6 +3612,7 @@ const exportCommand = {
|
|
|
3127
3612
|
throw new CliError("--godot-root is a res:// path of letters, digits and . _ - /, with no ..");
|
|
3128
3613
|
}
|
|
3129
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") });
|
|
3130
3616
|
const art = artOf(local);
|
|
3131
3617
|
const out = resolve(context.io.cwd, stringOf(args, "out") ?? join(art, "export", engine));
|
|
3132
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`);
|
|
@@ -3616,7 +4102,8 @@ const generate = {
|
|
|
3616
4102
|
reference: { type: "string", multiple: true, value: "<png>", description: "an image to draw from" },
|
|
3617
4103
|
quality: { type: "string", value: "<quality>", description: `${EDIT_QUALITIES.join(", ")}; ${DEFAULT_QUALITY} unless given` },
|
|
3618
4104
|
yes: { type: "boolean", short: "y", description: "start it without asking, once the cost is said" },
|
|
3619
|
-
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" }
|
|
3620
4107
|
},
|
|
3621
4108
|
positionals: [1, 1],
|
|
3622
4109
|
run: async (context, args) => {
|
|
@@ -3624,6 +4111,7 @@ const generate = {
|
|
|
3624
4111
|
const project = linkedOf(local);
|
|
3625
4112
|
const kind = args.positionals[0];
|
|
3626
4113
|
const request = await requestOf(context, kind, args);
|
|
4114
|
+
await checkFolder(local, flagOf(args, "aside"));
|
|
3627
4115
|
const platform = await signedIn(context);
|
|
3628
4116
|
const cost = costOf(await platform.web.prices(), request);
|
|
3629
4117
|
await confirmed(context, platform, request.words, cost, flagOf(args, "yes"));
|
|
@@ -3632,7 +4120,7 @@ const generate = {
|
|
|
3632
4120
|
`);
|
|
3633
4121
|
const job = await followed(context, platform, jobId);
|
|
3634
4122
|
if (job.state === "failed") throw new CliError(`the generation failed: ${job.reason ?? "the platform gave no reason"}`);
|
|
3635
|
-
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") });
|
|
3636
4124
|
answer(context, { job: jobId, cost, ...pulled }, pulledWords(pulled));
|
|
3637
4125
|
}
|
|
3638
4126
|
};
|
|
@@ -3940,15 +4428,156 @@ const link = {
|
|
|
3940
4428
|
const seal = {
|
|
3941
4429
|
name: "seal",
|
|
3942
4430
|
summary: "Seal the local project's art into its container, as the editor does",
|
|
3943
|
-
|
|
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) => {
|
|
3944
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") });
|
|
3945
4439
|
const art = artOf(local);
|
|
3946
4440
|
const [result] = await sealProject(new Boundary([art]), art);
|
|
3947
4441
|
if (result === void 0 || result.code !== 0) throw new CliError(result?.output ?? "the container was not rebuilt");
|
|
3948
4442
|
answer(context, { sealed: true, output: result.output }, result.output);
|
|
3949
4443
|
}
|
|
3950
4444
|
};
|
|
3951
|
-
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";
|
|
3952
4581
|
const manifest = {
|
|
3953
4582
|
version
|
|
3954
4583
|
};
|
|
@@ -4395,6 +5024,7 @@ const COMMANDS = [
|
|
|
4395
5024
|
whoami,
|
|
4396
5025
|
init,
|
|
4397
5026
|
link,
|
|
5027
|
+
move,
|
|
4398
5028
|
generate,
|
|
4399
5029
|
list,
|
|
4400
5030
|
pull,
|