Files
CDJ900ModsWeb/builder/builder.js
T
Thiemo TizianiandClaude Opus 5.5 e4728b16ea Builder, upcoming page, mods page for the current mods
- builder.html: build C900NXS.UPD in the browser (official 1.31 update in,
  mods ticked with their dependencies, no-liability and Claude Code
  confirmations, download). Ports of upd.py, the LZSS packer and the mods'
  application; the result is checked against the SHA-256 the Python
  patcher gives and is byte for byte the patcher's file.
- builder/data.js from tools/make_builder_data.py: the mods' bytes per
  combination; firmware parts (UTILITY table, HOT KEYS box, the asset
  blob's resource table) are made from the user's own update.
- index.html: rgb_waveform, extended_bpm, phasemeter, cuecolours,
  hotcuekeys (experimental), ledsaver, credit; stable/experimental and a
  green "ran on a player" dot.
- upcoming.html: beat jump, stems, uniform Camelot / Open Key, 3-band
  waveform.
- docs.html: the builder, the new mod names, hot cue loading and locks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:21:47 +02:00

501 lines
24 KiB
JavaScript

// SPDX-License-Identifier: GPL-2.0-or-later
// CDJ-900NXS mods builder, in the browser: the official 1.31 C900NXS.UPD in,
// a new one with the chosen mods out. Nothing is uploaded or downloaded.
//
// Ports of the patcher's upd.py (update file), the emulator checkout's
// lzss_encode.py (packer) and patch_cdj900nxs.py's asset blob; the mods'
// bytes are in builder/data.js (tools/make_builder_data.py), with the
// SHA-256 of the finished application for every combination.
(function () {
'use strict';
var D = window.BUILDER_DATA;
var BASE = 0x08000000;
var APP_START = 0x40000, APP_END = 0x7E0000, RECORD_DATA = 32;
var ADDRESS_BYTES = { 0: 2, 1: 2, 2: 3, 3: 4, 5: 2, 6: 3, 7: 4, 8: 3, 9: 2 };
var LABEL = '1.32';
var UPSTREAM = ['extended_bpm', 'phasemeter', 'rgb_waveform'];
var OURS = ['cuecolours', 'hotcuekeys', 'ledsaver', 'credit'];
function fail(msg) { throw new Error(msg); }
// ---------- bytes ----------
function hex(u8) {
var s = '';
for (var i = 0; i < u8.length; i++) s += (u8[i] < 16 ? '0' : '') + u8[i].toString(16);
return s;
}
function fromHex(s) {
var out = new Uint8Array(s.length >> 1);
for (var i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
return out;
}
function fromB64(s) {
var bin = atob(s), out = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function equal(a, b) {
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
function u16(b, o) { return b[o] | b[o + 1] << 8; }
function u32(b, o) { return (b[o] | b[o + 1] << 8 | b[o + 2] << 16 | b[o + 3] << 24) >>> 0; }
function put16(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; }
function put32(b, o, v) { b[o] = v & 255; b[o + 1] = v >> 8 & 255; b[o + 2] = v >> 16 & 255; b[o + 3] = v >>> 24 & 255; }
function grow(b, size) {
if (b.length >= size) return b;
var out = new Uint8Array(size);
out.set(b);
return out;
}
function latin1(u8) {
var s = '';
for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
return s;
}
function ascii(s) {
var out = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
return out;
}
function sha256(u8) {
return crypto.subtle.digest('SHA-256', u8).then(function (h) { return hex(new Uint8Array(h)); });
}
// CRC-16/XMODEM (Python's binascii.crc_hqx with 0)
var CRC_TABLE = (function () {
var t = new Uint16Array(256);
for (var i = 0; i < 256; i++) {
var c = i << 8;
for (var k = 0; k < 8; k++) c = c & 0x8000 ? (c << 1 ^ 0x1021) & 0xFFFF : c << 1 & 0xFFFF;
t[i] = c;
}
return t;
})();
function crc16(u8, start, end) {
var c = 0;
for (var i = start; i < end; i++) c = (c << 8 & 0xFFFF) ^ CRC_TABLE[(c >> 8 ^ u8[i]) & 255];
return c;
}
// ---------- the update file (upd.py) ----------
var HEADER = /^CDJ-900NXS (DRIV|MAIN|PANL)Ver(\d\.\d\d)/;
function parseSection(blob) {
var n = blob.length;
if (n < 36) fail('a section is truncated');
if (crc16(blob, 0, n - 2) !== u16(blob, n - 2)) fail('a section fails its CRC');
var header = blob.subarray(0, 32), m = HEADER.exec(latin1(header));
if (!m) fail('unexpected section header');
var sec = { blob: blob, header: header, name: m[1], version: m[2], records: [] };
var end = n - 2;
if (blob[end - 2] !== 13 || blob[end - 1] !== 10) fail(sec.name + ': the S-records do not end in a line break');
var pos = 32, line = 0;
while (pos < end) {
var e = pos;
while (!(blob[e] === 13 && blob[e + 1] === 10)) e++;
line++;
if (blob[pos] !== 0x53 || e - pos < 4 || (e - pos) % 2) fail(sec.name + ': S-record ' + line + ' is malformed');
var kind = blob[pos + 1] - 48;
if (!(kind in ADDRESS_BYTES)) fail(sec.name + ': S-record ' + line + ' has type ' + kind);
var len = (e - pos - 2) >> 1, raw = new Uint8Array(len), sum = 0;
for (var i = 0; i < len; i++) {
var h = blob[pos + 2 + i * 2], l = blob[pos + 3 + i * 2];
var v = (h <= 57 ? h - 48 : h - 55) << 4 | (l <= 57 ? l - 48 : l - 55);
raw[i] = v; sum += v;
}
var width = ADDRESS_BYTES[kind];
if (len !== raw[0] + 1 || len < width + 2 || (sum & 255) !== 255) fail(sec.name + ': S-record ' + line + ' fails its length or checksum');
var addr = 0;
for (var k = 0; k < width; k++) addr = addr * 256 + raw[1 + k];
sec.records.push({ kind: kind, address: addr, data: raw.subarray(1 + width, len - 1) });
pos = e + 2;
}
if (sec.records[0].kind !== 0 || [7, 8, 9].indexOf(sec.records[sec.records.length - 1].kind) < 0) fail(sec.name + ': no S-record header or end');
return sec;
}
function dataRecords(sec) { return sec.records.filter(function (r) { return r.kind >= 1 && r.kind <= 3; }); }
function flashOf(sec, size) {
size = size || APP_END;
var img = new Uint8Array(size).fill(0xFF);
dataRecords(sec).forEach(function (r) {
if (r.address + r.data.length > size) fail(sec.name + ': a record reaches past 0x' + size.toString(16));
img.set(r.data, r.address);
});
return img;
}
function parseUpd(raw) {
var pos = 0, sizes = [];
for (var s = 0; s < 3; s++) {
var e = pos;
while (e < raw.length && !(raw[e] === 13 && raw[e + 1] === 10)) e++;
var t = latin1(raw.subarray(pos, e));
if (!/^\d+$/.test(t)) fail('the file does not start with three section lengths');
sizes.push(parseInt(t, 10));
pos = e + 2;
}
if (pos + sizes[0] + sizes[1] + sizes[2] !== raw.length) fail('the section lengths do not add up to the file');
var secs = sizes.map(function (n) { var b = raw.subarray(pos, pos + n); pos += n; return parseSection(b); });
if (secs.map(function (x) { return x.name; }).join() !== 'DRIV,MAIN,PANL') fail('the sections are not DRIV, MAIN, PANL');
return secs;
}
function unpackLzss(stream) {
var ring = new Uint8Array(4096).fill(0x20);
ring.fill(0, 4078);
var r = 4078, out = new Uint8Array(Math.max(1 << 20, stream.length * 3)), o = 0, pos = 0, n = stream.length;
function room(k) { if (o + k > out.length) { var b = new Uint8Array(out.length * 2); b.set(out); out = b; } }
while (pos < n) {
var flags = stream[pos++];
for (var bit = 0; bit < 8; bit++) {
if (pos >= n) break;
if (flags >> bit & 1) {
room(1);
var b1 = stream[pos++];
out[o++] = b1; ring[r] = b1; r = r + 1 & 0xFFF;
} else {
if (pos + 2 > n) fail('the packed application ends inside a match');
var lo = stream[pos], hi = stream[pos + 1];
pos += 2;
var at = lo | (hi & 0xF0) << 4, count = (hi & 0x0F) + 3;
room(count);
for (var k = 0; k < count; k++) {
var b2 = ring[at + k & 0xFFF];
out[o++] = b2; ring[r] = b2; r = r + 1 & 0xFFF;
}
}
}
}
return out.slice(0, o);
}
function appRegion(flash) {
var size = u32(flash, APP_START), end = APP_START + 4 + size;
if (end + 2 > APP_END) fail('the packed application runs past 0x' + APP_END.toString(16));
var sum = 0;
for (var i = APP_START; i < end; i++) sum += flash[i];
if (u16(flash, end) !== (sum & 0xFFFF)) fail('the packed application fails its additive checksum');
return { app: unpackLzss(flash.subarray(APP_START + 4, end)), region: flash.subarray(APP_START, end + 2),
stream: flash.subarray(APP_START + 4, end) };
}
// The official update, checked to be exactly the 1.31 file.
function readStock(raw) {
return sha256(raw).then(function (h) {
if (h !== D.stockUpdSha256) fail('This is not the original CDJ-900NXS 1.31 update file (C900NXS.UPD). Its SHA-256 is ' + h + '.');
var secs = parseUpd(raw), main = secs[1];
if (main.version !== '1.31' || main.header[31] !== 0x30) fail('MAIN is not 1.31 in application-only mode');
main.records.forEach(function (r) { if ([0, 2, 7].indexOf(r.kind) < 0) fail('MAIN has unexpected record types'); });
var flash = flashOf(main), reg = appRegion(flash);
return sha256(reg.app).then(function (ah) {
if (reg.app.length !== D.appSize || ah !== D.stockAppSha256) fail('the 1.31 application inside does not match');
var boot = dataRecords(main).filter(function (r) { return r.address < APP_START; });
boot.forEach(function (r) { if (r.address + r.data.length > APP_START) fail('a stock record crosses into the application'); });
return { raw: raw, sections: secs, flash: flash, app: reg.app, stream: reg.stream, boot: boot };
});
});
}
function encodeRecord(r) {
var width = ADDRESS_BYTES[r.kind], body = new Uint8Array(width + r.data.length + 2), sum = 0;
body[0] = width + r.data.length + 1;
for (var k = 0; k < width; k++) body[1 + k] = Math.floor(r.address / Math.pow(256, width - 1 - k)) & 255;
body.set(r.data, 1 + width);
for (var i = 0; i < body.length - 1; i++) sum += body[i];
body[body.length - 1] = ~sum & 255;
return 'S' + r.kind + hex(body).toUpperCase() + '\r\n';
}
function relabel(header, version) {
var s = latin1(header), m = HEADER.exec(s);
var old = m[0], neu = old.slice(0, -4) + version;
var out = s.replace(old, neu);
if (out.slice(0, old.length - 4) !== s.slice(0, old.length - 4) || out.slice(old.length) !== s.slice(old.length)) fail('relabelling would change more than the version');
return ascii(out);
}
function encodeSection(header, records) {
var parts = [header];
var text = '';
for (var i = 0; i < records.length; i++) {
text += encodeRecord(records[i]);
if (text.length > 1 << 20) { parts.push(ascii(text)); text = ''; }
}
parts.push(ascii(text));
var n = parts.reduce(function (a, p) { return a + p.length; }, 0), body = new Uint8Array(n + 2), o = 0;
parts.forEach(function (p) { body.set(p, o); o += p.length; });
put16(body, n, crc16(body, 0, n));
return body;
}
function join(blobs) {
var head = ascii(blobs.map(function (b) { return b.length + '\r\n'; }).join(''));
var n = head.length + blobs.reduce(function (a, b) { return a + b.length; }, 0), out = new Uint8Array(n), o = head.length;
out.set(head);
blobs.forEach(function (b) { out.set(b, o); o += b.length; });
return out;
}
function packRegion(stream) {
var n = 4 + stream.length + 2, pad = (RECORD_DATA - n % RECORD_DATA) % RECORD_DATA;
var out = new Uint8Array(n + pad).fill(0xFF);
put32(out, 0, stream.length);
out.set(stream, 4);
var sum = 0;
for (var i = 0; i < 4 + stream.length; i++) sum += out[i];
put16(out, 4 + stream.length, sum & 0xFFFF);
return out;
}
function buildUpd(stock, stream) {
var main = stock.sections[1], reg = packRegion(stream);
if (APP_START + reg.length > APP_END) fail('the packed application needs ' + reg.length + ' bytes, the flash has ' + (APP_END - APP_START));
var records = [main.records[0]].concat(stock.boot);
for (var off = 0; off < reg.length; off += RECORD_DATA) records.push({ kind: 2, address: APP_START + off, data: reg.subarray(off, off + RECORD_DATA) });
records.push(main.records[main.records.length - 1]);
var blob = encodeSection(relabel(main.header, LABEL), records);
return join([stock.sections[0].blob, blob, stock.sections[2].blob]);
}
function sameRecord(a, b) { return a.kind === b.kind && a.address === b.address && equal(a.data, b.data); }
// Every check on the finished file, as upd.verify().
function verify(raw, stock, app) {
var done = [], secs = parseUpd(raw);
done.push('section lengths, CRC-16 of every section, every S-record checksum');
if (!equal(secs[0].blob, stock.sections[0].blob) || !equal(secs[2].blob, stock.sections[2].blob)) fail('DRIV or PANL differs from stock');
done.push('DRIV and PANL byte for byte as stock');
var main = secs[1], smain = stock.sections[1];
if (!equal(main.header, relabel(smain.header, LABEL))) fail('the MAIN header differs beyond its version label');
if (main.header[31] !== 0x30) fail('the MAIN header no longer selects application-only mode');
done.push('MAIN header as stock but for the label ' + LABEL + '; application-only mode kept');
if (!sameRecord(main.records[0], smain.records[0]) || !sameRecord(main.records[main.records.length - 1], smain.records[smain.records.length - 1])) fail('the S0 header or S7 entry record changed');
done.push('S0 header record and S7 entry record as stock');
var low = dataRecords(main).filter(function (r) { return r.address < APP_START; });
if (low.length !== stock.boot.length || low.some(function (r, i) { return !sameRecord(r, stock.boot[i]); })) fail('the boot loader or recovery records changed');
var flash = flashOf(main);
if (!equal(flash.subarray(0, APP_START), stock.flash.subarray(0, APP_START))) fail('the boot loader or recovery bytes changed');
done.push('boot loader and recovery (flash 0x0-0x3FFFF): every record and byte as stock');
if (dataRecords(main).some(function (r) { return r.address < APP_START && APP_START < r.address + r.data.length; })) fail('a record crosses into the boot area');
done.push('no record reaches past 0x7E0000 or into the boot area');
var reg = appRegion(flash);
done.push('packed application: additive checksum');
if (!equal(reg.app, app)) fail('the update does not unpack to the patched application');
done.push('unpacked with the boot loader\'s LZSS: exactly the patched application');
done.push('flash used by the application: ' + reg.region.length + ' of ' + (APP_END - APP_START) + ' bytes');
return done;
}
// ---------- LZSS packer (lzss_encode.py), run in a worker ----------
function lzssPack(data) {
var RING = 4096, START = RING - 18, MIN = 3, MAX = 18, DEPTH = 48;
var n = data.length, total = START + n;
function cb(i) { return i < START ? 0x20 : data[i - START]; }
var head = new Int32Array(1 << 24).fill(-1), prev = new Int32Array(RING);
function key(p) { return cb(p) << 16 | cb(p + 1) << 8 | cb(p + 2); }
function insert(p) {
if (p + 3 <= total) { var k = key(p); prev[p & (RING - 1)] = head[k]; head[k] = p; }
}
for (var p0 = 0; p0 < START; p0++) insert(p0);
var out = new Uint8Array(n + (n >> 3) + 64), o = 0;
var flagAt = o++, mask = 1, items = 0;
out[flagAt] = 0;
var k2 = 0;
while (k2 < n) {
var a = START + k2, limit = Math.min(MAX, total - a), bestLen = 0, bestPos = 0;
if (limit >= MIN) {
var lo = a - RING, p = head[key(a)], checked = 0;
while (p >= 0 && p >= lo) {
var len = 0;
while (len < limit && cb(p + len) === cb(a + len)) len++;
if (len > bestLen) {
bestLen = len; bestPos = p;
if (bestLen >= limit) break;
}
if (++checked >= DEPTH) break;
p = prev[p & (RING - 1)];
}
}
var step;
if (bestLen >= MIN) {
step = bestLen;
var off = bestPos & (RING - 1);
out[o++] = off & 0xFF;
out[o++] = (off >> 4 & 0xF0) | (step - MIN);
} else {
step = 1;
out[flagAt] |= mask;
out[o++] = cb(a);
}
for (var t = 0; t < step; t++) insert(a + t);
k2 += step;
items++;
mask = mask << 1 & 0xFF;
if (mask === 0) {
mask = 1;
if (k2 < n) { flagAt = o++; out[flagAt] = 0; }
}
}
return out.slice(0, o);
}
function packInWorker(app) {
return new Promise(function (resolve, reject) {
var src = lzssPack.toString() + '\nonmessage = function (e) { var o = lzssPack(new Uint8Array(e.data)); postMessage(o.buffer, [o.buffer]); };';
var w;
try {
w = new Worker(URL.createObjectURL(new Blob([src], { type: 'text/javascript' })));
} catch (e) {
setTimeout(function () { try { resolve(lzssPack(app)); } catch (x) { reject(x); } }, 30);
return;
}
w.onmessage = function (e) { w.terminate(); resolve(new Uint8Array(e.data)); };
w.onerror = function (e) { w.terminate(); reject(new Error('packing failed: ' + e.message)); };
var copy = app.slice();
w.postMessage(copy.buffer, [copy.buffer]);
});
}
// ---------- arnoldasr's asset blob (patch_cdj900nxs.assets) ----------
var TABLE = 0x08100000, DESCRIPTOR = 0x2C, ORIGINAL_COUNT = 0x2A1, TEMPLATE = 0x1C8, DATA_BASE = 0x08FC2D00;
var WIDTH = 200, HEIGHT = 24, MARKER_WIDTH = 8, BAR = 5, KEY = 0xF81F;
var ORANGE_LINE = [120, 65, 20], ORANGE_MARK = [255, 140, 0], BLUE_LINE = [40, 90, 120], BLUE_MARK = [60, 170, 255];
var WAVE_COLOURS = [
448, 457, 47, 458, 449, 111, 495, 466, 175, 467, 487, 239, 303, 479, 367, 459, 456, 431,
468, 465, 475, 476, 474, 39, 477, 31, 471, 46, 469, 45, 23, 460, 109, 484, 110, 485, 103,
478, 483, 493, 494, 486, 95, 15, 450, 167, 492, 173, 174, 231, 115, 470, 238, 44, 237, 159,
295, 87, 108, 302, 51, 301, 491, 482, 359, 223, 366, 365, 236, 300, 179, 423, 461, 287, 430,
429, 172, 364, 473, 428, 351, 114, 151, 463, 299, 363, 415, 215, 427, 178, 490, 50, 279, 235,
79, 462, 451, 343, 511, 298, 362, 400, 52, 116, 426, 234, 407, 7, 143, 180, 207, 113, 233,
243, 481, 57, 271, 452, 417, 177];
var WHITENESS = [[150, 20, 20], [220, 40, 30], [255, 110, 20], [255, 190, 30], [170, 230, 60],
[40, 210, 170], [60, 160, 255], [225, 240, 255]];
function rgb565(c) { return (c[0] >> 3) << 11 | (c[1] >> 2) << 5 | c[2] >> 3; }
function lineImage(colour) {
var px = new Array(WIDTH * HEIGHT).fill(KEY), c = rgb565(colour);
for (var x = 2; x < WIDTH - 2; x++) px[12 * WIDTH + x] = c;
for (var i = 0; i < 5; i++) {
var xx = 2 + Math.floor(i * (WIDTH - 5) / 4);
for (var y = 7; y < 18; y++) px[y * WIDTH + xx] = c;
}
return px;
}
function markerImage(colour) {
var px = [], c = rgb565(colour);
for (var y = 0; y < HEIGHT; y++) for (var x = 0; x < MARKER_WIDTH; x++) px.push(y >= 3 && y <= 21 && x < BAR ? c : KEY);
return px;
}
function rgb9(code) { return [6, 3, 0].map(function (s) { return Math.floor(((code >> s) & 7) * 255 / 7); }); }
function assets(app) {
var images = [[WIDTH, lineImage(ORANGE_LINE)], [WIDTH, lineImage(BLUE_LINE)],
[MARKER_WIDTH, markerImage(ORANGE_MARK)], [MARKER_WIDTH, markerImage(BLUE_MARK)]];
var t0 = TABLE - BASE, count = ORIGINAL_COUNT + images.length, tableBytes = count * DESCRIPTOR;
var pixelsBase = DATA_BASE + ((tableBytes + 31) & ~31);
var template = app.subarray(t0 + TEMPLATE * DESCRIPTOR, t0 + (TEMPLATE + 1) * DESCRIPTOR);
var pixLen = 0;
images.forEach(function (im) { var l = im[1].length * 2; pixLen += l + (32 - l % 32) % 32; });
var blobLen = (pixelsBase - DATA_BASE) + pixLen, palette = 128 * 2, lut = 512;
var blob = new Uint8Array(blobLen + palette + lut);
blob.set(app.subarray(t0, t0 + ORIGINAL_COUNT * DESCRIPTOR));
var pix = pixelsBase - DATA_BASE, pl = 0;
images.forEach(function (im, i) {
var e = ORIGINAL_COUNT * DESCRIPTOR + i * DESCRIPTOR;
blob.set(template, e);
put16(blob, e + 4, im[0]); put16(blob, e + 6, HEIGHT);
put32(blob, e + 0x1C, (ORIGINAL_COUNT + i) * DESCRIPTOR);
put32(blob, e + 0x20, pixelsBase + pl - TABLE);
im[1].forEach(function (v, j) { put16(blob, pix + pl + j * 2, v); });
pl += im[1].length * 2;
pl += (32 - pl % 32) % 32;
});
var paletteAt = DATA_BASE + blobLen;
var colours = WHITENESS.concat(WAVE_COLOURS.map(rgb9));
colours.forEach(function (c, i) { put16(blob, blobLen + i * 2, rgb565(c)); });
for (var code = 0; code < 512; code++) {
var r = code >> 6 & 7, g = code >> 3 & 7, b = code & 7, best = 0, bd = Infinity;
for (var i = 0; i < WAVE_COLOURS.length; i++) {
var w = WAVE_COLOURS[i], d = Math.pow((w >> 6 & 7) - r, 2) + Math.pow((w >> 3 & 7) - g, 2) + Math.pow((w & 7) - b, 2);
if (d < bd) { bd = d; best = i; }
}
blob[blobLen + palette + code] = 8 + best;
}
return { blob: blob, phaseEnd: blobLen, paletteAt: paletteAt };
}
// ---------- the mods' bytes (builder/data.js) ----------
function boxPixels(stock, res, bg, ink) {
var B = D.box, rec = B.resTable + res * 44, w = u16(stock, rec + 4), h = u16(stock, rec + 6);
var off = B.resTable + u32(stock, rec + 0x20), px = new Uint16Array(w * h);
for (var i = 0; i < w * h; i++) px[i] = u16(stock, off + i * 2);
for (var y = 3; y < 13; y++) for (var x = 3; x < w - 3; x++) px[y * w + x] = bg;
// as box_pixels: width = sum(space for ' ', else glyph width + gap) - gap
var text = B.text, width = -B.gap, c;
for (c = 0; c < text.length; c++) width += text[c] === ' ' ? B.space : B.glyphs[text[c]][0].length + B.gap;
var x0 = Math.floor((w - width) / 2), y0 = 4;
for (c = 0; c < text.length; c++) {
if (text[c] === ' ') { x0 += B.space; continue; }
var g = B.glyphs[text[c]];
for (var gy = 0; gy < g.length; gy++) for (var gx = 0; gx < g[gy].length; gx++) if (g[gy][gx] === 'X') px[(y0 + gy) * w + x0 + gx] = ink;
x0 += g[0].length + B.gap;
}
var out = new Uint8Array(w * h * 2);
for (i = 0; i < w * h; i++) put16(out, i * 2, px[i]);
return out;
}
// One variant of data.js: the application grown to its size, then its
// operations: our bytes, a copy from the user's own application, the HOT
// KEYS box or arnoldasr's asset blob, both made from the user's application.
function applyVariant(app, stock, v) {
var out = grow(app.slice(), v.size), blob = null;
v.ops.forEach(function (o) {
var off = o[1] - BASE;
if (o[0] === 'data') out.set(fromB64(D.chunks[o[2]]), off);
else if (o[0] === 'copy') out.set(stock.subarray(o[2] - BASE, o[2] - BASE + o[3]), off);
else if (o[0] === 'box') out.set(boxPixels(stock, o[2], o[3], o[4]), off);
else if (o[0] === 'assets') { blob = blob || assets(stock).blob; out.set(blob.subarray(0, o[2]), off); }
else fail('unknown operation ' + o[0]);
});
return out;
}
// ---------- the whole build ----------
function patchApp(stock, names, log) {
var up = UPSTREAM.filter(function (n) { return names.indexOf(n) >= 0; });
var ours = OURS.filter(function (n) { return names.indexOf(n) >= 0; });
var app = stock;
if (up.length) {
app = applyVariant(app, stock, D.upstream[up.join(',')]);
log('arnoldasr\'s mods applied (' + up.join(', ') + ')');
}
if (ours.length) {
app = applyVariant(app, stock, D.ours[ours.join(',')]);
log('our mods applied (' + ours.join(', ') + ')');
}
var key = up.concat(ours).join(',');
return sha256(app).then(function (h) {
if (h !== D.expect[key]) fail('the patched application is not the one the patcher makes (SHA-256 ' + h + ')');
log('the patched application is exactly the patcher\'s (SHA-256 checked)');
return app;
});
}
function build(raw, names, log) {
var stock;
log('checking the update file...');
return readStock(raw).then(function (s) {
stock = s;
log('the official CDJ-900NXS 1.31 update: SHA-256 checked, application unpacked and checked');
if (!names.length) return { app: stock.app, stream: stock.stream };
return patchApp(stock.app, names, log).then(function (app) {
log('packing the application (LZSS), this takes a while...');
var t = Date.now();
return packInWorker(app).then(function (stream) {
log('packed ' + app.length + ' -> ' + stream.length + ' bytes in ' + Math.round((Date.now() - t) / 1000) + ' s');
return { app: app, stream: stream };
});
});
}).then(function (r) {
log('writing the update file...');
var out = buildUpd(stock, r.stream);
var checks = verify(out, stock, r.app);
return Promise.all([sha256(out), sha256(r.app)]).then(function (h) {
return { upd: out, checks: checks, updSha: h[0], appSha: h[1] };
});
});
}
window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, OURS: OURS, lzssPack: lzssPack, unpackLzss: unpackLzss };
})();