[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Apple Games Disassembly Project - Ultima 1



Apple Games Disassembly Project - Ultima 1

Curious how Ultima 1 rendering works?
Curious why the original Ultima 1 runs like crap?
 (That's a technical term for 'slow'. =)
Curious about learning how to optimize a 6502 map renderer?

Since I couldn't find any *good* technical notes
about the original Ultima 1 for the Apple ...

... for your viewing pleasure:

Michael's Ultima 1 Annotated Technical Notes


Table of Contents:
 0. Required Materials
 1. View outdoor tiles
 2. Map Viewer
 3. Fast Map Drawing
 4. Annotated original DrawMap
 5. Monster Shapes
 6. Bootstrapping
 7. Applesoft Variables
 8. Bugs
 9. Misc.


0. Required Materials
= = = = = = = = = =
 (x) Ultima 1 (Original 1981 DOS3.3 version) disks/images
     Notes:
       I mounted drive 1 with: ultima_1.dsk.gz
       I mounted drive 2 with: ultima_2.dsk.gz
     from Asimov: /games/rpg/ultima/ultima_I/
       After unzipping if they don't have the '.dsk'
     extension rename them so they do.
 (x) Apple ][ (real or emulator)

You can tell which version you have by the border
 No map border = original 1981
 Blue map border = re-released 1986 remake


1. View outdoor tiles
= = = = = = = = = = =

 1. Boot Ultima 1
 2. At the ] prompt press CTRL-RESET twice
    (AppleWin: Ctrl-F2)
 3. Type
    (or copy and use Shift-Insert to paste into AppleWin)
BLOAD ULTSHAPES,A$800
BLOAD DRAW 64.OBJ,A$A00
CALL-151
6:9 5 80
1000:00 10 20 30 40 50 60 70
1040:80 90 A0 B0 C0 D0 E0 F0
F3E2G
C052
A00G

To get back to Applesoft
C053
<Ctr-C> RETURN
TEXT

Tiles: (in hex)
 00 Water
 10 Land
 20 Woods
 30 Mountains
 40 Castle (top-down)
 50 Sign
 60 Town/City
 70 Dungeon
 80 Fighter (and Avatar)
 90 Horse
 A0 Cart
 B0 Raft
 C0 Pirate Ship
 D0 Hover Car
 E0 Space Shuttle
 F0 Time Machine

To draw the map from Applesoft:
 CALL 2560


2. More Fun - Map Viewer
= = = = = = = = = = = =

First, load the necessary data and code...
BLOAD ULTSHAPES,D1,A$800
BLOAD DRAW 64.OBJ,D1,A$A00
BLOAD BTERRA0,D2,A$1000

Then enter this program to view the world =)

CALL-151
- - - - - 8< - - - - -
C00:20 E2 F3 A9 81 85 08 85
C08:EB D0 20 AD 8B 0A C9 0A
C10:D0 04 A9 0C D0 06 C9 0C
C18:D0 05 A9 0A 8D 8B 0A A5
C20:EB 49 01 85 EB AA 9D D2
C28:BF D0 06 A9 20 85 06 85
C30:07 20 00 0A A5 06 20 DA
C38:FD A9 A0 20 ED FD A5 07
C40:20 DA FD A9 A0 20 ED FD
C48:AE C5 0C E8 8A 20 ED FD
C50:20 8E FD AD 00 C0 10 FB
C58:8D 10 C0 C9 8D F0 47 C9
C60:8B F0 43 C9 AF F0 3B C9
C68:8A F0 37 C9 88 F0 2E C9
C70:95 F0 26 C9 A0 F0 B2 C9
C78:9B F0 8E C9 D1 F0 2C C9
C80:B1 90 D0 C9 B5 B0 CC AA
C88:CA 8E C5 0C A2 00 BD B8
C90:0C F0 0C 20 BD 9E E8 D0
C98:F5 E6 06 E6 06 C6 06 18
CA0:90 8F E6 07 E6 07 C6 07
CA8:18 90 F5 A0 03 A9 00 C6
CB0:B8 91 B8 88 10 FB 60 EA
CB8:84 C2 CC CF C1 C4 A0 C2
CC0:D4 C5 D2 D2 C1 BE AC C1
CC8:A4 B1 B0 B0 B0 8D 00
- - - - - 8< - - - - -
C00G

Map Viewer Keys
 RETURN Move Up (or Up arrow)
 /      Move Down (or Down arrow)
 <-     Move Left
 ->     Move Right
 Space  Teleport to center of map
 1      Load map 0 (Lands of Lord British)
 2      Load map 1 (Lands of the Feudal Lords)
 3      Load map 2 (Lands of the Dark Unknown)
 4      Load map 3 (Lands of Danger and Despair)
 ESC    Toggle fullscreen
 Q      Exit

Note: Prints X Y W, where W is the map/world last loaded

For map details:
 http://shrines.rpgclassics.com/pc/ultima1/overworld.shtml

Annoted Ultima 1 (original) MapViewer 1e

 C00:20 E2 F3        JSR HGR
 C03:A9 81           LDA #81     ; sprite #80 = Fighter (vehicle =
