diff --git a/.gitignore b/.gitignore index bd214c44..07ad212d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.chr *.pyc *.bin +*.txt tetris.lst tetris.lbl tetris.map diff --git a/build.js b/build.js index bd79b285..40430934 100644 --- a/build.js +++ b/build.js @@ -97,6 +97,13 @@ if (args.includes('--')) { console.log(); +// build menu +if (!args.includes('-M')) { + console.time('menu'); + require('./src/gamemode/gametypemenu/menu'); + console.timeEnd('menu'); +} + // build / compress nametables console.time('nametables'); @@ -227,3 +234,5 @@ if (args.includes('-T')) { console.log(`\nrunning single test: ${singleTest}`); execArgs('cargo', [...'run --release --manifest-path tests/Cargo.toml -- -T'.split(' '), singleTest]); } + +require('./tools/disasm'); diff --git a/src/gamemode/gametypemenu/.gitignore b/src/gamemode/gametypemenu/.gitignore new file mode 100644 index 00000000..154cd49e --- /dev/null +++ b/src/gamemode/gametypemenu/.gitignore @@ -0,0 +1,2 @@ +menudata.asm +menuram.asm diff --git a/src/gamemode/gametypemenu/menu.asm b/src/gamemode/gametypemenu/menu.asm index f4fb67b1..df781832 100644 --- a/src/gamemode/gametypemenu/menu.asm +++ b/src/gamemode/gametypemenu/menu.asm @@ -1,651 +1,959 @@ -.include "linecap.asm" +; to do +; get into game +; do arbitrary action +; get back into menu from game or level menu +; get back into menu from game w/block tool on +; each title associated with action +; more sanity checks +; set defaults +; save/restore to/from sram + + +AUTO_MENU_VARS_HI = >autoMenuVars + +; valid background chars are 0-253 +EOL = $FE +EOF = $FF +NORAM = $00 + +MENU_TITLE_PPU = $2106 +MENU_STRIPE_WIDTH = 20 +MENU_ROWS = 9 +MENU_STACK = $DF ; $01C8 - $01DF intended range + +MODE_DEFAULT = 0 ; needs to be auto generated + +menuDataStart: +.include "menudata.asm" +.out .sprintf("Menu data: %d", *-menuDataStart) + +; tttnnnnnn n = mode +PAGE_DEFAULT = %00000000 + +; table of first items instead +; + table of item counts + +VALUE_MASK = %00011111 +TYPE_MASK = %11100000 + +; tttnnnnn +TYPE_UNUSED = %00000000 +TYPE_NUMBER = %00100000 ; n = limit +TYPE_CHOICES = %01000000 ; n = wordlist index +TYPE_FF_OFF = %01100000 ; n = limit + +TYPE_HEX = %10000000 ; n = digits +TYPE_MODE_ONLY = %10100000 ; n = mode +TYPE_BCD = %11000000 ; n = digits, v bit to differentiate from hex +TYPE_SUBMENU = %11100000 ; n = menu index + +DIGIT_MASK = %10100000 +DIGIT_COMPARE = %10000000 + + gameMode_gameTypeMenu: .if NO_MENU - inc gameMode - rts + inc gameMode + rts .endif - jsr makeNotReady - jsr calc_menuScrollY - sta menuScrollY - lda #0 - sta hideNextPiece - lda #$1 - sta renderMode - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi - jsr bulkCopyToPpu - .addr title_palette - jsr copyRleNametableToPpu - .addr game_type_menu_nametable - lda #$28 - sta tmp3 - jsr copyRleNametableToPpuOffset - .addr game_type_menu_nametable_extra + jsr updateAudioWaitForNmiAndDisablePpuRendering + jsr disableNmi + jsr bulkCopyToPpu + .addr title_palette + jsr copyRleNametableToPpu + .addr game_type_menu_nametable .if INES_MAPPER <> 0 - lda #CHRBankSet0 - jsr changeCHRBanks + lda #CHRBankSet0 + jsr changeCHRBanks .endif - lda #NMIEnable - sta currentPpuCtrl - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging + lda #NMIEnable + sta currentPpuCtrl + jsr waitForVBlankAndEnableNmi + jsr updateAudioWaitForNmiAndResetOamStaging + jsr updateAudioWaitForNmiAndEnablePpuRendering + jsr updateAudioWaitForNmiAndResetOamStaging + + lda #AUTO_MENU_VARS_HI + sta byteSpriteAddr+1 + lda #$1 + sta renderMode + lda #0 + sta hideNextPiece + sta byteSpriteTile + sta gameStarted + jsr makeNotReady + +; check to see if returning from level menu or game + ldy activeMenu + iny + bne @initMenu + jsr exitSubmenuNoSfx + jmp gameTypeLoop +@initMenu: + lda #MENU_STACK + sta menuStackPtr + lda #0 + jsr enterMenu gameTypeLoop: - ; memset FF-02 used to happen every loop - ; but it's done in ResetOamStaging anyway? - jmp seedControls - -gameTypeLoopContinue: - jsr menuConfigControls - jsr practiseTypeMenuControls - -gameTypeLoopCheckStart: - lda newlyPressedButtons_player1 - cmp #BUTTON_START - bne gameTypeLoopNext - - ; check double killscreen - lda practiseType - cmp #MODE_KILLX2 - bne @checkSpeedTest - lda #29 - sta startLevel - sta levelNumber - lda #$00 - sta gameModeState - lda #$02 - sta soundEffectSlot1Init - - jsr bufferScreen ; hides glitchy scroll - - inc gameMode - inc gameMode - rts - -@checkSpeedTest: - ; check if speed test mode - cmp #MODE_SPEED_TEST - beq changeGameTypeToSpeedTest - cmp #MODE_LINECAP - beq gotoLinecapMenu - - ; check for seed of 0000XX - cmp #MODE_SEED - bne @checkSelectable - lda set_seed_input - bne @checkSelectable - lda set_seed_input+1 - and #$FE ; treat 0001 like 0000 - beq gameTypeLoopNext - -@checkSelectable: - lda practiseType - cmp #MODE_GAME_QUANTITY - bpl gameTypeLoopNext - - lda #$02 - sta soundEffectSlot1Init - inc gameMode - rts - -changeGameTypeToSpeedTest: - lda #$02 - sta soundEffectSlot1Init - lda #7 - sta gameMode - rts - -gotoLinecapMenu: - jmp linecapMenu - -gameTypeLoopNext: - jsr renderMenuVars - jsr updateAudioWaitForNmiAndResetOamStaging - jmp gameTypeLoop - -seedControls: - lda practiseType - cmp #MODE_SEED - bne gameTypeLoopContinue - - lda newlyPressedButtons_player1 - cmp #BUTTON_SELECT - bne @skipSeedSelect - lda rng_seed - sta set_seed_input - lda rng_seed+1 - sta set_seed_input+1 - lda rng_seed+1 - eor #$77 - ror - sta set_seed_input+2 -@skipSeedSelect: - - lda #BUTTON_LEFT - jsr menuThrottle - beq @skipSeedLeft - lda #$01 - sta soundEffectSlot1Init - lda menuSeedCursorIndex - bne @noSeedLeftWrap - lda #7 - sta menuSeedCursorIndex -@noSeedLeftWrap: - dec menuSeedCursorIndex -@skipSeedLeft: - - lda #BUTTON_RIGHT - jsr menuThrottle - beq @skipSeedRight -@moveRight: - lda #$01 - sta soundEffectSlot1Init - inc menuSeedCursorIndex - lda menuSeedCursorIndex - cmp #7 - bne @skipSeedRight - lda #0 - sta menuSeedCursorIndex -@skipSeedRight: - - lda menuSeedCursorIndex + lda gameStarted + beq @noGame + inc gameMode + lda #$2 + sta soundEffectSlot1Init + rts +@noGame: + ; todo: write down which vars are used by which func + jsr collectControllerInput + jsr setScratch + jsr addInputs + jsr respondToInput + jsr stageCursor + + ; scratch is not important anymore + jsr stageBackgroundTiles + jsr stageCurrentValues +gameTypeLoopWait: + jsr updateAudioWaitForNmiAndResetOamStaging + jmp gameTypeLoop + + +.out .sprintf("bg setup & loop: %d", *-gameMode_gameTypeMenu) + +.macro switchToMenuStack + tsx + stx stackPtr + ldx menuStackPtr + txs +.endmacro + +.macro switchToNormalStack + tsx + stx menuStackPtr + ldx stackPtr + txs +.endmacro + +enterSubMenu: + ldy #$02 + sty soundEffectSlot1Init + pha + switchToMenuStack + lda activeRow + pha + lda activePage + pha + lda activeMenu + pha + switchToNormalStack + pla +enterMenu: + sta activeMenu + tay + iny + bne @normalMenu + rts +@normalMenu: + lda #0 +enterPage: + sta activePage + sta originalPage + ldy activeMenu + clc + adc startPageByMenu,y + sta actualPage + tax + + lda pageTypes,x + and #VALUE_MASK + sta unpackedPageValue ; always 0 for now + + lda pageTypes,x + and #TYPE_MASK + sta unpackedPageType + + lda pageCountByMenu,y + ldy #$00 + sty activeColumn + cmp #$1 + beq @storeRow + dey ; start at page select row for multipage + dec unpackedPageType ; hack for now +@storeRow: + sty activeRow + +setScratch: + ldx actualPage + lda activeRow + clc + adc startItemByPage,x + sta activeItem + tax + lda itemTypes,x + tay + and #VALUE_MASK + sta unpackedItemValue + + tya + and #TYPE_MASK + sta unpackedItemType + + jsr setupLR + jmp setupUD + +exitSubmenu: + ldy #$02 + sty soundEffectSlot1Init + +exitSubmenuNoSfx: + switchToMenuStack + pla + switchToNormalStack + + jsr enterMenu + + switchToMenuStack + pla + switchToNormalStack + + jsr enterPage .if KEYBOARD = 1 -@kbSeedLow = generalCounter -@kbSeedHigh = generalCounter2 - bne @checkForKbSeedEntry - jmp @skipSeedControl -@checkForKbSeedEntry: - jsr readKbSeedEntry - bmi @noKeysPressed - sta @kbSeedLow - asl - asl - asl - asl - sta @kbSeedHigh - ldy menuSeedCursorIndex - dey - tya - lsr - tay - ; y = (index-1) // 2 - ; c = (index-1) % 2 - lda set_seed_input,y - bcc @highByte -; low byte: - and #$F0 - ora @kbSeedLow - bcs @storeSeed -@highByte: - and #$0F - ora @kbSeedHigh -@storeSeed: - sta set_seed_input,y - jmp @moveRight -@noKeysPressed: -.else - beq @skipSeedControl +.warning "keyboard menu seed code is broken" +; @kbSeedLow = generalCounter +; @kbSeedHigh = generalCounter2 +; bne @checkForKbSeedEntry +; jmp @skipSeedControl +; @checkForKbSeedEntry: +; jsr readKbSeedEntry +; bmi @noKeysPressed +; sta @kbSeedLow +; asl +; asl +; asl +; asl +; sta @kbSeedHigh +; ldy menuSeedCursorIndex +; dey +; tya +; lsr +; tay +; ; y = (index-1) // 2 +; ; c = (index-1) % 2 +; lda set_seed_input,y +; bcc @highByte +; ; low byte: +; and #$F0 +; ora @kbSeedLow +; bcs @storeSeed +; @highByte: +; and #$0F +; ora @kbSeedHigh +; @storeSeed: +; sta set_seed_input,y +; jmp @moveRight +; @noKeysPressed: +; .else +; beq @skipSeedControl .endif - lda menuSeedCursorIndex - sbc #1 - lsr - tax ; save seed offset - - ; handle changing seed vals - - lda #BUTTON_UP - jsr menuThrottle - beq @skipSeedUp - lda #$01 - sta soundEffectSlot1Init - lda menuSeedCursorIndex - and #1 - beq @lowNybbleUp - - lda set_seed_input, x - clc - adc #$10 - sta set_seed_input, x - - jmp @skipSeedUp -@lowNybbleUp: - lda set_seed_input, x - clc - tay - and #$F - cmp #$F - bne @noWrapUp - tya - and #$F0 - sta set_seed_input, x - jmp @skipSeedUp -@noWrapUp: - tya - adc #1 - sta set_seed_input, x -@skipSeedUp: - - lda #BUTTON_DOWN - jsr menuThrottle - beq @skipSeedDown - lda #$01 - sta soundEffectSlot1Init - lda menuSeedCursorIndex - and #1 - beq @lowNybbleDown - - lda set_seed_input, x - sbc #$10 - clc - sta set_seed_input, x - - jmp @skipSeedDown -@lowNybbleDown: - lda set_seed_input, x - tay - and #$F - ; cmp #$0 ; and sets z flag - bne @noWrapDown - tya - and #$F0 - clc - adc #$F - sta set_seed_input, x - jmp @skipSeedDown -@noWrapDown: - tya - sec - sbc #1 - sta set_seed_input, x -@skipSeedDown: - - jmp gameTypeLoopCheckStart -@skipSeedControl: - jmp gameTypeLoopContinue - -menuConfigControls: - ; account for 'gaps' in config items of size zero - ; previously the offset was just set on X directly - - ldx #0 ; memory offset we want - ldy #0 ; cursor -@searchByte: - cpy practiseType - bne @notYet - lda menuConfigSizeLookup, y - beq @configEnd - ; if zero, caller will beq to skip the config - jmp @searchEnd -@notYet: - lda menuConfigSizeLookup, y - beq @noMem - inx -@noMem: - iny - jmp @searchByte -@searchEnd: - - ; actual offset now in Y - ; RAM offset now in X - - ; check if pressing left - lda #BUTTON_LEFT - jsr menuThrottle - beq @skipLeftConfig - ; check if zero - lda menuVars, x - ; cmp #0 ; lda sets z flag - beq @skipLeftConfig - ; dec value - dec menuVars, x - lda #$01 - sta soundEffectSlot1Init - jsr assertValues -@skipLeftConfig: - - ; check if pressing right - lda #BUTTON_RIGHT - jsr menuThrottle - beq @skipRightConfig - ; check if within the offset - lda menuVars, x - cmp menuConfigSizeLookup, y - bpl @skipRightConfig - inc menuVars, x - lda #$01 - sta soundEffectSlot1Init - jsr assertValues -@skipRightConfig: -@configEnd: - rts - -menuConfigSizeLookup: - MENUSIZES - -assertValues: - ; make sure you can only have block or qual - lda practiseType - cmp #MODE_QUAL - bne @noQual - lda menuVars, x - beq @noQual - lda #0 - sta debugFlag -@noQual: - lda practiseType - cmp #MODE_DEBUG - bne @noDebug - lda menuVars, x - beq @noDebug - lda #0 - sta qualFlag -@noDebug: - ; goofy - lda practiseType - cmp #MODE_GOOFY - bne @noFlip - lda heldButtons_player1 - asl - and #$AA - sta tmp3 - lda heldButtons_player1 - and #$AA - lsr - ora tmp3 - sta heldButtons_player1 -@noFlip: - rts - -practiseTypeMenuControls: - ; down - lda #BUTTON_DOWN - jsr menuThrottle - beq @downEnd - lda #$01 - sta soundEffectSlot1Init - - inc practiseType - lda practiseType - cmp #MODE_QUANTITY - bne @downEnd - lda #0 - sta practiseType -@downEnd: - - ; up - lda #BUTTON_UP - jsr menuThrottle - beq @upEnd - lda #$01 - sta soundEffectSlot1Init - lda practiseType - bne @noWrap - lda #MODE_QUANTITY - sta practiseType -@noWrap: - dec practiseType -@upEnd: - rts - -renderMenuVars: - - ; playType / seed cursors - - lda menuSeedCursorIndex - bne @seedCursor - - lda practiseType - jsr menuItemY16Offset - bne @cursorFinished - stx spriteYOffset - lda #$17 - sta spriteXOffset - lda #$1D - sta spriteIndexInOamContentLookup - jsr loadSpriteIntoOamStaging - jmp @cursorFinished - -@seedCursor: - clc - lda #MENU_SPRITE_Y_BASE + 7 - sbc menuScrollY - sta spriteYOffset - lda menuSeedCursorIndex - asl a - asl a - asl a - adc #$B1 - sta spriteXOffset - lda #$1B - sta spriteIndexInOamContentLookup - jsr loadSpriteIntoOamStaging - - ; indicator - - lda set_seed_input - bne @renderIndicator - lda set_seed_input+1 - and #$FE ; treat 0001 like 0000 - beq @cursorFinished -@renderIndicator: - ldx #$E - lda set_seed_input+2 - and #$F0 - beq @v5 - lda set_seed_input - bne @v4 - lda set_seed_input+1 - beq @v5 - jmp @v4 -@v5: - ldx #$F -@v4: - stx spriteIndexInOamContentLookup - sec - lda #(MODE_SEED*8) + MENU_SPRITE_Y_BASE + 1 - sbc menuScrollY - sta spriteYOffset - lda #$A0 - sta spriteXOffset - jsr stringSprite - -@cursorFinished: - -menuCounter := tmp1 -menuRAMCounter := tmp3 -menuYTmp := tmp2 - - ; render seed - - lda #$b8 - sta spriteXOffset - lda #MODE_SEED - jsr menuItemY16Offset - bne @notSeed - stx spriteYOffset - lda #set_seed_input - sta byteSpriteAddr - lda #0 - sta byteSpriteAddr+1 - lda #0 - sta byteSpriteTile - lda #3 - sta byteSpriteLen - jsr byteSprite -@notSeed: - - ; render config vars - - ; YTAX - lda #0 - sta menuCounter - sta menuRAMCounter + switchToMenuStack + pla + switchToNormalStack + + sta activeRow + jmp setScratch + + +setupUD: + ldy activeColumn + bne setupUDDigitChange + +setupUDRowChange: +; ud change row 1/2 - activeColumn == 0 + ldy #$00 + lda unpackedPageType + bpl @storeMin ; no page select row for single page + dey +@storeMin: + sty udMin + ldx actualPage + lda itemCountByPage,x + sta udMax + + lda #>activeRow + sta udPointer+1 + lda # 0 + dey + tya + lsr + tay ; y points to digit + php ; save for later, carry clear if hi byte + lda #$0 + sta udMin + sta udPointer+1 ; won't work if nybbleTemp is not zeropage + lda #activePage + sta lrPointer+1 + lda #= 0 && itemType < 128 + lda #AUTO_MENU_VARS_HI + sta lrPointer+1 + ldx activeItem + lda memoryOffsets,x + sta lrPointer + ldy #$0 + lda unpackedItemType + and #TYPE_MASK + cmp #TYPE_FF_OFF + bne @storeMin + dey +@storeMin: + sty lrMin + ldx unpackedItemValue + cmp #TYPE_CHOICES + bne @storeMax + lda choiceSetCounts,x + tax +@storeMax: + stx lrMax + rts + +setupLRColumnChange: +; setupLRColumnChange itemType & %10100000 == %10000000 + lda #0 + sta lrMin + lda #>activeColumn + sta lrPointer+1 + lda #MENU_TITLE_PPU + sta stack + lda # high byte of offset in A -; -> low byte in X -menuItemY16Offset: - sta tmpY - lda #8 - sta tmpX - ; get 16bit menuitem * 8 in tmpX/tmpY - lda #$0 - ldx #$8 - clc -@mulLoop: - bcc @mulLoop1 - clc - adc tmpY -@mulLoop1: - ror - ror tmpX - dex - bpl @mulLoop - sta tmpY - ; add offset - clc - lda tmpX - adc #MENU_SPRITE_Y_BASE + 1 - sta tmpX - lda tmpY - adc #0 - sta tmpY - ; remove menuscroll - sec - lda tmpX - sbc menuScrollY - sta tmpX - tax - lda tmpY - sbc #0 - rts - -bufferScreen: - lda #$0 - sta renderMode - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi - jsr drawBlackBGPalette - jsr resetScroll - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging - lda #$3 - sta sleepCounter -@endLoop: - jsr updateAudioWaitForNmiAndResetOamStaging - lda sleepCounter - bne @endLoop - rts + ldy #0 + lda (@stringPtr),y + tay + iny + beq @fillBlank ; stop advancing pointer when $FF is reached + inc @stringPtr + bne @noCarry + inc @stringPtr+1 +@noCarry: + iny + beq @fillBlank ; $FE also blanks line but after advancing pointer + sta stack,x + dec @blankCounter + inx + bne @loop ; always taken +@fillBlank: ; should only be entered directly when end of string reached + dec @blankCounter + bmi @finishRow + lda #$FF + sta stack,x + inx + bne @fillBlank ; always taken + +@finishRow: +; check if all rows drawn + dec @rowCounter + beq @shiftTitleRow + +; set next row based on last row + lda stack-((MENU_STRIPE_WIDTH+2)-1),x + clc + adc #$40 + sta stack+1,x + lda stack-(MENU_STRIPE_WIDTH+2),x + adc #$00 + sta stack,x + inx + inx + bne @nextRow ; always taken +@shiftTitleRow: +; bump title row 4 tiles to the right + lda stack+1 + eor #%1111 + sta stack+1 + rts + + +.out .sprintf("background staging: %d", *-stageBackgroundTiles) + + +stageCurrentValues: + @counter = blankCounter + @itemCount = rowCounter + + lda #$00 + sta @counter + lda #AUTO_MENU_VARS_HI + + ldx actualPage + lda startItemByPage,x + + sta activeItem + lda itemCountByPage,x + + sta @itemCount + + lda#(MENU_STRIPE_WIDTH+2) - 8 + sta stackPtr + +@memoryStageLoop: + lda stackPtr + clc + adc #MENU_STRIPE_WIDTH+2 + sta stackPtr + tax + + ldy activeItem + lda memoryOffsets,y + sta byteSpriteAddr + lda #AUTO_MENU_VARS_HI + sta byteSpriteAddr+1 + lda itemTypes,y + tax + ldy #0 + and #TYPE_MASK + bmi @digitInputOrEdge + + cmp #TYPE_CHOICES + beq @drawString + + cmp #TYPE_NUMBER + bne @drawFFOff +@setupOneByte: + lda #$02 + bne @drawOneByte + +@drawFFOff: + lda (byteSpriteAddr),y + bpl @setupOneByte + ldx #CHOICESET_OFFON + jsr @setStringList + jmp @startCopy + +@drawString: + txa + and #%11111 + tax + jsr @setStringList + lda (byteSpriteAddr),y + tay +@startCopy: + lda (stringSetPtr),y + tay + lda choiceSetTable,y + beq @endCopy + sta generalCounter + jsr setStackOffset + iny +@nextChar: + lda choiceSetTable,y + sta stack,x + inx + iny + dec generalCounter + bne @nextChar + +@endCopy: + jmp @nextByte + +@setStringList: + lda choiceSetIndexes,x + clc + adc #choiceSets + sta stringSetPtr+1 + rts + +@digitInputOrEdge: + and #TYPE_MASK + cmp #TYPE_MODE_ONLY + beq @nextByte + cmp #TYPE_SUBMENU + beq @nextByte + txa + and #%11111 +@drawOneByte: + pha + sec + sbc #1 + lsr + clc + adc #$1 + sta generalCounter + pla + + jsr setStackOffset + ldy #$00 +@digitLoop: + lda (byteSpriteAddr),y + pha + lsr + lsr + lsr + lsr + sta stack,x + inx + pla + and #$0F + sta stack,x + inx + iny + dec generalCounter + bne @digitLoop + jmp @nextByte + +@nextByte: + inc activeItem + inc @counter + lda @counter + cmp @itemCount + beq @ret + jmp @memoryStageLoop +@ret: + rts + +setStackOffset: + eor #$FF + clc + adc #$09 + clc + adc stackPtr + tax + rts + + +.out .sprintf("value staging: %d", *-stageCurrentValues) + + +stageCursor: + ldx activeMenu + lda pageCountByMenu,x + cmp #$1 + beq @singlePage + ldx oamStagingLength + sta oamStaging+9,x + lda #$4F + sta oamStaging+5,x + + lda #$CB + sta oamStaging+0,x + sta oamStaging+4,x + sta oamStaging+8,x + + lda #$C8 + sta oamStaging+3,x + clc + adc #$08 + sta oamStaging+7,x + adc #$08 + sta oamStaging+11,x + + lda #$00 + sta oamStaging+2,x + sta oamStaging+6,x + sta oamStaging+10,x + + ldy activePage + iny + tya + sta oamStaging+1,x + txa + clc + adc #$C + sta oamStagingLength + +@singlePage: + + lda activeRow + bpl @notTitle + + lda #$3F + sta spriteYOffset + lda #$10 + sta spriteXOffset + lda #$23 ; page select + sta spriteIndexInOamContentLookup + jmp loadSpriteIntoOamStaging + +@notTitle: + asl + asl + asl + asl + clc + adc #$4F + sta spriteYOffset +; digit input + ldx activeColumn + beq @notColumn + sec + sbc #$09 + sta spriteYOffset + txa + asl + asl + asl + clc + adc #$B9 + sta spriteXOffset + ldx activeItem + lda itemTypes,x + and #VALUE_MASK + sec + sbc #1 + lsr + asl + asl + asl + asl + eor #$FF + clc + adc #$01 + clc + adc spriteXOffset + sta spriteXOffset + lda #$1B ; digit select + bne @store +@notColumn: + lda #$14 + sta spriteXOffset + lda #$1D ; option select +@store: + sta spriteIndexInOamContentLookup +@stage: + jmp loadSpriteIntoOamStaging +gotoEdgeCase: + rts + + +.out .sprintf("cursor staging: %d", *-stageCursor) + + +render_mode_menu: + tsx + txa + ldx #$ff + txs + tax + ldy #MENU_ROWS +@nextRow: + pla + sta PPUADDR + pla + sta PPUADDR + .repeat MENU_STRIPE_WIDTH + pla + sta PPUDATA + .endrepeat + dey + bne @nextRow + txs + rts + + +.out .sprintf("render dump: %d", *-render_mode_menu) + + +.out .sprintf("total: %d", *-gameMode_gameTypeMenu) diff --git a/src/gamemode/gametypemenu/menu.js b/src/gamemode/gametypemenu/menu.js new file mode 100644 index 00000000..df352a1c --- /dev/null +++ b/src/gamemode/gametypemenu/menu.js @@ -0,0 +1,386 @@ +const { mainMenu, extraSpriteStrings } = require("./menudata"); +const { writeFileSync } = require("fs"); + +MAX_LENGTH_NAME = 14; +MAX_LENGTH_VALUE = 8; +DEBUG = false; + +labelMap = { + TYPE_BCD: typeDigit, + TYPE_HEX: typeDigit, + TYPE_NUMBER: typeNumber, + TYPE_FF_OFF: typeNumber, + TYPE_CHOICES: typeChoices, + TYPE_MODE_ONLY: getOutputLines, + TYPE_SUBMENU: typeSubMenu, + TYPE_BOOL: typeBool, +}; + +addedStrings = []; +buffer = []; +choiceSetCounts = []; +choiceSetEnums = []; +choiceSetIndexes = []; +choiceSets = []; +index = 0; +items = []; +lookupConstants = []; +memoryBuffer = []; +memoryMap = []; +memoryReservations = {}; +menuCount = 0; +menuEnums = []; +newStringLines = []; +pageCountByMenu = []; +pageIndex = 0; +pageLabelText = {}; +pagesOutput = []; +startItemByPage = []; +startPageByMenu = []; +unlabeledStringSets = {}; + +function checkStringSanity(string) { + if (string.length > MAX_LENGTH_VALUE) { + throw new Error(`${string} is more than MAX_LENGTH_VALUE chars`); + } + if ((match = string.match(/[^-\/ a-z0-9_?!*]/i))) { + throw new Error(`${string} has invalid char '${match[0]}'`); + } +} + +function cleanWord(word) { + word = word.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()); + return word.replace(/[- *?!(),\/]/g, ""); +} + +function getStringName(word) { + return `string${cleanWord(word)}`; +} + +function getChoiceSetName(word) { + return `choiceSet${cleanWord(word)}`; +} + +function getStringConstant(name) { + return `STRING_${cleanWord(name).toUpperCase()}`; +} + +function getChoiceSetConstant(name) { + return `CHOICESET_${cleanWord(name).toUpperCase()}`; +} + +function getByteLine(byte) { + return ` .byte ${byte}`; +} + +function getHexByte(number) { + if (isNaN(number)) return number; + return `$${number.toString(16).padStart(2, "0").toUpperCase()}`; +} + +function getOutputLines(itemType, string, memory) { + return { + string: string, + label: getByteLine(`${itemType} ; ${string}`), + memory: memory, // has to be processed separately to get output line + }; +} + +function getStringByte(c) { + replaceMap = { + ",": "$25", + "/": "$4F", + "(": "$5E", + ")": "$5F", + "*": "$69", // KSx2 x + " ": "$EF", + }; + return replaceMap[c] ? replaceMap[c] : `"${c.toUpperCase()}"`; +} + +function getStringBytes(string) { + return [...string.split("").map((c) => getStringByte(c))].join(","); +} + +function getLineString(string, multiline = false) { + if (string.length > MAX_LENGTH_NAME) { + throw new Error(`${string} is more than MAX_LENGTH_NAME chars`); + } + + return multiline + ? string + .split("") + .map((c) => getByteLine(getStringByte(c))) + .join("\n") + : getByteLine(getStringBytes(string)); +} + +function getPageLines(title, page, pages) { + DEBUG && console.log(`getPageLines`, title, page, pages); + pageType = "PAGE_DEFAULT"; + [_, label, mode] = title.match(/([^[]*)(?:\s*\[mode=(\w+)\])?/i); + const modifier = mode ? `MODE_${mode.toUpperCase()}` : "MODE_DEFAULT"; + const pagelabelsName = `pageLabels${cleanWord(label)}`; + + const endLabel = getByteLine("EOL"); + const endLabelSet = getByteLine("EOF"); + + pageLabelTextLines = []; + pageLabelTextLines.push(`${pagelabelsName}:`); + padding = [...Array(Math.round((MAX_LENGTH_NAME - label.length) / 2))] + .map(() => " ") + .join(""); + pageLabelTextLines.push(getLineString(`${padding}${label}`)); + pageLabelTextLines.push(endLabel); + page.forEach((p, i) => { + pageLabelTextLines.push(getLineString(p[1])); + if (i + 1 != page.length) pageLabelTextLines.push(endLabel); + }); + pageLabelTextLines.push(endLabelSet); + joined = pageLabelTextLines.join("\n"); + existing = pageLabelText[joined]; + if (!existing) pageLabelText[joined] = pagelabelsName; + + return { + label: getByteLine(`${pageType} | ${modifier} ; ${label}`), + count: getByteLine(`${getHexByte(page.length)} ; ${label}`), + hibytes: getByteLine( + `>${existing ? existing : pagelabelsName} ; ${label}`, + ), + lobytes: getByteLine( + `<${existing ? existing : pagelabelsName} ; ${label}`, + ), + choicesets: existing ? "" : joined, + }; +} + +function typeDigit(label, string, digits, memoryLabel) { + if (digits < 2 || digits > 8 || digits & 1) { + throw new Error(`${string}: digits can only be 2, 4, 6 or 8`); + } + memory = memoryLabel ? memoryLabel : (digits + 1) >> 1; + return getOutputLines(`${label} | ${getHexByte(digits)}`, string, memory); +} + +function typeChoices(label, string, choiceSet, memoryLabel) { + DEBUG && console.log(`Choice set ${string} with options ${choiceSet}`); + stringSet = [...choiceSet].map((c) => cleanWord(c.slice(0, 6))).join(""); + unlabeledStringSets[stringSet] = choiceSet; + return getOutputLines( + `${label} | ${getChoiceSetConstant(stringSet)}`, + string, + memoryLabel ? memoryLabel : 1, + ); +} + +function typeNumber(label, string, limit, memoryLabel) { + return getOutputLines( + `${label} | ${getHexByte(limit)}`, + string, + memoryLabel ? memoryLabel : 1, + ); +} + +function typeBool(label, string, memoryLabel) { + return typeChoices( + "TYPE_CHOICES", + string, + ["off", "on"], + memoryLabel ? memoryLabel : 1, + ); +} +function typeSubMenu(label, string) { + return getOutputLines( + `${label} | SUBMENU_${cleanWord(string).toUpperCase()}`, + `${string}`, + ); +} + +function getMemoryLabel(string, bytes) { + if (isNaN(bytes)) return bytes; // if label is specified use that instead + label = `menuVar${cleanWord(string)}`; + memoryReservations[label] = bytes; + return label; +} + +processPageSet = (pages, name) => { + DEBUG && name && console.log(`submenu ${name}`); + DEBUG && !name && console.log(`main menu`); + if (name) menuEnums.push(`SUBMENU_${cleanWord(name).toUpperCase()}`); + startPageByMenu.push( + `${getByteLine(getHexByte(pageIndex))} ; ${name ? name : "main menu"}`, + ); + // collect submenus to process after all pages + let subPageSets = {}; + Object.entries(pages).forEach(([title, page]) => { + DEBUG && console.log(`${title} with ${page.length} entries`); + pageIndex++; + startItemByPage.push( + getByteLine(`${getHexByte(index)} ; ${cleanWord(title)}`), + ); + pagesOutput.push(getPageLines(title, page, pages, index)); + page.forEach((item) => { + items.push(labelMap[item[0]](...item)); + index++; + if (item[0] === "TYPE_SUBMENU") subPageSets[item[1]] = item[2]; + }); + }); + pageCountByMenu.push( + getByteLine( + `${getHexByte(Object.values(pages).length)} ; ${name ? name : "main menu"}`, + ), + ); + + // process any submenus the same was as the main menu + Object.entries(subPageSets).forEach(([name, pages]) => { + processPageSet(pages, name); + }); +}; +processPageSet(mainMenu); + +items.forEach((i) => { + line = getByteLine( + `${i.memory ? "<" + getMemoryLabel(i.string, i.memory) : "NORAM"} ; ${i.string}`, + ); + memoryMap.push(line); +}); + +memoryBuffer.push("; generated by menu.js"); +memoryBuffer.push("autoMenuVars:"); +Object.entries(memoryReservations).forEach(([label, bytes]) => + memoryBuffer.push(`${label}: .res ${getHexByte(bytes)}`), +); +memoryBuffer.push(""); +// memory into separate file +writeFileSync(__dirname + "/menuram.asm", [...memoryBuffer, ""].join("\n")); + +[ + ["extraSpriteStrings", extraSpriteStrings], + ...Object.entries(unlabeledStringSets), +].forEach(([name, choiceSet], i) => { + if (!i) newStringLines.push("stringTable:"); + if (i == 1) { + newStringLines.push( + '\n.out .sprintf("%d/256 sprite string bytes", * - stringTable)\n', + ); + newStringLines.push("choiceSetTable:"); + } + + DEBUG && console.log(`stringlist`, name, choiceSet); + if (name != "extraSpriteStrings") { + choiceSetEnums.push(getChoiceSetConstant(name)); + choiceSetCounts.push(getByteLine(getHexByte(choiceSet.length))); + choiceSetIndexes.push( + getByteLine(`${getChoiceSetName(name)}-choiceSets`), + ); + choiceSets.push(`${getChoiceSetName(name)}:`); + } + DEBUG && console.log(`choiceSet: `, choiceSet); + choiceSet.forEach((choice) => { + choice = choice.toLowerCase(); + checkStringSanity(choice); + if (!addedStrings.includes(choice)) { + addedStrings.push(choice); + newStringLines.push(`${getStringName(choice)}:`); + newStringLines.push( + getByteLine( + `${getHexByte(choice.length)},${getStringBytes(choice)}`, + ), + ); + } + if (name == "extraSpriteStrings") { + lookupConstants.push( + `${getStringConstant(choice)} = ${getStringName(choice)}-stringTable`, + ); + } else { + choiceSets.push( + // getByteLine(`${getStringName(choice)}-${getChoiceSetName(name)}`), + getByteLine(`${getStringName(choice)}-choiceSetTable`), + ); + } + }); +}); +newStringLines.push( + '\n.out .sprintf("%d/256 choice set bytes", * - choiceSetTable)\n', +); + +buffer.push("; generated by menu.js"); +buffer.push("; will be overwritten unless built with -M"); +buffer.push(""); + +buffer.push(...lookupConstants); +buffer.push(""); + +buffer.push(".enum"); +buffer.push("MAIN_MENU"); +buffer.push(...menuEnums); +buffer.push("MENU_COUNT"); +buffer.push(".endenum"); +buffer.push('\n.out .sprintf("%d/32 menus", MENU_COUNT)\n'); +buffer.push(""); + +buffer.push(".enum"); +buffer.push(...choiceSetEnums); +buffer.push("CHOICESET_COUNT"); +buffer.push(".endenum"); +buffer.push('\n.out .sprintf("%d/32 choicesets", CHOICESET_COUNT)\n'); +buffer.push(""); + +buffer.push("; index activeMenu"); +buffer.push("startPageByMenu:"); +buffer.push(...startPageByMenu); +buffer.push(""); + +buffer.push("pageCountByMenu:"); +buffer.push(...pageCountByMenu); +buffer.push(""); + +buffer.push("; index activePage"); +buffer.push("pageTypes:"); +buffer.push(...pagesOutput.map((p) => p.label)); +buffer.push(""); + +buffer.push("itemCountByPage:"); +buffer.push(...pagesOutput.map((p) => p.count)); +buffer.push(""); + +buffer.push("pageLabelsHi:"); +buffer.push(...pagesOutput.map((p) => p.hibytes)); +buffer.push(""); + +buffer.push("pageLabelsLo:"); +buffer.push(...pagesOutput.map((p) => p.lobytes)); +buffer.push(""); + +buffer.push("startItemByPage:"); +buffer.push(...startItemByPage); +buffer.push(""); + +buffer.push("; index activeItem"); +buffer.push("memoryOffsets:"); +buffer.push(...memoryMap); +buffer.push(""); + +buffer.push("itemTypes:"); +buffer.push(...items.map((i) => i.label)); +buffer.push(""); + +buffer.push("choiceSetIndexes:"); +buffer.push(...choiceSetIndexes); +buffer.push(""); + +buffer.push("choiceSetCounts:"); +buffer.push(...choiceSetCounts); +buffer.push(""); + +buffer.push("choiceSets:"); +buffer.push(...choiceSets); +buffer.push(""); + +buffer.push(...newStringLines); +buffer.push(""); + +buffer.push(...pagesOutput.map((p) => p.choicesets)); +buffer.push(""); + +writeFileSync(__dirname + "/menudata.asm", [...buffer, ""].join("\n")); diff --git a/src/gamemode/gametypemenu/menudata.js b/src/gamemode/gametypemenu/menudata.js new file mode 100644 index 00000000..61f009f4 --- /dev/null +++ b/src/gamemode/gametypemenu/menudata.js @@ -0,0 +1,172 @@ +const seedToggle = ["TYPE_BOOL", "Seed Enabled"]; +const seedInput = ["TYPE_HEX", "seed", 6]; +const linecapWhen = ["TYPE_CHOICES", "linecap when", ["off", "lines", "level"]]; +const linecapHow = [ + "TYPE_CHOICES", + "linecap how", + ["ks*2", "floor", "inviz", "halt"], +]; +const linecapLevel = ["TYPE_NUMBER", "linecap level", 0]; +const linecapLines = ["TYPE_HEX", "linecap lines", 4]; +const dasOnly = ["TYPE_BOOL", "das only"]; + +const scoringModifier = [ + "TYPE_CHOICES", + "scoring", + ["classic", "letters", "7digit", "m", "capped", "hidden"], +]; +const paceModifier = ["TYPE_FF_OFF", "Pace", 16]; +const hzFlag = ["TYPE_BOOL", "HZ DISPLAY"]; +const inputDisplayFlag = ["TYPE_BOOL", "Input Display"]; +const disableFlash = ["TYPE_BOOL", "Disable Flash"]; +const darkMode = [ + "TYPE_CHOICES", + "dark mode", + ["off", "on", "neon", "lite", "teal", "og"], +]; +const paletteSelection = ["TYPE_CHOICES", "palette", ["vanilla", "pride"]]; + +const crashModifier = [ + "TYPE_CHOICES", + "crash", + ["off", "show", "top", "crash"], +]; +const strictCrashFlag = ["TYPE_BOOL", "strict crash"]; +const disablePause = ["TYPE_BOOL", "disable pause"]; +const goofyFlag = ["TYPE_BOOL", "goofy foot"]; +const debugFlag = ["TYPE_BOOL", "block tool"]; +const palFlag = ["TYPE_BOOL", "pal mode"]; +const keyboardFlag = ["TYPE_BOOL", "keyboard"]; +const qualFlag = ["TYPE_BOOL", "qual"]; + +const floorModifier = ["TYPE_NUMBER", "floor", 16]; +const crunchModifier = ["TYPE_NUMBER", "crunch", 16]; +const invisibleFlag = ["TYPE_BOOL", "invisible"]; +const ghostPiece = ["TYPE_BOOL", "ghost"]; +const hardDrop = ["TYPE_BOOL", "hardDrop"]; +const instantClear = ["TYPE_BOOL", "no line clear"]; + +const scrolltris = ["TYPE_BOOL", "scrolltris"]; +const horizMirror = ["TYPE_BOOL", "mirror horiz"]; +const vertMirror = ["TYPE_BOOL", "mirror vert"]; + +const presetModifier = ["TYPE_NUMBER", "setups", 8]; +const typeBModifier = ["TYPE_NUMBER", "type-b height", 9]; +const checkerModifier = ["TYPE_NUMBER", "checker height", 9]; +const quickTapLeftModifier = ["TYPE_NUMBER", "left cols", 20]; +const quickTapRightModifier = ["TYPE_NUMBER", "right cols", 20]; +const transitionModifier = ["TYPE_NUMBER", "transition", 16]; +const marathonModifier = ["TYPE_NUMBER", "marathon", 5]; +const tapqtyModifier = ["TYPE_NUMBER", "qty height", 16]; +const tapqtyLineClear = ["TYPE_BOOL", "lineclear", 16]; +const garbageModifier = ["TYPE_NUMBER", "garbage", 5]; +const droughtModifier = ["TYPE_NUMBER", "drought", 20]; +const lowStackRowModifier = ["TYPE_NUMBER", "lowstack", 20]; + +const anydasDas = ["TYPE_NUMBER", "das", 32]; +const anydasArr = ["TYPE_NUMBER", "arr", 32]; +const anydasEntryDelay = [ + "TYPE_CHOICES", + "entry delay", + ["off", "hydrant", "kitaru"], +]; + +const modsSubMenu = { + "board[mode=default]": [ + floorModifier, + crunchModifier, + invisibleFlag, + ghostPiece, + hardDrop, + instantClear, + ], +}; + +const cursedSubmenu = { + "modifiers[mode=default]": [scrolltris, horizMirror, vertMirror], +}; + +const anydasSubMenu = { + "anydas[mode=default]": [anydasDas, anydasArr, anydasEntryDelay], +}; + +const displaySubMenu = { + "display[mode=default]": [ + scoringModifier, + paceModifier, + hzFlag, + inputDisplayFlag, + disableFlash, + darkMode, + paletteSelection, + ], +}; + +const settingsSubMenu = { + "settings[mode=default]": [ + crashModifier, + strictCrashFlag, + disablePause, + goofyFlag, + debugFlag, + palFlag, + qualFlag, + keyboardFlag, + ], +}; + +const tournamentSubMenu = { + "tournament[mode=default]": [ + seedToggle, + seedInput, + linecapHow, + linecapWhen, + linecapLevel, + linecapLines, + dasOnly, + ], +}; + +const goToTournament = ["TYPE_SUBMENU", "tournament", tournamentSubMenu]; +const goToMods = ["TYPE_SUBMENU", "board", modsSubMenu]; +const goToCursed = ["TYPE_SUBMENU", "cursed", cursedSubmenu]; +const goToSettings = ["TYPE_SUBMENU", "settings", settingsSubMenu]; +const goToDisplay = ["TYPE_SUBMENU", "display", displaySubMenu]; +const goToAnydas = ["TYPE_SUBMENU", "anydas", anydasSubMenu]; + +const optionsSubmenu = { + "options[mode=tetris]": [ + goToTournament, + goToMods, + goToCursed, + goToSettings, + goToDisplay, + goToAnydas, + ], +}; + +const goToOptions = ["TYPE_SUBMENU", "options", optionsSubmenu]; + +const mainMenu = { + "play tetris[mode=tetris]": [goToOptions], + "t-spins[mode=tspins]": [goToOptions], + "setups[mode=presets]": [presetModifier, goToOptions], + "b-type[mode=typeb]": [typeBModifier, goToOptions], + "(quick)tap[mode=tap]": [ + quickTapLeftModifier, + quickTapRightModifier, + goToOptions, + ], + "tap quantity[mode=tapqty]": [tapqtyModifier, tapqtyLineClear, goToOptions], + "transition[mode=transition]": [transitionModifier, goToOptions], + "marathon[mode=marathon]": [marathonModifier, goToOptions], + "drought[mode=drought]": [droughtModifier, goToOptions], + "checkerboard[mode=checkerboard]": [checkerModifier, goToOptions], + "garbage[mode=garbage]": [garbageModifier, goToOptions], + "lowstack[mode=garbage]": [lowStackRowModifier, goToOptions], + "tap/roll speed[mode=speed_test]": [lowStackRowModifier, goToOptions], +}; + +const extraSpriteStrings = ["pause", "block", "clear?", "sure?!", "confetti"]; + +module.exports = { mainMenu, extraSpriteStrings }; diff --git a/src/gamemode/levelmenu.asm b/src/gamemode/levelmenu.asm index 751b4810..7db0af0f 100644 --- a/src/gamemode/levelmenu.asm +++ b/src/gamemode/levelmenu.asm @@ -49,21 +49,21 @@ levelMenuLinecapInfo: sta PPUADDR lda #$F5 sta PPUADDR - clc - lda #LINECAP_WHEN_STRING_OFFSET - adc linecapWhen - sta stringIndexLookup - jsr stringBackground + ; clc + ; lda #LINECAP_WHEN_STRING_OFFSET + ; adc linecapWhen + jsr stringLineCapWhen lda #$21 sta PPUADDR lda #$15 sta PPUADDR - clc - lda #LINECAP_HOW_STRING_OFFSET - adc linecapHow - sta stringIndexLookup - jsr stringBackground + ; clc + ; lda #LINECAP_HOW_STRING_OFFSET + ; adc linecapHow + ; sta stringIndexLookup + ; jsr stringBackground + jsr stringLineCapHow lda #$20 sta PPUADDR @@ -201,7 +201,7 @@ levelControlClearHighScores: sta spriteXOffset lda #$C8 sta spriteYOffset - lda #$C + lda #STRING_CLEAR sta spriteIndexInOamContentLookup jsr stringSprite @@ -222,7 +222,7 @@ levelControlClearHighScoresConfirm: sta spriteXOffset lda #$C8 sta spriteYOffset - lda #$D + lda #STRING_SURE sta spriteIndexInOamContentLookup jsr stringSprite diff --git a/src/gamemodestate/pause.asm b/src/gamemodestate/pause.asm index a3b5c70f..98b88b01 100644 --- a/src/gamemodestate/pause.asm +++ b/src/gamemodestate/pause.asm @@ -56,8 +56,11 @@ pause: @pauseLoopCommon: clc - lda #$A - adc debugFlag + lda #STRING_PAUSE + ldx debugFlag + beq @notDebug + lda #STRING_BLOCK +@notDebug: sta spriteIndexInOamContentLookup jsr stringSprite diff --git a/src/modes/crash.asm b/src/modes/crash.asm index 6bc3d8a9..728de778 100644 --- a/src/modes/crash.asm +++ b/src/modes/crash.asm @@ -221,7 +221,7 @@ testCrash: inc allegroIndex @newBit0: lda nmiReturnAddr - cmp # { + [_, section, line] = line.match(/(\S+)\s+(\S+)/); + line = line.trim().split(/,/); + result = {}; + line.forEach((kv) => (([k, v] = kv.split(/=/)), (result[k] = v))); + return result; +}; + +dbgfile + .filter((l) => l.match(/^seg.*/)) + .forEach((l) => { + kvs = kvsplit(l); + segments[eval(kvs.name)] = { + id: +kvs.id, + name: eval(kvs.name), + start: eval(kvs.start), + size: eval(kvs.size), + romOffset: eval(kvs.ooffs || "0"), + }; + }); + +const listfile = readFileSync("tetris.lst").toString().split(/\n/); +const rom = readFileSync("tetris.nes"); + +segment = segments["ZEROPAGE"]; +listfile.forEach((l, lineNo) => { + if (l.match(/.*\.bss/)) segment = segments.BSS; + if ((m = l.match(/\.segment\s+"(\w+)"/))) segment = segments[m[1]]; + + offset = parseInt(l.slice(0, 6), 16); + if (!isNaN(offset) && lineNo > 3) { + bytecode = l.slice(11, 23); + bytecode = bytecode.replace(/xx/g, " "); + slices = [ + bytecode.slice(0, 2), + bytecode.slice(3, 5), + bytecode.slice(6, 8), + ]; + for (i = 0; i < slices.length; i++) { + if (slices[i] === "rr") { + slices[i] = rom[offset + segment.romOffset + i] + .toString(16) + .padStart(2, "0") + .toUpperCase(); + } + } + address = + bytecode.trim() || l.match(/\.res/) + ? (offset + segment.start) + .toString(16) + .padStart(4, "0") + .toUpperCase() + : " "; + buffer.push( + `${address} ${slices.join(" ").padEnd(2)} ${bytecode.slice(9)} ${l.slice(23)}`.trimEnd(), + ); + } else { + buffer.push(l); + } +}); + +writeFileSync("tetris.txt", [...buffer].join("\n").trimEnd() + "\n");