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 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 ...