From 4eaf48111887e5a7dc4e799bed0f87e0459d6df5 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sat, 8 Aug 2026 15:49:48 +0200 Subject: [PATCH 01/21] 6502: Added Apple II support - Supports Apple II+ and later models - 64 or 128 kB - 40 or 80 columns - Aambundle builds 140 kB and 800 kB ProDOS-format disk images - No actual ProDOS system image is needed - Save/restore is supported with only one slot at the moment - Run "make -C test apple2" for unit tests Aambundle: Stories without a title in their metadata are named after the base name of the story file. --- .github/workflows/build.yml | 8 +- .gitignore | 10 +- readme.txt | 22 +- src/6502/Makefile | 52 +- src/6502/a2_0boot.acme | 379 +++ src/6502/a2_frontend.s | 3293 +++++++++++++++++++++++ src/6502/a2_prorwts2.acme | 3929 ++++++++++++++++++++++++++++ src/6502/a2_sboot.s | 240 ++ src/6502/a2_thirdparty_license.txt | 34 + src/6502/engine.s | 27 +- src/6502/license.txt | 2 +- src/Makefile | 28 +- src/aambundle.c | 76 +- src/aambundle.h | 1 + src/bundle_apple2.c | 850 ++++++ test/Makefile | 5 +- test/apple2/Makefile | 93 + test/apple2/a2test.exp | 94 + test/apple2/gosling-save.in | 16 + test/apple2/gosling-save2.in | 17 + test/apple2/gosling-undo.in | 15 + 21 files changed, 9157 insertions(+), 34 deletions(-) create mode 100644 src/6502/a2_0boot.acme create mode 100644 src/6502/a2_frontend.s create mode 100644 src/6502/a2_prorwts2.acme create mode 100644 src/6502/a2_sboot.s create mode 100644 src/6502/a2_thirdparty_license.txt create mode 100644 src/bundle_apple2.c create mode 100644 test/apple2/Makefile create mode 100755 test/apple2/a2test.exp create mode 100644 test/apple2/gosling-save.in create mode 100644 test/apple2/gosling-save2.in create mode 100644 test/apple2/gosling-undo.in diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87c5c03..5f6a7a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,8 +14,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v6 - - name: install assembler - run: sudo apt install xa65 + - name: install assemblers + run: sudo apt install xa65 acme - name: install node packager run: sudo npm install -g @yao-pkg/pkg - name: make test @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v6 - name: install dependencies - run: sudo apt install gcc-mingw-w64 xa65 + run: sudo apt install gcc-mingw-w64 xa65 acme - name: install node packager run: sudo npm install -g @yao-pkg/pkg - name: make (aamrun) @@ -84,7 +84,7 @@ jobs: - name: install dependencies run: | brew update - brew install xa + brew install xa acme - name: install node packager run: sudo npm install -g @yao-pkg/pkg - name: make (aamrun) diff --git a/.gitignore b/.gitignore index 93da497..d06b26b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,12 @@ aambox6502 **/#*# src/6502/cruncher src/6502/labels -src/6502/mkfont \ No newline at end of file +src/6502/mkfont +src/6502/*.lst +src/6502/*.rpt +src/6502/*.sym +src/6502/*.labels +src/6502/a2.system +# Apple II test disks (built by test/apple2/Makefile) +test/apple2/aa-exercise/ +test/apple2/gosling/ diff --git a/readme.txt b/readme.txt index c4b4996..f82d722 100644 --- a/readme.txt +++ b/readme.txt @@ -8,6 +8,7 @@ The following interpreters are included: * Javascript engine, web frontend. * Javascript engine, Node.js frontend. * 6502 engine, Commodore 64 frontend. + * 6502 engine, Apple II frontend. * 6502 engine, aambox6502 frontend. Version number: @@ -99,6 +100,11 @@ To run the example story using the Vice Commodore 64 emulator, available at To run on a real Commodore 64, insert the disk and type: LOAD"*",8 Then type: RUN +To run the example story using an online Apple II emulator, go to + then drag one of the bootable disk images +to the page. You can also run from the command line using a console +emulator such as . + To build the Å-machine tools under Linux (requires a C compiler and make): cd src @@ -114,7 +120,8 @@ To cross-compile the Windows version of the Å-machine tools under Linux Note that when aambundle is built, several files are copied from src/js and src/6502 into the resulting executable file. To rebuild the binary files in -src/6502, run "make" in that directory. This requires the xa65 assembler. +src/6502, run "make" in that directory. This requires the xa65 and ACME +assemblers. Project website: @@ -127,6 +134,19 @@ Release notes: 6502 engine: Fixed some memory corruption issues, especially involving save/restore/undo operations. + New Apple II interpreter: + - Supports Apple II+ and later models + - 64 or 128 kB + - 40 or 80 columns + - Aambundle builds 140 kB and 800 kB ProDOS-format disk images + - No actual ProDOS system image is needed + - Save/restore is supported with only one slot at the moment + - For more, consult the readme.txt generated by aambundle + - Run "make -C test apple2" for unit tests + + Aambundle: Stories without a title in their metadata are + named after the base name of the story file. + 1.0.2: Cleaned up the specification and some dev tools. diff --git a/src/6502/Makefile b/src/6502/Makefile index 27949d1..a393b95 100644 --- a/src/6502/Makefile +++ b/src/6502/Makefile @@ -1,32 +1,66 @@ VERSION=1.0.3 CFLAGS=-Wall -O2 -DVERSION=\"$(VERSION)\" -all: aambox6502 aambox_frontend.bin c64_crunched.bin c64_drivecode.bin c64_loader.prg +all: aambox6502 aambox_frontend.bin c64_crunched.bin c64_drivecode.bin c64_loader.prg a2.system a2_0boot.bin a2_sboot.bin + +check_xa: + @if ! command -v xa >/dev/null 2>&1; then \ + echo "ERROR: 'xa' (6502 cross-assembler) not found in PATH."; \ + echo " macOS: brew install xa"; \ + echo " Debian/Ubuntu: apt install xa65"; \ + echo " From source: https://www.floodgap.com/retrotech/xa/"; \ + echo "(To build the C tools without any 6502 code, run 'make no6502' from the top level.)"; \ + exit 1; fi + +check_acme: + @if ! command -v acme >/dev/null 2>&1; then \ + echo "ERROR: 'acme' (6502 cross-assembler) not found in PATH."; \ + echo " macOS: brew install acme"; \ + echo " Debian/Ubuntu: apt install acme"; \ + echo " From source: https://github.com/meonwax/acme"; \ + echo "(Only the Apple II targets need acme; the C64 and aambox targets build with xa alone.)"; \ + exit 1; fi clean: - rm -rf aambox6502 cruncher labels mkfont + rm -rf aambox6502 cruncher labels c64.labels mkfont + rm -f a2_frontend.bin a2_prorwts2.bin a2.labels a2.system a2_0boot.bin a2_sboot.bin a2_sboot.labels aambox6502: aambox6502.c fake6502.c -aambox_frontend.bin: aambox_frontend.s engine.s +aambox_frontend.bin: aambox_frontend.s engine.s | check_xa xa -l aambox.labels -o $@ $< -DRAMTOP=49152 -c64_frontend.bin: c64_frontend.s engine.s font.bin - xa -l labels -o $@ $< -DVERSION=\"$(VERSION)\" +c64_frontend.bin: c64_frontend.s engine.s font.bin | check_xa + xa -l c64.labels -o $@ $< -DVERSION=\"$(VERSION)\" font.bin: fontdef.txt mkfont ./mkfont <$< >$@ -c64_drivecode.bin: c64_drivecode.s +c64_drivecode.bin: c64_drivecode.s | check_xa xa -o $@ $< -c64_loader.prg: c64_loader.s +c64_loader.prg: c64_loader.s | check_xa xa -o $@ $< -decruncher.bin: decruncher.s +decruncher.bin: decruncher.s | check_xa xa -o $@ $< c64_crunched.bin: c64_frontend.bin decruncher.bin cruncher ./cruncher $< decruncher.bin >$@ -.PHONY: all clean +a2_0boot.bin: a2_0boot.acme a2.system | check_acme + acme -DA2_TRACKS=$$((($$(wc -c < a2.system) + 4095) / 4096)) -o $@ $< + +a2_sboot.bin: a2_sboot.s a2.system | check_xa + xa -l a2_sboot.labels -o $@ $< -DAAM_BLOCKS=$$((($$(wc -c < a2.system) + 511) / 512)) + +a2_prorwts2.bin: a2_prorwts2.acme a2_frontend.bin | check_acme + acme -DPRORWTS_ORG=$$(($$(wc -c < a2_frontend.bin) + 8192)) -l a2_prorwts2.sym -r a2_prorwts2.rpt $< + +a2_frontend.bin: a2_frontend.s engine.s | check_xa + xa -l a2.labels -P a2_frontend.lst -o $@ $< -DPRODOS=0 -DPRORWTS=1 -DVERSION=\"$(VERSION)\" + +a2.system: a2_frontend.bin a2_prorwts2.bin + cat $^ > $@ + +.PHONY: all clean check_xa check_acme diff --git a/src/6502/a2_0boot.acme b/src/6502/a2_0boot.acme new file mode 100644 index 0000000..e710782 --- /dev/null +++ b/src/6502/a2_0boot.acme @@ -0,0 +1,379 @@ +;license:BSD-3-Clause +;bootable zpage seek/read +;copyright (c) Peter Ferrie 2015-16, 2018 +;thanks to 4am for inspiration and testing +;assemble using ACME +!cpu 6502 +;!to "0boot",plain +*=$800 + ;Configured for the A-machine. AAM.SYSTEM is laid out at the + ;start of track 1 of a ProDOS-order disk, so whole tracks of it + ;are four consecutive kilobytes of the file. It is loaded where + ;ProDOS would have loaded it, and entered at $2003 rather than + ;$2000 so the interpreter knows there is no ProDOS underneath. + ;A2_TRACKS comes from the Makefile, which rounds the size of + ;a2.system up to whole tracks. + + !ifndef A2_TRACKS { A2_TRACKS = 5 } + + tracks = A2_TRACKS ;user-defined + first_trk = $01 ;user-defined + address = $20 ;user-defined, high byte of first page + entry = $2003 ;user-defined + sectors = $00 ;user-defined, sectors to load from partial track (increase tracks count first) + interleave= 1 ;user-defined, to retain .dsk sector ordering + multi_regn= 0 ;user-defined, load to non-sequential addresses + !if multi_regn=1 { + !macro region {region !byte $d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0,$d1,0} + ;user-defined + ;assign every '$d1' from left + ;for track with alternative address + ;or zero to continue from last address + ;beginning with second track + ;array size = tracks*2 + } + version = 3 + + ;memory usage: + ;256 bytes ($200-2FF) static table + grouped = $200 + ;106 bytes ($36C-3D5) static table + preshift = $36C + ;86 bytes ($300-355) dynamic table (2-bit array) + bit2tbl = $300 + + !byte 1 ;we'll read the other one ourselves + lsr ;check sector number + bne + ;branch if not 1 + inc $3d ;increment sector (faster to find) + txa + jsr $f87b + ora #$c0 ;slot to PROM base + pha + lda #$5b ;read-1 + pha + rts + + ;the following TAY is a workaround for a CFFA bug + ;the bug is that Y isn't zero on entry + ;the workaround sets it to one instead + ;it's not zero, but it's better than #$10 + ++ tay + txa + ora #$8c ;slot to Q6L +- ldx $900, y + stx $0, y ;copy RTS to zpage + iny + bne - +- iny + ldx <(patchtbl-1), y + sta $0, x ;replace placeholders with Q6L + bne - + and #$f0 ;PHASEOFF + sta <(slotpatch6+1) + ora #8 ;MOTOROFF + sta <(slotpatch8+1) +!if interleave=1 { + ldx #$0f + txs +- lda sectbl, x + pha + dex + bpl - +} + lda #>(entry-1) + pha + lda #<(entry-1) + pha + ldx #(stackend-addrtbl) +- lda <(addrtbl-1), x + pha ;copy stack frame + dex + bne - + ldy #<(tracks*2) + +!if sectors>0 { + txa + pha + lda #0 { +++ +} + pha + lda #<(seek-1) + pha ;push seek twice for two phases + dey + bne - + +!if first_trk>1 { + ldy #((first_trk-1)*2) +- txa + pha + lda #<(seek-1) + pha ;push seek twice for two phases + dey + bne - +} + + ;construct denibbilisation table + ;pre-shifted for interleave read + + ldx #$6a +- asl preshift-1,x + asl preshift-1,x + dex + bne - + + ;construct 2-bit group table + + iny +-- lsr <(curtrk+1) + lsr <(curtrk+1) +- lda 0 { + !byte $2c + ldx #<(-sectors) +} + stx $ff { + !serious "code is too large, ends at *" +} +} diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s new file mode 100644 index 0000000..3c7ef4a --- /dev/null +++ b/src/6502/a2_frontend.s @@ -0,0 +1,3293 @@ +; uncomment to print out debug info at runtime (dev only) +;#define DEBUG +;#define DEBUGIO +; +; Apple II (ProDOS) Aa-machine frontend. +; Designed for the xa65 assembler. +; +; ProDOS Version (-DPRODOS=1): +; +; The machine is identified at boot from the +; ProDOS global page, and the screen width, +; the character set and the use of auxiliary +; memory follow from that. +; +; Apple ][ / ][+ 40 columns, upper case +; //e / //c / IIgs 80 columns if an +; 80-column card is +; present, 40 otherwise +; 128 kB machines cache/undo in aux RAM +; +; Screen output is delegated to the Monitor +; ROM, and to the //e 80-column firmware when +; that is active. Keyboard input is NOT +; delegated, because RDKEY and KEYIN increment +; RNDL/RNDH at $4e/$4f, which alias the +; engine's rsim register. +; +; Storage is ProDOS 8 via the MLI. ProDOS +; lives in the language card; system programs +; run with ROM banked in and trampoline +; through the $bf00 global page, so $fded and +; the MLI are both available at all times. +; +; For standard 143,360 byte floppy disks: +; You can put the entire story file into a single +; file called STORY on the main disk, but this limits +; you to about 100 KB size story files. +; On Apple IIGS you can use 800 KB disk images, and on +; emulators you can typically use up to 32 MB. +; +; ProDOS isn't very memory-efficient, as it takes up the +; entire 16 KB language card and 1 KB of RAM per open file. +; Many games require too much memory to run. +; +; Note -- the volume containing the STORY file should be +; called AA.STORY. The ProDOS version will scan for this +; volume label if it fails to find STORY in the current prefix. +; +; Main memory map (ProDOS) +; ===================================== +; 0000 - 001f frontend zero page +; 0020 - 002b Monitor text variables +; 002c - 0031 frontend zero page +; 0032 - 0039 Monitor text variables +; 003a - 003f frontend and disk driver temporaries +; 0040 - 00ff engine zero page +; 0100 - 01ff stack +; 0200 - 02ff frontend buffers and MLI +; parameter blocks +; 0300 - 037f aux page cache tags +; 0400 - 07ff text page 1 +; 0800 -~4800 interpreter code +; ~4800 -~4e00 init code, reclaimed as cache +; ~4800 - baff heap / data +; bb00 - beff ProDOS file buffer +; bf00 - bfff ProDOS global page +; d000 - ffff ProDOS 8 (language card) +; +; Auxiliary memory, when there is any +; ===================================== +; 0400 - 07ff 80-column text page +; 0800 - bfff aux page cache +; d000 - ffff aux undo ring, both $d000 +; banks, 16 kB in all +; +; ProRWTS2 Version (-DPRORWTS=1) +; +; This version uses a small-footprint ProDOS library which +; resides at $d000-$dfff and replaces the existing ProDOS. +; It has a separate entry point ($2003) for 0boot which +; stubs out the few ProDOS calls ProRWTS2 needs to init. +; +; Main memory map (ProRWTS2) +; ===================================== +; 0800 - 2000 interpreter code +; ~2000 - bfff heap / data +; d000 - f800 interpreter code (bank 1) +; d000 - daff PRORWTS (bank 2) +; +; See also: +; https://prodos8.com/docs/techref/memory-use/ +; https://savagetaylor.com/til/TA33130.html +; https://github.com/peterferrie/prorwts2 +; https://github.com/peterferrie/0boot +; +; TODO +; - io_load flaky (hangs, inserting nop makes it say "Failed to *save* game state") +; and the savefiles differ at $300 +; - SAVEFILE on first disk when running AAM.SYSTEM from Prodos? +; - run STORY file from subdirectory? +; - victim cache (move stale pages to aux)? +; - use the packer instead of boot mover? +; - save/restore filenames or multiple slots? + +DEFWIDTH = 80 + +PREXTRA = 2 +PRSHIFT = 0 +HAVE_QUIT = 1 +HAVE_STATUS = 1 +HAVE_STYLE = 0 +SAVERESTORE = 1 +UNDO = 1 + +TRACE_INST = 0 +TRACE_STORE = 0 + +// ProRWTS can't create files, so our SAVEFILE +// must be created with maximum size in advance +// and thus we must reserve that much memory +#if PRORWTS +#define A2_EVICT_MAX 1 +#endif + +; ---- Monitor entry points ---- + +COUT = $fded +COUT1 = $fdf0 +HOME = $fc58 +VTAB = $fc22 +SETTXT = $fb39 +PRBYTE = $fdda + +AUXMOVE = $c311 +SLOT3 = $c300 + +CSW = $36 +A1 = $3c +A2 = $3e +A4 = $42 + +WNDLFT = $20 +WNDWDTH = $21 +WNDTOP = $22 +WNDBTM = $23 +CH = $24 +OURCH = 1403 +CV = $25 +INVFLG = $32 + +KBD = $c000 +KBDSTRB = $c010 +CLR80STORE = $c000 +SET80STORE = $c001 +CLR80COL = $c00c +TXTPAGE1 = $c054 +TXTPAGE2 = $c055 +ALTCHRSET_OFF = $c00e +ALTCHRSET_ON = $c00f + +; ALTZP picks main or auxiliary for the zero +; page, the stack and $d000-$ffff, all three +; together. The language card switches then +; apply to whichever side ALTZP has selected. + +CLRALTZP = $c008 +SETALTZP = $c009 + +; RAMRD and RAMWRT pick main or auxiliary for +; everything from $0200 to $bfff, independently +; for reads and writes. 80STORE overrides them +; for the text and hires pages, so it has to be +; off for these to mean anything. + +CLRAUXRD = $c002 +SETAUXRD = $c003 +CLRAUXWR = $c004 +SETAUXWR = $c005 + +LCRD2 = $c080 ; read bank 2 +LCROM2 = $c082 ; rom, bank 2 latched +LCWR2 = $c083 ; read/write bank 2, + ; access twice +LCRD1 = $c088 ; read bank 1 +LCWR1 = $c08b ; read/write bank 1, + ; access twice + +; ---- auxiliary memory ---- + +AUXCACHESTART = $800 +AUXCACHEEND = $c000 +AUXCACHEPAGES = (AUXCACHEEND-AUXCACHESTART)>>8 + +; Slots are indexed by the virtual page +; number modulo AUXCACHEPAGES, computed by +; auxslot with repeated subtraction, so the +; size does not have to be a power of two -- +; anything up to 255 pages works. +; +; auxslot hands back the quotient along with +; the remainder, and since +; page = slot + quotient * AUXCACHEPAGES +; the quotient alone identifies the page +; occupying a slot. That makes the tag one +; byte instead of two, which both fits the +; table below $03d0 (where the Monitor and +; ProDOS keep their vectors) and lifts the +; 128-slot ceiling that two-byte tags imposed +; on page 3. + +AUXCACHETAG = $300 ; AUXCACHEPAGES bytes + +; The undo ring lives in the auxiliary +; language card, so the page cache keeps all +; of aux $0800-$bfff. +; +; AUXMOVE is no help up there. It reaches +; auxiliary memory by flipping RAMRD and +; RAMWRT, and those switches stop at $bfff -- +; above that the bank is chosen by ALTZP +; instead, which also swaps the zero page and +; the stack. undomove drives the banking +; itself, and presents the two $d000 banks as +; one flat 16 kB range +; +; logical $c000-$cfff = bank 1 $d000-$dfff +; logical $d000-$ffff = bank 2 $d000-$ffff + +AUXUNDOLO = $c000 ; logical +AUXUNDOPAGES = 256-(AUXUNDOLO>>8) +AUXUNDOBANK2 = $d0 ; logical page from + ; which bank 2 serves + +; ---- frontend zero page ---- + +ioparam = $06 ; word, used by the engine +wrappos = $08 +xpos = $09 +pendspc = $0a +f_temp = $0b +f_temp2 = $0c +coutx = $0d +couty = $0e +tr0 = $0f +tr1 = $10 +stxoffs = $11 +statush = $12 +cury = $13 +seed = $14 ; word +rp_page = $16 +savedch = $17 +savedcv = $18 +strptr = $19 ; word +strow = $1b +scrw = $1c ; 40 or 80 +col80 = $1d ; $80 if the //e 80-column + ; firmware is driving COUT +foldup = $1e ; $80 to fold output to + ; upper case +auxram = $1f ; $80 if aux RAM is usable + +; ---- frontend buffers ---- + +; wrapbuf doubles as the buffer for status +; row 0, exactly as on the C64. io_sprepare +; clears it, io_sputc fills it via stxoffs, +; and io_scommit copies it to the screen. +; The two uses never interleave, because +; stflag routes every character to exactly +; one of io_mputc and io_sputc. + +wrapbuf = $0200 ; 80 bytes +zpbuf = $0250 ; 32, MLI zero-page save + +auxregsbuf = $0270 ; AUXMOVE zero-page save + +; Undo ring bookkeeping. Snapshots are all +; the same size, so the ring is a plain array +; of equal slots starting at AUXUNDOLO. Each +; slot is rounded up to whole pages, which +; puts every slot on a page boundary and keeps +; the bookkeeping to single bytes -- slots are +; tracked by index, and a slot address is just +; a msb. + +u_size = $0278 ; word, snapshot size +u_pages = $027a ; slot stride, in pages +u_max = $027b ; slot capacity +u_count = $027c ; snapshots stored +u_head = $027d ; index of the next slot +u_ready = $027e ; nonzero once sized +u_dir = $027f ; $80 = main to aux +u_page = $0280 ; logical msb of the slot + ; undomove is working on + +; undomove state. It lives here, and not in +; the zero page, because ALTZP swaps the zero +; page out from under the copy loop while the +; language card is banked in. Page 2 is +; unaffected by ALTZP. + +u_cur = $0281 ; logical page in flight +u_auxhi = $0282 ; msb it is banked in at +u_mainhi = $0283 ; msb on the main side +u_full = $0284 ; whole pages still to go +u_tail = $0285 ; bytes in the last page +u_limit = $0286 ; bytes in this page, + ; 0 meaning 256 + +#if PRODOS || PRORWTS +MACHID = $bf98 +#endif + +#if PRODOS + +#define ROMCALL(addr) jsr addr +#define ROMTAILCALL(addr) jmp addr + +; MLI parameter blocks have to survive into +; the run, so they live down here rather than +; in the init segment. + +p_open = $0288 ; 6 bytes +p_read = $028e ; 8 bytes +p_mark = $0296 ; 5 bytes +p_quit = $029b ; 7 bytes + +MLI = $bf00 +DEVADR = $bf10 +DEVCNT = $bf31 +DEVLST = $bf32 +FILEBUF = $bb00 + +#endif + +#if PRORWTS + +; for ProRWTS, some of engine gets put at $d000-$ffff +#define A2_ENGINE_HIMEM + +; Only ProRWTS2's own init calls the MLI, and +; only once. When 0boot has booted us there is +; no ProDOS at all and mlistub answers it. + +MLI = $bf00 + +#define ROMCALL(addr) bit LCROM2:jsr addr:bit LCRD1 +#define ROMTAILCALL(addr) ROMCALL(addr):rts + +; ProRWTS2 lives in bank 2 and writes to its own +; buffers up there, so it needs the bank write +; enabled, which takes two reads of the switch. + +#define RWTSCALL(addr) lda LCWR2:lda LCWR2:jsr addr:lda LCRD1 + +next_rpagelo = $029e +next_rpagehi = $029f +rwregsbuf = $02a0 ; PRORWTS zero-page save, + ; rwregs_count bytes + +; Arguments for rwts_openfile. opendir runs the +; transfer as part of the open, so command, size +; and buffer all have to be set before the call. + +sv_cmd = $02cd ; 0 seek, 1 read, 2 write +sv_size = $02ce ; word, bytes to transfer +sv_addr = $02d0 ; word, main memory buffer +sv_drive = $02d2 ; drive to open on, 0 boot +cur_drive = $02d3 ; drive ProRWTS2 is on now + +rwregs_first = $3b +rwregs_last = $67 +rwregs_count = rwregs_last - rwregs_first + 1 + +prorwts_zp = $50 +prorwts_status = prorwts_zp+0 +prorwts_auxreq = prorwts_zp+1 +prorwts_sizelo = prorwts_zp+2 +prorwts_sizehi = prorwts_zp+3 +prorwts_reqcmd = prorwts_zp+4 +prorwts_ldrlo = prorwts_zp+5 +prorwts_ldrhi = prorwts_zp+6 +prorwts_namlo = prorwts_zp+7 +prorwts_namhi = prorwts_zp+8 + +prorwts_reloc = $d000 +prorwts_rdwrpart = prorwts_reloc + 0 +prorwts_opendir = prorwts_reloc + 3 +#endif + +; ProDOS loads a SYS file at $2000, +; but the interpreter has to run at $800. + + * = $2000 +#if PRODOS || PRORWTS +; 0boot enters at $2003 which is how we +; know there is no ProDOS underneath us and the +; MLI/global page has to be faked before ProRWTS2 +; can initialise. + +#if PRORWTS + jmp boot_entry + jmp raw_entry +#endif + +; First, we move the mover to $300... +boot_entry + .( +#if PRORWTS + ; we have to call prorwts2_init here first, + ; before we copy over ProDOS in the language card + jsr prorwts2_init +#endif + ldx #moverlen +copy + lda moversrc,x + sta $0300,x + dex + bpl copy + + jmp $0300 + .) + +#if PRORWTS +; 0boot loads us the way ProDOS would and leaves +; DEVNUM at $bf30, so all that is missing is the +; MLI. Plant the stub, then join the normal +; path. + +raw_entry + .( + sei + cld + ldx #mlistublen-1 +copy + lda mlistubsrc,x + sta MLI,x + dex + bpl copy + + jmp boot_entry + .) +#endif + +; Then we run the mover at $300 + moversrc = * + * = $300 +mover + .( + lda #bootcode + sta $01 ; source = bootcode + + lda #$00 + sta $02 + lda #$08 + sta $03 ; dest = $0800 + + ldx #SAFEPG-$8 + jsr pageloop + +; ProRWTS requires we move engine code to $d000-$ffff +; and the init segment +#if PRORWTS +himemsegsec = engine_reloc + $2000 - $800 + boothdrlen + lda #himemsegsec + sta $01 ; source = bootcode + + lda #$00 + sta $02 + lda #$d0 + sta $03 ; dest = $d000 + + lda LCWR1 ; write lc_bank = 1 + lda LCWR1 + + ; exactly the himem segment, so that + ; whatever is above it -- including the + ; vectors at $fffa -- is left alone + + ldx #(himem_end-himem_start+$ff)>>8 + jsr pageloop + lda LCRD1 ; read lc_bank = 1 + +initsegsec = initsegment + $2000 - $800 + boothdrlen + himem_end - himem_start + lda #initsegsec + sta $01 ; source = bootcode2 + + lda #initsegment + sta $03 ; dest = initsegment + + ldx #SAFEPG-$8 + jsr pageloop + jsr swaprwregs ; load prorwts registers into save bank +#endif + jmp coldstart + +pageloop + ldy #0 +byteloop + lda ($00),y + sta ($02),y + iny + bne byteloop + + inc $01 + inc $03 + dex + bne pageloop + rts + .) + +moverlen = * - $300 + +#if PRORWTS +; The stub is assembled for $bf00 but lives in +; the boot header, right behind the mover. + + mlistubsrc = moversrc + moverlen + * = MLI +mlistub + .( + ; ProRWTS2 asks for the prefix once and + ; takes its "no prefix" path when the + ; answer is empty, so it never makes a + ; second call -- an empty $0200 is the + ; whole of the emulation. The call number + ; and the parameter pointer sit inline + ; after the jsr, so the return address has + ; to step over three bytes. + + tsx + lda $0101,x + clc + adc #3 + sta $0101,x + lda $0102,x + adc #0 + sta $0102,x + + lda #0 + sta $0200 ; prefix length + clc ; and no error + rts + .) +mlistublen = * - MLI +#else +mlistublen = 0 +#endif + +boothdrlen = moverlen + mlistublen + moversrc - $2000 +bootcode = $2000 + boothdrlen + +#endif + +; ===================================== +; Main code +; ===================================== + + * = $0800 + +do_foldup + .( + bit foldup + bpl nofold + + cmp #'a' + bcc nolower + + cmp #'z'+1 + bcs nolower + + and #$df +nolower +nofold + rts + .) + +io_mputc + ; input a = char + + .( + cmp #$20 + beq space + bcc ignore + + cmp #$80 + bcs extended +plain + jsr do_foldup + ldx xpos + cpx scrw + bcs wrap +postwrap + ldx wrappos + sta wrapbuf,x + inc wrappos + inc xpos + + cmp #'-' + beq dash +ignore + rts +dash + jmp io_mflush +space + jsr io_mflush + inc pendspc + inc xpos + rts +wrap + pha + jsr io_mline_raw + ldx wrappos + stx xpos + jsr io_mflush + pla + jmp postwrap +extended + jsr translit + + lda tr0 + jsr io_mputc ; always < $80 + + lda tr1 + beq done + + jmp io_mputc ; always < $80 +done + rts + .) + +io_mflush + .( + ldx pendspc + beq nospc +spcloop + lda #$a0 + jsr cout + dex + bne spcloop + + stx pendspc +nospc + ldx #0 + cpx wrappos + beq done +loop + lda wrapbuf,x + ora #$80 + jsr cout + inx + cpx wrappos + bne loop +done + lda #0 + sta wrappos + rts + .) + +io_mline + jsr io_mflush +io_mline_raw + .( + ldx xpos + dex + cpx scrw ; signed cmp + bpl nocr ; don't CR if we filled entire line + lda #$8d + jsr cout +nocr + lda #0 + sta xpos + sta pendspc + + ; wrappos is deliberately preserved -- + ; word-wrap path carries a pending word + ; across the line break. + + ldx cury + cpx #23 + bcs nobump + + inc cury +nobump + rts + .) + +io_mclear + .( + lda #0 + sta xpos + sta pendspc + sta wrappos + + jsr clrwin ; clears the window + ; only, so the status + ; area survives + lda statush + sta cury + rts + .) + + ; TODO some bugs in 80-column mode +io_mstyle + rts + ; input a = style bits + /* + .( + pha + jsr io_mflush + pla + and #6 ; bold and italic + beq normal + + jmp set_inverse +normal + jmp set_normal + .) + */ + +; TODO use MouseText? +io_mprogress + ; input x = progress + ; input y = total + ; where 0 <= x <= y + ; and y = + ; (width << PRSHIFT) - PREXTRA + + .( + jsr io_mflush + + stx f_temp + sty f_temp2 + + lda #$db ; '[' + jsr cout + + ldx #0 +loop + lda #$a0 + cpx f_temp + bcs past + + lda #$bd ; '=' +past + jsr cout + inx + cpx f_temp2 + bcc loop + + lda #$dd ; ']' + jmp cout + .) + +; ===================================== +; Status bar area +; ===================================== + +; The status area occupies the top statush +; rows. It is kept outside the text window +; (WNDTOP = statush) so that every scroll +; performed by COUT leaves it alone. To +; write into it, the window is opened up +; temporarily and closed again by io_scommit. + +io_sprepare + ; input a = height + + .( + sta statush + + lda CH + bit col80 + bpl noourch + lda OURCH +noourch + sta savedch + lda cury + sta savedcv + + ; clear the status rows by pointing the + ; window at them and homing + + lda #0 + sta WNDTOP + lda statush + sta WNDBTM + jsr clrwin + + lda statush + sta WNDTOP + lda #24 + sta WNDBTM + + ; blank the row 0 buffer + + ldy scrw + dey + lda #$20 +clr0 + sta wrapbuf,y + dey + bpl clr0 + + ; put the cursor back, clamped into the + ; main area + + lda savedcv + cmp statush + bcs keep + + lda statush + ldx #0 + stx savedch +keep + sta cury + tay + ldx savedch + jsr gotoxy + rts + .) + +io_slocate + ; input x = column + ; input y = row + ; + ; Row 0 is buffered in wrapbuf and + ; committed by io_scommit. Rows 1 and up + ; are written straight to the screen. + + .( + stx stxoffs + + dey + bpl notrow0 + + lda #$ff + sta strow + rts +notrow0 + iny + sty strow + + ; open the window over the status area so + ; that gotoxy can reach it + + lda #0 + sta WNDTOP + ldx stxoffs + jsr gotoxy + rts + .) + +io_sputc + ; input a = char + + .( + jsr do_foldup + ldy strow + bpl screen + + ldy stxoffs + cpy scrw + bcs skip + + sta wrapbuf,y + inc stxoffs +skip + rts +screen + ora #$80 + jsr cout + inc stxoffs + rts + .) + +io_sprogress + ; input x = progress + ; input y = total + ; where 0 <= x <= y + ; and y = + ; (width << PRSHIFT) - PREXTRA + + .( + stx f_temp + sty f_temp2 + + lda #'[' + jsr io_sputc + + ldx #0 +loop + lda #$20 + cpx f_temp + bcs past + + lda #'=' +past + jsr io_sputc + inx + cpx f_temp2 + bcc loop + + lda #']' + jmp io_sputc + .) + +io_scommit + .( + lda #0 + sta WNDTOP + + ldx #0 + ldy #0 + jsr gotoxy + jsr set_inverse + + ldx #0 +loop + lda wrapbuf,x + ora #$80 + jsr cout + inx + cpx scrw + bcc loop + + lda statush + sta WNDTOP + + ldx xpos + ldy cury + jsr gotoxy + jsr set_normal ; do we have to restore old value? + rts + .) + +; ===================================== +; Input +; ===================================== + +io_getc + ; output a = char + + .( + jsr io_mflush + jmp getkey + .) + +io_gets + ; input ioparam = buffer + ; (room for 64 chars) + ; output y = length + + .( + jsr io_mflush + + ldy #0 +loop + sty f_temp2 + jsr getkey + ldy f_temp2 + + cmp #13 + beq done + + cmp #8 + beq backspace + + cmp #$7f + beq backspace + + cmp #$20 + bcc loop + + cpy #64 + bcs loop + + sta (ioparam),y + iny + + ora #$80 + jsr cout + inc xpos + jmp loop +backspace + cpy #0 + beq loop + + dey + lda #$88 + jsr cout + lda #$a0 + jsr cout + lda #$88 + jsr cout + dec xpos + jmp loop +done + sty f_temp2 + jsr io_mline_raw + ldy f_temp2 + rts + .) + +getkey + ; output a = key, high bit clear + ; + ; A block cursor is drawn while waiting. + ; The wait loop also stirs the random + ; seed, which is the only entropy source + ; on a machine with no clock. + + .( + jsr set_inverse + lda #$a0 + jsr cout + lda #$88 + jsr cout + jsr set_normal +wait + inc seed+0 + bne nohi + + inc seed+1 +nohi + lda KBD + bpl wait + + sta KBDSTRB + and #$7f + pha + + lda #$a0 + jsr cout + lda #$88 + jsr cout + + pla + rts + .) + +; ===================================== +; Character translation +; ===================================== + +translit + ; input a = char code, >= $80 + ; output tr0, tr1 + ; tr1 is zero for a single character + + .( + jsr lookupchar + bcs unknown + + ldy #2 + lda (ioparam),y + bne unknown ; outside the BMP + + iny + lda (ioparam),y + sta f_temp + iny + lda (ioparam),y + sta f_temp2 + + ldx #NTRANS-1 +loop + lda trhi,x + cmp f_temp + bne next + + lda trlo,x + cmp f_temp2 + beq found +next + dex + bpl loop +unknown + lda #'?' + sta tr0 + lda #0 + sta tr1 + rts +found + lda trc0,x + sta tr0 + lda trc1,x + sta tr1 + rts + .) + +; ===================================== +; Screen primitives +; ===================================== + +cout + ; input a = char, high bit set + ; + ; COUT and the 80-column firmware both + ; clobber x and y. + + .( + stx coutx + sty couty + ROMCALL(COUT) + ldx coutx + ldy couty + rts + .) + +#ifdef DEBUG +prbyte + .( + stx coutx + sty couty + ROMCALL(PRBYTE) + ldx coutx + ldy couty + rts + .) +#endif + +clrwin + ; clear the text window and home the + ; cursor within it + + .( + bit col80 + bmi firmware + + ROMTAILCALL(HOME) +firmware + lda #$8c ; ctrl-L + jmp cout + .) + +gotoxy + ; input x = column, y = row + + .( + stx CH +; https://www.atarimagazines.com/compute/issue76/Feedback_3.php + stx OURCH ; synchronize CH and OURCH + sty CV + ROMTAILCALL(VTAB) + .) + +set_inverse + .( + bit col80 + bmi firmware + + lda #$3f + sta INVFLG + rts +firmware + lda #$8f ; ctrl-O + jmp cout + .) + +set_normal + .( + bit col80 + bmi firmware + + lda #$ff + sta INVFLG + rts +firmware + lda #$8e ; ctrl-N + jmp cout + .) + +; ===================================== +; System +; ===================================== + +;; TODO ProDOS thinks the system is corrupted? +;; "RESTART SYSTEM-$0B" +io_quit + .( +; ProRWTS2 can't quit, so we reboot +#if PRODOS + jsr set_normal + jsr io_mline + + lda #$65 ; QUIT + ldx #p_quit + jsr mlicall +#endif + inc $3f4 ; modify pwrup checksum so we reboot +halt + bit LCROM2 + jmp halt ; wait for reset + .) + +io_restart + ; The engine reloads the story itself. + ; All the frontend has to do is throw + ; away the undo history. + + .( +#if UNDO + lda #0 + sta u_count + sta u_head +#endif + rts + .) + +io_random + ; output x = lsb, y = msb + ; range is 0..7fff + + .( + lsr seed+1 + ror seed+0 + bcc noeor + + lda seed+1 + eor #$b4 + sta seed+1 +noeor + ldx seed+0 + lda seed+1 + and #$7f + tay + rts + .) + +; The story file is reopened after every save +; and load, so its name has to outlive the init +; segment that opens it the first time. + +#if PRODOS +pathname_full + .byt 5+8+2 + .asc "/AA.STORY/STORY" +#endif +pathname_short + .byt 5 + .asc "STORY" + +; ===================================== +; Saved games +; ===================================== +; +; ProDOS wants a kilobyte of buffer for +; every open file and main memory has none to +; spare, so STORY is closed for the duration +; and lends SAVEFILE its buffer. Nothing is +; lost by that, io_readpage sets the mark +; before every read, so the reopened file does +; not have to remember where it was. + +#if PRODOS && SAVERESTORE + +io_save + ; input x = size lsb + ; input y = size msb + ; data at SAVEADDR + ; output c = success + + .( + stx sv_size+0 + sty sv_size+1 + + jsr closestory + + lda #$c0 ; CREATE + ldx #p_create + jsr mlicall + bcc created + + cmp #$47 ; already there, and + bne fail ; about to be rewritten +created + jsr opensave + bcs fail + + lda #SAVEADDR + sta p_rw+3 + lda sv_size+0 + sta p_rw+4 + lda sv_size+1 + sta p_rw+5 + + lda #$cb ; WRITE + ldx #p_rw + jsr mlicall + bcs shut + + ; a short save must not leave the tail of + ; a longer one behind it + + lda sv_size+0 + sta p_eof+2 + lda sv_size+1 + sta p_eof+3 + lda #0 + sta p_eof+4 + lda #$d0 ; SET_EOF + ldx #p_eof + jsr mlicall + bcs shut + + jsr closesave + jsr reopenstory + sec + rts +shut + jsr closesave +fail + jsr reopenstory + clc + rts + .) + +io_load + ; verify AASV header, 12 bytes + ; if ok, put file at SAVEADDR + ; and set c + ; otherwise, clear c + + .( + jsr closestory + jsr opensave + bcs fail + + ; the 12 byte FORM header + ; length inside it says how much is left + + lda #SAVEADDR + sta p_rw+3 + lda #12 + sta p_rw+4 + lda #0 + sta p_rw+5 + jsr readsave + bcs shut + + ; bytes 6 and 7 are the length; the rest + ; of the header is a fixed signature + + ldy #11 +check + cpy #6 + bcc compare + + cpy #8 + bcc skip +compare + lda SAVEADDR,y + cmp form0000aasv,y + bne shut +skip + dey + bpl check + + ; four of those length bytes, the "AASV", + ; arrived with the header + + lda SAVEADDR+7 + sec + sbc #4 + sta p_rw+4 + lda SAVEADDR+6 + sbc #0 + sta p_rw+5 + + lda #SAVEADDR+12 + sta p_rw+3 + jsr readsave + bcs shut + + jsr closesave + jsr reopenstory + sec + rts +shut + jsr closesave +fail + jsr reopenstory + clc + rts + .) + +p_open_save + .byt 3 + .word sv_name + .word FILEBUF + .byt 0 + +opensave + ; output c = error + .( + lda #$c8 ; OPEN + ldx #p_open_save + jsr mlicall + bcs done + + lda p_open_save+5 + sta p_rw+1 + sta p_eof+1 + clc +done + rts + .) + +readsave + ; ProDOS reports a short read as an error, + ; which is exactly what a truncated + ; savefile deserves + + lda #$ca ; READ + ldx #p_rw + jmp mlicall + +closesave + lda p_rw+1 + jmp closefile + +closestory + lda p_read+1 + ;jmp closefile + +closefile + ; input a = ref num + + sta p_close+1 + lda #$cc ; CLOSE + ldx #p_close + jmp mlicall + +reopenstory + ; Losing the story file here would leave + ; nothing to go back to, so a failure is + ; as fatal as any other disk error. + + .( + lda #$c8 ; OPEN + ldx #p_open + jsr mlicall + bcs err + + lda p_open+5 + sta p_read+1 + sta p_mark+1 + rts +err + jmp diskerror + .) + +; Parameter blocks and names for the above. +; These outlive the init segment, so unlike +; parmsrc they are assembled where they stand. + +sv_size .word 0 + +p_create + .byt 7 + .word sv_name + .byt $c3 ; read, write, rename, + ; destroy + .byt $06 ; BIN + .word SAVEADDR ; aux type + .byt 1 ; standard file + .word 0 ; date, 0 = ask ProDOS + .word 0 ; time + +p_rw + .byt 4 + .byt 0 ; ref num + .word 0 ; data buffer + .word 0 ; request count + .word 0 ; bytes transferred + +p_eof + .byt 2 + .byt 0 ; ref num + .byt 0,0,0 ; new end of file + +p_close + .byt 1 + .byt 0 ; ref num + +sv_name + .byt 8 + .asc "SAVEFILE" + +#elseif PRORWTS && SAVERESTORE + +; One slot, a file called SAVEFILE alongside +; STORY. ProRWTS2 can only write to a file that +; is already there and it cannot change the +; length, so the bundler puts a SAVEMAXBYTES long +; SAVEFILE on the disk and a save overwrites it +; in place. A save that would not fit is +; refused rather than truncated. +; +; Transfers happen in whole 512 byte blocks -- +; opendir rounds the size up itself -- so a save +; carries some slack at the end. Nothing reads +; it, the length inside the AASV header says +; where the image really stops. + +io_save + ; input x = size lsb + ; input y = size msb + ; data at SAVEADDR + ; output c = success + + .( + stx sv_size+0 + sty sv_size+1 + + ; the file on disk cannot grow, so a + ; bigger image than that has nowhere + ; to go + + lda #SAVEMAXBYTES + sbc sv_size+1 + bcc fail + + lda #2 ; cmdwrite + jsr savexfer + bne fail + + sec + rts +fail + clc + rts + .) + +io_load + ; verify AASV header, 12 bytes + ; if ok, put file at SAVEADDR + ; and set c + ; otherwise, clear c + + .( + ; the whole file, since its length is + ; what the bundler wrote and not what + ; the last save happened to be + + lda #SAVEMAXBYTES + sta sv_size+1 + + lda #1 ; cmdread + jsr savexfer + bne fail + + ; bytes 6 and 7 are the length; the rest + ; of the header is a fixed signature. An + ; unwritten SAVEFILE is all zeroes and + ; fails here. + + ldy #11 +check + cpy #6 + bcc compare + + cpy #8 + bcc skip +compare + lda SAVEADDR,y + cmp form0000aasv,y + bne fail +skip + dey + bpl check + + sec + rts +fail + clc + rts + .) + +savexfer + ; input a = prorwts command + ; input sv_size = byte count + ; output a = status, z set on success + ; + ; Moves the savefile to or from SAVEADDR + ; and puts STORY back, because ProRWTS2 + ; has room for one open file at a time. + ; + ; The bundler puts SAVEFILE on the boot + ; disk, so it is on drive 0 even when the + ; story came off the other drive. + + .( + sta sv_cmd + lda #SAVEADDR + sta sv_addr+1 + lda #0 + sta sv_drive + + ldx #sv_name + jsr rwts_openfile + + pha + jsr reopenstory + pla ; sets z for the caller + rts + .) + +sv_name + .byt 8 + .asc "SAVEFILE" + +#endif + +#if UNDO + +; ===================================== +; Undo, in auxiliary memory +; ===================================== + +; Snapshots go into a ring of equal-sized +; slots starting at AUXUNDOLO. The engine +; asks for the same size every time, so the +; layout is computed once, on the first save. +; If not even one slot fits, undo just fails. + +io_undosupp + ; output c = undo supported + + .( + lda auxram + asl + rts + .) + +io_saveundo + ; input x = byte size lsb + ; input y = byte size msb + ; input ioparam = start of data + ; output c = success + + .( + jsr undoprep + bcc no + + lda u_head + jsr undoaddr + lda #$80 + sta u_dir + jsr undomove + + ; a full ring forgets its oldest entry, + ; which is the one u_head just landed on + + lda u_count + cmp u_max + bcs full + + inc u_count +full + ldx u_head + inx + cpx u_max + bcc store + + ldx #0 +store + stx u_head + + sec + rts +no + clc + rts + .) + +io_loadundo + ; input x = byte size lsb + ; input y = byte size msb + ; input ioparam = dest address + ; output a = status + ; 0 ok + ; 1 no more entries + ; 2 error + + .( + jsr undoprep + bcc err + + lda u_count + beq empty + + ; step back onto the newest snapshot + + ldx u_head + bne back + + ldx u_max +back + dex + stx u_head + txa + jsr undoaddr + + lda #0 + sta u_dir + jsr undomove + + dec u_count + lda #0 + rts +empty + lda #1 + rts +err + lda #2 + rts + .) + +undoprep + ; input x = byte size lsb + ; input y = byte size msb + ; output c = the ring holds at least one + ; snapshot of that size + + .( + bit auxram + bpl nofit + + lda u_ready + beq init + + cpx u_size+0 + bne init + + cpy u_size+1 + bne init +check + lda u_max + beq nofit + + sec + rts +nofit + clc + rts +init + jsr undoinit + jmp check + .) + +undoinit + ; input x = byte size lsb + ; input y = byte size msb + ; + ; Rounds the snapshot up to whole pages + ; and counts how many of those fit, one + ; subtraction at a time rather than + ; dividing. It runs once. + + .( + stx u_size+0 + sty u_size+1 + + lda #1 + sta u_ready + + lda #0 + sta u_max + sta u_count + sta u_head + + txa ; partial page? + beq whole + + iny +whole + tya + beq none ; empty, or 64 kB and up + + sta u_pages + + lda #AUXUNDOPAGES +loop + sec + sbc u_pages + bcc none + + inc u_max + bne loop ; always +none + rts + .) + +undoaddr + ; input a = slot index + ; output u_page = msb of that slot + ; clobbers a, x + + .( + tax + lda #>AUXUNDOLO +loop + dex + bmi done + + clc + adc u_pages + bne loop ; always, the last + ; slot ends at $ffff +done + sta u_page + rts + .) + +undomove + ; input u_page = logical msb of the slot + ; input ioparam = address in main memory + ; input u_size = bytes to copy + ; input u_dir bit 7 set = main to aux + ; + ; Copies a page at a time between main + ; memory and the auxiliary language card. + ; + ; ALTZP brings in the auxiliary zero page + ; and stack along with the language card, + ; so between SETALTZP and CLRALTZP there + ; is no zero page addressing, no stack + ; traffic and no jsr. The addresses ride + ; in the copy loop itself and the counters + ; sit on page 2, which ALTZP leaves alone. + ; Everything else the loop touches is code + ; below $c000, which ALTZP does not reach + ; either. + ; + ; Interrupts stay off for the whole copy, + ; because a ProDOS interrupt handler would + ; come up on the wrong zero page. + + .( + lda u_page + sta u_cur + lda ioparam+1 + sta u_mainhi + + lda u_size+1 + sta u_full + lda u_size+0 + sta u_tail + + ; The main side is not page aligned, so + ; the two operands carry different low + ; bytes. Neither low byte ever changes -- + ; only the msbs walk, one page at a time. + + bit u_dir + bmi tolow + + lda #0 ; aux is the source + sta srclo + lda ioparam+0 + sta dstlo + jmp begin +tolow + lda ioparam+0 + sta srclo + lda #0 + sta dstlo +begin + php + sei + sta SETALTZP + + ; ---- no zero page, no stack ---- +nextpg + lda u_full + beq lastpg + + dec u_full + lda #0 + sta u_limit ; 0 means 256 + beq bank ; always +lastpg + lda u_tail + beq done + + sta u_limit + lda #0 + sta u_tail ; there is only one +bank + ; Bank in whichever half holds the logical + ; page u_cur, and note the msb it shows up + ; at. Bank 1 is only reachable at its own + ; $d000, so the low half of the ring is + ; shifted up by the difference. + + lda u_cur + cmp #AUXUNDOBANK2 + bcs two + + clc + adc #AUXUNDOBANK2-(AUXUNDOLO>>8) + sta u_auxhi + bit u_dir + bmi wr1 + + lda LCRD1 + lda LCRD1 + jmp operands +wr1 + lda LCWR1 + lda LCWR1 + jmp operands +two + sta u_auxhi + bit u_dir + bmi wr2 + + lda LCRD2 + lda LCRD2 + jmp operands +wr2 + lda LCWR2 + lda LCWR2 +operands + bit u_dir + bmi frommain + + lda u_auxhi + sta srchi + lda u_mainhi + sta dsthi + jmp copy +frommain + lda u_mainhi + sta srchi + lda u_auxhi + sta dsthi +copy + ldy #0 +byte +srclo = *+1 +srchi = *+2 + lda $ffff,y +dstlo = *+1 +dsthi = *+2 + sta $ffff,y + iny + cpy u_limit + bne byte + + inc u_cur + inc u_mainhi + jmp nextpg +done + sta CLRALTZP + + ; ---- zero page and stack are back ---- +#if PRODOS + lda LCROM2 ; rom in, the way a + ; system program runs +#elseif PRORWTS + lda LCRD1 ; ram in, for ProRWTS2 +#endif + plp + rts + .) + +#endif + +; ===================================== +; Storage +; ===================================== + +swapauxregs + .( + ldx #7 +save + lda A1,x + pha + lda auxregsbuf,x + sta A1,x + pla + sta auxregsbuf,x + dex + bpl save + rts + .) + +#if PRORWTS +swaprwregs + .( + ldx #rwregs_count-1 +save + lda rwregs_first,x + pha + lda rwregsbuf,x + sta rwregs_first,x + pla + sta rwregsbuf,x + dex + bpl save + rts + .) +#endif + + +auxslot + ; input f_temp = virtual page, l-e word + ; output y = aux cache slot + ; output a = quotient, i.e. the tag + ; clobbers x, f_temp + + .( + ldx #$ff ; quotient +loop + inx + lda f_temp + sec + sbc #AUXCACHEPAGES + tay + lda f_temp+1 + sbc #0 + bcc done + + sty f_temp + sta f_temp+1 + bcs loop ; always +done + ldy f_temp + txa + rts + .) + +io_readpage + ; input x = physical ram target page + ; input ioparam = virtual address 23..8, l-e + ; output a = physical ram target page + + .( + stx rp_page + + ; do we have it in aux cache? + bit auxram + bpl noloadfromaux + ; check aux cache tag for this slot + lda ioparam + sta f_temp + lda ioparam+1 + sta f_temp+1 + jsr auxslot + cmp AUXCACHETAG,y + bne noloadfromaux + + ; we have it, transfer page from aux cache + jsr swapauxregs + lda #0 + sta A1+0 + sta A4+0 + tya ; y is preserved + clc + adc #>AUXCACHESTART + sta A1+1 ; src in aux + sta A2+1 ; src end in aux + lda rp_page + sta A4+1 ; dest in main + ; A2 = A1 + size - 1, inclusive + lda #$ff + sta A2+0 ; src ending addr + clc ; aux -> main + jsr AUXMOVE + jsr swapauxregs +#ifdef DEBUGIO + lda #'C' + jsr cout +#endif + jmp nosaveinaux ; page is loaded + +noloadfromaux + ; byte position = virtual page << 8 +#if PRODOS + lda #0 + sta p_mark+2 + sta p_read+2 + lda ioparam + sta p_mark+3 + lda ioparam+1 + sta p_mark+4 + + lda rp_page + sta p_read+3 + + lda #$ce ; SET_MARK + ldx #p_mark + jsr mlicall + bcs goterr + + lda #$ca ; READ + ldx #p_read + jsr mlicall + bcs goterr +#endif + +#if PRORWTS + jsr swaprwregs +seekoffset = f_temp +; compute desired - next seek offset +redoseek + lda ioparam + sec + sbc next_rpagelo + sta seekoffset + lda ioparam+1 + sbc next_rpagehi + sta seekoffset+1 +; are we seeking backwards? if so rewind + bmi rewind ; negative, rewind + ora seekoffset + beq noseek ; equal, no seek + bne norewind ; positive, no rewind + +; be kind, rewind +rewind +#ifdef DEBUGIO + lda #'R' + jsr cout +#endif + jsr rwts_rewind + beq redoseek ; recompute desired - next +norewind +#ifdef DEBUGIO + lda #'S' + jsr cout +#endif + lda #0 ; cmdseek + sta prorwts_reqcmd + sta prorwts_sizelo + lda seekoffset+1 + beq nobigseek +; big seek = $ff00 bytes +#ifdef DEBUGIO + lda #'B' + jsr cout +#endif + lda #$ff + sta prorwts_sizehi + clc + adc next_rpagelo + sta next_rpagelo + lda #0 + adc next_rpagehi + sta next_rpagehi + RWTSCALL(prorwts_rdwrpart) + jmp redoseek ; recompute +nobigseek + lda seekoffset + sta prorwts_sizehi +#ifdef DEBUGIO + jsr prbyte +#endif + RWTSCALL(prorwts_rdwrpart) +noseek + lda rp_page + sta prorwts_ldrhi + lda #1 ;cmdread + sta prorwts_reqcmd + sta prorwts_sizehi + lda #0 + sta prorwts_sizelo + sta prorwts_ldrlo + RWTSCALL(prorwts_rdwrpart) + + ldy prorwts_status + jsr swaprwregs + tya + beq noerr +noerr + lda ioparam + clc + adc #1 + sta next_rpagelo + lda ioparam+1 + adc #0 + sta next_rpagehi +#endif + +#ifdef DEBUGIO + lda ioparam+1 + jsr prbyte + lda ioparam + jsr prbyte + lda rp_page + jsr prbyte + lda #$a0 + jsr cout +#endif + ; store in aux memory + bit auxram + bpl nosaveinaux + + lda ioparam + sta f_temp + lda ioparam+1 + sta f_temp+1 + jsr auxslot + sta AUXCACHETAG,y ; tag this aux cache slot + + jsr swapauxregs + lda #0 + sta A1+0 + sta A4+0 + tya ; y is preserved + clc + adc #>AUXCACHESTART + sta A4+1 ; dest in aux + + lda rp_page + sta A1+1 ; src in main + sta A2+1 ; src end in main + ; A2 = A1 + size - 1, inclusive + lda #$ff + sta A2 ; src ending addr + sec ; main -> aux + jsr AUXMOVE + jsr swapauxregs + +nosaveinaux + lda rp_page + rts +goterr + jmp diskerror + .) + +#if PRORWTS +rwts_rewind + .( + lda #0 + sta $5b ; treeidx (TODO set B >> 8) + sta $5f ; blkidx + sta $63 ; blkofflo + sta $64 ; blkoffhi + ; next page read will be 0 + sta next_rpagelo + sta next_rpagehi + rts + .) + +rwts_openfile + ; input x = pathname lsb + ; input y = pathname msb + ; input sv_cmd = command + ; input sv_size = byte count + ; input sv_addr = main memory buffer + ; input sv_drive = 0 boot drive, 1 the other + ; output a = status, z set on success + ; + ; opendir falls through into the transfer, + ; so one call both opens the file and moves + ; the data. A command of zero moves + ; nothing and just opens. + ; + ; Bit 7 of the command makes the open swap + ; to the other drive in the slot rather + ; than naming one, so reaching a particular + ; drive means comparing with the one we are + ; on. ProRWTS2 remembers the choice, hence + ; cur_drive alongside it. A machine with + ; one drive ignores the swap, and then both + ; drives are the same drive and everything + ; below still works. + + .( + stx f_temp + sty f_temp2 + + jsr swaprwregs + + lda f_temp + sta prorwts_namlo + lda f_temp2 + sta prorwts_namhi + lda sv_cmd + ldx sv_drive + cpx cur_drive + beq samedrive + stx cur_drive + ora #$80 ; swap drives +samedrive + sta prorwts_reqcmd + lda sv_size+0 + sta prorwts_sizelo + lda sv_size+1 + sta prorwts_sizehi + lda sv_addr+0 + sta prorwts_ldrlo + lda sv_addr+1 + sta prorwts_ldrhi + + RWTSCALL(prorwts_opendir) + + ; opendir leaves the file positioned at + ; the start, so the reader has to agree. + ; This has to happen while the ProRWTS + ; registers are still swapped in. + + jsr rwts_rewind + + ldy prorwts_status + jsr swaprwregs + tya + rts + .) + +reopenstory + ; ProRWTS2 keeps one file open, so STORY + ; has to come back after every save and + ; load. Losing it would leave nothing to + ; go back to, so a failure is as fatal as + ; any other disk error. + + .( + jsr storyparms + jsr rwts_openfile + beq done + + jmp diskerror +done + rts + .) + +storyparms + ; open STORY, transferring nothing + + .( + lda #0 + sta sv_cmd ; cmdseek + sta sv_size+0 + sta sv_size+1 + sta sv_addr+0 + sta sv_addr+1 + lda storydrive + sta sv_drive + ldx #pathname_short + rts + .) + +storydrive + ; Which drive STORY turned up on, 0 being + ; the one we booted from. On a two disk + ; set the boot disk holds the interpreter + ; and SAVEFILE and the story disk goes in + ; the other drive, so this is 1 there. + + .byt 0 +#endif + +#if PRODOS +; ===================================== +; ProDOS glue +; ===================================== + +mlicall + ; input a = command + ; input x/y = parameter block, l/h + ; output c = error, a = error code + ; + ; The engine owns zero page from $40 up + ; and ProDOS is documented to use part of + ; that range, so it is saved across every + ; call. Every caller is disk-bound, so + ; the copy costs nothing in practice. + + .( + sta cmd + stx parm+0 + sty parm+1 + + ldx #31 +save + lda $40,x + sta zpbuf,x + dex + bpl save + + jsr MLI +cmd .byt 0 +parm .word 0 + + php + pha + + ldx #31 +restore + lda zpbuf,x + sta $40,x + dex + bpl restore + + pla + plp + rts + .) + +nodev + ; Stands in for the /RAM driver once the + ; RAM disk has been unhooked. It has to + ; be resident, because ProDOS keeps the + ; pointer. + + lda #$28 ; no device connected + sec + rts +#endif + +diskerror + .( + pha + jsr io_mline + lda #'E' + jsr cout + pla + ROMCALL(PRBYTE) ; error code +#ifdef DEBUG + lda rp_page + jsr prbyte ; error code + lda ioparam + jsr prbyte ; error code +#if PRODOS + lda p_read+3 + jsr prbyte ; error code + lda p_read+2 + jsr prbyte ; error code +#endif +#endif +halt + jmp halt + .) + +; ===================================== +; Resident data +; ===================================== + +; Transliteration table. Text mode draws +; from the character ROM, so anything outside +; ASCII has to be approximated. Codepoints +; are BMP only, and the replacement is at +; most two characters wide. + +trhi + .byt $00,$00,$00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$00,$00 + .byt $20,$20,$20,$20,$20,$20,$20,$20 +trlo + .byt $a0,$ab,$bb,$c4,$c5,$c6,$c9,$d6 + .byt $d8,$dc,$df,$e0,$e1,$e2,$e4,$e5 + .byt $e6,$e7,$e8,$e9,$ea,$eb,$ee,$ef + .byt $f1,$f4,$f6,$f8,$fc,$fd + .byt $10,$13,$14,$18,$19,$1c,$1d,$26 +trc0 + .byt ' ','"','"','A','A','A','E','O' + .byt 'O','U','s','a','a','a','a','a' + .byt 'a','c','e','e','e','e','i','i' + .byt 'n','o','o','o','u','y' + .byt '-','-','-',$27,$27,$22,$22,'.' +trc1 + .byt 0,0,0,0,0,'E',0,0 + .byt 0,0,'s',0,0,0,0,0 + .byt 'e',0,0,0,0,0,0,0 + .byt 0,0,0,0,0,0 + .byt 0,0,'-',0,0,0,0,'.' + +NTRANS = trlo-trhi + +; ===================================== +; Cold start +; ===================================== + +; This sequence has to be resident. +; initengine5 prefills the page cache +; starting at the init segment, so any +; caller living there would be overwritten +; while it ran. initengine5 itself is +; resident for the same reason. + +coldstart + .( + + jsr initsystem + + jsr initengine0 + + ; initengine0 has just set screenw from + ; DEFWIDTH; the real width is whatever + ; was detected. + + lda scrw + jsr setwidth + + jsr initengine1 + jsr initengine2 + jsr initengine3 + jsr initengine4 + jsr initengine5 +#ifdef DEBUG + jsr dumpvars +#endif + jsr io_mclear + jmp startengine + .) + +#ifdef DEBUG +prword .( + pha + txa + pha + pla + tax + pla + rts + .) + +dumpvars + .( + lda #$8d + jsr cout + lda firstpg + jsr prbyte + lda endpg + jsr prbyte + lda #$a0 + jsr cout + + lda vtmsb + jsr prbyte + lda vtptr+1 + jsr prbyte + lda vtptr+0 + jsr prbyte + lda #$a0 + jsr cout + + lda vtsize+1 + jsr prbyte + lda vtsize+0 + jsr prbyte + lda #$a0 + jsr cout + + lda freeptr+1 + jsr prbyte + lda freeptr+0 + jsr prbyte + lda #$a0 + jsr cout + + lda #$8d + jsr cout + + ; heap sizes (b-e words) + lda heapsz + jsr prbyte + lda heapsz+1 + jsr prbyte + lda #$a0 + jsr cout + lda auxsz + jsr prbyte + lda auxsz+1 + jsr prbyte + lda #$a0 + jsr cout + lda ramsz + jsr prbyte + lda ramsz+1 + jsr prbyte + lda #$8d + jsr cout + + ; bases (l-e words) + lda hpbase+1 + jsr prbyte + lda hpbase+0 + jsr prbyte + lda #$a0 + jsr cout + lda auxbase+1 + jsr prbyte + lda auxbase+0 + jsr prbyte + lda #$a0 + jsr cout + lda rambase+1 + jsr prbyte + lda rambase+0 + jsr prbyte + lda #$a0 + jsr cout + lda inbase+1 + jsr prbyte + lda inbase+0 + jsr prbyte + lda #$8d + jsr cout + + lda codeseg+0 + jsr prbyte + lda codeseg+1 + jsr prbyte + lda codeseg+2 + jsr prbyte + lda #$a0 + jsr cout + lda lngbase+1 + jsr prbyte + lda lngbase+0 + jsr prbyte + lda #$a0 + jsr cout + lda mapbase+1 + jsr prbyte + lda mapbase+0 + jsr prbyte + lda #$a0 + jsr cout + lda decoder+1 + jsr prbyte + lda decoder+0 + jsr prbyte + lda #$8d + jsr cout + rts +; jmp verify + .) + +; Read every whole page of the story file +; through io_readpage and Fletcher-8 it, so +; the storage layer can be checked against a +; checksum computed on the host. + +fla = numer +flb = numer+1 + +verify + .( + lda #0 + sta fla + sta flb + sta ioparam + sta ioparam+1 +pgloop + lda #0 + sta phydata + lda firstpg + sta phydata+1 + + ldy #0 + tya +clrloop + sta (phydata),y + iny + bne clrloop + + ldx firstpg + jsr io_readpage + + lda #0 + sta phydata + lda firstpg + sta phydata+1 + + ldy #0 +byteloop + lda (phydata),y + clc + adc fla + sta fla + clc + adc flb + sta flb + iny + bne byteloop + + inc ioparam + bne nov + inc ioparam+1 +nov + lda fla + jsr prbyte + lda flb + jsr prbyte + ; loop while ioparam < vtsize-0 + lda vtsize + clc + adc #1 + sta f_temp + lda vtsize+1 + adc #0 + sta f_temp+1 + + lda ioparam + cmp f_temp + lda ioparam+1 + sbc f_temp+1 + bcc pgloop + + lda #$8d + jsr cout + lda ioparam+1 + jsr prbyte + lda ioparam + jsr prbyte + lda #$a0 + jsr cout + lda flb + jsr prbyte + lda fla + jsr prbyte + lda #$8d + jsr cout +; rts +halt3 jmp halt3 + .) + +dumpram + .( + lda #$8d + jsr cout + ldy #$0 +loop2 + ldx #8 + tya + jsr prbyte + lda #':'+$80 + jsr cout + jsr cout +loop + lda $00,y + jsr prbyte + lda #$a0 + jsr cout + iny + dex + bne loop + lda #$8d + jsr cout + tya + bne loop2 + rts + .) +#endif + +; ===================================== +; Aa-machine Interpreter Engine +; ===================================== + +#include "engine.s" + +; ===================================== +; Initialization +; ===================================== + +; Everything from here on is overwritten by +; page buffers once the engine is running. + +putline + ; input x/y = string, l/h + ; plain ascii, nul terminated + .( + jsr puts_xy + lda #SPC_AUTO + sta rspc + jmp vio_line + .) + +initsystem + .( + ; Apple ][ boot doesn't clear engine zero page so we do it here + lda #0 + ldx #$c0 +clrlp + sta $3f,x + dex + bne clrlp + + ; clear the aux cache lookup table + lda #$ff ; never a valid quotient + ldx #AUXCACHEPAGES +auxclrlp + sta AUXCACHETAG-1,x + dex + bne auxclrlp + + lda #0 + sta wrappos + sta xpos + sta pendspc + sta statush + sta stxoffs + sta cury + sta strow +#if UNDO + sta u_ready + sta u_count +#endif + + lda #$e1 + sta seed+0 + lda #$27 + sta seed+1 + +#if PRORWTS + ROMCALL(setmachid) +#endif + jsr detect + jsr setupvideo +#if PRODOS + jsr initparms + jsr removeram +#endif + jsr banner + jmp openstory + .) + +detect + ; ProDOS has already worked out what it + ; is running on, so ask it rather than + ; poking at soft switches. + ; + ; MACHID + ; (Bit 3 off) BITS 7,6- 00=II 01=II+ 10=IIe 11=/// + ; (Bit 3 on) BITS 7,6- 00=NA 01=NA 10=//c 11=NA + ; BITS 5,4- 00=NA 01=48K 10=64K 11=128K + ; BIT 3 - Modifier for MACHID Bits 7,6. + ; BIT 1=1- 80 Column card + + .( + lda #0 + sta col80 + sta auxram + sta foldup + + lda #40 + sta scrw + + lda MACHID + and #$c0 + cmp #$80 + beq notplus + + ; The Apple ][ and ][+ character + ; generator has no lower case, and an + ; 80-column card in one of those is not + ; the //e firmware, so it goes unused. + + lda #$80 + sta foldup + rts +notplus + sta ALTCHRSET_ON ; display inverse lowercase + lda MACHID + and #$02 + beq no80 + + lda #$80 + sta col80 + lda #80 + sta scrw +no80 + lda MACHID + and #$30 + cmp #$30 ; %10 is 64K, not 128K + bne done + + lda #$80 + sta auxram +done + rts + .) + +#if PRORWTS +; Work out for ourselves what ProDOS would have +; put in MACHID, since under 0boot there is no +; ProDOS to ask and by this point in a ProDOS +; boot the global page is inside the heap. Only +; the three fields detect reads are filled in -- +; the clock bit and the bits 7,6 modifier stay +; clear. + +setmachid + .( + lda #$50 ; ][+, 48K, no 80 columns + ldx $fbb3 + cpx #$06 ; ][e, ][c and IIgs say 6 + bne store + + ; The Pascal 1.1 firmware signature is the + ; same thing ProDOS looks for, so a card in + ; slot 3 that is not a display -- an + ; accelerator, say -- is rejected here too. + + lda #$a0 ; ][e, 64K, no 80 columns + ldx $c305 + cpx #$38 + bne no80 + ldx $c307 + cpx #$18 + bne no80 + ldx $c30b + cpx #$01 + bne no80 + ldx $c30c + cpx #$80 ; high nibble 8 is a display + bcc no80 + cpx #$90 + bcs no80 + + ora #$02 ; 80 columns +no80 + sta MACHID + jsr auxtest + ora MACHID ; aux RAM +store + sta MACHID + rts + .) + +; Returns $10 in a if there is auxiliary RAM, +; which turns the 64K in bits 5,4 into 128K. +; Only ever reached on a ][e or later, so the +; switches are known to exist. The main side of +; the scratch byte is left as it was. +; +; RAMRD is no use for this. It hands reads of +; $0200 to $bfff to the auxiliary side, and that +; includes the opcode fetches of the routine +; doing the testing, so the read-back would come +; out of auxiliary memory as code. ALTZP moves +; the zero page, the stack and $d000-$ffff +; instead, and this routine sits in none of +; those -- ROMCALL has left rom banked in over +; $d000, and between SETALTZP and CLRALTZP there +; is no zero page addressing, no stack traffic +; and no jsr. +; +; A plain 80-column card carries 1 kB rather +; than 64K, and decodes none of the address +; lines above it, so it answers at every +; auxiliary address -- the sparse mapping the +; Apple II identification routine tests for. +; Zero page $0b and $040b are the same cell on +; such a card and different cells on a real +; one, and 80STORE with PAGE2 reaches the +; second of those without disturbing the fetch +; either, so writing one and reading the other +; tells the two cards apart. +; +; ProDOS starts by reading RDRAMRD and RDALTZP, +; on the grounds that anything already banked +; in must exist. Nothing has touched either +; switch this early, so that shortcut would +; always fall through to here. + +auxscratch = f_temp ; moved by ALTZP +auxmirror = $400 + auxscratch ; moved by PAGE2 + +auxtest + .( + lda auxscratch + pha + php + sei + + ; PAGE2 only picks the bank while 80STORE + ; is on, and gives the display back when + ; it goes off again, so the screen never + ; sees any of this. + + sta SET80STORE + sta TXTPAGE2 + lda auxmirror + pha + + lda #$5a + sta auxmirror + + lda #$a5 + sta SETALTZP + sta auxscratch ; auxiliary zero page + sta CLRALTZP + eor #$ff + sta auxscratch ; main gets $5a + + ldx #0 ; assume there is none + + lda auxmirror + cmp #$5a + bne restore ; 1 kB, mirrored + + sta SETALTZP + lda auxscratch + sta CLRALTZP + cmp #$a5 + bne restore ; main answered instead + + ldx #$10 +restore + pla + sta auxmirror + sta TXTPAGE1 + sta CLR80STORE + plp + pla + sta auxscratch + txa + rts + .) +#endif + +setupvideo + .( + bit col80 + bmi firmware + + lda #COUT1 + sta CSW+1 + jmp window +firmware + ROMCALL(SETTXT) + jsr SLOT3 ; hook up the //e + ; 80-column firmware + + lda #$92 ; ctrl-R, 80 columns + jsr cout +window + lda #0 + sta WNDLFT + sta WNDTOP + lda scrw + sta WNDWDTH + lda #24 + sta WNDBTM + jsr set_normal + jmp clrwin + .) + +#if PRODOS + +initparms + .( + ldx #nparm-1 +loop + lda parmsrc,x + sta p_open,x + dex + bpl loop + rts + .) + +removeram + ; ProDOS puts its RAM disk in auxiliary + ; memory, which is where the undo ring / aux page cache + ; goes, so unhook it first, if it's there. + ; TODO restore on quit + + .( + bit auxram + bpl done + + ldx DEVCNT +scan + lda DEVLST,x + and #$f0 + cmp #$b0 ; slot 3, drive 2 + beq found + + dex + bpl scan +done + rts +found + cpx DEVCNT + bcs last +shift + lda DEVLST+1,x + sta DEVLST,x + inx + cpx DEVCNT + bcc shift +last + dec DEVCNT + + lda #nodev + sta DEVADR+$17 + rts + .) +#endif + +banner + .( + bit auxram + bmi yesauxram + lda #0 + sta txt_128k +yesauxram + ldx #txt_banner + jsr putline + jsr io_mline + jmp io_mline + .) + +#if PRODOS +openstory + .( + jsr tryopen + bcc opened +; retry with full volume name +retry + lda #pathname_full + sta p_open+2 + + jsr tryopen + bcc opened + + ldx #txt_nostory + jsr putline + jsr io_getc + jmp retry +opened + lda p_open+5 + sta p_read+1 + sta p_mark+1 + rts +tryopen + lda #$c8 ; OPEN + ldx #p_open + jmp mlicall + .) + +; Image of the MLI parameter blocks, copied +; down to $0288 at boot. + +parmsrc + ; p_open + .byt 3 + .word pathname_short + .word FILEBUF + .byt 0 + ; p_read + .byt 4 + .byt 0 ; ref num + .word 0 ; data buffer + .word $0100 ; request count + .word 0 ; bytes read + ; p_mark + .byt 2 + .byt 0 + .byt 0,0,0 + ; p_quit + .byt 4 + .byt 0 + .word 0 + .byt 0 + .word 0 +nparm = *-parmsrc +#endif + +#if PRORWTS +openstory + ; A two disk set boots from the disk with + ; the interpreter on it and the story is in + ; the other drive, so a miss on the drive + ; we booted from is not an error until the + ; other one has been tried too. Each pass + ; looks at both, and storydrive keeps the + ; answer for every later open. + + .( + lda #0 + sta cur_drive +retry + jsr storyparms + jsr rwts_openfile + beq opened + + lda storydrive + eor #1 + sta storydrive + + jsr storyparms + jsr rwts_openfile + beq opened + + ldx #txt_nostory + jsr putline + jsr io_getc + jmp retry +opened + rts + .) +#endif + +txt_banner + .asc "Aa-machine " + .asc VERSION +#if PRODOS + .asc " ProDOS" +#elseif PRORWTS + .asc " ProRWTS2" +#endif +; previous strings continue w/o null terminator +txt_128k + .asc " 128K",0 + +txt_nostory + .asc "Insert story disk and press a key.",10,0 + +#if PRODOS +RAMEND = $bb00 +#endif +#if PRORWTS +RAMEND = $c000 +#endif + +SAFEPG = (* + $ff) >> 8 ; after init routines + +; Over the init routines, but rounded up to a +; page boundary. The engine reserves the save +; window by page number -- it keeps the page +; cache above >(SAVEADDR+SAVEMAXBYTES) while an +; image is being built, and drops that same +; range of pages before a restore. Unaligned, +; the last partial page is shared between the +; savefile buffer and a live cache page in both +; places. + +SAVEADDR = ((initsegment + $ff) >> 8) << 8 + +; the ProRWTS2 library is at the end of the file +; but it does not have to be moved, it will +; relocate itself + +#if PRORWTS +prorwts2_init = * + $2000 - $800 + boothdrlen + himem_end - himem_start +#endif + +; Make sure the engine fits in the language card area +.assert himem_start == $d000, "A2_ENGINE_HIMEM: engine code must start at $d000" +.assert himem_end <= $f800, "A2_ENGINE_HIMEM: engine code exceeds LC RAM" diff --git a/src/6502/a2_prorwts2.acme b/src/6502/a2_prorwts2.acme new file mode 100644 index 0000000..ba1362c --- /dev/null +++ b/src/6502/a2_prorwts2.acme @@ -0,0 +1,3929 @@ +;license:BSD-3-Clause +;extended open/read/write binary file in ProDOS filesystem, with random access +;copyright (c) Peter Ferrie 2013-2022 +;assemble using ACME + +ver_02 = 1 + +!if ver_02 = 1 { + !cpu 6502 +} else { ;ver_02 = 0 + !cpu 65c02 +} ;ver_02 +!to "a2_prorwts2.bin",plain + +*=PRORWTS_ORG + +;place no code before init label below. + + ;user-defined options + verbose_info = 1 ;set to 1 to enable display of memory usage + enable_floppy= 1 ;set to 1 to enable floppy drive support + poll_drive = 1 ;set to 1 to check if disk is in drive, recommended if allow_multi is enabled + use_smartport= 1 ;set to 1 to enable support for more than two MicroDrive (or more than four CFFA) partitions + override_adr = 1 ;set to 1 to require an explicit load address + aligned_read = 0 ;set to 1 if all reads can be a multiple of block size (required for RWTS mode) + enable_readseq=1 ;set to 1 to enable reading multiple sequential times from the same file without seek + ;(exposes a fixed address that can be called for either floppy or hard disk support) + ;requires fast_subindex + ;can also be used for sequential writes, but size must be 512-aligned + enable_write = 1 ;set to 1 to enable write support + ;file must exist already and its size cannot be altered + ;writes occur in multiples of block size + enable_format= 0 ;used only by RWTS mode, requires enable_write and fast_subindex + enable_seek = 1 ;set to 1 to enable seek support + ;seeking with aligned_read=1 requires non-zero offset + allow_multi = 1 ;set to 1 to allow multiple floppies + allow_zerovol= 1 ;set to 1 to support volume 0 (=last used volume) + check_chksum = 0 ;set to 1 to enforce checksum verification for floppies + allow_subdir = 0 ;set to 1 to allow opening subdirectories to access files + might_exist = 1 ;set to 1 if file is not known to always exist already + ;makes use of status to indicate success or failure + many_files = 0 ;set to 1 to support more than 256 files in a directory + detect_wp = 1 ;detect write-protected floppy during writes + allow_aux = 0 ;set to 1 to allow read/write directly to/from aux memory + ;requires load_high to be set for arbitrary memory access + ;else driver must be running from same memory target + ;i.e. running from main if accessing main, running from aux if accessing aux + allow_saplings=1 ;enable support for saplings + allow_trees = 1 ;enable support for tree files, as opposed to only seedlings and saplings + ;required in RWTS mode if file > 128kb + fast_trees = 1 ;keep tree block in memory, requires an additional 512 bytes of RAM + always_trees = 0 ;set to 1 if the only file access involves tree files + ;not compatible with allow_subdir, allow_saplings + ;required in RWTS mode if allow_trees is enabled + detect_treof = 0 ;detect EOF during read of tree files + fast_subindex= 1 ;keep subindex block in memory, requires an additional 512 bytes of RAM + ;halves the disk access for double the speed (ideal for RWTS mode) + allow_sparse = 0 ;enable support for reading sparse files + write_sparse = 0 ;enable support for writing to sparse files (blocks are allocated even if empty) + ;used only by RWTS mode, writing to sparse files in non-RWTS mode will corrupt the file! + bounds_check = 0 ;set to 1 to prevent access beyond the end of the file + ;but limits file size to 64k-2 bytes. + return_size = 0 ;set to 1 to receive file size on open in read-only mode + one_shot = 0 ;set to 1 to load entire file in one pass (avoids the need to specify size) + no_interrupts= 1 ;set to 1 to disable interrupts across calls + detect_err = 1 ;set to 1 to to detect errors in no_interrupts mode + swap_zp = 0 ;set to 1 to include code to preserve zpage + ;used only by RWTS mode + swap_scrn = 0 ;set to 1 to preserve screen hole contents across SmartPort calls + ;recommended if allow_aux is used, to avoid device reset + ;requires 64 bytes to save all holes + read_scrn = 0 ;set to 1 to support reading into screen memory + ;requires swap_scrn + rwts_mode = 0 ;set to 1 to enable emulation of DOS RWTS when running from hard disk + ;uses a one-time open of a tree file, no other file access allowed + ;use unique volume numbers to distinguish between images in the same file + ;requires override_adr, allow_trees, always_trees + ;or fast_subindex if file is smaller than a tree + ;not compatible with enable_floppy, allow_subdir, might_exist, bounds_check + mem_swap = 0 ;set to 1 if zpage can be swapped between main and aux, and swap_zp is unsuitable + ;(caches index registers in code instead of zpage) + write_ffff = 0 ;set to 1 if there might be reads to $FFxx and allow_sparse=1 + load_high = 0 ;set to 1 to load to top of RAM (either main or banked, enables a himem check) + load_aux = 0 ;load to aux memory + load_banked = 1 ;set to 1 to load into banked RAM instead of main RAM (can be combined with load_aux for aux banked) + lc_bank = 2 ;load into specified bank (1 or 2) if load_banked=1 + one_page = 0 ;set to 1 if verbose mode says that you should (smaller code) + two_pages = 0 ;default size + three_pages = 1 ;set to 1 if verbose mode says that you should (code is larger than two pages) + + ;user-defined driver load address +!if load_banked = 1 { + !if load_high = 1 { + !ifdef PASS2 { + } else { ;PASS2 not defined + reloc = $ff00 ;page-aligned, as high as possible, the ideal value will be shown on mismatch + } ;PASS2 + } else { ;load_high = 0 + reloc = $d000 ;page-aligned, but otherwise wherever you want + } ;load_high = 1 +} else { ;load_banked = 0 + !if load_high = 1 { + !ifdef PASS2 { + } else { ;PASS2 not defined + reloc = $bf00 ;page-aligned, as high as possible, the ideal value will be shown on mismatch + } ;PASS2 + } else { ;load_high = 0 + reloc = $bc00 ;page-aligned, but otherwise wherever you want ($BC00 is common for rwts_mode) + } ;load_high = 1 +} ;load_banked = 1 + + ;there are also buffers that can be moved if necessary: + ;dirbuf, encbuf, treebuf (and corresponding hdd* versions that load to the same place) + ;they are independent of each other so they can be placed separately + ;see near EOF for those + ;note that hddencbuf must be even-page-aligned in RWTS-mode + + ;zpage usage, arbitrary selection except for the "ProDOS constant" ones + ;feel free to move them around + +!if (might_exist + poll_drive + detect_wp) > 0 { + status = $50 ;returns non-zero on error +} ;might_exist = 1 or poll_drive = 1 or detect_wp = 1 +!if allow_aux = 1 { + auxreq = $51 ;set to 1 to read/write aux memory, else main memory is used +} ;allow_aux = 1 + sizelo = $52 ;set if enable_write=1 and writing, or reading, or if enable_seek=1 and seeking + sizehi = $53 ;set if enable_write=1 and writing, or reading, or if enable_seek=1 and seeking +!if (enable_write + enable_seek + allow_multi + rwts_mode) > 0 { + reqcmd = $54 ;set (read/write/seek) if enable_write=1 or enable_seek=1 + ;if allow_multi=1, bit 7 selects floppy drive in current slot (clear=drive 1, set=drive 2) during open call + ;bit 7 must be clear for read/write/seek on opened file +} ;enable_write = 1 or enable_seek = 1 or allow_multi = 1 or rwts_mode = 1 + ldrlo = $55 ;set to load address if override_adr=1 + ldrhi = $56 ;set to load address if override_adr=1 + namlo = $57 ;name of file to access + namhi = $58 ;name of file to access + !set last_zp = $58 ;highest address to save if swap_zp enabled (max 127 entries later) +!if write_sparse = 1 { + sparseblk = $59 ;(internal) last-read block was sparse if zero + !set last_zp = $59 ;highest address to save if swap_zp enabled (max 127 entries later) +} ;write_sparse = 1 + +!if enable_floppy = 1 { + tmpsec = $3c ;(internal) sector number read from disk + reqsec = $3d ;(internal) requested sector number + curtrk = $40 ;(internal) track number read from disk +} ;enable_floppy = 1 + + command = $42 ;ProDOS constant + unit = $43 ;ProDOS constant + adrlo = $44 ;ProDOS constant + adrhi = $45 ;ProDOS constant + bloklo = $46 ;ProDOS constant + blokhi = $47 ;ProDOS constant + + scratchlo = $48 ;(internal) + scratchhi = $49 ;(internal) + + entries = $3f ;(internal) total number of entries in directory +!if many_files = 1 { + entrieshi = $3b ;(internal) total number of entries in directory +} ;many_files = 1 + +!if mem_swap = 0 { + !if rwts_mode = 1 { + lasttree = $5a ;(internal) last used index in tree buffer + } ;rwts_mode = 1 + !if allow_trees = 1 { + treeidx = $5b ;(internal) index into tree block + !set last_zp = $5b ;highest address to save if swap_zp enabled (max 127 entries later) + !if always_trees = 0 { + istree = $5c ;(internal) flag to indicate tree file + } ;always_trees = 0 + !if fast_trees = 0 { + treeblklo = $5d + treeblkhi = $5e + !set last_zp = $5e ;highest address to save if swap_zp enabled (max 127 entries later) + } ;fast_trees = 0 + } ;allow_trees = 1 + blkidx = $5f ;(internal) index into sapling block list + !if rwts_mode = 1 { + lastblk = $60 ;(internal) previous index into sapling block list + !set last_zp = $60 ;highest address to save if swap_zp enabled (max 127 entries later) + } ;rwts_mode = 1 + !if ((bounds_check or return_size) > 0) and ((rwts_mode or one_shot) = 0) { + bleftlo = $61 ;(internal) bytes left in file + } ;(bounds_check = 1 or return_size = 1) and (rwts_mode = 0 and one_shot = 0) + !if ((bounds_check or return_size or aligned_read) > 0) and ((rwts_mode or one_shot) = 0) { + blefthi = $62 ;(internal) bytes left in file + !set last_zp = $62 ;highest address to save if swap_zp enabled (max 127 entries later) + } ;(bounds_check = 1 or return_size = 1 or aligned_read = 1) and (rwts_mode and one_shot = 0) + !if aligned_read = 0 { + blkofflo = $63 ;(internal) offset within cache block + blkoffhi = $64 ;(internal) offset within cache block + !set last_zp = $64 ;highest address to save if swap_zp enabled (max 127 entries later) + } ;aligned_read = 0 +} ;mem_swap = 0 + +!if enable_floppy = 1 { + step = $65 ;(internal) state for stepper motor + tmptrk = $66 ;(internal) temporary copy of current track + phase = $67 ;(internal) current phase for seek + !set last_zp = $67 ;highest address to save if swap_zp enabled (max 127 entries later) +} ;enable_floppy = 1 + + ;constants + cmdseek = 0 ;requires enable_seek=1 + cmdread = 1 ;requires enable_write=1 + cmdwrite = 2 ;requires enable_write=1 + SETKBD = $fe89 + SETVID = $fe93 + DEVNUM = $bf30 + PHASEOFF = $c080 + PHASEON = $c081 + MOTOROFF = $c088 + MOTORON = $c089 + DRV0EN = $c08a + Q6L = $c08c + Q6H = $c08d + Q7L = $c08e + Q7H = $c08f + MLI = $bf00 + NAME_LENGTH = $4 ;ProDOS constant + MASK_SUBDIR = $d0 ;ProDOS constant + MASK_ALL = $f0 ;ProDOS constant + KEY_POINTER = $11 ;ProDOS constant + EOF_LO = $15 ;ProDOS constant + EOF_HI = $16 ;ProDOS constant + AUX_TYPE = $1f ;ProDOS constant + ENTRY_SIZE = $27 ;ProDOS constant + NEXT_BLOCK_LO = $2 ;ProDOS constant + NEXT_BLOCK_HI = $3 ;ProDOS constant + SAPLING = $20 ;ProDOS constant + FILE_COUNT = $25 ;ProDOS constant + DEVADR01HI = $bf11 ;ProDOS constant + ROMIN = $c081 + LCBANK2 = $c08b + CLRAUXRD = $c002 + CLRAUXWR = $c004 + SETAUXWR = $c005 + CLRAUXZP = $c008 + SETAUXZP = $c009 + + first_zp = $40 ;lowest address to save if swap_zp enabled + ;last_zp is calculated automatically + + D1S1 = 1 ;disk 1 side 1 volume ID if rwts_mode enabled + +init jsr SETKBD + jsr SETVID + lda DEVNUM + sta x80_parms + 1 + sta unrunit1 + 1 + and #$70 +!if (enable_floppy + enable_write) > 1 { + sta unrslot1 + 1 + sta unrslot2 + 1 + sta unrslot3 + 1 + sta unrslot4 + 1 +} ;enable_floppy = 1 and enable_write = 1 + pha +!if enable_floppy = 1 { + ora # 0 { + sta unrdrvoff3 + 1 + } ;might_exist = 1 or poll_drive = 1 + tax + inx ;MOTORON + stx unrdrvon1 + 1 + stx unrdrvon2 + 1 + inx ;DRV0EN + !if allow_multi = 1 { + stx unrdrvsel1 + 1 + stx unrdrvsel2 + 1 + stx unrdrvsel3 + 1 + } ;allow_multi = 1 + inx + inx ;Q6L + stx unrread1 + 1 + stx unrread2 + 1 + stx unrread3 + 1 + stx unrread4 + 1 + stx unrread5 + 1 + !if check_chksum = 1 { + stx unrread6 + 1 + } ;check_chksum = 1 +} ;enable_floppy = 1 + ldx #1 + stx namlo + inx + stx namhi + + ;fetch path, if any + + jsr MLI + !byte $c7 + !word c7_parms + ldx $200 + dex + stx sizelo + sec + bmi +++ + + ;find current directory name in directory + + php + +readblock jsr MLI + !byte $80 + !word x80_parms + + lda #<(readbuff + NAME_LENGTH) + sta scratchlo + lda #>(readbuff + NAME_LENGTH) + sta scratchhi +inextent ldy #0 + lda (scratchlo), y + pha + and #$0f + tax +-- iny + lda (scratchlo), y + cmp (namlo), y + beq ifoundname + + ;match failed, move to next directory in this block, if possible + +- pla + +skiphdr clc + lda scratchlo + adc #ENTRY_SIZE + sta scratchlo + bcc + + + ;there can be only one page crossed, so we can increment instead of adc + + inc scratchhi ++ cmp #<(readbuff + NAME_LENGTH + ($27 * $0d)) + lda scratchhi + sbc #>(readbuff + NAME_LENGTH + ($27 * $0d)) + bcc inextent + + ;read next directory block when we reach the end of this block + + lda readbuff + NEXT_BLOCK_LO + ldx readbuff + NEXT_BLOCK_HI + bcs + + +ifoundname dex + bne -- + + ;parse path until last directory is seen + + iny + lda (namlo), y + cmp #'/' + bne - + pla + and #$20 ;Volume Directory Header XOR subdirectory + beq adjpath + pla + clc + php + lsr + bcc skiphdr + inx + +adjpath tya + eor #$ff + adc sizelo + sta sizelo + clc + tya + adc namlo + sta namlo + dex + beq ++ + + ;cache block number of current directory + ;as starting position for subsequent searches + + ldy #(KEY_POINTER + 1) + lda (scratchlo), y + tax + dey + lda (scratchlo), y +!if enable_floppy = 1 { + sta unrblocklo + 1 + stx unrblockhi + 1 +} ;enable_floppy = 1 + sta unrhddblocklo + 1 + stx unrhddblockhi + 1 ++ sta x80_parms + 4 + stx x80_parms + 5 +++ lda sizelo + bne readblock + pla + + ;unit to slot for ProDOS interface + ;accept if slot code matches unit number + ++++ pla + lsr + lsr + lsr + tay + lsr + ora #$c0 + tax + cmp DEVADR01HI, y + clc + beq set_slot + +!if enable_floppy = 1 { + + ;check if current device is floppy + + stx scratchhi + ldy #0 + sty scratchlo + iny + lda (scratchlo), y + cmp #$20 + bne not_floppy + iny + iny + lda (scratchlo), y + bne not_floppy + iny + iny + lda (scratchlo), y + cmp #3 + bne not_floppy + ldy #$ff + lda (scratchlo), y + beq set_slot + +not_floppy +} ;enable_floppy = 1 + ldx #$c8 + + ;find SmartPort device for basic MicroDrive, BOOTi support + ;the BOOTi can load floppy .po images via the SmartPort interface + ;but the virtual slot behaviour differs slightly from regular ProDOS + ;so we scan for the SmartPort interface in all cases + +iterdevice +- dex + stx scratchhi + ldy #0 + sty scratchlo + iny + lda (scratchlo), y + cmp #$20 + bne - + iny + iny + lda (scratchlo), y + bne - + iny + iny + lda (scratchlo), y + cmp #3 + bne - + ldy #$ff + lda (scratchlo), y + beq - + +set_slot stx slot + 2 + stx unrentry + 2 +slot ldx $cfff + stx unrentry + 1 +!if enable_floppy = 1 { + php +} ;enable_floppy = 1 +!if use_smartport = 1 { + !if enable_floppy = 1 { + beq + + bcs ++ ++ jmp bankram +++ + } else { ;enable_floppy = 0 + !if rwts_mode = 0 { + bcc bankram + } else { ;rwts_mode = 1 + bcs + + jmp bankram ++ + } ;rwts_mode = 0 + } ;enable_floppy = 1 + + ldy #$8c ;STY + !if (rwts_mode + enable_write) > 1 { + sty unrcommand1 + } ;rwts_mode = 1 and enable_write = 1 + sty unrcommand3 + lda # 1 { + sta unrcommand1 + 1 + } ;rwts_mode = 1 and enable_write = 1 + !if (rwts_mode + aligned_read + (enable_write xor 1)) = 0 { + sta unrcommand2 + 1 + } ;rwts_mode = 0 and aligned_read = 0 and enable_write = 1 + sta unrcommand3 + 1 + lda #>pcommand + !if (rwts_mode + enable_write) > 1 { + sta unrcommand1 + 2 + } ;rwts_mode = 1 and enable_write = 1 + !if (rwts_mode + aligned_read + (enable_write xor 1)) = 0 { + sta unrcommand2 + 2 + } ;rwts_mode = 0 and aligned_read = 0 and enable_write = 1 + sta unrcommand3 + 2 + iny ;STA + sty unrblokhi1 + sty unrunit1 + 2 + iny ;STX + !if (rwts_mode + aligned_read + (enable_write xor 1)) = 0 { + sty unrcommand2 + } ;rwts_mode = 0 and aligned_read = 0 and enable_write = 1 + sty unrbloklo1 + ;;lda #>pblock + ;;pblock_enabled=1 + sta unrbloklo1 + 2 + !if (rwts_mode + write_sparse) > 1 { + sta unrbloklo2 + 2 + } ;rwts_mode = 1 and write_sparse = 1 + ;;lda #>(pblock + 1) + ;;pblock1_enabled=1 + sta unrblokhi1 + 2 + !if (rwts_mode + write_sparse) > 1 { + sta unrblokhi2 + 2 + sta unrblokhi3 + 2 + } ;rwts_mode = 1 and write_sparse = 1 + ;;lda #>paddr + sta unrunit1 + 4 + ldy # 1 { + sty unrbloklo2 + 1 + } ;rwts_mode = 1 and write_sparse = 1 + iny + sty unrblokhi1 + 1 + !if (rwts_mode + write_sparse) > 1 { + sty unrblokhi2 + 1 + sty unrblokhi3 + 1 + } ;rwts_mode = 1 and write_sparse = 1 + lda #$a5 ;LDA + sta unrunit1 + !if (rwts_mode + write_sparse) > 1 { + lda #$ee ;INC + sta unrblokhi2 + ldy #$ad ;LDA + sty unrblokhi3 + iny ;LDX + sty unrbloklo2 + } ;rwts_mode = 1 and write_sparse = 1 + lda #adrlo + sta unrunit1 + 1 + lda #packet + sta unrppacket + 1 +} ;use_smartport = 1 + +bankram +!if load_banked = 1 { + lda LCBANK2 - ((lc_bank - 1) * 8) - (rwts_mode * 2) + lda LCBANK2 - ((lc_bank - 1) * 8) - (rwts_mode * 2) +} ;load_banked = 1 +!if enable_floppy = 1 { + ldx #>unrelocdsk + ldy #unrelochdd + ldy #((codeend - rdwrpart) + $ff) + ldy #0 + !if (load_aux and (load_banked xor 1)) = 1 { + sta SETAUXWR + } ;load_aux = 1 and load_banked = 0 +- lda (scratchlo), y + !if (load_aux + load_banked) > 1 { + sta SETAUXZP + } ;load_aux = 1 and load_banked = 1 +reladr sta reloc, y + !if (load_aux + load_banked) > 1 { + sta CLRAUXZP + } ;load_aux = 1 and load_banked = 1 + iny + bne - + inc scratchhi + !if (load_aux and (load_banked xor 1)) = 1 { + sta CLRAUXWR + } ;load_aux = 1 and load_banked = 0 + inc reladr + 2 + !if (load_aux and (load_banked xor 1)) = 1 { + sta SETAUXWR + } ;load_aux = 1 and load_banked = 0 + dex + bne - + plp + !if (load_aux + load_banked) > 1 { + sta SETAUXZP + } ;load_aux = 1 and load_banked = 1 + !if swap_scrn = 1 { + beq + + jsr saveslot + lda #$91 + sta initpatch + } ;swap_scrn = 1 + bne ++ ++ + + ;build 6-and-2 denibbilisation table + + ldx #$16 +-- stx scratchlo + txa + asl + bit scratchlo + beq + + ora scratchlo + eor #$ff + and #$7e +- bcs + + lsr + bne - + tya + sta nibtbl - $16, x + !if enable_write = 1 { + ;and 6-and-2 nibbilisation table if writing + + txa + ora #$80 + sta xlattbl, y + } ;enable_write = 1 + iny ++ inx + bpl -- + +unrdrvon1 lda MOTORON + jsr readadr + lda curtrk + sta trackd1 + + !if allow_multi = 1 { + lda unrunit1 + 1 + asl + lda #0 + rol + sta driveind + 1 + pha + eor #1 + tay +unrdrvsel1 lda DRV0EN, y + jsr spinup + jsr poll + beq + + inc twodrives + 1 + lda #0 + sta phase + ldx #$22 + jsr seek ++ pla + tay +unrdrvsel2 lda DRV0EN, y + } ;allow_multi = 1 +unrdrvoff1 lda MOTOROFF +++ +} else { ;enable_floppy = 0 + !ifdef PASS2 { + !if (hddcodeend - reloc) > $100 { + !if one_page = 1 { + !error "one_page must be 0" + } ;one_page = 0 + !if (hddcodeend - reloc) > $200 { + !if three_pages = 0 { + !error "three_pages must be 1" + } ;three_pages = 0 + } ;hddcodeend + } ;hddcodeend + } ;PASS2 + !if three_pages = 1 { + ldx #>(hddcodeend + $ff - reloc) + } ;three_pages = 1 + ldy #0 + !if load_aux = 1 { + sta SETAUXWR + (load_banked * 4) ;SETAUXWR or SETAUXZP + } ;load_aux = 1 +multicopy +- lda unrelochdd, y + sta reloc, y + + !if three_pages = 0 { + !if two_pages = 1 { + lda unrelochdd + $100, y + sta reloc + $100, y + } ;two_pages = 1 + } ;three_pages = 0 + iny + bne - + !if three_pages = 1 { + !if (load_aux and (load_banked xor 1)) = 1 { + sta CLRAUXWR + } ;load_aux = 1 and load_banked = 0 + inc multicopy + 2 + inc multicopy + 5 + !if (load_aux and (load_banked xor 1)) = 1 { + sta SETAUXWR + } ;load_aux = 1 and load_banked = 0 + dex + bne multicopy + } ;three_pages = 1 + !if (fast_subindex + swap_zp) > 1 { + sty zp_array + adrlo - first_zp + } ;fast_subindex = 1 and swap_zp = 1 + + !if swap_scrn = 1 { + jsr saveslot + lda #$91 + sta initpatch + } ;swap_scrn = 1 +} ;enable_floppy = 1 + +!if rwts_mode = 1 { + ;read volume directory key block + ;self-modified by init code + +hddopendir +unrhddblocklo = * + ldx #2 +unrhddblockhi = * + lda #0 +hddreaddir1 jsr hddreaddirsel + + lda #NAME_LENGTH + ENTRY_SIZE +hddfirstent sta scratchlo + lda #>(hdddirbuf - 1) + sta scratchhi + + ;there can be only one page crossed, so we can increment here + +hddnextent1 inc scratchhi +hddnextent ldy #0 + + ;match name lengths before attempting to match names + + lda (scratchlo), y + and #$0f + tax + inx +- cmp filename, y + beq hddfoundname + + ;match failed, move to next entry in this block, if possible + ++ clc + lda scratchlo + adc #ENTRY_SIZE + sta scratchlo + bcs hddnextent1 + cmp #<(NAME_LENGTH + ($27 * $0d)) + bne hddnextent + + ;read next directory block when we reach the end of this block + + ldx hdddirbuf + NEXT_BLOCK_LO + lda hdddirbuf + NEXT_BLOCK_HI + jsr hddreaddirsec + lda #NAME_LENGTH + bne hddfirstent + +hddfoundname iny + lda (scratchlo), y + dex + bne - + + !if ((swap_zp xor 1) + mem_swap) > 0 { + !if allow_trees = 1 { + stx treeidx + sty lasttree ;guarantee no match + } ;allow_trees = 1 + stx blkidx + sty lastblk ;guarantee no match + } else { ;swap_zp = 1 and mem_swap = 0 + !if allow_trees = 1 { + stx zp_array + treeidx - first_zp + sty zp_array + lasttree - first_zp ;guarantee no match + } ;allow_trees = 0 + stx zp_array + blkidx - first_zp + sty zp_array + lastblk - first_zp ;guarantee no match + } ;swap_zp = 0 or mem_swap = 1 + + !if (allow_trees + fast_subindex) > 0 { + ;fetch KEY_POINTER + + ldy #KEY_POINTER + lda (scratchlo), y + !if (fast_trees + fast_subindex) = 0 { + !if ((swap_zp xor 1) + mem_swap) > 0 { + sta treeblklo + } else { ;swap_zp = 1 and mem_swap = 0 + sta zp_array + treeblklo - first_zp + } ;swap_zp = 0 or mem_swap = 1 + } else { ;fast_trees = 1 or fast_subindex = 1 + tax + } ;fast_trees = 0 and fast_subindex = 0 + iny + lda (scratchlo), y + !if (fast_trees + fast_subindex) = 0 { + !if ((swap_zp xor 1) + mem_swap) > 0 { + sta treeblkhi + } else { ;swap_zp = 1 and mem_swap = 0 + sta zp_array + treeblkhi - first_zp + } ;swap_zp = 0 or mem_swap = 1 + } else { ;fast_trees = 1 or fast_subindex = 1 + !if fast_trees = 1 { + ldy #>hddtreebuf + } ;fast_trees = 1; + jsr hddreaddirsect + } ;fast_trees = 0 and fast_subindex = 0 + } ;allow_trees = 1 or fast_subindex = 1 + + lda #>iob + ldy #iob + ldy #iob + ldy # 0 { + jmp rdwrfile + } ;enable_readseq = 1 or allow_subdir = 1 +opendir + !if no_interrupts = 1 { + !if detect_err = 1 { + clc + } ;detect_err = 1 + php + sei + jsr + + !if detect_err = 1 { + pla + adc #0 + pha + } ;detect_err = 1 + plp +unrdrvoff2 = unrelocdsk + (* - reloc) + lda MOTOROFF + rts ++ + } ;no_interrupts = 1 + + jsr prepdrive + + ;read volume directory key block + ;self-modified by init code + +unrblocklo = unrelocdsk + (* - reloc) + ldx #2 +unrblockhi = unrelocdsk + (* - reloc) + lda #0 + jsr readdirsel + +readdir + !if allow_subdir = 1 { + jsr prepdrive + } ;allow_subdir = 1 + !if might_exist = 1 { + lda dirbuf + FILE_COUNT ;assuming only 256 files per subdirectory + sta entries + !if many_files = 1 { + lda dirbuf + FILE_COUNT + 1 + sta entrieshi + } ;many_files = 1 + } ;might_exist = 1 + + lda #NAME_LENGTH + ENTRY_SIZE +firstent sta scratchlo + lda #>(dirbuf - 1) + sta scratchhi + + ;there can be only one page crossed, so we can increment here + +nextent1 inc scratchhi +nextent ldy #0 + !if (might_exist + allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + lda (scratchlo), y + !if might_exist = 1 { + + ;skip deleted entries without counting + + and #MASK_ALL + beq + + } ;might_exist = 1 + + !if (allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + ;remember type + ;now bits 5-4 are represented by carry (subdirectory), sign (sapling) + + asl + asl + + !if allow_trees = 1 { + ;now bits 5-3 are represented by carry (subdirectory), sign (sapling), + ;overflow (seedling), and sign+overflow (tree) + + sta treeidx + bit treeidx + } ;allow_trees = 1 + php + } ;allow_subdir = 1 or allow_saplings = 1 or (allow_trees = 1 and always_trees = 0) + } ;might_exist = 1 or allow_subdir = 1 or allow_saplings = 1 or (allows_trees = 1 and always_trees = 0) + + ;match name lengths before attempting to match names + + lda (scratchlo), y + and #$0f + tax + inx +- cmp (namlo), y + beq foundname + + ;match failed, check if any directory entries remain + + !if (allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + plp + } ;allow_subdir = 1 or allow_saplings = 1 or (allow_trees = 1 and always_trees = 0) + !if might_exist = 1 { + dec entries + bne + + !if many_files = 1 { + lda entrieshi + bne ++ + } ;many_files = 1 + } ;might_exist = 1 + !if (might_exist + poll_drive) > 0 { +nodisk inc status + !if detect_err = 1 { + sec + } ;detect_err = 1 + !if no_interrupts = 0 { +unrdrvoff3 = unrelocdsk + (* - reloc) + lda MOTOROFF + } ;no_interrupts = 0 + rts + } ;might_exist = 1 or poll_drive = 1 + + !if (might_exist + many_files) > 1 { +++ dec entrieshi + } ;might_exist = 1 and many_files = 1 + + ;move to next entry in this block, if possible + ++ clc + lda scratchlo + adc #ENTRY_SIZE + sta scratchlo + bcs nextent1 + cmp #<(NAME_LENGTH + ($27 * $0d)) + bne nextent + + ;read next directory block when we reach the end of this block + + ldx dirbuf + NEXT_BLOCK_LO + lda dirbuf + NEXT_BLOCK_HI + jsr readdirsec + lda #NAME_LENGTH + bne firstent + +foundname iny + lda (scratchlo), y + dex + bne - + + ;initialise essential variables + + !if allow_trees = 1 { + stx treeidx + !if always_trees = 0 { + stx istree + } ;always_trees = 0 + } ;allow_trees = 1 + stx blkidx + !if (aligned_read + one_shot) = 0 { + stx blkofflo + stx blkoffhi + } ;aligned_read = 0 and one_shot = 0 + !if enable_write = 1 { + !if aligned_read = 0 { + ldy reqcmd + cpy #cmdwrite ;control carry instead of zero + !if one_shot = 0 { + bne + + } ;one_shot = 0 + } ;aligned_read = 0 + !if one_shot = 0 { + + ;round requested size up to nearest block if writing + + lda sizelo + adc #$fe + lda sizehi + adc #1 + and #$fe + sta sizehi + !if aligned_read = 0 { + stx sizelo + !if bounds_check = 1 { + sec + } ;bounds_check = 1 + } ;aligned_read = 0 + } ;one_shot = 0 ++ + } ;enable_write = 1 + + !if (bounds_check + return_size + one_shot) > 0 { + ;cache EOF (file size, loaded backwards) + + ldy #EOF_HI + lda (scratchlo), y + !if (enable_write + aligned_read) > 0 { + tax + dey ;EOF_LO + lda (scratchlo), y + + ;round file size up to nearest block if writing without aligned reads + ;or always if using aligned reads + + !if aligned_read = 0 { + bcc + + } else { ;aligned_read = 1 + !if enable_write = 1 { + sec + } ;enable_write = 1 + } ;aligned_read = 0 + adc #$fe + txa + adc #1 + and #$fe + !if aligned_read = 0 { + tax + lda #0 + !if one_shot = 0 { ++ stx blefthi + sta bleftlo + } else { ;one_shot = 1 ++ stx sizehi + sta sizelo + } ;one_shot = 0 + } else { ;aligned_read = 1 + !if one_shot = 0 { + sta blefthi + } else { ;one_shot = 1 + sta sizehi + } ;one_shot = 0 + } ;aligned_read = 0 + } else { ;enable_write = 0 and aligned_read = 0 + !if one_shot = 0 { + sta blefthi + } else { ;one_shot = 1 + sta sizehi + } ;one_shot = 0 + dey ;EOF_LO + lda (scratchlo), y + !if one_shot = 0 { + sta bleftlo + } else { ;one_shot = 1 + sta sizelo + } ;one_shot = 0 + } ;enable_write = 1 or aligned_read = 1 + } ;bounds_check = 1 or return_size = 1 or one_shot = 1 + + ;cache AUX_TYPE (load offset for binary files) + + !if override_adr = 0 { + ldy #AUX_TYPE + lda (scratchlo), y + !if (allow_subdir + allow_saplings + allow_trees + (aligned_read xor 1)) > 0 { + sta ldrlo + iny + lda (scratchlo), y + sta ldrhi + } else { ;allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 and aligned_read = 1 + pha + iny + lda (scratchlo), y + pha + } ;allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 or aligned_read = 0 + } ;override_adr = 0 + + ;cache KEY_POINTER + + ldy #KEY_POINTER + lda (scratchlo), y + tax + !if (allow_subdir + allow_saplings + allow_trees) > 0 { + sta dirbuf + !if (allow_trees + (fast_trees xor 1)) > 1 { + sta treeblklo + } ;allow_trees = 1 and fast_trees = 0 + iny + lda (scratchlo), y + sta dirbuf + 256 + !if (allow_trees + (fast_trees xor 1)) > 1 { + sta treeblkhi + } ;allow_trees = 1 and fast_trees = 0 + + !if (allow_saplings + allow_sparse) > 1 { + ;clear dirbuf in case sparse seedling is sapling-sized + + pha + ldy #1 + lda #0 +- sta dirbuf, y + sta dirbuf + 256, y + iny + bne - + pla + } ;allow_saplings = 1 and allow_sparse = 1 + + !if always_trees = 0 { + plp + bpl ++ + !if allow_subdir = 1 { + php + } ;allow_subdir = 1 + !if allow_trees = 1 { + ldy #>dirbuf + bvc + + !if fast_trees = 1 { + ldy #>treebuf + } ;fast_trees = 1 + sty istree ++ + } ;allow_trees = 1 + } else { ;always_trees = 1 + ldy #>treebuf + } ;always_trees = 0 + } else { ;allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 + iny + lda (scratchlo), y + } ;allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 + + ;read index block in case of sapling or tree + + jsr readdirsect + + !if allow_subdir = 1 { + plp + } ;allow_subdir = 1 +++ + ;skip some stuff + ;drive is on already + ;and interrupt control is in place + + jmp rdwrfilei + +rdwrfile + !if allow_subdir = 1 { + clc + } ;allow_subdir = 1 + !if no_interrupts = 1 { + !if detect_err = 1 { + !if allow_subdir = 0 { + clc + } ;allow_subdir = 0 + } ;detect_err = 1 + php + sei + jsr + + !if detect_err = 1 { + pla + adc #0 + pha + } ;detect_err = 1 + plp +unrdrvoff3 = unrelocdsk + (* - reloc) + lda MOTOROFF + rts ++ + } ;no_interrupts = 1 + + jsr prepdrive + +rdwrfilei + !if (override_adr + allow_subdir + allow_saplings + allow_trees + (aligned_read xor 1)) > 0 { + ;restore load offset + + ldx ldrhi + lda ldrlo + !if allow_subdir = 1 { + ;check file type and fake size and load address for subdirectories + + bcc + + ldy #2 + sty sizehi + ldx #>dirbuf + lda #0 + !if aligned_read = 0 { + sta sizelo + } ;aligned_read = 0 ++ + } ;allow_subdir = 1 + sta adrlo + stx adrhi + } else { ;override_adr = 0 and allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 and aligned_read = 1 + pla + sta adrhi + pla + sta adrlo + } ;override_adr = 1 or allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 or aligned_read = 0 + + ;set requested size to min(length, requested size) + + !if aligned_read = 0 { + !if bounds_check = 1 { + ldy bleftlo + cpy sizelo + lda blefthi + tax + sbc sizehi + bcs copyblock + sty sizelo + stx sizehi + } ;bounds_check = 1 + +copyblock + !if allow_aux = 1 { + ldx auxreq + jsr setaux + } ;allow_aux = 1 + !if one_shot = 0 { + !if enable_write = 1 { + lda reqcmd + lsr + bne rdwrloop + } ;enable_write = 1 + + ;if offset is non-zero then we return from cache + + lda blkofflo + tax + ora blkoffhi + beq rdwrloop + lda sizehi + pha + lda sizelo + pha + lda adrhi + sta scratchhi + lda adrlo + sta scratchlo + stx adrlo + lda #>encbuf + clc + adc blkoffhi + sta adrhi + + ;determine bytes left in block + + lda #1 + sbc blkofflo + tay + lda #2 + sbc blkoffhi + tax + + ;set requested size to min(bytes left, requested size) + + cpy sizelo + sbc sizehi + bcs + + sty sizelo + stx sizehi ++ + !if enable_seek = 1 { + lda sizehi + } else { ;enable_seek = 0 + ldy sizehi + } ;enable_seek = 1 + jsr copycache + + ;align to next block and resume read + + lda ldrlo + adc sizelo + sta ldrlo + lda ldrhi + adc sizehi + sta ldrhi + sec + pla + sbc sizelo + sta sizelo + pla + sbc sizehi + sta sizehi + ora sizelo + !if allow_subdir = 1 { + !if no_interrupts = 1 { + clc + bne rdwrfilei + } else { ;no_interrupts = 0 + bne rdwrfile + } ;no_interrupts = 1 + } else { ;allow_subdir = 0 + bne rdwrfilei + } ;allow_subdir = 1 + !if allow_aux = 0 { + rts + } else { ;allow_aux = 1 + beq rdwrdone + } ;allow_aux = 0 + } ;one_shot = 0 + } else { ;aligned_read = 1 + !if bounds_check = 1 { + lda blefthi + cmp sizehi + bcs + + sta sizehi ++ + } ;bounds_check = 1 + !if allow_aux = 1 { + ldx auxreq + jsr setaux + } ;allow_aux = 1 + } ;aligned_read = 0 + +rdwrloop + !if aligned_read = 0 { + !if (enable_write + enable_seek) > 0 { + ldx reqcmd + } ;enable_write = 1 or enable_seek = 1 + + ;set read/write size to min(length, $200) + + lda sizehi + cmp #2 + bcs + + pha + + ;redirect read to private buffer for partial copy + + lda adrhi + pha + lda adrlo + pha + lda #>encbuf + sta adrhi + !if ver_02 = 1 { + ldx #0 + stx adrlo + !if (enable_write + enable_seek) > 0 { + inx ;ldx #cmdread + } ;enable_write = 1 or enable_seek = 1 + } else { ;ver_02 = 0 + stz adrlo + !if (enable_write + enable_seek) > 0 { + ldx #cmdread + } ;enable_write = 1 or enable_seek = 1 + } ;ver_02 = 1 ++ + } ;aligned_read = 0 + + !if allow_trees = 1 { + ;read tree data block only if tree and not read already + ;the indication of having read already is that at least one sapling/seed block entry has been read, too + + ldy blkidx + bne + + !if always_trees = 0 { + lda istree + beq + + } ;always_trees = 0 + lda adrhi + pha + lda adrlo + pha + !if ((aligned_read xor 1) + (enable_write or enable_seek)) > 1 { + !if ver_02 = 1 { + txa + pha + } else { ;ver_02 = 0 + phx + } ;ver_02 = 1 + } ;aligned_read = 0 and (enable_write = 1 or enable_seek = 1) + !if aligned_read = 0 { + php + } ;aligned_read = 0 + + ;fetch tree data block and read it + + !if fast_trees = 0 { + ldx treeblklo + lda treeblkhi + jsr readdirsel + } ;fast_trees = 0 + ldy treeidx + inc treeidx + ldx treebuf, y + lda treebuf + 256, y + !if detect_treof = 1 { + bne noteof1 + tay + txa + bne fixy1 + !if aligned_read = 0 { + plp + bcs fewpop + pla + pla + pla +fewpop + } ;aligned_read = 0 + pla + pla + sec + rts +fixy1 tya +noteof1 + } ;detect_treof = 1 + + !if fast_trees = 0 { + lda #>dirbuf + sta adrhi + jsr seekrd + } else { ;fast_trees = 1 + jsr readdirsel + } ;fast_trees = 0 + + !if aligned_read = 0 { + plp + } ;aligned_read = 0 + !if ((aligned_read xor 1) + (enable_write or enable_seek)) > 1 { + !if ver_02 = 1 { + pla + tax + } else { ;ver_02 = 0 + plx + } ;ver_02 = 1 + } ;aligned_read = 0 and (enable_write = 1 or enable_seek = 1) + pla + sta adrlo + pla + sta adrhi + } ;allow_trees = 1 + + ;fetch data block and read/write it + +skiptree ldy blkidx ++ inc blkidx + !if aligned_read = 0 { + !if enable_seek = 1 { + txa ;cpx #cmdseek, but that would require php at top + beq + + } ;enable_seek = 1 + !if enable_write = 1 { + stx command + } ;enable_write = 1 + } ;aligned_read = 0 + + ldx dirbuf, y + lda dirbuf + 256, y + !if detect_treof = 1 { + bne noteof2 + tay + txa + bne fixy2 + sec + rts +fixy2 tya +noteof2 + } ;detect_treof = 1 + !if allow_sparse = 1 { + pha + ora dirbuf, y + beq issparse + pla + } ;allow_sparse = 1 + !if (aligned_read and (enable_write or enable_seek)) = 1 { + ldy reqcmd + !if enable_seek = 1 { + beq + + } ;enable_seek = 1 + } ;aligned_read = 1 and (enable_write = 1 or enable_seek = 1) + !if aligned_read = 0 { + php + } ;aligned_read = 0 + !if enable_write = 1 { + jsr seekrdwr + } else { ;enable_write = 0 + jsr seekrd + } ;enable_write = 1 + !if aligned_read = 0 { + plp + } ;aligned_read = 0 + +resparse + !if aligned_read = 0 { ++ bcc + + !if bounds_check = 1 { + dec blefthi + dec blefthi + } ;bounds_check = 1 + } ;aligned_read = 0 + dec sizehi + dec sizehi + bne rdwrloop + + !if aligned_read = 0 { + lda sizelo + bne rdwrloop + } ;aligned_read = 0 +rdwrdone + !if no_interrupts = 0 { +unrdrvoff4 = unrelocdsk + (* - reloc) + lda MOTOROFF + } ;no_interrupts = 0 + !if allow_aux = 1 { + ldx #0 +setaux sta CLRAUXRD, x + sta CLRAUXWR, x + } ;allow_aux = 1 + rts + + !if allow_sparse = 1 { +issparse pla +- sta (adrlo), y + iny + bne - + inc adrhi +- sta (adrlo), y + iny + bne - + inc adrhi + !if write_ffff = 0 { + bne resparse + } else { + jmp resparse + } ;write_ffff + } ;allow_sparse = 1 + + !if aligned_read = 0 { + ;cache partial block offset + ++ pla + sta scratchlo + pla + sta scratchhi + pla + !if one_shot = 0 { + sta sizehi + } ;one_shot = 0 + dec adrhi + dec adrhi + + !if enable_seek = 1 { +copycache + ldy reqcmd + ;cpy #cmdseek + beq ++ + tay + } else { ;enable_seek = 0 + tay +copycache + } ;enable_seek = 1 + beq + + dey +- lda (adrlo), y + sta (scratchlo), y + iny + bne - + inc scratchhi + inc adrhi + bne + +- lda (adrlo), y + sta (scratchlo), y + iny ++ cpy sizelo + bne - +++ + !if one_shot = 0 { + !if bounds_check = 1 { + lda bleftlo + sec + sbc sizelo + sta bleftlo + lda blefthi + sbc sizehi + sta blefthi + } ;bounds_check = 1 + clc + !if enable_seek = 1 { + lda sizelo + } else { ;enable_seek = 0 + tya + } ;enable_seek = 1 + adc blkofflo + sta blkofflo + lda sizehi + adc blkoffhi + and #$fd + sta blkoffhi + bcc rdwrdone ;always + } else { ;one_shot = 1 + !if no_interrupts = 0 { +unrdrvoff5 = unrelocdsk + (* - reloc) + lda MOTOROFF + } ;no_interrupts = 0 + rts + } ;one_shot = 0 + } ;aligned_read = 0 + + ;no tricks here, just the regular stuff + +seek ldy #0 + sty step + asl phase + txa + asl + sta tmptrk + +copy_cur lda tmptrk + sta tmpsec + sec + sbc phase + beq +++ + bcs + + eor #$ff + inc tmptrk + bcc ++ ++ sbc #1 + dec tmptrk +++ cmp step + bcc + + lda step ++ cmp #8 + bcs + + tay + sec ++ jsr ++++ + lda step1, y + jsr delay + lda tmpsec + clc + jsr +++++ + lda step2, y + jsr delay + inc step + bne copy_cur ++++ jsr delay + clc +++++ lda tmptrk ++++++ and #3 + rol + tax + +unrseek = unrelocdsk + (* - reloc) + lda PHASEOFF, x + rts + +prepdrive + !if poll_drive = 1 { + tsx + stx callstack + 1 + } ;poll_drive = 1 + !if allow_multi = 1 { + ldy driveind + 1 + } ;allow_multi = 1 + !if (might_exist + poll_drive + detect_wp) > 0 { + !if ver_02 = 1 { + lda #0 + sta status + } else { ;ver_02 = 0 + stz status + } ;ver_02 = 1 + } ;might_exist = 1 or poll_drive = 1 or detect_wp = 1 + !if allow_multi = 1 { + asl reqcmd + bcc seldrive + tya +twodrives eor #0 ;replaced with 1 if drive exists + tay +seldrive lsr reqcmd +driveind cpy #0 + sty driveind + 1 + clc + bne newdrive + } ;allow_multi = 1 + jsr poll + !if allow_multi = 1 { + cpx #1 + } ;allow_multi = 1 + +newdrive +unrdrvon2 = unrelocdsk + (* - reloc) + sta MOTORON + + !if allow_multi = 1 { +unrdrvsel3 = unrelocdsk + (* - reloc) + sta DRV0EN, y + bcs seekret + } else { ;allow_multi = 0 + bne seekret + } ;allow_multi = 1 + +spinup ldy #6 +- jsr delay + dey + bpl - + +delay +-- ldx #$11 +- dex + bne - + inc scratchlo + bne + + inc scratchhi ++ sec + sbc #1 + bne -- + rts + +step1 !byte 1, $30, $28, $24, $20, $1e, $1d, $1c +step2 !byte $70, $2c, $26, $22, $1f, $1e, $1d, $1c + +readadr +- jsr readd5aa + cmp #$96 + bne - + ldy #3 +- sta curtrk + jsr readnib + rol + sta tmpsec + jsr readnib + and tmpsec + dey + bne - +seekret rts + +readd5aa + !if poll_drive = 1 { + ldx #0 + ldy #0 + } ;poll_drive = 1 +-- + !if poll_drive = 1 { + inx + beq + + } ;poll_drive = 1 +--- jsr readnib +- cmp #$d5 + bne -- + jsr readnib + cmp #$aa + bne - + +readnib +unrread1 = unrelocdsk + (* - reloc) +- lda Q6L + bpl - + rts + + !if poll_drive = 1 { ++ iny + bne --- + +callstack ldx #0 + inx + inx + txs + jmp nodisk + } ;poll_drive = 1 + +poll ldx #$ff +unrread2 = unrelocdsk + (* - reloc) +- lda Q6L + jsr seekret + pha + pla +unrread3 = unrelocdsk + (* - reloc) + eor Q6L + bne + + dex + bne - ++ rts + +readdirsel + !if (ver_02 + allow_multi) > 0 { + ldy #0 + sty adrlo + } else { ;ver_02 = 0 and allow_multi = 0 + stz adrlo + } ;ver_02 = 1 or allow_multi = 1 + +readdirsec + !if allow_trees = 0 { +readdirsect ldy #>dirbuf + } else { ;allow_trees = 1 + ldy #>dirbuf +readdirsect + } ;allow_trees = 1 + sty adrhi +seekrd ldy #cmdread + !if (aligned_read + enable_write) > 1 { +seekrdwr sty command + } else { ;aligned_read = 0 or enable_write = 0 + sty command +seekrdwr + } ;aligned_read = 1 and enable_write = 1 + + ;convert block number to track/sector + + lsr + txa + ror + lsr + lsr + sta phase + txa + and #3 + php + asl + plp + rol + sta reqsec + + !if allow_multi = 1 { + ldy driveind + 1 + ldx trackd1, y + } else { ;allow_multi = 0 +trackd1 = * + 1 + ldx #$d1 + } ;allow_multi = 1 + + ;if track does not match, then seek + + cpx phase + beq checksec + lda phase + !if allow_multi = 1 { + sta trackd1, y + } else { ;allow_multi = 0 + sta trackd1 + } ;allow_multi = 1 + jsr seek + + ;match or read/write sector + +checksec jsr cmpsecrd + inc reqsec + inc reqsec + +cmpsecrd jsr readadr + + !if enable_write = 1 { + ldy command + cpy #cmdwrite ;we need Y=2 below + beq encsec + } ;enable_write = 1 + cmp reqsec + bne cmpsecrd + + ;read sector data + + jsr readd5aa + eor #$ad ;zero A if match + bne cmpsecrd + ldy #$aa +unrread4 = unrelocdsk + (* - reloc) +- ldx Q6L + bpl - + eor nibtbl - $96, x + sta bit2tbl - $aa, y + iny + bne - +unrread5 = unrelocdsk + (* - reloc) +- ldx Q6L + bpl - + eor nibtbl - $96, x + sta (adrlo), y ;the real address + iny + bne - + !if check_chksum = 1 { +unrread6 = unrelocdsk + (* - reloc) +- ldx Q6L + bpl - + eor nibtbl - $96, x + bne cmpsecrd + } ;check_chksum = 1 +-- ldx #$a9 +- inx + beq -- + lda (adrlo), y + lsr bit2tbl - $aa, x + rol + lsr bit2tbl - $aa, x + rol + sta (adrlo), y + iny + bne - + inc adrhi + rts + + !if enable_write = 1 { +encsec +-- ldx #$aa +- dey + lda (adrlo), y + lsr + rol bit2tbl - $aa, x + lsr + rol bit2tbl - $aa, x + sta encbuf, y + lda bit2tbl - $aa, x + and #$3f + sta bit2tbl - $aa, x + inx + bne - + tya + bne -- + +cmpsecwr jsr readadr + cmp reqsec + bne cmpsecwr + + ;skip tail #$DE #$AA #$EB some #$FFs ... + + ldy #$24 +- dey + bpl - + + ;write sector data + +unrslot1 = unrelocdsk + (* - reloc) + ldx #$d1 + lda Q6H, x ;prime drive + lda Q7L, x ;required by Unidisk + !if detect_wp = 1 { + asl + ror status + } ;detect_wp = 1 + tya + sta Q7H, x + ora Q6L, x + + ;40 cycles + + ldy #4 ;2 cycles + pha ;3 cycles + pla ;4 cycles + nop ;2 cycles +loopchk1 +- jsr writenib1 ;(29 cycles) + + ;+6 cycles + dey ;2 cycles + bne - ;3 cycles if taken, 2 if not + + ;36 cycles + ;+10 cycles + ldy #(prolog_e - prolog) + ;2 cycles + !if >loopchk1 != >* { + !serious "loop1 crosses a page" + } + cmp $ea ;3 cycles +loopchk2 +- lda prolog - 1, y ;4 cycles + jsr writenib2 ;(17 cycles) + + ;32 cycles if branch taken + ;+6 cycles + dey ;2 cycles + bne - ;3 cycles if taken, 2 if not + + ;36 cycles on first pass + ;+10 cycles + tya ;2 cycles + !if >loopchk2 != >* { + !serious "loop2 crosses a page" + } + ldy #$56 ;2 cycles +loopchk3 +- eor bit2tbl - 1, y ;5 cycles + tax ;2 cycles + lda xlattbl, x ;4 cycles +unrslot2 = unrelocdsk + (* - reloc) + ldx #$d1 ;2 cycles + sta Q6H, x ;5 cycles + lda Q6L, x ;4 cycles + + ;32 cycles if branch taken + + lda bit2tbl - 1, y ;5 cycles + dey ;2 cycles + bne - ;3 cycles if taken, 2 if not + + ;32 cycles + ;+9 cycles + clc ;2 cycles + !if >loopchk3 != >* { + !serious "loop3 crosses a page" + } +loopchk4 +-- eor encbuf, y ;4 cycles +loopchk5 +- tax ;2 cycles + lda xlattbl, x ;4 cycles +unrslot3 = unrelocdsk + (* - reloc) + ldx #$d1 ;2 cycles + sta Q6H, x ;5 cycles + lda Q6L, x ;4 cycles + bcs + ;3 cycles if taken, 2 if not + + ;32 cycles if branch taken + + lda encbuf, y ;4 cycles +loopchk6 ;belongs to the "bcs +" above + iny ;2 cycles + bne -- ;3 cycles if taken, 2 if not + + ;32 cycles + ;+10 cycles + sec ;2 cycles + !if >loopchk4 != >* { + !serious "loop4 crosses a page" + } + bcs - ;3 cycles + + ;32 cycles + ;+3 cycles + !if >loopchk6 != >* { + !serious "loop6 crosses a page" + } ++ ldy #(epilog_e - epilog) + ;2 cycles + !if >loopchk5 != >* { + !serious "loop5 crosses a page" + } + nop ;2 cycles + nop ;2 cycles + nop ;2 cycles +loopchk7 +- lda epilog - 1, y ;4 cycles + jsr writenib2 ;(17 cycles) + + ;32 cycles if branch taken + ;+6 cycles + dey ;2 cycles + bne - ;3 cycles if branch taken, 2 if not + + lda Q7L, x + !if >loopchk7 != >* { + !serious "loop7 crosses a page" + } + lda Q6L, x ;flush final value + inc adrhi + rts + +writenib1 jsr writeret ;6 cycles +writenib2 +unrslot4 = unrelocdsk + (* - reloc) + ldx #$d1 ;2 cycles + sta Q6H, x ;5 cycles + ora Q6L, x ;4 cycles +writeret rts ;6 cycles + +prolog !byte $ad, $aa, $d5 +prolog_e + !if >(prolog - 1) != >(prolog_e - 1) { + !serious "prologue crosses a page" + } +epilog !byte $ff, $eb, $aa, $de +epilog_e + !if >(epilog - 1) != >(epilog_e - 1) { + !serious "epilogue crosses a page" + } + } ;enable_write = 1 +codeend + !if allow_multi = 1 { +trackd1 !byte 0 +trackd2 !byte 0 + } ;allow_multi = 1 +bit2tbl = (* + 255) & -256 +nibtbl = bit2tbl + 86 + !if enable_write = 1 { +xlattbl = nibtbl + 106 +dataend = xlattbl + 64 + } else { ;enable_write = 0 +dataend = nibtbl + 106 + } ;enable_write = 1 +} ;enable_floppy = 1 +} ;reloc + +unrelochdd +!pseudopc reloc { +!if rwts_mode = 1 { + !if swap_zp = 1 { + sta zp_array + namhi - first_zp + sty zp_array + namlo - first_zp + jsr swap_zpg + } else { ;swap_zp = 0 + sta namhi + sty namlo + } ;swap_zp = 1 + +loopsect + !if ver_02 = 1 { + lda #0 + sta sizehi + } else { ;ver_02 + stz sizehi + } ;ver_02 = 1 + !if enable_format = 1 { + ldy #$0c ;command + lda (namlo),y + cmp #2 ;write (or format if greater) + php + bcc skipinit ;read + beq skipinit ;write + ldy #5 ;sector + !if ver_02 = 1 { + txa + } else { ;ver_02 + lda #0 + } ;ver_02 = 1 + sta (namlo),y + dey ;track + sta (namlo),y +skipinit + } ;enable_format = 1 + !if allow_multi = 1 { + ldy #3 ;volume + lda (namlo),y + !if allow_zerovol = 1 { + bne + +lastvol = * + 1 + lda #D1S1 ++ sta lastvol + } ;allow_zerovol = 1 + ldy #$0e ;returned volume + sta (namlo),y + ldx #vollist_e-vollist_b +- dex + cmp vollist_b,x + bne - + } ;allow_multi = 1 + ldy #4 ;track + lda (namlo),y + asl + asl + asl + rol sizehi + asl + rol sizehi + iny ;sector + ora (namlo),y + !if allow_multi = 1 { + ldy sizehi +- dex + bmi ++ + clc + adc #$30 + bcc + + iny ++ iny + iny + bne - +++ + } ;allow_multi = 1 + !if allow_trees = 1 { + tax + !if allow_multi = 1 { + tya + } else { ;allow_multi = 0 + lda sizehi + } ;allow_multi = 1 + lsr + sta treeidx + txa + } else { ;allow_trees = 0 + lsr sizehi + } ;allow_trees = 1 + ror + php + jsr seek1 + plp + !if fast_subindex = 0 { + lda #>hddencbuf + adc #0 + sta adrhi + } else { ;fast_subindex = 1 + bcc + + inc adrhi ++ + } ;fast_subindex = 0 + ldy #9 ;adrhi + lda (namlo),y + sta scratchhi + dey ;adrlo + lda (namlo),y + sta scratchlo + !if enable_format = 1 { + ldy #0 + ldx #0 + plp + bcs runinit + } else { ;enable_format = 0 + !if enable_write = 1 { + ldy #$0c ;command + lda (namlo),y + !if enable_seek = 1 { + !if swap_zp = 0 { + beq + + } else { ;swap_zp = 1 + beq swap_zpg + } ;swap_zp = 0 + } ;enable_seek + ldy #0 + lsr + bne runinit + } else { ;enable_write = 0 + ldy #0 + } ;enable_write = 1 + } ;enable_format = 1 + +- lda (adrlo),y + sta (scratchlo),y + iny + bne - + !if swap_zp = 0 { ++ clc + rts + } else { ;swap_zp = 1 + !if enable_write = 1 { + beq swap_zpg + } ;enable_write = 1 + } ;swap_zp = 0 + + !if enable_write = 1 { +runinit + !if enable_format = 1 { + bne format + } ;enable_format = 1 + !if write_sparse = 1 { + lda sparseblk + beq writesparse + } ;write_sparse = 1 +- lda (scratchlo),y + sta (adrlo),y + iny + bne - + !if write_sparse = 0 { + lda #>hddencbuf + sta adrhi + ldy #cmdwrite +unrcommand1 = unrelochdd + (* - reloc) + sty command + !if use_smartport = 1 { + nop ;allow replacing "sty command" with "sty pcommand" in extended SmartPort mode + } ;use_smartport = 1 + !if swap_zp = 1 { + jsr hddwriteimm + !if enable_format = 1 { + bcc swap_zpg ;always + } ;enable_format = 1 + } else { ;swap_zp = 0 + jmp hddwriteimm + } ;swap_zp = 1 + !if enable_format = 1 { +clrcarry clc + inc adrhi +format lda blanksec,x + sta (adrlo),y + inx + txa + and #7 + tax + iny + bne format + bcs clrcarry + dex + stx lasttree + iny + lda #$18 ;blocks + sta namlo + sty namhi + lda #>hddencbuf + sta adrhi + lda #cmdwrite + sta reqcmd + inc lastblk ;force mismatch +- jsr hddrdwrloop + inc blkidx + bne + + inc treeidx ++ dec namlo + bne - + dec namhi + bpl - + } ;enable_format = 1 + } else { ;write_sparse = 1 + !if swap_zp = 1 { + jsr hddwriteenc + } else { ;swap_zp = 0 + jmp hddwriteenc + } ;swap_zp = 1 + } ;write_sparse = 0 + } ;enable_write = 1 + + !if swap_zp = 1 { +swap_zpg ldx #(last_zp - first_zp) +- lda first_zp,x + ldy zp_array,x + sta zp_array,x + sty first_zp,x + dex + bpl - + } ;swap_zp = 1 + + !if (enable_write + swap_zp) > 0 { + clc + rts + } ;enable_write = 1 or swap_zp = 1 + + !if enable_format = 1 { +blanksec !text "SAN INC." + } ;enable_format = 1 + + !if write_sparse = 1 { +writesparse ldx #2 + tya + !if fast_subindex = 0 { + jsr hddreaddirsec + } else { ;fast_subindex = 1 + ldy #>hddencbuf + jsr hddreaddirsect + } ;fast_subindex = 0 + !if ver_02 = 1 { + lda #0 + sta sizelo + sta sizehi + } else { ;ver_02 = 0 + stz sizelo + stz sizehi + } ;ver_02 = 1 + + ;round up to block count + + lda hddencbuf + $29 + adc #$ff + lda hddencbuf + $2A + adc #1 + lsr + sta ldrhi + ldx hddencbuf + $27 + lda hddencbuf + $28 +--- ldy #>hddencbuf + sty adrhi + jsr hddseekrd + ldy #0 + + ;scan for a free block + +-- lda #$80 + sta ldrlo +- lda (adrlo), y + and ldrlo + bne foundbit + inc sizelo + lsr ldrlo + bcc - + lda sizelo + bne + + inc sizehi ++ iny + bne -- + inc adrhi + lda adrhi + cmp #(>hddencbuf) + 2 + bne -- +unrbloklo2 = unrelochdd + (* - reloc) + ldx bloklo + !if use_smartport = 1 { + nop ;allow replacing "ldx bloklo" with "ldx pblock" in extended SmartPort mode + } ;use_smartport = 1 + + inx + bne + +unrblokhi2 = unrelochdd + (* - reloc) + inc blokhi + !if use_smartport = 1 { + nop ;allow replacing "inc blokhi" with "inc pblock + 1" in extended SmartPort mode + } ;use_smartport = 1 ++ +unrblokhi3 = unrelochdd + (* - reloc) + lda blokhi + !if use_smartport = 1 { + nop ;allow replacing "lda blokhi" with "lda pblock + 1" in extended SmartPort mode + } ;use_smartport = 1 + dec ldrhi + bne --- + + ;disk full + + !if swap_zp = 0 { + clc + rts + } else { ;swap_zp = 1 + beq swap_zpg + } ;swap_zp = 0 + + ;allocate block and update bitmap + +foundbit lda (adrlo), y + eor ldrlo + sta (adrlo), y + jsr hddwriteenc + inc lasttree + lda #$60 ;RTS + sta hddskiptree + 2 + jsr hddrdfile + lda #$be ;LDX ,Y + sta hddskiptree + 2 + lda sizelo + sta hdddirbuf, y + lda sizehi + sta hdddirbuf + 256, y + jsr hddwritedir + lda #0 + jsr savebyte + ldx sizelo + lda sizehi + ldy #cmdwrite + jsr hddseekrdwr + jmp loopsect + +hddwriteenc lda #>hddencbuf + sta adrhi +hddwritedir ldy #cmdwrite + +unrcommand1 = unrelochdd + (* - reloc) + sty command + !if use_smartport = 1 { + nop ;allow replacing "sty command" with "sty pcommand" in extended SmartPort mode + } ;use_smartport = 1 + bne hddwriteimm + } ;write_sparse = 1 + +seek1 sta blkidx + !if enable_write = 1 { + ldy #cmdread + sty reqcmd + } ;enable_write = 1 +} else { ;rwts_mode = 0 + !if (enable_readseq + allow_subdir) > 0 { +hddrdwrpart jmp hddrdwrfile + } ;enable_readseq = 1 or allow_subdir = 1 + !if enable_floppy = 1 { + !if (* - reloc) < (unrblocklo - unrelocdsk) { + ;essential padding to match offset with floppy version + !fill (unrblocklo - unrelocdsk) - (* - reloc), $ea + } + } ;enable_floppy = 1 + + ;read volume directory key block + ;self-modified by init code + +hddopendir +unrhddblocklo = unrelochdd + (* - reloc) + ldx #2 +unrhddblockhi = unrelochdd + (* - reloc) + lda #0 + jsr hddreaddirsel + +hddreaddir + !if might_exist = 1 { + lda hdddirbuf + FILE_COUNT ;assuming only 256 files per subdirectory + sta entries + !if many_files = 1 { + lda hdddirbuf + FILE_COUNT + 1 + sta entrieshi + } ;many_files = 1 + } ;might_exist = 1 + + lda #NAME_LENGTH + ENTRY_SIZE +hddfirstent sta scratchlo + lda #>(hdddirbuf - 1) + sta scratchhi + + ;there can be only one page crossed, so we can increment here + +hddnextent1 inc scratchhi +hddnextent ldy #0 + !if (might_exist + allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + lda (scratchlo), y + !if might_exist = 1 { + sty status + + ;skip deleted entries without counting + + and #MASK_ALL + beq + + } ;might_exist = 1 + + !if (allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + ;remember type + ;now bits 5-4 are represented by carry (subdirectory), sign (sapling) + + asl + asl + + !if allow_trees = 1 { + ;now bits 5-3 are represented by carry (subdirectory), sign (sapling), + ;overflow (seedling), and sign+overflow (tree) + + sta treeidx + bit treeidx + } ;allow_trees = 1 + php + } ;allow_subdir = 1 or allow_saplings = 1 or (allow_trees = 1 and always_trees = 0) + } ;might_exist = 1 or allow_subdir = 1 or allow_saplings = 1 or (allow_trees = 1 and always_trees = 0) + + ;match name lengths before attempting to match names + + lda (scratchlo), y + and #$0f + tax + inx +- cmp (namlo), y + beq hddfoundname + + ;match failed, check if any directory entries remain + + !if (allow_subdir + allow_saplings + (allow_trees xor always_trees)) > 0 { + plp + } ;allow_subdir = 1 or allow_saplings = 1 or (allow_trees = 1 and always_trees = 0) + !if might_exist = 1 { + dec entries + bne + + !if many_files = 1 { + lda entrieshi + bne ++ + } ;many_files = 1 + inc status + rts + + !if many_files = 1 { +++ dec entrieshi + } ;many_files = 1 + } ;might_exist = 1 + + ;move to next entry in this block, if possible + ++ clc + lda scratchlo + adc #ENTRY_SIZE + sta scratchlo + bcs hddnextent1 + cmp #<(NAME_LENGTH + ($27 * $0d)) + bne hddnextent + + ;read next directory block when we reach the end of this block + + ldx hdddirbuf + NEXT_BLOCK_LO + lda hdddirbuf + NEXT_BLOCK_HI + jsr hddreaddirsec + lda #NAME_LENGTH + bne hddfirstent + +hddfoundname iny + lda (scratchlo), y + dex + bne - + + ;initialise essential variables + + !if allow_trees = 1 { + stx treeidx + !if always_trees = 0 { + stx istree + } ;always_trees = 0 + } ;allow_trees = 1 + stx blkidx + !if (aligned_read + one_shot) = 0 { + stx blkofflo + stx blkoffhi + } ;aligned_read = 0 and one_shot = 0 + !if enable_write = 1 { + !if aligned_read = 0 { + ldy reqcmd + cpy #cmdwrite ;control carry instead of zero + !if one_shot = 0 { + bne + + } ;one_shot = 0 + } ;aligned_read = 0 + !if one_shot = 0 { + + ;round requested size up to nearest block if writing + + lda sizelo + adc #$fe + lda sizehi + adc #1 + and #$fe + sta sizehi + !if aligned_read = 0 { + stx sizelo + !if bounds_check = 1 { + sec + } ;bounds_check = 1 + } ;aligned_read = 0 + } ;one_shot = 0 ++ + } ;enable_write = 1 + + !if (bounds_check + return_size + one_shot) > 0 { + ;cache EOF (file size, loaded backwards) + + ldy #EOF_HI + lda (scratchlo), y + !if (enable_write + aligned_read) > 0 { + tax + dey ;EOF_LO + lda (scratchlo), y + + ;round file size up to nearest block if writing without aligned reads + ;or always if using aligned reads + + !if aligned_read = 0 { + bcc + + } else { ;aligned_read = 1 + !if (enable_write + (one_shot xor 1)) > 1 { + sec + } ;enable_write = 1 and one_shot = 0 + } ;aligned_read = 0 + adc #$fe + txa + adc #1 + and #$fe + !if aligned_read = 0 { + tax + lda #0 + !if one_shot = 0 { ++ stx blefthi + sta bleftlo + } else { ;one_shot = 1 ++ stx sizehi + sta sizelo + } ;one_shot = 0 + } else { ;aligned_read = 1 + !if one_shot = 0 { + sta blefthi + } else { ;one_shot = 1 + sta sizehi + } ;one_shot = 0 + } ;aligned_read = 0 + } else { ;enable_write = 0 and aligned_read = 0 + !if one_shot = 0 { + sta blefthi + } else { ;one_shot = 1 + sta sizehi + } ;one_shot = 0 + dey ;EOF_LO + lda (scratchlo), y + !if one_shot = 0 { + sta bleftlo + } else { ;one_shot = 1 + sta sizelo + } ;one_shot = 0 + } ;enable_write = 1 or aligned_read = 1 + } ;bounds_check = 1 or return_size = 1 or one_shot = 1 + + ;cache AUX_TYPE (load offset for binary files) + + !if override_adr = 0 { + ldy #AUX_TYPE + lda (scratchlo), y + !if (allow_subdir + allow_saplings + allow_trees + (aligned_read xor 1)) > 0 { + sta ldrlo + iny + lda (scratchlo), y + sta ldrhi + } else { ;allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 and aligned_read = 1 + pha + iny + lda (scratchlo), y + pha + } ;allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 or aligned_read = 0 + } ;override_adr = 0 + + ;cache KEY_POINTER + + ldy #KEY_POINTER + lda (scratchlo), y + tax + !if (allow_subdir + allow_saplings + allow_trees) > 0 { + sta hdddirbuf + !if (allow_trees + (fast_trees xor 1)) > 1 { + sta treeblklo + } ;allow_trees = 1 and fast_trees = 0 + iny + lda (scratchlo), y + sta hdddirbuf + 256 + !if (allow_trees + (fast_trees xor 1)) > 1 { + sta treeblkhi + } ;allow_trees = 1 and fast_trees = 0 + + !if (allow_saplings + allow_sparse) > 1 { + ;clear dirbuf in case sparse seedling is sapling-sized + + pha + ldy #1 + lda #0 +- sta hdddirbuf, y + sta hdddirbuf + 256, y + iny + bne - + pla + } ;allow_saplings = 1 and allow_sparse = 1 + + !if always_trees = 0 { + plp + bpl ++ + !if allow_subdir = 1 { + php + } ;allow_subdir = 1 + !if allow_trees = 1 { + ldy #>hdddirbuf + bvc + + !if fast_trees = 1 { + ldy #>hddtreebuf + } ;fast_trees = 1 + sty istree ++ + } ;allow_trees = 1 + } else { ;always_trees = 1 + ldy #>hddtreebuf + } ;always_trees = 0 + } else { ;allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 + iny + lda (scratchlo), y + } ;allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 + + ;read index block in case of sapling or tree + + jsr hddreaddirsect + + !if allow_subdir = 1 { + plp + !byte $24 ;mask the clc that follows + } else { ;allow_subdir = 0 +++ + } ;allow_subdir = 1 +} ;rwts_mode = 1 + +hddrdfile +hddrdwrfile +!if allow_subdir = 1 { + clc +++ +} ;allow_subdir = 1 + +hddrdwrfilei +!if rwts_mode = 0 { + !if (override_adr + allow_subdir + allow_saplings + allow_trees + (aligned_read xor 1)) > 0 { + ;restore load offset + + ldx ldrhi + lda ldrlo + !if allow_subdir = 1 { + ;check file type and fake size and load address for subdirectories + + bcc + + ldy #2 + sty sizehi + ldx #>hdddirbuf + lda #0 + !if aligned_read = 0 { + sta sizelo + } ;aligned_read = 0 ++ + } ;allow_subdir = 1 + sta adrlo + stx adrhi + } else { ;override_adr = 0 and allow_subdir = 0 and allow_saplings = 0 and allow_trees = 0 and aligned_read = 1 + pla + sta adrhi + pla + sta adrlo + } ;override_adr = 1 or allow_subdir = 1 or allow_saplings = 1 or allow_trees = 1 or aligned_read = 0 + + ;set requested size to min(length, requested size) + + !if aligned_read = 0 { + !if bounds_check = 1 { + ldy bleftlo + cpy sizelo + lda blefthi + tax + sbc sizehi + bcs hddcopyblock + sty sizelo + stx sizehi + } ;bounds_check = 1 + +hddcopyblock + !if allow_aux = 1 { + ldx auxreq + jsr hddsetaux + } ;allow_aux = 1 + !if one_shot = 0 { + !if enable_write = 1 { + lda reqcmd + lsr + bne hddrdwrloop + } ;enable_write = 1 + + ;if offset is non-zero then we return from cache + + lda blkofflo + tax + ora blkoffhi + beq hddrdwrloop + lda sizehi + pha + lda sizelo + pha + lda adrhi + sta scratchhi + lda adrlo + sta scratchlo + stx adrlo + lda #>hddencbuf + clc + adc blkoffhi + sta adrhi + + ;determine bytes left in block + + lda #1 + sbc blkofflo + tay + lda #2 + sbc blkoffhi + tax + + ;set requested size to min(bytes left, requested size) + + cpy sizelo + sbc sizehi + bcs + + sty sizelo + stx sizehi ++ + !if enable_seek = 1 { + lda sizehi + !if read_scrn = 1 { + clv + } ;read_scrn = 1 + } else { ;enable_seek = 0 + ldy sizehi + } ;enable_seek = 1 + jsr hddcopycache + + ;align to next block and resume read + + lda ldrlo + adc sizelo + sta ldrlo + lda ldrhi + adc sizehi + sta ldrhi + sec + pla + sbc sizelo + sta sizelo + pla + sbc sizehi + sta sizehi + ora sizelo + !if allow_subdir = 1 { + bne hddrdwrfile + } else { ;allow_subdir = 0 + bne hddrdwrfilei + } ;allow_subdir = 1 + !if no_interrupts = 1 { + clc + } ;no_interrupts = 1 + !if allow_aux = 0 { + rts + } else { ;allow_aux = 1 + beq hddrdwrdone + } ;allow_aux = 0 + } ;one_shot = 0 + } else { ;aligned_read = 1 + !if bounds_check = 1 { + lda blefthi + cmp sizehi + bcs + + sta sizehi ++ + } ;bounds_check = 1 + !if allow_aux = 1 { + ldx auxreq + jsr hddsetaux + } ;allow_aux = 1 + } ;aligned_read = 0 +} ;rwts_mode = 0 + +hddrdwrloop +!if (aligned_read + rwts_mode) = 0 { + !if (enable_write + enable_seek) > 0 { + ldx reqcmd + } ;enable_write = 1 or enable_seek = 1 + + ;set read/write size to min(length, $200) + + lda sizehi + cmp #2 + !if read_scrn = 1 { + clv + bcc redirect + !if (enable_write + enable_seek) > 0 { + txa + beq + + } ;enable_write = 1 or enable_seek = 1 + ldy ldrhi + cpy #8 + bcs + + bit knownrts ;set O flag + lda sizehi + pha + lda sizelo + pha + lda #0 + sta sizelo + lda #1 + +redirect + } else { ;read_scrn = 0 + bcs + + } ;read_scrn = 1 + pha + + ;redirect read to private buffer for partial copy + + lda adrhi + pha + lda adrlo + pha + lda #>hddencbuf + sta adrhi + !if ver_02 = 1 { + ldx #0 + stx adrlo + !if (enable_write + enable_seek) > 0 { + inx ;ldx #cmdread + } ;enable_write = 1 or enable_seek = 1 + } else { ;ver_02 = 0 + stz adrlo + !if (enable_write + enable_seek) > 0 { + ldx #cmdread + } ;enable_write = 1 or enable_seek = 1 + } ;ver_02 = 1 ++ +} ;aligned_read = 0 and rwts_mode = 0 + +!if allow_trees = 1 { + ;read tree data block only if tree and not read already + ;the indication of having read already is that at least one sapling/seed block entry has been read, too + + !if rwts_mode = 0 { + ldy blkidx + bne + + !if always_trees = 0 { + lda istree + beq + + } ;always_trees = 0 + lda adrhi + pha + lda adrlo + pha + !if ((aligned_read xor 1) + (enable_write or enable_seek)) > 1 { + !if ver_02 = 1 { + txa + pha + } else { ;ver_02 = 0 + phx + } ;ver_02 = 1 + } ;aligned_read = 0 and (enable_write = 1 or enable_seek = 1) + !if aligned_read = 0 { + php + } ;aligned_read = 0 + } else { ;rwts_mode = 1 + !if fast_subindex = 0 { + ;read whenever block index changes + + !if mem_swap = 0 { + cmp lastblk + sta lastblk + } else { ;mem_swap = 1 +blkidx = * + 1 + ldy #$d1 +lastblk = * + 1 + cpy #$d1 + sty lastblk + } ;mem_swap = 0 + php + pla + !if mem_swap = 0 { + ;read whenever tree index changes + + ldy treeidx + cpy lasttree + } else { ;mem_swap = 1 +treeidx = * + 1 + ldy #$d1 +lasttree = * + 1 + cpy #$d1 + } ;mem_swap = 0 + + sty lasttree + bne readtree + pha + plp + !if enable_write = 1 { + bne readtree + lda reqcmd + lsr + } ;enable_write = 1 + beq skipblk + +readtree + } else { ;fast_subindex = 1 + ;read whenever tree index changes + + !if mem_swap = 0 { + ldy treeidx + cpy lasttree + beq hddskiptree + sty lasttree + ldx blkidx + } else { ;mem_swap = 1 +treeidx = * + 1 + ldy #$d1 +lasttree = * + 1 + cpy #$d1 + beq hddskiptree + sty lasttree +blkidx = * + 1 + ldx #$d1 + } ;mem_swap = 0 + inx + stx lastblk + } ;fast_subindex = 0 + } ;rwts_mode = 0 + + ;fetch tree data block and read it + + !if fast_trees = 0 { + ldx treeblklo + lda treeblkhi + jsr hddreaddirsel + ldy treeidx + !if rwts_mode = 0 { + inc treeidx + } ;rwts_mode = 0 + ldx hdddirbuf, y + lda hdddirbuf + 256, y + } else { ;fast_trees = 1 + ldy treeidx + !if rwts_mode = 0 { + inc treeidx + } ;rwts_mode = 0 + ldx hddtreebuf, y + lda hddtreebuf + 256, y + } ;fast_trees = 0 + !if detect_treof = 1 { + bne hddnoteof1 + tay + txa + bne hddfixy1 + !if aligned_read = 0 { + plp + bcs hddfewpop + pla + pla + pla +hddfewpop + } ;aligned_read = 0 + pla + pla + sec + rts +hddfixy1 tya +hddnoteof1 + } ;detect_treof = 1 + + !if fast_trees = 0 { + jsr hddseekrd + } else { ;fast_trees = 1 + jsr hddreaddirsel + } ;fast_trees = 0 + + !if rwts_mode = 0 { + !if aligned_read = 0 { + plp + } ;aligned_read = 0 + !if ((aligned_read xor 1) + (enable_write or enable_seek)) > 1 { + !if ver_02 = 1 { + pla + tax + } else { ;ver_02 = 0 + plx + } ;ver_02 = 1 + } ;aligned_read = 0 and (enable_write = 1 or enable_seek = 1) + pla + sta adrlo + pla + sta adrhi + } ;rwts_mode = 0 +} ;allow_trees = 1 + + ;fetch data block and read/write it + +hddskiptree ldy blkidx +!if rwts_mode = 0 { ++ inc blkidx + !if aligned_read = 0 { + !if enable_seek = 1 { + txa ;cpx #cmdseek, but that would require php at top + beq + + } ;enable_seek = 1 + !if enable_write = 1 { +unrcommand2 = unrelochdd + (* - reloc) + stx command + !if use_smartport = 1 { + nop ;allow replacing "stx command" with "stx pcommand" in extended SmartPort mode + } ;use_smartport = 1 + } ;enable_write = 1 + } ;aligned_read = 0 +} else { ;rwts_mode = 1 + !if fast_subindex = 1 { + lda #>hddencbuf + sta adrhi + + ;read whenever block index changes + + !if mem_swap = 0 { + cpy lastblk + } else { ;mem_swap = 1 +lastblk = * + 1 + cpy #$d1 + } ;mem_swap = 0 + !if enable_write = 0 { + beq skipblk + } else { ;enable_write = 1 + bne + + lda reqcmd + lsr + beq skipblk ++ + } ;enable_write = 0 + sty lastblk + } ;fast_subindex = 1 +} ;rwts_mode = 0 + ldx hdddirbuf, y + lda hdddirbuf + 256, y +!if detect_treof = 1 { + bne hddnoteof2 + tay + txa + bne hddfixy2 + sec + rts +hddfixy2 tya +hddnoteof2 +} ;detect_treof = 1 +!if allow_sparse = 0 { + !if rwts_mode = 1 { + !if enable_write = 0 { + jmp hddseekrd + } else { ;enable_write = 1 + ldy reqcmd + !if enable_seek = 1 { + jmp hddseekrdwr + } else { ;enable_seek = 0 + bne hddseekrdwr + } ;enable_seek = 1 + } ;enable_write = 0 + } ;rwts_mode = 1 +} else { ;allow_sparse = 1 + pha + ora hdddirbuf, y + !if write_sparse = 1 { + sta sparseblk + } ;write_sparse = 1 + !if rwts_mode = 1 { + !if enable_write = 1 { + cmp #1 + pla + ldy reqcmd + bcs hddseekrdwr +savebyte + tay + } else { ;enable_write = 0 + tay + pla + dey + iny ;don't affect carry + bne hddseekrd + } ;enable_write = 1 + } ;rwts_mode = 1 +} ;allow_sparse = 0 +!if rwts_mode = 0 { + !if allow_sparse = 1 { + beq hddissparse + pla + } ;allow_sparse = 1 + !if (aligned_read and (enable_write or enable_seek)) = 1 { + ldy reqcmd + !if enable_seek = 1 { + beq + + } ;enable_seek = 1 + } ;aligned_read = 1 and (enable_write = 1 or enable_seek = 1) + !if aligned_read = 0 { + php + } ;aligned_read = 0 + !if enable_write = 1 { + jsr hddseekrdwr + } else { ;enable_write = 0 + jsr hddseekrd + } ;enable_write = 1 + !if aligned_read = 0 { + plp + } ;aligned_read = 0 + +hddresparse + !if aligned_read = 0 { ++ bcc + + } ;aligned_read = 0 + inc adrhi + inc adrhi + !if aligned_read = 0 { +resumescrn + !if bounds_check = 1 { + dec blefthi + dec blefthi + } ;bounds_check = 1 + } ;aligned_read = 0 + dec sizehi + dec sizehi + bne hddrdwrloop + !if aligned_read = 0 { + lda sizelo + bne hddrdwrloop + } ;aligned_read = 0 +hddrdwrdone + !if allow_aux = 1 { + ldx #0 +hddsetaux sta CLRAUXRD, x + sta CLRAUXWR, x + } ;allow_aux = 1 + rts +} ;rwts_mode = 0 + +!if allow_sparse = 1 { +hddissparse + !if rwts_mode = 0 { + pla + } ;rwts_mode = 0 +- sta (adrlo), y + inc adrhi + sta (adrlo), y + dec adrhi + iny + bne - + !if rwts_mode = 0 { + beq hddresparse + } else { ;rwts_mode = 1 +skipblk rts + } ;rwts_mode = 0 +} ;allow_sparse = 1 +!if rwts_mode = 0 { + !if aligned_read = 0 { + ;cache partial block offset + ++ pla + sta scratchlo + pla + sta scratchhi + pla + !if one_shot = 0 { + sta sizehi + } ;one_shot = 0 + + !if enable_seek = 1 { +hddcopycache + ldy reqcmd + ;cpy #cmdseek + beq ++ + tay + } else { ;enable_seek = 0 + tay +hddcopycache + } ;enable_seek = 1 + beq + + dey +- lda (adrlo), y + sta (scratchlo), y + iny + bne - + inc scratchhi + inc adrhi + !if read_scrn = 1 { + bvs copyhalf + } ;read_scrn = 1 + bne + +copyhalf +- lda (adrlo), y + sta (scratchlo), y + iny ++ cpy sizelo + bne - + !if read_scrn = 1 { + bvc ++ + pla + sta sizelo + pla + sta sizehi + ldx scratchhi + inx + stx adrhi + lda scratchlo + sta adrlo + bvs resumescrn + } ;read_scrn = 1 +++ + !if one_shot = 0 { + !if bounds_check = 1 { + lda bleftlo + sec + sbc sizelo + sta bleftlo + lda blefthi + sbc sizehi + sta blefthi + } ;bounds_check = 1 + clc + !if enable_seek = 1 { + lda sizelo + } else { ;enable_seek = 0 + tya + } ;enable_seek = 1 + adc blkofflo + sta blkofflo + lda sizehi + adc blkoffhi + and #$fd + sta blkoffhi + bcc hddrdwrdone ;always + } else { ;one_shot = 1 + !if allow_aux = 1 { + beq hddrdwrdone + } else { ;allow_aux = 0 + rts + } ;allow_aux = 1 + } ;one_shot = 0 + } ;aligned_read = 0 +} ;rwts_mode = 0 + +hddreaddirsel +!if ver_02 = 1 { + ldy #0 + sty adrlo +} else { ;ver_02 = 0 + stz adrlo +} ;ver_02 = 1 + +!if (enable_floppy + allow_multi) > 1 { + asl reqcmd + lsr reqcmd +} ;enable_floppy = 1 and allow_multi = 1 + +hddreaddirsec +!if allow_trees = 0 { +hddreaddirsect ldy #>hdddirbuf +} else { ;allow_trees = 1 + ldy #>hdddirbuf +hddreaddirsect +} ;allow_trees = 0 + sty adrhi +hddseekrd ldy #cmdread +!if ((rwts_mode or aligned_read) + enable_write) > 1 { +hddseekrdwr +} ;(rwts_mode = 1 or aligned_read = 1) and enable_write = 1 +unrcommand3 = unrelochdd + (* - reloc) + sty command +!if use_smartport = 1 { + nop ;allow replacing "sty command" with "sty pcommand" in extended SmartPort mode +} ;use_smartport = 1 +!if (aligned_read and enable_write) = 0 { +hddseekrdwr +} ;aligned_read = 0 or enable_write = 0 + +unrbloklo1 = unrelochdd + (* - reloc) + stx bloklo +!if use_smartport = 1 { + nop ;allow replacing "stx bloklo" with "stx pblock" in extended SmartPort mode +} ;use_smartport = 1 +unrblokhi1 = unrelochdd + (* - reloc) + sta blokhi +!if use_smartport = 1 { + nop ;allow replacing "sta blokhi" with "sta pblock + 1" in extended SmartPort mode +} ;use_smartport = 1 +unrunit1 = unrelochdd + (* - reloc) + lda #$d1 + sta unit +!if use_smartport = 1 { + nop ;allow replacing "lda #$d1/sta unit" with "lda adrlo/sta paddr" in extended SmartPort mode +} ;use_smartport = 1 +hddwriteimm lda adrhi ;for Trackstar support + pha +!if use_smartport = 1 { + sta paddr + 1 +} ;use_smartport = 1 +!if swap_scrn = 1 { + jsr saveslot +} ;swap_scrn = 1 + +unrentrysei = unrelochdd + (* - reloc) +!if no_interrupts = 1 { + php + sei +} ;no_interrupts = 1 +unrentry = unrelochdd + (* - reloc) + jsr $d1d1 +!if use_smartport = 1 { +unrpcommand = unrelochdd + (* - reloc) +pcommand !byte $2c ;hide packet in non-SmartPort mode +unrppacket = unrelochdd + (* - reloc) + !word unrelochdd + (packet - reloc) +} ;use_smartport = 1 +!if no_interrupts = 1 { + plp +} ;no_interrupts = 1 +hackstar = unrelochdd + (* - reloc) + pla + sta adrhi ;Trackstar does not preserve adrhi + +!if swap_scrn = 1 { +saveslot + lda #4 + sta $49 + ldx #0 + stx $48 + sta $4a +-- ldy #$78 +- lda ($48), y + pha + lda scrn_array, x +initpatch lda ($48), y + pla + sta scrn_array, x + inx + tya + eor #$80 + tay + bmi - + iny + bpl - + inc $49 + dec $4a + bne -- +} ;swap_scrn = 1 +!if (rwts_mode + (allow_sparse xor 1)) > 1 { +skipblk +} ;rwts_mode = 1 and allow_sparse = 0 + rts + +!if use_smartport = 1 { +unrpacket = unrelochdd + (* - reloc) +packet !byte 3 +unrunit2 = unrelochdd + (* - reloc) + !byte 0 +paddr !word readbuff + $200 +pblock !byte 2, 0, 0 + !if >pcommand != >(pblock + 1) { + !if >pcommand != >pblock { + !ifdef pblock_enabled { + } else { + !ifdef PASS2 { + !warn "uncomment ';;lda #>pblock'" + !warn "uncomment ';;pblock_enabled=1'" + !warn "uncomment ';;lda #>paddr'" + } + } + } else { + !ifdef pblock1_enabled { + } else { + !ifdef PASS2 { + !warn "uncomment ';;lda #>(pblock + 1)'" + !warn "uncomment ';;pblock1_enabled=1'" + !warn "uncomment ';;lda #>paddr'" + } + } + } + } +} ;use_smartport = 1 + +!if (rwts_mode + allow_multi) > 1 { +vollist_b +!byte D1S1 +vollist_e +} ;rwts_mode = 1 and allow_multi = 1 +hddcodeend +!if swap_scrn = 1 { +scrn_array + !if swap_zp = 1 { +zp_array = scrn_array + 64 +hdddataend = zp_array + 1 + last_zp - first_zp + } else { ;swap_zp = 0 +hdddataend = scrn_array + 64 + } ;swap_zp = 1 +} else { ;swap_scrn = 0 + !if swap_zp = 1 { +zp_array +hdddataend = zp_array + 1 + last_zp - first_zp + } else { ;swap_zp = 0 +hdddataend + } ;swap_zp = 1 +} ;swap_scrn = 1 +} ;reloc + +;[music] you can't touch this [music] +;math magic to determine ideal loading address, and information dump +!ifdef PASS2 { +} else { ;PASS2 not defined + !set PASS2=1 + !if enable_floppy = 1 { + !if reloc < $c000 { + !if ((dataend + $ff) & -256) > $c000 { + !serious "initial reloc too high, adjust to ", $c000 - (((dataend + $ff) & -256) - reloc) + } ;dataend + !if load_high = 1 { + !if ((dataend + $ff) & -256) != $c000 { + !warn "initial reloc too low, adjust to ", $c000 - (((dataend + $ff) & -256) - reloc) + } ;dataend + dirbuf = reloc - $200 + !if ((aligned_read xor 1) + enable_write) > 0 { + encbuf = dirbuf - $200 + } ;aligned_read = 0 or enable_write = 1 + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + treebuf = encbuf - $200 + } else { ;aligned_read = 1 and enable_write = 0 + treebuf = dirbuf - $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + treebuf = dirbuf + } ;fast_trees + } ;allow_trees + } else { ;load_high = 0 + !pseudopc ((dataend + $ff) & -256) { + dirbuf = * + !if (dirbuf + $200) > $c000 { + !if dirbuf < $d000 { + !set dirbuf = reloc - $200 + } ;dirbuf + } ;dirbuf + } + !if ((aligned_read xor 1) + enable_write) > 0 { + !if fast_subindex = 0 { + encbuf = dirbuf ;writes come from cache + } else { ;fast_subindex = 1 + !if dirbuf < reloc { + encbuf = dirbuf - $200 + } else { ;dirbuf + encbuf = dirbuf + $200 + !if (encbuf + $200) > $c000 { + !if encbuf < $d000 { + !set encbuf = reloc - $200 + } ;encbuf + } ;encbuf + } ;dirbuf + } ;fast_subindex + } ;aligned_read = 0 or enable_write = 1 + !if allow_trees = 1 { + !if fast_trees = 1 { + !if enable_write = 0 { + encbuf = dirbuf ;there is no encbuf + } ;enable_write = 0 + !if ((aligned_read xor 1) + rwts_mode) > 0 { + !if encbuf < reloc { + treebuf = encbuf - $200 + } else { ;encbuf + treebuf = encbuf + $200 + !if (treebuf + $200) > $c000 { + !if treebuf < $d000 { + !set treebuf = reloc - $200 + } ;treebuf + } ;treebuf + } ;encbuf + } else { ;aligned_read = 1 and rwts_mode = 0 + !if dirbuf < reloc { + treebuf = dirbuf - $200 + } else { ;dirbuf + treebuf = dirbuf + $200 + !if (treebuf + $200) > $c000 { + !if treebuf < $d000 { + !set treebuf = reloc - $200 + } ;treebuf + } ;treebuf + } ;dirbuf + } ;aligned_read = 0 or rwts_mode = 1 + } else { ;fast_trees = 0 + treebuf = dirbuf + } ;fast_trees + } ;allow_trees + } ;load_high + } else { ;reloc > $c000 + !if ((dataend + $ff) & -256) != 0 { + !if ((dataend + $ff) & -256) < reloc { + !serious "initial reloc too high, adjust to ", (0 - (((dataend + $ff) & -256) - reloc)) & $ffff + } ;dataend + } ;dataend + !if load_high = 1 { + !if (((dataend + $ff) & -256) & $ffff) != 0 { + !warn "initial reloc too low, adjust to ", (0 - (((dataend + $ff) & -256) - reloc)) & $ffff + } ;dataend + dirbuf = reloc - $200 + !if aligned_read = 0 { + encbuf = dirbuf - $200 + } ;aligned_read + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + treebuf = encbuf - $200 + } else { ;aligned_read = 1 and enable_write = 0 + treebuf = dirbuf - $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + treebuf = dirbuf + } ;fast_trees + } ;allow_trees + } else { ;load_high = 0 + !pseudopc ((dataend + $ff) & -256) { + dirbuf = * + } + !if ((aligned_read xor 1) + enable_write) > 0 { + encbuf = dirbuf + $200 + } ;aligned_read = 0 or enable_write = 1 + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + treebuf = encbuf + $200 + } else { ;aligned_read = 1 and enable_write = 0 + treebuf = dirbuf + $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + treebuf = dirbuf + } ;fast_trees + } ;allow_trees + } ;load_high + } ;reloc + !if verbose_info = 1 { + !warn "floppy code: ", reloc, "-", codeend - 1 + !warn "floppy data: ", bit2tbl, "-", dataend - 1 + !warn "floppy dirbuf: ", dirbuf, "-", dirbuf + $1ff + !if aligned_read = 0 { + !warn "floppy encbuf: ", encbuf, "-", encbuf + $1ff + } ;aligned_read + !if allow_trees = 1 { + !warn "floppy treebuf: ", treebuf, "-", treebuf + $1ff + } ;allow_trees + !warn "floppy driver start: ", unrelocdsk - init + } ;verbose_info + } ;enable_floppy + !if reloc < $c000 { + !if ((hdddataend + $ff) & -256) > $c000 { + !serious "initial reloc too high, adjust to ", $c000 - (((hdddataend + $ff) & -256) - reloc) + } ;hdddataend + !if load_high = 1 { + !if ((hdddataend + $ff) & -256) != $c000 { + !warn "initial reloc too low, adjust to ", $c000 - (((hdddataend + $ff) & -256) - reloc) + } ;hdddataend + hdddirbuf = reloc - $200 + !if aligned_read = 0 { + hddencbuf = hdddirbuf - $200 + } ;aligned_read + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + hddtreebuf = hddencbuf - $200 + } else { ;aligned_read = 1 and enable_write = 0 + hddtreebuf = hdddirbuf - $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + hddtreebuf = hdddirbuf + } ;fast_trees + } ;allow_trees + } else { ;load_high = 0 + !pseudopc ((hdddataend + $ff) & -256) { + hdddirbuf = * + !if (hdddirbuf + $200) > $c000 { + !if hdddirbuf < $d000 { + !set hdddirbuf = reloc - $200 + } ;hdddirbuf + } ;hdddirbuf + } + !if ((aligned_read xor 1) + rwts_mode) > 0 { + !if fast_subindex = 0 { + hddencbuf = hdddirbuf ;writes come from cache + } else { ;fast_subindex = 1 + !if hdddirbuf < reloc { + hddencbuf = hdddirbuf - $200 + } else { ;hdddirbuf + hddencbuf = hdddirbuf + $200 + !if (hddencbuf + $200) > $c000 { + !if hddencbuf < $d000 { + !set hddencbuf = reloc - $200 + } ;hddencbuf + } ;hddencbuf + } ;hdddirbuf + } ;fast_subindex + } ;aligned_read = 0 or rwts_mode = 1 + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + rwts_mode) > 0 { + !if hddencbuf < reloc { + hddtreebuf = hddencbuf - $200 + } else { ;hddencbuf + hddtreebuf = hddencbuf + $200 + !if (hddtreebuf + $200) > $c000 { + !if hddtreebuf < $d000 { + !set hddtreebuf = reloc - $200 + } ;hddtreebuf + } ;hddtreebuf + } ;hddencbuf + } else { ;aligned_read = 1 + !if hdddirbuf < reloc { + hddtreebuf = hdddirbuf - $200 + } else { ;hdddirbuf + hddtreebuf = hdddirbuf + $200 + !if (hddtreebuf + $200) > $c000 { + !if hddtreebuf < $d000 { + !set hddtreebuf = reloc - $200 + } ;hddtreebuf + } ;hddtreebuf + } ;hdddirbuf + } ;aligned_read + } else { ;fast_trees = 0 + hddtreebuf = hdddirbuf + } ;fast_trees + } ;allow_trees + } ;load_high + } else { ;reloc > $c000 + !if ((hdddataend + $ff) & -256) != 0 { + !if ((hdddataend + $ff) & -256) < reloc { + !serious "initial reloc too high, adjust to ", (0 - (((hdddataend + $ff) & -256) - reloc)) & $ffff + } ;hdddataend + } ;hdddataend + !if load_high = 1 { + !if enable_floppy = 0 { + !if (((hdddataend + $ff) & -256) & $ffff) != 0 { + !warn "initial reloc too low, adjust to ", (0 - (((hdddataend + $ff) & -256) - reloc)) & $ffff + } ;hdddataend + } ;enable_floppy + hdddirbuf = reloc - $200 + !if aligned_read = 0 { + hddencbuf = hdddirbuf - $200 + } ;aligned_read + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + hddtreebuf = hddencbuf - $200 + } else { ;aligned_read = 1 and enable_write = 0 + hddtreebuf = hdddirbuf - $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + hddtreebuf = hdddirbuf + } ;fast_trees + } ;allow_trees + } else { ;load_high = 0 + !pseudopc ((hdddataend + $ff) & -256) { + hdddirbuf = * + } + !if ((aligned_read xor 1) + rwts_mode) > 0 { + !if fast_subindex = 0 { + hddencbuf = hdddirbuf ;writes come from cache + } else { ;fast_subindex = 1 + hddencbuf = hdddirbuf + $200 + } ;fast_subindex + } ;aligned_read = 0 or rwts_mode = 1 + !if allow_trees = 1 { + !if fast_trees = 1 { + !if ((aligned_read xor 1) + enable_write) > 0 { + hddtreebuf = hddencbuf + $200 + } else { ;aligned_read = 1 and enable_write = 0 + hddtreebuf = hdddirbuf + $200 + } ;aligned_read = 0 or enable_write = 1 + } else { ;fast_trees = 0 + hddtreebuf = hdddirbuf + } ;fast_trees + } ;allow_trees + } ;load_high + } ;reloc + !if verbose_info = 1 { + !warn "hdd code: ", reloc, "-", hddcodeend - 1 + !if hddcodeend != hdddataend { + !warn "hdd data: ", hddcodeend, "-", hdddataend - 1 + } + !warn "hdd dirbuf: ", hdddirbuf, "-", hdddirbuf + $1ff + !if ((aligned_read xor 1) + rwts_mode) > 0 { + !warn "hdd encbuf: ", hddencbuf, "-", hddencbuf + $1ff + } ;aligned_read = 0 or rwts_mode = 1 + !if allow_trees = 1 { + !warn "hdd treebuf: ", hddtreebuf, "-", hddtreebuf + $1ff + } ;allow_trees + !warn "hdd driver start: ", unrelochdd - init + !if (one_page + enable_floppy) = 0 { + !if ((hddcodeend - reloc) <= $100) { + !warn "one_page can be enabled, code is small enough" + } ;hddcodeend + } ;not one_page and not enable_floppy + } ;verbose_info +} ;PASS2 + +readbuff +!byte $D3,$C1,$CE,$A0,$C9,$CE,$C3,$AE diff --git a/src/6502/a2_sboot.s b/src/6502/a2_sboot.s new file mode 100644 index 0000000..5bb39a3 --- /dev/null +++ b/src/6502/a2_sboot.s @@ -0,0 +1,240 @@ +; A2 SmartPort boot loader ("sboot"). +; Designed for the xa65 assembler. +; +; Replaces ProDOS on an 800k (or bigger) SmartPort volume. Block 0 is +; the classic boot0 signature -- format byte, then SEC/BCS+3/JMP so a +; format-sniffing tool sees the same four bytes ProDOS's own boot0 uses +; -- followed by code that finds the slot from the boot ROM handoff, +; verifies the card answers the SmartPort ROM signature, and reads +; AAM.SYSTEM's data blocks straight to $2000. +; +; Unlike a general ProDOS reader, this does not walk the volume +; directory at runtime -- the bundler builds the disk, so it already +; knows exactly where AAM.SYSTEM's data blocks are. AAMSTART is the +; first block past the volume bitmap (block 7, given this volume's +; fixed layout -- see AAMSTART below), and AAM_BLOCKS is however many +; 512-byte blocks the interpreter takes, passed in with -DAAM_BLOCKS +; at assemble time (see the 6502/Makefile rule). The bundler allocates +; AAM.SYSTEM as the very first file on a freshly formatted volume, and +; a sapling/tree's data blocks are always written contiguously before +; their index block(s) (see vol_write_sapling in bundle_apple2.c), so +; that range is guaranteed contiguous -- the same trick 0boot uses to +; read whole tracks on a 5.25" disk. A sanity check on the loaded +; data's first byte (always the $4c of a JMP, see a2_frontend.s's boot +; header) catches a mismatched build before jumping into it. +; +; Once loaded, jump to $2003 (raw_entry in a2_frontend.s), the same +; entry 0boot uses, so the interpreter knows there is no ProDOS +; underneath. +; +; SmartPort calling convention (see e.g. 1000bit.it's Apple technote +; archive, smpt/tn.smpt.2 -- "SmartPort Technical Note #2") +; +; Boot ROM hands off to $0801 with X = slot * 16. +; $Cn00 is the slot's own I/O ROM. A block device answers +; $Cn01=$20, $Cn03=$00, $Cn05=$03, $Cn07=$00 (SmartPort; nonzero +; at $Cn07 means "generic ProDOS block device", no raw SmartPort +; calls). +; $Cn00 + [$CnFF] is the ProDOS-style entry point; 3 bytes further in +; is the raw SmartPort entry, called as +; jsr entry +; .byte cmd +; .word paramlist +; which returns with carry set and an error code in A on failure. +; READ BLOCK (cmd=1) parameter list -- count=3, unit, buffer ptr (2 +; bytes), block # (3 bytes, little-endian). + +#ifndef AAM_BLOCKS +#define AAM_BLOCKS 40 ; default for standalone assembly/testing +#endif + + COUT = $fded + + ; A sliver of the ProDOS global page that ProRWTS2's init (see + ; a2_prorwts2.acme) reads before it ever calls the (faked) MLI -- + ; DEVNUM tells it which slot/drive it booted from, and DEVADR01HI is + ; where it looks that slot up. 0boot fakes DEVNUM for the same + ; reason (see 0boot.acme); we additionally fake DEVADR01HI (below, + ; in cont) because a smartport boot -- unlike 0boot's floppy boot -- + ; falls through ProRWTS2's initial device-list checks and lands in + ; its SmartPort multi-unit scan, which calls the real MLI's + ; READ_BLOCK to seed a reference buffer. mlistub (a2_frontend.s) + ; only emulates GET_PREFIX, so that read silently no-ops and the + ; scan compares real data against a zeroed buffer forever. Faking + ; DEVADR01HI to match makes ProRWTS2 take its early "already know + ; the slot" exit instead, the same shortcut 0boot gets for free from + ; the Disk ][ ROM signature match. + DEVNUM = $bf30 + DEVADR01HI = $bf11 + + ; AAMSTART -- see the header comment above. Tied to this volume's + ; fixed layout -- BITMAP_BLOCK=6 plus one bitmap block, which covers + ; up to 4096 total blocks -- comfortably more than BLOCKS_800K + ; (1600) in bundle_apple2.c. A bigger volume needing a second + ; bitmap block would have to bump this. + AAMSTART = 7 + + ; zero page scratch -- nothing else is running yet, so any of it + ; is free to use. + ptr = $f0 ; 2 bytes, generic indirect pointer + cn_hi = $f2 ; 1 byte, $c0 + slot + blknum = $f3 ; 3 bytes, block # for do_read + blkleft = $f6 ; 1 byte, blocks still to load + msgptr = $f0 ; 2 bytes, reuses ptr -- boot is over by + ; the time a failure message needs it + +; ===================================== +; Block 0 +; ===================================== + + * = $0800 + + .byt 1 ; format marker, not executed (entry is $0801) + sec ; $0801 -- entry point, X = slot*16 + bcs cont ; always taken -- skips the jmp below + jmp cont ; dead code, present for format-sniffing tools + +cont + .( + stx DEVNUM ; DEVNUM = slot*16, drive 1 (see above) + txa + lsr + lsr + lsr + lsr ; a = slot number, 0-7 + tax ; x = slot number, stashed for below + ora #$c0 + sta cn_hi ; cn_hi = $c0 + slot + sta ptr+1 + txa + asl + tay ; y = slot*2 -- DEVADR01HI's index + lda cn_hi + sta DEVADR01HI,y ; see above + lda #0 + sta ptr + + ; verify the SmartPort ROM signature at $Cn01/03/05/07 + ldy #1 + lda (ptr),y + cmp #$20 + bne notsp + ldy #3 + lda (ptr),y + bne notsp + ldy #5 + lda (ptr),y + cmp #$03 + bne notsp + ldy #7 + lda (ptr),y +; bne notsp ; TODO doesnt work on izapple? + + ; entry = $Cn00 + [$CnFF]; raw SmartPort entry is 3 bytes further in + ldy #$ff + lda (ptr),y + clc + adc #3 + sta docall+1 + lda cn_hi + sta docall+2 + + ; read AAM_BLOCKS blocks starting at AAMSTART into $2000 + lda #AAMSTART + sta blknum + lda #0 + sta blknum+1 + sta blknum+2 + lda #0 + sta parambuf ; dest lo = 0 + lda #$20 + sta parambuf+1 ; dest hi = $20 -- $2000 + lda #AAM_BLOCKS + sta blkleft +loadloop + jsr do_read + bcs diskerr +rdok + inc blknum + bne nocarry + inc blknum+1 +nocarry + lda parambuf+1 + clc + adc #2 + sta parambuf+1 + dec blkleft + bne loadloop + + ; sanity check -- AAM.SYSTEM's boot header always starts with a + ; JMP ($4c); catches a mismatched build before jumping into it + lda $2000 + cmp #$4c + beq loaded + ldx #msg_badsys + jmp fail + +loaded + jmp $2003 ; raw_entry -- no ProDOS underneath us + +notsp + ldx #msg_notsp + jmp fail + +diskerr + ldx #msg_diskerr + jmp fail + .) + +; input blknum (3 bytes) + parambuf (2 bytes), pre-set by the caller; +; output c = error +do_read + lda blknum + sta paramblk + lda blknum+1 + sta paramblk+1 + lda blknum+2 + sta paramblk+2 +docall + jsr $0000 ; operand patched at boot to the raw + ; SmartPort entry + .byt $01 ; READ BLOCK + .word paramlist + rts + +paramlist + .byt 3 +paramunit + .byt 1 +parambuf + .word 0 +paramblk + .byt 0,0,0 + +; input x/y = message, low/high; never returns +fail + .( + stx msgptr + sty msgptr+1 + ldy #0 +loop + lda (msgptr),y + beq halt + jsr COUT + iny + bne loop +halt + jmp halt + .) + +msg_notsp + .asc "NOT A SMARTPORT DEVICE",0 +msg_diskerr + .asc "DISK ERROR",0 +msg_badsys + .asc "BAD AAM.SYSTEM",0 + + .dsb $0a00-*,0 ; error out if this grew past one block diff --git a/src/6502/a2_thirdparty_license.txt b/src/6502/a2_thirdparty_license.txt new file mode 100644 index 0000000..aa5d79f --- /dev/null +++ b/src/6502/a2_thirdparty_license.txt @@ -0,0 +1,34 @@ + + +The Apple II interpreter also contains ProRWTS2 (block-level file access) +and 0boot (the boot loader used on 140k disks), both under the following +terms: + +ProRWTS2, copyright (c) Peter Ferrie 2013-2022 +0boot, copyright (c) Peter Ferrie 2015-2016, 2018 + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/6502/engine.s b/src/6502/engine.s index 9768337..623ec1d 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -585,7 +585,7 @@ initengine5 .) .( - lda #>initsegment + lda #>initsegment+$ff ; in case initsegment isn't on a page boundary sta firstpg .) @@ -2150,6 +2150,12 @@ listdone rts .) +#ifdef A2_ENGINE_HIMEM +engine_reloc = * +* = $d000 +himem_start = * +#endif + error .( sta regs+1 @@ -8179,9 +8185,23 @@ ext0_restore adc SAVEADDR+6 sta physize+1 +#ifdef A2_EVICT_MAX + ; io_load may refill the whole + ; SAVEMAXBYTES window whatever the image + ; really weighs, so every page it can + ; reach has to go, up to and including the + ; one holding the last byte. That last one + ; is a partial page unless SAVEADDR is + ; page aligned, hence unmap1 rather than + ; falling into the dex. + + ldx #>(SAVEADDR+SAVEMAXBYTES-1) + bne unmap1 ; always, page is nonzero +#else tax lda physize bne unmap1 +#endif unmap dex unmap1 @@ -9409,6 +9429,11 @@ optable engine_footprint = *-engine_firstaddr +#ifdef A2_ENGINE_HIMEM +himem_end = * +* = engine_reloc +#endif + ;====================================== initsegment ;====================================== diff --git a/src/6502/license.txt b/src/6502/license.txt index 4d2a85b..5c90c0b 100644 --- a/src/6502/license.txt +++ b/src/6502/license.txt @@ -1,4 +1,4 @@ -Å-machine 6502 C64 Interpreter +Å-machine 6502 C64 / Apple II Interpreter Copyright 2019-2026 Linus Åkesson and the Dialog Project Redistribution and use in source and binary forms, with or without diff --git a/src/Makefile b/src/Makefile index e465910..5ccbac3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -40,7 +40,11 @@ distclean: clean uninstall aamshow: aamshow.c crc32.c aavm.c aavm.h crc32.h ${CC} ${CFLAGS} -o $@ aamshow.c crc32.c aavm.c -aambundle: aambundle.c bundle_web.c bundle_c64.c \ +aambundle: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ + table_a2terp.h \ + table_a20boot.h \ + table_a2sboot.h \ + table_a2license.h \ table_c64license.h \ table_c64drive.h \ table_c64load.h \ @@ -51,7 +55,7 @@ aambundle: aambundle.c bundle_web.c bundle_c64.c \ table_front.h \ table_jquery.h \ table_play.h - ${CC} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c + ${CC} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c aamrun.linux: js/nodefrontend.js js/engine.js pkg -t linux -o aamrun.linux js/nodefrontend.js @@ -68,7 +72,11 @@ aamrun.exe: js/nodefrontend.js js/engine.js aamshow.exe: aamshow.c crc32.c aavm.c aavm.h crc32.h ${MINGW32} ${CFLAGS} -o $@ aamshow.c crc32.c aavm.c -aambundle.exe: aambundle.c bundle_web.c bundle_c64.c \ +aambundle.exe: aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c \ + table_a2terp.h \ + table_a20boot.h \ + table_a2sboot.h \ + table_a2license.h \ table_c64license.h \ table_c64drive.h \ table_c64load.h \ @@ -79,7 +87,19 @@ aambundle.exe: aambundle.c bundle_web.c bundle_c64.c \ table_front.h \ table_jquery.h \ table_play.h - ${MINGW32} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c + ${MINGW32} ${CFLAGS} -o $@ aambundle.c bundle_web.c bundle_c64.c bundle_apple2.c + +table_a2terp.h: 6502 6502/a2.system mkheader + ./mkheader table_a2terp <6502/a2.system >$@ + +table_a20boot.h: 6502 6502/a2_0boot.bin mkheader + ./mkheader table_a20boot <6502/a2_0boot.bin >$@ + +table_a2sboot.h: 6502 6502/a2_sboot.bin mkheader + ./mkheader table_a2sboot <6502/a2_sboot.bin >$@ + +table_a2license.h: 6502 6502/license.txt 6502/a2_thirdparty_license.txt mkheader + cat 6502/license.txt 6502/a2_thirdparty_license.txt | ./mkheader table_a2license >$@ table_c64license.h: 6502 6502/license.txt mkheader ./mkheader table_c64license <6502/license.txt >$@ diff --git a/src/aambundle.c b/src/aambundle.c index 4143648..69b3cd1 100644 --- a/src/aambundle.c +++ b/src/aambundle.c @@ -15,6 +15,53 @@ uint8_t *story; uint32_t storysize; static char *dirname; +static char *storyfile; + +static int append_name(char *storyname, int storynamesize, int snamelen, char ch) { + if(snamelen < storynamesize - 1) { + if((ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || (ch >= '0' && ch <= '9')) { + storyname[snamelen++] = ch; + } else { + storyname[snamelen++] = '-'; + } + } + return snamelen; +} + +// Stories without a title in their metadata are named after +// the base name of the input file, e.g. "foo/bar.aastory" +// becomes "bar". + +static int name_from_filename(char *storyname, int storynamesize) { + char *base, *end, *p; + int snamelen = 0; + + if(!storyfile) { + return 0; + } + base = storyfile; + for(p = storyfile; *p; p++) { + if(*p == '/' || *p == '\\') { + base = p + 1; + } + } + end = 0; + for(p = base; *p; p++) { + if(*p == '.') { + end = p; + } + } + if(!end || end == base) { + end = p; + } + for(p = base; p < end; p++) { + snamelen = append_name(storyname, storynamesize, snamelen, *p); + } + storyname[snamelen] = 0; + return snamelen; +} void visit_chunks(char *storyname, int storynamesize, file_visitor_t file_visitor) { uint32_t pos = 12, size; @@ -39,15 +86,7 @@ void visit_chunks(char *storyname, int storynamesize, file_visitor_t file_visito if(chunk[0] == AAMETA_TITLE) { chunk++; while((ch = *chunk++)) { - if(snamelen < storynamesize - 1) { - if((ch >= 'a' && ch <= 'z') - || (ch >= 'A' && ch <= 'Z') - || (ch >= '0' && ch <= '9')) { - storyname[snamelen++] = ch; - } else { - storyname[snamelen++] = '-'; - } - } + snamelen = append_name(storyname, storynamesize, snamelen, ch); } storyname[snamelen] = 0; } else { @@ -61,6 +100,13 @@ void visit_chunks(char *storyname, int storynamesize, file_visitor_t file_visito } pos += (8 + size + 1) & ~1; } + + if(!snamelen) { + snamelen = name_from_filename(storyname, storynamesize); + } + if(!snamelen) { + snprintf(storyname, storynamesize, "story"); + } } void trim_chunks(int align_writ) { @@ -123,11 +169,12 @@ void usage(char *prgname) { fprintf(stderr, "--help -h Display this information.\n"); fprintf(stderr, "\n"); fprintf(stderr, "--output -o Set output directory/file name.\n"); - fprintf(stderr, "--target -t Select target (web, c64, web:story).\n"); + fprintf(stderr, "--target -t Select target (web, c64, apple2, web:story).\n"); fprintf(stderr, "\n"); fprintf(stderr, "Targets:\n"); fprintf(stderr, "web (default) Directory with web interpreter.\n"); fprintf(stderr, "c64 Directory with c64 disk image.\n"); + fprintf(stderr, "apple2 Directory with apple2 disk images.\n"); fprintf(stderr, "web:story Just story.js for the web interpreter.\n"); exit(1); } @@ -176,9 +223,12 @@ int main(int argc, char **argv) { usage(prgname); } + storyfile = argv[optind]; + if(strcmp(target, "web") && strcmp(target, "web:story") - && strcmp(target, "c64")) { + && strcmp(target, "c64") + && strcmp(target, "apple2")) { fprintf(stderr, "Unsupported target \"%s\".\n", target); exit(1); } @@ -239,7 +289,7 @@ int main(int argc, char **argv) { if(!strcmp(target, "web:story")) { bundle_web_story(dirname); } else { - if(mkdir(dirname, 0777)) { + if(mkdir(dirname, 0777) && errno != EEXIST) { fprintf(stderr, "%s: %s\n", dirname, strerror(errno)); exit(1); } @@ -247,6 +297,8 @@ int main(int argc, char **argv) { bundle_web(dirname); } else if(!strcmp(target, "c64")) { bundle_c64(dirname); + } else if(!strcmp(target, "apple2")) { + bundle_apple2(dirname); } } diff --git a/src/aambundle.h b/src/aambundle.h index 789128e..82f2e6d 100644 --- a/src/aambundle.h +++ b/src/aambundle.h @@ -13,4 +13,5 @@ void trim_chunks(int align_writ); void bundle_web(char *dirname); void bundle_c64(char *dirname); +void bundle_apple2(char *dirname); void bundle_web_story(char *filename); diff --git a/src/bundle_apple2.c b/src/bundle_apple2.c new file mode 100644 index 0000000..a1ca58b --- /dev/null +++ b/src/bundle_apple2.c @@ -0,0 +1,850 @@ +#include +#include +#include +#include +#include +#include + +#include "aambundle.h" + +#include "table_a20boot.h" +#include "table_a2sboot.h" +#include "table_a2license.h" +#include "table_a2terp.h" + +/* Apple II bundling. + * + * 140k disks boot with 0boot, Peter Ferrie's track loader, which reads + * AAM.SYSTEM straight off the raw tracks and enters it at $2003. The volume is + * still an ordinary ProDOS one -- only block 0 differs -- so ProRWTS2 can find + * STORY by name and the files can be copied off with any ProDOS tool. + * These disks are DOS 3.3 order (.dsk) not ProDOS order (.po) + * + * 0boot only understands 5.25" tracks, so 800k images boot with a2sboot + * instead: a from-scratch SmartPort block-0 loader. Like 0boot it does not + * walk the volume directory -- it just reads a fixed run of blocks, known at + * assemble time, straight to $2000 and enters at $2003. See build_800k and + * a2_sboot.s for how the bundler and the loader agree on where that run is. + * No ProDOS kernel or boot blocks are needed on either disk size. + * + * AAM.SYSTEM is a ProDOS system program, type SYS ($ff), load address + * $2000. + * + * STORY is read through the a page at a time and never loaded whole, + * so its file type should be $06 (binary). It's padded to 256-byte pages. + * + * SAVEFILE is an empty 4 kB file on the boot volume. ProRWTS2 can only write + * to a file that already exists, and cannot change its length, so the + * interpreter overwrites this one in place instead of creating a savefile. + * When a story fills a 140k disk to the point where the savefile no longer + * fits, the single disk is built without one and a two-disk set is built + * alongside it, so there is always a way to save. + * + * The disk volume containing the story file should be named "AA.STORY" + * but it need not be the same as the boot/interpreter volume. + * This mainly applies when booting from "real" ProDOS, because ProRWTS + * doesn't do a volume scan. + */ + +static char storyname[48]; + +/* ProDOS on-disk layout. Blocks are 512 bytes and .po images store them + * in plain ascending order, so a block number is just an offset. */ +#define BLOCK_SIZE 512 +#define ENTRY_LENGTH 0x27 +#define ENTRIES_PER_BLOCK 13 +#define VOLUME_DIR_BLOCK 2 +#define VOLUME_DIR_BLOCKS 4 +#define BITMAP_BLOCK 6 +#define POINTERS_PER_INDEX 256 +#define MAX_FILES (VOLUME_DIR_BLOCKS * ENTRIES_PER_BLOCK - 1) + +/* storage types, the high nibble of the first byte of a directory entry */ +#define ST_SEEDLING 1 +#define ST_SAPLING 2 +#define ST_TREE 3 +#define ST_VOLUME_HEADER 0xf + +#define ACCESS_UNLOCKED 0xe3 +#define ACCESS_VOLUME 0xc3 + +#define TYPE_BIN 0x06 +#define TYPE_SYS 0xff + +#define BLOCKS_140K 280 +#define BLOCKS_800K 1600 +#define BLOCKS_PER_TRACK 8 + +/* A 5.25" disk holds 35 tracks of 16 physical sectors, and there are two + * conventions for storing them in a file. A .po image is a plain run of + * ProDOS blocks, which is what we build; a .dsk image holds the same sectors + * in DOS 3.3 order within each track, which is what most emulators and + * disk-writing tools expect from a 5.25" image, so that is what we ship. + * 3.5" disks have no such split and stay .po. + * + * Indexed by DOS logical sector, giving the ProDOS logical sector that holds + * the same physical sector. The table happens to be its own inverse. */ +#define SECTOR_SIZE 256 +#define SECTORS_PER_TRACK 16 +#define TRACK_SIZE (SECTOR_SIZE * SECTORS_PER_TRACK) + +static const int dos_order[SECTORS_PER_TRACK] = { + 0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15 +}; + +/* ProRWTS2 can neither create a file nor change the length of one, so a + * savefile has to be on the disk already, at its full size, for a save to + * overwrite in place. An all-zero one has no AASV header, which is how the + * interpreter tells "never saved" from a real savefile. + * (This should be the same as SAVEMAXBYTES in engine.s) + */ +#define SAVEFILE_BYTES 4096 + +/* 0boot's two stages go where ProDOS puts its own. The boot PROM reads + * physical sector 0 into $800 and hands over with $3d already at 1, so 0boot's + * inc asks for physical sector 2 -- which a ProDOS-order image holds at + * logical sector 1, the high half of block 0. So the blob drops in at offset + * 0 as it stands, its second stage landing at $100 by itself. + * + * 0boot then reads whole tracks from track 1, which means AAM.SYSTEM has to + * start on that boundary, at block 8. Block 7 is the cost of the alignment. */ +#define BOOT0_FIRST_DATA BLOCKS_PER_TRACK + +/* Writes data, then pads the file with zeros up to a multiple of padto + * bytes (padto = 1 means no padding). */ +static void writefile_padded(char *dirname, char *name, const uint8_t *data, size_t size, size_t padto) { + char *filename; + FILE *f; + size_t npad; + + filename = malloc(strlen(dirname) + strlen(name) + 2); + sprintf(filename, "%s/%s", dirname, name); + + f = fopen(filename, "wb"); + if(!f) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + if(size != fwrite(data, 1, size, f)) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + npad = (padto - size % padto) % padto; + while(npad--) { + if(EOF == fputc(0, f)) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + size++; + } + fclose(f); + + printf("%-14s %7lu bytes\n", name, (unsigned long) size); + + free(filename); +} + +static void writefile(char *dirname, char *name, const uint8_t *data, size_t size) { + writefile_padded(dirname, name, data, size, 1); +} + +/* ---------------------------------------------------------------- writing */ + +/* A freshly formatted ProDOS volume, built up block by block. Blocks 0-1 + * hold the boot code, 2-5 the volume directory and 6 the bitmap; ProDOS + * reserves all of those whatever they contain, so the boot blocks are free + * to carry, and a data disk gets them too in order to fail politely when + * someone boots it by mistake. */ +struct volume { + uint8_t *image; + uint8_t *isfree; + int total_blocks; + int bitmap_blocks; + int first_data; + int file_count; + char name[16]; +}; + +struct diskfile { + const char *name; + const uint8_t *data; + uint32_t size; + uint8_t type; + uint16_t aux; +}; + +/* An unwritten savefile is all zeroes, which is exactly what the interpreter + * expects to find before the first save. */ +static const uint8_t blank_savefile[SAVEFILE_BYTES]; + +static const struct diskfile savefile = { + "SAVEFILE", blank_savefile, SAVEFILE_BYTES, TYPE_BIN, 0 +}; + +static uint32_t prodos_now(void) { + time_t now = time(0); + struct tm *tm = localtime(&now); + uint16_t date, clock; + + date = ((tm->tm_year % 100) << 9) | ((tm->tm_mon + 1) << 5) | tm->tm_mday; + clock = (tm->tm_hour << 8) | tm->tm_min; + return ((uint32_t) clock << 16) | date; +} + +static void put_datetime(uint8_t *dest, uint32_t stamp) { + dest[0] = stamp & 0xff; + dest[1] = (stamp >> 8) & 0xff; + dest[2] = (stamp >> 16) & 0xff; + dest[3] = (stamp >> 24) & 0xff; +} + +static void put_word(uint8_t *dest, uint16_t value) { + dest[0] = value & 0xff; + dest[1] = value >> 8; +} + +/* Returns the address of directory slot n; slot 0 is the volume header. */ +static uint8_t *dir_slot(struct volume *v, int slot) { + int block = VOLUME_DIR_BLOCK + slot / ENTRIES_PER_BLOCK; + + return v->image + + (size_t) block * BLOCK_SIZE + + 4 + (slot % ENTRIES_PER_BLOCK) * ENTRY_LENGTH; +} + +/* first_data is the first block files may use; 0 means the usual one, just + * past the bitmap. 0boot volumes pass 8 to keep the interpreter on a track + * boundary. */ +static void vol_init(struct volume *v, const char *name, int total_blocks, const uint8_t *boot, size_t bootsize, int first_data) { + uint8_t *entry; + int i, num, prev, next; + + memset(v, 0, sizeof(*v)); + snprintf(v->name, sizeof(v->name), "%s", name); + v->total_blocks = total_blocks; + v->image = calloc(total_blocks, BLOCK_SIZE); + v->isfree = malloc(total_blocks); + if(!v->image || !v->isfree) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + + if(boot) memcpy(v->image, boot, bootsize); + + /* one bit per block, most significant bit first; one bitmap block + * covers 8 * 512 = 4096 blocks */ + v->bitmap_blocks = (total_blocks + 4095) / 4096; + v->first_data = BITMAP_BLOCK + v->bitmap_blocks; + if(first_data > v->first_data) v->first_data = first_data; + memset(v->isfree, 1, total_blocks); + for(i = 0; i < v->first_data; i++) v->isfree[i] = 0; + + entry = dir_slot(v, 0); + entry[0] = (ST_VOLUME_HEADER << 4) | strlen(v->name); + memcpy(entry + 1, v->name, strlen(v->name)); + put_datetime(entry + 0x18, prodos_now()); + entry[0x1e] = ACCESS_VOLUME; + entry[0x1f] = ENTRY_LENGTH; + entry[0x20] = ENTRIES_PER_BLOCK; + put_word(entry + 0x23, BITMAP_BLOCK); + put_word(entry + 0x25, total_blocks); + + /* link the directory blocks into a doubly linked chain */ + for(i = 0; i < VOLUME_DIR_BLOCKS; i++) { + num = VOLUME_DIR_BLOCK + i; + prev = i? num - 1 : 0; + next = (i < VOLUME_DIR_BLOCKS - 1)? num + 1 : 0; + put_word(v->image + (size_t) num * BLOCK_SIZE + 0, prev); + put_word(v->image + (size_t) num * BLOCK_SIZE + 2, next); + } +} + +static void vol_free(struct volume *v) { + free(v->image); + free(v->isfree); + v->image = 0; + v->isfree = 0; +} + +static int vol_blocks_free(struct volume *v) { + int i, n = 0; + + for(i = 0; i < v->total_blocks; i++) if(v->isfree[i]) n++; + return n; +} + +/* Returns a fresh block number, or -1 when the volume is full. */ +static int vol_alloc(struct volume *v) { + int i; + + for(i = v->first_data; i < v->total_blocks; i++) { + if(v->isfree[i]) { + v->isfree[i] = 0; + return i; + } + } + return -1; +} + +static void vol_put(struct volume *v, int num, const uint8_t *data, uint32_t size) { + memcpy(v->image + (size_t) num * BLOCK_SIZE, data, size); +} + +/* Stores up to 256 blocks under one index block. Returns -1 if the volume + * fills up, otherwise the index block number, with the number of blocks + * consumed (index block included) in *used. + * + * The data goes down first and the index block after it, so that a file's + * payload starts at the first free block on the volume. ProDOS does not care + * where the index block sits, and 0boot needs AAM.SYSTEM to begin exactly on + * the track boundary. */ +static int vol_write_sapling(struct volume *v, const uint8_t *data, uint32_t size, int *used) { + uint32_t pos = 0, chunk; + int index, num, i = 0; + uint8_t pointers[BLOCK_SIZE]; + + memset(pointers, 0, sizeof(pointers)); + *used = 1; + while(pos < size) { + chunk = (size - pos > BLOCK_SIZE)? BLOCK_SIZE : size - pos; + num = vol_alloc(v); + if(num < 0) return -1; + vol_put(v, num, data + pos, chunk); + pointers[i] = num & 0xff; + pointers[POINTERS_PER_INDEX + i] = num >> 8; + (*used)++; + pos += chunk; + i++; + } + + index = vol_alloc(v); + if(index < 0) return -1; + vol_put(v, index, pointers, sizeof(pointers)); + return index; +} + +/* Stores the payload as a seedling, sapling or tree, whichever fits. */ +static int vol_write_data(struct volume *v, const uint8_t *data, uint32_t size, uint8_t *storage, int *key, int *used) { + uint32_t nblocks, pos = 0, chunk; + uint8_t pointers[BLOCK_SIZE]; + int master, index, sub, i = 0; + + nblocks = (size + BLOCK_SIZE - 1) / BLOCK_SIZE; + if(!nblocks) nblocks = 1; + + if(nblocks == 1) { + *key = vol_alloc(v); + if(*key < 0) return -1; + vol_put(v, *key, data, size); + *storage = ST_SEEDLING; + *used = 1; + return 0; + } + + if(nblocks <= POINTERS_PER_INDEX) { + index = vol_write_sapling(v, data, size, used); + if(index < 0) return -1; + *storage = ST_SAPLING; + *key = index; + return 0; + } + + /* Same again for the master index block -- data first, pointers after. */ + memset(pointers, 0, sizeof(pointers)); + *used = 1; + while(pos < size) { + chunk = POINTERS_PER_INDEX * BLOCK_SIZE; + if(chunk > size - pos) chunk = size - pos; + index = vol_write_sapling(v, data + pos, chunk, &sub); + if(index < 0) return -1; + pointers[i] = index & 0xff; + pointers[POINTERS_PER_INDEX + i] = index >> 8; + *used += sub; + pos += chunk; + i++; + } + + master = vol_alloc(v); + if(master < 0) return -1; + vol_put(v, master, pointers, sizeof(pointers)); + *storage = ST_TREE; + *key = master; + return 0; +} + +/* Returns 0, or -1 if the file does not fit. */ +static int vol_add(struct volume *v, const struct diskfile *f) { + uint8_t *entry, storage = 0; + uint32_t stamp; + int key = 0, used = 0, slot, block; + + if(v->file_count >= MAX_FILES) return -1; + if(vol_write_data(v, f->data, f->size, &storage, &key, &used)) return -1; + + slot = ++v->file_count; + block = VOLUME_DIR_BLOCK + slot / ENTRIES_PER_BLOCK; + stamp = prodos_now(); + + entry = dir_slot(v, slot); + entry[0] = (storage << 4) | strlen(f->name); + memcpy(entry + 1, f->name, strlen(f->name)); + entry[0x10] = f->type; + put_word(entry + 0x11, key); + put_word(entry + 0x13, used); + entry[0x15] = f->size & 0xff; + entry[0x16] = (f->size >> 8) & 0xff; + entry[0x17] = (f->size >> 16) & 0xff; + put_datetime(entry + 0x18, stamp); + entry[0x1e] = ACCESS_UNLOCKED; + put_word(entry + 0x1f, f->aux); + put_datetime(entry + 0x21, stamp); + put_word(entry + 0x25, block); + + put_word(dir_slot(v, 0) + 0x21, v->file_count); + + return 0; +} + +static void vol_write_bitmap(struct volume *v) { + uint8_t *bits = v->image + (size_t) BITMAP_BLOCK * BLOCK_SIZE; + int i; + + memset(bits, 0, (size_t) v->bitmap_blocks * BLOCK_SIZE); + for(i = 0; i < v->total_blocks; i++) { + if(v->isfree[i]) bits[i >> 3] |= 0x80 >> (i & 7); + } +} + +/* Writes the volume image out, reordering the sectors of every track into DOS + * 3.3 order for a .dsk. Returns 0, or -1 on a write error. */ +static int write_image(FILE *f, const uint8_t *image, size_t size, int dsk) { + size_t track; + int i; + + if(!dsk) { + return (1 == fwrite(image, size, 1, f))? 0 : -1; + } + for(track = 0; track < size; track += TRACK_SIZE) { + for(i = 0; i < SECTORS_PER_TRACK; i++) { + if(1 != fwrite(image + track + + (size_t) dos_order[i] * SECTOR_SIZE, + SECTOR_SIZE, 1, f)) { + return -1; + } + } + } + return 0; +} + +/* Builds a volume holding the given files and writes it out. Returns 0, or + * -1 if they do not all fit, in which case nothing is written. */ +static int build_disk(char *dirname, const char *leaf, const char *volname, int total_blocks, const uint8_t *boot, size_t bootsize, int first_data, const struct diskfile *files, int nfiles) { + struct volume vol; + char *filename; + size_t namelen; + FILE *f; + int i; + + vol_init(&vol, volname, total_blocks, boot, bootsize, first_data); + for(i = 0; i < nfiles; i++) { + if(vol_add(&vol, &files[i])) { + vol_free(&vol); + return -1; + } + } + vol_write_bitmap(&vol); + + /* 0boot reads whole tracks, so the file it loads has to be one + * unbroken run from where it starts reading. It always is, but the + * disk is unbootable if it ever is not, so check rather than trust. */ + if(first_data && memcmp(vol.image + (size_t) first_data * BLOCK_SIZE, + files[0].data, files[0].size)) { + fprintf(stderr, "%s: %s is not contiguous from block %d\n", + leaf, files[0].name, first_data); + exit(1); + } + + namelen = strlen(dirname) + strlen(leaf) + 2; + filename = malloc(namelen); + snprintf(filename, namelen, "%s/%s", dirname, leaf); + + f = fopen(filename, "wb"); + if(!f) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + if(write_image(f, vol.image, (size_t) total_blocks * BLOCK_SIZE, + total_blocks == BLOCKS_140K)) { + fprintf(stderr, "%s: %s\n", filename, strerror(errno)); + exit(1); + } + fclose(f); + + printf("%-14s %4dk /%-8s %d files, %d blocks free\n", + leaf, + total_blocks * BLOCK_SIZE / 1024, + vol.name, + vol.file_count, + vol_blocks_free(&vol)); + + free(filename); + vol_free(&vol); + return 0; +} + +/* ---------------------------------------------------------------- readme */ + +enum { + DISKS_SINGLE, /* one disk, savefile and all */ + DISKS_BOTH, /* one disk without a savefile, or two disks with */ + DISKS_SPLIT, /* two disks, the story does not fit on one */ + DISKS_BIGONLY /* 800k only, the story does not fit on a floppy */ +}; + +static const char readme_head[] = +"Aa-machine for the Apple II family\n" +"==================================\n" +"\n" +"This directory holds bootable disk images for the story, together with\n" +"the files they were built from.\n" +"\n" +"System Requirements\n" +"-------------------\n" +"\n" +"Requires 64 kB. All the disks boot by themselves and need no ProDOS.\n" +"\n" +"The interpreter identifies the machine at boot time:\n" +"\n" +" Apple ][+ 40 columns, upper case only\n" +" //e / //c / IIgs 80 columns when an 80-column card is present\n" +" 128 kB machines additional page cache, undo support\n" +"\n" +"Disk Images\n" +"-----------\n" +"\n" +; + +/* One %s, the disk image name. */ +static const char readme_single[] = +" %s\n" +" A 140 kB disk, volume /AA.STORY, holding AAM.SYSTEM and\n" +" STORY. Boot it and the story starts.\n" +"\n" +; + +/* Two %s, the boot and story disk image names. */ +static const char readme_split[] = +"This story does not fit on one 140 kB disk alongside the interpreter,\n" +"so it comes as a two-disk set:\n" +"\n" +" %s\n" +" %s\n" +" Boot the first (volume /AA.BOOT, holding AAM.SYSTEM) and keep\n" +" the second (volume /AA.STORY, holding STORY) in the other\n" +" drive.\n" +"\n" +; + +/* Three %s, the single, boot and story disk image names. */ +static const char readme_both[] = +"This story fits on one 140 kB disk, but not with a savefile beside it,\n" +"so it comes both ways:\n" +"\n" +" %s\n" +" Bootable disk holding AAM.SYSTEM and STORY.\n" +" It has no SAVEFILE, so saving is unavailable.\n" +"\n" +" %s\n" +" %s\n" +" The same story as a two-disk set that can save. Boot the\n" +" first (volume /AA.BOOT, holding AAM.SYSTEM and SAVEFILE) and\n" +" keep the second (volume /AA.STORY, holding STORY) in the\n" +" other drive.\n" +"\n" +; + +static const char readme_toobig[] = +"This story is too large for a 140 kB disk even with nothing else on\n" +"it, so it needs an 800 kB drive or a hard disk.\n" +"\n" +; + +/* One %s, the 800k disk image name. */ +static const char readme_800k[] = +" %s\n" +" Bootable 800 kB disk (volume /AA.STORY) for a\n" +" 3.5\" drive or an emulator.\n" +"\n" +; + +static const char readme_emul[] = +"Running on an Emulator\n" +"----------------------\n" +"\n" +"The disk images boot in any Apple II emulator. The easiest one to\n" +"reach is Apple2TS, which runs in a browser with nothing to install:\n" +"\n" +" https://apple2ts.com\n" +"\n" +"Drop the 800 kb image onto the window, or use its disk menu to pick\n" +"from your machine, and it boots into the story.\n" +"\n" +"The 140 kB images are 5.25\" floppies and go into a Disk ][ drive.\n" +; + +/* Only when there is an 800k image. */ +static const char readme_emul_800k[] = +"The 800 kB image is a 3.5\" disk and has to go into an emulated 3.5\"\n" +"drive: it boots through SmartPort, so a slot configured as a plain\n" +"ProDOS hard disk will not start it.\n" +; + +static const char readme_emul_tail[] = +"\n" +"To save, the boot disk has to be write enabled, and the emulator has\n" +"to keep the change: in Apple2TS use \"Save Disk to Device\" to download\n" +"the modified image, or the saved game goes away with the browser tab.\n" +"\n" +; + +static const char readme_save[] = +"Saving\n" +"------\n" +"\n" +"Saved games are written into SAVEFILE, an empty 4 kB file that sits on\n" +"the boot volume next to AAM.SYSTEM. The interpreter overwrites it in\n" +"place -- it cannot create one or make it bigger -- so do not delete it,\n" +"and leave that disk write enabled if you want to save. On a two-disk\n" +"set it is the boot disk that has to be writable, not the story disk.\n" +"\n" +; + +static const char readme_build[] = +"Building Your Own ProDOS Volume\n" +"-------------------------------\n" +"\n" +"The other files in this directory are the ones that go on a ProDOS\n" +"disk, for putting the story on a hard disk or a volume of your own:\n" +"\n" +" AAM.SYSTEM the interpreter. ProDOS file type SYS ($ff),\n" +" auxiliary type (load address) $2000. ProDOS runs the\n" +" first *.SYSTEM file in the volume directory at boot,\n" +" so this should be the only one on the disk.\n" +"\n" +" STORY the story file. ProDOS file type BIN ($06) is a\n" +" reasonable choice. The name must be STORY, in the\n" +" volume directory, because that is the pathname the\n" +" interpreter opens.\n" +"\n" +" SAVEFILE an empty 4 kB file, ProDOS file type BIN ($06), that\n" +" saved games are written into. Optional, but without\n" +" it the story cannot be saved. It goes on the boot\n" +" volume, next to AAM.SYSTEM.\n" +"\n" +"When booting from real ProDOS, name the volume containing the story\n" +"file AA.STORY because the interpreter will scan for it by volume path.\n" +"\n" +; + +static void write_readme(char *dirname, int mode, const char *single, const char *bootdisk, const char *storydisk, const char *bigdisk) { + char *text; + size_t size; + + size = sizeof(readme_head) + sizeof(readme_single) + sizeof(readme_split) + + sizeof(readme_both) + sizeof(readme_toobig) + sizeof(readme_800k) + + sizeof(readme_emul) + sizeof(readme_emul_800k) + + sizeof(readme_emul_tail) + sizeof(readme_save) + + sizeof(readme_build); + if(single) size += strlen(single); + if(bootdisk) size += strlen(bootdisk) + strlen(storydisk); + if(bigdisk) size += strlen(bigdisk); + text = malloc(size); + if(!text) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + + strcpy(text, readme_head); + switch(mode) { + case DISKS_BOTH: + sprintf(text + strlen(text), readme_both, single, bootdisk, storydisk); + break; + case DISKS_SPLIT: + sprintf(text + strlen(text), readme_split, bootdisk, storydisk); + break; + case DISKS_BIGONLY: + strcat(text, readme_toobig); + break; + default: + sprintf(text + strlen(text), readme_single, single); + break; + } + if(bigdisk) { + sprintf(text + strlen(text), readme_800k, bigdisk); + } + strcat(text, readme_save); + strcat(text, readme_build); + + strcat(text, readme_emul); + if(bigdisk) { + strcat(text, readme_emul_800k); + } + strcat(text, readme_emul_tail); + + writefile(dirname, "readme.txt", (const uint8_t *) text, strlen(text)); + free(text); +} + +/* ---------------------------------------------------------------- bundle */ + +static char *disk_name(const char *suffix, const char *ext) { + char *name; + size_t size; + + size = strlen(storyname) + strlen(suffix) + strlen(ext) + 2; + name = malloc(size); + snprintf(name, size, "%s%s%s", storyname, suffix, ext); + return name; +} + +/* a2sboot reads AAM.SYSTEM's data blocks the dumb way -- a fixed run of + * AAM_BLOCKS blocks starting at AAMSTART, both baked into a2_sboot.s at + * assemble time (see its Makefile rule and header comment) -- rather than + * walking the volume directory at boot. That only works if AAM.SYSTEM is + * the first file allocated on a freshly formatted volume, so its data blocks + * land contiguously right there (see vol_write_sapling); passing first_data + * explicitly here (rather than 0, "use the default") turns on build_disk's + * existing contiguity check, the same one that guards 0boot, so a layout + * that ever breaks this assumption fails the build loudly instead of + * shipping a disk that boots into garbage. Must equal AAMSTART. */ +#define SBOOT_FIRST_DATA (BITMAP_BLOCK + 1) + +/* Builds the 800k image, self-booting via a2sboot. Sets *bigdisk. The + * story not fitting on an 800k disk at all is a hard error, handled here + * rather than passed back. */ +static void build_800k(char *dirname, const struct diskfile *aam, const struct diskfile *storyfile, char **bigdisk) { + struct diskfile files[3]; + + files[0] = *aam; + files[1] = *storyfile; + files[2] = savefile; + + *bigdisk = disk_name("-800k", ".po"); + + if(build_disk(dirname, *bigdisk, "AA.STORY", BLOCKS_800K, + table_a2sboot, sizeof(table_a2sboot), SBOOT_FIRST_DATA, files, 3) + && build_disk(dirname, *bigdisk, "AA.STORY", BLOCKS_800K, + table_a2sboot, sizeof(table_a2sboot), SBOOT_FIRST_DATA, files, 2)) { + fprintf(stderr, "%s: story too large for an 800k disk\n", *bigdisk); + exit(1); + } +} + +/* Builds the bootable images. Returns which set of 140k disks we made. */ +static int build_disks(char *dirname, uint8_t *padded, uint32_t paddedsize, char **single, char **bootdisk, char **storydisk, char **bigdisk) { + struct diskfile files[3], bootfiles[2]; + int single_nosave; + + files[0].name = "AAM.SYSTEM"; + files[0].data = table_a2terp; + files[0].size = sizeof(table_a2terp); + files[0].type = TYPE_SYS; + files[0].aux = 0x2000; + + files[1].name = "STORY"; + files[1].data = padded; + files[1].size = paddedsize; + files[1].type = TYPE_BIN; + files[1].aux = 0; + + files[2] = savefile; + + *single = disk_name("", ".dsk"); + if(!build_disk(dirname, *single, "AA.STORY", BLOCKS_140K, + table_a20boot, sizeof(table_a20boot), BOOT0_FIRST_DATA, + files, 3)) { + return DISKS_SINGLE; + } + + /* No room for a savefile. The story still fits on one disk without + * one, and a single disk is worth having even though it cannot save, + * so keep it and build the two-disk set as well -- on that set the + * interpreter has a disk to itself and the savefile fits. */ + single_nosave = !build_disk(dirname, *single, "AA.STORY", BLOCKS_140K, + table_a20boot, sizeof(table_a20boot), BOOT0_FIRST_DATA, + files, 2); + if(!single_nosave) { + free(*single); + *single = 0; + } + + /* The story disk goes first because it is the one that can fail, and + * failing before anything is written keeps us from leaving half a set + * behind. It carries no boot code -- under ProDOS it would get the + * boot blocks so that booting it by mistake gives ProDOS's own "unable + * to load" message, but 0boot's would try to load an interpreter that + * is not there, so it gets none, and the block back. */ + *storydisk = disk_name("-story", ".dsk"); + + if(build_disk(dirname, *storydisk, "AA.STORY", BLOCKS_140K, + 0, 0, 0, files + 1, 1)) { + /* Too big even for a floppy of its own, so 5.25" is out + * altogether and the 800k disk is the only way to ship it. */ + free(*storydisk); + *storydisk = 0; + build_800k(dirname, &files[0], &files[1], bigdisk); + return DISKS_BIGONLY; + } + + /* The boot disk has room to spare, and it is the one that stays in + * drive 1, so the savefile goes there rather than on the story disk. */ + bootfiles[0] = files[0]; + bootfiles[1] = savefile; + + *bootdisk = disk_name("-boot", ".dsk"); + if(build_disk(dirname, *bootdisk, "AA.BOOT", BLOCKS_140K, + table_a20boot, sizeof(table_a20boot), BOOT0_FIRST_DATA, + bootfiles, 2)) { + fprintf(stderr, "The interpreter does not fit on a 140k disk!\n"); + exit(1); + } + + build_800k(dirname, &files[0], &files[1], bigdisk); + return single_nosave? DISKS_BOTH : DISKS_SPLIT; +} + +void bundle_apple2(char *dirname) { + char *single = 0, *bootdisk = 0, *storydisk = 0, *bigdisk = 0; + uint8_t *padded; + uint32_t paddedsize; + int mode; + + visit_chunks(storyname, sizeof(storyname), 0); + trim_chunks(1); + + writefile(dirname, "AAM.SYSTEM", table_a2terp, sizeof(table_a2terp)); + /* The interpreter reads STORY a page at a time, so round the file up + * to a whole number of 256-byte pages. */ + writefile_padded(dirname, "STORY", story, storysize, 256); + writefile(dirname, "SAVEFILE", blank_savefile, SAVEFILE_BYTES); + + paddedsize = (storysize + 0xff) & ~0xffu; + padded = calloc(paddedsize? paddedsize : 1, 1); + if(!padded) { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + memcpy(padded, story, storysize); + + mode = build_disks(dirname, padded, paddedsize, &single, &bootdisk, &storydisk, &bigdisk); + + write_readme(dirname, mode, single, bootdisk, storydisk, bigdisk); + writefile(dirname, "interpreter_license.txt", + table_a2license, sizeof(table_a2license)); + + free(padded); + free(single); + free(bootdisk); + free(storydisk); + free(bigdisk); +} diff --git a/test/Makefile b/test/Makefile index c6feda2..8c6f873 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,5 +1,8 @@ all: test clean +apple2: + $(MAKE) -C apple2 test + test: ## This one is a 0.x file using 67 ENTER_STATUS_0 and E7 LEAVE_STATUS make -C gosling test @@ -13,4 +16,4 @@ clean: $(MAKE) -C body_not_status clean $(MAKE) -C impossible clean -.PHONY: all test clean +.PHONY: all test apple2 clean diff --git a/test/apple2/Makefile b/test/apple2/Makefile new file mode 100644 index 0000000..584251d --- /dev/null +++ b/test/apple2/Makefile @@ -0,0 +1,93 @@ +# Optional Apple II tests. +# +# These boot the 6502 interpreter from real disk images inside izapple2's +# scriptable console frontend (izconsole), driving it with the expect script +# a2test.exp. Unlike the other test suites this one needs an +# external emulator, so it is not part of the default `make test`. +# +# If `make test` complains about a missing emulator, build and install it +# (https://github.com/ivanizag/izapple2): +# +# git clone https://github.com/ivanizag/izapple2 +# cd izapple2 +# go build -o izconsole ./frontend/console +# cp izconsole ~/bin/ # or anywhere on your PATH + +IZCONSOLE ?= $(HOME)/bin/izconsole + +DIR6502 = ../../src/6502 +A2TEST = ./a2test.exp +AAMBUNDLE = ../../src/aambundle + +AAEXERCISE_DIR = aa-exercise +AAEXERCISE_DSK = $(AAEXERCISE_DIR)/aa-exercise.dsk +GOSLING_DIR = gosling +GOSLING_PO = $(GOSLING_DIR)/Miss-Gosling-s-Last-Case-800k.po + +all: test + +test: check test.exercise test.gosling test.undo test.save test.save2 + +# -- emulator and tool checks ------------------------------------------- + +check: + @if [ ! -x "$(IZCONSOLE)" ] && ! command -v $(IZCONSOLE) >/dev/null 2>&1; then \ + echo "*** izconsole not found (looked for $(IZCONSOLE) and on PATH)."; \ + echo "*** The Apple II tests need the izapple2 console emulator."; \ + echo "*** Build and install it:"; \ + echo "*** git clone https://github.com/ivanizag/izapple2"; \ + echo "*** cd izapple2 && go build -o izconsole ./frontend/console"; \ + echo "*** cp izconsole $(HOME)/bin/ # or anywhere on your PATH"; \ + exit 1; \ + fi + @command -v expect >/dev/null 2>&1 || { \ + echo "*** expect not found on PATH (needed by $(A2TEST))."; \ + exit 1; \ + } + +# -- the tests ------------------------------------------------------------ +# Each target exits nonzero if the expected text never appears on the +# emulated screen (see a2test.exp for the exit code conventions). + +test.exercise: $(AAEXERCISE_DSK) + $(A2TEST) 250101 $(AAEXERCISE_DSK) + +test.gosling: $(GOSLING_PO) + $(A2TEST) 'Your work here is finished' $(GOSLING_PO) ../gosling/gosling.in + +# Save/restore/undo tests. Each one boots the +# Gosling disk (which has a SAVEFILE), plays a few turns of the intro and +# then checks one metacommand: +# +# test.undo -- UNDO takes back a DROP, ball is back in Watson's mouth +# test.save -- SAVE, drop the ball, RESTORE: biting it must pop the treat +# out again, which is only possible with the restored state +# test.save2 -- SAVE, drop the ball, RESTORE: INVENTORY must show the ball + +test.undo: $(GOSLING_PO) + $(A2TEST) 'Undoing the last turn' $(GOSLING_PO) gosling-undo.in + +test.save: $(GOSLING_PO) + $(A2TEST) 'pops out' $(GOSLING_PO) gosling-save.in + +test.save2: $(GOSLING_PO) + $(A2TEST) 'holds up the rubber ball proudly' $(GOSLING_PO) gosling-save2.in + +# -- prerequisites -------------------------------------------------------- + +# The bundled disks live here too (gitignored), so the story directories +# stay clean. + +$(AAMBUNDLE): + $(MAKE) -C ../../src aambundle + +$(AAEXERCISE_DSK): ../aa-exercise/aa-exercise.aastory $(AAMBUNDLE) + $(AAMBUNDLE) -t apple2 -o $(AAEXERCISE_DIR) ../aa-exercise/aa-exercise.aastory + +$(GOSLING_PO): ../gosling/gosling.aastory $(AAMBUNDLE) + $(AAMBUNDLE) -t apple2 -o $(GOSLING_DIR) ../gosling/gosling.aastory + +clean: + @echo "Nothing to clean (the bundled disk images are .gitignored and left in place)." + +.PHONY: all test check test.exercise test.gosling test.undo test.save test.save2 clean diff --git a/test/apple2/a2test.exp b/test/apple2/a2test.exp new file mode 100755 index 0000000..604f4ad --- /dev/null +++ b/test/apple2/a2test.exp @@ -0,0 +1,94 @@ +#!/usr/bin/expect -f +# a2test.exp -- feed commands into izconsole and wait for a target string. +# +# Usage: ./a2test.exp "TARGET STRING" dsk [walkthrough-file | "command string"] [-- izconsole flags...] +# ./a2test.exp "Intro Text" story.dsk +# ./a2test.exp "DARKNESS" story.dsk walk.txt +# ./a2test.exp "DARKNESS" story.dsk "open door +# > go north" +# ./a2test.exp "DARKNESS" story.dsk walk.txt -- --model 2plus +# +# The disk image is required. The walkthrough is optional: if the third +# argument names an existing file its lines are sent as commands; otherwise it +# is taken as a literal command string, one command per line. With no third +# argument, only the story's opening text is checked for the target. +# Everything after "--" is passed through to izconsole. +# +# Exit codes: 0 target string seen +# 1 commands exhausted, target never seen +# 2 usage error +# 3 emulator died or hung before finishing + +set target [lindex $argv 0] +set dsk [lindex $argv 1] +set arg3 [lindex $argv 2] +if {$target eq "" || $dsk eq ""} { + puts stderr "usage: a2test.exp \"TARGET STRING\" dsk \[walkthrough-file | command string\] \[-- izconsole flags...\]" + exit 2 +} + +set lines {} +if {$arg3 ne ""} { + if {[file exists $arg3]} { + set fd [open $arg3 r] + set lines [split [read $fd] "\n"] + close $fd + } else { + set lines [split $arg3 "\n"] + } +} + +# everything after "--" is passed through to izconsole +set dash [lsearch -exact $argv "--"] +if {$dash >= 0} { + set izflags [lrange $argv [expr {$dash + 1}] end] +} else { + set izflags {} +} + +log_user 0 +spawn -noecho ~/bin/izconsole -speed full {*}$izflags $dsk + +# wait for the game prompt (or the target, which may appear in the intro) +expect { + -timeout 10 + -exact $target { puts "TARGET FOUND in intro"; exit 0 } + "> " { } + eof { puts "emulator exited before prompt"; exit 3 } + timeout { puts "no prompt after $timeout s"; exit 3 } +} + +set found 0 +set n 0 +foreach line $lines { + if {[string trim $line] eq ""} { continue } + incr n + send -- "$line\r" + expect { + -timeout 10 + -exact $target { + set found 1 + puts "TARGET FOUND (command $n: \"$line\")" + break + } + "> " { } ;# this command completed, keep going + eof { puts "*** emulator exited (command $n: \"$line\")"; exit 3 } + timeout { puts "*** timed out after command $n: \"$line\""; exit 3 } + } +} + +if {!$found} { + expect { + -timeout 60 + -exact $target { + set found 1 + puts "TARGET FOUND" + } + eof { puts "*** emulator exited"; exit 3 } + timeout { puts "*** timed out"; exit 3 } + } +} + +if {!$found} { puts "*** TARGET NOT FOUND after $n commands" } +close +exit [expr {$found ? 0 : 1}] diff --git a/test/apple2/gosling-save.in b/test/apple2/gosling-save.in new file mode 100644 index 0000000..0abe6d7 --- /dev/null +++ b/test/apple2/gosling-save.in @@ -0,0 +1,16 @@ +* *** Opening scene *** +x watson +x me +north +zxcvbn +watson, north +hughes, wait +zxcvbn +x zxcvbn +sing zxcvbn +* *** Save, diverge, restore *** +watson, take ball +save +drop ball +restore +bite ball diff --git a/test/apple2/gosling-save2.in b/test/apple2/gosling-save2.in new file mode 100644 index 0000000..0e8df64 --- /dev/null +++ b/test/apple2/gosling-save2.in @@ -0,0 +1,17 @@ +* *** Opening scene *** +x watson +x me +north +zxcvbn +watson, north +hughes, wait +zxcvbn +x zxcvbn +sing zxcvbn +* *** Save, diverge, restore *** +watson, take ball +save +drop ball +x ball +restore +inventory diff --git a/test/apple2/gosling-undo.in b/test/apple2/gosling-undo.in new file mode 100644 index 0000000..393ad74 --- /dev/null +++ b/test/apple2/gosling-undo.in @@ -0,0 +1,15 @@ +* *** Opening scene *** +x watson +x me +north +zxcvbn +watson, north +hughes, wait +zxcvbn +x zxcvbn +sing zxcvbn +* *** Undo *** +watson, take ball +drop ball +undo +inventory From 3fc14f43939a77907e04597a040f8349549b20cf Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 21:10:28 +0200 Subject: [PATCH 02/21] 6502: updated license for ProRWTS2 and 0boot --- license.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/license.txt b/license.txt index 71d657a..ce02fd4 100644 --- a/license.txt +++ b/license.txt @@ -87,3 +87,38 @@ redistributed inside this archive in compliance with the MIT license: The commandline tool aambox6502, for testing the 6502 interpreter, makes use of a 6502 emulator by Mike Chambers called fake6502. This emulator is in the public domain. + +--- + +The Apple II interpreter also contains ProRWTS2 (block-level file access) +and 0boot (the boot loader used on 140k disks), both under the following +terms: + +ProRWTS2, copyright (c) Peter Ferrie 2013-2022 +0boot, copyright (c) Peter Ferrie 2015-2016, 2018 + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 1e13cfa65ade22b8abf15440c218b659aa4c9d74 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 21:13:44 +0200 Subject: [PATCH 03/21] 6502: turn off verbose info during make --- src/6502/a2_prorwts2.acme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/6502/a2_prorwts2.acme b/src/6502/a2_prorwts2.acme index ba1362c..6754771 100644 --- a/src/6502/a2_prorwts2.acme +++ b/src/6502/a2_prorwts2.acme @@ -17,7 +17,7 @@ ver_02 = 1 ;place no code before init label below. ;user-defined options - verbose_info = 1 ;set to 1 to enable display of memory usage + verbose_info = 0 ;set to 1 to enable display of memory usage enable_floppy= 1 ;set to 1 to enable floppy drive support poll_drive = 1 ;set to 1 to check if disk is in drive, recommended if allow_multi is enabled use_smartport= 1 ;set to 1 to enable support for more than two MicroDrive (or more than four CFFA) partitions From 9531f6660355cbe5c087ec0949da921e4104927a Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 22:34:13 +0200 Subject: [PATCH 04/21] apple2: "more" prompt when a page of text printed (like c64) --- src/6502/a2_frontend.s | 82 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 3c7ef4a..3913a90 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -233,6 +233,7 @@ AUXUNDOBANK2 = $d0 ; logical page from ; ---- frontend zero page ---- +nunread = $05 ; lines scrolled off since last clear ioparam = $06 ; word, used by the engine wrappos = $08 xpos = $09 @@ -687,15 +688,74 @@ nocr ldx cury cpx #23 bcs nobump - inc cury nobump + inc nunread + lda nunread + clc + adc statush + cmp #23 + bcs moreprompt + rts + .) + +moreprompt + ; Print the [MORE] prompt on the current + ; line, wait for a key, and erase it + ; again. Only cout is used, so this can + ; be called from io_mline_raw without + ; recursing back into it. + + .( + ldx #0 +loop + lda moretxt,x + beq zterm + jsr cout + inx + bne loop +zterm + ; save the size of the prompt + ; we'll use backspaces in a silly way + ; because we have to be 80-column compatible + txa + pha + jsr getkey + pla + tax +erase + lda #$88 ; backspace + jsr cout + lda #$a0 + jsr cout + lda #$88 + jsr cout + dex + bne erase + + ldx #1 + stx nunread ; we wanna see the bottom line at the top rts .) io_mclear .( + ; on the c64, we stop for a [MORE] prompt + ; before clearing the screen if there + ; is any unread line + lda nunread + beq nomore + + ; do we need to issue a newline? + ldx xpos + beq atcol0 + lda #$8d + jsr cout +atcol0 + jsr moreprompt +nomore lda #0 + sta nunread sta xpos sta pendspc sta wrappos @@ -708,6 +768,13 @@ io_mclear rts .) + ; on the //e the 80-column card uses + ; characters 0-31 for control codes + ; so only 32-63 work with II+ and //e + ; in inverse mode +moretxt + .asc "<...>",0 + ; TODO some bugs in 80-column mode io_mstyle rts @@ -951,6 +1018,9 @@ io_getc .( jsr io_mflush + ; c64 clears this in both gets and getc + lda #0 + sta nunread jmp getkey .) @@ -1006,6 +1076,9 @@ backspace done sty f_temp2 jsr io_mline_raw + ; c64 clears this in both gets and getc + lda #0 + sta nunread ldy f_temp2 rts .) @@ -2520,6 +2593,10 @@ NTRANS = trlo-trhi coldstart .( + ; so we don't get a [MORE] prompt before the banner + lda #0 + sta nunread + jsr initsystem jsr initengine0 @@ -2539,6 +2616,9 @@ coldstart #ifdef DEBUG jsr dumpvars #endif + ; make sure we have no unread lines when we print the banner + lda #0 + sta nunread jsr io_mclear jmp startengine .) From 5a814c8bc2907e08b60bfaa3c4ea1a7b71f59644 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 22:59:06 +0200 Subject: [PATCH 05/21] apple2: fix status bar clearing w/ inverse spaces --- src/6502/a2_frontend.s | 67 +++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 3913a90..128c619 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -844,28 +844,52 @@ io_sprepare .( sta statush + ; Save horizontal position lda CH bit col80 bpl noourch - lda OURCH + lda OURCH ; 80 column mode uses another variable noourch sta savedch + ; Save vertical position too lda cury sta savedcv - ; clear the status rows by pointing the - ; window at them and homing + ; Fill every status row with inverse spaces - lda #0 - sta WNDTOP - lda statush + jsr sbar_home + lda #24 sta WNDBTM - jsr clrwin + + ldy statush + beq filled ; no status bar + +rowloop + bit col80 + bpl dospaces +; faster 80-column path using CLREOL + lda #29+128 ; clreol + jsr cout + lda #$8d ; next line + jsr cout + jmp nextrow +; We have to do it the hard way because the +; 40-column path clears with non-inverted spaces +dospaces + ldx scrw +sploop + lda #$a0 ; space + jsr cout + dex + bne sploop +nextrow + dey + bne rowloop +filled + jsr set_normal lda statush sta WNDTOP - lda #24 - sta WNDBTM ; blank the row 0 buffer @@ -923,7 +947,13 @@ notrow0 sta WNDTOP ldx stxoffs jsr gotoxy - rts + + ; rows 1 and up go straight to the screen + ; through cout, so inverse has to be on + ; for them too. io_scommit turns it back + ; off when the bar is done. + + jmp set_inverse .) io_sputc @@ -982,13 +1012,7 @@ past io_scommit .( - lda #0 - sta WNDTOP - - ldx #0 - ldy #0 - jsr gotoxy - jsr set_inverse + jsr sbar_home ldx #0 loop @@ -1009,6 +1033,15 @@ loop rts .) +sbar_home + .( + ldx #0 + stx WNDTOP + ldy #0 + jsr gotoxy + jmp set_inverse + .) + ; ===================================== ; Input ; ===================================== From 15fbcbfb13e4bdc8ee509050c0db1973132d9cb0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 23:17:43 +0200 Subject: [PATCH 06/21] apple2: added codepoints U+201E, U+2022, and U+202F, and 23 more for which the c64 has a glyph --- src/6502/a2_frontend.s | 44 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 128c619..d390233 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -2589,26 +2589,38 @@ trhi .byt $00,$00,$00,$00,$00,$00,$00,$00 .byt $00,$00,$00,$00,$00,$00,$00,$00 .byt $00,$00,$00,$00,$00,$00,$00,$00 - .byt $00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$00,$00,$00,$00 + .byt $00,$00,$00,$00,$01,$01,$01,$01 + .byt $01,$1e,$20,$20,$20,$20,$20,$20 .byt $20,$20,$20,$20,$20,$20,$20,$20 trlo - .byt $a0,$ab,$bb,$c4,$c5,$c6,$c9,$d6 - .byt $d8,$dc,$df,$e0,$e1,$e2,$e4,$e5 - .byt $e6,$e7,$e8,$e9,$ea,$eb,$ee,$ef - .byt $f1,$f4,$f6,$f8,$fc,$fd - .byt $10,$13,$14,$18,$19,$1c,$1d,$26 + .byt $a0,$ab,$bb,$c0,$c2,$c4,$c5,$c6 + .byt $c7,$c8,$c9,$ca,$cb,$ce,$cf,$d4 + .byt $d6,$d8,$d9,$db,$dc,$df,$e0,$e1 + .byt $e2,$e4,$e5,$e6,$e7,$e8,$e9,$ea + .byt $eb,$ee,$ef,$f1,$f4,$f6,$f8,$f9 + .byt $fb,$fc,$fd,$ff,$41,$42,$52,$53 + .byt $78,$9e,$10,$13,$14,$18,$19,$1c + .byt $1d,$1e,$22,$26,$2f,$39,$3a,$ac trc0 - .byt ' ','"','"','A','A','A','E','O' - .byt 'O','U','s','a','a','a','a','a' - .byt 'a','c','e','e','e','e','i','i' - .byt 'n','o','o','o','u','y' - .byt '-','-','-',$27,$27,$22,$22,'.' + .byt ' ','"','"','A','A','A','A','A' + .byt 'C','E','E','E','E','I','I','O' + .byt 'O','O','U','U','U','s','a','a' + .byt 'a','a','a','a','c','e','e','e' + .byt 'e','i','i','n','o','o','o','u' + .byt 'u','u','y','y','L','l','O','o' + .byt 'Y','S','-','-','-',$27,$27,$22 + .byt $22,$22,'*','.',' ',$27,$27,'E' trc1 - .byt 0,0,0,0,0,'E',0,0 - .byt 0,0,'s',0,0,0,0,0 - .byt 'e',0,0,0,0,0,0,0 - .byt 0,0,0,0,0,0 - .byt 0,0,'-',0,0,0,0,'.' + .byt 0,0,0,0,0,0,0,'E' + .byt 0,0,0,0,0,0,0,0 + .byt 0,0,0,0,0,'s',0,0 + .byt 0,0,0,'e',0,0,0,0 + .byt 0,0,0,0,0,0,0,0 + .byt 0,0,0,0,0,0,'E','e' + .byt 0,'S',0,0,'-',0,0,0 + .byt 0,0,0,'.',0,0,0,0 NTRANS = trlo-trhi From bdba69bf6af87d655226a4b5144ccf5706c3a574 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 24 Aug 2026 23:42:57 +0200 Subject: [PATCH 07/21] apple2: fix non-breaking spaces --- src/6502/a2_frontend.s | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index d390233..4173ee9 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -594,6 +594,7 @@ io_mputc cmp #$80 bcs extended +; entry point if we don't have an extended character plain jsr do_foldup ldx xpos @@ -627,13 +628,20 @@ wrap extended jsr translit + ; The only codepoints that transliterate + ; to space are U+00A0 and U+202F, both + ; non-breaking, so reenter at 'plain' + ; to avoid line breaks. + ; tr0 and tr1 are always < $80, so the + ; extended test is not needed either. + lda tr0 - jsr io_mputc ; always < $80 + jsr plain lda tr1 beq done - jmp io_mputc ; always < $80 + jmp plain done rts .) From e586f962ccf7565910d21b23cc3f0f93b12d7637 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 01:58:15 +0200 Subject: [PATCH 08/21] 6502: nbsp now degrades to space (instead of nothing) in engine.s --- readme.txt | 3 +++ src/6502/engine.s | 14 +++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/readme.txt b/readme.txt index f82d722..10cf6ef 100644 --- a/readme.txt +++ b/readme.txt @@ -147,6 +147,9 @@ Release notes: Aambundle: Stories without a title in their metadata are named after the base name of the story file. + 6502 engine: Fixed NBSP so that it properly degrades to a + breaking space instead of being ignored. + 1.0.2: Cleaned up the specification and some dev tools. diff --git a/src/6502/engine.s b/src/6502/engine.s index 623ec1d..7ae9b4a 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -8511,19 +8511,27 @@ ext0_clr_div ext0_clr_status ; This one could be implemented, but I don't know how jmp ldyfetchnext -ext0_nbsp ; Copied from op_space; can't call it directly because ext0 clobbered the Y register so we have to jump to ldyfetchnext instead of fetchnext +ext0_nbsp + ; This engine has no non-breaking space + ; state, so NBSP degrades to an ordinary + ; space, which is what the spec calls for + ; when an interpreter cannot provide one. + .( lda rcwl bne skip - lda #SPC_NO - adc #0 + ; op_space uses the dispatcher's carry flag + ; but here it was clobbered by ext0. + ; so we can't tell SPACE from NO_SPACE. + lda #SPC_PENDING cmp rspc bcc skip sta rspc skip + ; Y register was clobbered by ext0 jmp ldyfetchnext .) From 9f6f082bdf15cdebf7e0d577684c2d394d479c82 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 01:58:42 +0200 Subject: [PATCH 09/21] added codepoints test --- .gitignore | 4 + test/Makefile | 3 + test/apple2/a2test.exp | 14 +- test/codepoints/Makefile | 62 ++++ test/codepoints/codepoints.6502.gold | 364 ++++++++++++++++++++++ test/codepoints/codepoints.aastory | Bin 0 -> 5682 bytes test/codepoints/codepoints.dg | 445 +++++++++++++++++++++++++++ test/codepoints/codepoints.in | 1 + test/codepoints/codepoints.js.gold | 358 +++++++++++++++++++++ 9 files changed, 1250 insertions(+), 1 deletion(-) create mode 100644 test/codepoints/Makefile create mode 100644 test/codepoints/codepoints.6502.gold create mode 100644 test/codepoints/codepoints.aastory create mode 100644 test/codepoints/codepoints.dg create mode 100644 test/codepoints/codepoints.in create mode 100644 test/codepoints/codepoints.js.gold diff --git a/.gitignore b/.gitignore index d06b26b..13024c1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ src/6502/a2.system # Apple II test disks (built by test/apple2/Makefile) test/apple2/aa-exercise/ test/apple2/gosling/ +# Bundled disks for the codepoint exercise (built by test/codepoints/Makefile) +test/codepoints/codepoints/ +# Test out files +test/*/*.out diff --git a/test/Makefile b/test/Makefile index 8c6f873..1869422 100644 --- a/test/Makefile +++ b/test/Makefile @@ -10,10 +10,13 @@ test: make -C body_not_status test ## This one is whatever the latest version of Dialog produces (update periodically) make -C impossible test + ## Character set and status bar exercise; source is in the directory + make -C codepoints test clean: $(MAKE) -C gosling clean $(MAKE) -C body_not_status clean $(MAKE) -C impossible clean + $(MAKE) -C codepoints clean .PHONY: all test apple2 clean diff --git a/test/apple2/a2test.exp b/test/apple2/a2test.exp index 604f4ad..514bbea 100755 --- a/test/apple2/a2test.exp +++ b/test/apple2/a2test.exp @@ -12,6 +12,11 @@ # argument names an existing file its lines are sent as commands; otherwise it # is taken as a literal command string, one command per line. With no third # argument, only the story's opening text is checked for the target. +# A walkthrough line starting with "@" is sent without a trailing carriage +# return (for stories that act on single keypresses, where CR would arrive as +# a keypress of its own). +# Whenever the interpreter's <...> paging prompt appears, a space is sent +# automatically so walkthroughs do not have to step over full screens. # Everything after "--" is passed through to izconsole. # # Exit codes: 0 target string seen @@ -53,6 +58,7 @@ spawn -noecho ~/bin/izconsole -speed full {*}$izflags $dsk expect { -timeout 10 -exact $target { puts "TARGET FOUND in intro"; exit 0 } + -exact "<...>" { send -- " "; exp_continue } "> " { } eof { puts "emulator exited before prompt"; exit 3 } timeout { puts "no prompt after $timeout s"; exit 3 } @@ -63,7 +69,11 @@ set n 0 foreach line $lines { if {[string trim $line] eq ""} { continue } incr n - send -- "$line\r" + if {[string index $line 0] eq "@"} { + send -- [string range $line 1 end] + } else { + send -- "$line\r" + } expect { -timeout 10 -exact $target { @@ -71,6 +81,7 @@ foreach line $lines { puts "TARGET FOUND (command $n: \"$line\")" break } + -exact "<...>" { send -- " "; exp_continue } "> " { } ;# this command completed, keep going eof { puts "*** emulator exited (command $n: \"$line\")"; exit 3 } timeout { puts "*** timed out after command $n: \"$line\""; exit 3 } @@ -84,6 +95,7 @@ if {!$found} { set found 1 puts "TARGET FOUND" } + -exact "<...>" { send -- " "; exp_continue } eof { puts "*** emulator exited"; exit 3 } timeout { puts "*** timed out"; exit 3 } } diff --git a/test/codepoints/Makefile b/test/codepoints/Makefile new file mode 100644 index 0000000..49dcae3 --- /dev/null +++ b/test/codepoints/Makefile @@ -0,0 +1,62 @@ +# Call `make DIFF=meld` to get a fancy diff +DIFF = diff + +DIR6502 = ../../src/6502 +AAMBOX = $(DIR6502)/aambox6502 +FRONTEND = $(DIR6502)/aambox_frontend.bin +AAMBUNDLE = ../../src/aambundle +DIALOGC ?= dialogc + +$(AAMBOX): + $(MAKE) -C $(DIR6502) aambox6502 + +$(FRONTEND): + $(MAKE) -C $(DIR6502) aambox_frontend.bin + +$(AAMBUNDLE): + $(MAKE) -C ../../src aambundle + +all: test + +test: test.js test.6502 + +test.js: codepoints.js.out + $(DIFF) codepoints.js.out codepoints.js.gold + +codepoints.js.out: codepoints.aastory + node ../../src/js/nodefrontend.js -s 1234 codepoints.aastory codepoints.js.out + +test.6502: codepoints.6502.out + $(DIFF) codepoints.6502.out codepoints.6502.gold + +codepoints.6502.out: codepoints.aastory $(AAMBOX) $(FRONTEND) + $(AAMBOX) -s 1234 $(FRONTEND) codepoints.aastory codepoints.6502.out + +codepoints.aastory: codepoints.dg + $(DIALOGC) -t aa -o $@ $< + +# Neither the node frontend nor the aambox frontend renders a status area, and +# neither one transliterates -- both emit UTF-8 straight through. So the two +# things this story actually exercises are invisible to `make test`, and have +# to be looked at on the real backends: +# +# make disks && x64sc -truedrive codepoints/Codepoint-Exercise.d64 +# make disks && ~/bin/izconsole codepoints/Codepoint-Exercise.dsk +# +# Page 1-4 the transliteration table, one codepoint per line +# Page 5 the Z-machine fallback set in running text +# Page 6 line breaking: a run that breaks ends lines in 'aa', one that +# holds ends every line in 'bb' +# (the runs exceed 80 columns, so `make test` exercises them at +# the automated backends' width too; real hardware adds nothing +# for this page but is still needed for pages 1-5 and 7) +# Page 7 negative control, every line should be the unknown-char glyph + +disks: codepoints.aastory $(AAMBUNDLE) + $(AAMBUNDLE) -t apple2 -o codepoints codepoints.aastory + $(AAMBUNDLE) -t c64 -o codepoints codepoints.aastory + +clean: + rm -f *.out + +.PHONY: all test test.js test.6502 disks clean diff --git a/test/codepoints/codepoints.6502.gold b/test/codepoints/codepoints.6502.gold new file mode 100644 index 0000000..7b31ffb --- /dev/null +++ b/test/codepoints/codepoints.6502.gold @@ -0,0 +1,364 @@ + + +Codepoint Exercise +SPACE/ENTER = next page +B = back +1-4 = set status bar height +W = probe the wrap buffer +Q = quit + + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> 1 + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> 2 + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> 3 + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> 4 + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> h + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> n + +U+00D6 [Ö] O diaeresis +U+00D8 [Ø] O stroke +U+00D9 [Ù] U grave +U+00DB [Û] U circumflex +U+00DC [Ü] U diaeresis +U+00DF [ß] sharp s +U+00E0 [à] a grave +U+00E1 [á] a acute +U+00E2 [â] a circumflex +U+00E4 [ä] a diaeresis +U+00E5 [å] a ring +U+00E6 [æ] ae ligature +U+00E7 [ç] c cedilla +U+00E8 [è] e grave +U+00E9 [é] e acute +U+00EA [ê] e circumflex> n + +U+00EB [ë] e diaeresis +U+00EE [î] i circumflex +U+00EF [ï] i diaeresis +U+00F1 [ñ] n tilde +U+00F4 [ô] o circumflex +U+00F6 [ö] o diaeresis +U+00F8 [ø] o stroke +U+00F9 [ù] u grave +U+00FB [û] u circumflex +U+00FC [ü] u diaeresis +U+00FD [ý] y acute +U+00FF [ÿ] y diaeresis +U+0141 [Ł] L stroke +U+0142 [ł] l stroke +U+0152 [Œ] OE ligature +U+0153 [œ] oe ligature> n + +U+0178 [Ÿ] Y diaeresis +U+1E9E [᚞] capital sharp s +U+2010 [‐] hyphen +U+2013 [–] en dash +U+2014 [—] em dash +U+2018 [‘] left single quote +U+2019 [’] right single quote +U+201C [“] left double quote +U+201D [”] right double quote +U+201E [„] low double quote +U+2022 [•] bullet +U+2026 […] ellipsis +U+202F [ ] narrow no-break space +U+2039 [‹] left single guillemet +U+203A [›] right single guillemet +U+20AC [€] euro sign> n + +Z-machine fallback set, in context: +Dashes: an en–dash, an em—dash. +Quotes: “double”, „low”, ‘single’. +Bullet: • first • second • third. +Ellipsis: wait… and then… +Progress bars: +[ ] +[=================== ] +[======================================= ] +[================================================================= ] +[==============================================================================] +> n + +Line breaking. A run that breaks has +lines ending in aa; one that holds +together has every line ending in bb. +U+00A0, should break: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa +bb aa bb aa bb +(nbsp), currently degrades to breaking space: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa +bb aa bb aa bb +U+202F, should hold: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb +aa bb aa bb aa bb> n + +Negative control. Every line below +should show the unknown-character +glyph (? on the Apple II, a solid +box on the C64): +U+00A9 [©] copyright sign +U+00BF [¿] inverted question mark +U+0100 [Ā] A macron +U+0393 [Γ] Greek capital gamma +U+2190 [←] leftwards arrow +U+2264 [≤] less-than or equal +U+2764 [❤] heavy black heart> n + +Fold straddling. A two-char expansion +split across a fold must lose nothing. +39-pad puts an expansion on col 40; +79-pad puts one on col 80. +No blank lines should appear. +--- +Normal, 80 chars: +12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Word wrap at 40 chars: +1234567890123456789012345678901234567890 +1234567890123456789012345678901234567890 +Em dash, 38-pad: +12345678901234567890123456789012345678—123456789012345678901234567890123456789 +Ellipsis, 38-pad: +123456789 123456789 123456789 12345678… 123456789 123456789 123456789 12345678 +Em dash, 39-pad: +123456789012345678901234567890123456789—123456789012345678901234567890123456789 +Ellipsis, 39-pad: +123456789 123456789 123456789 123456789… 123456789 123456789 123456789 12345678 +Em dash, 79-pad: + +1234567890123456789012345678901234567890123456789012345678901234567890123456789—X +Ellipsis, 79-pad: +123456789 123456789 123456789 123456789 123456789 123456789 123456789 +123456789…X +--- +Overlong words of expanding characters +must hard-fold without corruption: +ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß END +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ END +--- +Transliterated space on the fold. It is +laid down as an ordinary character, so +if it wraps the next line starts with +it (watch for an indented Z): +NBSP, 39-pad: +123456789012345678901234567890123456789 Z +NNBS, 39-pad: +123456789012345678901234567890123456789 Z +NBSP, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 +Z +NNBS, 79-pad: + +1234567890123456789012345678901234567890123456789012345678901234567890123456789 Z +--- +Flowing paragraphs whose wrap point +lands exactly after an em dash. No +blank line may appear below: +123456789 123456789 123456789 123456789 123456789 123456789 12345678 abcdefghij— +zzz zzz zzz +123456789 123456789 123456789 1 abcdef— qqq qqq qqq> n + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> w +Alias probe: alpha beta pending +gamma delta resume. +'pending' must appear exactly once, +and 'gamma delta resume.' must follow +it on the same line. + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> 3 + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> 4 + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> h + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> b + +Fold straddling. A two-char expansion +split across a fold must lose nothing. +39-pad puts an expansion on col 40; +79-pad puts one on col 80. +No blank lines should appear. +--- +Normal, 80 chars: +12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Word wrap at 40 chars: +1234567890123456789012345678901234567890 +1234567890123456789012345678901234567890 +Em dash, 38-pad: +12345678901234567890123456789012345678—123456789012345678901234567890123456789 +Ellipsis, 38-pad: +123456789 123456789 123456789 12345678… 123456789 123456789 123456789 12345678 +Em dash, 39-pad: +123456789012345678901234567890123456789—123456789012345678901234567890123456789 +Ellipsis, 39-pad: +123456789 123456789 123456789 123456789… 123456789 123456789 123456789 12345678 +Em dash, 79-pad: + +1234567890123456789012345678901234567890123456789012345678901234567890123456789—X +Ellipsis, 79-pad: +123456789 123456789 123456789 123456789 123456789 123456789 123456789 +123456789…X +--- +Overlong words of expanding characters +must hard-fold without corruption: +ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß END +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ END +--- +Transliterated space on the fold. It is +laid down as an ordinary character, so +if it wraps the next line starts with +it (watch for an indented Z): +NBSP, 39-pad: +123456789012345678901234567890123456789 Z +NNBS, 39-pad: +123456789012345678901234567890123456789 Z +NBSP, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 +Z +NNBS, 79-pad: + +1234567890123456789012345678901234567890123456789012345678901234567890123456789 Z +--- +Flowing paragraphs whose wrap point +lands exactly after an em dash. No +blank line may appear below: +123456789 123456789 123456789 123456789 123456789 123456789 12345678 abcdefghij— +zzz zzz zzz +123456789 123456789 123456789 1 abcdef— qqq qqq qqq> b + +Negative control. Every line below +should show the unknown-character +glyph (? on the Apple II, a solid +box on the C64): +U+00A9 [©] copyright sign +U+00BF [¿] inverted question mark +U+0100 [Ā] A macron +U+0393 [Γ] Greek capital gamma +U+2190 [←] leftwards arrow +U+2264 [≤] less-than or equal +U+2764 [❤] heavy black heart> q +Done. diff --git a/test/codepoints/codepoints.aastory b/test/codepoints/codepoints.aastory new file mode 100644 index 0000000000000000000000000000000000000000..a75f9d6539d5bc2bc5fdcd9b2acfbdbb1c006162 GIT binary patch literal 5682 zcmbtV30PCfvhJ|RqG$qcD1-n4jta5_0SCDWgne^EQMqH_unQys9LYF1VG9YX-~u8z z8Wd4h6(yn$QCW1r1yE6m0-^zBL{tS?PU>`5chz5iRac)ld9UHZ@1c!`s^(Skxm$ zGzA$WFT_IILxaLXLSvUw%z`3OOe{Ad$T-SBGHB_?SY`-)GBE$D0ib>|u=uKh#lH=x zUp1g26db|w2MWr*N;!bgYyKgG-j$^y)Kn)x zgq1=eqRdW9Lln*QbVSk0$UqeB%uGZfXJsLZO?Eb-EZx2xQS@?h5Je+57f}p%>_C+A zojVaK+qDa!GbOtbl9c8lbm3?|LXG8n5NfU{Kr&c?1W2gM55&mi>A(_%!R<1c$W^Hs1$h&y{0k@uZDhbY9~ zenXVS_4NoHkw~EDFP}#Q^!o)QzvJRXq`SSL0qF%cHX@@yoZ5LjS{sgh54R)Wb|(nP zWYPC}3+8`^kKdvB2zk<%f9yMktHbJv4s`W$#b;AM)^=*h&~P;VyPd-&3BB;622$_>gU`Ig#-UN)9BIP?6GZU(To}dt8 z3iU8%B1NbqucV)zEy7VU6D&uqP25r z$`me5g#hgMtsv1Le!y=U#+(U*3C7$i4{ZMc3B{PH0;r}YB=RfPD~BvILPp|}* z0h@1%F+E^+8OEf+z{QwUfPnBTVOTkV;09{on=kJ5D9W0aBz;z)^g!vd#k*&c*{|}i2IEg=E%uE#sBS-{| z-v`JWAQps4J_8GSSR@V60^zMW{x% z13@<>L$4(EfWX-#4ub%2!T`u?*n@*kf;3V9An7LPdGc_JpqD3F*$O(@02)~X`j|iv zu#kM3BHu*qnK1nsuE~K$`K%E@Q~#`?G};e*c~F(*Ni-6jWFM@MMspPrBuv#HiJDA= z&J!;nz6FyUbe^dYbb*-$fhG7YoP-6@SCe}CHI}cevVx7J(uev?7?qeS%Rv1=@Vtet%TvWkT3IMNf(=b$ShGyM{3dw+^18rTP{xCu%C=Eek(T|~b2ccYeM19<{Fe%k zB%MF2)GyC?5&Gjay#+n6v!63MU1VlUX|MsiovPX5D@3z{POqh6h~#GdHlc0dK4O5D z7?}UP`CWmL+>C=aPxTD{SfSONpxeMHU*C5~0w~4qI-GDyhE=|i2_707Qe)WkPSS1R zO`OiWvV;cPQ!gN1zr(=WqiqYWDVt5jbD?oY-iUn0iQ#*pV__>61kCD8#}n9pY?LJ0 ztX$pK!dqwf-rnL;$H_}GUNyVJUcZCc=t|M*k0`RLwox@vbR1m}4H5EevQMk2m#nwS zsb5>UEBLUzUD5nP|G7<7&1(U)>e%QM(K51Zp|V#GX8I?-ACdjrMusSkE<+0{x=Q*cvG8K&YhWY8A z@npr~u}BfiY~6FM@Gktsi|y^6%+L62pU3JOvSf5`UpO4-52sluJ8V6kc|mb%#Dbpo zxFxSc%eB=*&uXh1X3XW{03wmygAuysX9oQaEjpXs4A%X7!Ce|J3Elm!F{FW076}!R z9W5RrF|1!)e>pQHHGdW)X)m%X-03+d;KT!K=o&bi?&2XUhS%xO7UqX`IM7MzG2SX3r^cFT?{S} zT$|OK>otx;yqnkCVV-ITrOxxZ0!YcXteE1^3 zXUTb_2s~YGzUr~fUlip9Xn`lOF%yCIJK6WgO1Y-K(n7fCTW{+Rxidoe6n#sTbCH_8 z`|~{Rhig2!s~FEYhF=zbt{-(N-m)!^vJcnkK@q&CEYf{csSXz^u+H~`kYWGM+%@Zu zaI(itDZK#!}C})+!}Bvy`RCD zrkIPJnUlIhG4nFC#IA+a&eB`~*|IJ$aBQzAMlnfEvd(DxOLN=SS#`RvX5F$KC~OM- z!0?gOYW!ZLdSvWqz^0=`t6Yj=P>%ld7GC$Y(&jbySpB#5wic*8;Alt~D+7HbSXor< z%g1#;{cN>ocYc`R*{xgs()FQDKK!k3$dTba60^Il&o$8Wno@Y1RCTN`1ZSQ8M6>R6 zyTk{S=ugsXupxJg^x2I|J=MloWM(?? zk)(a^x;{sTCovTU!jpy8@m}jMG~8`DBvE;l9%8EYK*4GDaSm~;@TA$2!pooz}kDz78#Z1`=2v%YT1Xq`OUXUijEB`BULHeP6AGt?Rx@+UKKxDHkR6pwb;bB-DnD@vw>(4oqMA6Ic<78`h1$GYEbjGt%=os`nvB1j z;Qw@R-EPNwG3UiwZhd9-WSN+0{$!};qN^qqD_P9$v%TfCg_-m8I$vFHbihaA?LN3; zHT!8^XR7NeV)OEhJ%**dT}#X(#fBfOo#F(SPq7^Yq=b!rg@ZF*Jy?9CvhvU6sdE!) z(K4V>I}^IE_6(Dz@7YJUlfjq%^2%KL!?v0TYtEUJIQZE0x2({KR@ECLK2T{5o~#u% ztA1sh=x&cnm1fq5c!vnPl7YtfISSbL`OfB?k(N&VuxYTt<@z>anE4&q)u#_%(hsTM zi&yUnr?1mJn|b#(`-k49_5KGF?wu`)a$4J&>dtE$c{;bXs*)Z|e8Sq`G@Q?gcdi{M zlu*ieUh4h6RaGhtPy@Aj;?P2gm4KvK?;SI>M}3}4jvhn!Uj{=Tk9KKogEjA2r?9`y zEJErhlXJecaIk^9gMQXax3*Mj-ISx0rMvM5Z37RZePb+hz;qc4AAQo}8ERKBV~4;z zX7-KDANc%xLe!qwnT!d3N!#&zmLBx>KoJd7iCNUewiXzC%q1WWrRc@0OQsrm`J)^K9sJ z=ly42e&5|?k{8edn(}?1@-kJH+a=@rv2*8V||+!ch8!_T|~Q 1) (then) + (line) 2 \x{00C4}\x{00E9}\x{00EE}\x{00F6}\x{00FC} \x{00C6}\x{0152}\x{00DF} + (endif) + (if) ($H > 2) (then) + (line) 3 \x{2018}q\x{2019} \x{201C}q\x{201D} \x{201E}q\x{201D} \x{2039}q\x{203A} + (endif) + (if) ($H > 3) (then) + (line) 4 \x{2022} \x{2013} \x{2014} \x{2026} \x{20AC} \x{00AB}\x{00BB} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} \x{00E9}\x{00DF} + (endif) + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Charts + +(chart 1) + (line) U+00A0 \[\x{00A0}\] no-break space + (line) U+00AB \[\x{00AB}\] left guillemet + (line) U+00BB \[\x{00BB}\] right guillemet + (line) U+00C0 \[\x{00C0}\] A grave + (line) U+00C2 \[\x{00C2}\] A circumflex + (line) U+00C4 \[\x{00C4}\] A diaeresis + (line) U+00C5 \[\x{00C5}\] A ring + (line) U+00C6 \[\x{00C6}\] AE ligature + (line) U+00C7 \[\x{00C7}\] C cedilla + (line) U+00C8 \[\x{00C8}\] E grave + (line) U+00C9 \[\x{00C9}\] E acute + (line) U+00CA \[\x{00CA}\] E circumflex + (line) U+00CB \[\x{00CB}\] E diaeresis + (line) U+00CE \[\x{00CE}\] I circumflex + (line) U+00CF \[\x{00CF}\] I diaeresis + (line) U+00D4 \[\x{00D4}\] O circumflex + +(chart 2) + (line) U+00D6 \[\x{00D6}\] O diaeresis + (line) U+00D8 \[\x{00D8}\] O stroke + (line) U+00D9 \[\x{00D9}\] U grave + (line) U+00DB \[\x{00DB}\] U circumflex + (line) U+00DC \[\x{00DC}\] U diaeresis + (line) U+00DF \[\x{00DF}\] sharp s + (line) U+00E0 \[\x{00E0}\] a grave + (line) U+00E1 \[\x{00E1}\] a acute + (line) U+00E2 \[\x{00E2}\] a circumflex + (line) U+00E4 \[\x{00E4}\] a diaeresis + (line) U+00E5 \[\x{00E5}\] a ring + (line) U+00E6 \[\x{00E6}\] ae ligature + (line) U+00E7 \[\x{00E7}\] c cedilla + (line) U+00E8 \[\x{00E8}\] e grave + (line) U+00E9 \[\x{00E9}\] e acute + (line) U+00EA \[\x{00EA}\] e circumflex + +(chart 3) + (line) U+00EB \[\x{00EB}\] e diaeresis + (line) U+00EE \[\x{00EE}\] i circumflex + (line) U+00EF \[\x{00EF}\] i diaeresis + (line) U+00F1 \[\x{00F1}\] n tilde + (line) U+00F4 \[\x{00F4}\] o circumflex + (line) U+00F6 \[\x{00F6}\] o diaeresis + (line) U+00F8 \[\x{00F8}\] o stroke + (line) U+00F9 \[\x{00F9}\] u grave + (line) U+00FB \[\x{00FB}\] u circumflex + (line) U+00FC \[\x{00FC}\] u diaeresis + (line) U+00FD \[\x{00FD}\] y acute + (line) U+00FF \[\x{00FF}\] y diaeresis + (line) U+0141 \[\x{0141}\] L stroke + (line) U+0142 \[\x{0142}\] l stroke + (line) U+0152 \[\x{0152}\] OE ligature + (line) U+0153 \[\x{0153}\] oe ligature + +(chart 4) + (line) U+0178 \[\x{0178}\] Y diaeresis + (line) U+1E9E \[\x{1E9E}\] capital sharp s + (line) U+2010 \[\x{2010}\] hyphen + (line) U+2013 \[\x{2013}\] en dash + (line) U+2014 \[\x{2014}\] em dash + (line) U+2018 \[\x{2018}\] left single quote + (line) U+2019 \[\x{2019}\] right single quote + (line) U+201C \[\x{201C}\] left double quote + (line) U+201D \[\x{201D}\] right double quote + (line) U+201E \[\x{201E}\] low double quote + (line) U+2022 \[\x{2022}\] bullet + (line) U+2026 \[\x{2026}\] ellipsis + (line) U+202F \[\x{202F}\] narrow no-break space + (line) U+2039 \[\x{2039}\] left single guillemet + (line) U+203A \[\x{203A}\] right single guillemet + (line) U+20AC \[\x{20AC}\] euro sign +%% Page 5 puts the set that Dialog's Z-machine runtime has fallbacks for into +%% running text, where the transliteration has to survive word wrapping too. +%% The em dash and the ellipsis expand to two characters, so they are the ones +%% most likely to overrun a line. + +(chart 5) + (line) Z-machine fallback set, in context: + (line) + (line) Dashes: an en\x{2013}dash, an em\x{2014}dash. + (line) Quotes: \x{201C}double\x{201D}, \x{201E}low\x{201D}, \x{2018}single\x{2019}. + (line) Bullet: \x{2022} first \x{2022} second \x{2022} third. + (line) Ellipsis: wait\x{2026} and then\x{2026} + (line) + (line) Progress bars: + (line) (progress bar 0 of 100) + (line) (progress bar 25 of 100) + (line) (progress bar 50 of 100) + (line) (progress bar 16 of 19) + (line) (progress bar 75 of 19) + +%% Line breaking. Each run below is long enough to fold at the automated +%% backends' 80-column width as well as on real 40-column hardware, so where +%% the folds land is the whole test: if the character +%% joining a pair is a break opportunity, some lines end in 'aa', and if it is +%% not, every line ends in 'bb'. +%% +%% Three different things are being measured, and they are not all supposed to +%% agree: +%% +%% U+00A0 Dialog's tokenizer treats a literal no-break space as ordinary +%% whitespace and emits a plain space, so this run SHOULD break. +%% (nbsp) is Dialog's way to ask for a real non-breaking space. +%% +%% (nbsp) Should not break. The JS engine has a distinct SP_NBSP spacing +%% state and an io.nbsp() that appends without flushing, so it +%% holds there. The 6502 engine has no such state and degrades to +%% an ordinary space, which the spec allows, so this run does break +%% on the C64 and the Apple II. Worth watching all the same: this +%% run is what caught ext0_nbsp setting SPC_NO instead of +%% SPC_PENDING, which dropped the space altogether and ran the two +%% words together as 'aabb'. +%% +%% U+202F Should not break on any backend. It is a printable character, +%% not a spacing directive, so it belongs to the word it sits in. +%% The Apple II transliterates it to a space for display, which is +%% why io_mputc lays transliterated spaces down as ordinary +%% characters instead of routing them through its space branch. + +(chart 6) + (line) Line breaking. A run that breaks has + (line) lines ending in aa; one that holds + (line) together has every line ending in bb. + (line) + (line) U+00A0, should break: + (line) aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb aa\x{00A0}bb + (line) + (line) \(nbsp\), currently degrades to breaking space: + (line) aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb aa(nbsp)bb + (line) + (line) U+202F, should hold: + (line) aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb aa\x{202F}bb + +%% Page 7 is the negative control. None of these are in the Apple II table or +%% the C64 font, so every one of them should render as the interpreter's +%% unknown-character glyph. If one of them ever starts rendering correctly, +%% either a table grew or something is reading past the end of one. + +(chart 7) + (line) Negative control. Every line below + (line) should show the unknown-character + (line) glyph \(? on the Apple II, a solid + (line) box on the C64\): + (line) + (line) U+00A9 \[\x{00A9}\] copyright sign + (line) U+00BF \[\x{00BF}\] inverted question mark + (line) U+0100 \[\x{0100}\] A macron + (line) U+0393 \[\x{0393}\] Greek capital gamma + (line) U+2190 \[\x{2190}\] leftwards arrow + (line) U+2264 \[\x{2264}\] less-than or equal + (line) U+2764 \[\x{2764}\] heavy black heart + +%% Page 8 targets the fold logic. Every replacement in the Apple II table is +%% at most two characters wide, and io_mputc feeds each half through its own +%% margin check -- so an expansion can be split across a hard fold. These +%% lines are padded so that an expansion begins exactly on the last column: +%% the 39-padded ones for 40-column screens, the 79-padded ones for +%% 80-column ones. On the other width they are ordinary mid-line expansions +%% and should look unremarkable. +(chart 8) + (line) Fold straddling. A two-char expansion + (line) split across a fold must lose nothing. + (line) 39-pad puts an expansion on col 40; + (line) 79-pad puts one on col 80. + (line) No blank lines should appear. + (line) --- + (line) Normal, 80 chars: + (line) 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + (line) Word wrap at 40 chars: + (line) 1234567890123456789012345678901234567890 1234567890123456789012345678901234567890 + (line) Em dash, 38-pad: + (line) 12345678901234567890123456789012345678\x{2014}123456789012345678901234567890123456789 + (line) Ellipsis, 38-pad: + (line) 123456789 123456789 123456789 12345678\x{2026} 123456789 123456789 123456789 12345678 + (line) Em dash, 39-pad: + (line) 123456789012345678901234567890123456789\x{2014}123456789012345678901234567890123456789 + (line) Ellipsis, 39-pad: + (line) 123456789 123456789 123456789 123456789\x{2026} 123456789 123456789 123456789 12345678 + (line) Em dash, 79-pad: + (line) 1234567890123456789012345678901234567890123456789012345678901234567890123456789\x{2014}X + (line) Ellipsis, 79-pad: + (line) 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789\x{2026}X + (line) --- + (line) Overlong words of expanding characters + (line) must hard-fold without corruption: + (line) \x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF}\x{00DF} END + (line) \x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6}\x{00C6} END + (line) --- + (line) Transliterated space on the fold. It is + (line) laid down as an ordinary character, so + (line) if it wraps the next line starts with + (line) it \(watch for an indented Z\): + (line) NBSP, 39-pad: + (line) 123456789012345678901234567890123456789\x{00A0}Z + (line) NNBS, 39-pad: + (line) 123456789012345678901234567890123456789\x{202F}Z + (line) NBSP, 79-pad: + (line) 1234567890123456789012345678901234567890123456789012345678901234567890123456789\x{00A0}Z + (line) NNBS, 79-pad: + (line) 1234567890123456789012345678901234567890123456789012345678901234567890123456789\x{202F}Z + (line) --- + (line) Flowing paragraphs whose wrap point + (line) lands exactly after an em dash. No + (line) blank line may appear below: + (line) + 123456789 123456789 123456789 123456789 123456789 123456789 12345678 abcdefghij\x{2014} zzz zzz zzz + (line) + 123456789 123456789 123456789 1 abcdef\x{2014} qqq qqq qqq + +%% Page 9 is a second negative control, for consecutive unknown codepoints. +%% Each one must become its own unknown-character glyph, including when they +%% sit inside a word; if any of them starts rendering correctly, something is +%% reading past the end of the transliteration table. +(chart 9) + (line) Negative control 2. Consecutive + (line) unknowns -- each must become its + (line) own unknown-character glyph: + (line) + (line) \x{0393}\x{2190}\x{2764} + (line) \x{00A9}\x{00BF}\x{0100}\x{2264} + (line) + (line) Inside words: + (line) aa\x{0393}\x{2190}bb cc\x{2764}\x{0100}dd + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Main loop + +(program entry point) + (clear all) + (line) Codepoint Exercise + (line) + (line) SPACE/ENTER = next page + (line) B = back + (line) 1-4 = set status bar height + (line) W = probe the wrap buffer + (line) Q = quit + (line) + (show page) + (main loop) + +(main loop) + > (nbsp) + (get key $Key) + (if) (quit key $Key) (then) + (clear status bar) + (line) Done. + (else) + (handle $Key) + (show page) + (main loop) + (endif) + +(quit key @q) + +(show page) + (clear all) + (draw bar) + (current page $P) + (chart $P) + +%% Key handling. Anything unrecognized just redraws, which is itself a useful +%% thing to be able to do by hand on real hardware. + +(handle @\s) (next page) +(handle @\r) (next page) +(handle @\n) (next page) +(handle @n) (next page) +(handle @b) (prev page) +(handle @p) (prev page) +(handle @w) (aliasing probe) +(handle 1) (now) (bar height 1) +(handle 2) (now) (bar height 2) +(handle 3) (now) (bar height 3) +(handle 4) (now) (bar height 4) +(handle $) + +(next page) + (current page $P) (last page $L) + (if) ($P < $L) (then) + ($P plus 1 into $Next) + (now) (current page $Next) + (else) + (now) (current page 1) + (endif) + +(prev page) + (current page $P) (last page $L) + (if) ($P > 1) (then) + ($P minus 1 into $Prev) + (now) (current page $Prev) + (else) + (now) (current page $L) + (endif) + +%% Wrap-buffer aliasing probe. Leaves a word pending in the middle of a text +%% line, then forces a status bar redraw before the word is flushed. Row 0 +%% of the bar shares its buffer (wrapbuf) with the pending word, so this is +%% the sequence most likely to expose stale or doubled buffer contents. +(aliasing probe) + (bar height $H) + (if) ($H = 1) (then) + (now) (bar height 2) + (else) + (now) (bar height 1) + (endif) + (line) Alias probe: alpha beta pending + (draw bar) + gamma delta resume. + (line) + (line) 'pending' must appear exactly once, + (line) and 'gamma delta resume.' must follow + (line) it on the same line. diff --git a/test/codepoints/codepoints.in b/test/codepoints/codepoints.in new file mode 100644 index 0000000..57acd20 --- /dev/null +++ b/test/codepoints/codepoints.in @@ -0,0 +1 @@ +1234hnnnnnnnnw34hbbq \ No newline at end of file diff --git a/test/codepoints/codepoints.js.gold b/test/codepoints/codepoints.js.gold new file mode 100644 index 0000000..7b84057 --- /dev/null +++ b/test/codepoints/codepoints.js.gold @@ -0,0 +1,358 @@ + +Codepoint Exercise +SPACE/ENTER = next page +B = back +1-4 = set status bar height +W = probe the wrap buffer +Q = quit + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> 1234hnnnnnnnnw34hbbq + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> + +U+00A0 [] no-break space +U+00AB [«] left guillemet +U+00BB [»] right guillemet +U+00C0 [À] A grave +U+00C2 [Â] A circumflex +U+00C4 [Ä] A diaeresis +U+00C5 [Å] A ring +U+00C6 [Æ] AE ligature +U+00C7 [Ç] C cedilla +U+00C8 [È] E grave +U+00C9 [É] E acute +U+00CA [Ê] E circumflex +U+00CB [Ë] E diaeresis +U+00CE [Î] I circumflex +U+00CF [Ï] I diaeresis +U+00D4 [Ô] O circumflex> + +U+00D6 [Ö] O diaeresis +U+00D8 [Ø] O stroke +U+00D9 [Ù] U grave +U+00DB [Û] U circumflex +U+00DC [Ü] U diaeresis +U+00DF [ß] sharp s +U+00E0 [à] a grave +U+00E1 [á] a acute +U+00E2 [â] a circumflex +U+00E4 [ä] a diaeresis +U+00E5 [å] a ring +U+00E6 [æ] ae ligature +U+00E7 [ç] c cedilla +U+00E8 [è] e grave +U+00E9 [é] e acute +U+00EA [ê] e circumflex> + +U+00EB [ë] e diaeresis +U+00EE [î] i circumflex +U+00EF [ï] i diaeresis +U+00F1 [ñ] n tilde +U+00F4 [ô] o circumflex +U+00F6 [ö] o diaeresis +U+00F8 [ø] o stroke +U+00F9 [ù] u grave +U+00FB [û] u circumflex +U+00FC [ü] u diaeresis +U+00FD [ý] y acute +U+00FF [ÿ] y diaeresis +U+0141 [Ł] L stroke +U+0142 [ł] l stroke +U+0152 [Œ] OE ligature +U+0153 [œ] oe ligature> + +U+0178 [Ÿ] Y diaeresis +U+1E9E [ẞ] capital sharp s +U+2010 [‐] hyphen +U+2013 [–] en dash +U+2014 [—] em dash +U+2018 [‘] left single quote +U+2019 [’] right single quote +U+201C [“] left double quote +U+201D [”] right double quote +U+201E [„] low double quote +U+2022 [•] bullet +U+2026 […] ellipsis +U+202F [ ] narrow no-break space +U+2039 [‹] left single guillemet +U+203A [›] right single guillemet +U+20AC [€] euro sign> + +Z-machine fallback set, in context: +Dashes: an en–dash, an em—dash. +Quotes: “double”, „low”, ‘single’. +Bullet: • first • second • third. +Ellipsis: wait… and then… +Progress bars: +[ ] +[=================== ] +[======================================= ] +[================================================================= ] +[================================================================================================================================================================================================================================================================================================================] +> + +Line breaking. A run that breaks has +lines ending in aa; one that holds +together has every line ending in bb. +U+00A0, should break: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa +bb aa bb aa bb +(nbsp), currently degrades to breaking space: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb +aa bb aa bb aa bb +U+202F, should hold: +aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb aa bb +aa bb aa bb aa bb> + +Negative control. Every line below +should show the unknown-character +glyph (? on the Apple II, a solid +box on the C64): +U+00A9 [©] copyright sign +U+00BF [¿] inverted question mark +U+0100 [Ā] A macron +U+0393 [Γ] Greek capital gamma +U+2190 [←] leftwards arrow +U+2264 [≤] less-than or equal +U+2764 [❤] heavy black heart> + +Fold straddling. A two-char expansion +split across a fold must lose nothing. +39-pad puts an expansion on col 40; +79-pad puts one on col 80. +No blank lines should appear. +--- +Normal, 80 chars: +12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Word wrap at 40 chars: +1234567890123456789012345678901234567890 +1234567890123456789012345678901234567890 +Em dash, 38-pad: +12345678901234567890123456789012345678—123456789012345678901234567890123456789 +Ellipsis, 38-pad: +123456789 123456789 123456789 12345678… 123456789 123456789 123456789 12345678 +Em dash, 39-pad: +123456789012345678901234567890123456789—123456789012345678901234567890123456789 +Ellipsis, 39-pad: +123456789 123456789 123456789 123456789… 123456789 123456789 123456789 12345678 +Em dash, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789—X +Ellipsis, 79-pad: +123456789 123456789 123456789 123456789 123456789 123456789 123456789 +123456789…X +--- +Overlong words of expanding characters +must hard-fold without corruption: +ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß END +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ END +--- +Transliterated space on the fold. It is +laid down as an ordinary character, so +if it wraps the next line starts with +it (watch for an indented Z): +NBSP, 39-pad: +123456789012345678901234567890123456789 Z +NNBS, 39-pad: +123456789012345678901234567890123456789 Z +NBSP, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 +Z +NNBS, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 Z +--- +Flowing paragraphs whose wrap point +lands exactly after an em dash. No +blank line may appear below: +123456789 123456789 123456789 123456789 123456789 123456789 12345678 abcdefghij— +zzz zzz zzz +123456789 123456789 123456789 1 abcdef— qqq qqq qqq> + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> +Alias probe: alpha beta pending +gamma delta resume. +'pending' must appear exactly once, +and 'gamma delta resume.' must follow +it on the same line. + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> + +Negative control 2. Consecutive +unknowns--each must become its +own unknown-character glyph: +Γ←❤ +©¿Ā≤ +Inside words: +aaΓ←bb cc❤Ādd> + +Fold straddling. A two-char expansion +split across a fold must lose nothing. +39-pad puts an expansion on col 40; +79-pad puts one on col 80. +No blank lines should appear. +--- +Normal, 80 chars: +12345678901234567890123456789012345678901234567890123456789012345678901234567890 +Word wrap at 40 chars: +1234567890123456789012345678901234567890 +1234567890123456789012345678901234567890 +Em dash, 38-pad: +12345678901234567890123456789012345678—123456789012345678901234567890123456789 +Ellipsis, 38-pad: +123456789 123456789 123456789 12345678… 123456789 123456789 123456789 12345678 +Em dash, 39-pad: +123456789012345678901234567890123456789—123456789012345678901234567890123456789 +Ellipsis, 39-pad: +123456789 123456789 123456789 123456789… 123456789 123456789 123456789 12345678 +Em dash, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789—X +Ellipsis, 79-pad: +123456789 123456789 123456789 123456789 123456789 123456789 123456789 +123456789…X +--- +Overlong words of expanding characters +must hard-fold without corruption: +ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß END +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ END +--- +Transliterated space on the fold. It is +laid down as an ordinary character, so +if it wraps the next line starts with +it (watch for an indented Z): +NBSP, 39-pad: +123456789012345678901234567890123456789 Z +NNBS, 39-pad: +123456789012345678901234567890123456789 Z +NBSP, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 +Z +NNBS, 79-pad: +1234567890123456789012345678901234567890123456789012345678901234567890123456789 Z +--- +Flowing paragraphs whose wrap point +lands exactly after an em dash. No +blank line may appear below: +123456789 123456789 123456789 123456789 123456789 123456789 12345678 abcdefghij— +zzz zzz zzz +123456789 123456789 123456789 1 abcdef— qqq qqq qqq> + +Negative control. Every line below +should show the unknown-character +glyph (? on the Apple II, a solid +box on the C64): +U+00A9 [©] copyright sign +U+00BF [¿] inverted question mark +U+0100 [Ā] A macron +U+0393 [Γ] Greek capital gamma +U+2190 [←] leftwards arrow +U+2264 [≤] less-than or equal +U+2764 [❤] heavy black heart> +Done. From 913e245447af2d0f1d9d5c38171a29c163fc22e7 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 01:36:42 +0200 Subject: [PATCH 10/21] apple2: transliterate characters in status bar (c64 probably has similar issue) --- src/6502/a2_frontend.s | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 4173ee9..c3dc225 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -968,6 +968,16 @@ io_sputc ; input a = char .( + ; extended characters? (check just like io_mputc) + cmp #$80 + bcc one + + jsr translit + lda tr0 + jsr one + lda tr1 + beq skip +one jsr do_foldup ldy strow bpl screen From 5df5c20325f52e6dff66354e54adc84edc096879 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 01:47:41 +0200 Subject: [PATCH 11/21] apple2: cleaned up some commented code --- src/6502/a2_frontend.s | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index c3dc225..7a07c28 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -94,8 +94,6 @@ ; https://github.com/peterferrie/0boot ; ; TODO -; - io_load flaky (hangs, inserting nop makes it say "Failed to *save* game state") -; and the savefiles differ at $300 ; - SAVEFILE on first disk when running AAM.SYSTEM from Prodos? ; - run STORY file from subdirectory? ; - victim cache (move stale pages to aux)? @@ -783,25 +781,10 @@ nomore moretxt .asc "<...>",0 - ; TODO some bugs in 80-column mode +; HAVE_STYLE=0 for this port io_mstyle rts - ; input a = style bits - /* - .( - pha - jsr io_mflush - pla - and #6 ; bold and italic - beq normal - - jmp set_inverse -normal - jmp set_normal - .) - */ -; TODO use MouseText? io_mprogress ; input x = progress ; input y = total From 4bddf3ecfbe4f1a89b3423301f81423c125424c8 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 10:43:20 +0200 Subject: [PATCH 12/21] apple2: prevent translit and status overflow from wrapping --- src/6502/a2_frontend.s | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 7a07c28..7ee3207 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -639,6 +639,7 @@ extended lda tr1 beq done + inc xpos ; we printed 1 more char jmp plain done rts @@ -864,8 +865,7 @@ rowloop lda #$8d ; next line jsr cout jmp nextrow -; We have to do it the hard way because the -; 40-column path clears with non-inverted spaces +; slower 40-column path using spaces dospaces ldx scrw sploop @@ -906,8 +906,7 @@ keep sta cury tay ldx savedch - jsr gotoxy - rts + jmp gotoxy .) io_slocate @@ -962,22 +961,24 @@ io_sputc beq skip one jsr do_foldup - ldy strow - bpl screen + ; prevent running off the right edge ldy stxoffs cpy scrw bcs skip - sta wrapbuf,y + ; first row is written to wrapbuf + ; other rows are direct to screen inc stxoffs + bit strow + bpl screen + + sta wrapbuf,y skip rts screen ora #$80 - jsr cout - inc stxoffs - rts + jmp cout .) io_sprogress @@ -1030,8 +1031,7 @@ loop ldx xpos ldy cury jsr gotoxy - jsr set_normal ; do we have to restore old value? - rts + jmp set_normal .) sbar_home From bc19304f83f3de6d4ec697d15a9750a84fc079bf Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 10:55:19 +0200 Subject: [PATCH 13/21] apple2: refactor shared code --- src/6502/a2_frontend.s | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 7ee3207..694c083 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -731,12 +731,7 @@ zterm pla tax erase - lda #$88 ; backspace - jsr cout - lda #$a0 - jsr cout - lda #$88 - jsr cout + jsr erase_prev dex bne erase @@ -745,6 +740,17 @@ erase rts .) +; Do a backspace, space, backspace to +; clear the previous char (or cursor) +erase_prev + lda #$88 + jsr cout +erase_curr + lda #$a0 + jsr cout + lda #$88 + jmp cout + io_mclear .( ; on the c64, we stop for a [MORE] prompt @@ -786,6 +792,7 @@ moretxt io_mstyle rts +; TODO consider refactoring with sprogress? io_mprogress ; input x = progress ; input y = total @@ -1099,12 +1106,7 @@ backspace beq loop dey - lda #$88 - jsr cout - lda #$a0 - jsr cout - lda #$88 - jsr cout + jsr erase_prev dec xpos jmp loop done @@ -1127,10 +1129,7 @@ getkey .( jsr set_inverse - lda #$a0 - jsr cout - lda #$88 - jsr cout + jsr erase_curr jsr set_normal wait inc seed+0 @@ -1145,10 +1144,7 @@ nohi and #$7f pha - lda #$a0 - jsr cout - lda #$88 - jsr cout + jsr erase_curr pla rts From f0da1b81a16172518fb3c87dab3738be637f114d Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 11:36:04 +0200 Subject: [PATCH 14/21] apple2: prevent dual-char translits from being split --- src/6502/a2_frontend.s | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 694c083..612b0ea 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -625,6 +625,7 @@ wrap jmp postwrap extended jsr translit + bne dualchar ; Z flag set in translit ; The only codepoints that transliterate ; to space are U+00A0 and U+202F, both @@ -633,14 +634,19 @@ extended ; tr0 and tr1 are always < $80, so the ; extended test is not needed either. + ; single char case + lda tr0 + jmp plain +dualchar + ; wrap the two-char translit early + dec scrw + dec scrw lda tr0 jsr plain - lda tr1 - beq done - - inc xpos ; we printed 1 more char - jmp plain + jsr plain + inc scrw + inc scrw done rts .) @@ -1156,8 +1162,8 @@ nohi translit ; input a = char code, >= $80 - ; output tr0, tr1 - ; tr1 is zero for a single character + ; output tr0, tr1, Z + ; tr1 is zero (and Z clear) for a single character .( jsr lookupchar From 3858e030c2341d35a51c3c03b728bc1bfd8c42b9 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 12:33:51 +0200 Subject: [PATCH 15/21] apple2: a2test.exp now uses a frame-based approach that strips ansi codes --- test/apple2/Makefile | 1 + test/apple2/a2test.exp | 181 ++++++++++++++++++++++++++++++++++------- 2 files changed, 152 insertions(+), 30 deletions(-) diff --git a/test/apple2/Makefile b/test/apple2/Makefile index 584251d..b86dfc0 100644 --- a/test/apple2/Makefile +++ b/test/apple2/Makefile @@ -16,6 +16,7 @@ IZCONSOLE ?= $(HOME)/bin/izconsole DIR6502 = ../../src/6502 +# Add --verbose to trace commands and dump the emulated screen on failure. A2TEST = ./a2test.exp AAMBUNDLE = ../../src/aambundle diff --git a/test/apple2/a2test.exp b/test/apple2/a2test.exp index 514bbea..e91546a 100755 --- a/test/apple2/a2test.exp +++ b/test/apple2/a2test.exp @@ -1,7 +1,7 @@ #!/usr/bin/expect -f # a2test.exp -- feed commands into izconsole and wait for a target string. # -# Usage: ./a2test.exp "TARGET STRING" dsk [walkthrough-file | "command string"] [-- izconsole flags...] +# Usage: ./a2test.exp [--verbose] "TARGET STRING" dsk [walkthrough-file | "command string"] [-- izconsole flags...] # ./a2test.exp "Intro Text" story.dsk # ./a2test.exp "DARKNESS" story.dsk walk.txt # ./a2test.exp "DARKNESS" story.dsk "open door @@ -15,20 +15,46 @@ # A walkthrough line starting with "@" is sent without a trailing carriage # return (for stories that act on single keypresses, where CR would arrive as # a keypress of its own). -# Whenever the interpreter's <...> paging prompt appears, a space is sent +# Whenever the interpreter's <...> paging prompt appears, a key is sent # automatically so walkthroughs do not have to step over full screens. -# Everything after "--" is passed through to izconsole. +# +# Args: --verbose traces progress and dumps the last screen on failure. (The +# spelling matters: expect grabs -d and -v for itself before the script +# ever sees them.) +# Everything after "--" is passed through to izconsole. +# +# izconsole draws the emulated screen as a full repaint: 27 lines of text, +# izconsole's own "Line:" prompt, then ESC[27A to move the cursor back to the +# top for the next frame. Inverse video is emitted as an ESC[7m/ESC[0m pair +# around every single character, so the text on screen is thoroughly diced up +# by escape sequences. We therefore chop the output into frames on the +# cursor-up sequence, strip the remaining ANSI codes, and match the target +# against the resulting plain text. +# +# A repaint is only terminated when the next one starts, so the screen we are +# looking at is normally the unterminated tail of the output; it is inspected +# as-is. A plain "> " is no use for synchronizing (every repaint still shows +# the previous prompts), but the interpreter draws its cursor as an +# inverse-video cell right after the prompt, and that only happens while it +# waits for input -- so a "> " followed by ESC[7m means the turn is over. If +# that never shows up we fall back on the emulator going quiet. # # Exit codes: 0 target string seen # 1 commands exhausted, target never seen # 2 usage error # 3 emulator died or hung before finishing +set debug 0 +while {[llength $argv] > 0 && [lindex $argv 0] eq "--verbose"} { + set debug 1 + set argv [lrange $argv 1 end] +} + set target [lindex $argv 0] set dsk [lindex $argv 1] set arg3 [lindex $argv 2] if {$target eq "" || $dsk eq ""} { - puts stderr "usage: a2test.exp \"TARGET STRING\" dsk \[walkthrough-file | command string\] \[-- izconsole flags...\]" + puts stderr "usage: a2test.exp \[--verbose\] \"TARGET STRING\" dsk \[walkthrough-file | command string\] \[-- izconsole flags...\]" exit 2 } @@ -51,17 +77,108 @@ if {$dash >= 0} { set izflags {} } +# ESC[A: izconsole starts a new repaint by moving the cursor back up. +set FRAMERE {\x1b\[[0-9]+A} +# Everything else the frontend emits: ESC[7m, ESC[0m, ESC[K, ... +set ANSIRE {\x1b\[[0-9;?]*[A-Za-z]} +# The paging prompt. The interpreter prints "<...>" in inverse video, which +# izconsole's inverse character map renders as "[...]", so accept either. +# (Matched against the scrubbed screen.) +set PAGERRE {[<\[]\.\.\.[>\]]} +# The input cursor: an inverse-video cell sitting right after the prompt. +# (Matched against the raw screen -- the escapes are the whole point.) +set PROMPTRE {> \x1b\[7m} + +set raw "" ;# bytes read but not yet split into frames +set text "" ;# scrubbed text of all complete frames so far +set lastframe "" ;# most recent screen, scrubbed, for --verbose dumps + +proc dbg {msg} { + global debug + if {$debug} { puts stderr "# $msg" } else { puts -nonewline stderr "."; flush stderr } +} + +proc message {msg} { + puts -nonewline stderr "\r\033\[K" + flush stderr + puts $msg +} + +proc dumpscreen {} { + global debug lastframe + if {$debug && $lastframe ne ""} { + puts stderr "# --- last screen ---" + puts stderr $lastframe + } +} + +# Remove ANSI escape sequences and carriage returns from a chunk of output. +proc scrub {s} { + global ANSIRE + regsub -all $ANSIRE $s "" s + regsub -all "\r" $s "" s + return $s +} + +# Move every complete frame out of the raw buffer into the accumulated text +# and return what is left: the repaint currently on screen, ANSI codes and +# all. +proc harvest {} { + global raw text FRAMERE + while {[regexp -indices $FRAMERE $raw m]} { + lassign $m from to + append text [scrub [string range $raw 0 [expr {$from - 1}]]] "\n" + set raw [string range $raw [expr {$to + 1}] end] + } + # keep the accumulated text from growing without bound + if {[string length $text] > 200000} { + set text [string range $text end-100000 end] + } + return $raw +} + +# Watch the screen until $target appears or the interpreter is waiting for +# input again. Paging prompts are answered as they show up. While the +# screen still reads exactly like $stale (i.e. nothing has happened since the +# command was sent) the input cursor is ignored, so the prompt we are about +# to overtype is not mistaken for the next one. Gives up after $limit +# seconds, or after $quiet seconds without a repaint. Returns target, +# prompt, idle, eof or timeout. +proc await {target quiet limit {stale ""}} { + global raw text lastframe PAGERRE PROMPTRE + set deadline [expr {[clock seconds] + $limit}] + while 1 { + set screen [harvest] + set flat [scrub $screen] + if {$flat ne ""} { set lastframe $flat } + if {[string first $target $text] >= 0 || [string first $target $flat] >= 0} { + return target + } + if {[regexp $PAGERRE $flat]} { + dbg "answering paging prompt" + set raw "" ;# so the same prompt is not answered twice + send -- "\r" + } elseif {$flat ne $stale && [regexp $PROMPTRE $screen]} { + return prompt + } + if {[clock seconds] >= $deadline} { return timeout } + expect { + -timeout $quiet + -re {(.|\n)+} { append raw $expect_out(buffer) } + eof { return eof } + timeout { return idle } + } + } +} + log_user 0 spawn -noecho ~/bin/izconsole -speed full {*}$izflags $dsk -# wait for the game prompt (or the target, which may appear in the intro) -expect { - -timeout 10 - -exact $target { puts "TARGET FOUND in intro"; exit 0 } - -exact "<...>" { send -- " "; exp_continue } - "> " { } - eof { puts "emulator exited before prompt"; exit 3 } - timeout { puts "no prompt after $timeout s"; exit 3 } +# wait for the story to boot and settle (the target may be in the intro) +switch [await $target 5 60] { + target { message "TARGET FOUND in intro"; exit 0 } + eof { message "emulator exited before prompt"; dumpscreen; exit 3 } + timeout { message "no prompt after 60 s"; dumpscreen; exit 3 } } set found 0 @@ -69,38 +186,42 @@ set n 0 foreach line $lines { if {[string trim $line] eq ""} { continue } incr n + dbg "command $n: \"$line\"" + set stale $lastframe if {[string index $line 0] eq "@"} { send -- [string range $line 1 end] } else { send -- "$line\r" } - expect { - -timeout 10 - -exact $target { + set raw "" ;# drop the screen this command is answering + # prompt: the turn finished; idle: it finished without redrawing the prompt + switch [await $target 2 30 $stale] { + target { set found 1 - puts "TARGET FOUND (command $n: \"$line\")" + message "TARGET FOUND (command $n: \"$line\")" break } - -exact "<...>" { send -- " "; exp_continue } - "> " { } ;# this command completed, keep going - eof { puts "*** emulator exited (command $n: \"$line\")"; exit 3 } - timeout { puts "*** timed out after command $n: \"$line\""; exit 3 } + prompt { } + idle { } + eof { message "*** emulator exited (command $n: \"$line\")"; dumpscreen; exit 3 } + timeout { message "*** timed out after command $n: \"$line\""; dumpscreen; exit 3 } } } if {!$found} { - expect { - -timeout 60 - -exact $target { - set found 1 - puts "TARGET FOUND" - } - -exact "<...>" { send -- " "; exp_continue } - eof { puts "*** emulator exited"; exit 3 } - timeout { puts "*** timed out"; exit 3 } + # the story may still be printing; give it a last chance to catch up + switch [await $target 5 60] { + target { set found 1; message "TARGET FOUND" } + prompt { } + idle { } + eof { message "*** emulator exited"; dumpscreen; exit 3 } + timeout { message "*** timed out"; dumpscreen; exit 3 } } } -if {!$found} { puts "*** TARGET NOT FOUND after $n commands" } +if {!$found} { + message "*** TARGET NOT FOUND after $n commands" + dumpscreen +} close exit [expr {$found ? 0 : 1}] From 56d2bce18fa04f00ecdc6af08814b27551912957 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 13:41:38 +0200 Subject: [PATCH 16/21] apple2: fixed word wrap edge cases --- src/6502/a2_frontend.s | 84 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 612b0ea..dff40ab 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -617,10 +617,7 @@ space rts wrap pha - jsr io_mline_raw - ldx wrappos - stx xpos - jsr io_mflush + jsr io_mfold pla jmp postwrap extended @@ -679,17 +676,73 @@ done rts .) +; Where the firmware thinks the cursor is. +; xpos cannot answer this -- it counts every +; character the frontend has taken in, +; including a pending space and a pending +; word still sitting in wrapbuf, none of +; which has reached cout yet. Returns the +; column in a, with z set when it is zero. +curcol + .( + bit col80 + bmi is80 + + lda CH + rts +is80 + lda OURCH ; the 80-column firmware + rts ; keeps its own copy + .) + io_mline jsr io_mflush io_mline_raw .( - ldx xpos - dex - cpx scrw ; signed cmp - bpl nocr ; don't CR if we filled entire line + lda xpos + beq docr ; nothing on this line at all, so + ; this is a deliberate blank line + jsr curcol + beq done ; we filled the entire line and the + ; firmware wrapped by itself after + ; the last column +docr lda #$8d jsr cout -nocr +done + jmp io_mendline + .) + +io_mfold + ; Break the line and carry the pending + ; word across, for word wrap. + ; + ; Unlike io_mline the word has not been + ; flushed yet, so the whole line so far + ; can still be sitting in wrapbuf with the + ; firmware cursor at column 0 -- and then + ; there is no line to end. + + .( + jsr curcol + beq atcol0 + + lda #$8d + jsr cout +atcol0 + jsr io_mendline + jsr io_mflush ; the pending word lands + ; on the new line, which + jsr curcol ; is where xpos has to + sta xpos ; pick up again + rts + .) + +io_mendline + ; Line-break bookkeeping, shared by + ; io_mline_raw and io_mfold. + + .( lda #0 sta xpos sta pendspc @@ -766,8 +819,9 @@ io_mclear beq nomore ; do we need to issue a newline? - ldx xpos + jsr curcol beq atcol0 + lda #$8d jsr cout atcol0 @@ -850,11 +904,7 @@ io_sprepare sta statush ; Save horizontal position - lda CH - bit col80 - bpl noourch - lda OURCH ; 80 column mode uses another variable -noourch + jsr curcol sta savedch ; Save vertical position too lda cury @@ -1041,7 +1091,9 @@ loop lda statush sta WNDTOP - ldx xpos + ldx savedch ; where io_sprepare found + ; the cursor -- xpos counts + ; the pending word too ldy cury jsr gotoxy jmp set_normal From d6b1a592a4de017bdb330d1fc4398311c79e096d Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 15:00:25 +0200 Subject: [PATCH 17/21] apple2: flush before io_mclear --- src/6502/a2_frontend.s | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index dff40ab..6fd59ec 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -812,6 +812,8 @@ erase_curr io_mclear .( + ; flush in case we still have pending text + jsr io_mflush ; on the c64, we stop for a [MORE] prompt ; before clearing the screen if there ; is any unread line From 9b04d20942b0de4470d7caaa22414fd185006739 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 18:02:24 +0200 Subject: [PATCH 18/21] apple2: added test-apple2 github workflow --- .github/workflows/build.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f6a7a6..0b95abd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,42 @@ jobs: run: sudo npm install -g @yao-pkg/pkg - name: make test run: make test + + test-apple2: + runs-on: ubuntu-latest + env: + IZAPPLE2_SHA1: "1b563d7" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v6 + - name: install tools + run: | + sudo apt update + sudo apt install xa65 acme expect git + - uses: actions/cache@v4 + id: izcache + with: + path: ~/bin/izconsole + key: izconsole-${{ runner.os }}-${{ env.IZAPPLE2_SHA1 }} + - uses: actions/setup-go@v6 + if: steps.izcache.outputs.cache-hit != 'true' + with: + go-version: 'stable' + - name: build izconsole (izapple2) + if: steps.izcache.outputs.cache-hit != 'true' + run: | + git init /tmp/izapple2 + cd /tmp/izapple2 + git remote add origin https://github.com/ivanizag/izapple2 + git fetch --depth 1 origin ${{ env.IZAPPLE2_SHA1 }} + git checkout FETCH_HEAD + mkdir -p "$HOME/bin" + go build -o "$HOME/bin/izconsole" ./frontend/console + - name: build aambundle + run: make --directory=./src aambundle + - name: make apple2 tests + timeout-minutes: 5 + run: make --directory=./test apple2 -j 8 build-manual: runs-on: ubuntu-latest From 831d49ddcfca87cb71d9976c2238f59e5f1c0265 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 18:14:46 +0200 Subject: [PATCH 19/21] apple2: test/apple2 scripts look for izconsole more thoroughly --- test/apple2/Makefile | 10 +++++++--- test/apple2/a2test.exp | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/test/apple2/Makefile b/test/apple2/Makefile index b86dfc0..2f59854 100644 --- a/test/apple2/Makefile +++ b/test/apple2/Makefile @@ -13,7 +13,11 @@ # go build -o izconsole ./frontend/console # cp izconsole ~/bin/ # or anywhere on your PATH -IZCONSOLE ?= $(HOME)/bin/izconsole +# Autodetect: on PATH if installed there, otherwise ~/bin/izconsole. +# Override with: make IZCONSOLE=/path/to/izconsole +IZCONSOLE ?= $(shell command -v izconsole 2>/dev/null || echo "$$HOME/bin/izconsole") +# Make sure a2test.exp (run via expect) sees it too. +export IZCONSOLE DIR6502 = ../../src/6502 # Add --verbose to trace commands and dump the emulated screen on failure. @@ -32,8 +36,8 @@ test: check test.exercise test.gosling test.undo test.save test.save2 # -- emulator and tool checks ------------------------------------------- check: - @if [ ! -x "$(IZCONSOLE)" ] && ! command -v $(IZCONSOLE) >/dev/null 2>&1; then \ - echo "*** izconsole not found (looked for $(IZCONSOLE) and on PATH)."; \ + @if [ ! -x "$(IZCONSOLE)" ]; then \ + echo "*** izconsole not found (looked for $(IZCONSOLE))."; \ echo "*** The Apple II tests need the izapple2 console emulator."; \ echo "*** Build and install it:"; \ echo "*** git clone https://github.com/ivanizag/izapple2"; \ diff --git a/test/apple2/a2test.exp b/test/apple2/a2test.exp index e91546a..ea98e0f 100755 --- a/test/apple2/a2test.exp +++ b/test/apple2/a2test.exp @@ -23,6 +23,9 @@ # ever sees them.) # Everything after "--" is passed through to izconsole. # +# The emulator is found via the IZCONSOLE environment variable if set, +# otherwise ~/bin/izconsole is used. +# # izconsole draws the emulated screen as a full repaint: 27 lines of text, # izconsole's own "Line:" prompt, then ESC[27A to move the cursor back to the # top for the next frame. Inverse video is emitted as an ESC[7m/ESC[0m pair @@ -172,7 +175,19 @@ proc await {target quiet limit {stale ""}} { } log_user 0 -spawn -noecho ~/bin/izconsole -speed full {*}$izflags $dsk +if {[info exists env(IZCONSOLE)] && $env(IZCONSOLE) ne ""} { + set izconsole $env(IZCONSOLE) +} else { + # autodetect: on PATH if installed there, otherwise ~/bin/izconsole + set izconsole "$env(HOME)/bin/izconsole" + if {![file executable $izconsole]} { + if {[catch {exec sh -c {command -v izconsole 2>/dev/null}} p] == 0 + && [file executable $p]} { + set izconsole $p + } + } +} +spawn -noecho $izconsole -speed full {*}$izflags $dsk # wait for the story to boot and settle (the target may be in the intro) switch [await $target 5 60] { From cb5ef3183ded3e7a03bb350bfaa97456d8aea4c0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 18:34:01 +0200 Subject: [PATCH 20/21] apple2: fixed inline progress bars --- src/6502/a2_frontend.s | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/6502/a2_frontend.s b/src/6502/a2_frontend.s index 6fd59ec..f4ade35 100644 --- a/src/6502/a2_frontend.s +++ b/src/6502/a2_frontend.s @@ -863,11 +863,15 @@ io_mprogress ; (width << PRSHIFT) - PREXTRA .( - jsr io_mflush stx f_temp sty f_temp2 + jsr io_mflush + ; so we know we already wrapped to the next line + lda scrw + sta xpos + lda #$db ; '[' jsr cout From 8f3e461beba9febb6e74cbe625de4b82557b9ed4 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 25 Aug 2026 19:19:38 +0200 Subject: [PATCH 21/21] apple2: fixed test-apple2 github workflow --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b95abd..cf0eeb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,11 +44,9 @@ jobs: - name: build izconsole (izapple2) if: steps.izcache.outputs.cache-hit != 'true' run: | - git init /tmp/izapple2 + git clone https://github.com/ivanizag/izapple2 /tmp/izapple2 cd /tmp/izapple2 - git remote add origin https://github.com/ivanizag/izapple2 - git fetch --depth 1 origin ${{ env.IZAPPLE2_SHA1 }} - git checkout FETCH_HEAD + git checkout ${{ env.IZAPPLE2_SHA1 }} mkdir -p "$HOME/bin" go build -o "$HOME/bin/izconsole" ./frontend/console - name: build aambundle