Patcher: disclaimer, every mod can be unticked, hot cue docs

- A disclaimer (mods and patcher developed with Claude Code, at your own
  risk) must be accepted by typing yes, or with --accept, before a build.
- Unticking a mod unticks what needs it instead of ticking it back, so
  code can be unticked; hotcuekeys needs cuecolours.
- beatjump starts unticked and is marked with its faults on a player.
- README: the hot cue keys (play, gate, set, delete, LEDs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Thiemo TizianiandClaude Opus 5.5 committed 2026-10-18 01:22:01 +02:00
1 parent cfa0e85854
commit bbe784aad2
3 files changed
+119 -16

No files matched your search

+58 -11
View File
@@ -16,6 +16,9 @@ choose, as a new C900NXS.UPD for the player's own USB update.
* The new file says 1.32 in its MAIN header so that the player's updater
does not skip it as the version it already has; the application itself
still reports 1.31 (as arnoldasr's builds do). Only that label differs.
* Before anything is built a disclaimer must be accepted: the mods and
this patcher were developed with Claude Code (--accept where no one can
type).
* Nothing here talks to a player. You copy the file to a USB stick and run
the update on the player yourself, at your own risk: nothing of this has
been tested on hardware except the relabelled stock update (by arnoldasr),
@@ -36,8 +39,11 @@ import upd # noqa: E402
DEFAULT_REPO = os.path.join(os.path.dirname(HERE), 'CDJ900NXSVirtual')
UPSTREAM_MODS = ('code', 'utility', 'overview')
NEEDS = {'utility': 'code'} # the PHASE METER switch chooses the code mod's meter
NEEDS = {'utility': 'code', # the PHASE METER switch chooses the code mod's meter
'hotcuekeys': 'cuecolours'} # the hot cues the keys play are read by cuecolours
LABEL = '1.32'
# not ticked at the start of the list, and why
KNOWN_FAULTS = {'beatjump': 'known faults on a player (jumps off the grid, "file corrupted" after a few jumps)'}
def load_repo(repo):
@@ -60,6 +66,8 @@ def available(patcher, repo):
or os.path.join(repo, 'mods', 'cdj900nxs', 'upstream'))
out = []
for name, what in patcher.MODS.items():
if name in KNOWN_FAULTS:
what = '%s [%s]' % (what, KNOWN_FAULTS[name])
ok = upstream or name not in UPSTREAM_MODS
out.append((name, what, ok, '' if ok else "needs arnoldasr's checkout in mods/cdj900nxs/upstream"))
return out
@@ -102,9 +110,16 @@ def _keys():
return read
def _fix_needs(chosen, mods):
for name, need in NEEDS.items():
if name in chosen and need not in chosen and any(m[0] == need and m[2] for m in mods):
def _toggle(chosen, name, mods):
"""Tick or untick one mod: ticking one ticks what it needs, unticking
one unticks what needs it (so every mod can be unticked)."""
if name in chosen:
chosen.discard(name)
chosen.difference_update(n for n, need in NEEDS.items() if need == name)
else:
chosen.add(name)
need = NEEDS.get(name)
if need and any(m[0] == need and m[2] for m in mods):
chosen.add(need)
@@ -115,7 +130,7 @@ def choose(mods):
return choose_by_number(mods)
if os.name == 'nt':
os.system('') # VT sequences on in the Windows console
chosen = {m[0] for m in mods if m[2]}
chosen = {m[0] for m in mods if m[2] and m[0] not in KNOWN_FAULTS}
at = 0
lines = len(mods) + 3
@@ -140,8 +155,7 @@ def choose(mods):
elif k == 'down':
at = (at + 1) % len(mods)
elif k == 'space' and mods[at][2]:
chosen ^= {mods[at][0]}
_fix_needs(chosen, mods)
_toggle(chosen, mods[at][0], mods)
elif k == 'all':
usable = {m[0] for m in mods if m[2]}
chosen = set() if chosen == usable else usable
@@ -155,9 +169,9 @@ def choose(mods):
def choose_by_number(mods):
"""For terminals that cannot be read key by key (mintty, pipes)."""
chosen = {m[0] for m in mods if m[2]}
chosen = {m[0] for m in mods if m[2] and m[0] not in KNOWN_FAULTS}
while True:
print('\n Mods (all usable ones are ticked):')
print('\n Mods (all usable ones without known faults are ticked):')
for i, (name, what, ok, why) in enumerate(mods, 1):
mark = '[x]' if name in chosen else ('[ ]' if ok else ' - ')
print(' %d %s %-11s %s' % (i, mark, name, what if ok else why))
@@ -168,11 +182,41 @@ def choose_by_number(mods):
raise SystemExit('nothing written')
for tok in answer.replace(',', ' ').split():
if tok.isdigit() and 1 <= int(tok) <= len(mods) and mods[int(tok) - 1][2]:
chosen ^= {mods[int(tok) - 1][0]}
_fix_needs(chosen, mods)
_toggle(chosen, mods[int(tok) - 1][0], mods)
return [m[0] for m in mods if m[0] in chosen]
# --- the disclaimer ------------------------------------------------------------
DISCLAIMER = '''
Please read before anything is built:
The mods of this patcher (beatjump, cuecolours, hotcuekeys) and the patcher
itself were developed with Claude Code, Anthropic's AI coding assistant,
by reverse engineering the firmware in an emulator. They have not been
reviewed by Pioneer DJ / AlphaTheta and have not been tested on a player
except where the README says so. Flashing modified firmware can leave the
player unusable. You use the result at your own risk; there is no warranty.
'''
def confirm(accepted, ask=input):
"""The disclaimer, which must be accepted before a build: by typing yes,
or with --accept where no one can type."""
print(DISCLAIMER)
if accepted:
print(' accepted with --accept')
return
if not sys.stdin.isatty():
raise SystemExit('nothing written: accept the disclaimer by typing yes, or with --accept')
try:
answer = ask(' Type yes to accept and go on: ').strip().lower()
except EOFError:
answer = ''
if answer not in ('yes', 'y', 'ja', 'j'):
raise SystemExit('nothing written: the disclaimer was not accepted')
# --- building ------------------------------------------------------------------
def build(stock, names, patcher, lzss, label=LABEL, log=print):
@@ -205,6 +249,8 @@ def main(argv=None):
help='no mods: the original application labelled %s, to go back to stock' % LABEL)
ap.add_argument('--repo', default=DEFAULT_REPO, help='the CDJ900NXSVirtual checkout (default %(default)s)')
ap.add_argument('--list', action='store_true', help='list the mods and exit')
ap.add_argument('--accept', action='store_true',
help='accept the disclaimer (developed with Claude Code, at your own risk) without asking')
args = ap.parse_args(argv)
patcher, lzss = load_repo(args.repo)
@@ -246,6 +292,7 @@ def main(argv=None):
print(' no mod chosen: use --stock for the unmodified update')
return 1
print(' mods: %s' % (', '.join(names) or 'none (stock application)'))
confirm(args.accept)
try:
out_raw, app, checks = build(stock, names, patcher, lzss)