none), #1 = MixedMode
 C05:85 08           STA $8      ; player vehicle
 C07:85 EB           STA $EB     ; FullScreenFlag
 C09:D0 20           BNE .warp
 C0B:AD 8B 0A TFS    LDA $0A8B   ; have custom DrawMap Patch
v4?
 C0E:C9 0A           CMP #A
 C10:D0 04           BNE .mix
 C12:A9 0C           LDA #C
 C14:D0 06           BNE .doFS
 C16:C9 0C    .mix   CMP #C
 C18:D0 05           BNE .tfs
 C1A:A9 0A           LDA #A
 C1C:8D 8B 0A .doFS  STA $0A8B
 C1F:A5 EB    .tfs   LDA $EB     ; ToggleFullScreen
 C21:49 01           EOR #1
 C23:85 EB    .fs    STA $EB     ; FullScreen
 C25:AA              TAX
 C26:9D D2 BF        STA $BFD2,X ; $BFD2+$80 = $C052 or $C053
 C29:D0 06           BNE .draw   ; don't reset player position
 C2B:A9 20    .warp  LDA #20     ; reset player to center of map
 C2D:85 06           STA PlayerX
 C2F:85 07           STA PlayerY
 C31:20 00 0A .draw  JSR DrawMap
 C34:A5 06           LDA $06
 C36:20 DA FD        JSR PRBYTE  ; $FDDA
 C39:A9 A0           LDA ' '
 C3B:20 ED FD        JSR COUT    ; $FDED
 C3D:A5 07           LDA $07
 C40:20 DA FD        JSR PRBTE   ; $FDDA
 C43:A9 A0           LDA ' '
 C45:20 ED FD        JSR COUT    ; $FDED
 C48:AE C5 0C        LDX LOAD+#D ; last loaded map, bterra#
 C4B:E8              INX
 C4C:8A              TXA
 C4D:20 ED FD        JSR COUT    ; $FDED
 C50:20 8E FD        JSR CROUT   ; $FD8E -> $FDED
 C53:AD 00 C0 .key   LDA Key
 C56:10 FB           BPL .key
 C58:8D 10 C0        STA KeyStrobe
 C5B:C9 8D           CMP kRETURN ; #$8D Return
 C5D:F0 47           BEQ .up
 C5F:C9 8B           CMP kUP     ; #$8B Ctrl-K ^
 C61:F0 43           BEQ .up     :             |
 C63:C9 AF           CMP kSLASH  ; #$AF /
 C65:F0 3B           BEQ .dn     ;             |
 C67:C9 8A           CMP kDOWN   ; #$8A Ctrl-J v
 C69:F0 37           BEQ .dn
 C6B:C9 88           CMP kLEFT   ; #$88 Ctrl-H <--
 C6D:F0 2E           BEQ .lEFT
 C6F:C9 95           CMP kRIGHT  ; #$95 Ctrl-U -->
 C71:F0 26           BEQ .rIGHT
 C73:C9 A0           CMP kSPACE  ; #$A0 ' '
 C75:F0 B2           BEQ .pos
 C77:C9 9B           CMP kESC    ; #$9B ESC
 C79:F0 8E           BEQ TFS     ; ToggleFullScreen
 C7B:C9 D1           CMP 'Q'     ; #$D1 'Q'
 C7D:F0 2C           BEQ .done
 C7F:C9 B1           CMP '1'     ; <  '1'
 C81:90 D0           BCC .key
 C83:C9 B5           CMP '5'     ; >= '5'
 C85:B0 CC           BCS .key
 C87:AA              TAX
 C88:CA              DEX
 C89:8E C5 0C        STX LOAD+#D ; bterra#
 C8C:A2 00           LDX #0
 C8E:BD B8 0C .load  LDA $0CC0,X
 C91:F0 0C           BEQ .2draw  ; to lazy to calculate .draw offset
 C93:20 BD 9E        JSR $9EBD   ; DOS3.3 -> $9EBD CSW
 C96:E8              INX
 C97:D0 F5           BNE load
 C99:E6 06    .right INC PlayerX
 C9B:E6 06           INC PlayerX
 C9D:C6 06    .left  DEC PlayerX
 C9F:18       .2draw CLC
 CA0:90 8F           BCC .draw
 CA2:E6 07    .dn    INC PlayerY
 CA4:E6 07           INC PlayerY
 CA6:C6 07    .up    DEC PlayerY
 CA8:18              CLC
 CA9:90 F5           BCC .2draw+1 ; to lazy to calculate .draw offset
 CAB:A0 03    .done  LDY #3       ; clear rest of input line (may have
had a BLOAD)
 CAD:A9 00           LDA #0       ; Applesoft: D7EB: LDY #2 LDA
($B8),Y --> if line #0 then END
 CAF:C6 B8           DEC $B8      ; Monitor: get 'CR' ($8D) from
'C00G'
 CB1:91 B8    .clear STA ($B8),Y  ;    w/o adjust, would get 'A' from
BLOAD...
 CB3:88              DEY          ; we can't store '$8D' as Applesoft
would assume 'PLOT' token
 CB4:10 FB           BPl .clear   ; $0CB3
 CB6:60
 CB7:EA              NOP          ; pad out to $CB8
 CB8:84       LOAD   DB $84   ; Ctrl-D for DOS3.3
 CB9:C2 CC CF C1 C4  ASC "BLOAD BTERRA>,A$1000" ; $BE '>' + 1 --> '?'
