export const meta = { name: 'modemwars-swiftlink', description: 'Build an alternate SwiftLink (6551 ACIA) opponent module for Modem Wars', phases: [ { title: 'Spec', detail: 'extract the exact contract of the routines that will be replaced' }, { title: 'Implement', detail: 'rewrite the UART layer against the 6551' }, { title: 'Build', detail: 'assemble, check the ABI, install on a disk image' }, { title: 'Test', detail: 'boot the patched disk in VICE with SwiftLink emulation' }, { title: 'Document', detail: 'write the driver README' }, ], } const RESULT = { type: 'object', properties: { path: { type: 'string' }, ok: { type: 'boolean' }, summary: { type: 'string' }, problems: { type: 'array', items: { type: 'string' } }, }, required: ['path', 'ok', 'summary', 'problems'], } const common = ` PROJECT. /home/scott/claude/modemwars holds a complete, byte-exact, heavily commented disassembly of "Modem Wars" (C64, Electronic Arts / Ozark Softscape, 1988). Read docs/overview.md first, especially section 9 about the two opponent modules. THE GOAL. An alternate opponent module that talks to a SwiftLink cartridge (a 6551 ACIA on the expansion port) instead of the stock module's bit-banged user-port UART, so the game can play over a modem or null-modem link at far higher speeds than 300/1200 baud. THE KEY DESIGN DECISION (already made - do not redesign it). The stock module disassembly/game/modemDriverE000.s is layered: packet / chat layer ($E111-$E2FF) game-facing packets, host rings, exchange flags modem + link state ($E756-$E8xx) Hayes control, connection phases, carrier frame / ARQ layer ($E909-$EAB2) length + control + sequence + checksum, retransmit ring buffers ($E40F-$E533) four descending circular buffers UART layer (the rest) CIA2 timers, NMI bit-banging, DTR/RTS on the user port ONLY THE UART LAYER IS HARDWARE SPECIFIC. Replace just that and the module stays wire compatible with the stock driver at the same baud rate, because every byte above it is unchanged. HARD CONSTRAINTS. * The module is exactly 4096 bytes and loads at $E000-$EFFF. * Every routine you replace must stay at its original address and must not grow past its original end, so that nothing else in the module moves. Pad the leftover space (the 6551 code is much smaller than the bit-bang code it replaces). * $E000-$E017 (the jump table), $E01D-$E047 (shared variables the game reads and writes) and $EC00-$EFFF (utilities the map generator overlay calls) must come out BYTE-IDENTICAL to the stock module. swiftlink/checkAbi.py enforces this. * Plain ASCII only. Comment style matches the rest of the project: explain intent, not the mnemonic. THE HARDWARE. SwiftLink is a 6551 ACIA at $DE00 (some are strapped to $DF00), interrupting through the expansion port NMI line. base+0 data read = received byte, write = byte to transmit base+1 status bit7 IRQ, bit6 DSR, bit5 DCD, bit4 TDRE (transmit register empty), bit3 RDRF (receive register full), bit2 overrun, bit1 framing, bit0 parity (reading the status register clears the IRQ flag) base+2 command bit0 DTR, bit1 receiver IRQ disable, bits3-2 transmit control (00 = TX IRQ off / RTS high, 01 = TX IRQ on / RTS low, 10 = TX IRQ off / RTS low), bit4 echo, bits7-5 parity base+3 control bit7 stop bits, bits6-5 word length (00 = 8), bit4 clock source (1 = internal), bits3-0 baud rate SwiftLink fits a 3.6864 MHz crystal, twice what the 6551 expects, so every rate in the table comes out DOUBLED. Nominal rate bits: $05=150 $06=300 $07=600 $08=1200 $09=1800 $0A=2400 $0B=3600 $0C=4800 $0D=7200 $0E=9600 $0F=19200 - so on SwiftLink $05 gives 300, $07 gives 1200, $08 gives 2400, $0C gives 9600, $0E gives 19200, $0F gives 38400. Control byte = $10 (8N1, internal clock) OR the rate bits. VERIFY this doubling claim against a second source before relying on it, and say in your report what you concluded. DO NOT edit anything under disassembly/, annotations/, tools/ or docs/. Your work goes in swiftlink/. ` phase('Spec') const [uartSpec, linkSpec] = await parallel([ () => agent(`${common} TASK: write swiftlink/spec_uart.md - the exact contract of every routine in the stock module that touches the hardware, so it can be reimplemented against the 6551 without breaking its callers. Work from disassembly/game/modemDriverE000.s (it is fully commented) and disassembly/XREF.txt. For EACH of these, give: address range (start and the address of the byte after its last), what it does, entry conditions (registers, variables), exit conditions (registers, flags, variables), every caller, and every variable it reads or writes: $E353 loadBaudParameters, $E534 flushUartTxRing, $E540 configureUserPortLines, $E574 dropDtrLine, $E57B installCommNmiVector, $E586 restartUart, $E593 setCiaNmiMask, $E5B6 clearUartState, $E5C1 suspendUartIfRunning, $E5C3 stopCommNmi, $E607 startNextTxChar, $E654 setBitPeriodFull, $E657 setBitPeriod, $E663 nmiStartNextChar, $E685 commNmiHandler, $E69C nmiMergeIcrFlags, $E73C queueByteForTransmit. Then list, as a table, every byte of module RAM those routines own ($E5BB-$E5C0 and friends, the bit period bytes, the transmit pacing byte, the pending-character count $E0A5, the carrier sample timer) with its meaning, and say which of them a 6551 implementation still needs and which become dead. Finally: state exactly how the NMI handler decides an interrupt is not its own and how it chains to the game's handler through $E031/$E032, since the replacement must do the same. Check every address you quote against the listing. Return path, ok, a one-paragraph summary, problems.`, { label: 'spec:uart', phase: 'Spec', schema: RESULT }), () => agent(`${common} TASK: write swiftlink/spec_link.md covering the parts of the module that surround the UART, so the replacement keeps behaving the way the game expects. 1. The suspend/resume handshake: the disk loader sets $E039 and waits on $E03A ($0F5F/$0F71 in game/mainProgram0800.s). Document exactly what values mean what, and what the UART layer must do on suspend and on resume. 2. Carrier detection: how serviceCarrierAndSuspendRequest ($E5D6) and pollCarrierState ($E3CB) sample the modem's carrier line, how the result is debounced, and how it reaches $E03C/$E03D and isLinkActive $E03B. Say precisely which user-port bit the stock code reads, so the replacement can read the ACIA's DCD bit instead. 3. The modem state machine ($E756) and connection phases ($E040): what each phase does, where the Hayes strings are sent, and what a direct null-modem connection (no dial tone, no CONNECT message, DCD possibly not wired) would need to skip. 4. Baud selection: the table loadBaudParameters reads, what the game stores where, and which Commodore-key hot keys readAndHandleModemHotkeys ($E2EC) already uses - list every key it tests and what it does, so new baud-rate keys can be added without clashing. 5. What the module does with $E030 (the build id the game reads at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D) - what value the stock modem driver holds there, what the trainer holds, and what the game does with the difference. The SwiftLink module must keep the modem driver's value. Check every address against the listings. Return path, ok, summary, problems.`, { label: 'spec:link', phase: 'Spec', schema: RESULT }), ]) phase('Implement') const implemented = await agent(`${common} The two specifications swiftlink/spec_uart.md and swiftlink/spec_link.md are ready - read them. TASK: turn swiftlink/swiftlinkDriverE000.s (currently an exact copy of the stock module's source) into the SwiftLink driver. METHOD. Edit that file in place. For each routine you replace, keep its label at its original address and pad the tail so the following routine still starts where it did. The file is ca65 source with a plain ".org $E000" and no segments, so the simplest way to guarantee this is to put .assert * <= $XXXX, error, "routine overran" .res $XXXX - *, $EA at the end of each replaced routine, where $XXXX is the original address of the next routine. Do that for every routine you touch, and add a line comment on the .res saying what used to be there. WHAT TO IMPLEMENT. 1. ACIA base detection at init: try $DE00, fall back to $DF00, by writing a control value and reading it back (the data and status registers are not suitable for this). Keep the detected base in a spare byte inside your own replaced region and use absolute,X addressing (X = 0 or the $100 offset) or self-modified operands - whichever you can fit. If you cannot fit detection, default to $DE00 and say so. 2. Initialisation: control = $10 | rateBits (8N1, internal baud generator), command = $09 (DTR asserted, RTS low, receiver IRQ enabled, transmit IRQ off). 3. Transmit: enable the transmit interrupt (command bits3-2 = 01, i.e. $05) only while the transmit ring has something in it, and drop back to $09 when it drains. queueByteForTransmit must kick the transmitter when it was idle. Keep the pending-character count the frame layer relies on. 4. Receive: in the NMI, read the status register, and if it shows a received byte push it into the same raw receive ring the stock code uses, keeping the carrier sample timer behaviour. Report framing errors, parity errors and overruns through the same error hook the stock code uses. 5. NMI chaining: if the ACIA did not raise the interrupt, chain to the game's handler exactly the way the stock handler does. 6. Suspend/resume: on suspend, disable the receiver interrupt but leave DTR asserted so the connection survives a disk load; on resume, re-enable it. Keep the $E039/$E03A protocol. 7. Carrier: read the ACIA's DCD bit instead of the user-port line, feeding the same debounce. Because a null-modem cable often leaves DCD unwired, add a direct-connect mode that reports carrier present; make it the default when DCD never goes active, or offer it on a hot key, and document which you chose. 8. Baud: replace the baud parameter table with ACIA control values and support 300, 1200, 2400, 4800, 9600, 19200 and 38400. Keep the existing hot keys working and add keys for the new rates using keys spec_link.md shows to be free. Leave the default at the rate the game selects, so a SwiftLink player and a stock-driver player can still meet at 300 or 1200 baud. Do NOT touch anything outside the routines listed in spec_uart.md. Do NOT change the jump table, the shared variables or $EC00-$EFFF. Build and check as you go with: ./swiftlink/build.sh /tmp/swiftlinkTest.d64 python3 swiftlink/checkAbi.py swiftlink/build/swiftlinkDriverE000.bin disassembly/build/game_modemDriverE000.orig.bin checkAbi must pass, and the changed regions it prints must all be inside the routines you meant to replace. Report the list of changed regions in your summary. Return path, ok, summary, problems.`, { label: 'implement', phase: 'Implement', schema: RESULT }) phase('Build') const built = await agent(`${common} TASK: verify the build of swiftlink/swiftlinkDriverE000.s rigorously. 1. ./swiftlink/build.sh ../Modem_Wars_SwiftLink.d64 - must produce exactly 4096 bytes and a disk. 2. python3 swiftlink/checkAbi.py swiftlink/build/swiftlinkDriverE000.bin disassembly/build/game_modemDriverE000.orig.bin - must pass. 3. Disassemble the changed regions of the new module and read them back, checking that: - every replaced routine still ends before the next routine's address; - no routine falls through into padding; - the NMI handler saves and restores every register it uses and ends in RTI or a chain; - no code writes to the frozen regions. Use tools/m6502.py if it helps. 4. Confirm the disk image differs from the original ONLY in track 18 sector 7 and track 34 sectors 1-15: decrypt both disks with the project's own code and compare every other sector. 5. Confirm the stock driver still assembles byte-exact (./disassembly/verify.sh | grep -c OK must print 24) - the SwiftLink work must not have disturbed the main project. Fix what you can in swiftlink/ only. Return path, ok, summary, problems.`, { label: 'build', phase: 'Build', schema: RESULT }) phase('Test') const tested = await agent(`${common} TASK: test the patched disk ../Modem_Wars_SwiftLink.d64 in VICE. VICE emulates SwiftLink: -acia1 -acia1mode 1 -acia1base 0xDE00 -acia1irq 1 -myaciadev , and the RS232 devices can be pointed at a TCP address (see x64sc -help, the -rsdev* options). IMPORTANT: other people are using this machine. Pick a free TCP port and a free X display, never use a fixed one, and only kill processes you started yourself. extracted/vice/playSession.py shows the pattern that works: a private Xvfb display, the remote monitor on a free port, -model ntsc (the loader's timing needs NTSC), and xdotool for key events. 1. Boot the patched disk with SwiftLink emulation enabled and confirm it reaches the options menu exactly like the stock disk does. Take a screenshot into swiftlink/shots/. 2. Select "COMPETE WITH MODEM OPPONENT" and confirm the game gets into the modem screens without hanging or crashing - the stock driver would be waiting on the user port, so reaching the same prompts proves the module is at least alive and being called. Screenshot it. 3. Verify the module really is the SwiftLink one and really talks to the ACIA: with the VICE monitor, breakpoint or watch the ACIA registers ($DE00-$DE03) and show that the game writes the control and command registers after the link is opened. Report the actual values written and whether they match the intended 8N1 + internal clock + DTR/RTS settings. 4. If you can, run two instances connected to each other over TCP (one -rsdev configured as a server, the other as a client) and see whether they exchange bytes. This is the ideal test but it may not be reachable; if it is not, say exactly how far you got and what blocked it. Do not claim anything you did not observe. Put screenshots in swiftlink/shots/ and a log of what you did in swiftlink/testReport.md. Return path, ok, summary, problems.`, { label: 'test', phase: 'Test', schema: RESULT }) phase('Document') const documented = await agent(`${common} The driver, its build script and its test report exist. Read swiftlink/spec_uart.md, swiftlink/spec_link.md, swiftlink/testReport.md and the source, then write swiftlink/README.md for someone who wants to use or modify this: * what it is, and the one-sentence reason it works (only the bottom layer is hardware specific); * what hardware it expects and what it does NOT need (no user-port cable, no CIA2 timers); * exactly how to build and install it, and what disk it produces; * the baud rates it supports, the hot keys, and the direct-connect mode; * wire compatibility: a SwiftLink player and a stock-driver player CAN play each other at a shared rate, because every layer above the UART is untouched - and say what happens if the rates differ; * what was verified and what was not (be exact and honest, quoting the test report); * the map of what changed inside the module, address by address, and what is guaranteed unchanged. Also add a short "SwiftLink driver" section near the end of the top-level README.md pointing at it - that is the ONE file outside swiftlink/ you may edit. Return path, ok, summary, problems.`, { label: 'document', phase: 'Document', schema: RESULT }) return { uartSpec, linkSpec, implemented, built, tested, documented }