Writing this up because I took a 1-year break on this and now forgot how it all works and am having to re-learn my own code. Fun! ROM/RAM layout: ~~~~~~~~~~~~~~~ Mapper: ~~~~~~~ Uses the E7 ROM mapper which provides 16K of bank-switched ROM and 2k of bank-switched RAM, accessible through a 4k address range. Fancy, but there in theory were games that used this mapper back in the day (Burgertime being the most famous example). address ranges ~~~~~~~~~~~~~~ $1000-$17FF (2k) one of six 2K ROM banks or 1K of RAM can be mapped here When RAM enabled there's 1k available: $1000-$13FF used to write RAM $1400-$17FF used to read RAM (this is a hack due to the ROM cartridge not having access to the read/write line from the CPU) $1800-$19FF (512B) one of four 256B RAM banks can be mapped here $1800-$18FF is write $1900-$19FF is read $1A00-$1FFF (1.5k) fixed as the last chunk of ROM soft-switches ~~~~~~~~~~~~~ $1FE0 - $1FE6 selects bank 0 through 6 of the ROM into $1000-$17FF $1FE7 enables 1K of the 2K RAM there instead $1FE8 - $1FEB selects which of four 256B banks appear at $1800 NOTE! Watch out for 6502 phantom/dummy memory accesses which can trigger the bank-switch behavior. Especially likely to happen when running code in the $1EEx area Myst cartridge layout: ~~~~~~~~~~~~~~~~~~~~~~ ROM BANK0 -- level data ROM BANK1 -- level data ROM BANK2 -- level data ROM BANK3 -- level data ROM BANK4 -- level data ROM BANK5 -- level data / puzzle code ROM BANK6 -- title, intro and book code ROM BANK7 -- always mapped, engine and routines Having the E7 detected properly by emulators: MAME checks for "lda $FFE7" (FFE5?) somewhere in the code? (double check this) I'm setting $E7 in the IRQ vector (for reasons?) Zero Page Layout (on VCS this is *all* of the stock RAM) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ $80-$8F : various variables $90-$9F : more variables $A0-$AF : level data area $B0-$B9 : variables for ZX02 decompression $BA-$C7 : fireplace puzzle $C8-$DF : clock puzzle and misc variables $E0-$EF : holds sprite for current pointer (hand or page) $F0-$FF : we reserve this for stack Build Process ~~~~~~~~~~~~~ Each bank of the ROM built separately. They all need the origin to be $1000 except for the always-there ROM which is at $1A00 Then it is all merged together. "myst.s" does this by binary including the files, including padding for the 512 bytes that can't be accessed. Probably could have written a linker script to do this instead. In the end we have a lot of circular dependencies as code running in banks 5, 6 and always-there main ROM call back and forth a lot. Build system is a hack, it builds the main ROM with dummy values for the globally visible symbols, then rebuilds again after they've been resolved. Decompression: ~~~~~~~~~~~~~~ Uses ZX02 compression by Einar Saukas and decompression code by DMSC Modified, so that when it reads from previously written-out data it uses the proper read address (offset by 1k) since the hack that makes E7 mappers work has separate address ranges for read/write Modified, as it takes too long to finish in [lookup numbers] cycles (more than a 60Hz frame) and we're responsible for doing the VSYNC and if we don't the display glitches). So we set up a timer to break up the decompression across frames so we still sync. Otherwise the screen glitches horribly This still leads to some unfortunate flicker as the screen blanks to all-black for ~3 frames. The way we do this is set up the 1024T timer to countdown from (18-1) which is almost exactly 229 scanlines. We set up VSYNC (our routine takes 4 cycles), 229, then 29 for overscan which is 262. We modify the code so the get_elias() routine that gets called roughly once per scanline does a check to see if the counter is down to 0 (or 1 to be safe as we can't guarantee exactly) and then it finishes out a frame and re-starts things. When the compression is done we pad things until the end of a frame. Clickable/Grabbable/Action Items: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specify the range of the screen to be grabbable in the level data Note, the x-coords are 0-159 not 0-39 as you might expect If things are clicked, Locations 0..7 = assume it's a marker switch and handle Locations 8+ = assume you're grabbing something and handle via a jump table Note locations 8-12 or so are special, as they are also book destinations, so the book code assumes it can figure out which one by subtracting 8 from the current location Book Implementation: ~~~~~~~~~~~~~~~~~~~~ Common routines for drawing books as there are many of them Alternate between two images. This allows animation, or the static effect in the portraits. Data is compressed. Because the code lives in BANK6 the decompressed data has to live in the 256-byte bank. There's 48 bytes of data for the edge of book colors There's 2*48 bytes of data for overlay1 (sprite+colors) There's 2*48 bytes of data for overlay2 (sprite+colors) This is a bit wasteful as especially for the red/blue pages there's a lot of common data. However it would be tricky to have separate top and bottom overlays combined so I've given up on that for now despite it taking up a big chunk of ROM. Level Engine: ~~~~~~~~~~~~~ Load level routine loads level. Which level to load is in zero page CURRENT_LOCATION Source is found by using CURRENT_LOCATION to look up source rom bank and address This is packed with top 3 bits bank, bottom 13 bits adr Copies 256 bytes of data (max we allow) to the $1800 RAM window HAVE TO BE CAREFUL! We always copy 256 bytes for simplicity. !!! If the ROM location starts at > $1700 the copy will overflow into the $1800 range which is the RAM-write area and this will corrupt things. Avoid having location data starting above $1700 Next it swaps in 1K of RAM to the lower $1000 address space and decompresses from the 256 byte RAM to this area (ZX02 decompression is described in a bit more detail elsewhere in this document) Finally the first 16 bytes with level info is copied to the zero page Why this roundabout way of doing things? Can't have ROM and the larger RAM active at same time Level Data description: ~~~~~~~~~~~~~~~~~~~~~~~ Offset Length Description 0 1 color of pointer (sprite0) 1 2 background colors (primary, secondary) 3 1 x-coord of sprite1 overlay 4 1 destination if click on forward hand ($FF=none) 5 1 destination if click on left-facing hand ($FF=none) 6 1 destination if click on right-facing hand ($FF=none) 7 2 XMAX/XMIN of action grab area (0=none) 9 2 YMAX/YMIN of action grab area (0=none) 11 1 x-coord of missile0 overlay (vertical line) (0=none) 12 1 alternate center destination barrier check (0 = none) 13 1 alternate center destination 14 1 overlay patch type 15 1 UNUSED destination is center/left/right rather than l/c/r to save 13 bytes in the movement code alternate center: overlay patch type: one of three. use top bits so BIT can set flags for bmi/bvs for library page, bottom bit indicates red/blue page, next bit whether near/close (as that affects where to start masking the overlay) Description of the images: standard Atari VCS asymmetric display each line: 40 4-pixel wide bytes has background and foreground colors to save room use 4-line high pixels if second background is given, it will switch to that color on 2/4 of the lines at cycle 60 (X across the screen) vertical bar: optional, full height of screen at given co-ords, same color as pointer overlay: one color per line (though black is hard) overlays with one more color on top for detail best if at least 4 blocks from left of screen Images are made in the gimp as a PNG file, separate overlay file, then some scripts grab the results When uncompressed the data above can be found here: level_data = $1400 level_colors = $1410 level_playfield0_left = $1440 level_playfield1_left = $1470 level_playfield2_left = $14A0 level_playfield0_right = $14D0 level_playfield1_right = $1500 level_playfield2_right = $1530 level_overlay_colors = $1560 level_overlay_sprite = $1590 Marker Switch Implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ data: 8 switches, 1 bit each, stored in SWITCH_STATUS switch position: it patches the overlay on the fly for different colors means switch has to be at same location in all graphics marker switch locations are in level LOCATION 0..7 which makes implementation slightly easier Drawing the Pointer ~~~~~~~~~~~~~~~~~~~ It is sprite0, drawn single-copy quad-size By default it's just a hand If at left or right edge of screen, and that direction is clickable (not $FF) then draw left/right hand If there are grab co-ordinates, if in range draw as grab special cases currently handle: 1. Marker switches 2. Linking books If we picked up a page, drawing page over-rides all this page color is based on ?? other stuff to describe: ~~~~~~~~~~~~~~~~~~~~~~~~ book animations music/sound graphics workflow: ~~~~~~~~~~~~~~~~~~ take screenshot of VGA version with ScummVM scale to 320x192 convert to VCS colormap put 40x48 grid on top draw over things, careful to have one color per line + overlay overlays really shouldn't be within 4 blocks of left side of screen create separate bg and overlay files, overlay starts 4 blocks to the right in PNG as this made coding up the command line tool easier Hooking up a new location ~~~~~~~~~~~~~~~~~~~~~~~~~ Super labor-intensive. Have to: 1. Create an ID for it in locations.inc 2. Create a wrapper "locations/something.s" file that includes the level data 3. Modify locations/Makefile to run the scripts to extract graphics and compress the result 4. Edit the proper rom_bankX.s file to include the data 5. Edit load_level.s table to point to the proper bank and address for the level 6. Rebuild. I'm missing a dependency in the Makefiles somewhere as often you have to make clean / make to get it to properly have all the updated files Fireplace Puzzle ~~~~~~~~~~~~~~~~ original proof of concept: fireplace_update = $A0 main = $34 lookup_table = $12 -------------------------------- = $E6 = 230 bytes zero page = 6+30 scanlines = 15+10 = 25 Original solution had the 6 bytes holding the data, but complex code to split the bit pattern up into the 5 playfield values, and finally a loop that iterated 30 times to update the whole puzzle field (with three slow indirect-Y (6 cycles) stores in the inner loop). Was being clever and moving the stack to point to the data in the zero page and PLA to auto-increment load. Very slow. Thought was to only update the playfield for the block that changed, not all 30 areas each time. updated version: fireplace_update = $A0 main = $2B (226-1fb) lookup_table = $12 -------------------------------- = $DD = 221 bytes zero page = 6+5 = 11 scanlines = 4 + 10 = 14 fireplace puzzle co-ords ~~~~~~~~~~~~~~~~~~~~~~~~ To position things on screen is difficult. You set a sprite's X-position by writing to a register, but since the TIA clock is 3x the CPU clock you can only set this at multiples of 3 (usually if you use a loop to do this you can only really do this at 5 cpu cycle granularity, or 15 TIA cycle granularity). You can ajdust after the fact to -7/+8. Simple code will ``cheat'' and divide by 16 as close enough (easy to do with shifts) though it means your position on screen in relation to the playfield will be offset a bit from the actual co-ords Maybe not a big deal... until the fireplace puzzle and trying to click on playfield positions. The proper solution is to have a div-15 routine (not easy on 6502). There is some code floating around that was reverse-engineered from the "river raid" game that is compact and convenient. optimizing using fancier position code book: before: $355 (book clicked) after: $349 (book clicked) cleverness in clock face code: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + notice almost duplicate inc hours / inc minutes code, instead set X=0 or X=1 and used indexed instructions in increment and overflow at 12 code + Sprite code. Copy 9 byte clockface sprite into overlay. 12x9 = 108 bytes of sprites for minute hand (and potentially hour hand) cheat a bit and crudely truncate the minute hand to be hour hand instead of multiplying by 9 to get to proper sprite, use table lookup using (IN),Y addressing. Table small enough we only need low byte of index in table. Now that it's an index we can move around the sprites so that same values overlap. Save 32 bytes by overlapping. Could maybe do better. + compare if clock right. "proper" way to check two values is compares and branches. But since it's essentialy at 16-bit number, just subtract and see if 0? would that work? weird bugs ~~~~~~~~~~ had code that did a BCS back to $1EE9 from page $1F on 6502 this will do a phantom load of address $1FE9 which is an E7 bank switch location. hard to debug TODO ~~~~ describe alternate overlays (doors/bridges) describe red/blue page code DEBUG ~~~~~ + to make it easier to get white page, set uncomment the dec in main.s + to make the fireplace puzzle easier: ldx in update_fireplace.s layout ~~~~~~ spent a lot of time properly packing the data to leave as little room as possible left over. Extra challenge as for reasons mentioned earlier level data can't start in the last 256 bytes of a ROM bank. In the end there's about 66 bytes left over but scattered enough can't easily fit another location (even though I have a dentist/planetarium level ready to go) timing ~~~~~~ The scene transition has about 2 frames (1/30s) of black in between which might be annoying flashing when moving. As a workaround if you flip the color/bw switch on the console it was insert a longer delay to reduce flicker