$BF
    A0 C2 D4 C5 D2
    D2 C1 BE AC C1
    A4 B1 B0 B0 B0
CCD:8D 00           DB $8D, $8D, $00

AppleWin Symbols
 DB LOAD CB8:CCE

 SYM MapView = C00
 SYM .mix    = C16
 SYM .doFS   = C1C
 SYM .tfs    = C1F
 SYM .fs     = C2D
 SYM .warp   = C2B
 SYM .draw   = C31
 SYM .key    = C53
 SYM .load   = C8E
 SYM .right  = C99
 SYM .left   = C9D
 SYM .2draw  = C9F
 SYM .down   = CA2
 SYM .up     = CA6
 SYM .done   = CAB
 SYM .clear  = CB1


3. Fast Map Drawing
= = = = = = = = = =

You can definitely tell Richard Garriot was
still cutting his teeth learning 6502 assembly.

Ultima 1 outdoor drawing is SLOW because:

- it *always* draw an extra 2 bottom rows of the outside map
  even though those are always covered by
  the 4 line command history and status window!
- it draws the 14x16 sprites in 2 column passes;
  redundant HGR Y calculations at $A6E
- The GetMapPtr at $A31 uses a SLOW Y*64 calculation
- it doesn't use a HGR Y table
- The map tiles are organized by column, not scanline
  This means the blitter is horribly inefficient.
  (The enhanced version of Ultima 1 uses a scanline blitter! =)
- It always draws the map tile under the avatar and THEN
  draws the vehicle instead of ONLY drawing the vehicle
  for the player tile. This causes flicker when moving.

Here is a faster and smaller version of DrawMap.
 - About tTwice as fast as original
 - Fixes the vehicle flicker
 - Smaller!
 - Annotated source is provided
 - Only draws 10 visible tile rows
 - Uses a mini HGR Y lookup table

CALL-151
- - - - - 8< - - - - -
A00:38 A5 07 E9 05 85 D7 38
A08:A5 06 E9 09 85 EF A2 00
A10:86 09 A5 EF 85 D6 A0 00
A18:BD 90 0A 85 FE BD 9C 0A
A20:85 FF A2 00 86 FC A5 D6
A28:C5 06 D0 0C A5 D7 C5 07
A30:D0 04 A5 08 D0 1E A5 D6
A38:C9 40 B0 1B A5 D7 C9 40
A40:B0 15 6A 66 FC 6A 66 FC
A48:09 10 85 FD A5 FC 65 D6
A50:85 FC A1 FC 29 F0 AA A5
A58:FE 85 FA A5 FF 85 FB BD
A60:00 09 91 FA C8 BD 00 08
A68:91 FA 88 E8 18 A5 FB 69
A70:04 C9 40 90 E8 A5 FA 49
A78:80 C5 FE D0 DC E6 D6 C8
A80:C8 C0 28 D0 9D E6 D7 A6
A88:09 E8 E0 0A D0 82 60 EA
0A90:00 00 00 00 28 28 28 28 50 50 50 50
0A9C:20 21 22 23 20 21 22 23 20 21 22 23
- - - - - 8< - - - - -
A00G

If you Want to patch a copy of your "Master" disk
 UNLOCK DRAW 64.oBJ
 BLOAD DRAW 64.OBJ,A$A00
Enter the above in...
 BSAVE DRAW 64.OBJ,A$A00,L$200
 LOCK DRAW 64.oBJ

Notes:
 For some reason DRAW 64.OBJ also includes
 an Applesoft DRAW shape table at $B00.
 Don't ask me why since the game also
 loads them via:
   BLOAD OUT.SHAPES,A$B00

Annotated Optimized DrawMap ver 4c:
 0A00:38       DrawMap   SEC
 0A01:A5 07              LDA $7       ; Player.Y
 0A03:E9 05              SBC #5       ; World to MapWindow.Top
 0A05:85 D7              STA TileY    ; CenterY (10 Tiles Tall)
 0A07:38                 SEC
 0A08:A5 06              LDA $6       ; Player.X
 0A0A:E9 09              SBC #9       ; World to MapWindow.Left
 0A0C:85 EF              STA TileLeft ; CenterX (20 Tiles Across)
 0A0E:A2 00              LDX #0       ;
 0A10:86 09    drawRow   STX Row      ; Row is zero based
 0A12:A5 EF              LDA TileLeft
 0A14:85 D6              STA TileX
 0A16:A0 00              LDY #0       ; NextDrawColumn
 0A18:BD 90 0A           LDA HGRYLO,X
 0A1B:85 FE              STA $FE
 0A1D:BD 9C 0A           LDA HGRYHI,X
 0A20:85 FF              STA $FF
 0A22:A2 00    drawCols  LDX #0       ; default Water Tile (00)
 0A24:86 FC              STX MAP.PTR  ; Init MapTilePtrL
 0A26:A5 D6              LDA TileX
 0A28:C5 06              CMP PlayerX  ; CurTileX == PlayerX ?
 0A2A:D0 0C              BNE clipX2
 0A2C:A5 D7              LDA TileY    ; CurTileY == PlayerY ?
 0A2E:C5 07              CMP PlayerY
 0A30:D0 04              BNE clipX
 0A32:A5 08    doVehicle LDA Vehicle  ; ($08) is always > #$80
 0A34:D0 1E              BNE maskTile ; could also use A6 08 LDX
