From 07b12755c65be25ae7913e3520d782e03ee2f5ef Mon Sep 17 00:00:00 2001 From: zohassadar Date: Mon, 5 Jan 2026 11:09:20 +0000 Subject: [PATCH 1/2] menu --- .gitignore | 1 + build.js | 9 + src/gamemode/gametypemenu/linecap.asm | 236 ---- src/gamemode/gametypemenu/menu.asm | 1515 ++++++++++++++---------- src/gamemode/gametypemenu/menu.js | 386 ++++++ src/gamemode/gametypemenu/menudata.asm | 843 +++++++++++++ src/gamemode/gametypemenu/menudata.js | 551 +++++++++ src/gamemode/gametypemenu/menuram.asm | 8 + src/gamemode/levelmenu.asm | 24 +- src/gamemodestate/pause.asm | 7 +- src/modes/crash.asm | 4 +- src/nametables.asm | 2 - src/nametables/game_type_menu.js | 76 +- src/nmi/nmi.asm | 32 +- src/nmi/render.asm | 53 +- src/ram.asm | 71 +- src/sprites/bytesprite.asm | 43 +- src/sprites/loadsprite.asm | 10 + src/util/core.asm | 9 - src/util/strings.asm | 128 +- tools/disasm.js | 69 ++ 21 files changed, 2968 insertions(+), 1109 deletions(-) delete mode 100644 src/gamemode/gametypemenu/linecap.asm create mode 100644 src/gamemode/gametypemenu/menu.js create mode 100644 src/gamemode/gametypemenu/menudata.asm create mode 100644 src/gamemode/gametypemenu/menudata.js create mode 100644 src/gamemode/gametypemenu/menuram.asm create mode 100644 tools/disasm.js 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 aaa24637..f20953a3 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'); @@ -226,3 +233,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/linecap.asm b/src/gamemode/gametypemenu/linecap.asm deleted file mode 100644 index 11982391..00000000 --- a/src/gamemode/gametypemenu/linecap.asm +++ /dev/null @@ -1,236 +0,0 @@ -linecapMenu: - -linecapMenuCursorIndices := 3 - lda #$8 - sta renderMode - jsr updateAudioWaitForNmiAndDisablePpuRendering - jsr disableNmi - - jsr clearNametable - - jsr bulkCopyToPpu - .addr linecapMenuNametable - - lda #RENDER_LINES - sta renderFlags - - lda #$02 - sta soundEffectSlot1Init - - jsr waitForVBlankAndEnableNmi - jsr updateAudioWaitForNmiAndResetOamStaging - jsr updateAudioWaitForNmiAndEnablePpuRendering - jsr updateAudioWaitForNmiAndResetOamStaging - lda #$10 - sta sleepCounter -@menuLoop: - jsr updateAudioWaitForNmiAndResetOamStaging - - jsr linecapMenuRenderSprites - jsr linecapMenuControls - - lda newlyPressedButtons_player1 - and #BUTTON_B - bne @back - beq @menuLoop -@back: - - lda #$02 - sta soundEffectSlot1Init - jmp gameMode_gameTypeMenu - -linecapMenuRenderSprites: - ; when - clc - lda #LINECAP_WHEN_STRING_OFFSET - adc linecapWhen - sta spriteIndexInOamContentLookup - lda #$6F - sta spriteYOffset - lda #$B0 - sta spriteXOffset - jsr stringSpriteAlignRight - - ; how - clc - lda #LINECAP_HOW_STRING_OFFSET - adc linecapHow - sta spriteIndexInOamContentLookup - lda #$8F - sta spriteYOffset - lda #$B0 - sta spriteXOffset - jsr stringSpriteAlignRight - - ldx linecapCursorIndex - lda linecapCursorYOffset, x - sta spriteYOffset - lda #$40 - sta spriteXOffset - lda #$1D - sta spriteIndexInOamContentLookup - jsr loadSpriteIntoOamStaging - rts - -linecapMenuControls: - lda #BUTTON_DOWN - jsr menuThrottle - beq @downEnd - lda #$01 - sta soundEffectSlot1Init - - inc linecapCursorIndex - lda linecapCursorIndex - cmp #linecapMenuCursorIndices - bne @downEnd - lda #0 - sta linecapCursorIndex -@downEnd: - - lda #BUTTON_UP - jsr menuThrottle - beq @upEnd - lda #$01 - sta soundEffectSlot1Init - dec linecapCursorIndex - lda linecapCursorIndex - cmp #$FF - bne @upEnd - lda #linecapMenuCursorIndices-1 - sta linecapCursorIndex -@upEnd: - - jsr linecapMenuControlsLR - rts - -linecapMenuControlsLR: - lda linecapCursorIndex - jsr switch_s_plus_2a - .addr linecapMenuControlsWhen - .addr linecapMenuControlsLinesLevel - .addr linecapMenuControlsHow -linecapMenuControlsWhen: - lda newlyPressedButtons_player1 - and #BUTTON_LEFT|BUTTON_RIGHT - beq @ret - lda #$01 - sta soundEffectSlot1Init - lda #RENDER_LINES - sta renderFlags - lda linecapWhen - eor #1 - sta linecapWhen -@ret: - rts - -linecapMenuControlsLinesLevel: - lda #BUTTON_RIGHT - jsr menuThrottle - beq @notRight - lda linecapWhen - bne linecapMenuControlsAdjLinesUp - lda #1 - jsr linecapMenuControlsAdjLevel -@notRight: - - lda #BUTTON_LEFT - jsr menuThrottle - beq @notLeft - lda linecapWhen - bne linecapMenuControlsAdjLinesDown - lda #$FF - jsr linecapMenuControlsAdjLevel -@notLeft: - rts - -linecapMenuControlsAdjLevel: - sta tmpZ - clc - lda linecapLevel - adc tmpZ - sta linecapLevel - -linecapMenuControlsBoopAndRender: - lda #$01 - sta soundEffectSlot1Init - lda #RENDER_LINES - sta renderFlags - rts - -linecapMenuControlsAdjLinesUp: - clc - lda linecapLines - adc #$10 - cmp #$A0 - beq @overflowLines - sta linecapLines - bne @noverflow -@overflowLines: - lda #0 - sta linecapLines - clc - lda linecapLines+1 - adc #1 - and #$1F - sta linecapLines+1 -@noverflow: - jmp linecapMenuControlsBoopAndRender - -linecapMenuControlsAdjLinesDown: - sec - lda linecapLines - beq @overflowLines - sbc #$10 - sta linecapLines - jmp @noverflow -@overflowLines: - lda #$90 - sta linecapLines - sec - lda linecapLines+1 - sbc #1 - and #$1F - sta linecapLines+1 -@noverflow: - jmp linecapMenuControlsBoopAndRender - - -linecapMenuControlsHow: - lda #BUTTON_RIGHT - jsr menuThrottle - beq @notRight - lda #$01 - sta soundEffectSlot1Init - inc linecapHow - lda linecapHow - cmp #4 - bne @notRight - lda #0 - sta linecapHow -@notRight: - - lda #BUTTON_LEFT - jsr menuThrottle - beq @notLeft - lda #$01 - sta soundEffectSlot1Init - dec linecapHow - lda linecapHow - cmp #$FF - bne @notLeft - lda #3 - sta linecapHow -@notLeft: - rts - -linecapMenuNametable: ; stripe - .byte $21, $0A, 12, 'L','I','N','E','C','A','P',' ','M','E','N','U' - .byte $21, $CA, 4, 'W','H','E','N' - .byte $22, $4A, 3, 'H','O','W' - .byte $21, $2A, $4C, $39 - .byte $FF - -linecapCursorYOffsetOffset := $6F - -linecapCursorYOffset: - .byte 0+linecapCursorYOffsetOffset, 8+linecapCursorYOffsetOffset, 32+linecapCursorYOffsetOffset diff --git a/src/gamemode/gametypemenu/menu.asm b/src/gamemode/gametypemenu/menu.asm index 9ce4695d..6e299f0a 100644 --- a/src/gamemode/gametypemenu/menu.asm +++ b/src/gamemode/gametypemenu/menu.asm @@ -1,613 +1,920 @@ -.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 - lda #$01 - sta soundEffectSlot1Init - inc menuSeedCursorIndex - lda menuSeedCursorIndex - cmp #7 - bne @skipSeedRight - lda #0 - sta menuSeedCursorIndex -@skipSeedRight: - - lda menuSeedCursorIndex - beq @skipSeedControl - - 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 + 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 + + 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..622ce576 --- /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.asm b/src/gamemode/gametypemenu/menudata.asm new file mode 100644 index 00000000..d319a4e5 --- /dev/null +++ b/src/gamemode/gametypemenu/menudata.asm @@ -0,0 +1,843 @@ +; generated by menu.js +; will be overwritten unless built with -M + +STRING_PAUSE = stringPause-stringTable +STRING_BLOCK = stringBlock-stringTable +STRING_CLEAR = stringClear-stringTable +STRING_SURE = stringSure-stringTable +STRING_CONFETTI = stringConfetti-stringTable + +.enum +MAIN_MENU +SUBMENU_BOOLEANS +SUBMENU_NUMBERS +SUBMENU_DIGITS +SUBMENU_32BYTES +SUBMENU_DEBUG +SUBMENU_A +SUBMENU_TOURNAMENT +SUBMENU_DISPLAY +SUBMENU_SETTINGS +MENU_COUNT +.endenum + +.out .sprintf("%d/32 menus", MENU_COUNT) + + +.enum +CHOICESET_TETTSPSEESTAPACSETBTFLOCRUQCKTRNMARTAPCKRGARLOBDASLOWKILINVHRD +CHOICESET_OFFON +CHOICESET_ONOFF +CHOICESET_AB +CHOICESET_OFFONABSHOWTOPCRASHNEONLITETEALOGCLASSILETTER7DIGITMCAPPEDHIDDENLINESLEVELKS2FLOORINVIZHALTTETTSPSEESTAPACSETBTFLOCRUQCKTRNMARTAPCKRGARLOBDASLOWKILINVHRDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +CHOICESET_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +CHOICESET_LINESLEVEL +CHOICESET_KS2FLOORINVIZHALT +CHOICESET_CLASSILETTER7DIGITMCAPPEDHIDDEN +CHOICESET_OFFONNEONLITETEALOG +CHOICESET_OFFSHOWTOPCRASH +CHOICESET_COUNT +.endenum + +.out .sprintf("%d/32 choicesets", CHOICESET_COUNT) + + +; index activeMenu +startPageByMenu: + .byte $00 ; main menu + .byte $03 ; booleans + .byte $05 ; numbers + .byte $07 ; Digits + .byte $0A ; 32 bytes + .byte $0B ; debug + .byte $0C ; a + .byte $0D ; Tournament + .byte $0F ; Display + .byte $11 ; Settings + +pageCountByMenu: + .byte $03 ; main menu + .byte $02 ; booleans + .byte $02 ; numbers + .byte $03 ; Digits + .byte $01 ; 32 bytes + .byte $01 ; debug + .byte $01 ; a + .byte $02 ; Tournament + .byte $02 ; Display + .byte $02 ; Settings + +; index activePage +pageTypes: + .byte PAGE_DEFAULT | MODE_DEFAULT ; (new menu,*)?! + .byte PAGE_DEFAULT | MODE_DEFAULT ; kinda working + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; boolean + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; Numbers + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; BCD + .byte PAGE_DEFAULT | MODE_DEFAULT ; Hex + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; 07** + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; aaaaaaaaaaaaaa + .byte PAGE_DEFAULT | MODE_DEFAULT ; Tournament + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; Display + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + .byte PAGE_DEFAULT | MODE_DEFAULT ; Settings + .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg + +itemCountByPage: + .byte $06 ; (new menu,*)?! + .byte $04 ; kinda working + .byte $05 ; dbg + .byte $04 ; boolean + .byte $05 ; dbg + .byte $07 ; Numbers + .byte $05 ; dbg + .byte $04 ; BCD + .byte $04 ; Hex + .byte $05 ; dbg + .byte $08 ; 07** + .byte $05 ; dbg + .byte $08 ; aaaaaaaaaaaaaa + .byte $08 ; Tournament + .byte $05 ; dbg + .byte $06 ; Display + .byte $05 ; dbg + .byte $06 ; Settings + .byte $05 ; dbg + +pageLabelsHi: + .byte >pageLabelsNewMenu ; (new menu,*)?! + .byte >pageLabelsKindaWorking ; kinda working + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsBoolean ; boolean + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsNumbers ; Numbers + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsBcd ; BCD + .byte >pageLabelsHex ; Hex + .byte >pageLabelsDbg ; dbg + .byte >pageLabels07 ; 07** + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsAaaaaaaaaaaaaa ; aaaaaaaaaaaaaa + .byte >pageLabelsTournament ; Tournament + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsDisplay ; Display + .byte >pageLabelsDbg ; dbg + .byte >pageLabelsSettings ; Settings + .byte >pageLabelsDbg ; dbg + +pageLabelsLo: + .byte (render_mode_static-1) + .byte >(render_mode_menu-1) + .byte >(render_mode_congratulations_screen-1) + .byte >(render_mode_play_and_demo-1) + .byte >(render_mode_pause-1) + .byte >(render_mode_rocket-1) + .byte >(render_mode_speed_test-1) + .byte >(render_mode_level_menu-1) + .byte >(render_mode_linecap_menu-1) + +renderRTSJumpLo: + .byte <(render_mode_static-1) + .byte <(render_mode_menu-1) + .byte <(render_mode_congratulations_screen-1) + .byte <(render_mode_play_and_demo-1) + .byte <(render_mode_pause-1) + .byte <(render_mode_rocket-1) + .byte <(render_mode_speed_test-1) + .byte <(render_mode_level_menu-1) + .byte <(render_mode_linecap_menu-1) + +render: + ldx renderMode + lda renderRTSJumpHi,x + pha + lda renderRTSJumpLo,x + pha + rts + +; render: lda renderMode +; jsr switch_s_plus_2a +; .addr render_mode_static +; .addr render_mode_menu +; .addr render_mode_congratulations_screen +; .addr render_mode_play_and_demo +; .addr render_mode_pause +; .addr render_mode_rocket +; .addr render_mode_speed_test +; .addr render_mode_level_menu +; .addr render_mode_linecap_menu .include "render_mode_level_menu.asm" ; no rts / jmp @@ -23,7 +53,6 @@ render_mode_static: .include "render_mode_pause.asm" .include "render_mode_congratulations_screen.asm" .include "render_mode_rocket.asm" -.include "render_mode_scroll.asm" .include "render_mode_speed_test.asm" .include "render_mode_play_and_demo.asm" diff --git a/src/ram.asm b/src/ram.asm index f021f206..1f960636 100644 --- a/src/ram.asm +++ b/src/ram.asm @@ -30,8 +30,8 @@ lagState: .res 1 ; $0022 for lagged lines & score verticalBlankingInterval: .res 1 ; $0033 set_seed: .res 3 ; $0034 ; rng_seed, rng_seed+1, spawnCount -set_seed_input: .res 3 ; $0037 ; copied to set_seed during gameModeState_initGameState - .res 6 +.res 3 +.res 6 tetriminoX: .res 1 ; $0040 tetriminoY: .res 1 ; $0041 @@ -78,7 +78,56 @@ pztemp := mathRAM+$D byteSpriteAddr: .res 2 byteSpriteTile: .res 1 byteSpriteLen: .res 1 - .res $2A + +; (up to) 32 bytes menu scratch ram. can be reused in any other mode +; can also overlap with mathram +; this is to spread out for easier thinking + +; needs to be the same shape as lr* below +udPointer: .res $2 +udAdjust: .res $1 +udMin: .res $1 +udMax: .res $1 +; needs to be the same shape ud* above +lrPointer: .res $2 +lrAdjust: .res $1 +lrMin: .res $1 +lrMax: .res $1 + +activeItem: .res $1 +MENU_PTR_DISTANCE = lrPointer-udPointer +stringSetPtr: .res $2 +stackPtr: .res $1 + +unpackedPageType: .res $1 +unpackedPageValue: .res $1 +unpackedItemType: .res $1 +unpackedItemValue: .res $1 +digitPtr: .res $2 +originalPage: .res $1 +nybbleTemp: .res $1 +blankCounter: .res $1 +rowCounter: .res $1 + +; probably no value here +APressed: .res $1 +startPressed: .res $1 +startOrAPressed: .res $1 +BPressed: .res $1 +selectPressed: .res $1 + +actualPage: .res $1 +gameStarted: .res $1 +.res $1 + +; lr page ; mem address never changes (activePage) +; lr column ; mem address never changes (activeColumn) +; lr value ; current item is set every time anyway +; ud item ; mem address never changes (activeItem) +; ud value ; mem address never changes (expandedDigit) + + + .res $A spriteXOffset: .res 1 ; $00A0 spriteYOffset: .res 1 ; $00A1 @@ -171,8 +220,7 @@ playfield: .res $c8 ; $0400 .res $38 ; still technically part of playfield .res $100 ; $500 ; 2 player playfield - -practiseType: .res 1 ; $600 +.res 1 spawnDelay: .res 1 ; $601 dasValueDelay: .res 1 ; $602 dasValuePeriod: .res 1 ; $603 @@ -352,4 +400,17 @@ dasOnlyFlag: .res 1 qualFlag: .res 1 palFlag: .res 1 + +set_seed_input: .res 3 ; $0037 ; copied to set_seed during gameModeState_initGameState +practiseType: .res 1 ; $600 +; menu +activeMenu: .res 1 +activePage: .res 1 +activeRow: .res 1 +activeColumn: .res 1 +menuStackPtr: .res 1 +; cursorToggle: .res 1 + +.include "gamemode/gametypemenu/menuram.asm" + ; ... $7FF diff --git a/src/sprites/bytesprite.asm b/src/sprites/bytesprite.asm index c822b809..a951c276 100644 --- a/src/sprites/bytesprite.asm +++ b/src/sprites/bytesprite.asm @@ -1,19 +1,19 @@ byteSprite: -menuXTmp := tmp2 ldy #0 @loop: + ldx oamStagingLength tya asl asl asl asl adc spriteXOffset - sta menuXTmp - - ldx oamStagingLength + sta oamStaging+3,x + adc #$8 + sta oamStaging+7,x lda spriteYOffset - sta oamStaging, x - inx + sta oamStaging+0,x + sta oamStaging+4,x lda (byteSpriteAddr), y and #$F0 lsr a @@ -21,37 +21,18 @@ menuXTmp := tmp2 lsr a lsr a adc byteSpriteTile - sta oamStaging, x - inx + sta oamStaging+1,x lda #$00 - sta oamStaging, x - inx - lda menuXTmp - sta oamStaging, x - inx - - lda spriteYOffset - sta oamStaging, x - inx + sta oamStaging+2,x + sta oamStaging+6,x lda (byteSpriteAddr), y and #$F adc byteSpriteTile - sta oamStaging, x - inx - lda #$00 - sta oamStaging, x - inx - lda menuXTmp - adc #$8 - sta oamStaging, x - inx - - ; increase OAM index - lda #$08 + sta oamStaging+5, x + txa clc - adc oamStagingLength + adc #$08 sta oamStagingLength - iny cpy byteSpriteLen bne @loop diff --git a/src/sprites/loadsprite.asm b/src/sprites/loadsprite.asm index 23e0e427..978dca6e 100644 --- a/src/sprites/loadsprite.asm +++ b/src/sprites/loadsprite.asm @@ -79,6 +79,8 @@ oamContentLookup: .addr spriteReady ; $20 .addr spriteCustomLevelCursor ; $21 .addr spriteIngameHeart ; $22 + .addr spriteMenuPageSelect ; $23 +; .addr spriteMenuPageSelect2 ; $24 ; Sprites are sets of 4 bytes in the OAM format, terminated by FF. byte0=y, byte1=tile, byte2=attrs, byte3=x ; YY AA II XX sprite00LevelSelectCursor: @@ -88,6 +90,14 @@ sprite00LevelSelectCursor: sprite01GameTypeCursor: .byte $00,$27,$00,$00,$00,$27,$40,$3A .byte $FF +spriteMenuPageSelect: + .byte $00,$27,$40,$00 + .byte $00,$27,$00,$D9 + .byte $FF +; spriteMenuPageSelect2: +; .byte $00,$27,$40,$08 +; .byte $00,$27,$40,$D1 +; .byte $FF ; Used as a sort of NOOP for cursors sprite02Blank: .byte $00,$FF,$00,$00,$FF diff --git a/src/util/core.asm b/src/util/core.asm index 9204e1fd..406091a0 100644 --- a/src/util/core.asm +++ b/src/util/core.asm @@ -257,15 +257,6 @@ generateNextPseudorandomNumber: sta oneThirdPRNG rts -; canon is initializeOAM -copyOamStagingToOam: - lda #$00 - sta OAMADDR - lda #$02 - sta OAMDMA - rts - - ; reg a: value; reg x: start page; reg y: end page (inclusive) memset_page: pha diff --git a/src/util/strings.asm b/src/util/strings.asm index f5ab17ee..5148cc79 100644 --- a/src/util/strings.asm +++ b/src/util/strings.asm @@ -1,25 +1,28 @@ +stringLineCapWhen: + ldx linecapWhen + lda choiceSetLineslevel, x + jmp stringBackground +stringLineCapHow: + ldx linecapHow + lda choiceSetKs2floorinvizhalt, x stringBackground: - ldx stringIndexLookup - lda stringLookup, x tax - lda stringLookup, x - sta tmpZ + lda choiceSetTable,x + beq @ret + tay inx - ldy #0 @loop: - lda stringLookup, x + lda choiceSetTable, x sta PPUDATA inx - iny - cpy tmpZ + dey bne @loop +@ret: rts stringSprite: ldx spriteIndexInOamContentLookup - lda stringLookup, x - tax - lda stringLookup, x + lda stringTable, x sta tmpZ inx lda spriteXOffset @@ -28,9 +31,9 @@ stringSprite: stringSpriteAlignRight: ldx spriteIndexInOamContentLookup - lda stringLookup, x +stringSpriteAlignRightA: tax - lda stringLookup, x + lda stringTable, x inx sta tmpZ lda tmpZ @@ -48,7 +51,7 @@ stringSpriteLoop: sec lda spriteYOffset sta oamStaging, y - lda stringLookup, x + lda stringTable, x inx sta oamStaging+1, y lda #$00 @@ -68,100 +71,3 @@ stringSpriteLoop: lda tmpZ bne stringSpriteLoop rts - -stringLookup: - .byte stringClassic-stringLookup - .byte stringLetters-stringLookup - .byte stringSevenDigit-stringLookup - .byte stringFloat-stringLookup - .byte stringScorecap-stringLookup - .byte stringHidden-stringLookup - .byte stringNull-stringLookup ; reserved for future use - .byte stringNull-stringLookup - .byte stringOff-stringLookup ; 8 - .byte stringOn-stringLookup - .byte stringPause-stringLookup - .byte stringDebug-stringLookup - .byte stringClear-stringLookup - .byte stringConfirm-stringLookup - .byte stringV4-stringLookup - .byte stringV5-stringLookup ; F - .byte stringLevel-stringLookup - .byte stringLines-stringLookup - .byte stringKSX2-stringLookup - .byte stringFromBelow-stringLookup - .byte stringInviz-stringLookup - .byte stringHalt-stringLookup - .byte stringShown-stringLookup ;16 - .byte stringTopout-stringLookup - .byte stringCrash-stringLookup - .byte stringConfetti-stringLookup ;19 - .byte stringStrict-stringLookup - .byte stringNeon-stringLookup - .byte stringLite-stringLookup - .byte stringTeal-stringLookup - .byte stringOG-stringLookup -stringClassic: - .byte $7,'C','L','A','S','S','I','C' -stringLetters: - .byte $7,'L','E','T','T','E','R','S' -stringSevenDigit: - .byte $6,'7','D','I','G','I','T' -stringFloat: - .byte $1,'M' -stringScorecap: - .byte $6,'C','A','P','P','E','D' -stringHidden: - .byte $6,'H','I','D','D','E','N' -stringOff: - .byte $3,'O','F','F' -stringOn: - .byte $2,'O','N' -stringPause: - .byte $5,'P','A','U','S','E' -stringDebug: - .byte $5,'B','L','O','C','K' -stringClear: -.if SAVE_HIGHSCORES - .byte $6,'C','L','E','A','R','?' -.endif -stringConfirm: -.if SAVE_HIGHSCORES - .byte $6,'S','U','R','E','?','!' -.endif -stringV4: - .byte $2,'V','4' -stringV5: - .byte $2,'V','5' -stringLines: - .byte $5,'L','I','N','E','S' -stringLevel: - .byte $5,'L','E','V','E','L' -stringKSX2: - .byte $4,'K','S',$69,'2' -stringFromBelow: - .byte $5,'F','L','O','O','R' -stringInviz: - .byte $5,'I','N','V','I','Z' -stringHalt: - .byte $4,'H','A','L','T' -stringNull: - .byte $0 -stringShown: - .byte $4,'S','H','O','W' -stringTopout: - .byte $6,'T','O','P','O','U','T' -stringCrash: - .byte $5,'C','R','A','S','H' -stringConfetti: - .byte $8,'C','O','N','F','E','T','T','I' -stringStrict: - .byte $6,'S','T','R','I','C','T' -stringNeon: - .byte $4,'N','E','O','N' -stringTeal: - .byte $4,'T','E','A','L' -stringLite: - .byte $4,'L','I','T','E' -stringOG: - .byte $2,'O','G' diff --git a/tools/disasm.js b/tools/disasm.js new file mode 100644 index 00000000..de394673 --- /dev/null +++ b/tools/disasm.js @@ -0,0 +1,69 @@ +const { writeFileSync, readFileSync } = require("fs"); + +buffer = []; +segments = {}; + +const dbgfile = readFileSync("tetris.dbg").toString().split(/\n/); + +kvsplit = (line) => { + [_, 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"); From d20ff7cb3860447fef2fc2d0d2d8517c38215d20 Mon Sep 17 00:00:00 2001 From: zohassadar Date: Sat, 24 Jan 2026 21:41:02 +0000 Subject: [PATCH 2/2] update menu --- src/gamemode/gametypemenu/.gitignore | 2 + src/gamemode/gametypemenu/menu.js | 2 +- src/gamemode/gametypemenu/menudata.asm | 843 ------------------------- src/gamemode/gametypemenu/menudata.js | 681 +++++--------------- src/gamemode/gametypemenu/menuram.asm | 8 - src/util/strings.asm | 2 +- 6 files changed, 155 insertions(+), 1383 deletions(-) create mode 100644 src/gamemode/gametypemenu/.gitignore delete mode 100644 src/gamemode/gametypemenu/menudata.asm delete mode 100644 src/gamemode/gametypemenu/menuram.asm 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.js b/src/gamemode/gametypemenu/menu.js index 622ce576..df352a1c 100644 --- a/src/gamemode/gametypemenu/menu.js +++ b/src/gamemode/gametypemenu/menu.js @@ -43,7 +43,7 @@ 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))) { + if ((match = string.match(/[^-\/ a-z0-9_?!*]/i))) { throw new Error(`${string} has invalid char '${match[0]}'`); } } diff --git a/src/gamemode/gametypemenu/menudata.asm b/src/gamemode/gametypemenu/menudata.asm deleted file mode 100644 index d319a4e5..00000000 --- a/src/gamemode/gametypemenu/menudata.asm +++ /dev/null @@ -1,843 +0,0 @@ -; generated by menu.js -; will be overwritten unless built with -M - -STRING_PAUSE = stringPause-stringTable -STRING_BLOCK = stringBlock-stringTable -STRING_CLEAR = stringClear-stringTable -STRING_SURE = stringSure-stringTable -STRING_CONFETTI = stringConfetti-stringTable - -.enum -MAIN_MENU -SUBMENU_BOOLEANS -SUBMENU_NUMBERS -SUBMENU_DIGITS -SUBMENU_32BYTES -SUBMENU_DEBUG -SUBMENU_A -SUBMENU_TOURNAMENT -SUBMENU_DISPLAY -SUBMENU_SETTINGS -MENU_COUNT -.endenum - -.out .sprintf("%d/32 menus", MENU_COUNT) - - -.enum -CHOICESET_TETTSPSEESTAPACSETBTFLOCRUQCKTRNMARTAPCKRGARLOBDASLOWKILINVHRD -CHOICESET_OFFON -CHOICESET_ONOFF -CHOICESET_AB -CHOICESET_OFFONABSHOWTOPCRASHNEONLITETEALOGCLASSILETTER7DIGITMCAPPEDHIDDENLINESLEVELKS2FLOORINVIZHALTTETTSPSEESTAPACSETBTFLOCRUQCKTRNMARTAPCKRGARLOBDASLOWKILINVHRDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -CHOICESET_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -CHOICESET_LINESLEVEL -CHOICESET_KS2FLOORINVIZHALT -CHOICESET_CLASSILETTER7DIGITMCAPPEDHIDDEN -CHOICESET_OFFONNEONLITETEALOG -CHOICESET_OFFSHOWTOPCRASH -CHOICESET_COUNT -.endenum - -.out .sprintf("%d/32 choicesets", CHOICESET_COUNT) - - -; index activeMenu -startPageByMenu: - .byte $00 ; main menu - .byte $03 ; booleans - .byte $05 ; numbers - .byte $07 ; Digits - .byte $0A ; 32 bytes - .byte $0B ; debug - .byte $0C ; a - .byte $0D ; Tournament - .byte $0F ; Display - .byte $11 ; Settings - -pageCountByMenu: - .byte $03 ; main menu - .byte $02 ; booleans - .byte $02 ; numbers - .byte $03 ; Digits - .byte $01 ; 32 bytes - .byte $01 ; debug - .byte $01 ; a - .byte $02 ; Tournament - .byte $02 ; Display - .byte $02 ; Settings - -; index activePage -pageTypes: - .byte PAGE_DEFAULT | MODE_DEFAULT ; (new menu,*)?! - .byte PAGE_DEFAULT | MODE_DEFAULT ; kinda working - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; boolean - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; Numbers - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; BCD - .byte PAGE_DEFAULT | MODE_DEFAULT ; Hex - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; 07** - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; aaaaaaaaaaaaaa - .byte PAGE_DEFAULT | MODE_DEFAULT ; Tournament - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; Display - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - .byte PAGE_DEFAULT | MODE_DEFAULT ; Settings - .byte PAGE_DEFAULT | MODE_DEFAULT ; dbg - -itemCountByPage: - .byte $06 ; (new menu,*)?! - .byte $04 ; kinda working - .byte $05 ; dbg - .byte $04 ; boolean - .byte $05 ; dbg - .byte $07 ; Numbers - .byte $05 ; dbg - .byte $04 ; BCD - .byte $04 ; Hex - .byte $05 ; dbg - .byte $08 ; 07** - .byte $05 ; dbg - .byte $08 ; aaaaaaaaaaaaaa - .byte $08 ; Tournament - .byte $05 ; dbg - .byte $06 ; Display - .byte $05 ; dbg - .byte $06 ; Settings - .byte $05 ; dbg - -pageLabelsHi: - .byte >pageLabelsNewMenu ; (new menu,*)?! - .byte >pageLabelsKindaWorking ; kinda working - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsBoolean ; boolean - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsNumbers ; Numbers - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsBcd ; BCD - .byte >pageLabelsHex ; Hex - .byte >pageLabelsDbg ; dbg - .byte >pageLabels07 ; 07** - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsAaaaaaaaaaaaaa ; aaaaaaaaaaaaaa - .byte >pageLabelsTournament ; Tournament - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsDisplay ; Display - .byte >pageLabelsDbg ; dbg - .byte >pageLabelsSettings ; Settings - .byte >pageLabelsDbg ; dbg - -pageLabelsLo: - .byte