-- Porting Ultima 5 to ProDOS --
The primary change was to the DINKEYDOS file.
We start with this code:
20AB JSR $D158 ;read root block
20AE BCS $2059 ;wipe memory and reboot on failure!
20B0 LDA #$00
20B2 STA $75
20B4 LDA #$D6
20B6 STA $76
20B8 LDY #$04
20BA LDA ($75),Y
20BC AND #$0F
20BE TAY
20BF CLC
20C0 LDA $75
20C2 ADC #$05
20C4 STA $75
20C6 LDA $76
20C8 ADC #$00
20CA STA $76
20CC LDA ($75),Y
20CE ORA #$80
20D0 STA $D5CA,Y ;save volume label for later
20D3 DEY
20D4 BPL $20CC
20D6 JSR $216D ;delay for ~2.75 seconds
20D9 LDY #$07
20DB TYA
20DC ASL
20DD ASL
20DE ASL
20DF ASL
20E0 AND #$F0
20E2 TAX
20E3 STX $7F
20E5 LDA #$01
20E7 STA $78
20E9 JSR $2117 ;try to read a sector from drive 1 in current slot
20EC BCS $20F2
20EE TXA
20EF STA $D5DA,Y
20F2 LDA #$02
20F4 STA $78
20F6 JSR $2117 ;try to read a sector from drive 2 in current slot
20F9 BCS $2106
20FB TXA
20FC STA $D5E2,Y
20FF STY $51
2101 JSR $216D ;delay for ~2.75 seconds
2104 LDY $51
2106 DEY
2107 BNE $20DB
It reads the boot disk to find out the volume label. This is used to check when the boot disk is in the drive.
Then it performs a slot scan to find potentially usable disk drives. However, there's an initial delay of nearly three seconds for no good reason, and another one for each time that drive 2 was read successfully in any slot.
I changed it to not store the volume label, and not pause so that the game starts more quickly.
Within DinkeyDOS itself, we have this:
D2ED LDA #$01 ;read command
D2EF STA $77
D2F1 LDA #$02
D2F3 STA $70
D2F5 LDA #$00 ;block #02
D2F7 STA $71
D2F9 LDA #$2B ;offset of first filename
D2FB STA $7B
D2FD LDA #$00
D2FF STA $75
D301 LDA #$D6 ;buffer $D600
D303 STA $76
D305 JSR $D363 ;read block
D308 BCC $D30B
D30A RTS
This code is the start of the routine that searches the root directory for a particular file.
To make it search within subdirectories instead, I just had to change the block number to the proper value.
That required a routine that I put into the U5 file, which reads the disk and walks the prefix until it reaches the current subdirectory.
DinkeyDOS performs exact matching of filenames:
D320 LDY #$00
D322 LDA ($75),Y
D324 BEQ $D33C
D326 AND #$0F
D328 TAY
D329 CMP $D5B9 ;match length
D32C BNE $D33C
D32E LDA $D5B9,Y
D331 AND #$7F
D333 CMP ($75),Y ;match filename
D335 BNE $D33C
D337 DEY
D338 BNE $D32E
D33A CLC
D33B RTS
This code compares the contents of the disk buffer with the requested filename.
I extended this code a little bit, to provide a wildcard option:
D335 BNE $D37E
...
D37E CMP #$5F ;'_', wildcard character
D380 BEQ $D337
D382 BNE $D33C
This was needed after I found that the game performs direct block reads while loading map data.
The problem was that the reads didn't come from a constant location, unlike in Ultima 4.
Instead, the code knows, based on which disk is in the drive, what is the starting track for the map data.
Fortunately, the starting track is always within the $1x range, so I created a corresponding file named "TRACKx" in each subdirectory, where 'x' is replaced appropriately.
Then there was the possibility that the file doesn't exist on the disk, and how the original game handles that:
D34F LDA $75
D351 CMP #$00
D353 BCC $D320
D355 LDA #$04
D357 STA $7B
D359 INC $70
D35B LDA $70
D35D CMP #$06
D35F BCC $D2FD
D361 SEC
This code searches all six blocks in the root directory, even if they're mostly empty.
In my case, I had to remember the pointer to the next block and check for the end of the list, all within the same space:
D34F LDA $D602
D352 STA $70
D354 LDA $D603
D357 STA $71
D359 ORA $70
D35B CMP #$01
D35D LDA #$04
D35F BCS $D2FB
D361 SEC
I got lucky. It was an exact fit.
Next was the block translation:
D363 LDA #$00
D365 STA $72
D367 LDA $71
D369 STA $7A
D36B LDA $70
D36D STA $79
D36F AND #$07
D371 TAY
D372 CLC
D373 ROR $7A
D375 ROR $79
D377 ROR $7A
D379 ROR $79
D37B ROR $7A
D37D ROR $79
D37F LDA $79
D381 STA $72
D383 LDA $D5A9,Y
D386 STA $73
D388 LDA $D5B1,Y
D38B STA $74
This monstrosity converts a block number to a track and sector combination, but using neither DOS nor ProDOS ordering.
My version is much simpler:
D363 LDA #$D1 ;self-modified
D365 STA $43
D367 LDA $71
D369 STA $47
D36B LDA $70
D36D STA $46
D36F LDA $76
D371 STA $45
D373 LDA $75
D375 STA $44
D377 LDA $77
D379 STA $42
D37B JMP $D1D1 ;self-modified
I replaced the first 'Q' with the unit number, and the other two with the SmartPort interface address.
There's a separate routine which checks the volume name:
D463 JSR $D3F9
D466 BCS $D4A6
D468 LDA #$00
D46A STA $75
D46C LDA #$D6
D46E STA $76
D470 LDA #$02
D472 STA $70
D474 LDA #$00
D476 STA $71
D478 LDA #$01
D47A STA $77
D47C JSR $D363
D47F LDY #$04
D481 LDA ($75),Y
D483 AND #$0F
D485 TAY
D486 DEY
D487 CLC
D488 LDA $75
D48A ADC #$05
D48C STA $75
D48E LDA $76
D490 ADC #$00
D492 STA $76
D494 LDA ($75),Y
D496 ORA #$80
D498 CMP #$AF
D49A BEQ $D4A1
D49C CMP $D5CA,Y
D49F BNE $D4A6
D4A1 DEY
D4A2 BPL $D494
D4A4 CLC
D4A5 RTS
Interestingly, it supports path separators ('/').
Again, my version is much simpler:
D463 LDA #$02
D465 STA $D2F2
D468 LDA #$00
D46A STA $D2F6 ;block 2, self-modified
D46D LDA #$00
D46F STA $D32D ;disable length check
D472 LDA #$C9 ;alter buffer offset
D474 STA $D32F
D477 JSR $D2ED
D47A LDY #$11
D47C LDA ($75),Y
D47E STA $D2F2
D481 INY
D482 LDA ($75),Y
D484 STA $D2F6 ;subdirectory block number
D487 LDA #$0E
D489 STA $D32D ;enable length check
D48C LDA #$B9
D48E STA $D32F ;restore buffer offset
D491 RTS
I set the block number for the subdirectory that holds the U5 file, as a starting point for the search for the disk subdirectories.
It's set initially by the same routine in the U5 file that sets the value for the file search.
Then I cheated a bit and simply reused the filename matching routine for the volume name matching.
All that was needed was to disable the name-length check temporarily because the original string didn't have it and there wasn't room to insert it.
The final step was to deal with the "TRACKx" file. Since the game obviously doesn't know anything about it, I had to find space to intercept the direct disk requests and redirect to a file.
Fortunately, since the game was running from a 5.25" disk, there's a 112 bytes array for 6-and-2 decoding, which I replaced:
DA96 JSR $D4AB ;save return address
DA99 JSR $D52E ;fetch track number
DA9C JSR $D564 ;skip request block and set return address
DA9F LDA $70
DAA1 PHA
DAA2 LDY #$06
DAA4 LDA $DAE8,Y
DAA7 STA $D5B9,Y ;copy requested filename
DAAA DEY
DAAB BPL $DAA4
DAAD JSR $D2ED ;subdirectory search
DAB0 LDY #$06
DAB2 LDA ($75),Y ;the 'x' in the filename
DAB4 ASL
DAB5 BPL $DAB9 ;skip if number already
DAB7 SBC #$0D ;translate letter to number
DAB9 ASL
DABA ASL
DABB STA $DAC1 ;convert 'x' to block
DABE SEC
DABF PLA
DAC0 SBC #$D1 ;determine block relative to 'x'
DAC2 PHA
DAC3 LDY #$11
DAC5 LDA ($75),Y
DAC7 STA $70
DAC9 INY
DACA LDA ($75),Y ;KEY POINTER
DACC STA $71
DACE LDA #0
DAD0 STA $75
DAD2 LDA #$D6 ;buffer address
DAD4 STA $76
DAD6 JSR $D363 ;read block
DAD9 PLA
DADA TAY
DADB LDA $D600,Y
DADE STA $70
DAE0 LDA $D700,Y ;set requested block number
DAE3 STA $71
DAE5 JMP $D363 ;read block
DAE8 .BYTE 6,'TRACK_'
Beyond that, I just had to change a single JSR in a few files to call this new routine:
TEMP.SUBS and TRANSFER from the boot disk; MAIN.TWN, TALK from the other disks, and MAIN.DNG from the dungeon disk.
That's it.