Vehicle 30 21 BMI drawTile for speed
 0A36:A5 D6    clipX     LDA TileX
 0A38:C9 40    clipX2    CMP #40      ; Win.X > 64 ?
 0A3A:B0 1B              BCS drawTile
 0A3C:A5 D7              LDA TileX    ; 0 00cdefgh 00000000
 0A3E:C9 40              CMP #40      ; Win.Y > 64 ?
 0A40:B0 15              BCS drawTile ; get a free Clear Carry :)
                       ; CLC        carry Acc/$FD  $FC
 0A42:6A                 ROR          ; h 000cdefg 00000000
 0A43:66 FC              ROR $FC      ; 0 000cdefg h0000000
 0A45:6A                 ROR          ; g 0000cdef h0000000
 0A46:66 FC              ROR $FC      ; 0 0000cdef gh000000
 0A48:09 10              OR  #10      ; $FD.$FC max = $0FFF
 0A4A:85 FD              STA $FD      ; Map64x64 at $1000..$1FFF
 0A4C:A5 FC              LDA $FC
 0A4E:65 D6              ADC $D6
 0A50:85 FC              STA $FC
 0A52:A1 FC              LDA ($FC,X)  ; CurTile
 0A54:29 F0    maskTile  AND #F0
 0A56:AA                 TAX
 0A57:A5 FE    drawTile  LDA $FE
 0A59:85 FA    drawHalf  STA $FA
 0A5B:A5 FF              LDA $FF
 0A5D:85 FB    draw8     STA $FB
 0A5F:BD 00 09           LDA $0900,X
 0A62:91 FA              STA ($FA),Y  ; draw even col
 0A64:C8                 INY
 0A65:BD 00 08           LDA $0800,X
 0A68:91 FA              STA ($FA),Y  ; draw odd col
 0A6A:88                 DEY
 0A6B:E8                 INX
 0A6C:18                 CLC
 0A6D:A5 FB              LDA $FB
 0A6F:69 04              ADC #4       ; next screen row
 0A71:C9 40              CMP #40      ; done 8 rows?
 0A73:90 E8              BCC draw8
 0A75:A5 FA              LDA $FA
 0A77:49 80              EOR #80      ; optimization: ADC #80
 0A79:C5 FE              CMP $FE
 0A7B:D0 DC              BNE drawHalf ; done all 16 scanlines?
 0A7D:E6 D6              INC TileX
 0A7F:C8                 INY
 0A80:C8                 INY
 0A81:C0 28              CPY #$28     ; drawn all tiles acrosss?
 0A83:D0 9D              BNE drawCols
 0A85:E6 D7              INC TileY
 0A87:A6 09              LDX Row      ;
 0A89:E8                 INX
 0A8A:E0 0A              CPX #A       ; done 10 tile rows?
 0A8C:D0 82              BNE drawRow  ; ... must ... reach ...
 0A8E:60
 0A8F:EA                 NOP          ; pad to $A90
 0A90:00 00 00 00        HGRYLO DB 00,00,00,00 ; dest address of every
16 scanlines
 0A94:28 28 28 28               DB 28,28,28,28
 0A98:50 50 50 50               DB 50,50,50,50
 0A9C:20 21 22 23        HGRYHI DB 20,21,22,23
 0AA0:20 21 22 23               DB 20,21,22,23
 0AA4:20 21 22 23               DB 20,21,22,23

Applewin symbol commands
 DB PlayerX       6
 DB PlayerY       7
 DB Vehicle       8 // player vehicle sprite #
 DB Row           9
 DB TileX        D6 // = (PlayerX - 9)
 DB TileY        D7 // = (PlayerY - 5)
 DB TileLeft     EF
 DB HGR.DST      FA
 DB MAP.PTR      FC
 DB HGR.BASE     FE
 DB HGRYLO      A90:A9B
 DB HGRYHI      A9C:AA7
 DB TilesOdd    800
 DB TilesEvn    900
 DB Map        1000

 SYM DrawMap    = A00
 SYM drawRow    = A10
 SYM drawCols   = A22
 SYM drawPlayer = A32
 SYM clipX      = A36
 SYM clipX2     = A38
 SYM maskTile   = A54
 SYM drawTile   = A57
 SYM drawHalf   = A59
 SYM draw8      = A5D

In order to get any more speed the tiles
at $800 .. $9FF would need to be swizzled.

Ideally, there would also be 4 map scrollers that scroll
 the HGR page "in-place"
 - scroll left
 - scroll right
 - scroll down
 - scroll up
And the corresponding 4 draw partial map functions:
 - draw right edge
 - draw left edge
 - draw top edge
 - draw bottom edge

This is left as an exercise for the reader. =)


4. Annotated original DrawMap
= = = = = = = = = = = = = = =

If you want a "good" example of how NOT to
write a 2D map renderer here is my
annotated Ultima 1 for the original Draw Map code.

