diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 87c5c03..cf0eeb3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,12 +14,46 @@ 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
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 clone https://github.com/ivanizag/izapple2 /tmp/izapple2
+ cd /tmp/izapple2
+ git checkout ${{ env.IZAPPLE2_SHA1 }}
+ 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
@@ -48,7 +82,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 +118,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..13024c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,16 @@ 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/
+# Bundled disks for the codepoint exercise (built by test/codepoints/Makefile)
+test/codepoints/codepoints/
+# Test out files
+test/*/*.out
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.
diff --git a/readme.txt b/readme.txt
index c4b4996..10cf6ef 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,22 @@ 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.
+
+ 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/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..f4ade35
--- /dev/null
+++ b/src/6502/a2_frontend.s
@@ -0,0 +1,3479 @@
+; 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
+; - 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 ----
+
+nunread = $05 ; lines scrolled off since last clear
+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
+; entry point if we don't have an extended character
+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_mfold
+ pla
+ 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
+ ; non-breaking, so reenter at 'plain'
+ ; to avoid line breaks.
+ ; 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
+ jsr plain
+ inc scrw
+ inc scrw
+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
+ .)
+
+; 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
+ .(
+ 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
+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
+
+ ; wrappos is deliberately preserved --
+ ; word-wrap path carries a pending word
+ ; across the line break.
+
+ 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
+ jsr erase_prev
+ dex
+ bne erase
+
+ ldx #1
+ stx nunread ; we wanna see the bottom line at the top
+ 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
+ .(
+ ; 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
+ lda nunread
+ beq nomore
+
+ ; do we need to issue a newline?
+ jsr curcol
+ beq atcol0
+
+ lda #$8d
+ jsr cout
+atcol0
+ jsr moreprompt
+nomore
+ lda #0
+ sta nunread
+ sta xpos
+ sta pendspc
+ sta wrappos
+
+ jsr clrwin ; clears the window
+ ; only, so the status
+ ; area survives
+ lda statush
+ sta cury
+ 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
+
+; HAVE_STYLE=0 for this port
+io_mstyle
+ rts
+
+; TODO consider refactoring with sprogress?
+io_mprogress
+ ; input x = progress
+ ; input y = total
+ ; where 0 <= x <= y
+ ; and y =
+ ; (width << PRSHIFT) - PREXTRA
+
+ .(
+
+ 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
+
+ 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
+
+ ; Save horizontal position
+ jsr curcol
+ sta savedch
+ ; Save vertical position too
+ lda cury
+ sta savedcv
+
+ ; Fill every status row with inverse spaces
+
+ jsr sbar_home
+ lda #24
+ sta WNDBTM
+
+ 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
+; slower 40-column path using 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
+
+ ; 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
+ jmp gotoxy
+ .)
+
+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
+
+ ; 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
+ ; 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
+
+ ; prevent running off the right edge
+ ldy stxoffs
+ cpy scrw
+ bcs skip
+
+ ; 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
+ jmp cout
+ .)
+
+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
+ .(
+ jsr sbar_home
+
+ ldx #0
+loop
+ lda wrapbuf,x
+ ora #$80
+ jsr cout
+ inx
+ cpx scrw
+ bcc loop
+
+ lda statush
+ sta WNDTOP
+
+ ldx savedch ; where io_sprepare found
+ ; the cursor -- xpos counts
+ ; the pending word too
+ ldy cury
+ jsr gotoxy
+ jmp set_normal
+ .)
+
+sbar_home
+ .(
+ ldx #0
+ stx WNDTOP
+ ldy #0
+ jsr gotoxy
+ jmp set_inverse
+ .)
+
+; =====================================
+; Input
+; =====================================
+
+io_getc
+ ; output a = char
+
+ .(
+ jsr io_mflush
+ ; c64 clears this in both gets and getc
+ lda #0
+ sta nunread
+ 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
+ jsr erase_prev
+ dec xpos
+ jmp loop
+done
+ sty f_temp2
+ jsr io_mline_raw
+ ; c64 clears this in both gets and getc
+ lda #0
+ sta nunread
+ 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
+ jsr erase_curr
+ jsr set_normal
+wait
+ inc seed+0
+ bne nohi
+
+ inc seed+1
+nohi
+ lda KBD
+ bpl wait
+
+ sta KBDSTRB
+ and #$7f
+ pha
+
+ jsr erase_curr
+
+ pla
+ rts
+ .)
+
+; =====================================
+; Character translation
+; =====================================
+
+translit
+ ; input a = char code, >= $80
+ ; output tr0, tr1, Z
+ ; tr1 is zero (and Z clear) 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,$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,$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','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,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
+
+; =====================================
+; 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
+ .(
+
+ ; so we don't get a [MORE] prompt before the banner
+ lda #0
+ sta nunread
+
+ 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
+ ; make sure we have no unread lines when we print the banner
+ lda #0
+ sta nunread
+ 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..6754771
--- /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 = 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
+ 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..7ae9b4a 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
@@ -8491,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
.)
@@ -9409,6 +9437,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..1869422 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
@@ -7,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 clean
+.PHONY: all test apple2 clean
diff --git a/test/apple2/Makefile b/test/apple2/Makefile
new file mode 100644
index 0000000..2f59854
--- /dev/null
+++ b/test/apple2/Makefile
@@ -0,0 +1,98 @@
+# 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
+
+# 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.
+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)" ]; 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"; \
+ 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..ea98e0f
--- /dev/null
+++ b/test/apple2/a2test.exp
@@ -0,0 +1,242 @@
+#!/usr/bin/expect -f
+# a2test.exp -- feed commands into izconsole and wait for a target string.
+#
+# 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
+# > 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.
+# 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 key is sent
+# automatically so walkthroughs do not have to step over full screens.
+#
+# 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.
+#
+# 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
+# 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 \[--verbose\] \"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 {}
+}
+
+# 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
+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] {
+ 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
+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"
+ }
+ 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
+ message "TARGET FOUND (command $n: \"$line\")"
+ break
+ }
+ 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} {
+ # 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} {
+ message "*** TARGET NOT FOUND after $n commands"
+ dumpscreen
+}
+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
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 0000000..a75f9d6
Binary files /dev/null and b/test/codepoints/codepoints.aastory differ
diff --git a/test/codepoints/codepoints.dg b/test/codepoints/codepoints.dg
new file mode 100644
index 0000000..3828fde
--- /dev/null
+++ b/test/codepoints/codepoints.dg
@@ -0,0 +1,445 @@
+%% codepoints.dg -- Å-machine character set and status bar exercise.
+%%
+%% Several things are being exercised here which are invisible to the
+%% aambox and node frontends and have to be eyeballed on real hardware (or in
+%% an emulator):
+%%
+%% * Word wrapping behavior -- different on every implementation.
+%%
+%% * Every codepoint the 8-bit backends claim to handle. The C64 draws
+%% these from a custom font; the Apple II has no custom font in text mode
+%% and transliterates them to ASCII through the table at the bottom of
+%% src/6502/a2_frontend.s. Page 6 is a negative control: those codepoints
+%% are in neither backend and should come out as the interpreter's
+%% "unknown character" glyph ('?' on the Apple II, a solid box on the C64).
+%%
+%% * Status-bar edge cases: extended characters in row 0 (which goes
+%% through the buffered wrapbuf/io_scommit path instead of cout), a row 0
+%% too wide for the screen (the buffered path truncates instead of
+%% wrapping), and a row 1+ too wide for the screen (the cout path wraps
+%% it -- watch where the overflow lands).
+%%
+%% * A wrap-buffer aliasing probe (press W): leaves a word pending in the
+%% middle of a text line and redraws the status bar before the word is
+%% flushed. Row 0 of the status bar shares its buffer with the pending
+%% word, so a stale or doubled word here means aliasing trouble.
+%%
+%% Page 8 fold straddling: two-character expansions split across a hard
+%% fold must lose nothing (39-padded lines target 40 columns,
+%% 79-padded lines target 80), overlong words made of expanding
+%% characters must hard-fold cleanly, transliterated spaces
+%% landing exactly on the fold must not vanish, and lines and
+%% paragraphs that fill the screen exactly -- notably right after
+%% an em dash expansion -- must not leave a spurious blank line
+%% (the firmware wraps by itself after the last column)
+%% Page 9 negative control 2, consecutive unknown codepoints: each must
+%% become its own unknown-character glyph
+%%
+%% * A status bar whose height changes at run time. The Å-machine reads the
+%% height of the top status area straight out of the style class, so a
+%% variable-height bar means a set of classes -- one per height -- picked
+%% at run time. ('status bar $ with height $' is a Z-machine affordance
+%% and is ignored by this backend.) Rows 2 and up take a different code
+%% path in the Apple II frontend than row 0 does: row 0 is buffered in
+%% wrapbuf and committed by io_scommit, while the rest go straight to the
+%% screen through cout. Cycling the height exercises both, plus the
+%% shrink case, where the bar has to give rows back to the text window.
+
+(story title) Codepoint Exercise
+(story author) Å-machine test suite
+(story noun) Character set and status bar exercise
+(story release 1)
+
+(story ifid)
+ 9C4F2E17-6B0A-4D3C-8E5F-1A72B9D0C384
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Style classes
+
+%% One class per status bar height. Heights above 19 are clamped by the 6502
+%% engine, and a height of 0 is treated as 1.
+
+(style class @status1)
+ height: 1em;
+
+(style class @status2)
+ height: 2em;
+
+(style class @status3)
+ height: 3em;
+
+(style class @status4)
+ height: 4em;
+
+(style class @rightcol)
+ float: right;
+ width: 10ch;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% State
+
+(global variable (current page 1))
+(global variable (bar height 1))
+
+(last page 9)
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% The status bar
+
+%% Each row is deliberately made of characters that need transliterating, so
+%% that a too-short bar (text missing) and a too-tall bar (leftover inverse
+%% rows) are both obvious at a glance.
+
+(draw bar)
+ (bar height $H)
+ (draw bar $H)
+
+(draw bar 1)
+ (status bar @status1) (bar contents)
+
+(draw bar 2)
+ (status bar @status2) (bar contents)
+
+(draw bar 3)
+ (status bar @status3) (bar contents)
+
+(draw bar 4)
+ (status bar @status4) (bar contents)
+
+%% Row 0 is deliberately given a long tail of expanding characters: it goes
+%% through the buffered wrapbuf path, which truncates at the screen width
+%% instead of wrapping, so both the transliteration and the truncation point
+%% (possibly splitting an expansion) are under test on every page.
+(bar contents)
+ (div @rightcol) {
+ (bar height $H)
+ h= $H
+ }
+ (current page $P) (last page $L)
+ Codepoints $P/$L \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014} \x{00E9}\x{00DF}\x{2014}
+ (bar height $H)
+ (if) ($H > 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.