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? =)