238 lines
16 KiB
JavaScript
238 lines
16 KiB
JavaScript
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 <n>, 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 }
|