193 lines
7.4 KiB
JavaScript
193 lines
7.4 KiB
JavaScript
import * as util from './format-util.js';
|
|
import { TILE_TYPES, CC2_TILE_TYPES } from './tiletypes.js';
|
|
|
|
// Decompress the little ad-hoc compression scheme used for both map data and
|
|
// solution playback
|
|
function decompress(buf) {
|
|
let decompressed_length = new DataView(buf).getUint16(0, true);
|
|
let out = new ArrayBuffer(decompressed_length);
|
|
let outbytes = new Uint8Array(out);
|
|
let bytes = new Uint8Array(buf);
|
|
let p = 2;
|
|
let q = 0;
|
|
while (p < buf.byteLength) {
|
|
let len = bytes[p];
|
|
p++;
|
|
if (len < 0x80) {
|
|
// Data block
|
|
outbytes.set(new Uint8Array(buf.slice(p, p + len)), q);
|
|
p += len;
|
|
q += len;
|
|
}
|
|
else {
|
|
// Back-reference block
|
|
len -= 0x80;
|
|
let offset = bytes[p];
|
|
p++;
|
|
// Can't use set + slice here because the copy can overlap and that
|
|
// doesn't work so great, so just do a regular loop and let the JIT
|
|
// deal with it
|
|
let start = q - offset;
|
|
for (let i = 0; i < len; i++) {
|
|
outbytes[q] = outbytes[start + i];
|
|
q++;
|
|
}
|
|
}
|
|
}
|
|
if (q !== decompressed_length)
|
|
throw new Error(`Expected to decode ${decompressed_length} bytes but got ${q} instead`);
|
|
return out;
|
|
}
|
|
|
|
export function parse_level(buf) {
|
|
let level = new util.StoredLevel;
|
|
let full_view = new DataView(buf);
|
|
let next_section_start = 0;
|
|
while (next_section_start < buf.byteLength) {
|
|
// Read section header and length
|
|
let section_start = next_section_start;
|
|
let section_type = util.string_from_buffer_ascii(buf.slice(section_start, section_start + 4));
|
|
let section_length = full_view.getUint32(section_start + 4, true);
|
|
next_section_start = section_start + 8 + section_length;
|
|
if (next_section_start > buf.byteLength)
|
|
throw new Error(`Section at byte ${section_start} of type '${section_type}' extends ${buf.length - next_section_start} bytes past the end of the file`);
|
|
|
|
if (section_type === 'CC2M' || section_type === 'LOCK' || section_type === 'TITL' || section_type === 'AUTH' || section_type === 'VERS' || section_type === 'CLUE' || section_type === 'NOTE') {
|
|
// These are all singular strings (with a terminating NUL, for some reason)
|
|
// XXX character encoding??
|
|
// FIXME assign to appropriate fields
|
|
let field = section_type;
|
|
if (section_type === 'TITL') {
|
|
field = 'title';
|
|
}
|
|
else if (section_type === 'AUTH') {
|
|
field = 'author';
|
|
}
|
|
/*
|
|
else if (section_type === 'CLUE') {
|
|
field = 'hint';
|
|
}
|
|
*/
|
|
level[field] = util.string_from_buffer_ascii(buf.slice(section_start + 8, next_section_start - 1)).replace(/\r\n/g, "\n");
|
|
continue;
|
|
}
|
|
|
|
let section_buf = buf.slice(section_start + 8, next_section_start);
|
|
let section_view = new DataView(buf, section_start + 8, section_length);
|
|
|
|
if (section_type === 'OPTN') {
|
|
// Level options, which may be truncated at any point
|
|
// TODO implement most of these
|
|
level.time_limit = section_view.getUint16(0, true);
|
|
|
|
// TODO 0 - 10x10, 1 - 9x9, 2 - split, otherwise unknown which needs handling
|
|
let viewport = section_view.getUint8(2, true);
|
|
if (viewport === 0) {
|
|
level.viewport_size = 10;
|
|
}
|
|
else if (viewport === 1) {
|
|
level.viewport_size = 9;
|
|
}
|
|
else if (viewport === 2) {
|
|
// FIXME this is split
|
|
level.viewport_size = 10;
|
|
}
|
|
else {
|
|
throw new Error(`Unrecognized viewport size option ${viewport}`);
|
|
}
|
|
|
|
if (section_view.byteLength <= 3)
|
|
continue;
|
|
//options.has_solution = section_view.getUint8(3, true);
|
|
|
|
if (section_view.byteLength <= 4)
|
|
continue;
|
|
//options.show_map_in_editor = section_view.getUint8(4, true);
|
|
|
|
if (section_view.byteLength <= 5)
|
|
continue;
|
|
//options.is_editable = section_view.getUint8(5, true);
|
|
|
|
if (section_view.byteLength <= 6)
|
|
continue;
|
|
//options.solution_hash = util.string_from_buffer_ascii(buf.slice(
|
|
//section_start + 6, section_start + 22));
|
|
|
|
if (section_view.byteLength <= 22)
|
|
continue;
|
|
//options.hide_logic = section_view.getUint8(22, true);
|
|
|
|
if (section_view.byteLength <= 23)
|
|
continue;
|
|
level.use_cc1_boots = section_view.getUint8(23, true);
|
|
|
|
if (section_view.byteLength <= 24)
|
|
continue;
|
|
//level.blob_behavior = section_view.getUint8(24, true);
|
|
}
|
|
else if (section_type === 'MAP ' || section_type === 'PACK') {
|
|
let data = section_buf;
|
|
if (section_type === 'PACK') {
|
|
data = decompress(data);
|
|
}
|
|
let bytes = new Uint8Array(data);
|
|
let width = bytes[0];
|
|
let height = bytes[1];
|
|
level.size_x = width;
|
|
level.size_y = height;
|
|
let p = 2;
|
|
for (let n = 0; n < width * height; n++) {
|
|
let cell = new util.StoredCell;
|
|
while (true) {
|
|
let tile_byte = bytes[p];
|
|
p++;
|
|
let tile_name = CC2_TILE_TYPES[tile_byte];
|
|
if (! tile_name)
|
|
throw new Error(`Unrecognized tile type 0x${tile_byte.toString(16)}`);
|
|
|
|
let tile = {name: tile_name};
|
|
cell.push(tile);
|
|
let tiledef = TILE_TYPES[tile_name];
|
|
if (tiledef.is_required_chip) {
|
|
level.chips_required++;
|
|
}
|
|
if (tiledef.is_player) {
|
|
// TODO handle multiple starts
|
|
level.player_start_x = n % width;
|
|
level.player_start_y = Math.floor(n / width);
|
|
}
|
|
if (tiledef.has_direction) {
|
|
let dirbyte = bytes[p];
|
|
p++;
|
|
let direction = ['north', 'east', 'south', 'west'][dirbyte];
|
|
if (! direction) {
|
|
console.warn(`'${tile_name}' tile at ${n % width}, ${Math.floor(n / width)} has bogus direction byte ${dirbyte}; defaulting to south`);
|
|
direction = 'south';
|
|
}
|
|
tile.direction = direction;
|
|
}
|
|
if (! tiledef.is_top_layer)
|
|
break;
|
|
}
|
|
level.linear_cells.push(cell);
|
|
}
|
|
}
|
|
else if (section_type === 'KEY ') {
|
|
}
|
|
else if (section_type === 'REPL') {
|
|
}
|
|
else if (section_type === 'PRPL') {
|
|
}
|
|
else if (section_type === 'RDNY') {
|
|
}
|
|
else if (section_type === 'END ') {
|
|
}
|
|
else {
|
|
console.warn(`Unrecognized section type '${section_type}' at offset ${section_start}`);
|
|
// TODO save it, persist when editing level
|
|
}
|
|
}
|
|
console.log(level);
|
|
return level;
|
|
}
|