@pixelvibing/cli 0.46.0 → 0.49.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.
@@ -576,10 +576,11 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
576
576
  /** Every version of the guide, newest first, with how and when each was made (specs/platform-projects/ R2.10). */
577
577
  guideVersions: async (projectId) => (await json2(`/projects/${id(projectId)}/style-guide/versions`)).versions,
578
578
  reviseGuide: (projectId, instruction) => paying2(async () => (await json2(`/projects/${id(projectId)}/style-guide/revisions`, { method: "POST", body: { instruction } })).job),
579
- /** A page of a project's assets, of one kind when one is named. */
580
- assets: (projectId, kind = null, before = null) => {
579
+ /** A page of a project's assets, of one kind and carrying one tag when they are named. */
580
+ assets: (projectId, kind = null, before = null, tag2 = null) => {
581
581
  const query = new URLSearchParams();
582
582
  if (kind !== null) query.set("kind", kind);
583
+ if (tag2 !== null) query.set("tag", tag2);
583
584
  if (before !== null) query.set("before", before);
584
585
  const search = query.toString();
585
586
  return json2(`/projects/${id(projectId)}/assets${search === "" ? "" : `?${search}`}`);
@@ -592,6 +593,10 @@ function createWebApi(fetcher = (input, init2) => fetch(input, init2), base = AP
592
593
  },
593
594
  /** One of a project's assets, by its id or its name. */
594
595
  asset: (projectId, ref) => json2(`/projects/${id(projectId)}/assets/${id(ref)}`),
596
+ /** The project's tags for concept art: the offered ones first, then its own (specs/concept-art/ R3.1). */
597
+ tags: async (projectId) => (await json2(`/projects/${id(projectId)}/tags`)).tags,
598
+ /** A concept art's tags replaced by these, a tag the project has not held made (R3.2). */
599
+ tagAsset: (projectId, ref, tags) => json2(`/projects/${id(projectId)}/assets/${id(ref)}/tags`, { method: "PUT", body: { tags } }),
595
600
  /** Keep a PNG for the account, answering its id. */
596
601
  uploadImage: async (png) => (await json2("/images", { method: "POST", bytes: { data: png, type: "image/png" } })).id,
597
602
  /** Where the page shows a kept image from; the cookie carries the account. */
@@ -1066,1019 +1071,1117 @@ function describedInterface(manifest2, kept, earlier) {
1066
1071
  next[name] = { meta: { kind: "ui", ...piece.nineGrid === void 0 ? {} : { nineGrid: piece.nineGrid, scale: "stretch" } } };
1067
1072
  return next;
1068
1073
  }
1069
- const SECTOR_SIDE$1 = 128;
1070
- const SECTOR_CELLS = SECTOR_SIDE$1 * SECTOR_SIDE$1;
1071
- const SECTOR_BYTES = 512 * 1024;
1072
- const MAP_LEVELS = { first: 0, last: 14 };
1073
- const LARGEST_MAP_SIDE = 1e4;
1074
- const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE$1);
1075
- const SECTOR_FORMAT = 1;
1076
- const MAGIC = [80, 86, 67];
1077
- const PRESENT = 1;
1078
- const AUDIO = 2;
1079
- const INTERACT = 4;
1080
- const EXTRA = 8;
1081
- const U16 = 65535;
1082
- const MAX_LAYERS = 255;
1083
- const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
1084
- class SectorError extends Error {
1074
+ const TILE_SIZE = 256;
1075
+ const EAI_MAGIC = 1229018181;
1076
+ const EAI_VERSION = 1;
1077
+ const HEADER_SIZE$1 = 32;
1078
+ const ASSET_SIZE = 24;
1079
+ const ATLAS_SIZE = 12;
1080
+ class AssetIndexError extends Error {
1081
+ // Assigned in the body: the suites run under Node's strip-only TypeScript
1082
+ // mode, which rejects parameter properties (docs/notes.md n-0023).
1085
1083
  constructor(message) {
1086
1084
  super(message);
1087
- this.name = "SectorError";
1085
+ this.name = "AssetIndexError";
1088
1086
  }
1089
1087
  }
1090
- function u16(value, what) {
1091
- 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`);
1092
- return value;
1088
+ function parseAssetIndex(buffer) {
1089
+ if (buffer.byteLength < HEADER_SIZE$1) {
1090
+ throw new AssetIndexError(`buffer too small: ${buffer.byteLength} < ${HEADER_SIZE$1}`);
1091
+ }
1092
+ const view = new DataView(buffer);
1093
+ const magic = view.getUint32(0, true);
1094
+ if (magic !== EAI_MAGIC) {
1095
+ throw new AssetIndexError(`bad magic 0x${magic.toString(16)} (expected 0x${EAI_MAGIC.toString(16)})`);
1096
+ }
1097
+ const version2 = view.getUint32(4, true);
1098
+ if (version2 !== EAI_VERSION) {
1099
+ throw new AssetIndexError(`unsupported version ${version2}, reader speaks v${EAI_VERSION}`);
1100
+ }
1101
+ const assetCount = view.getUint32(8, true);
1102
+ const atlasCount = view.getUint32(12, true);
1103
+ const assetsOffset = view.getUint32(16, true);
1104
+ const atlasesOffset = view.getUint32(20, true);
1105
+ const stringsOffset = view.getUint32(24, true);
1106
+ const stringsLength = view.getUint32(28, true);
1107
+ if (buffer.byteLength !== stringsOffset + stringsLength) {
1108
+ throw new AssetIndexError(`file size ${buffer.byteLength} ≠ header-declared ${stringsOffset + stringsLength}`);
1109
+ }
1110
+ if (assetsOffset < HEADER_SIZE$1) {
1111
+ throw new AssetIndexError(`assetsOffset ${assetsOffset} is inside the ${HEADER_SIZE$1}-byte header`);
1112
+ }
1113
+ if (assetsOffset + assetCount * ASSET_SIZE > atlasesOffset) {
1114
+ throw new AssetIndexError("asset section overlaps atlas section");
1115
+ }
1116
+ if (atlasesOffset + atlasCount * ATLAS_SIZE > stringsOffset) {
1117
+ throw new AssetIndexError("atlas section overlaps string pool");
1118
+ }
1119
+ const pool = new Pool(buffer, stringsOffset, stringsLength);
1120
+ const atlases = readAtlases(view, pool, atlasCount, atlasesOffset);
1121
+ const assets = readAssets(view, pool, assetCount, assetsOffset, atlases);
1122
+ return { version: version2, assets, atlases };
1093
1123
  }
1094
- function encodeSector(cells) {
1095
- if (cells.length !== SECTOR_CELLS) throw new SectorError(`a sector is ${SECTOR_CELLS} cells, not ${cells.length}`);
1096
- const encoder2 = new TextEncoder();
1097
- const bytes = [...MAGIC, SECTOR_FORMAT];
1098
- const push16 = (value) => bytes.push(value & 255, value >> 8);
1099
- for (const cell of cells) {
1100
- if (cell === void 0) {
1101
- bytes.push(0);
1102
- continue;
1124
+ class Pool {
1125
+ buffer;
1126
+ offset;
1127
+ length;
1128
+ decoder = new TextDecoder("utf-8", { fatal: true });
1129
+ constructor(buffer, offset, length) {
1130
+ this.buffer = buffer;
1131
+ this.offset = offset;
1132
+ this.length = length;
1133
+ }
1134
+ read(what, nameOffset, nameLength) {
1135
+ if (nameOffset < this.offset || nameOffset + nameLength > this.offset + this.length) {
1136
+ throw new AssetIndexError(`${what}: name range [${nameOffset}, +${nameLength}] outside the string pool`);
1103
1137
  }
1104
- const extra = cell.extra === void 0 ? null : encoder2.encode(JSON.stringify(cell.extra));
1105
- if (extra !== null && extra.length > U16) throw new SectorError("a tile's extra fields are past 65,535 bytes");
1106
- if (cell.layers.length > MAX_LAYERS) throw new SectorError(`a tile has ${cell.layers.length} layers, and a sector holds at most ${MAX_LAYERS}`);
1107
- bytes.push(PRESENT | (cell.audioZone === void 0 ? 0 : AUDIO) | (cell.interact === void 0 ? 0 : INTERACT) | (extra === null ? 0 : EXTRA));
1108
- bytes.push(cell.layers.length);
1109
- for (const layer of cell.layers) push16(u16(layer, "a layer id"));
1110
- push16(u16(cell.blocking, "blocking"));
1111
- push16(u16(cell.zone, "a zone"));
1112
- if (cell.audioZone !== void 0) push16(u16(cell.audioZone, "an audio zone"));
1113
- if (cell.interact !== void 0) push16(u16(cell.interact, "interact"));
1114
- if (extra !== null) {
1115
- push16(extra.length);
1116
- for (const byte of extra) bytes.push(byte);
1138
+ try {
1139
+ return this.decoder.decode(new Uint8Array(this.buffer, nameOffset, nameLength));
1140
+ } catch (error) {
1141
+ throw new AssetIndexError(`${what}: bad UTF-8 name (${error.message})`);
1117
1142
  }
1118
1143
  }
1119
- return Uint8Array.from(bytes);
1120
1144
  }
1121
- function decodeSector(bytes) {
1122
- if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
1123
- if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
1124
- const decoder = new TextDecoder("utf-8", { fatal: true });
1125
- let at = 4;
1126
- const need = (count) => {
1127
- if (at + count > bytes.length) throw new SectorError("the sector's bytes end before its last cell");
1128
- };
1129
- const read8 = () => {
1130
- need(1);
1131
- return bytes[at++];
1132
- };
1133
- const read16 = () => {
1134
- need(2);
1135
- const value = bytes[at] | bytes[at + 1] << 8;
1136
- at += 2;
1137
- return value;
1138
- };
1139
- const cells = [];
1140
- for (let index = 0; index < SECTOR_CELLS; index += 1) {
1141
- const flags = read8();
1142
- if ((flags & PRESENT) === 0) {
1143
- cells.push(void 0);
1144
- continue;
1145
+ function readAtlases(view, pool, atlasCount, atlasesOffset) {
1146
+ const atlases = new Array(atlasCount);
1147
+ for (let index = 0; index < atlasCount; index += 1) {
1148
+ const base = atlasesOffset + index * ATLAS_SIZE;
1149
+ atlases[index] = {
1150
+ index,
1151
+ width: view.getUint16(base, true),
1152
+ height: view.getUint16(base + 2, true),
1153
+ name: pool.read(`atlas ${index}`, view.getUint32(base + 4, true), view.getUint32(base + 8, true))
1154
+ };
1155
+ }
1156
+ return atlases;
1157
+ }
1158
+ function readAssets(view, pool, assetCount, assetsOffset, atlases) {
1159
+ const assets = /* @__PURE__ */ new Map();
1160
+ for (let index = 0; index < assetCount; index += 1) {
1161
+ const base = assetsOffset + index * ASSET_SIZE;
1162
+ const name = pool.read(`asset ${index}`, view.getUint32(base, true), view.getUint16(base + 4, true));
1163
+ if (assets.has(name)) {
1164
+ throw new AssetIndexError(`duplicate asset name ${name}`);
1145
1165
  }
1146
- const count = read8();
1147
- const layers = [];
1148
- for (let layer = 0; layer < count; layer += 1) layers.push(read16());
1149
- const blocking = read16();
1150
- const zone = read16();
1151
- const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
1152
- const interact = (flags & INTERACT) === 0 ? void 0 : read16();
1153
- let extra;
1154
- if ((flags & EXTRA) !== 0) {
1155
- const length = read16();
1156
- need(length);
1157
- let parsed2;
1158
- try {
1159
- parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
1160
- } catch {
1161
- throw new SectorError("a tile's extra fields are not JSON");
1162
- }
1163
- if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
1164
- const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
1165
- if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
1166
- extra = parsed2;
1167
- at += length;
1166
+ const atlasIndex = view.getUint16(base + 6, true);
1167
+ const atlas = atlases[atlasIndex];
1168
+ if (atlas === void 0) {
1169
+ throw new AssetIndexError(`asset ${name}: atlasIndex ${atlasIndex} ≥ atlasCount ${atlases.length}`);
1168
1170
  }
1169
- cells.push({
1170
- layers,
1171
- blocking,
1172
- zone,
1173
- ...audioZone === void 0 ? {} : { audioZone },
1174
- ...interact === void 0 ? {} : { interact },
1175
- ...extra === void 0 ? {} : { extra }
1176
- });
1171
+ const entry = {
1172
+ name,
1173
+ atlasIndex,
1174
+ x: view.getUint16(base + 8, true),
1175
+ y: view.getUint16(base + 10, true),
1176
+ width: view.getUint16(base + 12, true),
1177
+ height: view.getUint16(base + 14, true),
1178
+ offsetX: view.getInt16(base + 16, true),
1179
+ offsetY: view.getInt16(base + 18, true),
1180
+ flags: view.getUint16(base + 20, true)
1181
+ };
1182
+ if (entry.x + entry.width > atlas.width || entry.y + entry.height > atlas.height) {
1183
+ throw new AssetIndexError(
1184
+ `asset ${name}: rectangle ${entry.x},${entry.y} ${entry.width}x${entry.height} leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`
1185
+ );
1186
+ }
1187
+ assets.set(name, entry);
1177
1188
  }
1178
- if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
1179
- return cells;
1189
+ return assets;
1180
1190
  }
1181
- const NAMED = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
1182
- function cellOfTile(tile) {
1183
- const extra = Object.fromEntries(Object.entries(tile).filter(([key]) => !NAMED.has(key)));
1184
- return {
1185
- layers: tile.layers,
1186
- blocking: tile.blocking,
1187
- zone: tile.zone,
1188
- ...tile.audioZone === void 0 ? {} : { audioZone: tile.audioZone },
1189
- ...tile.interact === void 0 ? {} : { interact: tile.interact },
1190
- ...Object.keys(extra).length === 0 ? {} : { extra }
1191
+ const U16_MAX = 65535;
1192
+ const I16_MIN = -32768;
1193
+ const I16_MAX = 32767;
1194
+ function writeAssetIndex(atlases, assets) {
1195
+ const encoder2 = new TextEncoder();
1196
+ const ordered = [...assets].sort((left, right) => compareBytes(encoder2, left.name, right.name));
1197
+ const seen = /* @__PURE__ */ new Set();
1198
+ for (const asset of ordered) {
1199
+ if (seen.has(asset.name)) throw new AssetIndexError(`duplicate asset name ${asset.name}`);
1200
+ seen.add(asset.name);
1201
+ if (asset.atlasIndex >= atlases.length) {
1202
+ throw new AssetIndexError(`asset ${asset.name}: atlasIndex ${asset.atlasIndex} ≥ atlasCount ${atlases.length}`);
1203
+ }
1204
+ checkU16(asset.name, { x: asset.x, y: asset.y, width: asset.width, height: asset.height, flags: asset.flags });
1205
+ checkI16(asset.name, { offsetX: asset.offsetX, offsetY: asset.offsetY });
1206
+ const atlas = atlases[asset.atlasIndex];
1207
+ if (asset.x + asset.width > atlas.width || asset.y + asset.height > atlas.height) {
1208
+ throw new AssetIndexError(`asset ${asset.name}: rectangle leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`);
1209
+ }
1210
+ }
1211
+ for (const atlas of atlases) {
1212
+ checkU16(atlas.name, { width: atlas.width, height: atlas.height });
1213
+ }
1214
+ const pool = [];
1215
+ const ranges = /* @__PURE__ */ new Map();
1216
+ const intern = (text2) => {
1217
+ const found = ranges.get(text2);
1218
+ if (found !== void 0) return found;
1219
+ const bytes = encoder2.encode(text2);
1220
+ const range = { offset: pool.length, length: bytes.length };
1221
+ if (range.length > U16_MAX) throw new AssetIndexError(`name too long: ${text2}`);
1222
+ pool.push(...bytes);
1223
+ ranges.set(text2, range);
1224
+ return range;
1191
1225
  };
1226
+ for (const asset of ordered) intern(asset.name);
1227
+ for (const atlas of atlases) intern(atlas.name);
1228
+ const assetsOffset = HEADER_SIZE$1;
1229
+ const atlasesOffset = assetsOffset + ordered.length * ASSET_SIZE;
1230
+ const stringsOffset = atlasesOffset + atlases.length * ATLAS_SIZE;
1231
+ const buffer = new ArrayBuffer(stringsOffset + pool.length);
1232
+ const view = new DataView(buffer);
1233
+ view.setUint32(0, EAI_MAGIC, true);
1234
+ view.setUint32(4, EAI_VERSION, true);
1235
+ view.setUint32(8, ordered.length, true);
1236
+ view.setUint32(12, atlases.length, true);
1237
+ view.setUint32(16, assetsOffset, true);
1238
+ view.setUint32(20, atlasesOffset, true);
1239
+ view.setUint32(24, stringsOffset, true);
1240
+ view.setUint32(28, pool.length, true);
1241
+ ordered.forEach((asset, index) => {
1242
+ const base = assetsOffset + index * ASSET_SIZE;
1243
+ const range = ranges.get(asset.name);
1244
+ view.setUint32(base, stringsOffset + range.offset, true);
1245
+ view.setUint16(base + 4, range.length, true);
1246
+ view.setUint16(base + 6, asset.atlasIndex, true);
1247
+ view.setUint16(base + 8, asset.x, true);
1248
+ view.setUint16(base + 10, asset.y, true);
1249
+ view.setUint16(base + 12, asset.width, true);
1250
+ view.setUint16(base + 14, asset.height, true);
1251
+ view.setInt16(base + 16, asset.offsetX, true);
1252
+ view.setInt16(base + 18, asset.offsetY, true);
1253
+ view.setUint16(base + 20, asset.flags, true);
1254
+ });
1255
+ atlases.forEach((atlas, index) => {
1256
+ const base = atlasesOffset + index * ATLAS_SIZE;
1257
+ const range = ranges.get(atlas.name);
1258
+ view.setUint16(base, atlas.width, true);
1259
+ view.setUint16(base + 2, atlas.height, true);
1260
+ view.setUint32(base + 4, stringsOffset + range.offset, true);
1261
+ view.setUint32(base + 8, range.length, true);
1262
+ });
1263
+ new Uint8Array(buffer).set(new Uint8Array(pool), stringsOffset);
1264
+ return buffer;
1192
1265
  }
1193
- function tileOfCell(cell, x, y, z) {
1194
- return {
1195
- ...cell.extra ?? {},
1196
- x,
1197
- y,
1198
- z,
1199
- layers: [...cell.layers],
1200
- blocking: cell.blocking,
1201
- zone: cell.zone,
1202
- ...cell.audioZone === void 0 ? {} : { audioZone: cell.audioZone },
1203
- ...cell.interact === void 0 ? {} : { interact: cell.interact }
1204
- };
1266
+ function compareBytes(encoder2, left, right) {
1267
+ const a = encoder2.encode(left);
1268
+ const b = encoder2.encode(right);
1269
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
1270
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
1271
+ if (difference !== 0) return difference;
1272
+ }
1273
+ return a.length - b.length;
1274
+ }
1275
+ function checkU16(what, fields) {
1276
+ for (const [field, value] of Object.entries(fields)) {
1277
+ if (!Number.isInteger(value) || value < 0 || value > U16_MAX) {
1278
+ throw new AssetIndexError(`${what}: ${field} ${value} does not fit in a u16`);
1279
+ }
1280
+ }
1281
+ }
1282
+ function checkI16(what, fields) {
1283
+ for (const [field, value] of Object.entries(fields)) {
1284
+ if (!Number.isInteger(value) || value < I16_MIN || value > I16_MAX) {
1285
+ throw new AssetIndexError(`${what}: ${field} ${value} does not fit in an i16`);
1286
+ }
1287
+ }
1288
+ }
1289
+ const METADATA_ENTRY = "meta.json";
1290
+ const METADATA_VERSION = 1;
1291
+ const ANIMATION_KEYS = [
1292
+ "idle",
1293
+ "stand-north",
1294
+ "stand-south",
1295
+ "stand-west",
1296
+ "stand-east",
1297
+ "walk-north",
1298
+ "walk-south",
1299
+ "walk-west",
1300
+ "walk-east",
1301
+ "attack-north",
1302
+ "attack-south",
1303
+ "attack-west",
1304
+ "attack-east",
1305
+ "icon"
1306
+ ];
1307
+ const ANIMATION_KEY_SET = new Set(ANIMATION_KEYS);
1308
+ function isAnimationKey(key) {
1309
+ return ANIMATION_KEY_SET.has(key);
1310
+ }
1311
+ const ASSET_KINDS = ["outfit", "tile", "item", "effect", "ui"];
1312
+ const UI_SCALES = ["stretch", "repeat", "round"];
1313
+ const PLAY_EVERY_MIN_MS = 250;
1314
+ const PLAY_EVERY_MAX_MS = 6e5;
1315
+ const MAX_ANIMATION_NAME = 48;
1316
+ const ANIMATION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1317
+ function isAnimationName(name) {
1318
+ return name.length <= MAX_ANIMATION_NAME && ANIMATION_NAME.test(name);
1205
1319
  }
1206
- const MIRROR_MAP_DIRECTORY = "map";
1207
- const MIRROR_INDEX_FILE = `${MIRROR_MAP_DIRECTORY}/index.json`;
1208
- const MIRROR_FORMAT = 1;
1209
- class MirrorError extends Error {
1210
- // Assigned in the body: Node's strip-only TypeScript mode rejects parameter
1211
- // properties (docs/notes.md n-0023).
1320
+ class AssetMetadataError extends Error {
1321
+ // Assigned in the body: the suites run under Node's strip-only TypeScript
1322
+ // mode, which rejects parameter properties (docs/notes.md n-0023).
1212
1323
  constructor(message) {
1213
1324
  super(message);
1214
- this.name = "MirrorError";
1325
+ this.name = "AssetMetadataError";
1215
1326
  }
1216
1327
  }
1217
- function mirrorSectorPath(z, cx, cy) {
1218
- if (!isPlace(z, cx, cy)) throw new MirrorError(`no sector of a map is at level ${z}, column ${cx}, row ${cy}`);
1219
- return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
1220
- }
1221
- function isPlace(z, cx, cy) {
1222
- const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
1223
- return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
1224
- }
1225
- function isSize(value) {
1226
- const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
1227
- return typeof value === "object" && value !== null && side(value.width) && side(value.height);
1228
- }
1229
- function mirrorIndexText(index) {
1230
- return `${JSON.stringify(index, null, 2)}
1231
- `;
1328
+ function isRecord$2(value) {
1329
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1232
1330
  }
1233
- function parseMirrorIndex(text2) {
1234
- let value;
1235
- try {
1236
- value = JSON.parse(text2);
1237
- } catch {
1238
- throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
1331
+ const MAX_ANIMATION_FRAMES = 64;
1332
+ function readFrames(value, asset, animation) {
1333
+ if (!Array.isArray(value)) {
1334
+ throw new AssetMetadataError(`${asset}: animation ${animation} is not a list of frames`);
1239
1335
  }
1240
- const index = value;
1241
- if (typeof index !== "object" || index === null) throw new MirrorError(`${MIRROR_INDEX_FILE} is not an object`);
1242
- if (index.format !== MIRROR_FORMAT)
1243
- throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
1244
- if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
1245
- if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
1246
- if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
1247
- for (const named of index.names) {
1248
- if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
1336
+ if (value.length > MAX_ANIMATION_FRAMES) {
1337
+ throw new AssetMetadataError(`${asset}: animation ${animation} has ${value.length} frames, and an animation holds at most ${MAX_ANIMATION_FRAMES}`);
1249
1338
  }
1250
- const seen = /* @__PURE__ */ new Set();
1251
- for (const sector of index.sectors) {
1252
- const [z, cx, cy, version2] = Array.isArray(sector) ? sector : [];
1253
- if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
1254
- throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
1255
- }
1256
- const key = `${z}:${cx}:${cy}`;
1257
- if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
1258
- throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
1339
+ return value.map((raw, index) => {
1340
+ if (!isRecord$2(raw)) {
1341
+ throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} is not a frame`);
1259
1342
  }
1260
- if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
1261
- seen.add(key);
1262
- }
1263
- return index;
1264
- }
1265
- async function readMirroredMap(index, read, inflate2) {
1266
- const tiles = [];
1267
- for (const [z, cx, cy] of index.sectors) {
1268
- const path = mirrorSectorPath(z, cx, cy);
1269
- let cells;
1270
- try {
1271
- cells = decodeSector(await inflate2(await read(path)));
1272
- } catch (error) {
1273
- throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
1343
+ const name = raw.asset;
1344
+ if (typeof name !== "string" || name === "") {
1345
+ throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} names no asset`);
1274
1346
  }
1275
- for (const [at, cell] of cells.entries()) {
1276
- if (cell === void 0) continue;
1277
- const x = cx * SECTOR_SIDE$1 + at % SECTOR_SIDE$1;
1278
- const y = cy * SECTOR_SIDE$1 + Math.floor(at / SECTOR_SIDE$1);
1279
- if (x >= index.size.width || y >= index.size.height) continue;
1280
- tiles.push(tileOfCell(cell, x, y, z));
1347
+ const ms = raw.ms;
1348
+ if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
1349
+ throw new AssetMetadataError(
1350
+ `${asset}: animation ${animation} frame ${index} has duration ${String(ms)}, which is not a whole number of milliseconds at or above zero`
1351
+ );
1281
1352
  }
1282
- }
1283
- tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
1284
- return { width: index.size.width, height: index.size.height, tiles };
1353
+ return { asset: name, ms, flip: raw.flip === true };
1354
+ });
1285
1355
  }
1286
- function sectorsOfMap(map) {
1287
- const sectors = /* @__PURE__ */ new Map();
1288
- for (const tile of map.tiles) {
1289
- const { x, y, z } = tile;
1290
- if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= LARGEST_MAP_SIDE || y >= LARGEST_MAP_SIDE || !isPlace(z, 0, 0)) {
1291
- throw new MirrorError(`the tile at (${x}, ${y}) on level ${z} is at no place a map has`);
1356
+ function readAnimations(value, asset, kind) {
1357
+ const animations = /* @__PURE__ */ new Map();
1358
+ if (value === void 0) return animations;
1359
+ if (!isRecord$2(value)) {
1360
+ throw new AssetMetadataError(`${asset}: animations is not a map of name to frames`);
1361
+ }
1362
+ for (const [name, frames] of Object.entries(value)) {
1363
+ if (kind === "outfit" && !isAnimationKey(name)) {
1364
+ throw new AssetMetadataError(`${asset}: unknown animation ${name} (expected one of ${ANIMATION_KEYS.join(", ")})`);
1292
1365
  }
1293
- const cx = Math.floor(x / SECTOR_SIDE$1);
1294
- const cy = Math.floor(y / SECTOR_SIDE$1);
1295
- const key = `${z}:${cx}:${cy}`;
1296
- let sector = sectors.get(key);
1297
- if (sector === void 0) {
1298
- sector = { z, cx, cy, cells: new Array(SECTOR_CELLS) };
1299
- sectors.set(key, sector);
1366
+ if (kind !== "outfit" && !isAnimationName(name)) {
1367
+ throw new AssetMetadataError(`${asset}: animation ${name} is not a kebab-case name of at most ${MAX_ANIMATION_NAME} characters`);
1300
1368
  }
1301
- sector.cells[y % SECTOR_SIDE$1 * SECTOR_SIDE$1 + x % SECTOR_SIDE$1] = cellOfTile(tile);
1302
- }
1303
- return [...sectors.values()].sort((a, b) => a.z - b.z || a.cy - b.cy || a.cx - b.cx);
1304
- }
1305
- class MapTextError extends Error {
1306
- // Assigned in the body: the suites run under Node's strip-only TypeScript
1307
- // mode, which rejects parameter properties (docs/notes.md n-0023).
1308
- constructor(message) {
1309
- super(message);
1310
- this.name = "MapTextError";
1369
+ animations.set(name, readFrames(frames, asset, name));
1311
1370
  }
1371
+ return animations;
1312
1372
  }
1313
- function tileKey(x, y, z) {
1314
- return `${x}:${y}:${z}`;
1315
- }
1316
- function scanTileSources(text2) {
1317
- const key = '"tiles"';
1318
- let at = text2.indexOf(key);
1319
- if (at === -1) return [];
1320
- at = text2.indexOf("[", at + key.length);
1321
- if (at === -1) return [];
1322
- const sources = [];
1323
- let depth = 0;
1324
- let inString = false;
1325
- let start = -1;
1326
- for (let index = at + 1; index < text2.length; index += 1) {
1327
- const char = text2[index];
1328
- if (inString) {
1329
- if (char === "\\") index += 1;
1330
- else if (char === '"') inString = false;
1373
+ function readPlays(value, asset, animations, kind) {
1374
+ const plays = /* @__PURE__ */ new Map();
1375
+ if (value === void 0) return plays;
1376
+ if (!isRecord$2(value)) throw new AssetMetadataError(`${asset}: plays is not a map of animation to how it plays`);
1377
+ for (const [name, play] of Object.entries(value)) {
1378
+ if (!animations.has(name)) throw new AssetMetadataError(`${asset}: plays names ${name}, and there is no such animation`);
1379
+ if (!isRecord$2(play)) throw new AssetMetadataError(`${asset}: how ${name} plays is not an object`);
1380
+ if (kind === "outfit" && play.mode !== "loop")
1381
+ throw new AssetMetadataError(`${asset}: ${name} is an outfit's animation, which plays as the game drives it, in a loop`);
1382
+ if (play.mode === "loop" || play.mode === "change") {
1383
+ plays.set(name, { mode: play.mode });
1331
1384
  continue;
1332
1385
  }
1333
- if (char === '"') {
1334
- inString = true;
1335
- } else if (char === "{") {
1336
- if (depth === 0) start = index;
1337
- depth += 1;
1338
- } else if (char === "}") {
1339
- depth -= 1;
1340
- if (depth === 0 && start !== -1) {
1341
- sources.push(text2.slice(start, index + 1));
1342
- start = -1;
1386
+ if (play.mode === "every") {
1387
+ const ms = play.ms;
1388
+ if (typeof ms !== "number" || !Number.isInteger(ms) || ms < PLAY_EVERY_MIN_MS || ms > PLAY_EVERY_MAX_MS) {
1389
+ throw new AssetMetadataError(
1390
+ `${asset}: ${name} plays every ${String(ms)} ms, and an interval is a whole ${PLAY_EVERY_MIN_MS} to ${PLAY_EVERY_MAX_MS} ms`
1391
+ );
1343
1392
  }
1344
- } else if (char === "]" && depth === 0) {
1345
- break;
1393
+ plays.set(name, { mode: "every", ms });
1394
+ continue;
1346
1395
  }
1396
+ throw new AssetMetadataError(`${asset}: ${name} plays as ${String(play.mode)}, and an animation plays as loop, every or change`);
1347
1397
  }
1348
- return sources;
1349
- }
1350
- function isTile(value) {
1351
- if (typeof value !== "object" || value === null) return false;
1352
- const record = value;
1353
- return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
1398
+ return plays;
1354
1399
  }
1355
- function parseMapText(text2, path = "the map") {
1356
- let parsed2;
1357
- try {
1358
- parsed2 = JSON.parse(text2);
1359
- } catch (error) {
1360
- throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
1361
- }
1362
- if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
1363
- throw new MapTextError(`${path} holds no tiles array`);
1400
+ function readNineGrid(value, asset) {
1401
+ if (value === void 0) return void 0;
1402
+ if (!isRecord$2(value)) {
1403
+ throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
1364
1404
  }
1365
- const document = parsed2;
1366
- const sources = scanTileSources(text2);
1367
- if (sources.length !== document.tiles.length) {
1368
- throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
1405
+ const insets = {};
1406
+ for (const edge of ["top", "right", "bottom", "left"]) {
1407
+ const inset = value[edge];
1408
+ if (inset === void 0) {
1409
+ throw new AssetMetadataError(`${asset}: nineGrid declares no ${edge} inset`);
1410
+ }
1411
+ if (typeof inset !== "number" || !Number.isInteger(inset) || inset < 0) {
1412
+ throw new AssetMetadataError(`${asset}: nineGrid ${edge} is ${String(inset)}, which is not a whole number of pixels at or above zero`);
1413
+ }
1414
+ insets[edge] = inset;
1369
1415
  }
1370
- const byKey = /* @__PURE__ */ new Map();
1371
- document.tiles.forEach((tile, index) => {
1372
- if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
1373
- byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
1374
- });
1375
- return { document, sources: byKey };
1416
+ return { top: insets.top, right: insets.right, bottom: insets.bottom, left: insets.left };
1376
1417
  }
1377
- const TILE_KEYS = ["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"];
1378
- function serializeTile(tile) {
1379
- const ordered = {};
1380
- for (const key of TILE_KEYS) {
1381
- if (tile[key] !== void 0) ordered[key] = tile[key];
1382
- }
1383
- for (const key of Object.keys(tile)) {
1384
- if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
1418
+ function readScale(value, asset) {
1419
+ if (value === void 0) return void 0;
1420
+ if (typeof value !== "string" || !UI_SCALES.includes(value)) {
1421
+ throw new AssetMetadataError(`${asset}: scale ${String(value)} is not one of ${UI_SCALES.join(", ")}`);
1385
1422
  }
1386
- return JSON.stringify(ordered);
1423
+ return value;
1387
1424
  }
1388
- function formatMap(next, previous) {
1389
- const lines = [];
1390
- let changed = 0;
1391
- for (const tile of next.tiles) {
1392
- {
1393
- lines.push(serializeTile(tile));
1394
- changed += 1;
1425
+ function readCollide(value, asset) {
1426
+ if (value === void 0) return void 0;
1427
+ if (!isRecord$2(value)) {
1428
+ throw new AssetMetadataError(`${asset}: collide is not a box of x, y, w and h in cells`);
1429
+ }
1430
+ const box = {};
1431
+ for (const side of ["x", "y", "w", "h"]) {
1432
+ const cells = value[side];
1433
+ if (typeof cells !== "number" || !Number.isInteger(cells)) {
1434
+ throw new AssetMetadataError(`${asset}: collide ${side} is ${String(cells)}, which is not a whole number of cells`);
1395
1435
  }
1436
+ box[side] = cells;
1396
1437
  }
1397
- const rest = {};
1398
- for (const key of Object.keys(next)) {
1399
- if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
1438
+ if (box.w < 1 || box.h < 1) {
1439
+ throw new AssetMetadataError(`${asset}: collide is ${box.w}x${box.h} cells, and a box is at least one by one`);
1400
1440
  }
1401
- const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
1402
- const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
1403
- const body = lines.length === 0 ? "" : `
1404
- ${lines.join(",\n")}
1405
- `;
1406
- return { text: `${head}${body}]${trailer}}
1407
- `, changed };
1408
- }
1409
- const SIGNATURE$1 = [137, 80, 78, 71, 13, 10, 26, 10];
1410
- function isPng(bytes) {
1411
- return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
1412
- }
1413
- function pngSize(bytes) {
1414
- if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
1415
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1416
- return { width: view.getUint32(16), height: view.getUint32(20) };
1417
- }
1418
- const GROUND_KINDS = /* @__PURE__ */ new Set(["ground", "floor", "wall", "cliff", "interior", "grass-stone"]);
1419
- function isGroundName(name) {
1420
- const { inner } = splitTileset(name);
1421
- const kind = inner.split("/")[1] ?? "";
1422
- return GROUND_KINDS.has(kind) || inner.includes("/wang-");
1441
+ return { x: box.x, y: box.y, w: box.w, h: box.h };
1423
1442
  }
1424
- const MAX_ASSEMBLED_PX = 4096;
1425
- const TILE_SIZE = 256;
1426
- const EAI_MAGIC = 1229018181;
1427
- const EAI_VERSION = 1;
1428
- const HEADER_SIZE$1 = 32;
1429
- const ASSET_SIZE = 24;
1430
- const ATLAS_SIZE = 12;
1431
- class AssetIndexError extends Error {
1432
- // Assigned in the body: the suites run under Node's strip-only TypeScript
1433
- // mode, which rejects parameter properties (docs/notes.md n-0023).
1434
- constructor(message) {
1435
- super(message);
1436
- this.name = "AssetIndexError";
1443
+ const MAX_DRAW_OFFSET = TILE_SIZE * 8;
1444
+ const MIN_DRAW_SCALE = 0.05;
1445
+ const MAX_DRAW_SCALE = 8;
1446
+ function readOffset(value, asset) {
1447
+ if (value === void 0) return void 0;
1448
+ if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
1449
+ throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
1450
+ }
1451
+ for (const axis of ["x", "y"]) {
1452
+ const px = value[axis];
1453
+ if (Math.abs(px) > MAX_DRAW_OFFSET) {
1454
+ throw new AssetMetadataError(`${asset}: offset ${axis} is ${px} px, and an offset is within ${MAX_DRAW_OFFSET} px of the tile`);
1455
+ }
1437
1456
  }
1457
+ return { x: value.x, y: value.y };
1438
1458
  }
1439
- function parseAssetIndex(buffer) {
1440
- if (buffer.byteLength < HEADER_SIZE$1) {
1441
- throw new AssetIndexError(`buffer too small: ${buffer.byteLength} < ${HEADER_SIZE$1}`);
1459
+ function readDrawScale(value, asset) {
1460
+ if (value === void 0) return void 0;
1461
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1462
+ throw new AssetMetadataError(`${asset}: drawScale is ${String(value)}, and a scale is a number above zero`);
1442
1463
  }
1443
- const view = new DataView(buffer);
1444
- const magic = view.getUint32(0, true);
1445
- if (magic !== EAI_MAGIC) {
1446
- throw new AssetIndexError(`bad magic 0x${magic.toString(16)} (expected 0x${EAI_MAGIC.toString(16)})`);
1464
+ if (value < MIN_DRAW_SCALE || value > MAX_DRAW_SCALE) {
1465
+ throw new AssetMetadataError(`${asset}: drawScale is ${value}, and a scale is between ${MIN_DRAW_SCALE} and ${MAX_DRAW_SCALE}`);
1447
1466
  }
1448
- const version2 = view.getUint32(4, true);
1449
- if (version2 !== EAI_VERSION) {
1450
- throw new AssetIndexError(`unsupported version ${version2}, reader speaks v${EAI_VERSION}`);
1467
+ return value;
1468
+ }
1469
+ function readEntry(value, asset) {
1470
+ if (!isRecord$2(value)) {
1471
+ throw new AssetMetadataError(`${asset}: not an entry`);
1451
1472
  }
1452
- const assetCount = view.getUint32(8, true);
1453
- const atlasCount = view.getUint32(12, true);
1454
- const assetsOffset = view.getUint32(16, true);
1455
- const atlasesOffset = view.getUint32(20, true);
1456
- const stringsOffset = view.getUint32(24, true);
1457
- const stringsLength = view.getUint32(28, true);
1458
- if (buffer.byteLength !== stringsOffset + stringsLength) {
1459
- throw new AssetIndexError(`file size ${buffer.byteLength} ≠ header-declared ${stringsOffset + stringsLength}`);
1473
+ const kind = value.kind;
1474
+ if (typeof kind !== "string") {
1475
+ throw new AssetMetadataError(`${asset}: declares no kind`);
1460
1476
  }
1461
- if (assetsOffset < HEADER_SIZE$1) {
1462
- throw new AssetIndexError(`assetsOffset ${assetsOffset} is inside the ${HEADER_SIZE$1}-byte header`);
1477
+ if (!ASSET_KINDS.includes(kind)) {
1478
+ throw new AssetMetadataError(`${asset}: unknown kind ${kind} (expected ${ASSET_KINDS.join(", ")})`);
1463
1479
  }
1464
- if (assetsOffset + assetCount * ASSET_SIZE > atlasesOffset) {
1465
- throw new AssetIndexError("asset section overlaps atlas section");
1480
+ const minimap = value.minimap;
1481
+ if (minimap !== void 0 && (typeof minimap !== "number" || !Number.isInteger(minimap))) {
1482
+ throw new AssetMetadataError(`${asset}: minimap category is not a whole number`);
1466
1483
  }
1467
- if (atlasesOffset + atlasCount * ATLAS_SIZE > stringsOffset) {
1468
- throw new AssetIndexError("atlas section overlaps string pool");
1484
+ if (kind !== "ui" && (value.nineGrid !== void 0 || value.scale !== void 0)) {
1485
+ throw new AssetMetadataError(`${asset}: nineGrid and scale are interface fields, and this is a ${kind}`);
1469
1486
  }
1470
- const pool = new Pool(buffer, stringsOffset, stringsLength);
1471
- const atlases = readAtlases(view, pool, atlasCount, atlasesOffset);
1472
- const assets = readAssets(view, pool, assetCount, assetsOffset, atlases);
1473
- return { version: version2, assets, atlases };
1487
+ if (kind !== "tile" && (value.collide !== void 0 || value.offset !== void 0 || value.drawScale !== void 0)) {
1488
+ throw new AssetMetadataError(`${asset}: collide, offset and drawScale are tile fields, and this is a ${kind}`);
1489
+ }
1490
+ const animations = readAnimations(value.animations, asset, kind);
1491
+ return {
1492
+ kind,
1493
+ animations,
1494
+ plays: readPlays(value.plays, asset, animations, kind),
1495
+ blocking: value.blocking === true,
1496
+ foreground: value.foreground === true,
1497
+ roof: value.roof === true,
1498
+ door: value.door === true,
1499
+ water: value.water === true,
1500
+ minimap: minimap ?? 0,
1501
+ collide: readCollide(value.collide, asset),
1502
+ offset: readOffset(value.offset, asset),
1503
+ drawScale: readDrawScale(value.drawScale, asset),
1504
+ nineGrid: readNineGrid(value.nineGrid, asset),
1505
+ scale: readScale(value.scale, asset)
1506
+ };
1474
1507
  }
1475
- class Pool {
1476
- buffer;
1477
- offset;
1478
- length;
1479
- decoder = new TextDecoder("utf-8", { fatal: true });
1480
- constructor(buffer, offset, length) {
1481
- this.buffer = buffer;
1482
- this.offset = offset;
1483
- this.length = length;
1508
+ function parseAssetMetadata(text2) {
1509
+ let parsed2;
1510
+ try {
1511
+ parsed2 = JSON.parse(text2);
1512
+ } catch (error) {
1513
+ throw new AssetMetadataError(`not JSON: ${error.message}`);
1484
1514
  }
1485
- read(what, nameOffset, nameLength) {
1486
- if (nameOffset < this.offset || nameOffset + nameLength > this.offset + this.length) {
1487
- throw new AssetIndexError(`${what}: name range [${nameOffset}, +${nameLength}] outside the string pool`);
1488
- }
1489
- try {
1490
- return this.decoder.decode(new Uint8Array(this.buffer, nameOffset, nameLength));
1491
- } catch (error) {
1492
- throw new AssetIndexError(`${what}: bad UTF-8 name (${error.message})`);
1493
- }
1515
+ if (!isRecord$2(parsed2)) {
1516
+ throw new AssetMetadataError("not an object");
1494
1517
  }
1495
- }
1496
- function readAtlases(view, pool, atlasCount, atlasesOffset) {
1497
- const atlases = new Array(atlasCount);
1498
- for (let index = 0; index < atlasCount; index += 1) {
1499
- const base = atlasesOffset + index * ATLAS_SIZE;
1500
- atlases[index] = {
1501
- index,
1502
- width: view.getUint16(base, true),
1503
- height: view.getUint16(base + 2, true),
1504
- name: pool.read(`atlas ${index}`, view.getUint32(base + 4, true), view.getUint32(base + 8, true))
1505
- };
1518
+ const version2 = parsed2.version;
1519
+ if (version2 !== METADATA_VERSION) {
1520
+ throw new AssetMetadataError(`unsupported version ${String(version2)}, reader speaks v${METADATA_VERSION}`);
1521
+ }
1522
+ const rawAssets = parsed2.assets;
1523
+ if (rawAssets !== void 0 && !isRecord$2(rawAssets)) {
1524
+ throw new AssetMetadataError("assets is not a map of name to entry");
1506
1525
  }
1507
- return atlases;
1508
- }
1509
- function readAssets(view, pool, assetCount, assetsOffset, atlases) {
1510
1526
  const assets = /* @__PURE__ */ new Map();
1511
- for (let index = 0; index < assetCount; index += 1) {
1512
- const base = assetsOffset + index * ASSET_SIZE;
1513
- const name = pool.read(`asset ${index}`, view.getUint32(base, true), view.getUint16(base + 4, true));
1514
- if (assets.has(name)) {
1515
- throw new AssetIndexError(`duplicate asset name ${name}`);
1516
- }
1517
- const atlasIndex = view.getUint16(base + 6, true);
1518
- const atlas = atlases[atlasIndex];
1519
- if (atlas === void 0) {
1520
- throw new AssetIndexError(`asset ${name}: atlasIndex ${atlasIndex} ≥ atlasCount ${atlases.length}`);
1527
+ for (const [name, entry] of Object.entries(rawAssets ?? {})) {
1528
+ assets.set(name, readEntry(entry, name));
1529
+ }
1530
+ return { version: version2, assets };
1531
+ }
1532
+ function writeAssetMetadata(metadata) {
1533
+ const assets = {};
1534
+ for (const [name, entry] of [...metadata.assets].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1535
+ const out = { kind: entry.kind };
1536
+ if (entry.blocking) out.blocking = true;
1537
+ if (entry.foreground) out.foreground = true;
1538
+ if (entry.roof) out.roof = true;
1539
+ if (entry.door) out.door = true;
1540
+ if (entry.water) out.water = true;
1541
+ if (entry.minimap !== 0) out.minimap = entry.minimap;
1542
+ if (entry.collide !== void 0) out.collide = entry.collide;
1543
+ if (entry.offset !== void 0) out.offset = entry.offset;
1544
+ if (entry.drawScale !== void 0) out.drawScale = entry.drawScale;
1545
+ if (entry.nineGrid !== void 0) out.nineGrid = entry.nineGrid;
1546
+ if (entry.scale !== void 0) out.scale = entry.scale;
1547
+ if (entry.animations.size > 0) {
1548
+ const animations = {};
1549
+ const ordered = [...entry.animations].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1550
+ for (const [animation, frames] of ordered) {
1551
+ animations[animation] = frames.map((frame) => frame.flip ? { asset: frame.asset, ms: frame.ms, flip: true } : { asset: frame.asset, ms: frame.ms });
1552
+ }
1553
+ out.animations = animations;
1521
1554
  }
1522
- const entry = {
1523
- name,
1524
- atlasIndex,
1525
- x: view.getUint16(base + 8, true),
1526
- y: view.getUint16(base + 10, true),
1527
- width: view.getUint16(base + 12, true),
1528
- height: view.getUint16(base + 14, true),
1529
- offsetX: view.getInt16(base + 16, true),
1530
- offsetY: view.getInt16(base + 18, true),
1531
- flags: view.getUint16(base + 20, true)
1532
- };
1533
- if (entry.x + entry.width > atlas.width || entry.y + entry.height > atlas.height) {
1534
- throw new AssetIndexError(
1535
- `asset ${name}: rectangle ${entry.x},${entry.y} ${entry.width}x${entry.height} leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`
1536
- );
1555
+ if (entry.plays.size > 0) {
1556
+ const plays = {};
1557
+ for (const [animation, play] of [...entry.plays].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) plays[animation] = play;
1558
+ out.plays = plays;
1537
1559
  }
1538
- assets.set(name, entry);
1560
+ assets[name] = out;
1539
1561
  }
1540
- return assets;
1562
+ return `${JSON.stringify({ version: metadata.version, assets }, null, 2)}
1563
+ `;
1541
1564
  }
1542
- const U16_MAX = 65535;
1543
- const I16_MIN = -32768;
1544
- const I16_MAX = 32767;
1545
- function writeAssetIndex(atlases, assets) {
1546
- const encoder2 = new TextEncoder();
1547
- const ordered = [...assets].sort((left, right) => compareBytes(encoder2, left.name, right.name));
1548
- const seen = /* @__PURE__ */ new Set();
1549
- for (const asset of ordered) {
1550
- if (seen.has(asset.name)) throw new AssetIndexError(`duplicate asset name ${asset.name}`);
1551
- seen.add(asset.name);
1552
- if (asset.atlasIndex >= atlases.length) {
1553
- throw new AssetIndexError(`asset ${asset.name}: atlasIndex ${asset.atlasIndex} ≥ atlasCount ${atlases.length}`);
1565
+ class IdMapError extends Error {
1566
+ // Assigned in the body: the suites run under Node's strip-only TypeScript
1567
+ // mode, which rejects parameter properties (docs/notes.md n-0023).
1568
+ constructor(message) {
1569
+ super(message);
1570
+ this.name = "IdMapError";
1571
+ }
1572
+ }
1573
+ const SECTIONS = ["looktypes", "frames"];
1574
+ const WHOLE_NUMBER = /^(0|[1-9][0-9]*)$/;
1575
+ const FRAME_KEY = /^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/;
1576
+ function parseIdMap(raw) {
1577
+ const record = asObject(raw, "the id map");
1578
+ for (const section2 of Object.keys(record)) {
1579
+ if (!SECTIONS.includes(section2)) {
1580
+ throw new IdMapError(`unknown section ${section2}: the map has looktypes and frames`);
1554
1581
  }
1555
- checkU16(asset.name, { x: asset.x, y: asset.y, width: asset.width, height: asset.height, flags: asset.flags });
1556
- checkI16(asset.name, { offsetX: asset.offsetX, offsetY: asset.offsetY });
1557
- const atlas = atlases[asset.atlasIndex];
1558
- if (asset.x + asset.width > atlas.width || asset.y + asset.height > atlas.height) {
1559
- throw new AssetIndexError(`asset ${asset.name}: rectangle leaves atlas ${atlas.name} (${atlas.width}x${atlas.height})`);
1582
+ }
1583
+ const looktypes = /* @__PURE__ */ new Map();
1584
+ const frames = /* @__PURE__ */ new Map();
1585
+ const names = /* @__PURE__ */ new Set();
1586
+ for (const [key, value] of Object.entries(section(record, "looktypes"))) {
1587
+ if (!WHOLE_NUMBER.test(key)) {
1588
+ throw new IdMapError(`looktype key ${key} is not a whole number`);
1560
1589
  }
1590
+ const name = assetName(value, `looktype ${key}`);
1591
+ looktypes.set(Number(key), name);
1592
+ names.add(name);
1561
1593
  }
1562
- for (const atlas of atlases) {
1563
- checkU16(atlas.name, { width: atlas.width, height: atlas.height });
1594
+ for (const [key, value] of Object.entries(section(record, "frames"))) {
1595
+ if (!FRAME_KEY.test(key)) {
1596
+ throw new IdMapError(`frame key ${key} is not packId:frameId`);
1597
+ }
1598
+ const name = assetName(value, `frame ${key}`);
1599
+ frames.set(key, name);
1600
+ names.add(name);
1564
1601
  }
1565
- const pool = [];
1566
- const ranges = /* @__PURE__ */ new Map();
1567
- const intern = (text2) => {
1568
- const found = ranges.get(text2);
1569
- if (found !== void 0) return found;
1570
- const bytes = encoder2.encode(text2);
1571
- const range = { offset: pool.length, length: bytes.length };
1572
- if (range.length > U16_MAX) throw new AssetIndexError(`name too long: ${text2}`);
1573
- pool.push(...bytes);
1574
- ranges.set(text2, range);
1575
- return range;
1602
+ return {
1603
+ nameForLooktype: (looktype) => looktypes.get(looktype) ?? null,
1604
+ nameForFrame: (packId, frameId) => frames.get(`${packId}:${frameId}`) ?? null,
1605
+ names,
1606
+ looktypeCount: looktypes.size,
1607
+ frameCount: frames.size
1576
1608
  };
1577
- for (const asset of ordered) intern(asset.name);
1578
- for (const atlas of atlases) intern(atlas.name);
1579
- const assetsOffset = HEADER_SIZE$1;
1580
- const atlasesOffset = assetsOffset + ordered.length * ASSET_SIZE;
1581
- const stringsOffset = atlasesOffset + atlases.length * ATLAS_SIZE;
1582
- const buffer = new ArrayBuffer(stringsOffset + pool.length);
1583
- const view = new DataView(buffer);
1584
- view.setUint32(0, EAI_MAGIC, true);
1585
- view.setUint32(4, EAI_VERSION, true);
1586
- view.setUint32(8, ordered.length, true);
1587
- view.setUint32(12, atlases.length, true);
1588
- view.setUint32(16, assetsOffset, true);
1589
- view.setUint32(20, atlasesOffset, true);
1590
- view.setUint32(24, stringsOffset, true);
1591
- view.setUint32(28, pool.length, true);
1592
- ordered.forEach((asset, index) => {
1593
- const base = assetsOffset + index * ASSET_SIZE;
1594
- const range = ranges.get(asset.name);
1595
- view.setUint32(base, stringsOffset + range.offset, true);
1596
- view.setUint16(base + 4, range.length, true);
1597
- view.setUint16(base + 6, asset.atlasIndex, true);
1598
- view.setUint16(base + 8, asset.x, true);
1599
- view.setUint16(base + 10, asset.y, true);
1600
- view.setUint16(base + 12, asset.width, true);
1601
- view.setUint16(base + 14, asset.height, true);
1602
- view.setInt16(base + 16, asset.offsetX, true);
1603
- view.setInt16(base + 18, asset.offsetY, true);
1604
- view.setUint16(base + 20, asset.flags, true);
1605
- });
1606
- atlases.forEach((atlas, index) => {
1607
- const base = atlasesOffset + index * ATLAS_SIZE;
1608
- const range = ranges.get(atlas.name);
1609
- view.setUint16(base, atlas.width, true);
1610
- view.setUint16(base + 2, atlas.height, true);
1611
- view.setUint32(base + 4, stringsOffset + range.offset, true);
1612
- view.setUint32(base + 8, range.length, true);
1613
- });
1614
- new Uint8Array(buffer).set(new Uint8Array(pool), stringsOffset);
1615
- return buffer;
1616
- }
1617
- function compareBytes(encoder2, left, right) {
1618
- const a = encoder2.encode(left);
1619
- const b = encoder2.encode(right);
1620
- for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
1621
- const difference = (a[index] ?? 0) - (b[index] ?? 0);
1622
- if (difference !== 0) return difference;
1623
- }
1624
- return a.length - b.length;
1625
1609
  }
1626
- function checkU16(what, fields) {
1627
- for (const [field, value] of Object.entries(fields)) {
1628
- if (!Number.isInteger(value) || value < 0 || value > U16_MAX) {
1629
- throw new AssetIndexError(`${what}: ${field} ${value} does not fit in a u16`);
1630
- }
1610
+ function checkNamesAgainstIndex(map, index, declared = /* @__PURE__ */ new Set()) {
1611
+ const absent = [...map.names].filter((name) => !index.assets.has(name) && !declared.has(name)).sort();
1612
+ if (absent.length > 0) {
1613
+ throw new IdMapError(`the id map names assets the index does not hold: ${absent.join(", ")}`);
1631
1614
  }
1632
1615
  }
1633
- function checkI16(what, fields) {
1634
- for (const [field, value] of Object.entries(fields)) {
1635
- if (!Number.isInteger(value) || value < I16_MIN || value > I16_MAX) {
1636
- throw new AssetIndexError(`${what}: ${field} ${value} does not fit in an i16`);
1637
- }
1616
+ function asObject(value, what) {
1617
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1618
+ throw new IdMapError(`${what} must be a JSON object`);
1638
1619
  }
1620
+ return value;
1639
1621
  }
1640
- const METADATA_ENTRY = "meta.json";
1641
- const METADATA_VERSION = 1;
1642
- const ANIMATION_KEYS = [
1643
- "idle",
1644
- "stand-north",
1645
- "stand-south",
1646
- "stand-west",
1647
- "stand-east",
1648
- "walk-north",
1649
- "walk-south",
1650
- "walk-west",
1651
- "walk-east",
1652
- "attack-north",
1653
- "attack-south",
1654
- "attack-west",
1655
- "attack-east",
1656
- "icon"
1657
- ];
1658
- const ANIMATION_KEY_SET = new Set(ANIMATION_KEYS);
1659
- function isAnimationKey(key) {
1660
- return ANIMATION_KEY_SET.has(key);
1622
+ function section(record, name) {
1623
+ const value = record[name];
1624
+ return value === void 0 ? {} : asObject(value, `${name}`);
1661
1625
  }
1662
- const ASSET_KINDS = ["outfit", "tile", "item", "effect", "ui"];
1663
- const UI_SCALES = ["stretch", "repeat", "round"];
1664
- const PLAY_EVERY_MIN_MS = 250;
1665
- const PLAY_EVERY_MAX_MS = 6e5;
1666
- const MAX_ANIMATION_NAME = 48;
1667
- const ANIMATION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1668
- function isAnimationName(name) {
1669
- return name.length <= MAX_ANIMATION_NAME && ANIMATION_NAME.test(name);
1626
+ function assetName(value, what) {
1627
+ if (typeof value !== "string" || value.trim() === "") {
1628
+ throw new IdMapError(`${what}: the asset name must be a non-empty string`);
1629
+ }
1630
+ return value;
1670
1631
  }
1671
- class AssetMetadataError extends Error {
1672
- // Assigned in the body: the suites run under Node's strip-only TypeScript
1673
- // mode, which rejects parameter properties (docs/notes.md n-0023).
1632
+ const EAC_MAGIC = 826491205;
1633
+ const EAC_VERSION = 1;
1634
+ const INDEX_ENTRY = "assets.eai";
1635
+ const ID_MAP_ENTRY = "id-map.json";
1636
+ const HEADER_SIZE = 32;
1637
+ const FLAG_MASKED = 1;
1638
+ const MASK_KEY = Uint8Array.from([69, 116, 101, 114, 110, 105, 116, 121, 77, 69, 45, 115, 101, 97, 108, 33]);
1639
+ class ContainerError extends Error {
1640
+ // Assigned in the body: Node's strip-only TypeScript mode rejects parameter
1641
+ // properties (docs/notes.md n-0023).
1674
1642
  constructor(message) {
1675
1643
  super(message);
1676
- this.name = "AssetMetadataError";
1644
+ this.name = "ContainerError";
1677
1645
  }
1678
1646
  }
1679
- function isRecord$2(value) {
1680
- return typeof value === "object" && value !== null && !Array.isArray(value);
1647
+ function maskByte(value, offset) {
1648
+ const key = MASK_KEY[offset % MASK_KEY.length];
1649
+ const drift = Math.imul(offset, 31) + (offset >>> 5) & 255;
1650
+ return (value ^ key ^ drift) & 255;
1681
1651
  }
1682
- const MAX_ANIMATION_FRAMES = 64;
1683
- function readFrames(value, asset, animation) {
1684
- if (!Array.isArray(value)) {
1685
- throw new AssetMetadataError(`${asset}: animation ${animation} is not a list of frames`);
1652
+ function maskInto(target, start, source) {
1653
+ for (let index = 0; index < source.length; index += 1) {
1654
+ target[start + index] = maskByte(source[index], start + index);
1686
1655
  }
1687
- if (value.length > MAX_ANIMATION_FRAMES) {
1688
- throw new AssetMetadataError(`${asset}: animation ${animation} has ${value.length} frames, and an animation holds at most ${MAX_ANIMATION_FRAMES}`);
1656
+ }
1657
+ function sealContainer(entries) {
1658
+ const seen = /* @__PURE__ */ new Set();
1659
+ for (const entry of entries) {
1660
+ if (seen.has(entry.name)) throw new ContainerError(`duplicate entry name ${entry.name}`);
1661
+ seen.add(entry.name);
1689
1662
  }
1690
- return value.map((raw, index) => {
1691
- if (!isRecord$2(raw)) {
1692
- throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} is not a frame`);
1693
- }
1694
- const name = raw.asset;
1695
- if (typeof name !== "string" || name === "") {
1696
- throw new AssetMetadataError(`${asset}: animation ${animation} frame ${index} names no asset`);
1697
- }
1698
- const ms = raw.ms;
1699
- if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
1700
- throw new AssetMetadataError(
1701
- `${asset}: animation ${animation} frame ${index} has duration ${String(ms)}, which is not a whole number of milliseconds at or above zero`
1702
- );
1703
- }
1704
- return { asset: name, ms, flip: raw.flip === true };
1663
+ const encoder2 = new TextEncoder();
1664
+ const ordered = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
1665
+ const names = ordered.map((entry) => encoder2.encode(entry.name));
1666
+ const tocLength = names.reduce((total, name) => total + 2 + name.length + 8, 0);
1667
+ const tocOffset = HEADER_SIZE;
1668
+ const payloadOffset = tocOffset + tocLength;
1669
+ const payloadLength = ordered.reduce((total, entry) => total + entry.bytes.length, 0);
1670
+ const out = new Uint8Array(payloadOffset + payloadLength);
1671
+ const view = new DataView(out.buffer);
1672
+ view.setUint32(0, EAC_MAGIC, true);
1673
+ view.setUint32(4, EAC_VERSION, true);
1674
+ view.setUint32(8, ordered.length, true);
1675
+ view.setUint32(12, tocOffset, true);
1676
+ view.setUint32(16, tocLength, true);
1677
+ view.setUint32(20, payloadOffset, true);
1678
+ view.setUint32(24, FLAG_MASKED, true);
1679
+ view.setUint32(28, 0, true);
1680
+ const toc = new Uint8Array(tocLength);
1681
+ const tocView = new DataView(toc.buffer);
1682
+ let cursor = 0;
1683
+ let entryOffset = payloadOffset;
1684
+ ordered.forEach((entry, index) => {
1685
+ const name = names[index];
1686
+ tocView.setUint16(cursor, name.length, true);
1687
+ cursor += 2;
1688
+ toc.set(name, cursor);
1689
+ cursor += name.length;
1690
+ tocView.setUint32(cursor, entryOffset, true);
1691
+ cursor += 4;
1692
+ tocView.setUint32(cursor, entry.bytes.length, true);
1693
+ cursor += 4;
1694
+ maskInto(out, entryOffset, entry.bytes);
1695
+ entryOffset += entry.bytes.length;
1705
1696
  });
1697
+ maskInto(out, tocOffset, toc);
1698
+ return out;
1706
1699
  }
1707
- function readAnimations(value, asset, kind) {
1708
- const animations = /* @__PURE__ */ new Map();
1709
- if (value === void 0) return animations;
1710
- if (!isRecord$2(value)) {
1711
- throw new AssetMetadataError(`${asset}: animations is not a map of name to frames`);
1712
- }
1713
- for (const [name, frames] of Object.entries(value)) {
1714
- if (kind === "outfit" && !isAnimationKey(name)) {
1715
- throw new AssetMetadataError(`${asset}: unknown animation ${name} (expected one of ${ANIMATION_KEYS.join(", ")})`);
1716
- }
1717
- if (kind !== "outfit" && !isAnimationName(name)) {
1718
- throw new AssetMetadataError(`${asset}: animation ${name} is not a kebab-case name of at most ${MAX_ANIMATION_NAME} characters`);
1719
- }
1720
- animations.set(name, readFrames(frames, asset, name));
1700
+ function tileSpritePlacement(tx, ty, width, height, tileSize = TILE_SIZE) {
1701
+ const overhangX = (width - tileSize) / 2;
1702
+ const overhangY = (height - tileSize) / 2;
1703
+ return { x: tx * tileSize - overhangX, y: ty * tileSize - overhangY };
1704
+ }
1705
+ function placeTileSprite(tx, ty, width, height, options = {}) {
1706
+ const tileSize = options.tileSize ?? TILE_SIZE;
1707
+ const scale = options.drawScale ?? 1;
1708
+ const scaledWidth = width * scale;
1709
+ const scaledHeight = height * scale;
1710
+ const centred = tileSpritePlacement(tx, ty, scaledWidth, scaledHeight, tileSize);
1711
+ return {
1712
+ x: centred.x + (options.offset?.x ?? 0),
1713
+ y: centred.y + (options.offset?.y ?? 0),
1714
+ width: scaledWidth,
1715
+ height: scaledHeight
1716
+ };
1717
+ }
1718
+ class MapTextError extends Error {
1719
+ // Assigned in the body: the suites run under Node's strip-only TypeScript
1720
+ // mode, which rejects parameter properties (docs/notes.md n-0023).
1721
+ constructor(message) {
1722
+ super(message);
1723
+ this.name = "MapTextError";
1721
1724
  }
1722
- return animations;
1723
1725
  }
1724
- function readPlays(value, asset, animations, kind) {
1725
- const plays = /* @__PURE__ */ new Map();
1726
- if (value === void 0) return plays;
1727
- if (!isRecord$2(value)) throw new AssetMetadataError(`${asset}: plays is not a map of animation to how it plays`);
1728
- for (const [name, play] of Object.entries(value)) {
1729
- if (!animations.has(name)) throw new AssetMetadataError(`${asset}: plays names ${name}, and there is no such animation`);
1730
- if (!isRecord$2(play)) throw new AssetMetadataError(`${asset}: how ${name} plays is not an object`);
1731
- if (kind === "outfit" && play.mode !== "loop")
1732
- throw new AssetMetadataError(`${asset}: ${name} is an outfit's animation, which plays as the game drives it, in a loop`);
1733
- if (play.mode === "loop" || play.mode === "change") {
1734
- plays.set(name, { mode: play.mode });
1726
+ function tileKey(x, y, z) {
1727
+ return `${x}:${y}:${z}`;
1728
+ }
1729
+ function scanTileSources(text2) {
1730
+ const key = '"tiles"';
1731
+ let at = text2.indexOf(key);
1732
+ if (at === -1) return [];
1733
+ at = text2.indexOf("[", at + key.length);
1734
+ if (at === -1) return [];
1735
+ const sources = [];
1736
+ let depth = 0;
1737
+ let inString = false;
1738
+ let start = -1;
1739
+ for (let index = at + 1; index < text2.length; index += 1) {
1740
+ const char = text2[index];
1741
+ if (inString) {
1742
+ if (char === "\\") index += 1;
1743
+ else if (char === '"') inString = false;
1735
1744
  continue;
1736
1745
  }
1737
- if (play.mode === "every") {
1738
- const ms = play.ms;
1739
- if (typeof ms !== "number" || !Number.isInteger(ms) || ms < PLAY_EVERY_MIN_MS || ms > PLAY_EVERY_MAX_MS) {
1740
- throw new AssetMetadataError(
1741
- `${asset}: ${name} plays every ${String(ms)} ms, and an interval is a whole ${PLAY_EVERY_MIN_MS} to ${PLAY_EVERY_MAX_MS} ms`
1742
- );
1746
+ if (char === '"') {
1747
+ inString = true;
1748
+ } else if (char === "{") {
1749
+ if (depth === 0) start = index;
1750
+ depth += 1;
1751
+ } else if (char === "}") {
1752
+ depth -= 1;
1753
+ if (depth === 0 && start !== -1) {
1754
+ sources.push(text2.slice(start, index + 1));
1755
+ start = -1;
1743
1756
  }
1744
- plays.set(name, { mode: "every", ms });
1745
- continue;
1746
- }
1747
- throw new AssetMetadataError(`${asset}: ${name} plays as ${String(play.mode)}, and an animation plays as loop, every or change`);
1748
- }
1749
- return plays;
1750
- }
1751
- function readNineGrid(value, asset) {
1752
- if (value === void 0) return void 0;
1753
- if (!isRecord$2(value)) {
1754
- throw new AssetMetadataError(`${asset}: nineGrid is not an object of four insets`);
1755
- }
1756
- const insets = {};
1757
- for (const edge of ["top", "right", "bottom", "left"]) {
1758
- const inset = value[edge];
1759
- if (inset === void 0) {
1760
- throw new AssetMetadataError(`${asset}: nineGrid declares no ${edge} inset`);
1761
- }
1762
- if (typeof inset !== "number" || !Number.isInteger(inset) || inset < 0) {
1763
- throw new AssetMetadataError(`${asset}: nineGrid ${edge} is ${String(inset)}, which is not a whole number of pixels at or above zero`);
1757
+ } else if (char === "]" && depth === 0) {
1758
+ break;
1764
1759
  }
1765
- insets[edge] = inset;
1766
1760
  }
1767
- return { top: insets.top, right: insets.right, bottom: insets.bottom, left: insets.left };
1761
+ return sources;
1768
1762
  }
1769
- function readScale(value, asset) {
1770
- if (value === void 0) return void 0;
1771
- if (typeof value !== "string" || !UI_SCALES.includes(value)) {
1772
- throw new AssetMetadataError(`${asset}: scale ${String(value)} is not one of ${UI_SCALES.join(", ")}`);
1773
- }
1774
- return value;
1763
+ function isTile(value) {
1764
+ if (typeof value !== "object" || value === null) return false;
1765
+ const record = value;
1766
+ return typeof record.x === "number" && typeof record.y === "number" && typeof record.z === "number" && Array.isArray(record.layers);
1775
1767
  }
1776
- function readCollide(value, asset) {
1777
- if (value === void 0) return void 0;
1778
- if (!isRecord$2(value)) {
1779
- throw new AssetMetadataError(`${asset}: collide is not a box of x, y, w and h in cells`);
1768
+ function parseMapText(text2, path = "the map") {
1769
+ let parsed2;
1770
+ try {
1771
+ parsed2 = JSON.parse(text2);
1772
+ } catch (error) {
1773
+ throw new MapTextError(`${path} is not valid JSON: ${error.message}`);
1780
1774
  }
1781
- const box = {};
1782
- for (const side of ["x", "y", "w", "h"]) {
1783
- const cells = value[side];
1784
- if (typeof cells !== "number" || !Number.isInteger(cells)) {
1785
- throw new AssetMetadataError(`${asset}: collide ${side} is ${String(cells)}, which is not a whole number of cells`);
1786
- }
1787
- box[side] = cells;
1775
+ if (typeof parsed2 !== "object" || parsed2 === null || !Array.isArray(parsed2.tiles)) {
1776
+ throw new MapTextError(`${path} holds no tiles array`);
1788
1777
  }
1789
- if (box.w < 1 || box.h < 1) {
1790
- throw new AssetMetadataError(`${asset}: collide is ${box.w}x${box.h} cells, and a box is at least one by one`);
1778
+ const document = parsed2;
1779
+ const sources = scanTileSources(text2);
1780
+ if (sources.length !== document.tiles.length) {
1781
+ throw new MapTextError(`${path}: the scanner found ${sources.length} tile sources for ${document.tiles.length} tiles`);
1791
1782
  }
1792
- return { x: box.x, y: box.y, w: box.w, h: box.h };
1783
+ const byKey = /* @__PURE__ */ new Map();
1784
+ document.tiles.forEach((tile, index) => {
1785
+ if (!isTile(tile)) throw new MapTextError(`${path}: tile ${index} lacks x, y, z or layers`);
1786
+ byKey.set(tileKey(tile.x, tile.y, tile.z), { tile, source: sources[index] });
1787
+ });
1788
+ return { document, sources: byKey };
1793
1789
  }
1794
- const MAX_DRAW_OFFSET = TILE_SIZE * 8;
1795
- const MIN_DRAW_SCALE = 0.05;
1796
- const MAX_DRAW_SCALE = 8;
1797
- function readOffset(value, asset) {
1798
- if (value === void 0) return void 0;
1799
- if (!isRecord$2(value) || typeof value.x !== "number" || typeof value.y !== "number" || !Number.isFinite(value.x) || !Number.isFinite(value.y)) {
1800
- throw new AssetMetadataError(`${asset}: offset is not an x and a y in pixels`);
1790
+ const TILE_KEYS = ["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"];
1791
+ function serializeTile(tile) {
1792
+ const ordered = {};
1793
+ for (const key of TILE_KEYS) {
1794
+ if (tile[key] !== void 0) ordered[key] = tile[key];
1801
1795
  }
1802
- for (const axis of ["x", "y"]) {
1803
- const px = value[axis];
1804
- if (Math.abs(px) > MAX_DRAW_OFFSET) {
1805
- throw new AssetMetadataError(`${asset}: offset ${axis} is ${px} px, and an offset is within ${MAX_DRAW_OFFSET} px of the tile`);
1806
- }
1796
+ for (const key of Object.keys(tile)) {
1797
+ if (!(key in ordered) && tile[key] !== void 0) ordered[key] = tile[key];
1807
1798
  }
1808
- return { x: value.x, y: value.y };
1799
+ return JSON.stringify(ordered);
1809
1800
  }
1810
- function readDrawScale(value, asset) {
1811
- if (value === void 0) return void 0;
1812
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1813
- throw new AssetMetadataError(`${asset}: drawScale is ${String(value)}, and a scale is a number above zero`);
1801
+ function formatMap(next, previous) {
1802
+ const lines = [];
1803
+ let changed = 0;
1804
+ for (const tile of next.tiles) {
1805
+ {
1806
+ lines.push(serializeTile(tile));
1807
+ changed += 1;
1808
+ }
1814
1809
  }
1815
- if (value < MIN_DRAW_SCALE || value > MAX_DRAW_SCALE) {
1816
- throw new AssetMetadataError(`${asset}: drawScale is ${value}, and a scale is between ${MIN_DRAW_SCALE} and ${MAX_DRAW_SCALE}`);
1810
+ const rest = {};
1811
+ for (const key of Object.keys(next)) {
1812
+ if (key !== "width" && key !== "height" && key !== "tiles") rest[key] = next[key];
1817
1813
  }
1818
- return value;
1814
+ const trailer = Object.keys(rest).length === 0 ? "" : `,${JSON.stringify(rest).slice(1, -1)}`;
1815
+ const head = `{"width":${JSON.stringify(next.width)},"height":${JSON.stringify(next.height)},"tiles":[`;
1816
+ const body = lines.length === 0 ? "" : `
1817
+ ${lines.join(",\n")}
1818
+ `;
1819
+ return { text: `${head}${body}]${trailer}}
1820
+ `, changed };
1819
1821
  }
1820
- function readEntry(value, asset) {
1821
- if (!isRecord$2(value)) {
1822
- throw new AssetMetadataError(`${asset}: not an entry`);
1823
- }
1824
- const kind = value.kind;
1825
- if (typeof kind !== "string") {
1826
- throw new AssetMetadataError(`${asset}: declares no kind`);
1827
- }
1828
- if (!ASSET_KINDS.includes(kind)) {
1829
- throw new AssetMetadataError(`${asset}: unknown kind ${kind} (expected ${ASSET_KINDS.join(", ")})`);
1830
- }
1831
- const minimap = value.minimap;
1832
- if (minimap !== void 0 && (typeof minimap !== "number" || !Number.isInteger(minimap))) {
1833
- throw new AssetMetadataError(`${asset}: minimap category is not a whole number`);
1834
- }
1835
- if (kind !== "ui" && (value.nineGrid !== void 0 || value.scale !== void 0)) {
1836
- throw new AssetMetadataError(`${asset}: nineGrid and scale are interface fields, and this is a ${kind}`);
1837
- }
1838
- if (kind !== "tile" && (value.collide !== void 0 || value.offset !== void 0 || value.drawScale !== void 0)) {
1839
- throw new AssetMetadataError(`${asset}: collide, offset and drawScale are tile fields, and this is a ${kind}`);
1840
- }
1841
- const animations = readAnimations(value.animations, asset, kind);
1842
- return {
1843
- kind,
1844
- animations,
1845
- plays: readPlays(value.plays, asset, animations, kind),
1846
- blocking: value.blocking === true,
1847
- foreground: value.foreground === true,
1848
- roof: value.roof === true,
1849
- door: value.door === true,
1850
- water: value.water === true,
1851
- minimap: minimap ?? 0,
1852
- collide: readCollide(value.collide, asset),
1853
- offset: readOffset(value.offset, asset),
1854
- drawScale: readDrawScale(value.drawScale, asset),
1855
- nineGrid: readNineGrid(value.nineGrid, asset),
1856
- scale: readScale(value.scale, asset)
1857
- };
1822
+ const ROUTE_MULTIPLE = 16;
1823
+ function blank(width, height) {
1824
+ return { width, height, data: new Uint8ClampedArray(width * height * 4) };
1858
1825
  }
1859
- function parseAssetMetadata(text2) {
1860
- let parsed2;
1861
- try {
1862
- parsed2 = JSON.parse(text2);
1863
- } catch (error) {
1864
- throw new AssetMetadataError(`not JSON: ${error.message}`);
1865
- }
1866
- if (!isRecord$2(parsed2)) {
1867
- throw new AssetMetadataError("not an object");
1826
+ const CELLS_PER_TILE = 64;
1827
+ const MAX_CELL = 16;
1828
+ const MIN_CELL = 8;
1829
+ const ROUTE_MAX_EDGE = 3840;
1830
+ const ROUTE_MAX_PIXELS = 8294400;
1831
+ const ROUTE_MIN_PIXELS = 655360;
1832
+ const ROUTE_STEP = 16;
1833
+ function gridForCells(cols, rows, cellsPerTile = CELLS_PER_TILE) {
1834
+ if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) return null;
1835
+ const drawn = (cell) => cols * cell <= ROUTE_MAX_EDGE && rows * cell <= ROUTE_MAX_EDGE && cols * cell * rows * cell <= ROUTE_MAX_PIXELS && cols * cell % ROUTE_STEP === 0 && rows * cell % ROUTE_STEP === 0;
1836
+ const unscaled = (cell) => cols * cell * rows * cell >= ROUTE_MIN_PIXELS;
1837
+ const canvas = (cell) => ({ width: cols * cell, height: rows * cell, cell, pxPerTile: cellsPerTile * cell });
1838
+ if (drawn(MAX_CELL) && unscaled(MAX_CELL)) return canvas(MAX_CELL);
1839
+ if (drawn(MAX_CELL)) {
1840
+ for (let cell = MAX_CELL + 1; cell * Math.max(cols, rows) <= ROUTE_MAX_EDGE; cell += 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
1841
+ return null;
1868
1842
  }
1869
- const version2 = parsed2.version;
1870
- if (version2 !== METADATA_VERSION) {
1871
- throw new AssetMetadataError(`unsupported version ${String(version2)}, reader speaks v${METADATA_VERSION}`);
1843
+ for (let cell = MAX_CELL - 1; cell >= MIN_CELL; cell -= 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
1844
+ return null;
1845
+ }
1846
+ const LEAST_CELLS_PER_TILE = 16;
1847
+ function cellCounts(cellsPerTile) {
1848
+ const counts = [cellsPerTile];
1849
+ for (let cells = cellsPerTile / 2; Number.isInteger(cells) && cells >= LEAST_CELLS_PER_TILE; cells /= 2) counts.push(cells);
1850
+ return counts;
1851
+ }
1852
+ function gridCanvas(footprint, cellsPerTile = CELLS_PER_TILE) {
1853
+ for (const cells of cellCounts(cellsPerTile)) {
1854
+ const canvas = gridForCells(footprint.cols * cells, footprint.rows * cells, cells);
1855
+ if (canvas !== null) return canvas;
1872
1856
  }
1873
- const rawAssets = parsed2.assets;
1874
- if (rawAssets !== void 0 && !isRecord$2(rawAssets)) {
1875
- throw new AssetMetadataError("assets is not a map of name to entry");
1857
+ return null;
1858
+ }
1859
+ function gridProblem(footprint, cellsPerTile = CELLS_PER_TILE) {
1860
+ if (gridCanvas(footprint, cellsPerTile) !== null) return null;
1861
+ const least = cellCounts(cellsPerTile).at(-1);
1862
+ const side = Math.floor(ROUTE_MAX_EDGE / (least * MIN_CELL));
1863
+ const area = Math.floor(ROUTE_MAX_PIXELS / (least * MIN_CELL) ** 2);
1864
+ return `a ${footprint.cols}×${footprint.rows} tile object is too large to draw on the grid: at most ${side} tile${side === 1 ? "" : "s"} on a side and ${area} tile${area === 1 ? "" : "s"} in all`;
1865
+ }
1866
+ const WINDOW_PX = { width: 1536, height: 1024 };
1867
+ const SCALES = [TILE_SIZE, TILE_SIZE / 2, TILE_SIZE / 4, TILE_SIZE / 8, TILE_SIZE / 16];
1868
+ function scaleFor(tilesWide, tilesHigh) {
1869
+ if (tilesWide <= 0 || tilesHigh <= 0) return null;
1870
+ for (const scale of SCALES) {
1871
+ if (tilesWide * scale <= WINDOW_PX.width && tilesHigh * scale <= WINDOW_PX.height) return scale;
1876
1872
  }
1877
- const assets = /* @__PURE__ */ new Map();
1878
- for (const [name, entry] of Object.entries(rawAssets ?? {})) {
1879
- assets.set(name, readEntry(entry, name));
1873
+ return null;
1874
+ }
1875
+ ({
1876
+ width: Math.floor(WINDOW_PX.width / SCALES.at(-1)),
1877
+ height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
1878
+ });
1879
+ function designCanvas(tiles) {
1880
+ if (!Number.isInteger(tiles.cols) || !Number.isInteger(tiles.rows) || tiles.cols < 1 || tiles.rows < 1) return null;
1881
+ const scale = scaleFor(tiles.cols, tiles.rows);
1882
+ if (scale === null) return null;
1883
+ return { cols: tiles.cols, rows: tiles.rows, scale, width: tiles.cols * scale, height: tiles.rows * scale };
1884
+ }
1885
+ const CONCEPT_ART = "concept-art";
1886
+ const OLD_CONCEPT_ART = "level-design";
1887
+ function currentKind(kind) {
1888
+ return kind === OLD_CONCEPT_ART ? CONCEPT_ART : kind;
1889
+ }
1890
+ const DEFAULT_CONCEPT_SIZE = { width: 1536, height: 1024 };
1891
+ const DEFAULT_CONCEPT_TILES = { cols: 6, rows: 4 };
1892
+ const CONCEPT_SIDE = { min: 256, max: 1536 };
1893
+ const WINDOWS = [
1894
+ { width: 1536, height: 1024 },
1895
+ { width: 1024, height: 1536 }
1896
+ ];
1897
+ function conceptSizeProblem(size) {
1898
+ const { width, height } = size;
1899
+ const side = (value) => Number.isInteger(value) && value >= CONCEPT_SIDE.min && value <= CONCEPT_SIDE.max && value % ROUTE_MULTIPLE === 0;
1900
+ if (!side(width) || !side(height))
1901
+ return `a concept art's sides are multiples of ${ROUTE_MULTIPLE} from ${CONCEPT_SIDE.min} to ${CONCEPT_SIDE.max} px, not ${width} × ${height}`;
1902
+ if (!WINDOWS.some((window) => width <= window.width && height <= window.height))
1903
+ return `a ${width} × ${height} px concept art is past the route's window: it fits within 1536 × 1024 or 1024 × 1536`;
1904
+ return null;
1905
+ }
1906
+ function conceptShapeProblem(shape) {
1907
+ if (shape.pixelArt === true) {
1908
+ if (shape.size !== void 0) return "a pixel-art concept art is sized in tiles, not in pixels";
1909
+ if (shape.background === "transparent") return "a pixel-art concept art is a whole area, with no background to cut away";
1910
+ const tiles = shape.tiles ?? DEFAULT_CONCEPT_TILES;
1911
+ return designCanvas(tiles) === null ? `a ${tiles.cols}×${tiles.rows} tile concept art does not fit the route's window` : null;
1880
1912
  }
1881
- return { version: version2, assets };
1913
+ if (shape.tiles !== void 0) return "tiles are a pixel-art concept art's: ask for pixel art too";
1914
+ return shape.size === void 0 ? null : conceptSizeProblem(shape.size);
1882
1915
  }
1883
- function writeAssetMetadata(metadata) {
1884
- const assets = {};
1885
- for (const [name, entry] of [...metadata.assets].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
1886
- const out = { kind: entry.kind };
1887
- if (entry.blocking) out.blocking = true;
1888
- if (entry.foreground) out.foreground = true;
1889
- if (entry.roof) out.roof = true;
1890
- if (entry.door) out.door = true;
1891
- if (entry.water) out.water = true;
1892
- if (entry.minimap !== 0) out.minimap = entry.minimap;
1893
- if (entry.collide !== void 0) out.collide = entry.collide;
1894
- if (entry.offset !== void 0) out.offset = entry.offset;
1895
- if (entry.drawScale !== void 0) out.drawScale = entry.drawScale;
1896
- if (entry.nineGrid !== void 0) out.nineGrid = entry.nineGrid;
1897
- if (entry.scale !== void 0) out.scale = entry.scale;
1898
- if (entry.animations.size > 0) {
1899
- const animations = {};
1900
- const ordered = [...entry.animations].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1901
- for (const [animation, frames] of ordered) {
1902
- animations[animation] = frames.map((frame) => frame.flip ? { asset: frame.asset, ms: frame.ms, flip: true } : { asset: frame.asset, ms: frame.ms });
1903
- }
1904
- out.animations = animations;
1905
- }
1906
- if (entry.plays.size > 0) {
1907
- const plays = {};
1908
- for (const [animation, play] of [...entry.plays].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) plays[animation] = play;
1909
- out.plays = plays;
1910
- }
1911
- assets[name] = out;
1916
+ const MAX_ART_TAGS = 8;
1917
+ const TAG = /^[a-z0-9][a-z0-9-]{0,31}$/;
1918
+ function tagProblem(tag2) {
1919
+ return TAG.test(tag2) ? null : `${JSON.stringify(tag2)} is not a tag: 1 to 32 lowercase letters, digits and dashes, starting with a letter or a digit`;
1920
+ }
1921
+ function tagOf(typed) {
1922
+ return typed.trim().toLowerCase().replace(/[\s_]+/g, "-");
1923
+ }
1924
+ function tagsProblem(tags) {
1925
+ if (tags.length > MAX_ART_TAGS) return `a concept art carries at most ${MAX_ART_TAGS} tags, not ${tags.length}`;
1926
+ for (const tag2 of tags) {
1927
+ const problem = tagProblem(tag2);
1928
+ if (problem !== null) return problem;
1912
1929
  }
1913
- return `${JSON.stringify({ version: metadata.version, assets }, null, 2)}
1914
- `;
1930
+ const repeated = tags.find((tag2, at) => tags.indexOf(tag2) !== at);
1931
+ return repeated === void 0 ? null : `${repeated} is given twice`;
1915
1932
  }
1916
- class IdMapError extends Error {
1917
- // Assigned in the body: the suites run under Node's strip-only TypeScript
1918
- // mode, which rejects parameter properties (docs/notes.md n-0023).
1933
+ const SECTOR_SIDE$1 = 128;
1934
+ const SECTOR_CELLS = SECTOR_SIDE$1 * SECTOR_SIDE$1;
1935
+ const SECTOR_BYTES = 512 * 1024;
1936
+ const MAP_LEVELS = { first: 0, last: 14 };
1937
+ const LARGEST_MAP_SIDE = 1e4;
1938
+ const OUTLINE_ACROSS = Math.ceil(LARGEST_MAP_SIDE / SECTOR_SIDE$1);
1939
+ const SECTOR_FORMAT = 1;
1940
+ const MAGIC = [80, 86, 67];
1941
+ const PRESENT = 1;
1942
+ const AUDIO = 2;
1943
+ const INTERACT = 4;
1944
+ const EXTRA = 8;
1945
+ const U16 = 65535;
1946
+ const MAX_LAYERS = 255;
1947
+ const NAMED$1 = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
1948
+ class SectorError extends Error {
1919
1949
  constructor(message) {
1920
1950
  super(message);
1921
- this.name = "IdMapError";
1951
+ this.name = "SectorError";
1922
1952
  }
1923
1953
  }
1924
- const SECTIONS = ["looktypes", "frames"];
1925
- const WHOLE_NUMBER = /^(0|[1-9][0-9]*)$/;
1926
- const FRAME_KEY = /^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/;
1927
- function parseIdMap(raw) {
1928
- const record = asObject(raw, "the id map");
1929
- for (const section2 of Object.keys(record)) {
1930
- if (!SECTIONS.includes(section2)) {
1931
- throw new IdMapError(`unknown section ${section2}: the map has looktypes and frames`);
1954
+ function u16(value, what) {
1955
+ 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`);
1956
+ return value;
1957
+ }
1958
+ function encodeSector(cells) {
1959
+ if (cells.length !== SECTOR_CELLS) throw new SectorError(`a sector is ${SECTOR_CELLS} cells, not ${cells.length}`);
1960
+ const encoder2 = new TextEncoder();
1961
+ const bytes = [...MAGIC, SECTOR_FORMAT];
1962
+ const push16 = (value) => bytes.push(value & 255, value >> 8);
1963
+ for (const cell of cells) {
1964
+ if (cell === void 0) {
1965
+ bytes.push(0);
1966
+ continue;
1932
1967
  }
1933
- }
1934
- const looktypes = /* @__PURE__ */ new Map();
1935
- const frames = /* @__PURE__ */ new Map();
1936
- const names = /* @__PURE__ */ new Set();
1937
- for (const [key, value] of Object.entries(section(record, "looktypes"))) {
1938
- if (!WHOLE_NUMBER.test(key)) {
1939
- throw new IdMapError(`looktype key ${key} is not a whole number`);
1968
+ const extra = cell.extra === void 0 ? null : encoder2.encode(JSON.stringify(cell.extra));
1969
+ if (extra !== null && extra.length > U16) throw new SectorError("a tile's extra fields are past 65,535 bytes");
1970
+ if (cell.layers.length > MAX_LAYERS) throw new SectorError(`a tile has ${cell.layers.length} layers, and a sector holds at most ${MAX_LAYERS}`);
1971
+ bytes.push(PRESENT | (cell.audioZone === void 0 ? 0 : AUDIO) | (cell.interact === void 0 ? 0 : INTERACT) | (extra === null ? 0 : EXTRA));
1972
+ bytes.push(cell.layers.length);
1973
+ for (const layer of cell.layers) push16(u16(layer, "a layer id"));
1974
+ push16(u16(cell.blocking, "blocking"));
1975
+ push16(u16(cell.zone, "a zone"));
1976
+ if (cell.audioZone !== void 0) push16(u16(cell.audioZone, "an audio zone"));
1977
+ if (cell.interact !== void 0) push16(u16(cell.interact, "interact"));
1978
+ if (extra !== null) {
1979
+ push16(extra.length);
1980
+ for (const byte of extra) bytes.push(byte);
1940
1981
  }
1941
- const name = assetName(value, `looktype ${key}`);
1942
- looktypes.set(Number(key), name);
1943
- names.add(name);
1944
1982
  }
1945
- for (const [key, value] of Object.entries(section(record, "frames"))) {
1946
- if (!FRAME_KEY.test(key)) {
1947
- throw new IdMapError(`frame key ${key} is not packId:frameId`);
1983
+ return Uint8Array.from(bytes);
1984
+ }
1985
+ function decodeSector(bytes) {
1986
+ if (bytes.length < 4 || MAGIC.some((byte, index) => bytes[index] !== byte)) throw new SectorError("these bytes are not a map sector");
1987
+ if (bytes[3] !== SECTOR_FORMAT) throw new SectorError(`a map sector of format ${bytes[3]} is not one this version reads`);
1988
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1989
+ let at = 4;
1990
+ const need = (count) => {
1991
+ if (at + count > bytes.length) throw new SectorError("the sector's bytes end before its last cell");
1992
+ };
1993
+ const read8 = () => {
1994
+ need(1);
1995
+ return bytes[at++];
1996
+ };
1997
+ const read16 = () => {
1998
+ need(2);
1999
+ const value = bytes[at] | bytes[at + 1] << 8;
2000
+ at += 2;
2001
+ return value;
2002
+ };
2003
+ const cells = [];
2004
+ for (let index = 0; index < SECTOR_CELLS; index += 1) {
2005
+ const flags = read8();
2006
+ if ((flags & PRESENT) === 0) {
2007
+ cells.push(void 0);
2008
+ continue;
1948
2009
  }
1949
- const name = assetName(value, `frame ${key}`);
1950
- frames.set(key, name);
1951
- names.add(name);
2010
+ const count = read8();
2011
+ const layers = [];
2012
+ for (let layer = 0; layer < count; layer += 1) layers.push(read16());
2013
+ const blocking = read16();
2014
+ const zone = read16();
2015
+ const audioZone = (flags & AUDIO) === 0 ? void 0 : read16();
2016
+ const interact = (flags & INTERACT) === 0 ? void 0 : read16();
2017
+ let extra;
2018
+ if ((flags & EXTRA) !== 0) {
2019
+ const length = read16();
2020
+ need(length);
2021
+ let parsed2;
2022
+ try {
2023
+ parsed2 = JSON.parse(decoder.decode(bytes.subarray(at, at + length)));
2024
+ } catch {
2025
+ throw new SectorError("a tile's extra fields are not JSON");
2026
+ }
2027
+ if (typeof parsed2 !== "object" || parsed2 === null || Array.isArray(parsed2)) throw new SectorError("a tile's extra fields are not an object");
2028
+ const named = Object.keys(parsed2).find((key) => NAMED$1.has(key));
2029
+ if (named !== void 0) throw new SectorError(`a tile's extra fields name ${named}, which the sector holds itself`);
2030
+ extra = parsed2;
2031
+ at += length;
2032
+ }
2033
+ cells.push({
2034
+ layers,
2035
+ blocking,
2036
+ zone,
2037
+ ...audioZone === void 0 ? {} : { audioZone },
2038
+ ...interact === void 0 ? {} : { interact },
2039
+ ...extra === void 0 ? {} : { extra }
2040
+ });
1952
2041
  }
2042
+ if (at !== bytes.length) throw new SectorError("the sector's bytes go on past its last cell");
2043
+ return cells;
2044
+ }
2045
+ const NAMED = /* @__PURE__ */ new Set(["x", "y", "z", "layers", "blocking", "zone", "audioZone", "interact"]);
2046
+ function cellOfTile(tile) {
2047
+ const extra = Object.fromEntries(Object.entries(tile).filter(([key]) => !NAMED.has(key)));
1953
2048
  return {
1954
- nameForLooktype: (looktype) => looktypes.get(looktype) ?? null,
1955
- nameForFrame: (packId, frameId) => frames.get(`${packId}:${frameId}`) ?? null,
1956
- names,
1957
- looktypeCount: looktypes.size,
1958
- frameCount: frames.size
2049
+ layers: tile.layers,
2050
+ blocking: tile.blocking,
2051
+ zone: tile.zone,
2052
+ ...tile.audioZone === void 0 ? {} : { audioZone: tile.audioZone },
2053
+ ...tile.interact === void 0 ? {} : { interact: tile.interact },
2054
+ ...Object.keys(extra).length === 0 ? {} : { extra }
1959
2055
  };
1960
2056
  }
1961
- function checkNamesAgainstIndex(map, index, declared = /* @__PURE__ */ new Set()) {
1962
- const absent = [...map.names].filter((name) => !index.assets.has(name) && !declared.has(name)).sort();
1963
- if (absent.length > 0) {
1964
- throw new IdMapError(`the id map names assets the index does not hold: ${absent.join(", ")}`);
1965
- }
1966
- }
1967
- function asObject(value, what) {
1968
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1969
- throw new IdMapError(`${what} must be a JSON object`);
1970
- }
1971
- return value;
1972
- }
1973
- function section(record, name) {
1974
- const value = record[name];
1975
- return value === void 0 ? {} : asObject(value, `${name}`);
1976
- }
1977
- function assetName(value, what) {
1978
- if (typeof value !== "string" || value.trim() === "") {
1979
- throw new IdMapError(`${what}: the asset name must be a non-empty string`);
1980
- }
1981
- return value;
1982
- }
1983
- const EAC_MAGIC = 826491205;
1984
- const EAC_VERSION = 1;
1985
- const INDEX_ENTRY = "assets.eai";
1986
- const ID_MAP_ENTRY = "id-map.json";
1987
- const HEADER_SIZE = 32;
1988
- const FLAG_MASKED = 1;
1989
- const MASK_KEY = Uint8Array.from([69, 116, 101, 114, 110, 105, 116, 121, 77, 69, 45, 115, 101, 97, 108, 33]);
1990
- class ContainerError extends Error {
2057
+ function tileOfCell(cell, x, y, z) {
2058
+ return {
2059
+ ...cell.extra ?? {},
2060
+ x,
2061
+ y,
2062
+ z,
2063
+ layers: [...cell.layers],
2064
+ blocking: cell.blocking,
2065
+ zone: cell.zone,
2066
+ ...cell.audioZone === void 0 ? {} : { audioZone: cell.audioZone },
2067
+ ...cell.interact === void 0 ? {} : { interact: cell.interact }
2068
+ };
2069
+ }
2070
+ const MIRROR_MAP_DIRECTORY = "map";
2071
+ const MIRROR_INDEX_FILE = `${MIRROR_MAP_DIRECTORY}/index.json`;
2072
+ const MIRROR_FORMAT = 1;
2073
+ class MirrorError extends Error {
1991
2074
  // Assigned in the body: Node's strip-only TypeScript mode rejects parameter
1992
2075
  // properties (docs/notes.md n-0023).
1993
2076
  constructor(message) {
1994
2077
  super(message);
1995
- this.name = "ContainerError";
2078
+ this.name = "MirrorError";
1996
2079
  }
1997
2080
  }
1998
- function maskByte(value, offset) {
1999
- const key = MASK_KEY[offset % MASK_KEY.length];
2000
- const drift = Math.imul(offset, 31) + (offset >>> 5) & 255;
2001
- return (value ^ key ^ drift) & 255;
2081
+ function mirrorSectorPath(z, cx, cy) {
2082
+ if (!isPlace(z, cx, cy)) throw new MirrorError(`no sector of a map is at level ${z}, column ${cx}, row ${cy}`);
2083
+ return `${MIRROR_MAP_DIRECTORY}/${z}/${cx}-${cy}.bin`;
2002
2084
  }
2003
- function maskInto(target, start, source) {
2004
- for (let index = 0; index < source.length; index += 1) {
2005
- target[start + index] = maskByte(source[index], start + index);
2006
- }
2085
+ function isPlace(z, cx, cy) {
2086
+ const within = (value, last) => Number.isInteger(value) && value >= 0 && value <= last;
2087
+ return within(z, MAP_LEVELS.last) && z >= MAP_LEVELS.first && within(cx, OUTLINE_ACROSS - 1) && within(cy, OUTLINE_ACROSS - 1);
2007
2088
  }
2008
- function sealContainer(entries) {
2089
+ function isSize(value) {
2090
+ const side = (n) => Number.isInteger(n) && n >= 1 && n <= LARGEST_MAP_SIDE;
2091
+ return typeof value === "object" && value !== null && side(value.width) && side(value.height);
2092
+ }
2093
+ function mirrorIndexText(index) {
2094
+ return `${JSON.stringify(index, null, 2)}
2095
+ `;
2096
+ }
2097
+ function parseMirrorIndex(text2) {
2098
+ let value;
2099
+ try {
2100
+ value = JSON.parse(text2);
2101
+ } catch {
2102
+ throw new MirrorError(`${MIRROR_INDEX_FILE} is not JSON`);
2103
+ }
2104
+ const index = value;
2105
+ if (typeof index !== "object" || index === null) throw new MirrorError(`${MIRROR_INDEX_FILE} is not an object`);
2106
+ if (index.format !== MIRROR_FORMAT)
2107
+ throw new MirrorError(`${MIRROR_INDEX_FILE} is in format ${String(index.format)}, and this reads format ${MIRROR_FORMAT}`);
2108
+ if (!Number.isInteger(index.revision) || index.revision < 0) throw new MirrorError(`${MIRROR_INDEX_FILE} names no revision`);
2109
+ if (!isSize(index.size) || !isSize(index.limit)) throw new MirrorError(`${MIRROR_INDEX_FILE} names no size or limit a map may have`);
2110
+ if (!Array.isArray(index.names) || !Array.isArray(index.sectors)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists no levels or sectors`);
2111
+ for (const named of index.names) {
2112
+ if (typeof named?.name !== "string" || !isPlace(named.z, 0, 0)) throw new MirrorError(`${MIRROR_INDEX_FILE} names a level that is none`);
2113
+ }
2009
2114
  const seen = /* @__PURE__ */ new Set();
2010
- for (const entry of entries) {
2011
- if (seen.has(entry.name)) throw new ContainerError(`duplicate entry name ${entry.name}`);
2012
- seen.add(entry.name);
2115
+ for (const sector of index.sectors) {
2116
+ const [z, cx, cy, version2] = Array.isArray(sector) ? sector : [];
2117
+ if (!isPlace(z, cx, cy) || !Number.isInteger(version2) || version2 < 1) {
2118
+ throw new MirrorError(`${MIRROR_INDEX_FILE} lists a sector that is none: ${JSON.stringify(sector)}`);
2119
+ }
2120
+ const key = `${z}:${cx}:${cy}`;
2121
+ if (cx * SECTOR_SIDE$1 >= index.size.width || cy * SECTOR_SIDE$1 >= index.size.height) {
2122
+ throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key}, outside the map's size`);
2123
+ }
2124
+ if (seen.has(key)) throw new MirrorError(`${MIRROR_INDEX_FILE} lists the sector ${key} twice`);
2125
+ seen.add(key);
2013
2126
  }
2014
- const encoder2 = new TextEncoder();
2015
- const ordered = [...entries].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
2016
- const names = ordered.map((entry) => encoder2.encode(entry.name));
2017
- const tocLength = names.reduce((total, name) => total + 2 + name.length + 8, 0);
2018
- const tocOffset = HEADER_SIZE;
2019
- const payloadOffset = tocOffset + tocLength;
2020
- const payloadLength = ordered.reduce((total, entry) => total + entry.bytes.length, 0);
2021
- const out = new Uint8Array(payloadOffset + payloadLength);
2022
- const view = new DataView(out.buffer);
2023
- view.setUint32(0, EAC_MAGIC, true);
2024
- view.setUint32(4, EAC_VERSION, true);
2025
- view.setUint32(8, ordered.length, true);
2026
- view.setUint32(12, tocOffset, true);
2027
- view.setUint32(16, tocLength, true);
2028
- view.setUint32(20, payloadOffset, true);
2029
- view.setUint32(24, FLAG_MASKED, true);
2030
- view.setUint32(28, 0, true);
2031
- const toc = new Uint8Array(tocLength);
2032
- const tocView = new DataView(toc.buffer);
2033
- let cursor = 0;
2034
- let entryOffset = payloadOffset;
2035
- ordered.forEach((entry, index) => {
2036
- const name = names[index];
2037
- tocView.setUint16(cursor, name.length, true);
2038
- cursor += 2;
2039
- toc.set(name, cursor);
2040
- cursor += name.length;
2041
- tocView.setUint32(cursor, entryOffset, true);
2042
- cursor += 4;
2043
- tocView.setUint32(cursor, entry.bytes.length, true);
2044
- cursor += 4;
2045
- maskInto(out, entryOffset, entry.bytes);
2046
- entryOffset += entry.bytes.length;
2047
- });
2048
- maskInto(out, tocOffset, toc);
2049
- return out;
2050
- }
2051
- function tileSpritePlacement(tx, ty, width, height, tileSize = TILE_SIZE) {
2052
- const overhangX = (width - tileSize) / 2;
2053
- const overhangY = (height - tileSize) / 2;
2054
- return { x: tx * tileSize - overhangX, y: ty * tileSize - overhangY };
2127
+ return index;
2055
2128
  }
2056
- function placeTileSprite(tx, ty, width, height, options = {}) {
2057
- const tileSize = options.tileSize ?? TILE_SIZE;
2058
- const scale = options.drawScale ?? 1;
2059
- const scaledWidth = width * scale;
2060
- const scaledHeight = height * scale;
2061
- const centred = tileSpritePlacement(tx, ty, scaledWidth, scaledHeight, tileSize);
2062
- return {
2063
- x: centred.x + (options.offset?.x ?? 0),
2064
- y: centred.y + (options.offset?.y ?? 0),
2065
- width: scaledWidth,
2066
- height: scaledHeight
2067
- };
2129
+ async function readMirroredMap(index, read, inflate2) {
2130
+ const tiles = [];
2131
+ for (const [z, cx, cy] of index.sectors) {
2132
+ const path = mirrorSectorPath(z, cx, cy);
2133
+ let cells;
2134
+ try {
2135
+ cells = decodeSector(await inflate2(await read(path)));
2136
+ } catch (error) {
2137
+ throw new MirrorError(`${path} is not a sector the map can read: ${error instanceof Error ? error.message : String(error)}`);
2138
+ }
2139
+ for (const [at, cell] of cells.entries()) {
2140
+ if (cell === void 0) continue;
2141
+ const x = cx * SECTOR_SIDE$1 + at % SECTOR_SIDE$1;
2142
+ const y = cy * SECTOR_SIDE$1 + Math.floor(at / SECTOR_SIDE$1);
2143
+ if (x >= index.size.width || y >= index.size.height) continue;
2144
+ tiles.push(tileOfCell(cell, x, y, z));
2145
+ }
2146
+ }
2147
+ tiles.sort((a, b) => a.z - b.z || a.y - b.y || a.x - b.x);
2148
+ return { width: index.size.width, height: index.size.height, tiles };
2068
2149
  }
2069
- const WINDOW_PX = { width: 1536, height: 1024 };
2070
- const SCALES = [TILE_SIZE, TILE_SIZE / 2, TILE_SIZE / 4, TILE_SIZE / 8, TILE_SIZE / 16];
2071
- function scaleFor(tilesWide, tilesHigh) {
2072
- if (tilesWide <= 0 || tilesHigh <= 0) return null;
2073
- for (const scale of SCALES) {
2074
- if (tilesWide * scale <= WINDOW_PX.width && tilesHigh * scale <= WINDOW_PX.height) return scale;
2150
+ function sectorsOfMap(map) {
2151
+ const sectors = /* @__PURE__ */ new Map();
2152
+ for (const tile of map.tiles) {
2153
+ const { x, y, z } = tile;
2154
+ if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= LARGEST_MAP_SIDE || y >= LARGEST_MAP_SIDE || !isPlace(z, 0, 0)) {
2155
+ throw new MirrorError(`the tile at (${x}, ${y}) on level ${z} is at no place a map has`);
2156
+ }
2157
+ const cx = Math.floor(x / SECTOR_SIDE$1);
2158
+ const cy = Math.floor(y / SECTOR_SIDE$1);
2159
+ const key = `${z}:${cx}:${cy}`;
2160
+ let sector = sectors.get(key);
2161
+ if (sector === void 0) {
2162
+ sector = { z, cx, cy, cells: new Array(SECTOR_CELLS) };
2163
+ sectors.set(key, sector);
2164
+ }
2165
+ sector.cells[y % SECTOR_SIDE$1 * SECTOR_SIDE$1 + x % SECTOR_SIDE$1] = cellOfTile(tile);
2075
2166
  }
2076
- return null;
2167
+ return [...sectors.values()].sort((a, b) => a.z - b.z || a.cy - b.cy || a.cx - b.cx);
2077
2168
  }
2078
- ({
2079
- width: Math.floor(WINDOW_PX.width / SCALES.at(-1)),
2080
- height: Math.floor(WINDOW_PX.height / SCALES.at(-1))
2081
- });
2169
+ const SIGNATURE$1 = [137, 80, 78, 71, 13, 10, 26, 10];
2170
+ function isPng(bytes) {
2171
+ return bytes.length >= 24 && SIGNATURE$1.every((byte, index) => bytes[index] === byte);
2172
+ }
2173
+ function pngSize(bytes) {
2174
+ if (!isPng(bytes)) throw new RangeError("those bytes are not a PNG");
2175
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2176
+ return { width: view.getUint32(16), height: view.getUint32(20) };
2177
+ }
2178
+ const GROUND_KINDS = /* @__PURE__ */ new Set(["ground", "floor", "wall", "cliff", "interior", "grass-stone"]);
2179
+ function isGroundName(name) {
2180
+ const { inner } = splitTileset(name);
2181
+ const kind = inner.split("/")[1] ?? "";
2182
+ return GROUND_KINDS.has(kind) || inner.includes("/wang-");
2183
+ }
2184
+ const MAX_ASSEMBLED_PX = 4096;
2082
2185
  const REACHED = Array.from({ length: 16 }, (_value, mask) => [0, 1, 2, 3].filter((corner) => (mask >> corner & 1) === 1));
2083
2186
  REACHED.map((reached) => [0, 1, 2, 3].find((corner) => !reached.includes(corner)));
2084
2187
  Array.from({ length: 16 }, (_value, index) => index);
@@ -2702,7 +2805,7 @@ function assetOf(id2, raw) {
2702
2805
  if (!Array.isArray(files) || files.length === 0 || !files.every((file) => typeof file === "string" && isPulledPath(file))) {
2703
2806
  throw new PulledError(`${PULLED_FILE}: the asset ${id2} names a file that is not a picture under out/ or ref/`);
2704
2807
  }
2705
- return { kind, name, hash, files };
2808
+ return { kind: currentKind(kind), name, hash, files };
2706
2809
  }
2707
2810
  function guideOf(raw) {
2708
2811
  if (raw === void 0 || raw === null) return null;
@@ -2750,7 +2853,7 @@ async function readRecord(local) {
2750
2853
  async function writeRecord(local, record) {
2751
2854
  await artBoundary(local).writeFileReplacing(recordPath(local), writePulled(record));
2752
2855
  }
2753
- const REFERENCE_KINDS = /* @__PURE__ */ new Set(["level-design", "hud-mockup"]);
2856
+ const REFERENCE_KINDS = /* @__PURE__ */ new Set([CONCEPT_ART, "hud-mockup"]);
2754
2857
  function setPieces(asset) {
2755
2858
  return setPiecesOf(asset.kind, asset.images.length, asset.data);
2756
2859
  }
@@ -2819,23 +2922,28 @@ const date = (at) => at.slice(0, 10);
2819
2922
  function listed(assets) {
2820
2923
  if (assets.length === 0) return "No assets.";
2821
2924
  return assets.map(
2822
- (asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}`
2925
+ (asset) => `${asset.id} ${asset.kind.padEnd(12)} ${(asset.name ?? "—").padEnd(28)} ${asset.images} image${asset.images === 1 ? "" : "s"} ${date(asset.createdAt)}${asset.tags === void 0 || asset.tags.length === 0 ? "" : ` #${asset.tags.join(" #")}`}`
2823
2926
  ).join("\n");
2824
2927
  }
2825
2928
  const list = {
2826
2929
  name: "list",
2827
- summary: "List the platform project's assets, of one kind when given, newest first",
2828
- usage: "[<kind>] [--before <id>]",
2829
- options: { before: { type: "string", value: "<id>", description: "the page after the asset with this id, as the last page said" } },
2930
+ summary: "List the platform project's assets, of one kind and carrying one tag when given, newest first",
2931
+ usage: "[<kind>] [--tag <tag>] [--before <id>]",
2932
+ options: {
2933
+ tag: { type: "string", value: "<tag>", description: "only the concept art carrying this tag, such as anchor" },
2934
+ before: { type: "string", value: "<id>", description: "the page after the asset with this id, as the last page said" }
2935
+ },
2830
2936
  positionals: [0, 1],
2831
2937
  run: async (context, args) => {
2832
2938
  const local = await projectHere(context.io.cwd);
2833
2939
  const project = linkedOf(local);
2834
2940
  const platform = await signedIn(context);
2835
2941
  const [kind] = args.positionals;
2836
- const page = await platform.web.assets(project, kind ?? null, stringOf(args, "before") ?? null);
2942
+ const given = stringOf(args, "tag");
2943
+ const tag2 = given === void 0 ? null : tagOf(given);
2944
+ const page = await platform.web.assets(project, kind ?? null, stringOf(args, "before") ?? null, tag2);
2837
2945
  const more = page.next === null ? "" : `
2838
- More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`} --before ${page.next}`;
2946
+ More: pixel-vibing-cli list${kind === void 0 ? "" : ` ${kind}`}${tag2 === null ? "" : ` --tag ${tag2}`} --before ${page.next}`;
2839
2947
  answer(context, page, `${listed(page.assets)}${more}`);
2840
2948
  }
2841
2949
  };
@@ -2856,6 +2964,39 @@ const pull = {
2856
2964
  answer(context, pulled, pulledWords(pulled));
2857
2965
  }
2858
2966
  };
2967
+ const tag = {
2968
+ name: "tag",
2969
+ summary: "Say what a concept art is for: set its tags, by its id or its name, or show them when none are given",
2970
+ usage: "<id|name> [<tag>…] [--none]",
2971
+ options: { none: { type: "boolean", description: "take every tag off it" } },
2972
+ positionals: [1, 9],
2973
+ run: async (context, args) => {
2974
+ const local = await projectHere(context.io.cwd);
2975
+ const project = linkedOf(local);
2976
+ const [ref, ...typed] = args.positionals;
2977
+ const none = flagOf(args, "none");
2978
+ if (none && typed.length > 0) throw new CliError("--none takes every tag off: give tags or --none, not both");
2979
+ const platform = await signedIn(context);
2980
+ if (typed.length === 0 && !none) {
2981
+ const asset = await platform.web.asset(project, ref);
2982
+ if (asset.kind !== CONCEPT_ART) throw new CliError(`${ref} is ${asset.kind}: only concept art carries tags`);
2983
+ const tags2 = asset.tags ?? [];
2984
+ const offered = (await platform.web.tags(project)).map((each) => each.name);
2985
+ answer(
2986
+ context,
2987
+ { id: asset.id, tags: tags2, offered },
2988
+ `${tags2.length === 0 ? `${ref} carries no tag.` : `${ref}: #${tags2.join(" #")}`}
2989
+ The project's tags: ${offered.join(", ")}`
2990
+ );
2991
+ return;
2992
+ }
2993
+ const tags = typed.map(tagOf);
2994
+ const problem = tagsProblem(tags);
2995
+ if (problem !== null) throw new CliError(problem);
2996
+ const set = await platform.web.tagAsset(project, ref, tags);
2997
+ answer(context, set, set.tags.length === 0 ? `${ref} carries no tag now.` : `${ref}: #${set.tags.join(" #")}`);
2998
+ }
2999
+ };
2859
3000
  const DOWNLOAD_URL = "https://github.com/Pixel-Vibing/pixel-vibing/releases/latest";
2860
3001
  const EDITOR_VARIABLE = "PIXELVIBING_EDITOR";
2861
3002
  async function exists$1(path) {
@@ -2920,9 +3061,6 @@ function drawLayerOf(band, foreground) {
2920
3061
  if (foreground) return "foreground";
2921
3062
  return band === "ground" ? "ground" : "object";
2922
3063
  }
2923
- function blank(width, height) {
2924
- return { width, height, data: new Uint8ClampedArray(width * height * 4) };
2925
- }
2926
3064
  const SOLID = /^terrain\/ground\/([a-z0-9]+)$/;
2927
3065
  const WANG = /^terrain\/([a-z0-9]+)-([a-z0-9]+)\/wang-(\d{2})$/;
2928
3066
  function terrainTable(names) {
@@ -3900,60 +4038,14 @@ function drawCanvas(footprint) {
3900
4038
  if (pxPerTile === void 0) return null;
3901
4039
  return { width: footprint.cols * pxPerTile, height: footprint.rows * pxPerTile, pxPerTile };
3902
4040
  }
3903
- const CELLS_PER_TILE = 64;
3904
- const MAX_CELL = 16;
3905
- const MIN_CELL = 8;
3906
- const ROUTE_MAX_EDGE = 3840;
3907
- const ROUTE_MAX_PIXELS = 8294400;
3908
- const ROUTE_MIN_PIXELS = 655360;
3909
- const ROUTE_STEP = 16;
3910
- function gridForCells(cols, rows, cellsPerTile = CELLS_PER_TILE) {
3911
- if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols <= 0 || rows <= 0) return null;
3912
- const drawn = (cell) => cols * cell <= ROUTE_MAX_EDGE && rows * cell <= ROUTE_MAX_EDGE && cols * cell * rows * cell <= ROUTE_MAX_PIXELS && cols * cell % ROUTE_STEP === 0 && rows * cell % ROUTE_STEP === 0;
3913
- const unscaled = (cell) => cols * cell * rows * cell >= ROUTE_MIN_PIXELS;
3914
- const canvas = (cell) => ({ width: cols * cell, height: rows * cell, cell, pxPerTile: cellsPerTile * cell });
3915
- if (drawn(MAX_CELL) && unscaled(MAX_CELL)) return canvas(MAX_CELL);
3916
- if (drawn(MAX_CELL)) {
3917
- for (let cell = MAX_CELL + 1; cell * Math.max(cols, rows) <= ROUTE_MAX_EDGE; cell += 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
3918
- return null;
3919
- }
3920
- for (let cell = MAX_CELL - 1; cell >= MIN_CELL; cell -= 1) if (drawn(cell) && unscaled(cell)) return canvas(cell);
3921
- return null;
3922
- }
3923
- const LEAST_CELLS_PER_TILE = 16;
3924
- function cellCounts(cellsPerTile) {
3925
- const counts = [cellsPerTile];
3926
- for (let cells = cellsPerTile / 2; Number.isInteger(cells) && cells >= LEAST_CELLS_PER_TILE; cells /= 2) counts.push(cells);
3927
- return counts;
3928
- }
3929
- function gridCanvas(footprint, cellsPerTile = CELLS_PER_TILE) {
3930
- for (const cells of cellCounts(cellsPerTile)) {
3931
- const canvas = gridForCells(footprint.cols * cells, footprint.rows * cells, cells);
3932
- if (canvas !== null) return canvas;
3933
- }
3934
- return null;
3935
- }
3936
- function gridProblem(footprint, cellsPerTile = CELLS_PER_TILE) {
3937
- if (gridCanvas(footprint, cellsPerTile) !== null) return null;
3938
- const least = cellCounts(cellsPerTile).at(-1);
3939
- const side = Math.floor(ROUTE_MAX_EDGE / (least * MIN_CELL));
3940
- const area = Math.floor(ROUTE_MAX_PIXELS / (least * MIN_CELL) ** 2);
3941
- return `a ${footprint.cols}×${footprint.rows} tile object is too large to draw on the grid: at most ${side} tile${side === 1 ? "" : "s"} on a side and ${area} tile${area === 1 ? "" : "s"} in all`;
3942
- }
3943
- function designCanvas(tiles) {
3944
- if (!Number.isInteger(tiles.cols) || !Number.isInteger(tiles.rows) || tiles.cols < 1 || tiles.rows < 1) return null;
3945
- const scale = scaleFor(tiles.cols, tiles.rows);
3946
- if (scale === null) return null;
3947
- return { cols: tiles.cols, rows: tiles.rows, scale, width: tiles.cols * scale, height: tiles.rows * scale };
3948
- }
3949
4041
  const GENERATED = {
3950
4042
  object: { references: 8, words: "an object" },
3951
4043
  creature: { references: 3, words: "a creature" },
3952
- "level-design": { references: 3, words: "a level design" },
4044
+ [CONCEPT_ART]: { references: 8, words: "a concept art" },
3953
4045
  icons: { references: 0, words: "an icon set" },
3954
4046
  "ui-sketch": { references: 3, words: "a UI" }
3955
4047
  };
3956
- const KIND_NAMES = { ui: "ui-sketch" };
4048
+ const KIND_NAMES = { ui: "ui-sketch", [OLD_CONCEPT_ART]: CONCEPT_ART };
3957
4049
  function listOf(given, what) {
3958
4050
  const items = given.split(",").map((item) => item.trim());
3959
4051
  if (items.some((item) => item === "")) throw new CliError(`${what} is a comma-separated list with no empty entry`);
@@ -4018,6 +4110,41 @@ async function sketchOf(context, path, instruction) {
4018
4110
  };
4019
4111
  }
4020
4112
  const MAX_DESIGN_TILES = 64;
4113
+ function pixelsOf(given, what) {
4114
+ const match = /^(\d{1,4})[x×](\d{1,4})$/i.exec(given.trim());
4115
+ if (match === null) throw new CliError(`${what} is <width>x<height> in px, such as 1536x1024, not ${given}`);
4116
+ return { width: Number(match[1]), height: Number(match[2]) };
4117
+ }
4118
+ function conceptFields(args, asked, instruction) {
4119
+ const pixelArt = flagOf(args, "pixel-art") || asked === OLD_CONCEPT_ART;
4120
+ const transparent = flagOf(args, "transparent");
4121
+ const sizeGiven = stringOf(args, "size");
4122
+ const tilesGiven = stringOf(args, "tiles");
4123
+ const size = sizeGiven === void 0 ? void 0 : pixelsOf(sizeGiven, "--size");
4124
+ const tiles = tilesGiven === void 0 ? void 0 : sizeOf(tilesGiven, "--tiles");
4125
+ if (tiles !== void 0 && (tiles.cols > MAX_DESIGN_TILES || tiles.rows > MAX_DESIGN_TILES))
4126
+ throw new CliError(`a pixel-art concept art is 1 to ${MAX_DESIGN_TILES} tiles each way, not ${tiles.cols}×${tiles.rows}`);
4127
+ const problem = conceptShapeProblem({
4128
+ pixelArt,
4129
+ ...size === void 0 ? {} : { size },
4130
+ ...tiles === void 0 ? {} : { tiles },
4131
+ ...transparent ? { background: "transparent" } : {}
4132
+ });
4133
+ if (problem !== null) throw new CliError(problem.replace("ask for pixel art too", "add --pixel-art"));
4134
+ const tags = stringsOf(args, "tag").map(tagOf);
4135
+ const tagged = tagsProblem(tags);
4136
+ if (tagged !== null) throw new CliError(tagged);
4137
+ const fields = {
4138
+ ...pixelArt ? { pixelArt: true, tiles: tiles ?? DEFAULT_CONCEPT_TILES } : {},
4139
+ ...size === void 0 ? {} : { size },
4140
+ ...transparent ? { background: "transparent" } : {},
4141
+ ...tags.length === 0 ? {} : { tags }
4142
+ };
4143
+ const drawn = tiles ?? DEFAULT_CONCEPT_TILES;
4144
+ const shown2 = size ?? DEFAULT_CONCEPT_SIZE;
4145
+ const what = pixelArt ? `a concept art ${drawn.cols}×${drawn.rows} tiles across, as pixel art` : `a concept art ${shown2.width}×${shown2.height} px${transparent ? ", its background cut away" : ""}`;
4146
+ return { fields, words: `${what}${tags.length === 0 ? "" : ` tagged ${tags.join(", ")}`}: ${instruction}` };
4147
+ }
4021
4148
  const MAX_REFERENCE_BYTES = 16 * 1024 * 1024;
4022
4149
  function sizeOf(given, what) {
4023
4150
  const match = /^(\d{1,3})[x×](\d{1,3})$/i.exec(given.trim());
@@ -4049,6 +4176,10 @@ async function requestOf(context, asked, args) {
4049
4176
  const paths = stringsOf(args, "reference");
4050
4177
  if (paths.length > known.references)
4051
4178
  throw new CliError(known.references === 0 ? `${known.words} takes no reference` : `${known.words} takes at most ${known.references} references`);
4179
+ if (kind !== CONCEPT_ART) {
4180
+ for (const flag of ["pixel-art", "transparent"]) if (flagOf(args, flag)) throw new CliError(`--${flag} is a concept art's`);
4181
+ if (stringsOf(args, "tag").length > 0) throw new CliError("--tag is a concept art's");
4182
+ }
4052
4183
  if (kind === "ui-sketch") {
4053
4184
  for (const flag of ["names", "purpose", "size", "tiles"])
4054
4185
  if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is not a UI's: its shapes are in the --sketch file`);
@@ -4065,13 +4196,17 @@ async function requestOf(context, asked, args) {
4065
4196
  if (stringOf(args, "sketch") !== void 0) throw new CliError("--sketch is a UI's");
4066
4197
  if (set) {
4067
4198
  const { fields, words: words2 } = iconFields(args);
4068
- if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a level design's");
4199
+ if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
4069
4200
  const body2 = { ...fields, ...instruction === "" ? {} : { instruction } };
4070
4201
  return { kind, body: body2, words: instruction === "" ? words2 : `${words2}; ${instruction}` };
4071
4202
  }
4072
4203
  for (const flag of ["names", "purpose"]) if (stringOf(args, flag) !== void 0) throw new CliError(`--${flag} is an icon set's`);
4073
4204
  const references = [];
4074
4205
  for (const path of paths) references.push(await referenceOf(context, path));
4206
+ if (kind === CONCEPT_ART) {
4207
+ const { fields, words: words2 } = conceptFields(args, asked, instruction);
4208
+ return { kind, body: { instruction, ...fields }, ...references.length > 0 ? { images: { references } } : {}, words: words2 };
4209
+ }
4075
4210
  const body = { instruction };
4076
4211
  let words = `${known.words}: ${instruction}`;
4077
4212
  if (kind === "object") {
@@ -4080,15 +4215,8 @@ async function requestOf(context, asked, args) {
4080
4215
  if (problem !== null) throw new CliError(problem);
4081
4216
  body.footprint = footprint;
4082
4217
  words = `an object ${footprint.cols}×${footprint.rows} tiles: ${instruction}`;
4083
- } else if (stringOf(args, "size") !== void 0) throw new CliError("--size is an object's");
4084
- if (kind === "level-design") {
4085
- const tiles = sizeOf(stringOf(args, "tiles") ?? "6x4", "--tiles");
4086
- if (tiles.cols > MAX_DESIGN_TILES || tiles.rows > MAX_DESIGN_TILES || designCanvas(tiles) === null) {
4087
- throw new CliError(`a ${tiles.cols}×${tiles.rows} tile level design is not one that is drawn: 1 to ${MAX_DESIGN_TILES} tiles each way`);
4088
- }
4089
- body.tiles = tiles;
4090
- words = `a level design ${tiles.cols}×${tiles.rows} tiles across: ${instruction}`;
4091
- } else if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a level design's");
4218
+ } else if (stringOf(args, "size") !== void 0) throw new CliError("--size is an object's or a concept art's");
4219
+ if (stringOf(args, "tiles") !== void 0) throw new CliError("--tiles is a pixel-art concept art's");
4092
4220
  return { kind, body, ...references.length > 0 ? { images: { references } } : {}, words };
4093
4221
  }
4094
4222
  async function confirmed(context, platform, what, prices, kind, yes) {
@@ -4118,8 +4246,8 @@ async function followed(context, platform, jobId) {
4118
4246
  }
4119
4247
  const generate = {
4120
4248
  name: "generate",
4121
- summary: "Generate an object, a creature, a level design, icons or a UI on the platform, then pull it",
4122
- usage: "<object|creature|level-design|icons|ui> [--instruction <text>] [--size <c>x<r>|<px>] [--tiles <c>x<r>] [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--yes]",
4249
+ summary: "Generate an object, a creature, concept art, icons or a UI on the platform, then pull it",
4250
+ usage: "<object|creature|concept-art|icons|ui> [--instruction <text>] [--size <c>x<r>|<w>x<h>|<px>] [--transparent] [--pixel-art] [--tiles <c>x<r>] [--tag <tag>]… [--names <list>] [--purpose skill|item] [--sketch <json>] [--reference <png>]… [--yes]",
4123
4251
  options: {
4124
4252
  instruction: {
4125
4253
  type: "string",
@@ -4129,11 +4257,19 @@ const generate = {
4129
4257
  size: {
4130
4258
  type: "string",
4131
4259
  value: "<cols>x<rows>|<px>",
4132
- description: "an object's footprint in tiles, 1x1 unless given; an icon's size, 16, 32 or 64 px, 32 unless given"
4260
+ description: "an object's footprint in tiles, 1x1 unless given; a concept art's size in px, multiples of 16 within 1536x1024 or 1024x1536, 1536x1024 unless given; an icon's size, 16, 32 or 64 px, 32 unless given"
4261
+ },
4262
+ transparent: { type: "boolean", description: "cut a concept art's background away, for one thing alone" },
4263
+ "pixel-art": { type: "boolean", description: "draw a concept art as pixel art some --tiles across, a level design to take apart" },
4264
+ tag: {
4265
+ type: "string",
4266
+ multiple: true,
4267
+ value: "<tag>",
4268
+ description: "what a concept art is for, such as anchor, ui, character, environment, prop or level-design; at most 8"
4133
4269
  },
4134
4270
  names: { type: "string", value: "<list>", description: `an icon set's icons, comma-separated, at most ${MAX_ICONS}` },
4135
4271
  purpose: { type: "string", value: "skill|item", description: "whether an icon set is skills on badges or items alone; item unless given" },
4136
- tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a level design is across, 6x4 unless given" },
4272
+ tiles: { type: "string", value: "<cols>x<rows>", description: "how many tiles a pixel-art concept art is across, 6x4 unless given" },
4137
4273
  sketch: {
4138
4274
  type: "string",
4139
4275
  value: "<json>",
@@ -4616,7 +4752,7 @@ const move = {
4616
4752
  );
4617
4753
  }
4618
4754
  };
4619
- const version = "0.46.0";
4755
+ const version = "0.49.0";
4620
4756
  const manifest = {
4621
4757
  version
4622
4758
  };
@@ -5098,6 +5234,7 @@ const COMMANDS = [
5098
5234
  generate,
5099
5235
  list,
5100
5236
  pull,
5237
+ tag,
5101
5238
  seal,
5102
5239
  exportCommand,
5103
5240
  editor,