site 1.7.0: builder covers every mod in any combination

Stage 1 (arnoldasr's code) still ships as checked data; stage 2 (our mods)
runs the patcher's own Python in the browser via self-hosted Pyodide 0.26.4,
byte-identical to the patcher. Added cuenames, played, endwarn, quicksearch,
usbautoload; phasemeter REVERSE and WAVEFORM modes included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Thiemo TizianiandClaude Opus 5.5 committed 2026-10-20 19:20:38 +02:00
1 parent 2f9c182279
commit f955e37927
19 files changed
+566 -93

No files matched your search

+62 -17
View File
@@ -3,9 +3,11 @@
// 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.
// lzss_encode.py (packer) and patch_cdj900nxs.py's asset blob. Stage 1,
// arnoldasr's mods, is in builder/data.js (tools/make_builder_data.py) with
// the SHA-256 of its result; stage 2, ours, is the patcher's own Python
// (builder/py/bundle.json) run here in Pyodide (builder/pyodide/, served
// from this site), so the result is the patcher's byte for byte.
(function () {
'use strict';
var D = window.BUILDER_DATA;
@@ -14,7 +16,7 @@
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', 'advanced_waveforms'];
var OURS = ['beatjump', 'cuecolours', 'hotcuekeys', 'ledsaver', 'keynotation', 'credit'];
var PY_BASE = 'builder/';
function fail(msg) { throw new Error(msg); }
@@ -450,25 +452,68 @@
return out;
}
// ---------- stage 2: our mods, the patcher's Python in Pyodide ----------
var pyReady = null;
function loadPython(log) {
if (pyReady) return pyReady;
log('loading Python for our mods (the first time takes a few seconds)...');
pyReady = new Promise(function (resolve, reject) {
var sc = document.createElement('script');
sc.src = PY_BASE + 'pyodide/pyodide.js';
sc.onload = resolve;
sc.onerror = function () { reject(new Error('could not load builder/pyodide/pyodide.js')); };
document.head.appendChild(sc);
}).then(function () {
return window.loadPyodide({ indexURL: new URL(PY_BASE + 'pyodide/', document.baseURI).href });
}).then(function (py) {
return fetch(PY_BASE + 'py/bundle.json').then(function (r) {
if (!r.ok) fail('could not load builder/py/bundle.json');
return r.json();
}).then(function (files) {
py.FS.mkdirTree('/cdj/mods/cdj900nxs');
Object.keys(files).forEach(function (rel) { py.FS.writeFile('/cdj/' + rel, files[rel]); });
py.runPython('import sys\nsys.path[:0] = ["/cdj/mods", "/cdj/mods/cdj900nxs"]\nimport patch_cdj900nxs');
return py;
});
});
pyReady.catch(function () { pyReady = null; });
return pyReady;
}
function patchOurs(app, stock, names, log) {
return loadPython(log).then(function (py) {
var t = Date.now();
py.globals.set('js_app', app);
py.globals.set('js_stock', stock);
py.globals.set('js_names', names.join(','));
var out = py.runPython([
'import patch_cdj900nxs as p',
'_names = p.check_names(js_names.split(","))',
'_out = p.patch_ours(bytes(js_app.to_bytes()), bytes(js_stock.to_bytes()), _names)',
'_out'].join('\n'));
var u8 = out.toJs();
out.destroy();
py.runPython('del js_app, js_stock, _out');
log('our mods applied by the patcher\'s own code (' + Math.round((Date.now() - t) / 100) / 10 + ' s)');
return u8;
});
}
// ---------- 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;
var app = stock, first;
if (up.length) {
app = applyVariant(app, stock, D.upstream[up.join(',')]);
log('arnoldasr\'s mods applied (' + up.join(', ') + ')');
first = sha256(app).then(function (h) {
if (h !== D.expect[up.join(',')]) fail('arnoldasr\'s part is not the one the patcher makes (SHA-256 ' + h + ')');
log('arnoldasr\'s part is exactly the patcher\'s (SHA-256 checked)');
});
} else {
first = Promise.resolve();
}
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;
});
return first.then(function () { return patchOurs(app, stock, names, log); });
}
function build(raw, names, log) {
@@ -496,5 +541,5 @@
});
}
window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, OURS: OURS, lzssPack: lzssPack, unpackLzss: unpackLzss };
window.CDJBuilder = { build: build, UPSTREAM: UPSTREAM, lzssPack: lzssPack, unpackLzss: unpackLzss, patchApp: patchApp };
})();