As you may have seen with S3K or ColinC10's Sonic 1 and 2, the S2 Level Select can become really cramped, really fast. Wouldn't it be great if you could somehow extend the menu? To do this, several changes will have to be made to the Level Select's code. This does not require a total rewrite, so your own hacks can coexist with this one. I recommend that you are familiar with the Level Select's many tables, as you will be creating your own. (HINT: look here) This guide will focus on the S2 (rev 01) HG disassembly, other disassembles may see support in the future, but not yet. The result will be your average S2 Level Select with multiple pages, you toggle between the pages with the 'NEXT' and 'PREV' options. The switch is instantaneous (no fading, no loading screen) and supports numerous pages, the only limit I can think of is RAM, but I suppose dynamic loading can be implemented at a later time. That or you store your mappings uncompressed. Let's begin. Cartography Spoiler First, open a plane map editor (I will be using PlaneEd), and edit your mappings/misc/Level Select.bin to feature a 'NEXT' option and, optionally, a 'PREV' option. The 'PREV' will simply loop from the first page to the last page. Also, remove the zeroes from the Sound Test selection, as they are redundant and are overwritten almost immediately. Doing so will save you a few bytes. For reference, this is my example: Take note of the location of your 'NEXT'. Mine is placed after Oil Ocean, and before Metropolis. Now, we shall create our second page, so list some new zones! Also, Sound Test must be present on all pages and it must be the last entry, it is currently hardcoded that way, sorry. Use the stock Level Select as a template: The area allocated to the emblem must be left untouched, as should the Sound Test. If this new page is your last (there are no pages after it), then you might not want to list a 'NEXT' option unless you want page looping. If it is, for example, the second out of three (there will be a following page), then you must list both 'NEXT' and 'PREV'. To give you an idea of what I mean, here is another example (second out of three): Once done, save as mappings/misc/Level Select2.bin (Next page will be 3, then 4, etc.) Repeat the process for each new page you want. Setting Up Constants Spoiler Before we touch any ASM, now is a good time to define the 1 byte large "LevSel_Page" RAM address constant. See you then. Now, wherever you feel it's appropriate, define the "MaxPageNum" constant. This constant will be used in calculations and checks regarding the maximum number of pages in your Level Select, such as those involved in page looping. Remember however, that zero counts as a number, so if you have three pages, the constant must be defined as 2. In the next section, Mappings Decompression, we'll be setting up the decompression of the mappings you made earlier. To make this easier, we'll define some constants, otherwise changing the decompression locations would require changing several lines, doing this requires only the one. Again, where appropriate, define the following constants: Page1_RAM Page2_RAM Page3_RAM The number of constants need to match the number of pages you plan to have. Adjust accordingly. By default, the first page occupies Chunk_Table, while the icons' mappings occupy Chunk_Table+$8C0, we're going to rearrange this a bit to fit with our new pages. Go to MenuScreen_LevelSelect and change Code (ASM): lea (Chunk_Table+$8C0).l,a1 to Code (ASM): lea (PageX_RAM+$8C0).l,a1 With X being the number of your last page. Go to LevelSelect_DrawIcon and do the same thing. Now we'll make our constants. Do so in this manner: Code (ASM): Page1_RAM = Chunk_Table Page2_RAM = Page1_RAM+$8C0 Page3_RAM = Page2_RAM+$8C0 You see, a decompressed full-screen mappings file always takes up 2240 bytes. This is 8C0 in hex. Doing this makes sure that no space is wasted, allowing more pages without encountering space issues. Mappings Decompression Spoiler Inside your s2.asm, go to MapEng_LevSel and, beneath it, add entries for your new mappings. Code (ASM): ; level select page 1 screen mappings (Enigma compressed) ; byte_9ADE: MapEng_LevSel: BINCLUDE "mappings/misc/Level Select.bin" ; level select page 2 screen mappings (Enigma compressed) MapEng_LevSel2: BINCLUDE "mappings/misc/Level Select2.bin" ; level select page 3 screen mappings (Enigma compressed) MapEng_LevSel3: BINCLUDE "mappings/misc/Level Select3.bin" With that done, go to MenuScreen_LevelSelect and add these two lines below the label: Code (ASM): clr.b (LevSel_Page).w clr.w (Level_select_zone).w Then replace the two "(Chunk_Table).l"s with "(Page1_RAM).l". After that you should see this (slighted formatted by myself): Code (ASM): MenuScreen_LevelSelect: lea (Page1_RAM).l,a1 lea (MapEng_LevSel).l,a0 ; 2 bytes per 8x8 tile, compressed move.w #make_art_tile(ArtTile_VRAM_Start,0,0),d0 bsr.w EniDec lea (Page1_RAM).l,a1 move.l #vdpComm(VRAM_Plane_A_Name_Table,VRAM,WRITE),d0 moveq #$27,d1 moveq #$1B,d2 ; 40x28 = whole screen bsr.w JmpTo_PlaneMapToVRAM ; display patterns The upper section of code decompresses the Enigma-compressed mappings to a RAM address, the second reads the decompressed data and applies the visuals. Using this we will load our new mappings. Here we have a choice: Do we make the new mappings decompress pre-fade in or post-fade in? Pre-fade will increase the duration of the loading screen (time the screen stays black), but loading it post-fade will run the risk of someone with speedy fingers loading the page before its mappings have fully decompressed. Pre-fade Copy the upper section of the code found above and paste it after Code (ASM): lea (MapEng_LevSel).l,a0 ; 2 bytes per 8x8 tile, compressed lea (Page1_RAM).l,a1 move.w #make_art_tile(ArtTile_VRAM_Start,0,0),d0 bsr.w EniDec Post-fade Copy the upper section of the code found above and paste it before the LevelSelect_Main label Code (ASM): bsr.w Pal_FadeTo ; <-------- HERE ;loc_93AC: LevelSelect_Main: ; routine running during level select Then make some adjustments to make it load your second page by changing the "MapEng_LevSel" to "MapEng_LevSel2", and adding to the "Page1_RAM". Do so again for any other pages you have, using MapEng_LevSelX and using other available RAM spaces. The result should look like this: Code (ASM): lea (Page2_RAM).l,a1 lea (MapEng_LevSel2).l,a0 ; 2 bytes per 8x8 tile, compressed move.w #make_art_tile(ArtTile_VRAM_Start,0,0),d0 bsr.w EniDec lea (Page3_RAM).l,a1 lea (MapEng_LevSel3).l,a0 ; 2 bytes per 8x8 tile, compressed move.w #make_art_tile(ArtTile_VRAM_Start,0,0),d0 bsr.w EniDec Table Expansion #1 - LevelSelect_Order Spoiler Go to LevelSelect_Order. Remember where you placed your 'NEXT'? That's going to come into play now. Insert "dc.w $3000" into the list to match where the 'NEXT' is on your page 1 mappings. If you're following my example, it should look like this: Code (ASM): LevelSelect_Order: dc.w emerald_hill_zone_act_1 dc.w emerald_hill_zone_act_2 ; 1 dc.w chemical_plant_zone_act_1 ; 2 dc.w chemical_plant_zone_act_2 ; 3 dc.w aquatic_ruin_zone_act_1 ; 4 dc.w aquatic_ruin_zone_act_2 ; 5 dc.w casino_night_zone_act_1 ; 6 dc.w casino_night_zone_act_2 ; 7 dc.w hill_top_zone_act_1 ; 8 dc.w hill_top_zone_act_2 ; 9 dc.w mystic_cave_zone_act_1 ; 10 dc.w mystic_cave_zone_act_2 ; 11 dc.w oil_ocean_zone_act_1 ; 12 dc.w oil_ocean_zone_act_2 ; 13 dc.w $3000 ; 14 - next page dc.w metropolis_zone_act_1 ; 15 dc.w metropolis_zone_act_2 ; 16 dc.w metropolis_zone_act_3 ; 17 dc.w sky_chase_zone_act_1 ; 18 dc.w wing_fortress_zone_act_1 ; 19 dc.w death_egg_zone_act_1 ; 20 dc.w $4000 ; 21 - special stage dc.w $FFFF ; 22 - sound test We will now create our new pages' tables. In the place of 'PREV' you must place "dc.w $5000". My second page's table looks like this: Code (ASM): LevelSelect_Order2: dc.w emerald_hill_zone_act_1 dc.w emerald_hill_zone_act_2 ; 1 dc.w death_egg_zone_act_1 ; 2 dc.w chemical_plant_zone_act_2 ; 3 dc.w aquatic_ruin_zone_act_1 ; 4 dc.w aquatic_ruin_zone_act_2 ; 5 dc.w casino_night_zone_act_1 ; 6 dc.w casino_night_zone_act_2 ; 7 dc.w hill_top_zone_act_1 ; 8 dc.w hill_top_zone_act_2 ; 9 dc.w mystic_cave_zone_act_1 ; 10 dc.w mystic_cave_zone_act_2 ; 11 dc.w oil_ocean_zone_act_1 ; 12 dc.w oil_ocean_zone_act_2 ; 13 dc.w $3000 ; 14 - next page dc.w $5000 ; 15 - prev page dc.w metropolis_zone_act_1 ; 16 dc.w metropolis_zone_act_2 ; 17 dc.w metropolis_zone_act_3 ; 18 dc.w sky_chase_zone_act_1 ; 19 dc.w wing_fortress_zone_act_1 ; 20 dc.w death_egg_zone_act_1 ; 21 dc.w $4000 ; 22 - special stage dc.w $FFFF ; 23 - sound test Once all of your tables are complete, we will bundle them all together with an index. Above LevelSelect_Order, paste this: Code (ASM): LevelSelect_OrderIndex: offsetTable offsetTableEntry.w LevelSelect_Order ; 0 offsetTableEntry.w LevelSelect_Order2 ; 1 offsetTableEntry.w LevelSelect_Order3 ; 2 For each page (and, by effect, table) you have, make an entry in the offset table. They must be in order! We'll be following this template for several other tables: LevelSelect_SwitchTable LevSel_IconTable LevSel_MarkTable Remember to place EVENs at the end of any tables and indexes whose data is byte-sized to avoid misaligned instructions Table Expansion #2 - LevelSelect_SwitchTable Spoiler Do as you did with LevelSelect_Order: A table for each page, add an index (same format as the one used above, uses word-sized offsets) Note, if you placed your 'NEXT' before the last entry of the table, then all references to entries after it will be shifted by it. So: Code (ASM): LevelSelect_SwitchTable: dc.b $E dc.b $F ; 1 dc.b $11 ; 2 dc.b $11 ; 3 dc.b $12 ; 4 dc.b $12 ; 5 dc.b $13 ; 6 dc.b $13 ; 7 dc.b $14 ; 8 dc.b $14 ; 9 dc.b $15 ; 10 dc.b $15 ; 11 dc.b $C ; 12 dc.b $D ; 13 dc.b 0 ; 14 dc.b 1 ; 15 dc.b 1 ; 16 dc.b 2 ; 17 dc.b 4 ; 18 dc.b 6 ; 19 dc.b 8 ; 20 dc.b $A ; 21 Becomes: Code (ASM): LevelSelect_SwitchTable: dc.b $F dc.b $10 ; 1 dc.b $12 ; 2 dc.b $12 ; 3 dc.b $13 ; 4 dc.b $13 ; 5 dc.b $14 ; 6 dc.b $14 ; 7 dc.b $15 ; 8 dc.b $15 ; 9 dc.b $16 ; 10 dc.b $16 ; 11 dc.b $C ; 12 dc.b $D ; 13 dc.b $E ; 14 NEXT dc.b 0 ; 15 dc.b 1 ; 16 dc.b 1 ; 17 dc.b 2 ; 18 dc.b 4 ; 19 dc.b 6 ; 20 dc.b 8 ; 21 dc.b $A ; 22 Table Expansion #3 - LevSel_IconTable Spoiler Not much to say here, I just use the Sound Test icon for 'NEXT' and 'PREV'. Table Expansion #4 - LevSel_MarkTable Spoiler Do not give this one an index! Again, not much to say except that I hope you know what you're doing. Table Expansion - For Reference Spoiler Here are my tables and indexes: LevelSelect_Order Spoiler Code (ASM): LevelSelect_OrderIndex: offsetTable offsetTableEntry.w LevelSelect_Order ; 0 offsetTableEntry.w LevelSelect_Order2 ; 1 offsetTableEntry.w LevelSelect_Order3 ; 2 ;Misc_9454: LevelSelect_Order: dc.w emerald_hill_zone_act_1 dc.w emerald_hill_zone_act_2 ; 1 dc.w chemical_plant_zone_act_1 ; 2 dc.w chemical_plant_zone_act_2 ; 3 dc.w aquatic_ruin_zone_act_1 ; 4 dc.w aquatic_ruin_zone_act_2 ; 5 dc.w casino_night_zone_act_1 ; 6 dc.w casino_night_zone_act_2 ; 7 dc.w hill_top_zone_act_1 ; 8 dc.w hill_top_zone_act_2 ; 9 dc.w mystic_cave_zone_act_1 ; 10 dc.w mystic_cave_zone_act_2 ; 11 dc.w oil_ocean_zone_act_1 ; 12 dc.w oil_ocean_zone_act_2 ; 13 dc.w $3000 ; 14 - next page dc.w metropolis_zone_act_1 ; 15 dc.w metropolis_zone_act_2 ; 16 dc.w metropolis_zone_act_3 ; 17 dc.w sky_chase_zone_act_1 ; 18 dc.w wing_fortress_zone_act_1 ; 19 dc.w death_egg_zone_act_1 ; 20 dc.w $4000 ; 21 - special stage dc.w $FFFF ; 22 - sound test LevelSelect_Order2: dc.w emerald_hill_zone_act_1 dc.w emerald_hill_zone_act_2 ; 1 dc.w death_egg_zone_act_1 ; 2 dc.w chemical_plant_zone_act_2 ; 3 dc.w aquatic_ruin_zone_act_1 ; 4 dc.w aquatic_ruin_zone_act_2 ; 5 dc.w casino_night_zone_act_1 ; 6 dc.w casino_night_zone_act_2 ; 7 dc.w hill_top_zone_act_1 ; 8 dc.w hill_top_zone_act_2 ; 9 dc.w mystic_cave_zone_act_1 ; 10 dc.w mystic_cave_zone_act_2 ; 11 dc.w oil_ocean_zone_act_1 ; 12 dc.w oil_ocean_zone_act_2 ; 13 dc.w $3000 ; 14 - next page dc.w $5000 ; 15 - prev page dc.w metropolis_zone_act_1 ; 16 dc.w metropolis_zone_act_2 ; 17 dc.w metropolis_zone_act_3 ; 18 dc.w sky_chase_zone_act_1 ; 19 dc.w wing_fortress_zone_act_1 ; 20 dc.w death_egg_zone_act_1 ; 21 dc.w $4000 ; 22 - special stage dc.w $FFFF ; 23 - sound test LevelSelect_Order3: dc.w emerald_hill_zone_act_1 dc.w emerald_hill_zone_act_2 ; 1 dc.w sky_chase_zone_act_1 ; 2 dc.w chemical_plant_zone_act_2 ; 3 dc.w aquatic_ruin_zone_act_1 ; 4 dc.w aquatic_ruin_zone_act_2 ; 5 dc.w casino_night_zone_act_1 ; 6 dc.w casino_night_zone_act_2 ; 7 dc.w hill_top_zone_act_1 ; 8 dc.w hill_top_zone_act_2 ; 9 dc.w mystic_cave_zone_act_1 ; 10 dc.w mystic_cave_zone_act_2 ; 11 dc.w oil_ocean_zone_act_1 ; 12 dc.w oil_ocean_zone_act_2 ; 13 dc.w $5000 ; 14 - prev page dc.w metropolis_zone_act_1 ; 15 dc.w metropolis_zone_act_2 ; 16 dc.w metropolis_zone_act_3 ; 17 dc.w sky_chase_zone_act_1 ; 18 dc.w wing_fortress_zone_act_1 ; 19 dc.w death_egg_zone_act_1 ; 20 dc.w $4000 ; 21 - special stage dc.w $FFFF ; 22 - sound test LevelSelect_SwitchTable Spoiler Code (ASM): LevelSelect_SwitchTableIndex: offsetTable offsetTableEntry.w LevelSelect_SwitchTable ; 0 offsetTableEntry.w LevelSelect_SwitchTable2 ; 1 offsetTableEntry.w LevelSelect_SwitchTable3 ; 2 ;byte_95A2: LevelSelect_SwitchTable: dc.b $F dc.b $10 ; 1 dc.b $12 ; 2 dc.b $12 ; 3 dc.b $13 ; 4 dc.b $13 ; 5 dc.b $14 ; 6 dc.b $14 ; 7 dc.b $15 ; 8 dc.b $15 ; 9 dc.b $16 ; 10 dc.b $16 ; 11 dc.b $C ; 12 dc.b $D ; 13 dc.b $E ; 14 dc.b 0 ; 15 dc.b 1 ; 16 dc.b 1 ; 17 dc.b 2 ; 18 dc.b 4 ; 19 dc.b 6 ; 20 dc.b 8 ; 21 dc.b $A ; 22 LevelSelect_SwitchTable2: dc.b $10 dc.b $11 ; 1 dc.b $13 ; 2 dc.b $13 ; 3 dc.b $14 ; 4 dc.b $14 ; 5 dc.b $15 ; 6 dc.b $15 ; 7 dc.b $16 ; 8 dc.b $16 ; 9 dc.b $17 ; 10 dc.b $17 ; 11 dc.b $C ; 12 dc.b $D ; 13 dc.b $E ; 14 dc.b $F ; 15 dc.b 0 ; 16 dc.b 1 ; 17 dc.b 1 ; 18 dc.b 2 ; 19 dc.b 4 ; 20 dc.b 6 ; 21 dc.b 8 ; 22 dc.b $A ; 23 LevelSelect_SwitchTable3: dc.b $F dc.b $10 ; 1 dc.b $12 ; 2 dc.b $12 ; 3 dc.b $13 ; 4 dc.b $13 ; 5 dc.b $14 ; 6 dc.b $14 ; 7 dc.b $15 ; 8 dc.b $15 ; 9 dc.b $16 ; 10 dc.b $16 ; 11 dc.b $C ; 12 dc.b $D ; 13 dc.b $E ; 14 dc.b 0 ; 15 dc.b 1 ; 16 dc.b 1 ; 17 dc.b 2 ; 18 dc.b 4 ; 19 dc.b 6 ; 20 dc.b 8 ; 21 dc.b $A ; 22 even LevSel_IconTable Spoiler Code (ASM): LevSel_IconTableIndex: offsetTable offsetTableEntry.w LevSel_IconTable ; 0 offsetTableEntry.w LevSel_IconTable2 ; 1 offsetTableEntry.w LevSel_IconTable3 ; 2 ;byte_96D8 LevSel_IconTable: dc.b 0,0 ;0 EHZ dc.b 7,7 ;2 CPZ dc.b 8,8 ;4 ARZ dc.b 6,6 ;6 CNZ dc.b 2,2 ;8 HTZ dc.b 5,5 ;$A MCZ dc.b 4,4 ;$C OOZ dc.b $E ;$E Next Page dc.b 1,1,1 ;$F MTZ dc.b 9 ;$12 SCZ dc.b $A ;$13 WFZ dc.b $B ;$14 DEZ dc.b $C ;$15 Special Stage dc.b $E ;$16 Sound Test LevSel_IconTable2: dc.b 0,0 ;0 EHZ dc.b $B,$B ;2 DEZ dc.b 8,8 ;4 ARZ dc.b 6,6 ;6 CNZ dc.b 2,2 ;8 HTZ dc.b 5,5 ;$A MCZ dc.b 4,4 ;$C OOZ dc.b $E ;$E Next Page dc.b $E ;$F Prev Page dc.b 1,1,1 ;$10 MTZ dc.b 9 ;$13 SCZ dc.b $A ;$14 WFZ dc.b $B ;$15 DEZ dc.b $C ;$16 Special Stage dc.b $E ;$17 Sound Test LevSel_IconTable3: dc.b 0,0 ;0 EHZ dc.b 9,9 ;2 SCZ dc.b 8,8 ;4 ARZ dc.b 6,6 ;6 CNZ dc.b 2,2 ;8 HTZ dc.b 5,5 ;$A MCZ dc.b 4,4 ;$C OOZ dc.b $E ;$E Prev Page dc.b 1,1,1 ;$F MTZ dc.b 9 ;$12 SCZ dc.b $A ;$13 WFZ dc.b $B ;$14 DEZ dc.b $C ;$15 Special Stage dc.b $E ;$16 Sound Test even LevSel_MarkTable Spoiler Code (ASM): ;byte_96EE: LevSel_MarkTable: ; 4 bytes per level select entry ; line primary, 2*column ($E fields), line secondary, 2*column secondary (1 field) dc.b 3, 6, 3,$24 ;0 dc.b 3, 6, 4,$24 dc.b 6, 6, 6,$24 dc.b 6, 6, 7,$24 dc.b 9, 6, 9,$24 ;4 dc.b 9, 6, $A,$24 dc.b $C, 6, $C,$24 dc.b $C, 6, $D,$24 dc.b $F, 6, $F,$24 ;8 dc.b $F, 6,$10,$24 dc.b $12, 6,$12,$24 dc.b $12, 6,$13,$24 dc.b $15, 6,$15,$24 ;$C dc.b $15, 6,$16,$24 dc.b $18, 6, 0, 0 ; --- second column --- dc.b 3,$2C, 3,$48 dc.b 3,$2C, 4,$48 ;$10 dc.b 3,$2C, 5,$48 dc.b 6,$2C, 0, 0 dc.b 9,$2C, 0, 0 dc.b $C,$2C, 0, 0 ;$14 dc.b $F,$2C, 0, 0 dc.b $12,$2C,$12,$48 LevSel_MarkTable2: ; 4 bytes per level select entry ; line primary, 2*column ($E fields), line secondary, 2*column secondary (1 field) dc.b 3, 6, 3,$24 ;0 dc.b 3, 6, 4,$24 dc.b 6, 6, 6,$24 dc.b 6, 6, 7,$24 dc.b 9, 6, 9,$24 ;4 dc.b 9, 6, $A,$24 dc.b $C, 6, $C,$24 dc.b $C, 6, $D,$24 dc.b $F, 6, $F,$24 ;8 dc.b $F, 6,$10,$24 dc.b $12, 6,$12,$24 dc.b $12, 6,$13,$24 dc.b $15, 6,$15,$24 ;$C dc.b $15, 6,$16,$24 dc.b $18, 6, 0, 0 dc.b $1A, 6, 0, 0 ; --- second column --- dc.b 3,$2C, 3,$48 ;$10 dc.b 3,$2C, 4,$48 dc.b 3,$2C, 5,$48 dc.b 6,$2C, 0, 0 dc.b 9,$2C, 0, 0 ;$14 dc.b $C,$2C, 0, 0 dc.b $F,$2C, 0, 0 dc.b $12,$2C,$12,$48 LevSel_MarkTable3: ; 4 bytes per level select entry ; line primary, 2*column ($E fields), line secondary, 2*column secondary (1 field) dc.b 3, 6, 3,$24 ;0 dc.b 3, 6, 4,$24 dc.b 6, 6, 6,$24 dc.b 6, 6, 7,$24 dc.b 9, 6, 9,$24 ;4 dc.b 9, 6, $A,$24 dc.b $C, 6, $C,$24 dc.b $C, 6, $D,$24 dc.b $F, 6, $F,$24 ;8 dc.b $F, 6,$10,$24 dc.b $12, 6,$12,$24 dc.b $12, 6,$13,$24 dc.b $15, 6,$15,$24 ;$C dc.b $15, 6,$16,$24 dc.b $1A, 6, 0, 0 ; --- second column --- dc.b 3,$2C, 3,$48 dc.b 3,$2C, 4,$48 ;$10 dc.b 3,$2C, 5,$48 dc.b 6,$2C, 0, 0 dc.b 9,$2C, 0, 0 dc.b $C,$2C, 0, 0 ;$14 dc.b $F,$2C, 0, 0 dc.b $12,$2C,$12,$48 even Dynamic Data #1 - LevelSelect_Order Spoiler Now we make use of those tables! Starting with LevelSelect_Order, go to LevelSelect_PressStart, you should see this: Code (ASM): LevelSelect_PressStart: move.w (Level_select_zone).w,d0 add.w d0,d0 move.w LevelSelect_Order(pc,d0.w),d0 bmi.w LevelSelect_Return ; sound test cmpi.w #$4000,d0 bne.s LevelSelect_StartZone Replace Code (ASM): move.w (Level_select_zone).w,d0 add.w d0,d0 move.w LevelSelect_Order(pc,d0.w),d0 With Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevelSelect_OrderIndex(pc,d0.w),d0 lea LevelSelect_OrderIndex(pc,d0.w),a0 move.w (Level_select_zone).w,d0 add.w d0,d0 movea.w (a0,d0.w),d0 LevelSelect_PressStart will now dynamically load LevelSelect_Order/2/3 depending on what page you're on. Dynamic Data #2 - LevelSelect_SwitchTable Spoiler Go to LevSelControls_SwitchSide, you should see this: Code (ASM): LevSelControls_SwitchSide: ; not in soundtest, not up/down pressed move.b (Ctrl_1_Press).w,d1 andi.b #button_left_mask|button_right_mask,d1 beq.s + ; no direction key pressed move.w (Level_select_zone).w,d0 ; left or right pressed move.b LevelSelect_SwitchTable(pc,d0.w),d0 ; set selected zone according to table move.w d0,(Level_select_zone).w + rts Replace Code (ASM): move.w (Level_select_zone).w,d0 ; left or right pressed move.b LevelSelect_SwitchTable(pc,d0.w),d0 ; set selected zone according to table With Code (ASM): move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevelSelect_SwitchTableIndex(pc,d0.w),d0 lea LevelSelect_SwitchTableIndex(pc,d0.w),a0 move.w (Level_select_zone).w,d0 ; left or right pressed move.b (a0,d0.w),d0 ; set selected zone according to table LevSelControls_SwitchSide will now dynamically load LevelSelect_SwitchTable/2/3 depending on what page you're on. Dynamic Data #3 - LevSel_IconTable Spoiler Go to LevelSelect_DrawIcon, you should see this: Code (ASM): LevelSelect_DrawIcon: move.w (Level_select_zone).w,d0 lea (LevSel_IconTable).l,a3 lea (a3,d0.w),a3 lea (Chunk_Table+$8C0).l,a1 moveq #0,d0 move.b (a3),d0 lsl.w #3,d0 move.w d0,d1 add.w d0,d0 add.w d1,d0 ... Replace Code (ASM): move.w (Level_select_zone).w,d0 lea (LevSel_IconTable).l,a3 With Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevSel_IconTableIndex(pc,d0.w),d0 lea LevSel_IconTableIndex(pc,d0.w),a3 move.w (Level_select_zone).w,d0 LevelSelect_DrawIcon will now dynamically load LevSel_IconTable/2/3 depending on what page you're on. Dynamic Data #4 - LevSel_MarkTable Spoiler This will be quite a bit different from the other three. Go to LevelSelect_MarkFields, you should find this: Code (ASM): LevelSelect_MarkFields: lea (Chunk_Table).l,a4 lea (LevSel_MarkTable).l,a5 lea (VDP_data_port).l,a6 moveq #0,d0 move.w (Level_select_zone).w,d0 ... Replace Code (ASM): lea (Chunk_Table).l,a4 lea (LevSel_MarkTable).l,a5 With Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevelSelect_MarkFieldsSubIndex(pc,d0.w),d0 jsr LevelSelect_MarkFieldsSubIndex(pc,d0.w) And above the LevelSelect_MarkFields label, add this: Code (ASM): LevelSelect_MarkFieldsSubIndex: offsetTable offsetTableEntry.w LevelSelect_MarkFieldsSub1 ; 0 offsetTableEntry.w LevelSelect_MarkFieldsSub2 ; 1 offsetTableEntry.w LevelSelect_MarkFieldsSub3 ; 2 LevelSelect_MarkFieldsSub1: lea (Page1_RAM).l,a4 lea (LevSel_MarkTable).l,a5 rts LevelSelect_MarkFieldsSub2: lea (Page2_RAM).l,a4 lea (LevSel_MarkTable2).l,a5 rts LevelSelect_MarkFieldsSub3: lea (Page3_RAM).l,a4 lea (LevSel_MarkTable3).l,a5 rts For each mapping, an entry, mapping decompression RAM address, and MarkTable is added. Since I have three pages, three 'subs' entries exist. Adjust accordingly. Fixing Sound Test Spoiler Now for some hardcoded values to become a little less hardcoded. The hardcoded Sound Test functions! Note where your Sound Test is (Level_select_zone). Count how many entries down LevelSelect_Order/2/3 "dc.w $FFFF ; sound test" is in hex, and there's your value. In vanilla S2, Sound Test is $15, but on your new pages, this is likely to have changed. Find these values for each page, and compile a small table, all in bytes, label it "LevSel_LimitTable" and place it above LevSelControls. Don't forget to EVEN it. Here's mine: Code (ASM): LevSel_LimitTable: dc.b $16 dc.b $17 dc.b $16 even Under LevSelControls, replace Code (ASM): subq.w #1,d0 ; decrease by 1 bcc.s + ; >= 0? moveq #$15,d0 ; set to $15 With Code (ASM): subq.w #1,d0 ; decrease by 1 bcc.s + ; >= 0? moveq #0,d0 move.b (LevSel_Page).w,d3 move.b LevSel_LimitTable(pc,d3.w),d0 And Code (ASM): cmpi.w #$16,d0 blo.s + ; smaller than $16? moveq #0,d0 ; if not, set to 0 With Code (ASM): moveq #0,d2 move.b (LevSel_Page).w,d3 move.b LevSel_LimitTable(pc,d3.w),d2 cmp.w d2,d0 bls.s + ; smaller than $18? moveq #0,d0 ; if not, set to 0 Now go to LevSelControls_CheckLR and replace Code (ASM): cmpi.w #$15,(Level_select_zone).w ; are we in the sound test? With Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d3 move.b LevSel_LimitTable(pc,d3.w),d0 move.w (Level_select_zone).w,d1 cmp.w d0,d1 ; are we in the sound test? Then go to LevelSelect_MarkFields (at the very bottom, right above the LevelSelect_DrawSoundNumber label) and replace Code (ASM): cmpi.w #$15,(Level_select_zone).w With Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d1 lea (LevSel_LimitTable).l,a0 move.b (a0,d1.w),d0 move.w (Level_select_zone).w,d1 cmp.w d0,d1 ; are we in the sound test? Using the New Pages Spoiler Now to make another choice. Do you want the 'NEXT' and 'PREV' to be activated using the start button, like several other elements, or A/B/C, like the Sound Test? Using Start Now to make 'NEXT' and 'PREV' work. Like the others, they'll be activated using the start button. Go to LevelSelect_PressStart and after Code (ASM): bmi.w LevelSelect_Return ; sound test Paste this: Code (ASM): cmpi.w #$3000,d0 ; was the selection Next Page? bne.w ++ add.b #1,(LevSel_Page).w cmpi.b #MaxPageNum+1,(LevSel_Page).w bne.s + clr.b (LevSel_Page).w + bra.w LevelSelect_LoadPage + cmpi.w #$5000,d0 ; was the selection Prev Page? bne.w ++ sub.b #1,(LevSel_Page).w bpl.s + move.b #MaxPageNum,(LevSel_Page).w + bra.w LevelSelect_LoadPage + Using A/B/C Now to make 'NEXT' and 'PREV' work. In this section, we'll make it so you activate them using the A/B/C buttons, similar to the Sound Test. Go to LevelSelect_PressStart and after Code (ASM): bmi.w LevelSelect_Return ; sound test Paste this: Code (ASM): cmpi.w #$3000,d0 ; was the selection Next Page? beq.w LevelSelect_Main cmpi.w #$5000,d0 ; was the selection Prev Page? beq.w LevelSelect_Main Now go to LevelSelect_Main, find this: Code (ASM): andi.b #button_start_mask,d0 bne.s LevelSelect_PressStart ; yes bra.w LevelSelect_Main ; no Replace it with this: Code (ASM): andi.b #button_start_mask,d0 bne.s LevelSelect_PressStart ; yes move.b (Ctrl_1_Press).w,d0 or.b (Ctrl_2_Press).w,d0 andi.b #button_A_mask|button_B_mask|button_C_mask,d0 bne.w LevelSelect_PressABC ; yes bra.w LevelSelect_Main ; no Go to LevelSelect_Return, and above the label, paste this: Code (ASM): LevelSelect_PressABC: moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 lea LevelSelect_OrderIndex(pc),a0 move.w (a0,d0.w),d0 lea (a0,d0.w),a0 move.w (Level_select_zone).w,d0 add.w d0,d0 movea.w (a0,d0.w),d0 cmpi.w #$3000,d0 ; was the selection Next Page? bne.w ++ add.b #1,(LevSel_Page).w cmpi.b #MaxPageNum+1,(LevSel_Page).w bne.s + clr.b (LevSel_Page).w + bra.w LevelSelect_LoadPage + cmpi.w #$5000,d0 ; was the selection Prev Page? bne.w ++ sub.b #1,(LevSel_Page).w bpl.s + move.b #MaxPageNum,(LevSel_Page).w + bra.w LevelSelect_LoadPage + bra.w LevelSelect_Main Then go to LevelSelect_StartZone, and above the label, paste this: Code (ASM): LevelSelect_LoadPage: moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevelSelect_LoadPageSubIndex(pc,d0.w),d0 jsr LevelSelect_LoadPageSubIndex(pc,d0.w) move.l #vdpComm(VRAM_Plane_A_Name_Table,VRAM,WRITE),d0 moveq #$27,d1 moveq #$1B,d2 ; 40x28 = whole screen bsr.w JmpTo_PlaneMapToVRAM ; display patterns clr.w (Level_select_zone).w moveq #0,d3 bsr.w LevelSelect_DrawSoundNumber bra.w LevelSelect_Main LevelSelect_LoadPageSubIndex: offsetTable offsetTableEntry.w LevelSelect_LoadPageSub1 ; 0 offsetTableEntry.w LevelSelect_LoadPageSub2 ; 1 offsetTableEntry.w LevelSelect_LoadPageSub3 ; 2 LevelSelect_LoadPageSub1: lea (Page1_RAM).l,a1 rts LevelSelect_LoadPageSub2: lea (Page2_RAM).l,a1 rts LevelSelect_LoadPageSub3: lea (Page3_RAM).l,a1 rts Each LoadPageSub contains a constant defined back in Setting Up Constants. As with the other indexes, there is one entry per page. Adjust accordingly. (Optional) Memorising LevSel_Page Spoiler If you want the page you are on to be memorised, à la Level_Select_Zone, so that when you return to the Level Select after leaving it, you're placed back where you were when you left, you'll have to do the following. This one's a little messy, as if you want that behaviour, you cannot have your page mappings decompress post-fade. Make this so before continuing. Go to MenuScreen_LevelSelect and replace this: Code (ASM): lea (Page1_RAM).l,a1 move.l #vdpComm(VRAM_Plane_A_Name_Table,VRAM,WRITE),d0 moveq #$27,d1 moveq #$1B,d2 ; 40x28 = whole screen bsr.w JmpTo_PlaneMapToVRAM ; display patterns With this: Code (ASM): moveq #0,d0 move.b (LevSel_Page).w,d0 add.b d0,d0 move.w LevelSelect_LoadPageSubIndex(pc,d0.w),d0 jsr LevelSelect_LoadPageSubIndex(pc,d0.w) move.l #vdpComm(VRAM_Plane_A_Name_Table,VRAM,WRITE),d0 moveq #$27,d1 moveq #$1B,d2 ; 40x28 = whole screen bra.w JmpTo_PlaneMapToVRAM ; display patterns Also, remove these two lines found at the top of MenuScreen_LevelSelect: Code (ASM): clr.b (LevSel_Page).w clr.w (Level_select_zone).w (Optional) Using Uncompressed Mappings Spoiler Only follow this if you want uncompressed mappings for whatever reason. Go to MenuScreen_LevelSelect and remove this: Code (ASM): lea (Page1_RAM).l,a1 lea (MapEng_LevSel).l,a0 ; 2 bytes per 8x8 tile, compressed move.w #make_art_tile(ArtTile_VRAM_Start,0,0),d0 bsr.w EniDec Do the same for the other pages'. Replace the nearby reference to Page1_RAM with MapEng_LevSel, note that you will have deleted that reference if you chose to memorise LevSel_Page. Next, head to LevelSelect_LoadPageSub1 and LevelSelect_MarkFieldsSub and begin replacing the references to PageX_RAM with the in-ROM equivalents (MapEng_LevSel, MapEng_LevSel2, etc.) Code (ASM): LevelSelect_LoadPageSub1: lea (MapEng_LevSel).l,a1 rts LevelSelect_LoadPageSub2: lea (MapEng_LevSel2).l,a1 rts LevelSelect_LoadPageSub3: lea (MapEng_LevSel3).l,a1 rts Code (ASM): LevelSelect_MarkFieldsSub1: lea (MapEng_LevSel).l,a4 lea (LevSel_MarkTable).l,a5 rts LevelSelect_MarkFieldsSub2: lea (MapEng_LevSel2).l,a4 lea (LevSel_MarkTable2).l,a5 rts LevelSelect_MarkFieldsSub3: lea (MapEng_LevSel3).l,a4 lea (LevSel_MarkTable3).l,a5 rts Doing this removes the decompression to RAM and instead loads directly from ROM. Decompressed plane mappings are quite a bit larger than their Enigma-compressed counterparts. My first page's mappings went to 2240 bytes (the usual size for full-screen mappings) from 340. If you encounter any of these errors... Code (Text): > > >s2.asm(11686): error: addressing mode not allowed on 68000 > > > move.b LevelSelect_OrderIndex(pc,d0.w),d0 ...regarding lines such as these: Code (ASM): move.w LevelSelect_OrderIndex(pc,d0.w),d0 lea LevelSelect_OrderIndex(pc,d0.w),a0 Rearrange them to follow this format: Code (ASM): lea LevelSelect_OrderIndex(pc),a0 ; if this still doesn't work, use (LevelSelect_OrderIndex).l,a0 instead move.w (a0,d0.w),d0 lea (a0,d0.w),a0 After fixing some 'branch out of range' errors, you should be good to go. Save and build. I have tested this guide on an untouched HG disasm. A prototype was built into my main hack with no compatibility issues between this hack and my other Level Select hacks. Hack has been tested with up to three pages. Further numbers are untested. Update Log (dd/mm/yyyy) Spoiler 23/01/2014 Spoiler "The only bug I've found so far is that when switching pages, the mappings overwrites the Sound Test selection, and that highlighting it will restore the old value. I've masked this by clearing Sound_test_sound whenever the page is changed." The above bug's been fixed, just had to clear d3 and call LevelSelect_DrawSoundNumber upon changing pages. Reworded the line "Remember to place EVENs at the end of your tables and indexes to avoid misaligned instructions". It's an old leftover from when all of the hack's indexes were byte-sized. This was changed to how it is in the released version to allow multiple pages without needing to worry about the tables being out of the indexes' range. Fixed a silly yet massive typo under Dynamic Data #3. Added A/B/C interaction. 26/01/2014 Spoiler Tidied up the guide a little. Fixed A/B/C interaction Added decompression location constants and matching section. Added uncompressed mappings section. Page loop added. External site-hosted images replaced with local images. 28/01/2014 Spoiler Changed both Code (ASM): lea LevelSelect_LoadPageSubIndex(pc,d0.w),a0 jsr (a0) ; dynamic call! into Code (ASM): jsr LevelSelect_LoadPageSubIndex(pc,d0.w) Fixed how LevSel_Page would be remembered, but the initial mapping loaded would not reflect it. LevSel_Page memorisation section added. 05/03/2014 Spoiler Fixed incompatible code under Memorising LevSel_Page. The code was adapted for use with byte-sized offsets, but the public version of this uses word-sized, leading to the game hanging upon going to the Level Select. Removed redundant branch under Memorising LevSel_Page. Forgot to update this when I did so to the SCHG version.
An interesting way to extend the menu indeed, and a rather thorough guide on how to do it too. Very nicely done. Please consider adding it to the wiki under the hacking how-tos (SCHG_How-to:Guide), if you haven't already. This is most likely due to the mappings themselves, with how you are decompressing to RAM and then drawing to a plane without any modification in-between. In the mappings themselves it probably contains the default 00, meaning the first time drawn will always be this. Add some code to take the sound test value and modify the loaded mappings in RAM before drawing to a plane and it should solve it nicely.
Thanks, it turns out your Sound Test fix wasn't necessary: It seems that the zeroes defined in the mappings are redundant, since LevelSelect_DrawSoundNumber is called immediately after the plane map is loaded and replaces them. Applying this to the page loading (sub?)routine fixes the bug, in fact, you can get away with removing the zeroes from the mappings altogether! I'm pretty happy that you recommend that the guide be added to the SCHG, but it'll be a while before I feel that it's ready. It's still incomplete, you see; scattered around the guide are a couple of to-do's, such as page-looping and dynamic mapping decompression. I should probably add constants for some of the reoccurring values too. EDIT: SCHG version uploaded.
I wonder if I could throw a suggestion into the hat. I was messing around with the Sonic 1 Level Select a while back... and did something where you had the zone name itself, followed by an Act # that would be changed ala the Sound Test. While I haven't been able to get the Act numbers to appear properly... the function works without a hitch. Image link In its current form, it looks like so. It has all planned zones for my hack, as well as Char Select, Score, etc. still could use a tad bit of work but the general aim was met. Also, going past the levels will jump all the way to the Sound Test. Any chance you'd be interested in such feature? While it wouldn't be ideal for Sonic 2, or 3... due to the two act system... a modded Sonic 1 Level Select could theoretically make use of it. If so I can send REV C's source to you to pick away at.
KoH, extending the Sonic 1 (Or more like Sonic 2 beta, but they are similar) level select is well within my interest, due to the fact that the level select in one of my hacks really just can't quite fit all the zones I need (And due to the nature of my hack, changing it would be kind of lame). While I would most ideally prefer a scrolling effect, anything to fit all the zones I want would be great. One column just isn't enough to fit even the $10 zones that Sonic 2 has by default.