File: DRAW 64.OBJ

 0A00:38        DrawMap SEC
 0A01:A5 07             LDA $7      ; Player.Y
 0A03:E9 05             SBC #5      ; World to MapWindow.Top
 0A05:85 D7             STA $D7     ; CurTile.Y
 0A07:A9 00             LDA #00
 0A09:85 FE             STA $FE     ; HGR.BASE (dest tile ptr)
 0A0B:A9 20   drawTriad LDA #20     ; every 64 rows dest Y start
address $20xx
 0A0D:85 FF             STA $FF     ;
 0A0F:A0 00     drawRow LDY #0      ; left column
 0A11:38                SEC
 0A12:A5 06             LDA $06     ; Player.X
 0A14:E9 09             SBC #9      ; World to MapWindow.Left
 0A16:85 D6             STA $D6     ; CurTile.X
 0A18:A9 09     drawCol LDA #9      ; start with even column of source
TileAddr.Hi
 0A1A:AD 68 0A          STA TileAdrH; Self Modifying Code:  GET
TileScanLine
 0A1D:A9 00             LDA #0      ; CurTile = default to Water Tile
(0)
 0A1F:48                PHA         ; save curtile
 0A20:A5 D6             LDA $D6     ; Win.X > 64 ?
 0A22:C9 40             CMP #40
 0A24:B0 28             BCS ClipWin ; $0A4E
 0A26:A5 D7             LDA $D7     ; Win.Y > 64 ?
 0A28:c9 40             CMP #40
 0A2A:B0 22             BCS ClipWin ; $0A4E
 0A2C:68                PLA         ; toss CurTile
 0A2D:85 FD             STA $FD     ; Init MapTilePtrH
 0A2F:A5 D7             LDA $D7     ; MapTileAddr(y,x) = $1000 + y *
64 + X
 0A31:0A                ASL         ; inefficient y*64
 0A32:0A                ASL
 0A33:0A                ASL
 0A34:26 FD             ROL $FD
 0A36:0A                ASL
 0A37:26 FD             ROL $FD
 0A39:0A                ASL
 0A3A:26 FD             ROL $FD
 0A3C:0A                ASL
 0A3D:26 FD             ROL $FD
 0A3F:65 D6             ADC $D5     ; +x
 0A41:84 FC             STA $FC
 0A43:A5 FD             LDA $FD
 0A45:69 10             ADC #10     ; #0..#0FFF -> #1000..#1FFF
 0A47:85 FD             STA $FD
 0A49:A2 00             LDX #0
 0A4B:A1 FC             LDA ($FC,X) ; get tile from map
 0A4D:48                PHA
 0A4E:68        clipWin PLA         ; get CurTile
 0A4F:09 0F             ORA #0F     ; Tile.Height = 16
 0A51:AA                TAX
 0A52:86 09             STX $09     ; not needed -- save CurTile??
 0A54:A9 80      doTile LDA #80     ; start HGR address Low bottom 8
scanlines
 0A56:A6 09             LDX $09
 0A58:48         doHalf PHA         ; save HGR.DST Low
 0A59:18                CLC
 0A5A:65 FE             ADC $FE     ; add HGR.BASE.LOW
 0A5C:85 FA             STA
 0A5E:A9 1C             LDA #1C     ; start at dest bottom half = 8
scanline
 0A60:48          draw8 PHA
 0A61:18                CLC
 0A62:65 FF             ADC $FF
 0A64:85 FB             STA $FB
 0A66:BD 00 07          LDA $0700,X ; source tile byte
 0A69:CA                DEX
 0A6A:91 FA             STA ($FA),Y ; dest HGR 7-pixels
 0A6C:38                SEC
 0A6D:68                PLA
 0A6E:E9 04             SBC #4      ; move to previous HGR scanline
 0A70:10 EE             BPL draw8   ; $0A60
 0A72:38                SEC
 0A74:E9 80             SBC #80     ; adjust dest scanline ptr for top
8
 0A76:10 E0             BPL doHalf  ; $0A58
 0A78:C8                INY         ; move to next HGR column
 0A79:CE 68 0A          DEC TileAdrH; move to source tile prev column
data
 0A7C:AD 68 0A          LDA TileAdrH; $A68
 0A7F:C9 08             CMP #08     ; done even source col of tile?
 0A81:10 D1             BPL doTile  ; $0A54
 0A83:E6 D6             INC $D6     ; TileX
 0A85:C0 28             CPY #28     ; Drawn all 40 columns?
 0A87:D0 8F             BNE drawCol ; $0A18
 0A89:E6 D7             INC $D7     ; TileY
 0A8B:E6 FF             INC $FF     ; HGR.BASE.HI = constant delta $1
every 16 scanlines
 0A8D:A5 FF             LDA $FF
 0A8F:C9 24             CMP #24     ; drawn 64 scanlines?
 0A91:F0 03             BEQ done64  ; $0A96
 0A93:4C 0F 0A          JMP drawRow ; $0A0F
 0A96:18         done64 CLC
 0A97:A5 FE             LDA $FE     ; HGR.BASE.LO
 0A99:69 28             ADC #28
 0A9B:85 FE             STA $FE
 0A9D:C9 78             CMP #78     ; done all 192 rows?
 0A9F:F0 03             BEQ vehicle ; $0AA4
 0AA1:4C 0B 0A          JMP drawTriad ; $0A0B

 0AA4:A9 28     vehicle LDA #28     ; y=#50 + x=#12 = $2128 + $12 =
