Loading listing…
1988 · Electronic Arts
Every routine, table and variable the game uses, with the names and descriptions given to them. Click an operand to follow it; the counter beside a label lists what refers to it. The reference below the fold is rendered from the game's facts file.
This game is in 64 parts, and the same addresses hold something else in each. Each part has a listing of its own, chosen from the list beside it.
The resident engine, the code every program of the game calls. It holds two tables of jumps, the routines and the engine's own text at $0200-$30FF, the sector buffer and a page of tables at $5A00-$5BFF, and the font with the track and key tables and the sound player at $C600-$C9FF. Under the I/O area it has the map window's scroll copy, $DD80-$DDFF. $EE00-$FBFF is work space and the game state, of which it saves $F400-$FBFF, and $FC00-$FFFF is the fast loader with the hardware vectors. It stays in memory from the boot onward, and every program is loaded beside it. What spans the parts (the memory layout, the loader's disk format, the save, the text format, the screen and the controls) is in the game's facts.md. This file holds what is true inside the engine. Addresses are the engine's own unless a part is named.
$0200-$30FF is side 1's T34/L10 and the 46 sectors that follow it as the loader steps, 47 pages, byte for byte at the hand-over (entry.vsf).2.0 carries the page at $5B00 at its own $C17B, and the scroll copy at $DD80 at its own $C27A, byte for byte.$C600-$C92F: 102 glyphs for codes $20-$85, eight bytes each. A code c is drawn from $C600 + 8 × ((c − $20) AND $7F) (draw_char_advance $1F86, draw_glyph $2217), so a key code with bit 7 set draws the same glyph as the code without it. After the font come the track table $C930, the key table $C953-$C992, play_sound $C993-$C9BB and its sound table $C9BC-$C9DC. The 35 bytes $C9DD-$C9FF are read only by play_sound's indexed loads with an X above 22, which no caller passes.$0524-$053A) reads the unpacker from T35/L16 over $C800-$C8FF, calls it at $C800 and reads T35/L11 back over it. That page holds glyphs $60-$7F, the font's third page; glyphs $80-$85 at $C900 stay in place.$CD (at $C834), uses $CE and $E1-$E5, and never reads $CC.$0202-$044E: 181 three-byte JMPs from $0204, and 46 bytes between them that are not entries.$0215, $0216, $021D, $021E, $0225, $0226, $022D, $022E, $0235, $0236, $0245, $0246, $024A, $024B, $0252, $0253, $025A, $025B, $0262, $0263, $026A, $026B, $0272, $0273, $027A, $027B, $0282, $0283, $028A and $02AA.$028B, newline_wait_count $0292, scroll_step_count $0293 and write_page_count $029B.$0202-$0203, $020D-$020E, $023D-$023E, $029A, $02A2-$02A3 and $0446-$0448. The only instruction in any part's code that names $02A2 is the BIT $02A2 at $1874, which hides an LDX #$02.$0200-$0201 holds $29E4, the address of the engine's text block.$04CF-$0504: 18 JMPs with no gaps. $04F6 and $04F9 are the NMI and IRQ entries that the RAM vectors at $FFFA and $FFFE name; no JSR or JMP in any part goes to them.$02CC and $02D5 to check_side_1 $1895, and $0332 and $033B to view_character $0DFC.JSR or JMP in the code of any part: $0257, $026C, $0277, $027C, $0294, $02C0, $02CC, $02D2, $0332, $0368, $0395, $03B3, $03D7, $0431, $0443 and $0449. A byte scan of every part finds 20 CC 02 once, inside map 26's text block (map-26 $53EE). All their routines but do_nothing $2421 (entry $0257) are used inside the engine, called or fallen into.$24D6 takes random_byte AND 7, throws again on 6 or 7, and adds 1.$24E3) makes each byte from the last one and the raster line and timer, which move a fixed number of cycles between two calls, so a draw that roll_d6 takes again at once ($24DD) follows from the one it threw away: after a draw of 7 the die comes out 2 or 3 39.6% of the time, and after a 6 it comes out 1 18.2% of the time and is thrown again 18.2%.$0845) was a double 20.39% of the time and under 5 13.07% (fair dice: 16.67% and 11.11%).facts.md).$24C4 takes random bytes until one is below A, then adds 1: 1 to A. With A = 0 it returns 0.$0845 adds two d6. While the two dice are equal it throws two more and adds them. The total is eight bits; X returns 0.$0867 gives 15 + 5X in A (low) and X (high).$0C29 takes A = skill and X = difficulty and works to the target 15 + 5X.$0C6A, is the open 2d6, plus the attribute that skill_attribute_table $0DD8 names for the skill, plus 4 × the skill's level (skill_level $1392, times_four $087D).$0C73-$0C75). Otherwise the dice alone, not the roll, are added to experience, pass or fail ($0C41-$0C49), and a roll at or above the target passes and goes on to skill_improve_roll ($0C4C-$0C57).$0DD8, a record offset per skill; entry 0 is 0):| Attribute | Skills |
|---|---|
ST (+$0E) | 5 Pugilism, 7 Swim |
IQ (+$0F) | 9, 20, 22, 24, 25, 27, 28, 30, 31, 32, 33, 35 |
LK (+$10) | 1 Brawling, 14 Gamble |
SP (+$11) | 17 Combat shooting |
AGL (+$12) | 2 Climb, 13 Acrobat, 16 Silent move |
DEX (+$13) | 3, 4, 6, 8, 10, 11, 12, 15, 19, 21, 26, 29, 34 |
CHR (+$14) | 18 Confidence, 23 Bureaucracy |
$0CA2 acts only when the skill's level is at least 1, below the rank (+$24) and below the difficulty X ($0CA8-$0CB8). Then a d10 of at most (X − level) / 2 + 1, halved down, raises the level by one ($0CBA-$0CE9).$0CD8-$0D00).$0CD2-$0D06).$0D0A takes A = value and Y = a record offset.$18, the sex, passes when the byte equals A.$24, the rank, passes when the rank is at least A.$0D18-$0D4B).$0D61 takes A = the asker's value and Y = an offset in the selected character, its refusal byte.$FF always refuses.$0DB4, which leaves exactly 10 alone). A roll below the target refuses and leaves the byte.$0DAA, never below 0).$0DC0 is 4 × the level of the skill named by the weapon's item record byte 5. With no weapon it uses item 0, Fists, whose byte 5 names skill 5, Pugilism ($0DC3).$14FD gives no experience: it rolls without add_experience ($1520-$1539).$F400 + 256*n*. select_character $1201 points $66/$67 at it and copies its sex byte to $BB; select_member $11F7 does the same for party position A of the current party.| Offset | What | Where |
|---|---|---|
+$00 | the name, ended by 0 | print_name $173A |
+$0E-$14 | ST, IQ, LK, SP, AGL, DEX, CHR | print_attributes $10EE |
+$15-$17 | cash, 24 bits | cash_add $1366 |
+$18 | sex: 0 male, 1 female | $120A, $0FD5 |
+$19 | nationality, 0-4 | $0FE4 |
+$1A | armour class | equip_toggle $10C9 |
+$1B-$1C | MAXCON | con_is_full $1493 |
+$1D-$1E | CON, signed | con_subtract $088E |
+$1F | weapon slot, 0 for none | $1276 |
+$20 | skill points | $1113 |
+$21-$23 | experience, 24 bits | add_experience $0825 |
+$24 | rank | $0CAD |
+$25 | armour slot, 0 for none | $1273 |
+$28 | the disease bits, 0 for none (module-0's facts); any set prints MAXCON in reverse | $16AC |
+$29 | not 0 for a non-player character | $0F63 |
+$2E | refusal byte for trading | $0F7F |
+$32 | the rank's name | $111B |
+$80-$BB | 30 skill pairs: skill, level | skill_pair_offset $14F3 (2A + $7E) |
+$BD-$F8 | 30 item pairs: item, count byte | item_slot_offset $14F8 (2A + $BB) |
$0FC5 opens the text window and prints messages 137 (the name), the attributes, 138 with 162 + sex (Male, Female) and 139 with 145 + nationality (U.S., Russian, Mexican, Indian, Chinese).$10EE takes no argument: its first instruction is JSR flush_line. It prints ST to CHR, SKP (+$20) and the rank's name from the labels at $1152.$0825 adds $1C-$1E and holds at $FFFFFF; cash_add $1366 does the same for cash.$17E9: carry clear only for CON above 0.$08A5 adds 1 to CON and returns carry clear when CON then equals MAXCON (con_is_full $1493), set otherwise.$1475 names the state of a CON of 0 or less, for the roster's CON column (roster_con_field $16CD, words at $16FA).$7F.$148E: −1 to −10 UNC, −11 to −19 SER, −20 to −29 CRT, −30 to −39 MRT, −40 to −256 COM (messages 133, 154-157). A CON below −256 is judged by its low byte, as if it were 256 higher.$08B4 counts the conscious members from position $07 down to 1. With $07 = 0 it makes 256 passes, position 0 and then 255 down to 1, reading far past the party's 14-byte table.$3108 + 8*n* (item_record $0BCD). Its name is message 36 + n (print_item_name $0BDC), from 36 Fists to 130 Cash, item 94.$144A is byte 3 shifted right three times.$1454 is byte 3 AND 7, through the word table at $1469: 0 is an RTS ($1474), 1 is screen_shake $0746. The table holds only those two words; 2-7 would take the code bytes from $146D on as addresses.$08D0 (the list $08E8) counts classes 2-13 as ranged. equip_toggle $10A2 treats class 15 as armour.60 60 60 00 37 08 F8 39.$80 (game $A5B5-$A5BB).$14A5 takes Y = an item pair. A count of 0 is not counted down. The last charge unequips the slot and clears the pair ($14C9-$14D7). Either way the item's effect runs (item_effect_of $1451).$1281 refuses a jammed weapon with message 152, "Your weapon is jammed." Otherwise it clears the clip's pair and sets the count to (count AND $C0) OR the weapon's record byte 4, so rounds left in the weapon are lost ($128F-$12A4).$14FD takes Y = the weapon's pair.+$0F) + weapon_skill_bonus. Dice below 5 fail.$153E-$1548).$85 ($154F-$155F).$0EA6 offers what fits the slot.$0EC2-$0ECD).$7C (message 134). get_key_not_n $1562 sets carry only for N and left-arrow, so any other key tries to unjam ($0EDD-$0EE8).$0EF6).$0F04 clears the pair, then unequips the slot.$10A2 unequips a slot that is the weapon or the armour. A class 15 item becomes the armour (+$25) and sets the armour class +$1A to its record byte 6; any other item becomes the weapon (+$1F).$10D8 clears +$1F, or +$25 and +$1A.$1268 gives item 0's record, Fists, when no weapon is equipped ($126B-$1270). Item 0's byte 7, its clip, is 0, so with no weapon no slot is offered Reload ($0EB5-$0EBB), and the game's L)oad finds no clip (game $A39C-$A39F).$0F1F:$0F2F-$0F32). With more, message 144, "Who wants it?", and pick_member, asked again while it names the giver; left-arrow leaves ($0F44-$0F4E).+$29 set who is conscious must pass npc_obey_check with the taker's CHR against its own +$2E, or the trade stops with message 135, "<name> doesn't want to trade." ($0F63-$0F85).$0F88-$0FA8).$1211 counts the occupied item pairs, and count_items_of_classes $1214 those whose class is in the list at $9D. Both return X = the count and $D1 = the last such slot.$124A and find_item $124C return Y = the pair, or Y = $FF with carry set when there is none.$1060) offers slots 1-30 ($4D = $1F). print_item_row $11C6 shows the equipped weapon and armour in reverse.$1005):$5A00-$5A3B and moves each picked pair to the next place in it (move_pair_to_buffer $143B, which zeroes the source). There is no way out until every item is picked ($1015-$1021).$1026 and $102C to +$1F and +$25 and copies the 60 bytes back to +$BD-$F8 ($1023-$103A).$103E-$104F). They take the count kept at $103F, which goes up on every pass, including the passes on which RETURN, space, R or left-arrow came back instead of a pick ($1015, $101F-$1021).$13B7), copies the 30 skill pairs to $5A00-$5A3B first and writes the picks over that copy from its start ($13B7-$13C2, $13E2-$13F0). A place the picks do not reach keeps the copied pair. Left-arrow does not leave: only the empty list does ($13CB-$13D1).$13F5) scans 31 pairs, +$80-$BD, and lists a pair whose skill byte is from 1 to $23.$1768 points $68/$69 at $F400 + 0, 14, 28 or 42 for parties 0-3 ($1778).$1759 counts the positions from 1 that hold a record, up to the first empty one: A = the count and Y = the first free position, 8 for a full party.$174C stores the record at Y and increments $07, with no check for room. A full party gets the record in its table's byte 8, and $07 becomes 8.$177C takes the keys 1 to the party size $07; left-arrow gives 0.$1795 searches tables 0-3 in turn for a record, and returns 3 both for party 3 and for a record in no party.$17F8 takes a position R in the current party.$171B copies record A over record X, and does nothing when X = 0), closes up positions R+1 to 7, and decrements $07 and $0A.$0B down to 0 it lowers by one each entry at or above the removed record ($1835-$1854), and leaves $68/$69 on table 0.$157E moves every other member's cash to member A, adding with saturation at $FFFFFF.$0E2A) divides the viewed member's own cash, not the party's, by the party size. Every member gets the quotient and the viewed member also gets the remainder (divide_24_by_8 $06E8).$17CA sets $6A and draws the roster. The header, message 136, is on row 15 ($15FC-$1605), and position n is on row 15 + n (roster_line_cursor $1857).$1664-$16F7).$167C, $16B9; print_bcd_field $1CB1 prints the last digits only), so a MAX or CON of 1,000 or more shows only its last three digits.$1685-$16A9).$1700).$0953 takes X = column and Y = row; an even column is the high nibble.$63, by shifting: the row times half the width's lowest set bit, which is half the width for a power of two from 4 up, the maps' 32 and 64.$0962-$0968).$098E starts the number layer at height × width / 2. set_class_at $0A57 stores the class nibble without masking the value.$0A00 takes A = an offset in the record at ($5F); the class byte and number byte there replace the square at X, Y.$AD/$AE before it writes.$FE writes the square held in $AD/$AE instead, so the map's square and the one held trade places ($0A28-$0A30); $FD does the same and returns carry clear.$0A06-$0A56).$08F5 returns, for the square at X, Y, the entry of distance_table $0921 at 10 × |dy| + |dx|, with no bound on the index.$0B53 draws the square at X, Y.+$33.$7F.$9F to its distance from the party when that is smaller ($0B74-$0B7E), and draws tile 8 when the hour $0E is below 6 or 18 and above, its ground otherwise ($0B80-$0B8A; live, the game's facts, "The maps").$0B9D-$0BAB).$0B2B gives column 2 × (X − $02) and pixel line 16 × (Y − $03).$22EA draws the 2 × 2 cells of tile n from $D000 + 32*n*, with its colours from $DC00 + 4*n*. It draws only cells in columns 1-36 and lines 8-135.$0AA1 fills columns 38-39 from row 17 up to row 0, two glyphs a row.$9F/8 rows are glyphs $80 and $82 in colour $62, and the rest glyphs $7E and $5B in the text colour $EB. A $9F of 144 or more gives no rows of the first kind ($0AB5-$0AC8).$80/$82 are in %10 pixels, the colour byte's low nibble; $7E/$5B have %01 edges round a %11 core.$25BD does nothing for the map already in memory ($91). Otherwise:$2856) and loads the new one's pages (load_map_pages $2603).$54), height and width ($55, $56) to the header's +$2C, and the party's place in the window to column 9, row 4 ($59, $5A). It clears $D2, unpacks the map's stream to $DE00, and marks the class row cache empty.+$30) when it differs from the one loaded, kept in the operand at $25F1, and ends with JMP save_game.$2603 reads T35/L8. The header is at $4C00 when that sector's byte for the map is $40, and at $3A00 otherwise. A map number of $80 or more is replaced by that sector's byte at that index ($261C-$2624); for such a number the header is at $3A00.$2631 does nothing when picture A AND $7F is already in memory ($0505); otherwise it unpacks the picture from the stream directory T35/L10 and returns carry set.$E000, limit $F000.$CA00, limit $D000, and marks the module cache $26BC empty.$AC at 0 during the unpack, so the animation does not run ($2666-$2675).$2691 unpacks module A from T34/L16 to $CA00, limit $D000, and marks the picture cache $0505 empty. It skips the load when A equals $26BC, but the only store to $26BC in any part is the $FF at $2644, so a module is unpacked on every call.$26E7 unpacks the map's stream (T35/L13) to $DE00, limit $EE00, with all RAM in.$BA is $DE. The stores to $BA found are engine $2680 ($CA or $E0), module-4 $CCB3 ($34) and startup $7FE2 ($8A).$80 or more is looked up in T35/L8 as in load_map_pages ($26F2-$26FE).$2722 unpacks tile set $09 (T35/L12) to $D000, limit $DE00, with all RAM in. It pushes the 128 bytes of $DD80 on the stack first and pulls them back after ($2725-$274C), since the limit would let the set run over them.$2759 finds a stream's first sector by stepping a count of logical sectors from the base in $5AFE/$5AFF (next_logical_sector $FD0B). Then it calls unpack_stream with the start byte and the destination.$2790:$27C1-$27C8).$26BD loads item file $75 to $3100 when it is not the one in $76. save_item_table $26D6 writes back file $76.$27E8 keeps $FD-$FF for its retries ($27F1-$27FD). It prints "Write protected" for loader status $2B and "I/O ERROR" for any other, at column 0, row 23, then message 167, waits, redraws the menu bar and tries again.$284A reads a run of pages in one loader call. write_pages $2866 writes one sector per loader call, stepping the logical sector and the page itself.$2890:$28BD, a bar with no items ($03 $00).$5A00, loads the whole file to the address in its bytes 3-4 for the pages in its byte 5, and jumps through $5A03, to the file's first byte.$1897 redraws the menu bar after its prompt.$18EA is as the game's facts say. It writes pages 0 to 7 in that order and leaves $66/$67 on $FB00, record 7 ($1901-$1913).$1872 and write_save_page $1874 read or write record page A, T35 logical sector 7 − A, through disk_io.$055F stores the picture number in $AB, loads it (load_portrait), sets $AC to 1, draws the picture window and its caption, and starts the picture.$12CF.$F4D0, at column 1, row 13 ($0571-$057D).$0586 reads the picture's header at ($B9).$C1, $BF, $C2, $C0).$DA-$DD to 1 and draws the first frame stored, not XORed ($064E).$05C5 does nothing while $AC is 0. Otherwise:$C5 is set, it reloads the picture when a load has replaced it (load_portrait, then start_picture), and redraws the window when $B8 AND $DF is 0 ($05C9-$05D8).$B1/$B2 lets the channels move once in 512 calls ($05DA-$05E4).$5BFB and its script offset are not 0 and its countdown runs out ($05EB-$060B). Then it reads the next script entry: a delay and a frame offset; a delay of $FF restarts the script. The frame is XORed onto the bitmap ($0648).$0651 draws a frame into the bitmap at $6000 column by column.$065A, $06A8). Then come blocks of column, line and escape value; $FF as the column ends the frame.$06B3-$06C3).$2534), each scroll step 192 ($215F), and each slow-scroll pause round 3 ($210A).$0519 points $43/$44 at the engine's block, $29E4.$1E0E takes the group word n/4 from the table after the alphabet, then skips n AND 3 messages.$1F0B handles each character:$A1 set it prints nothing.$1F0F-$1F21). Then it calls the handler from print_controls_direct $1F42 or print_controls_wrapped $1F64: one word each for codes 0-15, and the 17th for every code from $10.$253A.$0A, the singular ending, $0A, the plural ending, $0A, as in message 37, "Ax{0A}{0A}es{0A}". A count $38 of 1 keeps the singular ending, any other the plural (filter_plural $2063).$2099). The three-way form keeps part $CA, 0-2 (filter_choice $20C3).$1F9D, code $0F, prints a count of 0 as 1.$06 (ctl_return_prompt $1FC3) prints "(RETURN)" at column 23, row 13 and waits for RETURN, space or back-arrow (wait_return $2501). Code $07 opens the text window ($12B0). Code $09 moves the cursor right by the next byte ($1FAE).$F49C ($1FEA). A line is drawn up to its last space, and the rest is carried to the next line (draw_buffered_line $200C).$12B0).$12CF), cleared in colour $26.$10-$6F (window_clear $1300, clear_window $22A9).$81 and $83 (top corners), $84 (sides), $85 and $7D (bottom corners) and $5C (top and bottom) (draw_frame $130B).$1E60 prints a message in the window of rows 18-22, pixel lines $90-$B7, columns 0-39. It goes on from the column and row where the last message ended, kept in the operands at $1E78 and $1E7C (column 0, row 22 in play-map.vsf). It holds $A8 at 0 and sets $B0 while it prints.$20F9:$B0 set it counts lines in $AF. At 6 it prints the bar's "(RETURN)" (bar_return_prompt $21E8, row 23, text $220A), whose code $05 waits for RETURN, space or back-arrow. read_key clears $AF on every key.$A8 set), each new line first pauses newline_pause_counts $21E1 rounds of delay_a(11) and three animation ticks. The scroll is then scroll_step_counts $21D3 steps of scroll_step_lines $21DA lines each, both indexed by the speed $0F.$213B).Speed $0F | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| pause rounds | 255 | 1 | 192 | 1 | 192 | 1 | 1 |
| steps × lines | 8 × 1 | 8 × 1 | 4 × 2 | 4 × 2 | 2 × 4 | 2 × 4 | 1 × 8 |
$253A works only while $A8 is set: $8A (CRSR down) lowers the speed number and $95 (CRSR right, or C=) raises it, within 0-6, and the key is taken. The game sets $A8 and the speed around its slow square messages (game $914D-$915D, $9144-$914C).$1975 reads a key.$19A4-$19BD).$88 (DEL), $8B or I shows the page before; $95, $8A or K the page after.$FF, the extra key in $BE returns $FE, and RETURN or space returns $FD, all with carry set ($1990-$19A2).$1A7A). Paging works when the count $4E is above 9 (list_fits_page $19E3), and the MORE line shows when it is above 10 ($1A94-$1A99). The engine's lists keep $4E one above the number of items ($1066, $13FC), so a list of nine can be paged with no MORE line, and the MORE line comes from ten.$1AB4 prints the line's digit and >, or _ for the marked item $D1.$1BBF:, and . move the selection back and on, wrapping. RETURN runs the selected item.$1BEC-$1C0A).$1C55 calls the handler and then redraws the bar.$1B36 draws the bar on row 23 in reverse, and the selected item in normal video.$1D06:_ with delay_a($30).$FF. DEL or $FF deletes. RETURN removes trailing spaces and returns 0.$A0 up, and stores them AND $7F.$1D93-$1D9C).$1D00 differs from input_line only in a flag that chooses between two identical JSR read_key ($1D2D-$1D35).$6F/$70) and num2 ($71/$72) are added to by num1_add_clamped $079D and num2_add_clamped $07C6, which hold the sum at $FFFF or at 0 by the sign of the high byte added. compare_num1_num2 $07FC returns carry set for num1 ≥ num2.$243B turns the 24 bits in $1C-$1E into eight digits in $1F-$22, by subtracting powers of ten.$1CB1 prints the last $30 of the eight digits, with leading zeros as the pad character $2E; print_bcd $1CA9 sets eight digits and no pad.$06E8 divides $073F/X/A by Y, 24 steps of shift and subtract, with the remainder in $D3. divide_16_by_8 $06E1 clears the high byte first.$2947 saves A, X, Y and $01, sets $01 to $35, acknowledges the VIC with LSR $D019, scans the keyboard only when that was a raster interrupt, reads $DC0D, and restores.$2967:$29D4.$29DC).$C953, DEL gives $88, RETURN $8D, CRSR right and C= both $95, CRSR down $8A, F1, F3, F5 and F7 $F1, $F3, $F5 and $F7, RUN/STOP $98, left-arrow $9B and SPACE $A0. £, HOME, ↑, CTRL and both SHIFTs give 0, which is not stored. No key gives $8B or $FF.$2514 makes a random byte and runs speed_keys on every poll. On a key it clears $AF and $B5. Then it waits delay_a(4) and gives one animation tick.$24F2 waits for a key and returns carry set for left-arrow. wait_return $2501 waits for RETURN or space, or left-arrow with carry set.$24E3 increments its seed, the operand at $24ED, and adds the raster line $D012, CIA 1 timer A's low byte $DC04 and the seed, with the carries; the sum is the new seed.$242B runs an inner loop A + (A − 1) + … + 1 times. A = 255 takes 166,022 cycles counting its JSR and RTS, and A = 0 returns at once.$C993, X = 0, 11 or 22:$D418 to $0F.$D406 down to $D400, bytes 7-9 to $D417, $D416 and $D415, and byte 10 to $D404.$C9BC:| X | Attack/decay | Sustain/release | Pulse | Frequency | Control | Called by |
|---|---|---|---|---|---|---|
| 0 | $1A | $00 | $07F3 | $2000 | $85: noise, gate | screen_shake $0746 (entry $044C, item effect 1) |
| 11 | $00 | $60 | $06A4 | $2000 | $A4: gate clear, so silent | play_sound_1 $241C (entry $0232) |
| 22 | $00 | $00 | $0000 | $1000 | $81: noise, gate | play_sound_16 $0741 (entry $04D2) |
$22/$4E, $D417 = $F0) route no voice through the filter.$0746 plays sound 0. Then twelve times it waits delay_a($80) and writes random fine scroll bits into $D011 and $D016, keeping their other bits as read (random_fine_scroll $077A). At the end it writes both registers back as they read at the start and silences voice 1 ($076C-$0776).2.0 writes $D011 = 0 and $D012 = 0 from its table at $C14C (its loop at $C126-$C135), and the start-up writes $D011 = $37 (startup $7F55, $7F91). In the 74 snapshots of all the parts, no other instruction stores to $D012 by an absolute or indexed address.$FF00 takes A = the command, X = the sector and Y = the track, and the buffer in $FE/$FF.$FD. Command 2 writes one. Command 3 or more only sends the command and takes the status.$01 = $35, and sets $34 around each byte stored to or read from the buffer ($FF30-$FF39, $FF68-$FF71), so a buffer under the I/O area works.$FC. loader_exit $FF87 restores $01 and compares the status with 1, so carry is clear only for status 0.$FD0B takes X = track and Y = sector, the other way round from loader_main. read_sector_xy $FD1D reads one sector to $5A00 through disk_io (the jump at $03AA), so its errors are retried.$FD2C sends bit 7 first on $DD00 bit 5, with bit 4 as the clock. loader_send_command $FD63 sends the command, the track and the physical sector.$FD9E waits for a raster line whose entry in the table at ($F6) is not 0, so no character row can stretch the timed read. Then it reads $DD00 four times, two bits a read, corrects with $F4 and decodes through the table at $FE00.$04F6, RESET $0000, IRQ $04F9 ($FFFA-$FFFF).$DD80 copies one character row of the map window for the game's scrolls (game $BE26, $BE44, $BE74, $BE90). It runs only with the I/O banked out.$BECC-$BF32, from the tables at $DDE4-$DDF0).$DD9D copies $20 groups of eight bytes, then two or four more (the operand at $DD9C, patched at game $BE4B and $BEC0): 34 cells for a scroll left or right, 36 for one up or down.$DDC9 patches the first index, the page step and the eight INY/DEY slots for a forward or backward copy.$DDFB-$DDFF are five zero bytes that only the save and restore around a tile-set unpack read.$1CFD, A9 FF 2C, would start input_line with digits only ($1D4C-$1D56). Nothing calls it.$1015-$104F):+$1A.$1515-$1517) cannot be reached from the $80 the game writes when a weapon jams: failures stop adding at $85, five tries.$F4, over page $F3 (copy_character with X = $FF), then records 2-7 down over 1-6.$FF ($1807-$1854).$0B3F-$0B48). Its lines are 16 × the row, so draw_square then draws only the view's top row. The game's own test is the same (game $AD3F-$AD45).$9F = 0 would draw its first kind 256 times, the row counting down from 17 and wrapping past 0: rows 17-0, then 255 down to 25, which are off the screen, then 24-18. Its 18 rows of the second kind would then cover rows 17-0 ($0AB5-$0AB7, $0AD2-$0B00).$9F found give it $FF (game $AF7E-$AF80, $B190-$B192; module-4 $CB20-$CB22; utils $7E18-$7E1A), the start-up's $FF and $40 down to 1 (set_gauge, startup $8149), and draw_square's distances, which are at least 2 for the squares of the view.$90-$AF (utils $8649), but nothing reaches it, and it copies zero page back before it ends.$D011 back with bit 7 as it read it. Bit 7 is the raster line's ninth bit when read and the interrupt line's when written. A shake that starts on raster line 256 or later leaves the raster interrupt on line 256 instead of 0, still once a frame.$8B and input_line's $FF are codes no key produces.INC $32 at $1E26, for a group word beyond the table's first page, is never reached: the word index is (n/4) × 2, at most 126.Current truth for this game. The workflow lives in kit/skills/; how this understanding developed lives in agent-history.md. Every fact names the routine or table it comes from. Unless marked live, a fact comes from reading the code in the snapshot named in orientation.md.
The game is in 64 parts (orientation.md, "The parts"): a resident engine, the programs loaded over it, nine tile sets and 42 maps. What is true of one part is in that part's own parts/<id>/facts.md, with that part's addresses. This file holds what spans the parts, and names the part beside every address it gives: "engine $25BD" is the engine's routine at $25BD.
PRODOS and 2.0 on sides 1 and 3 and the game's data in raw sectors on all four (orientation.md, "The image").$81AD), and its packed text "Electronic Arts and Interplay Productions proudly present" and "Copyright 1986-88 Interplay Productions." (startup $84BE, $8505).$514F) reads "A computer software poster reads, "WASTELAND!"" and lists programming, design, maps, graphics, general help and playtest, 26 credits to 20 people, ending "Rated PG-13. Coming soon to a theatre near you." It is one of four messages on one wall square (column 20, row 25, class 11), each bump showing the next: 91 "Coming soon, "The Radheads!"", 92 the credits, 93 and 94 two more films, then 91 again (live). Map 1's message 26 is a gravestone: "Ken St. Andre, 4/28/47--4/28/87." (map-01 $4300), shown on the graveyard's square at column 27, row 24 (live). The string sweep of every program part and the decoded text of every text block (the 52 blocks' 4,920 messages, 478 the programs' and 4,442 the maps') find no other credit: the start-up's lines above are the only others that name a maker.$7E06, against side 1's $7F2A; 2,766 of its 4,096 bytes differ), and its own boot sector, named "A JERKVISION PRODUCTION". Its credits read "Copyright 1986,87 Interplay Productions." where side 1's read "1986-88", and where side 1's start-up calls its title sounds, side 3's calls three RTS stubs ($7E03-$7E05) and writes no SID register. Booted, side 3 cannot run: its 2.0 loads side 3's track 34, logical sector 10 to $0200-$30FF, where side 1 has the engine and side 3 has other bytes, and the first raster interrupt after the hand-over goes through engine $04F9 into them and halts the processor on a JAM at $2931 (live). Run over side 1's engine instead, with side 3 in the drive, it shows a title picture it unpacks from side 3's track 3 and stops at "Computer defense initiative activated." (live, reference/side-3-start-up.png).$8600-$8FFF is the radio program's $7E00-$87FF, all 2,560 bytes.$874B-$87FF, and so also the Ranger Center's $8F4B-$8FFF, is the game's $874B-$87FF except two operand bytes ($87B4, $87B9), each 4 lower than the game's: a copy from a slightly different build of the game.$860B-$86FF is the engine's $0A0B-$0AFF.In play (parts/game/work/play-map.vsf), with $01 = $35: all RAM but the I/O area, both ROMs out.
| Range | What | Part |
|---|---|---|
$0000-$00FF | the engine's zero page; $02-$0F are saved with the game | engine |
$0200-$30FF | the engine: two jump tables ($0204-$044E, $04CF-$0504), its code and its text block ($29E4-$30FE) | engine |
$3100-$33FF | the item table: eight bytes an item | game |
$3400-$59FF | the map the party is in: class layer, number layer, record, lists, text and code | map-nn |
$5A00-$5AFF | the sector buffer | engine |
$5B00-$5BFF | tables and work space the boot file copies from its $C17B, among them the menu bar's text at $5BAB | engine |
$5C00-$5FFF | the screen matrix: the bitmap's colours | engine (output) |
$6000-$7DFF | the bitmap, 24 character rows | engine (output) |
$7E00-$C5FF | the program in play: the game, or the start-up, the utilities, the Ranger Center, the radio or the death screen | each its own part |
$C600-$C9FF | the font, the track table $C930, the key table $C953, the sound player $C993 and its three sounds | engine |
$CA00-$CFFF | a module, the party-order program, or a picture unpacked for the picture window | module-n, order |
$D000-$DD7F | the tile set, in RAM under the I/O area | tiles-n |
$DD80-$DDFF | the map window's scroll copy, in RAM under the I/O area | engine |
$DE00-$EDFF | the tile layer of the map, one byte a square, under the I/O area and above it | map-nn |
$EE00-$F3FF | cleared by the start-up (startup $7F3D-$7F55); the game keeps four 376-byte records from $EE00 (game $BBC7) | engine |
$F400-$FBFF | the game state: party tables, saved zero page, line buffer, character records from $F500, written to the disk as the save | engine |
$FC00-$FFFF | the fast loader's computer side, and the hardware vectors in RAM | engine |
The bitmap's 25th character row would be $7E00-$7F3F; with $D011 set to $37 (24 rows, startup $7F55-$7F57) it is hidden, except that screen_shake's random vertical scroll (engine $077A) brings its top lines into view for a moment, and the programs load from $7E00.
$FF00), installed by the boot file. It moves whole 256-byte sectors with no link bytes: a file is a track, a logical sector and a page count, and logical sectors run down from there.$FFD3, $FDEA, $FDD8, $FFE8).$C930 gives for that track (engine $FD0B). The table is laid out as if indexed by the track number less one, and is indexed by the track number: track 17 is entered at logical sector 0, track 24 at 17 and track 30 at 16, below their top sectors of 20, 18 and 17. Files are laid out on the disks by the same rule: maps 0, 2, 5, 11 and 19 cross one of those tracks, and each loads the sectors the rule names, not the ones a full track would give (the 42 maps loaded by the game's own enter_map compared with the image read both ways). The game writes through the same routine.$2790).$2759).$27C3).$C800 (glyphs $60-$7F, the lower case), from T35/L16 for each unpack and replaces from T35/L11 afterwards (engine $0524). T35/L11 holds the same 256 bytes as the third page of the font's own file, four pages from T34/L14 (engine $C800).$280C-$283E).$D7 $CC side on the masters, $00 $CC side on copies (orientation.md, "The play disks").$1897) reads T35/L8 and wants its byte $FD to equal $ED, its byte $FE to be $CC and byte $FF the side asked for; until they are, it prints "INSERT SIDE n. (RETURN)". $ED is 0 in play, so the masters are refused (live). The start-up sets $ED to $D7 only around the check of side 1 before it loads the utilities (startup $81E4-$81ED), which is how Copy reads the masters.$2856, from $25BD); a map entered by a number from $80 up (the generic maps 5 and 11) never is (engine $2858).save_game (engine $18EA) copies zero page $02-$0F to $F478-$F485, sums $F400-$FBFF into one byte (engine $1918: for each page and Y = 0-254, the sum plus the byte, then plus Y and the carry out of that first addition; the carry out of the second is dropped, and byte 255 of each page is left out), keeps the sum at $F4FF, scrambles bytes 0-254 of every page by EOR with (2*Y* mod 256) EOR the sum (engine $1944), writes page n to T35, logical sector 7-n, and unscrambles. The key is the byte at $F4FF, saved in clear. The start-up reads the eight sectors back, unscrambles them and compares the sum (startup $830E-$832D), but a sum that does not match is accepted: every byte is EORed with twice its offset again, which leaves it as stored EORed with the key alone, and the start-up goes on with no message (startup $832F-$834C). So a save written in clear with a key of 0 loads as written, unless the sum the start-up forms from it comes out 0. Live: member 1's cash set to 12,345 in play and saved with S and Y; the eight sectors read off the disk image afterwards are this rule applied to memory as the save began, all 2,048 bytes, with the key $6B the sum as the code forms it.save_game has no side check, so a save lands on the side in the drive. It is called by Save (game $8820, after asking for side 1), by Radio (game $8802, before asking for side 1), at the end of every map change (engine $2600), by modules 1 and 3, by the Ranger Center's Start and by the utilities.$3400: the class layer, a nibble a square, the high nibble for an even column (engine $0953); the number layer, a byte a square (engine $098E); then the map record, at $3A00 on a 32 x 32 map and at $4C00 on the four 64 x 64 maps, 0, 12, 22 and 26. From $DE00: the tile layer, a byte a square (engine $0B06), unpacked from the map's stream.$5C-byte header: address words for the packed text (engine $1E4F), the monster names and records (game $9E76, $9EA4), a list of records for each square class 0-15 (engine $09B8), the map's own routines (game $885C) and its non-player characters (game $A424); then the size (+$2C, engine $25D3), the chance of a random encounter after a step (+$2F, game $B017), the tile set (+$30, engine $25EE), the tile drawn off the map (+$33, engine $0B5E), what a step costs (+$34 and +$35, a fraction of a minute and minutes, and +$36, ticks of the elapsed count, game $AF32-$AF72), and 37 message numbers for the phrases of combat (+$37-$5B, game $BCE1, $BCE5).$ACF3, a table of 16 words) runs on the record that word number of the class's list names (engine $09B8): class 1 shows a message, 2 tests the party (skills, attributes, items, cash, party size), 3 and 15 are encounters, 4 shows another tile and a message, 5 is loot, 6 an action, 8 a question, 9 radiation, 10 an exit, 11 a wall that shows its message when walked into (game $915E), and 12 changes other squares. Classes 0, 7, 13 and 14 do nothing, and no map uses 7, 13 or 14.$0B80-$0B8A; live). Tiles 0-8, the party, the creatures, the loot bag and the sign, are the same bytes in all nine tile sets as they load (the nine sets compared byte for byte); maps 0 and 26 exchange tile 7's bytes with the jeep's or the train's for their rides (map-00 $4FCD, map-26 $4EF9).$A0, "ignore armour", to 1 (game $82E7), but the routine that applies the damage replaces it, for each member, with bit 0 of the square record's byte 0 (game $90AF-$90B4), which is the flag byte of the check squares it was written for and the message number of a radiation square. So armour counts on radiation squares whose message is even and not on those whose message is odd. The radiation records name messages 23 on map 0, 28, 29 and 30 on map 19, 69, 70 and 71 on map 20, 0 on map 26, 12 on map 31, and 43 and 0 on map 38 (live).$0A00). An action square's byte $80 + n loads module n to $CA00 and runs it; a smaller n calls the map's own routine n (game $8845).$09B8). On map 11 the square at column 24, row 13, a wall on the screen, is class 2 number 2, and word 2 of class 2's list is $0000, so its record is read from address 0 on: the processor's port registers, then zero page. Its flags are the port's direction register, $2F, which has no checks tested and the move refused; the message it shows is the byte at $0001, the port, $35. So walking into that wall prints map 11's message 53, "Ah! Fresh air and open spaces once again.", every time (live). The wall is next to squares the party can walk to from the map's exit at column 28, row 0 (a search of the map's classes).$43/$44 points at (engine $1DFB). The engine's is $29E4-$30FE, messages 0-169, and $30FF is a spare byte.$291D): code $1E capitalises the next letter, $1F adds $1E to the next code, and the code whose alphabet byte is 0 ends the message (engine $28F4).$1F42, $1F64): $0A singular or plural on the count $38, $0B the current character's name, $0C his or her, $0E him, her or it, $0F the count, $0D a new line, $05 and $06 wait for RETURN, space or back-arrow (wait_return, engine $2501).$6000 with its colours at $5C00, in VIC bank $4000 (live: $DD00 = $86, $D018 = $79, $D011 = $B7, $D016 = $D8). The start-up writes $D011 as $37 (startup $7F55-$7F57, $7F91-$7F93); bit 7 reads back as the high bit of the raster line, which was past 255 when it was read. Text is drawn into the bitmap from the 6-pixel font at $C600 (engine $1F03).$2947), which scans the keyboard; the NMI vector ends in an RTI, so RESTORE does nothing (engine $2966).$C953: Apple II-style codes with bit 7 set, no shifted keys, no repeat, one new key per interrupt (engine $29BD-$29D4).$1B36, $1BBF): the game's bar is "Use Enc Order Disband View Save Radio", and its hidden keys I, J, K and L move the party, back-arrow and space (game $7F18).$7E00-$8DFF replaced at the hand-over, side 3 attached): above.orientation.md, "The play disks").$6A from 0 to 1 and the roster drawn over the bottom of the map window.$57/$58 before and after each key).$DC00) and port B ($DC01) and, as the control, on the interrupt handler (engine $2947). The party stayed at column 55, row 62 ($57/$58), and in each hold port A was read 0 times against 203 or 204 reads of port B and as many interrupts; I then moved the party north, to row$AE26), play_sound_16 (engine $0741) and play_sound (engine $C993). The three steps the party took called each once; K, which the map refused, called none.kit/c64/frame.py and drawn by site/lib/c64.js: 0 of 104,448 pixels differ from the emulator's picture, with the roster hidden and shown.enter_map, its pages, stream and tile set measured with two fills (orientation.md, "The maps").$6B.enter_map and K (south, into the wall) pressed five times, with RETURN through the credits' six screens; the messages came 91, 92, 93, 94, 91, and the clock did not move.enter_map and L (east) pressed three times; each press printed message 53 and left the party, the square (class 2, number 2) and the clock as they were. The processor read $2F $35 at $0000-$0001 throughout.redraw_view (game $AC8C) at 5:00, 6:00, 12:00, 17:00 and 18:00. The 36 radiation squares around the building at columns 57-58, rows 36-37 showed the sign at 5:00 and 18:00 and plain ground at the other three.$7E63), its idle turn (game $7E93) and, as the control, the interrupt handler (engine $2947): 2,999 interrupts, 58,486 passes of the loop (19.5 a frame), three idle turns, and the clock from 1:24 to 1:36, four minutes a turn on the world map. So with no key pressed a turn passes about every 840 frames, 17 seconds on a PAL machine.$9F = $FF (no radiation in view), and 471 in the one chunk that held an idle turn. The port of the loop on the page makes 21.19 passes a frame on a processor that has every cycle; giving the video chip 1,075 cycles a frame brings it to 20.02, and the Geiger counter's player on the How it works page runs it so.$8382-$83C0; parts/ranger/facts.md).$A8C5) was skipped for all four members and they lost 22, 23, 16 and 14 CON; with it poked to 22 the roll was taken four times and nobody lost any.$8390 read the five dice of each of 1,000 attribute rolls: 1,043 ones, 869 twos, 786 threes, 727 fours, 749 fives and 826 sixes (even dice: 833 each), and two neighbours equal 699 times in 4,000. From play-map.vsf, a stub threw 4,096 pairs of dice with roll_2d6_open's instructions between them (engine $084A-$0850), each after the seed was moved 1 to 256 times as read_key moves it: 869 doubles, 21.2% (fair dice: 16.7%).enter_map, every member's weapon cleared so that all four fought with fists, and K (south) pressed: "3 Shambler Ghouls appear at 14 feet." With every member ordered to attack each round, ten melee rounds (game $7F64) brought three kills. Each kill was worth 40 experience (engine $1C-$1E at game $8163), and each killer's experience went from 0 to 80 across the two adds at game $8163 and $8166. The screen named the kill ("KIT hammers the Shambler Ghoul for 6 points of damage killing it.") and printed no experience.enter_map), every weapon cleared, then Hell Razor (record $F500) given LK 3 (+$10) and a Throwing knife (item 15) in slot 1, equipped (+$BD, +$1F = 1); K brought "2 Shambler Ghouls appear at 14 feet." With every member ordered to attack, the first throw stopped at game $A7E7, where weapon_damage has written the roll into its two ADC operands ($A7EC, $A7F0): the roll was 10, and 2 and 0 were written there, the roll of a 1 and a 1. weapon_damage returned 65,535 ($A7F3: X:A = $FFFF), the kill path at game $A6E5 ran, and the screen read "Hell Razor attacks and hits 1 Shambler Ghoul for 65532 points of damage spinning it into a dance of death." (reference/unlucky-throw.png).$A7C3); weapon_damage adds it to the roll with plain ADCs ($A7EA-$A7F0), while the melee path adds it through num2_add_clamped (engine $07C6), which stops at 0. Of the item table's thrown and fired weapons the Throwing knife has the fewest dice, 2 (item 15, byte 6); every other gun and rocket has 3 or more (the Mangler's and Sabot rocket's 2 × dice − armour is never fewer than their dice), and explosives take no luck ($A054). So only a 1 and a 1 on the knife with an LK of 3 or 4 goes below 0. No loot record of the 42 maps names item 15 or a random class-2 item (43 records, scanned with the levels page's port); Christina, NPC 1 of map 32 ($40AE), carries one in her slot 3. The ranger roll gives an LK of 3 or 4 for 261 of the 7,776 ways five dice fall (3.4%), the best three of five for 6. The highest hit-point seed of any named monster in the 42 maps is the Night Terror's, 32,000 (map 23, monster 1), which gives at most about 40,000 hit points (game $9483-$94B9).$05C5), the calls that pass its flags (engine $05DA) and the channels' step (engine $05E6), read every 50 frames. While the title's picture moved, the engine called the tick 3,508 to 3,553 times in 50 frames, every call passing the flags, and the channels stepped 7 times, about 7 steps a second; in a later stretch of the same sequence 2,624 to 3,176 calls and 5 or 6 steps. Between the two the flag at engine $AC was 0 and nothing moved.work/lead/sample.py): one stratum for each annotator's range, and one for the maps' descriptions that a decoder wrote, 7,973 comments (69%). Three agents that wrote none of the 63 checked them against the bytes: 48 were right, 14 had a wrong detail and 1 had the code wrong, so 15 of 63, 23.8% (Wilson 95% interval 15.0-35.6%). Weighted by the strata's sizes the rate was about 52%, because two of the three generated descriptions drawn carried an error of their template. The details were callers and paths left out, counts, the keys a prompt takes, and a field read alike for every value of a flag; the one wrong comment had taken an operand's value in the snapshot for a constant (map 49's poisoned needle, said to miss its target: the member comes from an operand the game writes when a Use begins). All 15 were rewritten to agree with the code.$7E0C); the skill list's header said to print on row 2, where the new line it opens with puts it on row 3 (module 2 $CC63); and play_sound's last write said to start the note, which sound 11's does not, its control byte $A4 having the gate bit clear (engine $C993). All three were rewritten, and so were five neighbouring comments of the death screen that left out the game's instructions in the same way.Pokes that change the game within its own parameters. Each one names the variable it changes and whether it has been tested live. Untested pokes are labelled as candidates.
| Effect | Poke | Status |
|---|
Loading listing…