1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | const async = require("async");
|
9 | const path = require("path-extra");
|
10 | const fs = require("fs-extra");
|
11 | const utilities = require("extra-utilities");
|
12 | const jsonFormat = require("json-format");
|
13 | const ByteBuffer = require("bytebuffer");
|
14 | const Colour = require("colour-rgba");
|
15 | const Palette = require("duke3d-palette");
|
16 | const Tile = require("./tile.js");
|
17 |
|
18 | class ArtProperties {
|
19 | constructor() {
|
20 | let self = this;
|
21 |
|
22 | let _properties = {
|
23 | verbose: true,
|
24 | metadataFileExtension: Art.DefaultMetadataFileExtension
|
25 | };
|
26 |
|
27 | Object.defineProperty(self, "verbose", {
|
28 | enumerable: true,
|
29 | get() {
|
30 | return _properties.verbose;
|
31 | },
|
32 | set(value) {
|
33 | _properties.verbose = utilities.parseBoolean(value, false);
|
34 | }
|
35 | });
|
36 |
|
37 | Object.defineProperty(self, "metadataFileExtension", {
|
38 | enumerable: true,
|
39 | get() {
|
40 | return _properties.metadataFileExtension;
|
41 | },
|
42 | set(value) {
|
43 | let newValue = utilities.trimString(value);
|
44 |
|
45 | if(utilities.isEmptyString(newValue)) {
|
46 | throw new Error("Invalid metadata file extension, expected non-empty string.");
|
47 | }
|
48 |
|
49 | _properties.metadataFileExtension = newValue;
|
50 | }
|
51 | });
|
52 | }
|
53 | }
|
54 |
|
55 | class Art {
|
56 | constructor(legacyTileCount, localTileStart, localTileEnd, tiles, filePath) {
|
57 | let self = this;
|
58 |
|
59 | let _properties = {
|
60 | tiles: []
|
61 | };
|
62 |
|
63 | Object.defineProperty(self, "number", {
|
64 | get() {
|
65 | return Math.floor(_properties.localTileStart / Art.DefaultNumberOfTiles);
|
66 | }
|
67 | });
|
68 |
|
69 | Object.defineProperty(self, "localTileStart", {
|
70 | enumerable: true,
|
71 | get() {
|
72 | return _properties.localTileStart;
|
73 | },
|
74 | set(value) {
|
75 | const newValue = utilities.parseInteger(value);
|
76 |
|
77 | if(isNaN(newValue) || newValue < 0) {
|
78 | throw new TypeError("Invalid local tile start value: " + value + ", expected positive integer.");
|
79 | }
|
80 |
|
81 | _properties.localTileStart = localTileStart;
|
82 | }
|
83 | });
|
84 |
|
85 | Object.defineProperty(self, "localTileEnd", {
|
86 | enumerable: true,
|
87 | get() {
|
88 | return _properties.localTileEnd;
|
89 | },
|
90 | set(value) {
|
91 | const newValue = utilities.parseInteger(value);
|
92 |
|
93 | if(isNaN(newValue) || newValue < 0) {
|
94 | throw new TypeError("Invalid local tile end value: " + value + ", expected positive integer.");
|
95 | }
|
96 |
|
97 | _properties.localTileEnd = localTileEnd;
|
98 | }
|
99 | });
|
100 |
|
101 |
|
102 | Object.defineProperty(self, "legacyTileCount", {
|
103 | enumerable: true,
|
104 | get() {
|
105 | return _properties.legacyTileCount;
|
106 | },
|
107 | set(value) {
|
108 | const newValue = utilities.parseInteger(value);
|
109 |
|
110 | if(isNaN(newValue) || newValue < 0) {
|
111 | throw new TypeError("Invalid tile count value: " + value + ", expected positive integer.");
|
112 | }
|
113 |
|
114 | _properties.legacyTileCount = newValue;
|
115 | }
|
116 | });
|
117 |
|
118 | Object.defineProperty(self, "filePath", {
|
119 | enumerable: true,
|
120 | get() {
|
121 | return _properties.filePath;
|
122 | },
|
123 | set(filePath) {
|
124 | _properties.filePath = utilities.trimString(filePath);
|
125 | }
|
126 | });
|
127 |
|
128 | Object.defineProperty(self, "tiles", {
|
129 | enumerable: true,
|
130 | get() {
|
131 | return _properties.tiles;
|
132 | },
|
133 | set(value) {
|
134 | if(utilities.isNonEmptyArray(value)) {
|
135 | for(let i = 0; i < value.length; i++) {
|
136 | if(!Art.Tile.isTile(value[i])) {
|
137 | throw new TypeError("Invalid tile in tile list at index " + i + ".");
|
138 | }
|
139 | }
|
140 |
|
141 | _properties.tiles.length = 0;
|
142 | Array.prototype.push.apply(_properties.tiles, value);
|
143 | }
|
144 | }
|
145 | });
|
146 |
|
147 | Object.defineProperty(self, "data", {
|
148 | enumerable: true,
|
149 | get() {
|
150 | return self.serialize();
|
151 | }
|
152 | });
|
153 |
|
154 | self.legacyTileCount = legacyTileCount;
|
155 | self.localTileStart = localTileStart;
|
156 | self.localTileEnd = localTileEnd;
|
157 | self.tiles = tiles;
|
158 | self.filePath = filePath;
|
159 | }
|
160 |
|
161 | static isExtendedBy(artSubclass) {
|
162 | if(artSubclass instanceof Object) {
|
163 | return false;
|
164 | }
|
165 |
|
166 | let artSubclassPrototype = null;
|
167 |
|
168 | if(artSubclass instanceof Function) {
|
169 | artSubclassPrototype = artSubclass.prototype;
|
170 | }
|
171 | else {
|
172 | artSubclassPrototype = artSubclass.constructor.prototype;
|
173 | }
|
174 |
|
175 | return artSubclassPrototype instanceof Art;
|
176 | }
|
177 |
|
178 | static create(tileStartOffset, numberOfTiles) {
|
179 | const formattedTileStartOffset = utilities.parseInteger(tileStartOffset);
|
180 |
|
181 | if(isNaN(formattedTileStartOffset) || formattedTileStartOffset < 0) {
|
182 | formattedTileStartOffset = 0;
|
183 | }
|
184 |
|
185 | let formattedNumberOfTiles = utilities.parseInteger(numberOfTiles);
|
186 |
|
187 | if(isNaN(formattedNumberOfTiles) || formattedNumberOfTiles < 0) {
|
188 | formattedNumberOfTiles = Art.DefaultNumberOfTiles;
|
189 | }
|
190 |
|
191 | let tiles = [];
|
192 |
|
193 | for(let i = 0; i < formattedNumberOfTiles; i++) {
|
194 | tiles.push(new Art.Tile(formattedTileStartOffset + i, 0, 0, null, 0));
|
195 | }
|
196 |
|
197 | return new Art(0, formattedTileStartOffset, formattedTileStartOffset + formattedNumberOfTiles - 1, tiles);
|
198 | }
|
199 |
|
200 | getFileName() {
|
201 | let self = this;
|
202 |
|
203 | if(utilities.isEmptyString(self.filePath)) {
|
204 | return null;
|
205 | }
|
206 |
|
207 | return utilities.getFileName(self.filePath);
|
208 | }
|
209 |
|
210 | getExtension() {
|
211 | let self = this;
|
212 |
|
213 | return utilities.getFileExtension(self.filePath);
|
214 | }
|
215 |
|
216 | numberOfNonEmptyTiles() {
|
217 | let self = this;
|
218 |
|
219 | let tileCount = 0;
|
220 |
|
221 | for(let i = 0; i < self.tiles.length; i++) {
|
222 | if(!self.tiles[i].isEmpty()) {
|
223 | tileCount++;
|
224 | }
|
225 | }
|
226 |
|
227 | return tileCount;
|
228 | }
|
229 |
|
230 | numberOfEmptyTiles() {
|
231 | let self = this;
|
232 |
|
233 | let tileCount = 0;
|
234 |
|
235 | for(let i = 0; i < self.tiles.length; i++) {
|
236 | if(self.tiles[i].isEmpty()) {
|
237 | tileCount++;
|
238 | }
|
239 | }
|
240 |
|
241 | return tileCount;
|
242 | }
|
243 |
|
244 | getTileByNumber(tileNumber) {
|
245 | let self = this;
|
246 |
|
247 | tileNumber = utilities.parseInteger(tileNumber);
|
248 |
|
249 | if(isNaN(tileNumber)) {
|
250 | throw new Error("Invalid tile number: " + tileNumber + ", expected valid integer value.");
|
251 | }
|
252 |
|
253 | const tileIndex = tileNumber - self.localTileStart;
|
254 |
|
255 | if(tileIndex < 0 || tileIndex >= self.tiles.length) {
|
256 | throw new Error("Tile number " + tileNumber + " is out of range, expected integer number between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively.");
|
257 | }
|
258 |
|
259 | return self.tiles[tileIndex];
|
260 | }
|
261 |
|
262 | replaceTile(tile, tileNumber) {
|
263 | const self = this;
|
264 |
|
265 | if(!Art.Tile.isTile(tile)) {
|
266 | throw new Error("Cannot replace tile with invalid value!");
|
267 | }
|
268 |
|
269 | tileNumber = utilities.parseInteger(tileNumber);
|
270 |
|
271 | if(isNaN(tileNumber)) {
|
272 | tileNumber = tile.number;
|
273 | }
|
274 |
|
275 | const tileIndex = tileNumber - self.localTileStart;
|
276 |
|
277 | if(tileIndex < 0 || tileIndex >= self.tiles.length) {
|
278 | throw new Error(`Cannot replace tile #${tileNumber}, number must be within range of ${self.localTileStart} and ${self.localTileEnd}`);
|
279 | }
|
280 |
|
281 | const newTile = tile.clone();
|
282 | newTile.number = tileNumber;
|
283 |
|
284 | self.tiles[tileIndex] = newTile;
|
285 | }
|
286 |
|
287 | clearTile(tileNumber) {
|
288 | const self = this;
|
289 |
|
290 | tileNumber = utilities.parseInteger(tileNumber);
|
291 |
|
292 | if(isNaN(tileNumber)) {
|
293 | throw new Error("Invalid tile number: " + tileNumber + ", expected valid integer value.");
|
294 | }
|
295 |
|
296 | const tileIndex = tileNumber - self.localTileStart;
|
297 |
|
298 | if(tileIndex < 0 || tileIndex >= self.tiles.length) {
|
299 | throw new Error("Tile number " + tileNumber + " is out of range, expected integer number between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively.");
|
300 | }
|
301 |
|
302 | self.tiles[tileIndex].clear();
|
303 | }
|
304 |
|
305 | getTiles(includeEmpty) {
|
306 | let self = this;
|
307 |
|
308 | includeEmpty = utilities.parseBoolean(includeEmpty, false);
|
309 |
|
310 | let tiles = [];
|
311 |
|
312 | for(let i = 0; i < self.tiles.length; i++) {
|
313 | if(self.tiles[i].isEmpty() && !includeEmpty) {
|
314 | continue;
|
315 | }
|
316 |
|
317 | tiles.push(self.tiles[i]);
|
318 | }
|
319 |
|
320 | return tiles;
|
321 | }
|
322 |
|
323 | getNonEmptyTiles() {
|
324 | let self = this;
|
325 |
|
326 | let tiles = [];
|
327 |
|
328 | for(let i = 0; i < self.tiles.length; i++) {
|
329 | if(!self.tiles[i].isEmpty()) {
|
330 | tiles.push(self.tiles[i]);
|
331 | }
|
332 | }
|
333 |
|
334 | return tiles;
|
335 | }
|
336 |
|
337 | getEmptyTiles() {
|
338 | let self = this;
|
339 |
|
340 | let tiles = [];
|
341 |
|
342 | for(let i = 0; i < self.tiles.length; i++) {
|
343 | if(self.tiles[i].isEmpty()) {
|
344 | tiles.push(self.tiles[i]);
|
345 | }
|
346 | }
|
347 |
|
348 | return tiles;
|
349 | }
|
350 |
|
351 | compareTo(artFile) {
|
352 | const self = this;
|
353 |
|
354 | if(!Art.isArt(artFile)) {
|
355 | throw new Error("Cannot compare to invalid art file!");
|
356 | }
|
357 |
|
358 | const tileComparison = {
|
359 | new: [],
|
360 | modified: [],
|
361 | removed: [],
|
362 | attributesChanged: []
|
363 | };
|
364 |
|
365 | for(let i = 0; i < self.tiles.length; i++) {
|
366 | const tileA = self.tiles[i];
|
367 | const tileB = artFile.getTileByNumber(tileA.number);
|
368 | const tileAEmpty = tileA.isEmpty();
|
369 | const tileBEmpty = tileB.isEmpty();
|
370 |
|
371 | if(!tileAEmpty && tileBEmpty) {
|
372 | tileComparison.new.push(tileA);
|
373 | }
|
374 | else if(tileAEmpty && !tileBEmpty) {
|
375 | tileComparison.removed.push(tileB);
|
376 | }
|
377 | else if(!tileA.data.equals(tileB.data)) {
|
378 | tileComparison.modified.push(tileA);
|
379 | }
|
380 | else if(!tileA.attributes.equals(tileB.attributes)) {
|
381 | tileComparison.attributesChanged.push(tileA);
|
382 | }
|
383 | }
|
384 |
|
385 | return tileComparison;
|
386 | }
|
387 |
|
388 | getMetadata() {
|
389 | let self = this;
|
390 |
|
391 | let metadata = {
|
392 | number: self.number,
|
393 | count: self.legacyTileCount,
|
394 | start: self.localTileStart,
|
395 | end: self.localTileEnd,
|
396 | tiles: []
|
397 | };
|
398 |
|
399 | for(let i = 0; i < self.tiles.length; i++) {
|
400 | metadata.tiles.push(self.tiles[i].getMetadata());
|
401 | }
|
402 |
|
403 | return metadata;
|
404 | }
|
405 |
|
406 | static formatMetadata(metadata) {
|
407 | return utilities.formatValue(metadata, Art.MetadataFormat);
|
408 | }
|
409 |
|
410 | writeMetadata(filePath, overwrite, minify, fileName) {
|
411 | let self = this;
|
412 |
|
413 | overwrite = utilities.parseBoolean(overwrite, false);
|
414 | minify = utilities.parseBoolean(minify, false);
|
415 |
|
416 | if(utilities.isEmptyString(fileName)) {
|
417 | fileName = utilities.getFileName(filePath);
|
418 |
|
419 | if(utilities.isEmptyString(fileName)) {
|
420 | fileName = "TILES" + utilities.addLeadingZeroes(self.number, 3) + "." + Art.metadataFileExtension;
|
421 | }
|
422 | }
|
423 |
|
424 | const outputDirectory = utilities.getFilePath(filePath);
|
425 | const outputFilePath = utilities.joinPaths(outputDirectory, fileName);
|
426 |
|
427 | if(utilities.isNonEmptyString(outputDirectory)) {
|
428 | fs.ensureDirSync(outputDirectory);
|
429 | }
|
430 |
|
431 | const metadata = self.getMetadata();
|
432 |
|
433 | fs.writeFileSync(outputFilePath, minify ? JSON.stringify(metadata) : jsonFormat(metadata));
|
434 |
|
435 | return outputFilePath;
|
436 | }
|
437 |
|
438 | static readMetadata(filePath) {
|
439 | let self = this;
|
440 |
|
441 | return Art.formatMetadata(JSON.parse(fs.readFileSync(filePath)));
|
442 | }
|
443 |
|
444 | applyMetadata(metadata) {
|
445 | const formattedMetadata = Art.formatMetadata(metadata);
|
446 |
|
447 |
|
448 | }
|
449 |
|
450 | extractTileAtIndex(index, filePath, overwrite, palette, fileType, fileName, writeMetadata, minifyMetadata, callback) {
|
451 | let self = this;
|
452 |
|
453 | if(utilities.isFunction(writeMetadata)) {
|
454 | callback = writeMetadata;
|
455 | writeMetadata = true;
|
456 | minifyMetadata = null;
|
457 | }
|
458 | else if(utilities.isFunction(minifyMetadata)) {
|
459 | callback = minifyMetadata;
|
460 | minifyMetadata = null;
|
461 | }
|
462 |
|
463 | if(!utilities.isFunction(callback)) {
|
464 | throw new Error("Missing or invalid callback function!");
|
465 | }
|
466 |
|
467 | writeMetadata = utilities.parseBoolean(writeMetadata, true);
|
468 |
|
469 | const formattedIndex = utilities.parseInteger(index);
|
470 |
|
471 | if(isNaN(formattedIndex)) {
|
472 | return callback(new Error("Invalid tile index: " + index));
|
473 | }
|
474 |
|
475 | if(formattedIndex < 0 || formattedIndex >= self.tiles.length) {
|
476 | return callback(new Error("Tile index " + formattedIndex + " is out of range, expected value between 0 and " + (self.tiles.length - 1) + ", inclusively."))
|
477 | }
|
478 |
|
479 | if(writeMetadata) {
|
480 | try {
|
481 | self.writeMetadata(
|
482 | utilities.getFilePath(filePath) + "/",
|
483 | overwrite,
|
484 | minifyMetadata
|
485 | );
|
486 | }
|
487 | catch(error) {
|
488 | return callback(error);
|
489 | }
|
490 | }
|
491 |
|
492 | return self.tiles[formattedIndex].writeTo(
|
493 | filePath,
|
494 | overwrite,
|
495 | palette,
|
496 | fileType,
|
497 | fileName,
|
498 | function(error, filePath) {
|
499 | if(error) {
|
500 | return callback(error);
|
501 | }
|
502 |
|
503 | return callback(null, filePath, metadataFilePath);
|
504 | }
|
505 | );
|
506 | }
|
507 |
|
508 | extractTileByNumber(number, filePath, overwrite, palette, fileType, fileName, writeMetadata, minifyMetadata, callback) {
|
509 | let self = this;
|
510 |
|
511 | if(utilities.isFunction(writeMetadata)) {
|
512 | callback = writeMetadata;
|
513 | writeMetadata = true;
|
514 | minifyMetadata = null;
|
515 | }
|
516 | else if(utilities.isFunction(minifyMetadata)) {
|
517 | callback = minifyMetadata;
|
518 | minifyMetadata = null;
|
519 | }
|
520 |
|
521 | if(!utilities.isFunction(callback)) {
|
522 | throw new Error("Missing or invalid callback function!");
|
523 | }
|
524 |
|
525 | const formattedNumber = utilities.parseInteger(number);
|
526 |
|
527 | if(isNaN(formattedNumber)) {
|
528 | return callback(new Error("Invalid tile number: " + number));
|
529 | }
|
530 |
|
531 | const tileIndex = formattedNumber - self.localTileStart;
|
532 |
|
533 | if(tileIndex < 0 || tileIndex >= self.tiles.length) {
|
534 | return callback(new Error("Tile number " + formattedNumber + " is out of range, expected value between " + self.localTileStart + " and " + self.localTileEnd + ", inclusively."))
|
535 | }
|
536 |
|
537 | return self.extractTileAtIndex(tileIndex, filePath, overwrite, palette, fileType, fileName, writeMetadata, callback);
|
538 | }
|
539 |
|
540 | extractAllTiles(outputDirectory, overwrite, palette, fileType, writeMetadata, minifyMetadata, includeEmpty, callback) {
|
541 | let self = this;
|
542 |
|
543 | if(utilities.isFunction(writeMetadata)) {
|
544 | callback = writeMetadata;
|
545 | includeEmpty = false;
|
546 | minifyMetadata = null;
|
547 | writeMetadata = true;
|
548 | }
|
549 | else if(utilities.isFunction(minifyMetadata)) {
|
550 | callback = minifyMetadata;
|
551 | includeEmpty = false;
|
552 | minifyMetadata = null;
|
553 | }
|
554 | else if(utilities.isFunction(includeEmpty)) {
|
555 | callback = includeEmpty;
|
556 | includeEmpty = false;
|
557 | }
|
558 |
|
559 | if(!utilities.isFunction(callback)) {
|
560 | throw new Error("Missing or invalid callback function!");
|
561 | }
|
562 |
|
563 | return async.waterfall(
|
564 | [
|
565 | function(callback) {
|
566 | if(!writeMetadata) {
|
567 | return callback(null, null);
|
568 | }
|
569 |
|
570 | try {
|
571 | const metadataFilePath = self.writeMetadata(
|
572 | utilities.getFilePath(outputDirectory) + "/",
|
573 | overwrite,
|
574 | minifyMetadata
|
575 | );
|
576 |
|
577 | if(Art.verbose) {
|
578 | console.log("Saved ART file #" + self.number + " metadata to file: " + metadataFilePath);
|
579 | }
|
580 |
|
581 | return callback(null, metadataFilePath);
|
582 | }
|
583 | catch(error) {
|
584 | return callback(error);
|
585 | }
|
586 | },
|
587 | function(metadataFilePath, callback) {
|
588 | return async.concatSeries(
|
589 | self.getTiles(includeEmpty),
|
590 | function(tile, callback) {
|
591 | return tile.writeTo(
|
592 | outputDirectory,
|
593 | overwrite,
|
594 | palette,
|
595 | fileType,
|
596 | function(error, filePath) {
|
597 | if(error) {
|
598 | return callback(error);
|
599 | }
|
600 |
|
601 | if(Art.verbose) {
|
602 | console.log("Saved tile #" + tile.number + " image to file: " + filePath);
|
603 | }
|
604 |
|
605 | return callback(null, filePath);
|
606 | }
|
607 | );
|
608 | },
|
609 | function(error, filePaths) {
|
610 | if(error) {
|
611 | return callback(error);
|
612 | }
|
613 |
|
614 | if(Art.verbose) {
|
615 | console.log("Saved " + filePaths.length + " tile images from ART file #" + self.number + " to files at: " + outputDirectory);
|
616 | }
|
617 |
|
618 | return callback(null, filePaths, metadataFilePath);
|
619 | }
|
620 | );
|
621 | },
|
622 | ],
|
623 | function(error, filePaths, metadataFilePath) {
|
624 | if(error) {
|
625 | return callback(error);
|
626 | }
|
627 |
|
628 | return callback(null, filePaths, metadataFilePath);
|
629 | }
|
630 | );
|
631 | }
|
632 |
|
633 | getSize() {
|
634 | let self = this;
|
635 |
|
636 | let size = Art.HeaderSize + (self.tiles.length * 8);
|
637 |
|
638 | for(let i = 0; i < self.tiles.length; i++) {
|
639 | size += self.tiles[i].getSize();
|
640 | }
|
641 |
|
642 | return size;
|
643 | }
|
644 |
|
645 | serialize() {
|
646 | let self = this;
|
647 |
|
648 | let artByteBuffer = new ByteBuffer(self.getSize());
|
649 | artByteBuffer.order(true);
|
650 |
|
651 | artByteBuffer.writeInt32(Art.Version);
|
652 | artByteBuffer.writeInt32(self.legacyTileCount)
|
653 | artByteBuffer.writeInt32(self.localTileStart)
|
654 | artByteBuffer.writeInt32(self.localTileEnd);
|
655 |
|
656 | for(let i = 0; i < self.tiles.length; i++) {
|
657 | artByteBuffer.writeInt16(self.tiles[i].width);
|
658 | }
|
659 |
|
660 | for(let i = 0; i < self.tiles.length; i++) {
|
661 | artByteBuffer.writeInt16(self.tiles[i].height);
|
662 | }
|
663 |
|
664 | for(let i = 0; i < self.tiles.length; i++) {
|
665 | artByteBuffer.writeInt32(self.tiles[i].attributes.pack());
|
666 | }
|
667 |
|
668 | for(let i = 0; i < self.tiles.length; i++) {
|
669 | artByteBuffer.append(self.tiles[i].data);
|
670 | }
|
671 |
|
672 | artByteBuffer.flip();
|
673 |
|
674 | return artByteBuffer.toBuffer();
|
675 | }
|
676 |
|
677 | static deserialize(data) {
|
678 | let self = this;
|
679 |
|
680 | if(!Buffer.isBuffer(data)) {
|
681 | throw new Error("Invalid data, expected buffer.");
|
682 | }
|
683 |
|
684 | let artByteBuffer = new ByteBuffer();
|
685 | artByteBuffer.order(true);
|
686 | artByteBuffer.append(data, "binary");
|
687 | artByteBuffer.flip();
|
688 |
|
689 | if(artByteBuffer.remaining() < Art.HeaderSize) {
|
690 | throw new Error("Art file corrupted or invalid, missing full header data.");
|
691 | }
|
692 |
|
693 | const version = artByteBuffer.readInt32();
|
694 |
|
695 | if(!Number.isInteger(version)) {
|
696 | throw new Error("Invalid ART file version: " + version + ", expected a value of " + Art.Version + ".");
|
697 | }
|
698 |
|
699 | if(version !== Art.Version) {
|
700 | throw new Error("Unsupported ART file version: " + version + ", only version " + Art.Version + " is supported.");
|
701 | }
|
702 |
|
703 | const legacyTileCount = artByteBuffer.readInt32();
|
704 |
|
705 | if(!Number.isInteger(legacyTileCount) || legacyTileCount < 0) {
|
706 | throw new Error("Invalid tile count value: " + legacyTileCount + ", expected positive integer.");
|
707 | }
|
708 |
|
709 | const localTileStart = artByteBuffer.readInt32();
|
710 |
|
711 | if(!Number.isInteger(localTileStart) || localTileStart < 0) {
|
712 | throw new Error("Invalid local tile start value: " + localTileStart + ", expected positive integer.");
|
713 | }
|
714 |
|
715 | const localTileEnd = artByteBuffer.readInt32();
|
716 |
|
717 | if(!Number.isInteger(localTileEnd) || localTileEnd < 0) {
|
718 | throw new Error("Invalid local tile end value: " + localTileEnd + ", expected positive integer.");
|
719 | }
|
720 |
|
721 | if(localTileEnd < localTileStart) {
|
722 | throw new Error("Invalid local tile start / end values, start value: " + localTileStart + " should be greater than or equal to end value: " + localTileEnd + ".");
|
723 | }
|
724 |
|
725 | let numberOfTiles = localTileEnd - localTileStart + 1;
|
726 |
|
727 | if(artByteBuffer.remaining() < numberOfTiles * 8) {
|
728 | throw new Error("Art file corrupted or invalid, missing full sprite property data.");
|
729 | }
|
730 |
|
731 | let tileWidths = [];
|
732 |
|
733 | for(let i = 0; i < numberOfTiles; i++) {
|
734 | tileWidths.push(artByteBuffer.readInt16());
|
735 | }
|
736 |
|
737 | let tileHeights = [];
|
738 |
|
739 | for(let i = 0; i < numberOfTiles; i++) {
|
740 | tileHeights.push(artByteBuffer.readInt16());
|
741 | }
|
742 |
|
743 | let tileAttributes = [];
|
744 |
|
745 | for(let i = 0; i < numberOfTiles; i++) {
|
746 | tileAttributes.push(artByteBuffer.readInt32());
|
747 | }
|
748 |
|
749 | let tileData = [];
|
750 |
|
751 | for(let i = 0; i < numberOfTiles; i++) {
|
752 | const numberOfPixels = tileWidths[i] * tileHeights[i];
|
753 |
|
754 | if(artByteBuffer.remaining() < numberOfPixels) {
|
755 | throw new Error("Art file corrupted or invalid, missing sprite pixel data for tile #" + (localTileStart + i) + ".");
|
756 | }
|
757 |
|
758 | tileData.push(artByteBuffer.copy(artByteBuffer.offset, artByteBuffer.offset + numberOfPixels).toBuffer());
|
759 | artByteBuffer.skip(numberOfPixels);
|
760 | }
|
761 |
|
762 | let tiles = [];
|
763 |
|
764 | for(let i = 0; i < numberOfTiles; i++) {
|
765 | try {
|
766 | tiles.push(new Art.Tile(localTileStart + i, tileWidths[i], tileHeights[i], tileData[i], tileAttributes[i]));
|
767 | }
|
768 | catch(error) {
|
769 | error.message = "Failed to parse tile #" + (localTileStart + i) + " from: " + error.message;
|
770 | throw error;
|
771 | }
|
772 | }
|
773 |
|
774 | return new Art(legacyTileCount, localTileStart, localTileEnd, tiles);
|
775 | }
|
776 |
|
777 | static readFrom(filePath) {
|
778 | if(utilities.isEmptyString(filePath)) {
|
779 | throw new Error("Missing or invalid art file path.");
|
780 | }
|
781 |
|
782 | if(Art.verbose) {
|
783 | console.log("Reading ART file: " + filePath);
|
784 | }
|
785 |
|
786 | let data = null;
|
787 |
|
788 | try {
|
789 | data = fs.readFileSync(filePath);
|
790 | }
|
791 | catch(error) {
|
792 | if(error.code === "ENOENT") {
|
793 | throw new Error("Art file does not exist!");
|
794 | }
|
795 | else if(error.code === "EISDIR") {
|
796 | throw new Error("Art file path is not a file!");
|
797 | }
|
798 | else {
|
799 | throw new Error("Failed to read art file with error code: " + error.code);
|
800 | }
|
801 | }
|
802 |
|
803 | let art = Art.deserialize(data);
|
804 | art.filePath = filePath;
|
805 |
|
806 | if(Art.verbose) {
|
807 | console.log("Processed ART file: " + art.getFileName() + " (Start: " + art.localTileStart + ", End: " + art.localTileEnd + ", Number of Tiles: " + art.tiles.length + ", Non-Empty: " + art.numberOfNonEmptyTiles() + ")");
|
808 | }
|
809 |
|
810 | return art;
|
811 | }
|
812 |
|
813 | writeTo(filePath) {
|
814 | let self = this;
|
815 |
|
816 | if(utilities.isEmptyString(filePath)) {
|
817 | throw new Error("Must specify file path to save to.");
|
818 | }
|
819 |
|
820 | const outputDirectory = utilities.getFilePath(filePath);
|
821 |
|
822 | if(utilities.isNonEmptyString(outputDirectory)) {
|
823 | fs.ensureDirSync(outputDirectory);
|
824 | }
|
825 |
|
826 | if(Art.verbose) {
|
827 | console.log("Writing ART file #" + self.number + " to file: " + filePath);
|
828 | }
|
829 |
|
830 | fs.writeFileSync(filePath, self.serialize());
|
831 |
|
832 | return filePath;
|
833 | }
|
834 |
|
835 | clone() {
|
836 | const self = this;
|
837 |
|
838 | return new Art(self.legacyTileCount, self.localTileStart, self.localTileEnd, self.tiles.map(function(tile) { return tile.clone(); }), self.filePath);
|
839 | }
|
840 |
|
841 | static isArt(value) {
|
842 | return value instanceof Art;
|
843 | }
|
844 | }
|
845 |
|
846 | Object.defineProperty(Art, "Version", {
|
847 | value: 1,
|
848 | enumerable: true
|
849 | });
|
850 |
|
851 | Object.defineProperty(Art, "DefaultNumberOfTiles", {
|
852 | value: 256,
|
853 | enumerable: true
|
854 | });
|
855 |
|
856 | Object.defineProperty(Art, "HeaderSize", {
|
857 | value: 4 * 4,
|
858 | enumerable: true
|
859 | });
|
860 |
|
861 | Object.defineProperty(Art, "DefaultMetadataFileExtension", {
|
862 | value: "JSON",
|
863 | enumerable: true
|
864 | });
|
865 |
|
866 | Object.defineProperty(Art, "MetadataFormat", {
|
867 | value: {
|
868 | type: "object",
|
869 | strict: true,
|
870 | removeExtra: true,
|
871 | order: true,
|
872 | nonEmpty: true,
|
873 | required: true,
|
874 | format: {
|
875 | number: {
|
876 | type: "integer",
|
877 | validator: function(value) {
|
878 | return value >= 0;
|
879 | }
|
880 | },
|
881 | count: {
|
882 | type: "integer",
|
883 | validator: function(value) {
|
884 | return value >= 0;
|
885 | }
|
886 | },
|
887 | start: {
|
888 | type: "integer",
|
889 | validator: function(value) {
|
890 | return value >= 0;
|
891 | }
|
892 | },
|
893 | end: {
|
894 | type: "integer",
|
895 | validator: function(value) {
|
896 | return value >= 0;
|
897 | }
|
898 | },
|
899 | tiles: {
|
900 | type: "array",
|
901 | required: true,
|
902 | format: {
|
903 | type: "object",
|
904 | strict: true,
|
905 | removeExtra: true,
|
906 | order: true,
|
907 | nonEmpty: true,
|
908 | required: true,
|
909 | format: {
|
910 | number: {
|
911 | type: "integer",
|
912 | validator: function(value) {
|
913 | return value >= 0;
|
914 | }
|
915 | },
|
916 | attributes: {
|
917 | type: "object",
|
918 | strict: true,
|
919 | removeExtra: true,
|
920 | order: true,
|
921 | nonEmpty: true,
|
922 | required: true,
|
923 | format: {
|
924 | offset: {
|
925 | type: "object",
|
926 | strict: true,
|
927 | removeExtra: true,
|
928 | order: true,
|
929 | nonEmpty: true,
|
930 | required: true,
|
931 | format: {
|
932 | x: {
|
933 | type: "integer",
|
934 | validator: function(value) {
|
935 | return value >= Art.Tile.Attributes.Attribute.XOffset.min && value <= Art.Tile.Attributes.Attribute.XOffset.max;
|
936 | }
|
937 | },
|
938 | y: {
|
939 | type: "integer",
|
940 | validator: function(value) {
|
941 | return value >= Art.Tile.Attributes.Attribute.YOffset.min && value <= Art.Tile.Attributes.Attribute.YOffset.max;
|
942 | }
|
943 | }
|
944 | }
|
945 | },
|
946 | numberOfFrames: {
|
947 | type: "integer",
|
948 | validator: function(value) {
|
949 | return value >= Art.Tile.Attributes.Attribute.NumberOfFrames.min && value <= Art.Tile.Attributes.Attribute.NumberOfFrames.max;
|
950 | }
|
951 | },
|
952 | animation: {
|
953 | type: "object",
|
954 | strict: true,
|
955 | removeExtra: true,
|
956 | order: true,
|
957 | nonEmpty: true,
|
958 | required: true,
|
959 | format: {
|
960 | type: {
|
961 | type: "string",
|
962 | case: "title",
|
963 | trim: true,
|
964 | nonEmpty: true,
|
965 | required: true,
|
966 | validator: function(value) {
|
967 | for(let i = 0; i < Art.Tile.AnimationType.Types.length; i++) {
|
968 | if(value === Art.Tile.AnimationType.Types[i].name) {
|
969 | return true;
|
970 | }
|
971 | }
|
972 |
|
973 | return false;
|
974 | }
|
975 | },
|
976 | speed: {
|
977 | type: "integer",
|
978 | validator: function(value) {
|
979 | return value >= Art.Tile.Attributes.Attribute.AnimationSpeed.min && value <= Art.Tile.Attributes.Attribute.AnimationSpeed.max;
|
980 | }
|
981 | }
|
982 | }
|
983 | },
|
984 | extra: {
|
985 | type: "integer",
|
986 | validator: function(value) {
|
987 | return value >= Art.Tile.Attributes.Attribute.Extra.min && value <= Art.Tile.Attributes.Attribute.Extra.max;
|
988 | }
|
989 | }
|
990 | }
|
991 | }
|
992 | }
|
993 | }
|
994 | }
|
995 | }
|
996 | },
|
997 | enumerable: true
|
998 | });
|
999 |
|
1000 | Object.defineProperty(Art, "properties", {
|
1001 | value: new ArtProperties(),
|
1002 | enumerable: false
|
1003 | });
|
1004 |
|
1005 | Object.defineProperty(Art, "verbose", {
|
1006 | enumerable: true,
|
1007 | get() {
|
1008 | return Art.properties.verbose;
|
1009 | },
|
1010 | set(value) {
|
1011 | Art.properties.verbose = value;
|
1012 | }
|
1013 | });
|
1014 |
|
1015 | Object.defineProperty(Art, "metadataFileExtension", {
|
1016 | enumerable: true,
|
1017 | get() {
|
1018 | return Art.properties.metadataFileExtension;
|
1019 | },
|
1020 | set(value) {
|
1021 | Art.properties.metadataFileExtension = value;
|
1022 | }
|
1023 | });
|
1024 |
|
1025 | Object.defineProperty(Art, "Colour", {
|
1026 | value: Colour,
|
1027 | enumerable: true
|
1028 | });
|
1029 |
|
1030 | Object.defineProperty(Art, "Palette", {
|
1031 | value: Palette,
|
1032 | enumerable: true
|
1033 | });
|
1034 |
|
1035 | Object.defineProperty(Art, "Tile", {
|
1036 | value: Tile,
|
1037 | enumerable: true
|
1038 | });
|
1039 |
|
1040 | module.exports = Art;
|