$213A
 0AA6:85 FE             STA $FE
 0AA8:A9 21             LDA #21
 0AAA:85 FF             STA $FF
 0AAC:A0 12             LDY #12     ; start $12 columns over (center
of screen)
 0AAE:A5 08             LDA $8      ; PlayerVehicle
 0AB0:09 0F             ORA #0F     ; Tile.Height = 16
 0AB2:85 09             STA $09     ; CurTile
 0AB4:A9 09             LDA #09     ; start with even column of source
TileAddr.Hi
 0AB6:8D CD 0A          STA vehTile+2
 0AB9:A9 80     vehTile LDA #80     ; start HGR address Low bottom 8
scanlines
 0ABB:A6 09             LDX $09
 0ABD:48        vehHalf PHA
 0ABE:18                CLC
 0ABF:65 FE             ADC $FE
 0AC1:85 FA             STA $FA
 0AC3:A9 1C             LDA #1C     ; start at dest bottom 8 scanline
 0AC5:48        vehRow  PHA
 0AC6:18                CLC
 0AC7:65 FF             ADC $FF
 0AC9:85 FB             STA $FB
 0ACB:BD 00 07          LDA $0700,X
 0ACE:CA                DEX
 0ACF:91 FA             STA ($FA),Y
 0AD1:38                SEC
 0AD2:68                PLA
 0AD3:E9 04             SBC #4
 0AD5:10 EE             BPL vehRow  ; $0AC5
 0AD7:38                SEC
 0AD8:68                PLA
 0AD9:E9 80             SBC #80
 0ADB:10 E0             BPL vehHalf ; $0ABD
 0ADD:C8                INY         ; move to next HGR column
 0ADE:CE CD 0A          DEC vehTile+2
 0AE1:AD CD 0A          LDA vehTile+2
 0AE4:C9 08             CMP #8      ; done even source col of tile?
 0AE6:10 D1             BPL vehTile ; $0AB9
 0AE8:60                RTS

AppleWin Symbols
 DB PlayerX       6
 DB PlayerY       7
 DB Vehicle       8 // player vehicle sprite #
 DB CurTile       9
 DB TileX        D6 // = (PlayerX - 9)
 DB TileY        D7 // = (PlayerY - 5)
 DB HGR.DST      FA
 DB MAP.PTR      FC // Tile from world to fetch
 DB HGR.BASE     FE
 DB SpritesODD  800
 DB SpritesEVEN 900
 DB Map        1000 // 64x64 $1000..$1FFF

 SYM DrawMap    = A00
 SYM drawTriad  = A0B
 SYM drawRow    = A0F
 SYM drawCol    = A18
 SYM clipWin    = A4E
 SYM doTile     = A54
 SYM doHalf     = A58
 SYM draw8      = A60
 SYM TileAdrH   = A68
 SYM done64     = A96
 SYM vehicle    = AA4
 SYM vehTile    = AB9
 SYM vehHalf    = ABD
 SYM vehRow     = AC5
 SYM VehAdrH    = ACD

And
  reg Y = 0 .. $28 (column to draw)

Example Water Tile:
 Row  Hi-Res Data   Source
 [00] $2000: 80 80 ($900 $800)
 [01] $2400: 80 80 ($901 $801)
 [02] $2800: D0 A0 ($902 $802)
 [03] $2C00: 85 8A ($903 $803)
 [04] $3000: 80 80 ($904 $804)
 [05] $3400: 80 80 ($905 $805)
 [06] $3800: 85 82 ($906 $806)
 [07] $3C00: D0 A8 ($907 $807)
 [08] $2080: 80 80 ($908 $808)
 [09] $2480: 80 80 ($909 $809)
 [0A] $2880: 90 A8 ($90A $80A)
 [0B] $2C80: C5 82 ($90B $80B)
 [0C] $3080: 80 80 ($90C $80C)
 [0D] $3480: 80 80 ($90D $80D)
 [0E] $3880: C1 82 ($90E $80E)
 [0F] $3C80: 94 A8 ($90F $80F)


5. Monster Sprites
= = = = = = = = =

NPC Sprites
 HGR
 BLOAD OUT.SHAPES,D1,A$B00
 SCALE=1:ROT=0
 POKE 232,0:POKE 233,11
 NEW
 HCOLOR=3:DRAW 1 at 0, 0:DRAW 2 AT 14, 0:DRAW 3 AT 28, 0
 HCOLOR=3:FOR Y=16 TO 47:HPLOT 0,Y TO 41,Y:NEXT
 HCOLOr=0:DRAW 4 AT 0,16:DRAW 5 AT 14,16:DRAW 6 AT 28,16
 HCOLOr=0:DRAW 4 AT 0,32:DRAW 5 AT 14,32:DRAW 6 AT 28,32
 HCOLOR=3:DRAW 1 at 0,16:DRAW 2 AT 14,16:DRAW 3 AT 28,16

Partial Annotated Ultima 1 (original) Draw NPC Data
 B00:06 00     // 6 Sprites
 B02:0E 00     // #1 @ B0E OR mask Sea Serpent  - drawn over water
 B04:35 00     // #2 @ B35 OR mask Mage         - drawn over land
 B06:6D 00     // #3 @ B64 OR mask Ent          - drawn over forest
 B08:82 00     // #4 @ B82 AND mask Sea Serpent - drawn over water
 B0A:CF 00     // #5 @ BCF AND mask Mage        - drawn over land
 B0C:2C 01     // #6 @ C2C AND mask Ent         - drawn over forest

Tiles:
 $00 Water
 $10 Land
 $20 Forest

AppleWin
 DW NPC.SPRITES B00:B0D

Sprites used by:
  FILE: OUT MOVE
  LINE 1211 T1 = maptile(x,y)/16
    T1 = 0, 1, 2
  LINE 1213 draw map: if T1 > 2 T1 = 1
  LINE 1214
     HCOLOR=0:DRAW T1+4 AT (9+DX)*14,(5+DY)*16
     HCOLOR=3:DRAW T1+1 AT (9+DX)*14, 16*(DY+5)
  Note: The player/vehicle/avatar is always at ScreenTile 9,5.


6. Bootstrapping
= = = = = = = =

If you are using the cracked disk image
You will need to patch the Applesoft
program so you can view the BASIC source.
 LOAD INTRO
 806:0D
 89B:0D
 LIST

 RUN ULTIMA
 RUN INIT DISPLAY,D1
     LOMEM:30730
  IN = -1
     BLOAD CUR.PIC
     BLOAD B.CUR,A$B00
  IN = -2
     BLOAD CUR.PIC
     BLOAD B.CUR,$B00
  IN <> -3
     BLOAD ULTSHAPES,A$800,D1
     BLOAD DRAW 64.OBJ
     BLOAD TWN.CAS.SHAPES,A3700
  RUN OUT MOVE,D1



7. Applesoft Variables
= = = = = = = = = = =

Note: Applesoft only uses the first 2 characters
of a variable name. FA and FACE all
refer to the same variable.

 FILE: OUT MOVE

  10000 Load World Map
    Variable: FACE = which world
   PRINT D$;"BLOAD BTERRA";FA:
    BLOAD BTERRA0,A$1000
    BLOAD BTERRA1,A$1000
    BLOAD BTERRA2,A$1000
    BLOAD BTERRA3,A$1000

 Line 2100 of Gets the tile# with
  INT( PEEK (4096 + TY*64 + TX) /16) );


 Line 2150 updates the world map with the vehicle
  2150 RED(3) = T-8:
     POKE (4096+TY*64+TX),
     (PEEK(4096+TY*64+TX) - INT(PEEK(4096+TY*64+TX)/16)*16) * 16:
     GOSUB 1299: RETURN
Not a bug, but an perfect example why Applesoft is slow...
..  because there is no built-in MOD function.
 Context: Exit vehicle (Line 2100 = Board Vehicle/craft)
What this code does: multiply map tile * 16
i.e. If MapTile(x,y) = $AB
 Then MapTile(x,y) = $B0
i.e.
A = 33:PRINT (A-INT(A/16)*16)*16
FOR H=0 TO 15:FOR L=0 TO 15:B=H*16+L:PRINT B;" -> ";(B-INT(B/
16)*16)*16:NEXT:NEXT

For speed the code should of only set $08


 If you board a space machine this Applesoft program is run:
   2135 RUN SPA MOVE
 Enter town
    500 RUN TWN MOVE
 Enter castle
    520 RUN CAS MOVE
 Enter Dungeon
   530 RUN DNG MOVE
 Draw map
   91  ... GOSUB 1297 ...
   1213 CALL 2560
   1299 FOOD -= (7 - RED(3)) / 14
        TIME += (7 - RED(3)) / 7
        POKE 6,XX: POKE 7,YY: POKE 8,(RED(3)+8)*16: CALL 2560

Main Loop
 600 Print Player Stat Summary, Life, Food, Experience,Gold
 800 "COMMAND:", check for keypress
 840 "PASS" -- Time-out after 2 seconds
 1000 main loop
      Check if Life<0 Food<0
      ZZ = 141 $8D return = up arrow
      ZZ = 175 $AF /      = down arrow
      ZZ = 149 $95 ->     = right arrow
      ZZ = 136 $88 <-     = left arrow
      Call 1200, jump 1040
 1030 Handle normal key input - jump table
 1040 call 6000, jump 1000
Commands:
 2000  A = Attack
 2100  B = Board craft
 2200  C = Cast (readied spell)
         2210  0="POTENTITIS-LAUDIS", 33% for which random effect
           i) banish all monsters!
           ii) If almost dead (life < 10), set life = 10
           iii) If almost starving (food < 10), set food = 10
         2290 1, 2=Dungeon Only
         2220 3="DELICIO-ERU-U", magic-missile; dmg=C(5) +2 *
(7<weapon<12)
         2290 4, 5, 6, 7, 8, 9 Dungeon
         2230 10="INTERFICIO-NUNC", attack monster
 2300  D = Drop = n/a
 2400  E = Enter town/castle
 2500  F = Fire vehicle weapns.  20% chance to miss.
           Vehicle: 4 = Cannons
           Vehicle: 5 = Lasers
 2600  G = Get = n/a
 2700  H = n/a
 2800  I = Inform and search
 2900  J = n/a
 3000  K = n/a
 3100  L = Lag Time (0-9)
 3200  M = n/a
 3300  N = n/a
 3400  O = Open
 3500  P = n/a
 3600  Q = "Quit & Save Game"
   BSAVE BEVERY,A$7800,L$1E01
   BSAVE BTERRA,A$1000,L$1000
 3700  R = Ready Weapon, Armor, Spell
 3800  S = Steal -- n/a outdoors
 3900  T = Talk -- n/a outdoors
 4000  U = Unlock
 4100  V = n/a
 4200  W = n/a
 4300  X = Exit vehicle
 4400  Y = n/a
 4500  Z = Inventory
 6000 Combat - attacked by mob
 6400 Dead state

Variables (some initialized in Line 280)
 FACE - which land/world to enter next
   Called on movement
      1215: IF TX + DX >  75 THEN GOSUB 980, 900, 990: JUMP 1297
      1216: IF TX + DY < -12 THEN ...
      1217: IF TY + DY >  75 THEN ...
      1218: IF TY + DY < -10 THEN ...
   0  900: FACE++:  902: TX = -10
   1  920: FACE--:  922: TX =  73
   2  940: FACE-2:  942: TY = -10
   3  960: FACE+2:  962: TY = 73
   World saved at 985

 RED(0) = Spell Readied
 RED(1) = Weapon Readied
 RED(2) = Armor Readied
 RED(3) = Vehicle Boarded
 G1 = # of Red Gem
 G2 = # of Green Gems
 G3 = # of Blue Gems
 G4 = # of White Gems
 NA$ = Player name
 SPE() = # of each spell

 C(0) = Player Life/Health, Init = 100, Line #720
 C(1) = Strength
 C(2) = Agility
 C(3) =
 C(4) =
 C(5) = Wisdom
 C(6) = Intelligence
 C(7) = Player Gold, Init = 100, File: INIT DISPLAY, Line #720
 C(8) = Race, INIT DISPLAY, Line
   1 c(6) + 5
   2 c(2) + 5
   3 c(1) + 5
   4 c(5) + 10, c(1) - 5
 C(9) = Type, INIT DISPLAY Line 630
   1 c(1) + 10, c(2) + 10
   2 c(5) + 10
   3 c(6) + 10
   4 c(2) + 10

Race   Attribute Bonus
Human  +5 Intelligence
Elf    +5 Agility
Dwarf  +5 Strength
Bobbit +10 Wisdom; -5 Strength

Class/Profession
Attribute Bonus
Fighter +10 Strength; +10 Agility
Cleric  +10 Wisdom
Wizard  +10 Intelligence
Thief   +10 Agility

 FOOD = Player Food
 EPNT = Player Experience
 AN = Monster Count!
 AM = Monster Type
 MN$()
  6 = Ness Creature
  7 = Giant Squid
  10 = Hood
  12 = Hidden Archer
  19 = Evil Ranger
  20 = Wandering Warlock
  30 = Gelationous Cube
  33 = Lizard Man
  32 = Mimic
  39 = Wraith
  41 = Invisible Seeker
 TDT
  44 = The Souther Sign Post
 FO  =  ?
 TX = PlayerX
 TY = PlayerY

 Player disk name, File: INIT DISPLAY, Line 760, has typo??  PRINT NANE
$
   OPEN INFO
   WRITE INFO
   PRINT NANE$;" DISK "; CHR$(48):
   CLOSE INFO
 Player disk name read, Line 10000
   OPEN INFO
   READ INFO
   INPUT Q$
   CLOSE INFO

   TIME = 0
   WRLD = 0
   FACE = 0
   EPNTS = 1
   INOUT = 0
   LEV = 0
   TX = 40
   TY = 40
   PX = 0
   PY = 0
   DX = 0
   DY = 0
   FOOD = 100
   PW(1) = 2
   ARM(1) = 1
   RED(1) = 1
   RED(2) = 1

8. BUGS
= = = =

Death: When you die you will be put to a random map
 location.  Sometimes you will be stuck in the middle
 of the ocean unable to move.  (Guess swimming was
 not invented yet.)
FILE:OUT MOVE
LINE:6400
6400 PRINT NA$;", YOU ARE DEAD.":
    PRINT "ATTEMPTING RESURRECTION!":
    FOR X=1 TO 15: PW(X)=0: NEXT:
    IN=0: RE(1)=0: RE(3)=0: C(7)=0: FO=20:
    TX = INT(RND(1)*64):
    TY = INT(RND(1)*64):
    C(0) = 99: AM = 0
6402 FOR X=1 TO 2000:NEXT:GOSUB 600:GOSUB 1297:GOTO 1000

Add this line:
6401 TX = INT(RND(1)*48)+8: TY = INT(RND(1)*48)+8:
     T  = INT(PEEK(4096+TY*64+TX)/16):IF T=0 OR T=3 THEN 6401


9. Misc.
= = = =
Since the original came out in 1981 does this make
these notes the oldest fan-made patches for a 31 year
old game? =)